Skip to content

Commit 2165daa

Browse files
authored
feat(1232): add erc20 symbol command (#1284)
Signed-off-by: rozekmichal <michal.rozek@blockydevs.com>
1 parent 1c4367b commit 2165daa

File tree

7 files changed

+287
-0
lines changed

7 files changed

+287
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import type { AliasService, CommandHandlerArgs, CoreApi } from '@/core';
2+
import type { ContractErc20CallSymbolOutput } from '@/plugins/contract-erc20/commands/symbol/output';
3+
4+
import { ZodError } from 'zod';
5+
6+
import { makeArgs, makeLogger } from '@/__tests__/mocks/mocks';
7+
import { Status } from '@/core/shared/constants';
8+
import { symbol as erc20SymbolHandler } from '@/plugins/contract-erc20/commands/symbol/handler';
9+
import { ContractErc20CallSymbolInputSchema } from '@/plugins/contract-erc20/commands/symbol/input';
10+
11+
jest.mock('@hashgraph/sdk', () => ({
12+
ContractId: {
13+
fromString: jest.fn(() => ({
14+
toEvmAddress: jest.fn(() => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'),
15+
})),
16+
},
17+
TokenType: {
18+
NonFungibleUnique: 'NonFungibleUnique',
19+
FungibleCommon: 'FungibleCommon',
20+
},
21+
}));
22+
23+
const mockEncodeFunctionData = jest.fn().mockReturnValue('0xencoded');
24+
const mockDecodeFunctionResult = jest
25+
.fn()
26+
.mockReturnValue(['HBAR'] as unknown[]);
27+
28+
jest.mock('@/plugins/contract-erc20/utils/erc20-abi-resolver', () => ({
29+
getAbiErc20Interface: jest.fn(() => ({
30+
encodeFunctionData: mockEncodeFunctionData,
31+
decodeFunctionResult: mockDecodeFunctionResult,
32+
})),
33+
}));
34+
35+
jest.mock('@/core/utils/contract-resolver', () => ({
36+
resolveContractId: jest.fn(() => '0.0.1234'),
37+
}));
38+
39+
describe('contract-erc20 plugin - symbol command (unit)', () => {
40+
let api: CommandHandlerArgs['api'];
41+
let logger: ReturnType<typeof makeLogger>;
42+
43+
beforeEach(() => {
44+
jest.clearAllMocks();
45+
46+
logger = makeLogger();
47+
48+
api = {
49+
network: {
50+
getCurrentNetwork: jest.fn(() => 'testnet'),
51+
},
52+
alias: {} as unknown as AliasService,
53+
mirror: {
54+
postContractCall: jest.fn(),
55+
},
56+
} as unknown as CoreApi;
57+
});
58+
59+
test('calls ERC-20 symbol successfully and returns expected output', async () => {
60+
(api.mirror.postContractCall as jest.Mock).mockResolvedValue({
61+
result: '0xencodedResult',
62+
});
63+
64+
const args = makeArgs(api, logger, {
65+
contract: 'some-alias-or-id',
66+
});
67+
68+
const result = await erc20SymbolHandler(args);
69+
70+
expect(result.status).toBe(Status.Success);
71+
expect(result.outputJson).toBeDefined();
72+
73+
const parsed = JSON.parse(
74+
result.outputJson as string,
75+
) as ContractErc20CallSymbolOutput;
76+
77+
expect(parsed.contractId).toBe('0.0.1234');
78+
expect(parsed.contractSymbol).toBe('HBAR');
79+
expect(parsed.network).toBe('testnet');
80+
81+
expect(mockEncodeFunctionData).toHaveBeenCalledWith('symbol');
82+
expect(api.mirror.postContractCall).toHaveBeenCalledWith({
83+
to: `0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
84+
data: '0xencoded',
85+
});
86+
expect(mockDecodeFunctionResult).toHaveBeenCalledWith(
87+
'symbol',
88+
'0xencodedResult',
89+
);
90+
expect(logger.info).toHaveBeenCalledWith(
91+
'Calling ERC-20 "symbol" function on contract 0.0.1234 (network: testnet)',
92+
);
93+
});
94+
95+
test('returns failure when mirror returns no result', async () => {
96+
(api.mirror.postContractCall as jest.Mock).mockResolvedValue({});
97+
98+
const args = makeArgs(api, logger, {
99+
contract: 'some-alias-or-id',
100+
});
101+
102+
const result = await erc20SymbolHandler(args);
103+
104+
expect(result.status).toBe(Status.Failure);
105+
expect(result.errorMessage).toContain(
106+
'There was a problem with calling contract 0.0.1234 "symbol" function',
107+
);
108+
});
109+
110+
test('returns failure when decodeFunctionResult returns empty array', async () => {
111+
(api.mirror.postContractCall as jest.Mock).mockResolvedValue({
112+
result: '0xencodedResult',
113+
});
114+
mockDecodeFunctionResult.mockReturnValueOnce([]);
115+
116+
const args = makeArgs(api, logger, {
117+
contract: 'some-alias-or-id',
118+
});
119+
120+
const result = await erc20SymbolHandler(args);
121+
122+
expect(result.status).toBe(Status.Failure);
123+
expect(result.errorMessage).toContain(
124+
'There was a problem with decoding contract 0.0.1234 "symbol" function result',
125+
);
126+
});
127+
128+
test('returns failure when postContractCall throws', async () => {
129+
(api.mirror.postContractCall as jest.Mock).mockRejectedValue(
130+
new Error('mirror node error'),
131+
);
132+
133+
const args = makeArgs(api, logger, {
134+
contract: 'some-alias-or-id',
135+
});
136+
137+
const result = await erc20SymbolHandler(args);
138+
139+
expect(result.status).toBe(Status.Failure);
140+
expect(result.errorMessage).toContain(
141+
'Failed to call "symbol" function: mirror node error',
142+
);
143+
});
144+
145+
test('schema validation fails when contract is missing', () => {
146+
expect(() => {
147+
ContractErc20CallSymbolInputSchema.parse({});
148+
}).toThrow(ZodError);
149+
});
150+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Contract ERC20 symbol Command Handler
3+
*/
4+
import type { CommandExecutionResult, CommandHandlerArgs } from '@/core';
5+
import type { ContractErc20CallSymbolOutput } from '@/plugins/contract-erc20/commands/symbol/output';
6+
7+
import { ContractId } from '@hashgraph/sdk';
8+
9+
import { Status } from '@/core/shared/constants';
10+
import { resolveContractId } from '@/core/utils/contract-resolver';
11+
import { formatError } from '@/core/utils/errors';
12+
import { getAbiErc20Interface } from '@/plugins/contract-erc20/utils/erc20-abi-resolver';
13+
14+
import { ContractErc20CallSymbolInputSchema } from './input';
15+
16+
const ERC_20_FUNCTION_NAME = 'symbol';
17+
18+
export async function symbol(
19+
args: CommandHandlerArgs,
20+
): Promise<CommandExecutionResult> {
21+
const { logger, api } = args;
22+
try {
23+
const validArgs = ContractErc20CallSymbolInputSchema.parse(args.args);
24+
const contract = validArgs.contract;
25+
26+
const network = api.network.getCurrentNetwork();
27+
28+
const contractId = resolveContractId(contract, api.alias, network);
29+
logger.info(
30+
`Calling ERC-20 "symbol" function on contract ${contractId} (network: ${network})`,
31+
);
32+
const erc20Interface = getAbiErc20Interface();
33+
const data = erc20Interface.encodeFunctionData(ERC_20_FUNCTION_NAME);
34+
35+
const response = await api.mirror.postContractCall({
36+
to: `0x${ContractId.fromString(contractId).toEvmAddress()}`,
37+
data: data,
38+
});
39+
40+
if (!response || !response.result) {
41+
throw new Error(
42+
`There was a problem with calling contract ${contractId} "symbol" function`,
43+
);
44+
}
45+
const decodedParameter = erc20Interface.decodeFunctionResult(
46+
ERC_20_FUNCTION_NAME,
47+
response.result,
48+
);
49+
if (!decodedParameter || !decodedParameter[0]) {
50+
throw new Error(
51+
`There was a problem with decoding contract ${contractId} "symbol" function result`,
52+
);
53+
}
54+
const contractSymbol = String(decodedParameter[0]);
55+
56+
const outputData: ContractErc20CallSymbolOutput = {
57+
contractId,
58+
contractSymbol,
59+
network,
60+
};
61+
62+
return {
63+
status: Status.Success,
64+
outputJson: JSON.stringify(outputData),
65+
};
66+
} catch (error: unknown) {
67+
return {
68+
status: Status.Failure,
69+
errorMessage: formatError('Failed to call "symbol" function', error),
70+
};
71+
}
72+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Contract ERC20 Symbol Command Exports
3+
* For use by tests and external consumers
4+
*/
5+
export { symbol } from './handler';
6+
export type { ContractErc20CallSymbolOutput } from './output';
7+
export {
8+
CONTRACT_ERC20_CALL_SYMBOL_CREATE_TEMPLATE,
9+
ContractErc20CallSymbolOutputSchema,
10+
} from './output';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { z } from 'zod';
2+
3+
import { EntityReferenceSchema } from '@/core/schemas';
4+
5+
/**
6+
* Input schema for contract erc20 call symbol command
7+
*/
8+
export const ContractErc20CallSymbolInputSchema = z.object({
9+
contract: EntityReferenceSchema.describe('Contract identifier (ID or alias)'),
10+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { z } from 'zod';
2+
3+
import { EntityIdSchema, TokenSymbolSchema } from '@/core/schemas';
4+
import { SupportedNetwork } from '@/core/types/shared.types';
5+
6+
export const ContractErc20CallSymbolOutputSchema = z.object({
7+
contractId: EntityIdSchema,
8+
contractSymbol: TokenSymbolSchema,
9+
network: SupportedNetwork,
10+
});
11+
12+
export type ContractErc20CallSymbolOutput = z.infer<
13+
typeof ContractErc20CallSymbolOutputSchema
14+
>;
15+
16+
export const CONTRACT_ERC20_CALL_SYMBOL_CREATE_TEMPLATE = `
17+
✅ Contract ({{hashscanLink contractId "contract" network}}) function "symbol" called successfully!
18+
Contract symbol: {{contractSymbol}}
19+
`.trim();

src/plugins/contract-erc20/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
* Exports the config plugin manifest
44
*/
55
export { name } from './commands/name';
6+
export { symbol } from './commands/symbol';
67
export { contractErc20PluginManifest } from './manifest';

src/plugins/contract-erc20/manifest.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import {
1010
ContractErc20CallNameOutputSchema,
1111
name,
1212
} from '@/plugins/contract-erc20/commands/name';
13+
import {
14+
CONTRACT_ERC20_CALL_SYMBOL_CREATE_TEMPLATE,
15+
ContractErc20CallSymbolOutputSchema,
16+
symbol,
17+
} from '@/plugins/contract-erc20/commands/symbol';
1318

1419
export const contractErc20PluginManifest: PluginManifest = {
1520
name: 'contract-erc20',
@@ -37,6 +42,26 @@ export const contractErc20PluginManifest: PluginManifest = {
3742
humanTemplate: CONTRACT_ERC20_CALL_NAME_CREATE_TEMPLATE,
3843
},
3944
},
45+
{
46+
name: 'symbol',
47+
summary: 'Call symbol function',
48+
description: 'Command for calling ERC-20 symbol function',
49+
options: [
50+
{
51+
name: 'contract',
52+
short: 'c',
53+
type: OptionType.STRING,
54+
required: true,
55+
description:
56+
'Smart contract ID represented by alias or contract ID. Option required',
57+
},
58+
],
59+
handler: symbol,
60+
output: {
61+
schema: ContractErc20CallSymbolOutputSchema,
62+
humanTemplate: CONTRACT_ERC20_CALL_SYMBOL_CREATE_TEMPLATE,
63+
},
64+
},
4065
],
4166
};
4267

0 commit comments

Comments
 (0)