Skip to content

Commit

Permalink
feat(kadena-cli): improve create-transaction to allow selecting keys …
Browse files Browse the repository at this point in the history
…(wip)
  • Loading branch information
barthuijgen committed Jan 23, 2024
1 parent c792c24 commit 1e81d5f
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 99 deletions.
56 changes: 56 additions & 0 deletions packages/tools/kadena-cli/src/keys/utils/keysHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import yaml from 'js-yaml';
import { join } from 'node:path';
import sanitizeFilename from 'sanitize-filename';

import {
KEY_EXT,
KEY_LEGACY_EXT,
PLAIN_KEY_DIR,
WALLET_DIR,
WALLET_EXT,
WALLET_LEGACY_EXT,
} from '../../constants/config.js';
import { services } from '../../services/index.js';
import type { IKeyPair } from './storage.js';
import { getFilesWithExtension } from './storage.js';

export interface IWalletConfig {
Expand Down Expand Up @@ -88,6 +92,54 @@ export async function getWalletContent(
);
}

export type IWalletKey = {
alias: string;
key: string;
index: number;
wallet: IWallet;
} & IKeyPair;

/**
* This method throws if key is not found because we expect getWallet to have been used
* which means the key must exist on the filesystem
* @param wallet result of getWallet
* @param key key as present in wallet.keys array
* @returns key information
*/
export const getWalletKey = async (
wallet: IWallet,
key: string,
): Promise<IWalletKey> => {
const file = await services.filesystem.readFile(
join(WALLET_DIR, wallet.folder, key),
);
const parsed = yaml.load(file ?? '') as {
publicKey?: string;
secretKey?: string;
};

if (parsed.publicKey === undefined) {
throw new Error(
`Public key not found for ${key} in wallet ${wallet.folder}`,
);
}

const index =
Number(
(parsed as { index?: string }).index ??
(key.match(/-([0-9]+)\.key$/)?.[1] as string),
) || 0;
const alias = key.replace('.key', '').split('-').slice(0, 1).join('-');
return {
wallet,
key,
alias,
index,
publicKey: parsed.publicKey,
secretKey: parsed.secretKey,
};
};

/**
* Fetches all key files (non-legacy) from a specified wallet directory.
*
Expand Down Expand Up @@ -180,6 +232,10 @@ export async function getAllWallets(): Promise<string[]> {
return wallets;
}

export async function getAllPlainKeys(): Promise<string[]> {
return await getFilesWithExtension(PLAIN_KEY_DIR, KEY_EXT);
}

/**
* Converts a Uint8Array to a hexadecimal string.
*
Expand Down
158 changes: 142 additions & 16 deletions packages/tools/kadena-cli/src/prompts/tx.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,152 @@
import { input, select } from '@inquirer/prompts';
import chalk from 'chalk';
import type { IWalletKey } from '../keys/utils/keysHelpers.js';
import {
getAllWallets,
getWallet,
getWalletKey,
} from '../keys/utils/keysHelpers.js';
import { defaultTemplates } from '../tx/commands/templates/templates.js';
import type { IPrompt } from '../utils/createOption.js';

export const selectTemplate: IPrompt<string> = async () => {
const defaultTemplateKeys = Object.keys(defaultTemplates);
if (defaultTemplateKeys.length === 1) {
return defaultTemplates[defaultTemplateKeys[0]];

const choices = [
{
value: 'filepath',
name: 'Select file path',
},
...defaultTemplateKeys.map((x) => ({ value: x, name: x })),
];

const result = await select({
message: 'Which template do you want to use:',
choices,
});

if (result === 'filepath') {
const result = await input({
message: 'File path:',
});
return result;
}

return await select({
message: 'What do you wish to do',
choices: defaultTemplateKeys.map((template) => ({
value: template,
name: template,
})),
return result;
};

// aliases in templates need to select aliases for keys and/or accounts
// in account, we need to know what value exactly is expected. like public key, account name, or keyset
// the idea is to expect specific naming for the variables, like "account-from" or "pk-from" or "keyset-from"

const getAllAccounts = async (): Promise<string[]> => {
// Wait for account implementation
return [];
};

const notEmpty = <TValue>(value: TValue | null | undefined): value is TValue =>
value !== null && value !== undefined;

const getAllPublicKeys = async (): Promise<IWalletKey[]> => {
// Wait for account implementation
const walletNames = await getAllWallets();
const wallets = await Promise.all(
walletNames.map((wallet) => getWallet(wallet)),
);
const keys = await Promise.all(
wallets
.filter(notEmpty)
.map((wallet) =>
Promise.all(wallet.keys.map((key) => getWalletKey(wallet, key))),
),
);
return keys.flat();
};

const promptVariableValue = async (key: string): Promise<string> => {
if (key.startsWith('account-')) {
// search for account alias
const accounts = await getAllAccounts();
const choices = [
{
value: '_manual_',
name: 'Enter account manually',
},
...accounts.map((x) => ({ value: x, name: x })),
];
const value = await select({
message: `Select account alias for template value ${key}:`,
choices,
});

if (value === '_manual_') {
return await input({
message: `Manual entry for account for template value ${key}:`,
validate: (value) => {
if (value === '') return `${key} cannot be empty`;
return true;
},
});
}

return value;
}
if (key.startsWith('pk-')) {
const keys = await getAllPublicKeys();

const choices = [
{
value: '_manual_',
name: 'Enter public key manually',
},
...keys.map((key) => ({
value: key.key, // TODO: add wallet to key to prevent duplicate errors
name: `${key.alias} (wallet ${key.wallet.folder})`,
})),
];
const value = await select({
message: `Select public key alias for template value ${key}:`,
choices,
});

if (value === '_manual_') {
return await input({
message: `Manual entry for public key for template value ${key}:`,
validate: (value) => {
if (value === '') return `${key} cannot be empty`;
return true;
},
});
}
const walletKey = keys.find((x) => x.key === value);
if (walletKey === undefined) throw new Error('public key not found');

console.log(
`${chalk.green('>')} Key alias ${walletKey.alias} using public key ${
walletKey.publicKey
}`,
);
return walletKey.publicKey;
}
if (key.startsWith('keyset-')) {
// search for key alias
const alias = await input({
message: `Template value for keyset ${key}:`,
validate: (value) => {
if (value === '') return `${key} cannot be empty`;
return true;
},
});
console.log('keyset alias', alias);
return alias;
}

return await input({
message: `Template value ${key}:`,
validate: (value) => {
if (value === '') return `${key} cannot be empty`;
return true;
},
});
};

Expand All @@ -31,14 +164,7 @@ export const templateVariables: IPrompt<Record<string, string>> = async (
const match = values.find((value) => value.startsWith(`--${variable}=`));
if (match !== undefined) variableValues[variable] = match.split('=')[1];
else {
const promptedValue = await input({
message: `Template value ${variable}:`,
validate: (value) => {
if (value === '') return `${variable} cannot be empty`;
return true;
},
});
variableValues[variable] = promptedValue;
variableValues[variable] = await promptVariableValue(variable);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { IUnsignedCommand } from '@kadena/client';
import { createTransaction as kadenaCreateTransaction } from '@kadena/client';
import { createPactCommandFromStringTemplate } from '@kadena/client-utils/nodejs';

import debug from 'debug';
import { services } from '../../services/index.js';
import type { CommandResult } from '../../utils/command.util.js';
import { assertCommandError } from '../../utils/command.util.js';
Expand Down Expand Up @@ -75,7 +76,7 @@ export const createTransactionCommandNew = createCommandFlexible(
variables: template.templateConfig.variables,
});

console.log('create-transaction:action', {
debug.log('create-transaction:action', {
...template,
...templateVariables,
...outputFile,
Expand Down
11 changes: 5 additions & 6 deletions packages/tools/kadena-cli/src/tx/commands/templates/templates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const sendTemplate = `
const transferTemplate = `
code: |-
(coin.transfer "{{{from-acct}}}" "{{{to-acct}}}" {{amount}})
(coin.transfer "{{{pk-from}}}" "{{{to-acct}}}" {{amount}})
data:
meta:
chainId: "{{chain}}"
Expand All @@ -21,7 +21,6 @@ signers:
type: exec
`;

export const defaultTemplates = { send: sendTemplate } as Record<
string,
string
>;
export const defaultTemplates = {
transfer: transferTemplate,
} as Record<string, string>;
Loading

0 comments on commit 1e81d5f

Please sign in to comment.