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/create transaction #1501

Merged
merged 4 commits into from
Jan 23, 2024
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
2 changes: 2 additions & 0 deletions .changeset/blue-pans-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
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 @@ -39,7 +43,7 @@
* @param wallet wallet name without extension
* @returns
*/
export async function getWallet(walletFile: string): Promise<IWallet | null> {

Check warning on line 46 in packages/tools/kadena-cli/src/keys/utils/keysHelpers.ts

View workflow job for this annotation

GitHub Actions / Build & unit test

Usage of "null" is deprecated except when describing legacy APIs; use "undefined" instead
// Determine type of wallet
const walletNameParts = walletFile.split('.');
const isLegacy =
Expand Down Expand Up @@ -80,7 +84,7 @@

export async function getWalletContent(
walletPath: string,
): Promise<string | null> {

Check warning on line 87 in packages/tools/kadena-cli/src/keys/utils/keysHelpers.ts

View workflow job for this annotation

GitHub Actions / Build & unit test

Usage of "null" is deprecated except when describing legacy APIs; use "undefined" instead
const wallet = await getWallet(walletPath);
if (!wallet) return null;
return await services.filesystem.readFile(
Expand All @@ -88,6 +92,54 @@
);
}

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 @@
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
179 changes: 179 additions & 0 deletions packages/tools/kadena-cli/src/prompts/tx.ts
Original file line number Diff line number Diff line change
@@ -1,0 +1,179 @@
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);

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 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 =>

Check warning on line 47 in packages/tools/kadena-cli/src/prompts/tx.ts

View workflow job for this annotation

GitHub Actions / Build & unit test

Usage of "null" is deprecated except when describing legacy APIs; use "undefined" instead
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;
},
});
};

export const templateVariables: IPrompt<Record<string, string>> = async (
args,
) => {
const values = args.values as string[] | undefined;
const variables = args.variables as string[] | undefined;

if (!values || !variables) return {};

const variableValues = {} as Record<string, string>;

for (const variable of variables) {
const match = values.find((value) => value.startsWith(`--${variable}=`));
if (match !== undefined) variableValues[variable] = match.split('=')[1];
else {
variableValues[variable] = await promptVariableValue(variable);
}
}

return variableValues;
};

export const outFilePrompt: IPrompt<string | null> = async (args) => {

Check warning on line 174 in packages/tools/kadena-cli/src/prompts/tx.ts

View workflow job for this annotation

GitHub Actions / Build & unit test

Usage of "null" is deprecated except when describing legacy APIs; use "undefined" instead
const result = await input({
message: 'Where do you want to save the output',
});
return result ? result : null;
};
105 changes: 105 additions & 0 deletions packages/tools/kadena-cli/src/tx/commands/createTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import path from 'path';

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';
import { createCommandFlexible } from '../../utils/createCommandFlexible.js';
import { globalOptions } from '../../utils/globalOptions.js';

export const createTransaction = async (
template: string,
variables: Record<string, string>,
outFilePath: string | null,
): Promise<
CommandResult<{ transaction: IUnsignedCommand; filePath: string }>
> => {
// create transaction
const command = await createPactCommandFromStringTemplate(
template,
variables,
);

const transaction = kadenaCreateTransaction(command);

let filePath = null;
if (outFilePath === null) {
// write transaction to file
const directoryPath = path.join(process.cwd(), './transactions');
await services.filesystem.ensureDirectoryExists(directoryPath);

const files = await services.filesystem.readDir(directoryPath);
let fileNumber = files.length + 1;
while (filePath === null) {
const checkPath = path.join(
directoryPath,
`transaction${fileNumber}.json`,
);
if (!files.includes(checkPath)) {
filePath = checkPath;
break;
}
fileNumber++;
}
} else {
filePath = outFilePath;
}

await services.filesystem.writeFile(
filePath,
JSON.stringify(transaction, null, 2),
);

return { success: true, data: { transaction, filePath } };
};

export const createTransactionCommandNew = createCommandFlexible(
'create-transaction',
'select a template and crete a transaction',
[
globalOptions.selectTemplate(),
globalOptions.templateVariables(),
globalOptions.outFileJson(),
],
async (option, values) => {
const template = await option.template();
const templateVariables = await option.templateVariables({
values,
variables: template.templateConfig.variables,
});
const outputFile = await option.outFile({
values,
variables: template.templateConfig.variables,
});

debug.log('create-transaction:action', {
...template,
...templateVariables,
...outputFile,
});

if (template.templateConfig.template === undefined) {
return console.log('template not found');
}

const result = await createTransaction(
template.templateConfig.template,
templateVariables.templateVariables,
outputFile.outFile,
);
assertCommandError(result);

console.log(result.data.transaction);

console.log(
`transaction saved to: ./${path.relative(
process.cwd(),
result.data.filePath,
)}`,
);
},
);
26 changes: 26 additions & 0 deletions packages/tools/kadena-cli/src/tx/commands/templates/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const transferTemplate = `
code: |-
(coin.transfer "{{{pk-from}}}" "{{{to-acct}}}" {{amount}})
data:
meta:
chainId: "{{chain}}"
sender: "{{{to-acct}}}"
gasLimit: 4600
gasPrice: 0.000001
ttl: 600
networkId: "{{network}}"
signers:
- public: "{{from-key}}"
caps:
- name: "coin.TRANSFER"
args: ["{{{from-acct}}}", "{{{to-acct}}}", {{amount}}]
- public: "{{to-key}}"
caps:
- name: "coin.GAS"
args: []
type: exec
`;

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