Skip to content

Commit 4efcd0f

Browse files
eliasmpwaelesbao
authored andcommitted
feat: add 'contracts query balance' subcommand
1 parent 364154f commit 4efcd0f

File tree

16 files changed

+347
-150
lines changed

16 files changed

+347
-150
lines changed

src/arguments/contract.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,26 @@ import { sanitizeDirName } from '@/utils';
44

55
const ContractArgumentDescription = 'Name of the contract';
66

7+
/**
8+
* Definition of Contract name optional argument
9+
*/
10+
export const definitionContractNameOptional = {
11+
parse: async (val: string): Promise<string> => sanitizeDirName(val),
12+
description: ContractArgumentDescription,
13+
};
14+
15+
/**
16+
* Contract name optional argument
17+
*/
18+
export const contractNameOptional = Args.string(definitionContractNameOptional);
19+
720
/**
821
* Definition of Contract name required argument
922
*/
10-
export const definitionContractNameRequired = Args.string({
23+
export const definitionContractNameRequired = {
24+
...definitionContractNameOptional,
1125
required: true,
12-
parse: async val => sanitizeDirName(val),
13-
description: ContractArgumentDescription,
14-
});
26+
};
1527

1628
/**
1729
* Contract name required argument

src/commands/contracts/execute.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default class ContractsExecute extends BaseCommand<typeof ContractsExecut
6161
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
6262
const fromAccount: AccountWithMnemonic = await accountsDomain.getWithMnemonic(this.flags.from!);
6363

64-
const instantiated = await config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
64+
const instantiated = config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
6565

6666
if (!instantiated) throw new NotFoundError('Instantiated deployment with a contract address');
6767

src/commands/contracts/instantiate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default class ContractsInstantiate extends BaseCommand<typeof ContractsIn
7575
// If code id is not set as flag, try to get it from deployments history
7676
let codeId = this.flags.code;
7777
if (!codeId) {
78-
codeId = (await config.contractsInstance.findStoreDeployment(this.args.contract!, config.chainId))?.wasm.codeId;
78+
codeId = (config.contractsInstance.findStoreDeployment(this.args.contract!, config.chainId))?.wasm.codeId;
7979

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

src/commands/contracts/metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default class ContractsMetadata extends BaseCommand<typeof ContractsMetad
4141
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
4242
const fromAccount: AccountWithMnemonic = await accountsDomain.getWithMnemonic(this.flags.from!);
4343

44-
const instantiated = await config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
44+
const instantiated = config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
4545

4646
if (!instantiated) throw new NotFoundError('Instantiated deployment with a contract address');
4747

src/commands/contracts/migrate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default class ContractsMigrate extends BaseCommand<typeof ContractsMigrat
5555
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
5656
const fromAccount: AccountWithMnemonic = await accountsDomain.getWithMnemonic(this.flags.from!);
5757

58-
const instantiated = await config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
58+
const instantiated = config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
5959

6060
if (!instantiated) throw new NotFoundError('Instantiated deployment with a contract address');
6161

src/commands/contracts/premium.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default class ContractsPremium extends BaseCommand<typeof ContractsPremiu
3939
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
4040
const fromAccount: AccountWithMnemonic = await accountsDomain.getWithMnemonic(this.flags.from!);
4141

42-
const instantiated = await config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
42+
const instantiated = config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
4343

4444
if (!instantiated) throw new NotFoundError('Instantiated deployment with a contract address');
4545

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Args, Flags } from '@oclif/core';
2+
3+
import { BaseCommand } from '@/lib/base';
4+
import { definitionContractNameOptional } from '@/arguments';
5+
import { Config } from '@/domain';
6+
import { showSpinner } from '@/ui';
7+
import { NotFoundError } from '@/exceptions';
8+
9+
import { InstantiateDeployment } from '@/types';
10+
11+
/**
12+
* Command 'contracts query balance'
13+
* Access the bank module to query the balance of contracts
14+
*/
15+
export default class ContractsQuerySmart extends BaseCommand<typeof ContractsQuerySmart> {
16+
static summary = 'Access the bank module to query the balance of contracts';
17+
static args = {
18+
contract: Args.string({ ...definitionContractNameOptional }),
19+
};
20+
21+
static flags = {
22+
all: Flags.boolean({
23+
description: 'Shows the balance of all contracts',
24+
default: false,
25+
}),
26+
};
27+
28+
/**
29+
* Runs the command.
30+
*
31+
* @returns Empty promise
32+
*/
33+
public async run(): Promise<void> {
34+
if (!this.args.contract && !this.flags.all) {
35+
throw new NotFoundError('Contract name or --all flag');
36+
}
37+
38+
// Load config and contract info
39+
const config = await Config.open();
40+
await config.contractsInstance.assertValidWorkspace();
41+
42+
let contractsToQuery: InstantiateDeployment[] = [];
43+
44+
if (this.flags.all) {
45+
contractsToQuery = config.contractsInstance.getAllInstantiateDeployments(config.chainId);
46+
} else {
47+
const instantiated = config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
48+
49+
contractsToQuery = instantiated ? [instantiated] : [];
50+
}
51+
52+
if (contractsToQuery.length === 0) throw new NotFoundError('Instantiated contract with a contract address');
53+
54+
const result = await showSpinner(async () => {
55+
const client = await config.getStargateClient();
56+
57+
return config.contractsInstance.queryAllBalances(client, contractsToQuery);
58+
}, 'Querying contract balances...');
59+
60+
if (this.jsonEnabled()) {
61+
this.logJson({ contracts: result });
62+
} else {
63+
for (const item of result) {
64+
this.log(`${config.contractsInstance.prettyPrintBalances(item)}`);
65+
}
66+
}
67+
}
68+
}

src/commands/contracts/query/smart.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default class ContractsQuerySmart extends BaseCommand<typeof ContractsQue
5555
const accountsDomain = await Accounts.init(this.flags['keyring-backend'] as BackendType, { filesPath: this.flags['keyring-path'] });
5656
const fromAccount: AccountWithMnemonic = await accountsDomain.getWithMnemonic(this.flags.from!);
5757

58-
const instantiated = await config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
58+
const instantiated = config.contractsInstance.findInstantiateDeployment(this.args.contract!, config.chainId);
5959

6060
if (!instantiated) throw new NotFoundError('Instantiated deployment with a contract address');
6161

src/domain/Contracts.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import fs from 'node:fs/promises';
44
import toml from 'toml';
55
import Ajv from 'ajv';
66
import addFormats from 'ajv-formats';
7+
import { Coin, StargateClient } from '@cosmjs/stargate';
78

8-
import { readSubDirectories, getWorkspaceRoot, bold, green, red } from '@/utils';
9-
import { Contract } from '@/types';
9+
import { readSubDirectories, getWorkspaceRoot, bold, green, red, darkGreen, yellow } from '@/utils';
10+
import { AccountBalancesJSON, Contract } from '@/types';
1011
import { DEFAULT, REPOSITORIES } from '@/GlobalConfig';
1112
import { Cargo, Deployments } from '@/domain';
1213
import { ErrorCodes, ExecuteError, InstantiateError, NotFoundError, QueryError } from '@/exceptions';
@@ -320,9 +321,9 @@ export class Contracts {
320321
*
321322
* @param contractName - Name of the contract to search by
322323
* @param chainId - Chain id to search by
323-
* @returns Promise containing an instance of {@link StoreDeployment} or undefined if not found
324+
* @returns An instance of {@link StoreDeployment} or undefined if not found
324325
*/
325-
async findStoreDeployment(contractName: string, chainId: string): Promise<StoreDeployment | undefined> {
326+
findStoreDeployment(contractName: string, chainId: string): StoreDeployment | undefined {
326327
const contract = this.assertGetContractByName(contractName);
327328

328329
return contract.deployments.find(item => {
@@ -339,9 +340,9 @@ export class Contracts {
339340
*
340341
* @param contractName - Name of the contract to search by
341342
* @param chainId - Chain id to search by
342-
* @returns Promise containing an instance of {@link InstantiateDeployment} or undefined if not found
343+
* @returns An instance of {@link InstantiateDeployment} or undefined if not found
343344
*/
344-
async findInstantiateDeployment(contractName: string, chainId: string): Promise<InstantiateDeployment | undefined> {
345+
findInstantiateDeployment(contractName: string, chainId: string): InstantiateDeployment | undefined {
345346
const contract = this.assertGetContractByName(contractName);
346347

347348
return contract.deployments.find(item => {
@@ -354,6 +355,62 @@ export class Contracts {
354355
);
355356
}) as InstantiateDeployment | undefined;
356357
}
358+
359+
/**
360+
* Returns a list of all the last instantiated deployments of all contracts
361+
*
362+
* @param chainId - Chain id to search by
363+
* @returns An array of instances of {@link InstantiateDeployment}
364+
*/
365+
getAllInstantiateDeployments(chainId: string): InstantiateDeployment[] {
366+
const instantiatedDeployments = this._data.map(
367+
contract =>
368+
contract.deployments.find(item => {
369+
const pastDeploy = item as InstantiateDeployment;
370+
371+
return (
372+
pastDeploy.contract.version === contract.version &&
373+
pastDeploy.action === DeploymentAction.INSTANTIATE &&
374+
pastDeploy.chainId === chainId
375+
);
376+
}) as InstantiateDeployment | undefined
377+
);
378+
379+
return instantiatedDeployments.filter(item => item !== undefined) as InstantiateDeployment[];
380+
}
381+
382+
/**
383+
* Query the balance of contracts
384+
*
385+
* @param client - Stargate client to use when querying
386+
* @param instantiateDeployements - An array of instances of {@link InstantiateDeployment} to query
387+
* @returns Promise containing the balances result
388+
*/
389+
async queryAllBalances(client: StargateClient, instantiatedDeployments: InstantiateDeployment[]): Promise<AccountBalancesJSON[]> {
390+
const balances = await Promise.all(instantiatedDeployments.map(item => client.getAllBalances(item.contract.address)));
391+
392+
return instantiatedDeployments.map((item, index) => ({
393+
account: {
394+
name: item.contract.name,
395+
address: item.contract.address,
396+
balances: balances[index] as Coin[],
397+
},
398+
}));
399+
}
400+
401+
/**
402+
* Get a formatted version of a contract balance
403+
*
404+
* @param balance - Contract balance data
405+
* @returns Formatted contract address balance
406+
*/
407+
prettyPrintBalances(balance: AccountBalancesJSON): string {
408+
let result = `Balances for contract ${green(balance.account.name)} (${darkGreen(balance.account.address)})\n\n`;
409+
if (balance.account.balances.length === 0) result += `- ${yellow('Empty balance')}\n`;
410+
for (const item of balance.account.balances) result += `- ${bold(item.amount)}${item.denom}\n`;
411+
412+
return result;
413+
}
357414
}
358415

359416
/**

test/commands/contracts/execute.test.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('contracts execute', () => {
4545
validateSchemaStub = sinon.stub(Contracts.prototype, <any>'assertValidJSONSchema').callsFake(async () => {});
4646
findInstantiateStub = sinon
4747
.stub(Contracts.prototype, 'findInstantiateDeployment')
48-
.callsFake(async () => instantiateDeployment as InstantiateDeployment);
48+
.callsFake(() => instantiateDeployment as InstantiateDeployment);
4949
signingClientStub = sinon
5050
.stub(SigningArchwayClient, 'connectWithSigner')
5151
.callsFake(async () => ({ execute: async () => dummyExecuteTransaction } as any));
@@ -63,27 +63,24 @@ describe('contracts execute', () => {
6363
findInstantiateStub.restore();
6464
signingClientStub.restore();
6565
});
66-
describe('Executes a transaction in a contract', () => {
67-
test
68-
.stdout()
69-
.command(['contracts execute', contractName, '--args={}', `--from=${aliceAccountName}`])
70-
.it('Sets smart contract premium', ctx => {
71-
expect(ctx.stdout).to.contain('Executed contract');
72-
expect(ctx.stdout).to.contain(contractName);
73-
expect(ctx.stdout).to.contain('Transaction:');
74-
expect(ctx.stdout).to.contain(dummyExecuteTransaction.transactionHash);
75-
});
76-
});
7766

78-
describe('Prints json output', () => {
79-
test
80-
.stdout()
81-
.command(['contracts execute', contractName, '--args={}', `--from=${aliceAccountName}`, '--json'])
82-
.it('Sets smart contract premium', ctx => {
83-
expect(ctx.stdout).to.not.contain('Executed contract');
84-
expect(ctx.stdout).to.contain(dummyExecuteTransaction.transactionHash);
85-
expect(ctx.stdout).to.contain(dummyExecuteTransaction.gasWanted);
86-
expect(ctx.stdout).to.contain(dummyExecuteTransaction.gasUsed);
87-
});
88-
});
67+
test
68+
.stdout()
69+
.command(['contracts execute', contractName, '--args={}', `--from=${aliceAccountName}`])
70+
.it('Executes a transaction in a contract', ctx => {
71+
expect(ctx.stdout).to.contain('Executed contract');
72+
expect(ctx.stdout).to.contain(contractName);
73+
expect(ctx.stdout).to.contain('Transaction:');
74+
expect(ctx.stdout).to.contain(dummyExecuteTransaction.transactionHash);
75+
});
76+
77+
test
78+
.stdout()
79+
.command(['contracts execute', contractName, '--args={}', `--from=${aliceAccountName}`, '--json'])
80+
.it('Prints json output', ctx => {
81+
expect(ctx.stdout).to.not.contain('Executed contract');
82+
expect(ctx.stdout).to.contain(dummyExecuteTransaction.transactionHash);
83+
expect(ctx.stdout).to.contain(dummyExecuteTransaction.gasWanted);
84+
expect(ctx.stdout).to.contain(dummyExecuteTransaction.gasUsed);
85+
});
8986
});

0 commit comments

Comments
 (0)