Skip to content
Merged
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
4 changes: 4 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
"./typechain-types/index.js": {
"import": "./build/typechain-types/index.js",
"require": "./build/typechain-types/index.js"
},
"./typechain-types/*": {
"import": "./build/typechain-types/*",
"require": "./build/typechain-types/*"
}
},
"scripts": {
Expand Down
285 changes: 285 additions & 0 deletions packages/sdk/__tests__/app/service/TransactionService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
/*
*
* Hedera Stablecoin SDK
*
* Copyright (C) 2023 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import 'reflect-metadata';

// Mock tsyringe and Injectable before any other imports to prevent DI
// circular dependency issues when loading TransactionService.
jest.mock('tsyringe', () => ({
singleton: () => (cls: unknown): unknown => cls,
registry: () => (cls: unknown): unknown => cls,
injectable: () => (cls: unknown): unknown => cls,
inject: () => (): undefined => undefined,
delay: (fn: () => unknown): unknown => fn(),
container: {
register: jest.fn(),
resolve: jest.fn(),
resolveAll: jest.fn(),
},
}));

jest.mock('../../../src/core/Injectable', () => ({
__esModule: true,
default: {
resolve: (): Record<string, unknown> => ({}),
lazyResolve: (): Record<string, unknown> => ({}),
TOKENS: { COMMAND_HANDLER: Symbol(), QUERY_HANDLER: Symbol(), TRANSACTION_HANDLER: 'TransactionHandler' },
register: jest.fn(),
registerCommandHandler: jest.fn(),
registerTransactionHandler: jest.fn(),
resolveTransactionHandler: jest.fn(),
registerTransactionAdapterInstances: (): unknown[] => [],
getQueryHandlers: (): unknown[] => [],
getCommandHandlers: (): unknown[] => [],
isWeb: (): boolean => false,
},
}));

import { ethers } from 'ethers';
import {
CashInFacet__factory,
BurnableFacet__factory,
WipeableFacet__factory,
FreezableFacet__factory,
KYCFacet__factory,
PausableFacet__factory,
RescuableFacet__factory,
HoldManagementFacet__factory,
SupplierAdminFacet__factory,
RoleManagementFacet__factory,
CustomFeesFacet__factory,
HederaTokenManagerFacet__factory,
} from '@hashgraph/stablecoin-npm-contracts';
import TransactionService from '../../../src/app/service/TransactionService';
import Hex from '../../../src/core/Hex';

// Helper: encodes a function call and returns it as a Uint8Array
function encode(
iface: ethers.Interface,
fn: string,
params: unknown[],
): Uint8Array {
const hex = iface.encodeFunctionData(fn, params);
return Hex.toUint8Array(hex.slice(2)); // strip '0x' prefix
}

const DUMMY_ADDRESS = '0x0000000000000000000000000000000000000001';

describe('TransactionService.decodeFunctionCall', () => {
describe('HederaTokenManagerFacet — regression', () => {
it('decodes an existing function from HederaTokenManagerFacet', () => {
const iface = new ethers.Interface(
HederaTokenManagerFacet__factory.abi,
);
const fragment = iface.fragments.find(
(f) => f.type === 'function',
);
if (!fragment || fragment.type !== 'function') return;
const fn = fragment as ethers.FunctionFragment;
const params = fn.inputs.map((input) =>
input.type === 'bytes32'
? ethers.ZeroHash
: input.type === 'uint256'
? BigInt(0)
: DUMMY_ADDRESS,
);
const bytes = encode(iface, fn.name, params);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe(fn.name);
});
});

describe('CashInFacet', () => {
it('decodes mint(address, uint256)', () => {
const iface = new ethers.Interface(CashInFacet__factory.abi);
const bytes = encode(iface, 'mint', [DUMMY_ADDRESS, BigInt(1000)]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('mint');
});
});

describe('BurnableFacet', () => {
it('decodes burn(uint256)', () => {
const iface = new ethers.Interface(BurnableFacet__factory.abi);
const bytes = encode(iface, 'burn', [BigInt(500)]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('burn');
});
});

describe('WipeableFacet', () => {
it('decodes wipe(address, uint256)', () => {
const iface = new ethers.Interface(WipeableFacet__factory.abi);
const bytes = encode(iface, 'wipe', [DUMMY_ADDRESS, BigInt(200)]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('wipe');
});
});

describe('FreezableFacet', () => {
it('decodes freeze(address)', () => {
const iface = new ethers.Interface(FreezableFacet__factory.abi);
const bytes = encode(iface, 'freeze', [DUMMY_ADDRESS]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('freeze');
});

it('decodes unfreeze(address)', () => {
const iface = new ethers.Interface(FreezableFacet__factory.abi);
const bytes = encode(iface, 'unfreeze', [DUMMY_ADDRESS]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('unfreeze');
});
});

describe('KYCFacet', () => {
it('decodes grantKyc(address)', () => {
const iface = new ethers.Interface(KYCFacet__factory.abi);
const bytes = encode(iface, 'grantKyc', [DUMMY_ADDRESS]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('grantKyc');
});

it('decodes revokeKyc(address)', () => {
const iface = new ethers.Interface(KYCFacet__factory.abi);
const bytes = encode(iface, 'revokeKyc', [DUMMY_ADDRESS]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('revokeKyc');
});
});

describe('PausableFacet', () => {
it('decodes pause()', () => {
const iface = new ethers.Interface(PausableFacet__factory.abi);
const bytes = encode(iface, 'pause', []);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('pause');
});

it('decodes unpause()', () => {
const iface = new ethers.Interface(PausableFacet__factory.abi);
const bytes = encode(iface, 'unpause', []);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('unpause');
});
});

describe('RescuableFacet', () => {
it('decodes rescue(int64)', () => {
const iface = new ethers.Interface(RescuableFacet__factory.abi);
const bytes = encode(iface, 'rescue', [BigInt(100)]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('rescue');
});
});

describe('HoldManagementFacet', () => {
it('decodes createHold(tuple)', () => {
const iface = new ethers.Interface(HoldManagementFacet__factory.abi);
const hold = {
amount: BigInt(100),
expirationTimestamp: BigInt(9999999999),
escrow: DUMMY_ADDRESS,
to: DUMMY_ADDRESS,
data: '0x',
};
const bytes = encode(iface, 'createHold', [hold]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('createHold');
});

it('decodes executeHold(_holdIdentifier, _to, _amount)', () => {
const iface = new ethers.Interface(HoldManagementFacet__factory.abi);
const bytes = encode(iface, 'executeHold', [
{ tokenHolder: DUMMY_ADDRESS, holdId: BigInt(1) },
DUMMY_ADDRESS,
BigInt(100),
]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('executeHold');
});
});

describe('SupplierAdminFacet', () => {
it('decodes the first public function in the ABI', () => {
const iface = new ethers.Interface(SupplierAdminFacet__factory.abi);
const fn = iface.fragments.find(
(f) => f.type === 'function',
) as ethers.FunctionFragment;
const params = fn.inputs.map((input) =>
input.type === 'uint256' ? BigInt(100) : DUMMY_ADDRESS,
);
const bytes = encode(iface, fn.name, params);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe(fn.name);
});
});

describe('RoleManagementFacet', () => {
it('decodes grantRoles(bytes32[], address[], uint256[])', () => {
const iface = new ethers.Interface(RoleManagementFacet__factory.abi);
const bytes = encode(iface, 'grantRoles', [
[ethers.ZeroHash],
[DUMMY_ADDRESS],
[BigInt(0)],
]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('grantRoles');
});
});

describe('CustomFeesFacet', () => {
it('decodes updateTokenCustomFees(tuple[], tuple[])', () => {
const iface = new ethers.Interface(CustomFeesFacet__factory.abi);
const bytes = encode(iface, 'updateTokenCustomFees', [[], []]);
const result = TransactionService.decodeFunctionCall(bytes);
expect(result).not.toBeNull();
expect(result?.name).toBe('updateTokenCustomFees');
});
});

describe('Edge cases', () => {
it('returns null when the selector does not exist in any ABI', () => {
const unknown = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0, 0, 0, 0]);
const result = TransactionService.decodeFunctionCall(unknown);
expect(result).toBeNull();
});

it('returns null for an empty Uint8Array', () => {
const result = TransactionService.decodeFunctionCall(new Uint8Array(0));
expect(result).toBeNull();
});
});
});
19 changes: 19 additions & 0 deletions packages/sdk/jest.unit.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
testEnvironment: 'node',
preset: 'ts-jest',
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.(m)?js$': '$1',
'@hashgraph/hedera-wallet-connect':
'<rootDir>/__mocks__/hedera-wallet-connect.js',
'^uuid$': 'uuid',
},
testMatch: ['**/__tests__/app/**/*.(test|spec).[jt]s?(x)'],
testPathIgnorePatterns: ['/build/', '/src_old/', '/example/js/'],
modulePathIgnorePatterns: ['/example/js/'],
transform: {
'^.+\\.ts?$': 'ts-jest',
'^.+\\.[t|j]sx?$': 'babel-jest',
},
transformIgnorePatterns: ['node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)'],
testTimeout: 10_000,
};
2 changes: 2 additions & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},
"scripts": {
"execute:createMultisig": "node build/cjs/scripts/CreateMultisigAccount.js",
"execute:associateToken": "node build/cjs/scripts/AssociateToken.js",
"start": "node build/src/index.js",
"clean": "rimraf coverage build",
"clean:modules": "rimraf node_modules",
Expand All @@ -58,6 +59,7 @@
"build:release": "npm run clean && tsc -p tsconfig.release.json",
"lint": "eslint . --ext .ts --ext .mts",
"test": "NODE_OPTIONS=--max-old-space-size=8192 npx jest --forceExit --maxWorkers=50%",
"test:unit": "NODE_OPTIONS=--max-old-space-size=8192 npx jest --config jest.unit.config.js --forceExit --maxWorkers=50%",
"test:watch": "concurrently --kill-others \"npm run build:watch\" \"npm run test:jest:watch\"",
"test:jest:watch": "NODE_OPTIONS=--experimental-vm-modules npx jest --watch",
"test:ci": "NODE_OPTIONS=--max-old-space-size=8192 npx jest --ci --forceExit --runInBand",
Expand Down
Loading
Loading