Skip to content

Commit

Permalink
feat(cli): init a turbo cli tool featuring KYVE crypto fund PE-6449
Browse files Browse the repository at this point in the history
  • Loading branch information
fedellen committed Aug 29, 2024
1 parent 97ec203 commit 2eff402
Show file tree
Hide file tree
Showing 4 changed files with 363 additions and 1 deletion.
95 changes: 95 additions & 0 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env node

/**
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// eslint-disable-next-line header/header -- This is a CLI file
import { Command, program } from 'commander';

import { version } from '../version.js';
import {
applyOptions,
configFromOptions,
globalOptions,
optionMap,
privateKeyFromOptions,
tokenFromOptions,
valueFromOptions,
walletOptions,
} from './cliUtils.js';
import { cryptoFund, getBalance } from './commands.js';

applyOptions(
program
.name('turbo')
.version(version)
.description('Turbo CLI')
.helpCommand(true),
globalOptions,
);

applyOptions(
program.command('get-balance').description('Get balance of a Turbo address'),
[optionMap.address, optionMap.token],
).action((address, options) => {
getBalance(address, options.token);
});

applyOptions(
program.command('top-up').description('Top up a Turbo address with Fiat'),
[optionMap.address, optionMap.value, optionMap.token],
).action((options) => {
console.log(
'TODO: fiat top-up',
options.address,
options.token,
options.value,
);
});

applyOptions(
program.command('crypto-fund').description('Top up a wallet with crypto'),
[...walletOptions, optionMap.token, optionMap.value],
).action(async (_commandOptions, command: Command) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any = command.optsWithGlobals();

const token = tokenFromOptions(options);
const value = valueFromOptions(options);

const privateKey = await privateKeyFromOptions(options, token);

const config = configFromOptions(options);

cryptoFund({ privateKey, value, token, config });
});

applyOptions(
program
.command('upload-folder')
.description('Upload a folder to a Turbo address')
.argument('<folderPath>', 'Directory to upload'),
[...walletOptions, optionMap.token],
).action((directory, options) => {
console.log('upload-folder TODO', directory, options);
});

if (
process.argv[1].includes('.bin/turbo') || // Running from global .bin
process.argv[1].includes('cli/cli') // Running from source
) {
program.parse(process.argv);
}

Check warning on line 95 in src/cli/cli.ts

View check run for this annotation

Codecov / codecov/patch

src/cli/cli.ts#L1-L95

Added lines #L1 - L95 were not covered by tests
198 changes: 198 additions & 0 deletions src/cli/cliUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import bs58 from 'bs58';
import { Command } from 'commander';
import { readFileSync } from 'fs';

import {
TokenType,
TurboUnauthenticatedConfiguration,
defaultTurboConfiguration,
developmentTurboConfiguration,
isTokenType,
privateKeyFromKyveMnemonic,
} from '../node/index.js';

interface CommanderOption {
alias: string;
description: string;
default?: string | boolean;
}

export const optionMap = {
token: {
alias: '-t, --token <type>',
description: 'Token type for wallet or action',
default: 'arweave',
},
currency: {
alias: '-c, --currency <currency>',
description: 'Currency type to top up with',
default: 'usd',
},
address: {
alias: '-a, --address <walletAddress>',
description: 'Wallet address to use for action',
},
value: {
alias: '-v, --value <value>',
description: 'Value of fiat currency or crypto token for action',
},
walletFile: {
alias: '-w, --wallet-file <filePath>',
description:
'Wallet file to use with the action. Formats accepted: JWK.json, KYVE or ETH private key as a string, or SOL Secret Key as a Uint8Array',
},
mnemonic: {
alias: '-m, --mnemonic <phrase>',
description: 'Mnemonic to use with the action',
},
privateKey: {
alias: '-p, --private-key <key>',
description: 'Private key to use with the action',
},

gateway: {
alias: '-g, --gateway <url>',
description: 'Set a custom crypto gateway URL',
default: undefined,
},
dev: {
alias: '--dev',
description: 'Enable development endpoints',
default: false,
},
debug: {
// TODO: Implement
alias: '--debug',
description: 'Enable verbose logging',
default: false,
},
quiet: {
// TODO: Implement
alias: '--quiet',
description: 'Disable logging',
default: false,
},
} as const;

export const walletOptions = [
optionMap.walletFile,
optionMap.mnemonic,
optionMap.privateKey,
];

export const globalOptions = [
optionMap.dev,
optionMap.gateway,
optionMap.debug,
optionMap.quiet,
];

export function applyOptions(
command: Command,
options: CommanderOption[],
): Command {
[...options].forEach((option) => {
command.option(option.alias, option.description, option.default);
});
return command;
}

export function tokenFromOptions(options: unknown): TokenType {
const token = (options as { token: string }).token;
if (token === undefined) {
throw new Error('Token type required');
}

if (!isTokenType(token)) {
throw new Error('Invalid token type');
}
return token;
}

export function valueFromOptions(options: unknown): string {
const value = (options as { value: string }).value;
if (value === undefined) {
throw new Error('Value is required. Use --value <value>');
}
return value;
}

export async function privateKeyFromOptions(
{
mnemonic,
privateKey,
walletFile,
}: {
walletFile: string | undefined;
mnemonic: string | undefined;
privateKey: string | undefined;
},
token: TokenType,
): Promise<string> {
if (mnemonic !== undefined) {
if (token === 'kyve') {
return privateKeyFromKyveMnemonic(mnemonic);
} else {
throw new Error(
'mnemonic provided but this token type mnemonic to wallet is not supported',
);
}
} else if (walletFile !== undefined) {
const wallet = JSON.parse(readFileSync(walletFile, 'utf-8'));

return token === 'solana' ? bs58.encode(wallet) : wallet;
} else if (privateKey !== undefined) {
return privateKey;
}

throw new Error('mnemonic or wallet file required');
}

const tokenToDevGatewayMap: Record<TokenType, string> = {
arweave: 'https://arweave.net', // No arweave test net
solana: 'https://api.devnet.solana.com',
ethereum: 'https://ethereum-holesky-rpc.publicnode.com',
kyve: 'https://api.korellia.kyve.network',
};

export function configFromOptions({
gateway,
dev,
token,
}: {
gateway: string | undefined;
dev: boolean | undefined;
token: TokenType;
}): TurboUnauthenticatedConfiguration {
let config: TurboUnauthenticatedConfiguration = {};

if (dev) {
config = developmentTurboConfiguration;
config.gatewayUrl = tokenToDevGatewayMap[token];
} else {
config = defaultTurboConfiguration;
}

// If gateway is provided, override the default or dev gateway
if (gateway !== undefined) {
config.gatewayUrl = gateway;
}

return config;
}

Check warning on line 198 in src/cli/cliUtils.ts

View check run for this annotation

Codecov / codecov/patch

src/cli/cliUtils.ts#L1-L198

Added lines #L1 - L198 were not covered by tests
69 changes: 69 additions & 0 deletions src/cli/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
TokenType,
TurboFactory,
TurboUnauthenticatedConfiguration,
TurboWallet,
isTokenType,
tokenToBaseMap,
} from '../node/index.js';

export async function getBalance(address: string, token: string) {
if (!isTokenType(token)) {
throw new Error('Invalid token type!');
}

const unauthenticatedTurbo = TurboFactory.unauthenticated({
paymentServiceConfig: { token },
});
console.log('unauthenticatedTurbo', unauthenticatedTurbo);
// const balance = await unauthenticatedTurbo.getBalance({
// owner: address,
// });
// TODO: Implement unauthenticated getBalance
console.log('TODO: Get balance for', address);
}

export interface CryptoFundParams {
token: TokenType;
value: string;
privateKey: TurboWallet;
config: TurboUnauthenticatedConfiguration;
}
/** Fund the connected signer with crypto */
export async function cryptoFund({
value,
privateKey,
token,
config,
}: CryptoFundParams) {
const authenticatedTurbo = TurboFactory.authenticated({
...config,
privateKey: privateKey,
token,
});

const result = await authenticatedTurbo.topUpWithTokens({
tokenAmount: tokenToBaseMap[token](value),
});

console.log(
'Sent crypto fund transaction: \n',
JSON.stringify(result, null, 2),
);
}

Check warning on line 69 in src/cli/commands.ts

View check run for this annotation

Codecov / codecov/patch

src/cli/commands.ts#L1-L69

Added lines #L1 - L69 were not covered by tests
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ export type TurboUnauthenticatedConfiguration = {
paymentServiceConfig?: TurboUnauthenticatedPaymentServiceConfiguration;
uploadServiceConfig?: TurboUnauthenticatedUploadServiceConfiguration;
token?: TokenType;
gatewayUrl?: string;
};

export interface TurboLogger {
Expand Down Expand Up @@ -323,7 +324,6 @@ export type TurboAuthenticatedConfiguration =
/** @deprecated -- This parameter was added in release v1.5 for injecting an arweave TokenTool. Instead, the SDK now accepts `tokenTools` and/or `gatewayUrl` directly in the Factory constructor. This type will be removed in a v2 release */
tokenMap?: TokenMap;
tokenTools?: TokenTools;
gatewayUrl?: string;
};

export type TurboUnauthenticatedClientConfiguration = {
Expand Down

0 comments on commit 2eff402

Please sign in to comment.