Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: param validation for all RPC methods #29

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
924e14e
feat: added validation for createToken and sendNanoContractTx
andreabadesso Jan 15, 2025
de3b144
feat: added parameter validation for the sign oracle data RPC
andreabadesso Jan 15, 2025
d9cf6ce
feat: added parameter validation for signWithAddress
andreabadesso Jan 15, 2025
906f6c4
feat: added parameter validation for getBalance
andreabadesso Jan 15, 2025
6fd7768
feat: added parameter validation for getAddress RPC
andreabadesso Jan 15, 2025
157bdc0
feat: added parameter validation for getConnectedNetwork
andreabadesso Jan 15, 2025
a7a11b6
feat: parameter validation for getUtxos
andreabadesso Jan 15, 2025
ecb47ea
feat: added parameter validation for signWithAddress
andreabadesso Jan 15, 2025
5ba5526
refactor: amountSmallerThan and BiggerThan should not be negative
andreabadesso Jan 29, 2025
5fcf239
refactor: blueprint_id is nullable
andreabadesso Jan 29, 2025
0c3e6d5
refactor: nullable -> nullish
andreabadesso Feb 24, 2025
bc5d375
refactor: use null as default on zod schema for create Token RPC
andreabadesso Feb 24, 2025
eb4838c
refactor: using zod's .trasform method to change params to camelCase
andreabadesso Feb 24, 2025
4542861
refactor: transforming options into a specific options object
andreabadesso Feb 24, 2025
727404e
refactor: better errors on getAddress
andreabadesso Feb 24, 2025
c42f5d9
refactor: using discriminatedUnion in getAddress RPC
andreabadesso Feb 24, 2025
7b55ca6
refactor: using safeParse instead of wrapping the entire code in a tr…
andreabadesso Feb 24, 2025
26c92d6
refactor: better zod
andreabadesso Feb 25, 2025
b9fceef
refactor: default is already zero
andreabadesso Feb 25, 2025
ecf6825
refactor: stop multiplying options in getUtxos
andreabadesso Feb 25, 2025
2788c52
refactor: using params directly in send nano contract tx
andreabadesso Feb 25, 2025
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 @@ -7,7 +7,7 @@ import {
TriggerResponseTypes,
RpcResponseTypes,
} from '../../src/types';
import { CreateTokenError, PromptRejectedError } from '../../src/errors';
import { CreateTokenError, PromptRejectedError, InvalidParamsError } from '../../src/errors';

function toCamelCase(params: Pick<CreateTokenRpcRequest, 'params'>['params']) {
return {
Expand Down Expand Up @@ -176,5 +176,86 @@ describe('createToken', () => {

expect(wallet.isAddressMine).toHaveBeenCalledWith('changeAddress123');
});

describe('parameter validation', () => {
beforeEach(() => {
(wallet.isAddressMine as jest.Mock).mockResolvedValue(true);
triggerHandler.mockResolvedValue({
type: TriggerResponseTypes.CreateTokenConfirmationResponse,
data: { accepted: true },
});
});

it('should reject when required parameters are missing', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
symbol: 'MTK',
amount: 1000,
// name is missing
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when amount is not positive', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: 'My Token',
symbol: 'MTK',
amount: 0,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when parameters have wrong types', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: 'My Token',
symbol: 'MTK',
amount: '1000' as unknown as number,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when optional parameters have wrong types', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: 'My Token',
symbol: 'MTK',
amount: 1000,
create_mint: 'yes' as unknown as boolean,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when name or symbol are empty strings', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: '',
symbol: 'MTK',
amount: 1000,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});
});
});

235 changes: 162 additions & 73 deletions packages/hathor-rpc-handler/__tests__/rpcMethods/getAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,110 +5,199 @@
* LICENSE file in the root directory of this source tree.
*/

import { NotImplementedError, PromptRejectedError } from '../../src/errors';
import { getAddress } from '../../src/rpcMethods/getAddress';
import { HathorWallet } from '@hathor/wallet-lib';
import { TriggerTypes, GetAddressRpcRequest, RpcMethods } from '../../src/types';
import {
RpcMethods,
GetAddressRpcRequest,
TriggerResponseTypes,
AddressRequestClientResponse,
} from '../../src/types';
import { getAddress } from '../../src/rpcMethods/getAddress';
import { InvalidParamsError, NotImplementedError, PromptRejectedError } from '../../src/errors';

export const mockPromptHandler = jest.fn();
describe('getAddress parameter validation', () => {
const mockWallet = {
getNetwork: jest.fn().mockReturnValue('testnet'),
getCurrentAddress: jest.fn().mockResolvedValue('test-address'),
getAddressAtIndex: jest.fn().mockResolvedValue('test-address'),
} as unknown as HathorWallet;

describe('getAddress', () => {
let promptHandler: jest.Mock;
let mockWallet: jest.Mocked<HathorWallet>;
const mockTriggerHandler = jest.fn().mockResolvedValue(true);

beforeEach(() => {
promptHandler = jest.fn();
mockWallet = {
getAddressAtIndex: jest.fn().mockReturnValue('mocked_address'),
getCurrentAddress: jest.fn().mockReturnValue({
address: 'address1',
index: 0,
addressPath: `m/44'/280'/0'/0/10`,
}),
getNetwork: jest.fn().mockReturnValue('mainnet')
} as unknown as HathorWallet;
jest.clearAllMocks();
});

it('should return the current address for type "first_empty"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'first_empty', network: 'mainnet' },
it('should reject when network is missing', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockReturnValueOnce(true);

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);
params: {
type: 'first_empty',
// network is missing
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

expect(address.response).toBe('current-address');
expect(mockWallet.getCurrentAddress).toHaveBeenCalled();
it('should reject when type is invalid', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
params: {
type: 'invalid_type',
network: 'testnet',
},
} as unknown as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

it('should throw NotImplementedError for type "full_path"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'full_path', network: 'mainnet' },
it('should reject when type is index but index is missing', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
};
params: {
type: 'index',
network: 'testnet',
// index is missing
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

await expect(getAddress(rpcRequest, mockWallet, {}, promptHandler)).rejects.toThrow(NotImplementedError);
it('should reject when type is index but index is negative', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
params: {
type: 'index',
network: 'testnet',
index: -1,
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

it('should return the address at index for type "index"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'index', index: 5, network: 'mainnet' },
it('should reject when type is full_path but full_path is missing', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
};
mockWallet.getAddressAtIndex.mockResolvedValue('address-at-index');
promptHandler.mockReturnValueOnce(true);
params: {
type: 'full_path',
network: 'testnet',
// full_path is missing
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);
it('should reject when type is full_path but full_path is empty', async () => {
const invalidRequest = {
method: RpcMethods.GetAddress,
params: {
type: 'full_path',
network: 'testnet',
full_path: '',
},
} as GetAddressRpcRequest;

await expect(
getAddress(invalidRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(InvalidParamsError);
});

expect(address.response).toBe('address-at-index');
expect(mockWallet.getAddressAtIndex).toHaveBeenCalledWith(5);
it('should throw NotImplementedError when type is full_path', async () => {
const request = {
method: RpcMethods.GetAddress,
params: {
type: 'full_path',
network: 'testnet',
full_path: 'm/44/0/0',
},
} as GetAddressRpcRequest;

await expect(
getAddress(request, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(NotImplementedError);
});

it('should return the client address for type "client"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'client', network: 'mainnet' },
it('should accept valid first_empty request', async () => {
const validRequest = {
method: RpcMethods.GetAddress,
};
const clientPromptResponse = { data: { address: 'client-address' } };
promptHandler.mockResolvedValue(clientPromptResponse);
params: {
type: 'first_empty',
network: 'testnet',
},
} as GetAddressRpcRequest;

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);
await expect(
getAddress(validRequest, mockWallet, {}, mockTriggerHandler)
).resolves.toBeDefined();

expect(address.response).toBe('client-address');
expect(promptHandler).toHaveBeenCalledWith({
type: TriggerTypes.AddressRequestClientPrompt,
method: RpcMethods.GetAddress,
}, {});
expect(mockWallet.getCurrentAddress).toHaveBeenCalled();
});

it('should throw PromptRejectedError if address confirmation is rejected', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'first_empty', network: 'mainnet' },
it('should accept valid index request', async () => {
const validRequest = {
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockResolvedValueOnce(false);
params: {
type: 'index',
network: 'testnet',
index: 0,
},
} as GetAddressRpcRequest;

await expect(getAddress(rpcRequest, mockWallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError);
await expect(
getAddress(validRequest, mockWallet, {}, mockTriggerHandler)
).resolves.toBeDefined();

expect(mockWallet.getAddressAtIndex).toHaveBeenCalledWith(0);
});

it('should confirm the address if type is not "client"', async () => {
const rpcRequest: GetAddressRpcRequest = {
params: { type: 'first_empty', network: 'mainnet' },
it('should accept valid client request', async () => {
const validRequest = {
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockResolvedValue(true);

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);
params: {
type: 'client',
network: 'testnet',
},
} as GetAddressRpcRequest;

mockTriggerHandler.mockResolvedValueOnce({
type: TriggerResponseTypes.AddressRequestClientResponse,
data: {
address: 'client-address',
},
} as AddressRequestClientResponse);

await expect(
getAddress(validRequest, mockWallet, {}, mockTriggerHandler)
).resolves.toBeDefined();
});

expect(address.response).toBe('current-address');
expect(promptHandler).toHaveBeenCalledWith({
type: TriggerTypes.AddressRequestPrompt,
it('should throw PromptRejectedError when user rejects non-client address', async () => {
const validRequest = {
method: RpcMethods.GetAddress,
data: { address: 'current-address' },
}, {});
params: {
type: 'first_empty',
network: 'testnet',
},
} as GetAddressRpcRequest;

mockTriggerHandler.mockResolvedValueOnce(false);

await expect(
getAddress(validRequest, mockWallet, {}, mockTriggerHandler)
).rejects.toThrow(PromptRejectedError);
});
});
Loading