Skip to content

Commit

Permalink
fix: sign commands from chainweaver (#2457)
Browse files Browse the repository at this point in the history
* fix: sign commands from chainweaver

* fix: eslint warning

* fix: use flexible command schema when reading transaction files

* feat: add chainweaver output option for signing

---------

Co-authored-by: Bart Huijgen <[email protected]>
  • Loading branch information
barthuijgen and barthuijgen committed Aug 29, 2024
1 parent 013a55a commit d8ed195
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-pots-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kadena/kadena-cli': minor
---

Add support to sign transactions from chainweaver
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const options = [
globalOptions.directory({ disableQuestion: true }),
txOptions.txUnsignedTransactionFiles(),

globalOptions.legacy({ isOptional: true, disableQuestion: true }),
txOptions.chainweaverSignatures({ isOptional: true, disableQuestion: true }),

// sign with keypair
globalOptions.keyPairs(),
Expand Down
22 changes: 22 additions & 0 deletions packages/tools/kadena-cli/src/commands/tx/tests/sign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,26 @@ describe('template to legacy live test', () => {
});
expect(result.result.status).toBe('success');
});

it('signs a transaction when it is coming from chainweaver', async () => {
vi.resetAllMocks();
const chainweaverTx = `{"cmd":"{\"signers\":[{\"pubKey\":\"554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\",\"clist\":[{\"name\":\"coin.TRANSFER\",\"args\":[\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\",\"k:ff9b64a61902024870a59775624ca594ab14054c97eb6fae97105b88674b5edd\",1.000000000001]},{\"name\":\"coin.GAS\",\"args\":[]}]},{\"pubKey\":\"ff9b64a61902024870a59775624ca594ab14054c97eb6fae97105b88674b5edd\",\"clist\":[{\"name\":\"coin.TRANSFER\",\"args\":[\"k:ff9b64a61902024870a59775624ca594ab14054c97eb6fae97105b88674b5edd\",\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\",1.0e-12]}]}],\"meta\":{\"creationTime\":1720002679,\"ttl\":19128,\"chainId\":\"0\",\"gasPrice\":1.0e-8,\"gasLimit\":4720,\"sender\":\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\"},\"nonce\":\"chainweaver\",\"networkId\":\"testnet04\",\"payload\":{\"exec\":{\"code\":\"(coin.transfer-create \\\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\\\" \\\"k:ff9b64a61902024870a59775624ca594ab14054c97eb6fae97105b88674b5edd\\\" (read-keyset \\\"ks\\\") (+ 1.0 0.000000000001))\\n(coin.transfer \\\"k:ff9b64a61902024870a59775624ca594ab14054c97eb6fae97105b88674b5edd\\\" \\\"k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94\\\" 0.000000000001)\",\"data\":{\"ks\":{\"keys\":[\"ff9b64a61902024870a59775624ca594ab14054c97eb6fae97105b88674b5edd\"],\"pred\":\"keys-all\"}}}}}","hash":"OgrErbv9r4VhL1-jIhbneTzCsPp8JnkR78UGeiWCxHU","sigs":{"ff9b64a61902024870a59775624ca594ab14054c97eb6fae97105b88674b5edd":null,"554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94":"0144fcb6c550fcb1d5c1e6d712a2168e93f3081d624cdf20aa8983a372046f2eba7121b53f2a9d8837de0908c3dc16b976ad85cdf92c5eb5856c6a814611dd03"}}`;

mockPrompts({
select: {
'Select an action:': 'wallet',
},
verbose: true,
});

const { stderr } = await runCommand(['tx', 'sign'], {
stdin: chainweaverTx,
});

expect(stderr.includes('kadena tx sign --tx-sign-with="wallet"')).toEqual(
true,
);
expect(signWithWallet).toHaveBeenCalled();
expect(signWithKeypair).not.toHaveBeenCalled();
});
});
11 changes: 11 additions & 0 deletions packages/tools/kadena-cli/src/commands/tx/txOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,15 @@ export const txOptions = {
'Gas limit for the local transaction',
),
}),
chainweaverSignatures: createOption({
key: 'legacy' as const,
prompt: ({ legacy }): boolean => {
return legacy === true || legacy === 'true' || false;
},
validation: z.boolean().optional(),
option: new Option(
'-l, --legacy',
'Make the signed output transactions ChainWeaver and kda-tool compatible',
),
}),
};
29 changes: 26 additions & 3 deletions packages/tools/kadena-cli/src/commands/tx/utils/storage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { IPactCommand } from '@kadena/client';
import type { ICommand, IUnsignedCommand } from '@kadena/types';
import path from 'node:path';
import { WORKING_DIRECTORY } from '../../../constants/config.js';
Expand All @@ -10,25 +11,47 @@ export interface ISavedTransaction {
state: string;
}

interface SaveSignedTransactionsOptions {

Check warning on line 14 in packages/tools/kadena-cli/src/commands/tx/utils/storage.ts

View workflow job for this annotation

GitHub Actions / Changelog PR or Release

Interface name `SaveSignedTransactionsOptions` must match the RegExp: /^_?I[A-Z]/u
directory?: string;
chainweaverSignatures?: boolean;
}

/**
* Saves multiple signed transactions
*/
export async function saveSignedTransactions(
commands: (ICommand | IUnsignedCommand)[],
directory?: string,
options?: SaveSignedTransactionsOptions,
): Promise<ISavedTransaction[]> {
const result: ISavedTransaction[] = [];
for (let index = 0; index < commands.length; index++) {
const command = commands[index];
let command = commands[index];

const isPartial = isPartiallySignedTransaction(command);
const state = isPartial ? 'partial' : 'signed';
const fileDir = directory ?? WORKING_DIRECTORY;
const fileDir = options?.directory ?? WORKING_DIRECTORY;
const filePath = path.join(
fileDir,
`transaction-${command.hash.slice(0, 10)}-${state}.json`,
);

if (options?.chainweaverSignatures) {

Check warning on line 38 in packages/tools/kadena-cli/src/commands/tx/utils/storage.ts

View workflow job for this annotation

GitHub Actions / Changelog PR or Release

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly
console.log('chainweaverSignatures', command);

Check warning on line 39 in packages/tools/kadena-cli/src/commands/tx/utils/storage.ts

View workflow job for this annotation

GitHub Actions / Changelog PR or Release

Unexpected console statement
command = {
...command,
sigs: command.sigs.reduce(
(acc, sig, index) => {
const pubKey =
sig?.pubKey ??
(JSON.parse(command.cmd) as IPactCommand).signers[index].pubKey;
acc[pubKey] = sig?.sig ?? null;
return acc;
},
{} as Record<string, string | null>,
) as any,

Check warning on line 51 in packages/tools/kadena-cli/src/commands/tx/utils/storage.ts

View workflow job for this annotation

GitHub Actions / Changelog PR or Release

Unexpected any. Specify a different type
};
}

await services.filesystem.writeFile(
filePath,
JSON.stringify(command, null, 2),
Expand Down
21 changes: 9 additions & 12 deletions packages/tools/kadena-cli/src/commands/tx/utils/txHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
TRANSACTIONS_PATH,
TX_TEMPLATE_FOLDER,
} from '../../../constants/config.js';
import { ICommandSchema } from '../../../prompts/tx.js';
import { IUnsignedCommandSchema } from '../../../prompts/tx.js';
import { services } from '../../../services/index.js';
import { KadenaError } from '../../../services/service-error.js';
import type {
Expand Down Expand Up @@ -119,9 +119,11 @@ export async function getAllTransactions(
const content = await services.filesystem.readFile(filePath);
if (content === null) return null;
const JSONParsedContent = JSON.parse(content);
const parsed = ICommandSchema.safeParse(JSONParsedContent);
const parsed = IUnsignedCommandSchema.safeParse(JSONParsedContent);
if (parsed.success) {
const isSignedTx = isSignedTransaction(JSONParsedContent);
const isSignedTx = isSignedTransaction(
parsed.data as IUnsignedCommand,
);
return {
fileName,
signed: isSignedTx,
Expand All @@ -140,13 +142,6 @@ export async function getAllTransactions(
}
}

export async function getAllTransactionFileNames(
directory: string,
): Promise<string[]> {
const transactionFiles = await getAllTransactions(directory);
return transactionFiles.map((tx) => tx.fileName);
}

/**
* Retrieves all transaction file names from the transaction directory based on the signature status.
* @param {boolean} signed - Whether to retrieve signed or unsigned transactions.
Expand Down Expand Up @@ -310,9 +305,11 @@ export async function getTransactionFromFile(
throw Error(`Failed to read file at path: ${transactionFilePath}`);
}
const transaction = JSON.parse(fileContent);
const parsedTransaction = ICommandSchema.parse(transaction);
const parsedTransaction = IUnsignedCommandSchema.parse(transaction);
if (signed) {
const isSignedTx = isSignedTransaction(transaction);
const isSignedTx = isSignedTransaction(
parsedTransaction as IUnsignedCommand,
);
if (!isSignedTx) {
throw Error(`${transactionFile} is not a signed transaction`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ export const signTransactionWithKeyPairAction = async ({
commands: unsignedTransactions,
keyPairs,
directory,
chainweaverSignatures,
}: {
keyPairs: IWalletKeyPair[];
commands: IUnsignedCommand[];
directory?: string;
chainweaverSignatures?: boolean;
}): Promise<
CommandResult<{ commands: { command: ICommand; path: string }[] }>
> => {
Expand All @@ -41,10 +43,10 @@ export const signTransactionWithKeyPairAction = async ({
unsignedTransactions,
);

const savedTransactions = await saveSignedTransactions(
signedCommands,
const savedTransactions = await saveSignedTransactions(signedCommands, {
directory,
);
chainweaverSignatures,
});

const signingStatus = await assessTransactionSigningStatus(signedCommands);

Expand All @@ -60,6 +62,7 @@ export const signTransactionWithKeyPairAction = async ({
export const signTransactionFileWithKeyPairAction = async (data: {
keyPairs: IWalletKeyPair[];
files: string[];
chainweaverSignatures?: boolean;
}): Promise<
CommandResult<{ commands: { command: ICommand; path: string }[] }>
> => {
Expand All @@ -81,6 +84,7 @@ export async function signWithKeypair(
stdin?: string,
): Promise<void> {
const key = await option.keyPairs();
const { legacy: chainweaverSignatures } = await option.legacy();

const result = await (async () => {
if (stdin !== undefined) {
Expand Down Expand Up @@ -117,6 +121,7 @@ export async function signWithKeypair(
publicKey: x.publicKey,
secretKey: x.secretKey!,
})),
chainweaverSignatures,
});
}
})();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ import {

export const signTransactionsWithWallet = async ({
password,
signed,
unsignedCommands,
skippedCommands,
relevantKeyPairs,
wallet,
chainweaverSignatures,
}: {
password: string;
signed: boolean;
unsignedCommands: IUnsignedCommand[];
skippedCommands: IUnsignedCommand[];
relevantKeyPairs: IWalletKey[];
wallet: IWallet;
chainweaverSignatures?: boolean;
}): Promise<
CommandResult<{ commands: { command: ICommand; path: string }[] }>
> => {
Expand Down Expand Up @@ -66,7 +66,9 @@ export const signTransactionsWithWallet = async ({
transactions.push(signedCommand);
}

const savedTransactions = await saveSignedTransactions(transactions);
const savedTransactions = await saveSignedTransactions(transactions, {
chainweaverSignatures,
});

const signingStatus = await assessTransactionSigningStatus([
...skippedCommands,
Expand All @@ -88,6 +90,8 @@ export async function signWithWallet(
stdin?: string,
): Promise<void> {
const results = await (async () => {
const { legacy: chainweaverSignatures } = await option.legacy();

if (stdin !== undefined) {
const command = await parseTransactionsFromStdin(stdin);
const { walletName, walletNameConfig: walletConfig } =
Expand All @@ -110,11 +114,11 @@ export async function signWithWallet(
});
return await signTransactionsWithWallet({
password: password.passwordFile,
signed: false,
unsignedCommands: [command],
skippedCommands: [],
relevantKeyPairs: walletAndKeys.relevantKeyPairs,
wallet: walletConfig,
chainweaverSignatures,
});
} else {
const { directory } = await option.directory();
Expand Down Expand Up @@ -163,11 +167,11 @@ export async function signWithWallet(

return await signTransactionsWithWallet({
password: password.passwordFile,
signed: false,
unsignedCommands,
skippedCommands,
relevantKeyPairs,
wallet: walletConfig,
chainweaverSignatures,
});
}
})();
Expand Down
40 changes: 34 additions & 6 deletions packages/tools/kadena-cli/src/prompts/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,45 @@ export const SignatureOrUndefinedOrNull = z.union([
z.null(),
]);

const chainWeaverSignatureSchema = z.record(z.string(), z.string().nullable());

const ICommandSignatureSchema = z.array(SignatureOrUndefinedOrNull);

export const ICommandSchema = z.object({
cmd: CommandPayloadStringifiedJSONSchema,
hash: PactTransactionHashSchema,
sigs: z.array(SignatureOrUndefinedOrNull),
sigs: ICommandSignatureSchema,
});

export const IUnsignedCommandSchema = z.object({
cmd: CommandPayloadStringifiedJSONSchema,
hash: PactTransactionHashSchema,
sigs: z.array(SignatureOrUndefinedOrNull),
});
export const IUnsignedCommandSchema = z
.object({
cmd: CommandPayloadStringifiedJSONSchema,
hash: PactTransactionHashSchema,
sigs: ICommandSignatureSchema.or(chainWeaverSignatureSchema),
})
// Transform sings record to array
.transform((value) => {
if (Array.isArray(value.sigs)) {
return value as z.output<typeof ICommandSchema>;
}
const sigs = chainWeaverSignatureSchema.safeParse(value.sigs);
if (sigs.success) {
const cmd = z
.object({ signers: z.array(z.object({ pubKey: z.string() })) })
.safeParse(JSON.parse(value.cmd));
if (cmd.success) {
const keys = cmd.data.signers.map((signer) => signer.pubKey);
const result = {
...value,
sigs: keys.map((key) =>
sigs.data[key] !== null ? { sig: sigs.data[key] } : null,
),
};
return result;
}
}
throw new Error('Invalid signature schema');
});

export const ISignedCommandSchema = z.object({
cmd: CommandPayloadStringifiedJSONSchema,
Expand Down

0 comments on commit d8ed195

Please sign in to comment.