Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export class ContractCompilerServiceImpl implements ContractCompilerService {
}),
) as SolcOutput;

console.dir({ output }, { depth: 3 });

if (!output.contracts?.[params.contractFilename]?.[params.contractName]) {
throw new Error(
`Contract ${params.contractName} not found in compilation output`,
Expand Down
148 changes: 148 additions & 0 deletions src/plugins/contract-erc20/__tests__/unit/decimals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import type { AliasService, CommandHandlerArgs, CoreApi } from '@/core';
import type { ContractErc20CallDecimalsOutput } from '@/plugins/contract-erc20/commands/decimals/output';

import { ZodError } from 'zod';

import { makeArgs, makeLogger } from '@/__tests__/mocks/mocks';
import { Status } from '@/core/shared/constants';
import { decimals as erc20DecimalsHandler } from '@/plugins/contract-erc20/commands/decimals/handler';
import { ContractErc20CallDecimalsInputSchema } from '@/plugins/contract-erc20/commands/decimals/input';

jest.mock('@hashgraph/sdk', () => ({
ContractId: {
fromString: jest.fn(() => ({
toEvmAddress: jest.fn(() => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'),
})),
},
TokenType: {
NonFungibleUnique: 'NonFungibleUnique',
FungibleCommon: 'FungibleCommon',
},
}));

const mockEncodeFunctionData = jest.fn().mockReturnValue('0xencoded');
const mockDecodeFunctionResult = jest.fn().mockReturnValue([18] as unknown[]);

jest.mock('@/plugins/contract-erc20/utils/erc20-abi-resolver', () => ({
getAbiErc20Interface: jest.fn(() => ({
encodeFunctionData: mockEncodeFunctionData,
decodeFunctionResult: mockDecodeFunctionResult,
})),
}));

jest.mock('@/core/utils/contract-resolver', () => ({
resolveContractId: jest.fn(() => '0.0.1234'),
}));

describe('contract-erc20 plugin - decimals command (unit)', () => {
let api: CommandHandlerArgs['api'];
let logger: ReturnType<typeof makeLogger>;

beforeEach(() => {
jest.clearAllMocks();

logger = makeLogger();

api = {
network: {
getCurrentNetwork: jest.fn(() => 'testnet'),
},
alias: {} as unknown as AliasService,
mirror: {
postContractCall: jest.fn(),
},
} as unknown as CoreApi;
});

test('calls ERC-20 decimals successfully and returns expected output', async () => {
(api.mirror.postContractCall as jest.Mock).mockResolvedValue({
result: '0xencodedResult',
});

const args = makeArgs(api, logger, {
contract: 'some-alias-or-id',
});

const result = await erc20DecimalsHandler(args);

expect(result.status).toBe(Status.Success);
expect(result.outputJson).toBeDefined();

const parsed = JSON.parse(
result.outputJson as string,
) as ContractErc20CallDecimalsOutput;

expect(parsed.contractId).toBe('0.0.1234');
expect(parsed.decimals).toBe('18');
expect(parsed.network).toBe('testnet');

expect(mockEncodeFunctionData).toHaveBeenCalledWith('decimals');
expect(api.mirror.postContractCall).toHaveBeenCalledWith({
to: `0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
data: '0xencoded',
});
expect(mockDecodeFunctionResult).toHaveBeenCalledWith(
'decimals',
'0xencodedResult',
);
expect(logger.info).toHaveBeenCalledWith(
'Calling ERC-20 "decimals" function on contract 0.0.1234 (network: testnet)',
);
});

test('returns failure when mirror returns no result', async () => {
(api.mirror.postContractCall as jest.Mock).mockResolvedValue({});

const args = makeArgs(api, logger, {
contract: 'some-alias-or-id',
});

const result = await erc20DecimalsHandler(args);

expect(result.status).toBe(Status.Failure);
expect(result.errorMessage).toContain(
'There was a problem with calling contract 0.0.1234 "decimals" function',
);
});

test('returns failure when decodeFunctionResult returns empty array', async () => {
(api.mirror.postContractCall as jest.Mock).mockResolvedValue({
result: '0xencodedResult',
});
mockDecodeFunctionResult.mockReturnValueOnce([]);

const args = makeArgs(api, logger, {
contract: 'some-alias-or-id',
});

const result = await erc20DecimalsHandler(args);

expect(result.status).toBe(Status.Failure);
expect(result.errorMessage).toContain(
'There was a problem with decoding contract 0.0.1234 "decimals" function result',
);
});

test('returns failure when postContractCall throws', async () => {
(api.mirror.postContractCall as jest.Mock).mockRejectedValue(
new Error('mirror node error'),
);

const args = makeArgs(api, logger, {
contract: 'some-alias-or-id',
});

const result = await erc20DecimalsHandler(args);

expect(result.status).toBe(Status.Failure);
expect(result.errorMessage).toContain(
'Failed to call "decimals" function: mirror node error',
);
});

test('schema validation fails when contract is missing', () => {
expect(() => {
ContractErc20CallDecimalsInputSchema.parse({});
}).toThrow(ZodError);
});
});
72 changes: 72 additions & 0 deletions src/plugins/contract-erc20/commands/decimals/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Contract ERC20 decimals Command Handler
*/
import type { CommandExecutionResult, CommandHandlerArgs } from '@/core';
import type { ContractErc20CallDecimalsOutput } from '@/plugins/contract-erc20/commands/decimals/output';

import { ContractId } from '@hashgraph/sdk';

import { Status } from '@/core/shared/constants';
import { resolveContractId } from '@/core/utils/contract-resolver';
import { formatError } from '@/core/utils/errors';
import { getAbiErc20Interface } from '@/plugins/contract-erc20/utils/erc20-abi-resolver';

import { ContractErc20CallDecimalsInputSchema } from './input';

const ERC_20_FUNCTION_NAME = 'decimals';

export async function decimals(
args: CommandHandlerArgs,
): Promise<CommandExecutionResult> {
const { logger, api } = args;
try {
const validArgs = ContractErc20CallDecimalsInputSchema.parse(args.args);
const contract = validArgs.contract;

const network = api.network.getCurrentNetwork();

const contractId = resolveContractId(contract, api.alias, network);
logger.info(
`Calling ERC-20 "decimals" function on contract ${contractId} (network: ${network})`,
);
const erc20Interface = getAbiErc20Interface();
const data = erc20Interface.encodeFunctionData(ERC_20_FUNCTION_NAME);

const response = await api.mirror.postContractCall({
to: `0x${ContractId.fromString(contractId).toEvmAddress()}`,
data: data,
});

if (!response || !response.result) {
throw new Error(
`There was a problem with calling contract ${contractId} "decimals" function`,
);
}
const decodedParameter = erc20Interface.decodeFunctionResult(
ERC_20_FUNCTION_NAME,
response.result,
);
if (!decodedParameter || !decodedParameter[0]) {
throw new Error(
`There was a problem with decoding contract ${contractId} "decimals" function result`,
);
}
const decimalsValue = String(decodedParameter[0]);

const outputData: ContractErc20CallDecimalsOutput = {
contractId,
decimals: decimalsValue,
network,
};

return {
status: Status.Success,
outputJson: JSON.stringify(outputData),
};
} catch (error: unknown) {
return {
status: Status.Failure,
errorMessage: formatError('Failed to call "decimals" function', error),
};
}
}
6 changes: 6 additions & 0 deletions src/plugins/contract-erc20/commands/decimals/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { decimals } from './handler';
export {

Check failure on line 2 in src/plugins/contract-erc20/commands/decimals/index.ts

View workflow job for this annotation

GitHub Actions / Code Style / Check

Type export ContractErc20CallDecimalsOutput is not a value and should be exported using `export type`
CONTRACT_ERC20_CALL_DECIMALS_CREATE_TEMPLATE,
ContractErc20CallDecimalsOutput,
ContractErc20CallDecimalsOutputSchema,
} from './output';
10 changes: 10 additions & 0 deletions src/plugins/contract-erc20/commands/decimals/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod';

import { EntityReferenceSchema } from '@/core/schemas';

/**
* Input schema for contract erc20 call decimals command
*/
export const ContractErc20CallDecimalsInputSchema = z.object({
contract: EntityReferenceSchema.describe('Contract identifier (ID or alias)'),
});
19 changes: 19 additions & 0 deletions src/plugins/contract-erc20/commands/decimals/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { z } from 'zod';

import { EntityIdSchema } from '@/core/schemas';
import { SupportedNetwork } from '@/core/types/shared.types';

export const ContractErc20CallDecimalsOutputSchema = z.object({
contractId: EntityIdSchema,
decimals: z.string().describe('Number of decimal places'),
network: SupportedNetwork,
});

export type ContractErc20CallDecimalsOutput = z.infer<
typeof ContractErc20CallDecimalsOutputSchema
>;

export const CONTRACT_ERC20_CALL_DECIMALS_CREATE_TEMPLATE = `
✅ Contract ({{hashscanLink contractId "contract" network}}) function "decimals" called successfully!
Decimals: {{decimals}}
`.trim();
25 changes: 25 additions & 0 deletions src/plugins/contract-erc20/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
import type { PluginManifest } from '@/core';

import { OptionType } from '@/core/types/shared.types';
import {
CONTRACT_ERC20_CALL_DECIMALS_CREATE_TEMPLATE,
ContractErc20CallDecimalsOutputSchema,
decimals,
} from '@/plugins/contract-erc20/commands/decimals';
import {
CONTRACT_ERC20_CALL_NAME_CREATE_TEMPLATE,
ContractErc20CallNameOutputSchema,
Expand All @@ -17,6 +22,26 @@ export const contractErc20PluginManifest: PluginManifest = {
displayName: 'Smart Contract ERC20 Plugin',
description: "Plugin designed for calling ERC-20 contract's functions",
commands: [
{
name: 'decimals',
summary: 'Call decimals function',
description: 'Command for calling ERC-20 decimals function',
options: [
{
name: 'contract',
short: 'c',
type: OptionType.STRING,
required: true,
description:
'Smart contract ID represented by alias or contract ID. Option required',
},
],
handler: decimals,
output: {
schema: ContractErc20CallDecimalsOutputSchema,
humanTemplate: CONTRACT_ERC20_CALL_DECIMALS_CREATE_TEMPLATE,
},
},
{
name: 'name',
summary: 'Call name function',
Expand Down
1 change: 1 addition & 0 deletions src/plugins/contract/commands/create/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export async function createContract(
outputJson: JSON.stringify(output),
};
} catch (error) {
console.log({ error });
return {
status: Status.Failure,
errorMessage: formatError('Failed to create contract', error),
Expand Down
Loading