Skip to content

Commit a1f6cbb

Browse files
committed
feat(1286): Custom fees for fungible tokens
1 parent f435d03 commit a1f6cbb

File tree

13 files changed

+649
-73
lines changed

13 files changed

+649
-73
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "token-fixed-fee-hbar",
3+
"symbol": "FFHBAR",
4+
"decimals": 8,
5+
"supplyType": "finite",
6+
"initialSupply": 1000000,
7+
"maxSupply": 10000000,
8+
"treasuryKey": "<alias or accountId:privateKey>",
9+
"adminKey": "<alias or accountId:privateKey>",
10+
"memo": "Token with fixed fee paid in HBAR (tinybars)",
11+
"customFees": [
12+
{
13+
"type": "fixed",
14+
"amount": 100,
15+
"unitType": "HBAR",
16+
"collectorId": "<accountId>",
17+
"exempt": false
18+
}
19+
]
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "token-fixed-fee-token",
3+
"symbol": "FFTOK",
4+
"decimals": 8,
5+
"supplyType": "finite",
6+
"initialSupply": 1000000,
7+
"maxSupply": 10000000,
8+
"treasuryKey": "<alias or accountId:privateKey>",
9+
"adminKey": "<alias or accountId:privateKey>",
10+
"memo": "Token with fixed fee paid in same token units",
11+
"customFees": [
12+
{
13+
"type": "fixed",
14+
"amount": 50,
15+
"unitType": "TOKEN",
16+
"collectorId": "<accountId>",
17+
"exempt": false
18+
}
19+
]
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "token-fractional-receiver",
3+
"symbol": "FFRECV",
4+
"decimals": 8,
5+
"supplyType": "finite",
6+
"initialSupply": 1000000,
7+
"maxSupply": 10000000,
8+
"treasuryKey": "<alias or accountId:privateKey>",
9+
"adminKey": "<alias or accountId:privateKey>",
10+
"memo": "Token with fractional fee (receiver pays, netOfTransfers=false)",
11+
"customFees": [
12+
{
13+
"type": "fractional",
14+
"numerator": 1,
15+
"denominator": 10,
16+
"min": 10,
17+
"max": 1000,
18+
"netOfTransfers": false,
19+
"collectorId": "<accountId>",
20+
"exempt": false
21+
}
22+
]
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "token-fractional-sender",
3+
"symbol": "FFSEND",
4+
"decimals": 8,
5+
"supplyType": "finite",
6+
"initialSupply": 1000000,
7+
"maxSupply": 10000000,
8+
"treasuryKey": "<alias or accountId:privateKey>",
9+
"adminKey": "<alias or accountId:privateKey>",
10+
"memo": "Token with fractional fee (sender pays, netOfTransfers=true)",
11+
"customFees": [
12+
{
13+
"type": "fractional",
14+
"numerator": 1,
15+
"denominator": 10,
16+
"min": 10,
17+
"max": 1000,
18+
"netOfTransfers": true,
19+
"collectorId": "<accountId>",
20+
"exempt": false
21+
}
22+
]
23+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type { CoreApi } from '@/core/core-api/core-api.interface';
2+
import type { SupportedNetwork } from '@/core/types/shared.types';
3+
import type { CreateFungibleTokenFromFileOutput } from '@/plugins/token/commands/create-ft-from-file';
4+
5+
import '@/core/utils/json-serialize';
6+
7+
import * as fs from 'fs/promises';
8+
import * as path from 'path';
9+
10+
import { STATE_STORAGE_FILE_PATH } from '@/__tests__/test-constants';
11+
import { delay } from '@/__tests__/utils/common-utils';
12+
import { setDefaultOperatorForNetwork } from '@/__tests__/utils/network-and-operator-setup';
13+
import { createCoreApi } from '@/core';
14+
import { Status } from '@/core/shared/constants';
15+
import { createTokenFromFile } from '@/plugins/token';
16+
17+
const TEMP_DIR = path.join(__dirname, 'temp-token-files');
18+
19+
describe('Token Custom Fees Integration Tests', () => {
20+
let coreApi: CoreApi;
21+
let network: SupportedNetwork;
22+
23+
beforeAll(async () => {
24+
coreApi = createCoreApi(STATE_STORAGE_FILE_PATH);
25+
await setDefaultOperatorForNetwork(coreApi);
26+
network = coreApi.network.getCurrentNetwork();
27+
28+
await fs.mkdir(TEMP_DIR, { recursive: true });
29+
});
30+
31+
afterAll(async () => {
32+
await fs.rm(TEMP_DIR, { recursive: true, force: true });
33+
});
34+
35+
it('should create FT with fixed HBAR fee', async () => {
36+
const operatorAccountId = coreApi.network.getOperatorAccountId();
37+
38+
const tokenFile = {
39+
name: `FixedHbarFeeToken-${Date.now()}`,
40+
symbol: 'FHBAR',
41+
decimals: 8,
42+
supplyType: 'infinite',
43+
initialSupply: 1000000,
44+
treasuryKey: operatorAccountId,
45+
adminKey: operatorAccountId,
46+
customFees: [
47+
{
48+
type: 'fixed',
49+
amount: 100,
50+
unitType: 'HBAR',
51+
collectorId: operatorAccountId,
52+
exempt: false,
53+
},
54+
],
55+
memo: 'Token with fixed HBAR fee',
56+
};
57+
58+
const filePath = path.join(TEMP_DIR, 'fixed-hbar-fee.json');
59+
await fs.writeFile(filePath, JSON.stringify(tokenFile, null, 2));
60+
61+
const result = await createTokenFromFile({
62+
args: { file: filePath },
63+
api: coreApi,
64+
state: coreApi.state,
65+
logger: coreApi.logger,
66+
config: coreApi.config,
67+
});
68+
69+
expect(result.status).toBe(Status.Success);
70+
const output: CreateFungibleTokenFromFileOutput = JSON.parse(
71+
result.outputJson!,
72+
);
73+
expect(output.tokenId).toBeDefined();
74+
expect(output.name).toBe(tokenFile.name);
75+
expect(output.network).toBe(network);
76+
77+
await delay(5000);
78+
});
79+
80+
it('should create FT with fractional fee', async () => {
81+
const operatorAccountId = coreApi.network.getOperatorAccountId();
82+
83+
const tokenFile = {
84+
name: `FractionalFeeToken-${Date.now()}`,
85+
symbol: 'FFRAC',
86+
decimals: 8,
87+
supplyType: 'infinite',
88+
initialSupply: 1000000,
89+
treasuryKey: operatorAccountId,
90+
adminKey: operatorAccountId,
91+
customFees: [
92+
{
93+
type: 'fractional',
94+
numerator: 1,
95+
denominator: 10,
96+
min: 10,
97+
max: 1000,
98+
netOfTransfers: true,
99+
collectorId: operatorAccountId,
100+
exempt: false,
101+
},
102+
],
103+
memo: 'Token with fractional fee',
104+
};
105+
106+
const filePath = path.join(TEMP_DIR, 'fractional-fee.json');
107+
await fs.writeFile(filePath, JSON.stringify(tokenFile, null, 2));
108+
109+
const result = await createTokenFromFile({
110+
args: { file: filePath },
111+
api: coreApi,
112+
state: coreApi.state,
113+
logger: coreApi.logger,
114+
config: coreApi.config,
115+
});
116+
117+
expect(result.status).toBe(Status.Success);
118+
const output: CreateFungibleTokenFromFileOutput = JSON.parse(
119+
result.outputJson!,
120+
);
121+
expect(output.tokenId).toBeDefined();
122+
expect(output.name).toBe(tokenFile.name);
123+
expect(output.network).toBe(network);
124+
125+
await delay(5000);
126+
});
127+
128+
it('should create FT with fixed TOKEN fee', async () => {
129+
const operatorAccountId = coreApi.network.getOperatorAccountId();
130+
131+
const tokenFile = {
132+
name: `FixedTokenFeeToken-${Date.now()}`,
133+
symbol: 'FTOK',
134+
decimals: 8,
135+
supplyType: 'infinite',
136+
initialSupply: 1000000,
137+
treasuryKey: operatorAccountId,
138+
adminKey: operatorAccountId,
139+
customFees: [
140+
{
141+
type: 'fixed',
142+
amount: 50,
143+
unitType: 'TOKEN',
144+
collectorId: operatorAccountId,
145+
exempt: false,
146+
},
147+
],
148+
memo: 'Token with fixed TOKEN fee',
149+
};
150+
151+
const filePath = path.join(TEMP_DIR, 'fixed-token-fee.json');
152+
await fs.writeFile(filePath, JSON.stringify(tokenFile, null, 2));
153+
154+
const result = await createTokenFromFile({
155+
args: { file: filePath },
156+
api: coreApi,
157+
state: coreApi.state,
158+
logger: coreApi.logger,
159+
config: coreApi.config,
160+
});
161+
162+
expect(result.status).toBe(Status.Success);
163+
const output: CreateFungibleTokenFromFileOutput = JSON.parse(
164+
result.outputJson!,
165+
);
166+
expect(output.tokenId).toBeDefined();
167+
expect(output.name).toBe(tokenFile.name);
168+
expect(output.network).toBe(network);
169+
170+
await delay(5000);
171+
});
172+
});

src/core/services/token/__tests__/unit/mocks.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ export const createMockTokenAssociateTransaction = () => ({
3434

3535
export const createMockCustomFixedFee = () => ({
3636
setHbarAmount: jest.fn().mockReturnThis(),
37+
setAmount: jest.fn().mockReturnThis(),
38+
setDenominatingTokenToSameToken: jest.fn().mockReturnThis(),
39+
setFeeCollectorAccountId: jest.fn().mockReturnThis(),
40+
setAllCollectorsAreExempt: jest.fn().mockReturnThis(),
41+
});
42+
43+
export const createMockCustomFractionalFee = () => ({
44+
setNumerator: jest.fn().mockReturnThis(),
45+
setDenominator: jest.fn().mockReturnThis(),
46+
setMin: jest.fn().mockReturnThis(),
47+
setMax: jest.fn().mockReturnThis(),
48+
setAssessmentMethod: jest.fn().mockReturnThis(),
3749
setFeeCollectorAccountId: jest.fn().mockReturnThis(),
3850
setAllCollectorsAreExempt: jest.fn().mockReturnThis(),
3951
});

0 commit comments

Comments
 (0)