Skip to content

Commit

Permalink
feat: 'contracts instantiate' subcommand
Browse files Browse the repository at this point in the history
Add 'contracts instantiate' sub-command
  • Loading branch information
eliasmpw authored and aelesbao committed Aug 29, 2023
1 parent 3a19192 commit 39e81c3
Show file tree
Hide file tree
Showing 38 changed files with 1,582 additions and 547 deletions.
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@
"/oclif.manifest.json"
],
"dependencies": {
"@archwayhq/arch3-core": "^0.1.0",
"@archwayhq/arch3.js": "^0.3.0",
"@archwayhq/keyring-go": "../keyring-go",
"@cosmjs/cosmwasm-stargate": "^0.28.13",
"@cosmjs/proto-signing": "^0.28.13",
"@cosmjs/stargate": "^0.28.13",
"@cosmjs/cosmwasm-stargate": "^0.30.1",
"@cosmjs/proto-signing": "^0.30.1",
"@cosmjs/stargate": "^0.30.1",
"@cosmjs/tendermint-rpc": "^0.30.1",
"@oclif/core": "^2.8.2",
"@oclif/plugin-help": "^5.2.9",
"@oclif/plugin-plugins": "^2.4.4",
"@webassemblyjs/wasm-opt": "^1.11.5",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"bech32": "^2.0.0",
"bignumber.js": "^9.1.1",
"bip39": "^3.1.0",
Expand Down
11 changes: 8 additions & 3 deletions src/arguments/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import { Args } from '@oclif/core';
const AccountArgumentDescription = 'Name of the key/account OR a valid bech32 address';

/**
* Contract name argument
* Definition of Account required argument
*/
export const accountRequired = Args.string({
export const definitionAccountRequired = {
required: true,
description: AccountArgumentDescription,
});
};

/**
* Account required argument
*/
export const accountRequired = Args.string(definitionAccountRequired);
13 changes: 9 additions & 4 deletions src/arguments/amount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import { Amount } from '@/types/Coin';
const AmountArgumentDescription = 'Token amount';

/**
* Contract name argument
* Definition of Amount required argument
*/
export const amountRequired = Args.custom<Amount>({
export const definitionAmountRequired = {
required: true,
description: AmountArgumentDescription,
parse: async (val: string) => parseAmount(val),
});
parse: async (val: string): Promise<Amount> => parseAmount(val),
};

/**
* Amount required argument
*/
export const amountRequired = Args.custom<Amount>(definitionAmountRequired);
9 changes: 7 additions & 2 deletions src/arguments/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import { sanitizeDirName } from '@/utils/sanitize';
const ContractArgumentDescription = 'Name of the contract';

/**
* Contract name argument
* Definition of Contract name required argument
*/
export const contractNameRequired = Args.string({
export const definitionContractNameRequired = Args.string({
required: true,
parse: async val => sanitizeDirName(val),
description: ContractArgumentDescription,
});

/**
* Contract name required argument
*/
export const contractNameRequired = Args.string(definitionContractNameRequired);
15 changes: 15 additions & 0 deletions src/arguments/stdinInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Args } from '@oclif/core';

/**
* Definition of Contract name required argument
*/
export const definitionStdinInput = {
name: 'piped',
required: false,
hidden: true,
};

/**
* Contract name required argument
*/
export const stdinInput = Args.string(definitionStdinInput);
2 changes: 1 addition & 1 deletion src/commands/accounts/balances/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default class AccountsBalancesGet extends BaseCommand<typeof AccountsBala
const result = await showSpinner(async () => {
const client = await config.getStargateClient();

return accountsDomain.queryBalance(client, this.args.account);
return accountsDomain.queryBalance(client, this.args.account!);
}, 'Querying balance');

if (this.jsonEnabled()) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/accounts/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default class AccountsGet extends BaseCommand<typeof AccountsGet> {
*/
public async run(): Promise<void> {
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
const account = await accountsDomain.get(this.args.account);
const account = await accountsDomain.get(this.args.account!);

if (this.flags.address) {
this.log(account.address);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/accounts/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class AccountsNew extends BaseCommand<typeof AccountsNew> {
*/
public async run(): Promise<void> {
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
const account = await accountsDomain.new(this.args.account, this.flags.mnemonic);
const account = await accountsDomain.new(this.args.account!, this.flags.mnemonic);

this.success(`${darkGreen('Account')} ${green(account.name)} successfully created!`);
this.log(`\nAddress: ${green(account.address)}\n`);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/accounts/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default class AccountsRemove extends BaseCommand<typeof AccountsRemove> {
*/
public async run(): Promise<void> {
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
const accountInfo = await accountsDomain.keystore.assertAccountExists(this.args.account);
const accountInfo = await accountsDomain.keystore.assertAccountExists(this.args.account!);

this.warning(
`${yellow('Attention:')} this will permanently delete the account ${bold.green(accountInfo.name)} (${darkGreen(
Expand Down
16 changes: 9 additions & 7 deletions src/commands/config/chains/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import fs from 'node:fs/promises';
import { BaseCommand } from '@/lib/base';
import { DEFAULT } from '@/config';
import { bold, green, red } from '@/utils/style';
import { CosmosChain } from '@/types/Chain';
import { ChainRegistry } from '@/domain/ChainRegistry';
import { ConsoleError } from '@/types/ConsoleError';
import { ErrorCodes } from '@/exceptions/ErrorCodes';
import { stdinInput } from '@/arguments/stdinInput';

import { CosmosChain } from '@/types/Chain';
import { ConsoleError } from '@/types/ConsoleError';

/**
* Command 'config chains import'
Expand All @@ -21,7 +23,7 @@ export default class ConfigChainsImport extends BaseCommand<typeof ConfigChainsI

static args = {
file: Args.string({ name: 'file', required: false, ignoreStdin: true, description: 'Path to file to be imported' }),
piped: Args.string({ name: 'piped', required: false, hidden: true }),
stdinInput
};

/**
Expand All @@ -30,15 +32,15 @@ export default class ConfigChainsImport extends BaseCommand<typeof ConfigChainsI
* @returns Empty promise
*/
public async run(): Promise<void> {
if (this.args.file && this.args.piped) {
if (this.args.file && this.args.stdinInput) {
throw new OnlyOneImportError();
} else if (!this.args.file && !this.args.piped) {
} else if (!this.args.file && !this.args.stdinInput) {
throw new ImportFileRequiredError();
}

// If it is piped, parse the received content, otherwise try to open file
const chainInfo: CosmosChain = this.args.piped ?
JSON.parse(this.args.piped || '') :
const chainInfo: CosmosChain = this.args.stdinInput ?
JSON.parse(this.args.stdinInput || '') :
JSON.parse(await fs.readFile(this.args.file as string, 'utf-8'));

const chainRegistry = await ChainRegistry.init();
Expand Down
6 changes: 3 additions & 3 deletions src/commands/contracts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ export default class ContractsBuild extends BaseCommand<typeof ContractsBuild> {
if (this.flags.optimize) {
this.log(`Building optimized wasm file for ${this.args.contract}...`);

const resultPath = await config.contractsInstance.optimize(this.args.contract);
const resultPath = await config.contractsInstance.optimize(this.args.contract!);
this.success(`Optimized Wasm binary saved to ${cyan(resultPath)}}}`);
} else {
this.log(`Building the project ${this.args.contract}...`);
const resultPath = await config.contractsInstance.build(this.args.contract);
const resultPath = await config.contractsInstance.build(this.args.contract!);
this.success(`Wasm binary saved to ${cyan(resultPath)}}}`);
}

if (this.flags.schemas) {
await config.contractsInstance.schemas(this.args.contract);
await config.contractsInstance.schemas(this.args.contract!);
this.success('Schemas generated');
}
}
Expand Down
178 changes: 178 additions & 0 deletions src/commands/contracts/instantiate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Args, Flags } from '@oclif/core';
import fs from 'node:fs/promises';
import path from 'node:path';
import { InstantiateResult } from '@cosmjs/cosmwasm-stargate';

import { BaseCommand } from '@/lib/base';
import { definitionContractNameRequired } from '@/arguments/contract';
import { Config } from '@/domain/Config';
import { blue, green, red } from '@/utils/style';
import { buildStdFee } from '@/utils/coin';
import { showSpinner } from '@/ui/Spinner';
import { TransactionFlags } from '@/flags/transaction';
import { Accounts } from '@/domain/Accounts';
import { KeyringFlags } from '@/flags/keyring';
import { definitionAmountOptional } from '@/flags/amount';
import { stdinInput } from '@/arguments/stdinInput';
import { ConsoleError } from '@/types/ConsoleError';
import { ErrorCodes } from '@/exceptions/ErrorCodes';
import { NotFoundError } from '@/exceptions';

import { AccountWithMnemonic, BackendType } from '@/types/Account';
import { DeploymentAction, InstantiateDeployment } from '@/types/Deployment';
import { Amount } from '@/types/Coin';

/**
* Command 'contracts instantiate'
* Instantiates a code stored on-chain with the given arguments.
*/
export default class ContractsInstantiate extends BaseCommand<typeof ContractsInstantiate> {
static summary = 'Instantiates code stored on-chain with the given arguments';
static args = {
contract: Args.string({ ...definitionContractNameRequired, ignoreStdin: true }),
stdinInput,
};

static flags = {
admin: Flags.string({ description: 'Name of an account OR a valid bech32 address used as the contract admin' }),
'no-admin': Flags.boolean({ description: 'Instantiates the contract without an admin', default: false }),
label: Flags.string({ description: 'A human-readable name for this contract, displayed on explorers' }),
code: Flags.integer({ description: 'Code stored' }),
amount: Flags.custom<Amount | undefined>({
...definitionAmountOptional,
description: 'Funds to send to the contract during instantiation',
})(),
args: Flags.string({
required: true,
description: 'JSON string with a valid instantiate schema for the contract',
}),
'args-file': Flags.string({ description: 'Path to a JSON file with a valid instantiate schema for the contract' }),
...KeyringFlags,
...TransactionFlags,
};

/**
* Runs the command.
*
* @returns Empty promise
*/
public async run(): Promise<void> {
// Validate that we only get init args from one source of all 3 possible inputs
if (
(this.flags['args-file'] && this.args.stdinInput) ||
(this.flags['args-file'] && this.flags.args) ||
(this.flags.args && this.args.stdinInput)
) {
throw new OnlyOneInitArgsError();
} else if (!this.flags['args-file'] && !this.args.stdinInput && !this.flags.args) {
throw new NotFoundError('Init args to instantiate the contract');
}

// Load config and contract info
const config = await Config.open();
await config.contractsInstance.assertValidWorkspace();
const contract = config.contractsInstance.assertGetContractByName(this.args.contract!);
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
const fromAccount: AccountWithMnemonic = await accountsDomain.getWithMnemonic(this.flags.from!);

const label = this.flags.label || contract.label;
const admin = this.flags['no-admin'] ?
undefined :
(this.flags.admin ?
(await accountsDomain.accountBaseFromAddress(this.flags.admin)).address :
fromAccount.address);

// If code id is not set as flag, try to get it from deployments history
let codeId = this.flags.code;
if (!codeId) {
codeId = await config.contractsInstance.findCodeId(this.args.contract!, config.chainId);

if (!codeId) throw new NotFoundError("Code id of contract's store deployment");
}

// Log message that we are starting the instantiation
this.log(`Instantiating contract ${blue(contract.name)}`);
this.log(` Chain: ${blue(config.chainId)}`);
this.log(` Code: ${blue(codeId)}`);
this.log(` Label: ${blue(label)}`);
this.log(` Admin: ${blue(admin)}`);
this.log(` Signer: ${blue(fromAccount.name)}\n`);

// Validate init args schema
const initArgs = JSON.parse(this.flags.args || this.args.stdinInput || (await fs.readFile(this.flags['args-file']!, 'utf-8')));
await config.contractsInstance.validateInstantiateSchema(contract.name, initArgs);

let result: InstantiateResult;

await showSpinner(async () => {
try {
const signingClient = await config.getSigningArchwayClient(fromAccount);

result = await signingClient.instantiate(fromAccount.address, codeId!, initArgs, label, buildStdFee(this.flags.fee?.coin), {
funds: this.flags.amount?.coin ? [this.flags.amount.coin] : undefined,
admin,
});
} catch (error: Error | any) {
throw new InstantiateError(error?.message);
}
}, 'Waiting for tx to confirm...');

await config.deploymentsInstance.addDeployment(
{
action: DeploymentAction.INSTANTIATE,
txhash: result!.transactionHash,
contract: {
name: contract.name,
version: contract.version,
address: result!.contractAddress,
admin: admin,
},
wasm: {
codeId,
},
msg: initArgs,
} as InstantiateDeployment,
config.chainId
);

if (this.jsonEnabled()) {
this.logJson(result!);
}

this.success(`${green('Contract')} ${blue(path.basename(label))} ${green('instantiated')}`);
this.log(` Address: ${blue(result!.contractAddress)}`);
this.log(` Transaction: ${await config.prettyPrintTxHash(result!.transactionHash)}`);
}
}

/**
* Error when user tries to input init args from many sources
*/
export class OnlyOneInitArgsError extends ConsoleError {
constructor() {
super(ErrorCodes.ONLY_ONE_INIT_ARGS);
}

/**
* {@inheritDoc ConsoleError.toConsoleString}
*/
toConsoleString(): string {
return `${red('Please specify only one init args input')}`;
}
}

/**
* Error when instantiate contract fails
*/
export class InstantiateError extends ConsoleError {
constructor(public description: string) {
super(ErrorCodes.INSTANTIATE_FAILED);
}

/**
* {@inheritDoc ConsoleError.toConsoleString}
*/
toConsoleString(): string {
return red(`Failed to instantiate contract: ${this.description}`);
}
}
4 changes: 2 additions & 2 deletions src/commands/contracts/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ export default class ContractsStore extends BaseCommand<typeof ContractsStore> {
const config = await Config.open();

await config.contractsInstance.assertValidWorkspace();
const contract = config.contractsInstance.assertGetContractByName(this.args.contract);
const contract = config.contractsInstance.assertGetContractByName(this.args.contract!);

const existingDeployment = await config.contractsInstance.isChecksumAlreadyDeployed(this.args.contract, config.chainId);
const existingDeployment = await config.contractsInstance.isChecksumAlreadyDeployed(this.args.contract!, config.chainId);

if (existingDeployment) {
this.warning(
Expand Down
Loading

0 comments on commit 39e81c3

Please sign in to comment.