Skip to content

Commit

Permalink
feat: parameter validation for getUtxos
Browse files Browse the repository at this point in the history
  • Loading branch information
andreabadesso committed Jan 15, 2025
1 parent 157bdc0 commit a7a11b6
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 33 deletions.
78 changes: 76 additions & 2 deletions packages/hathor-rpc-handler/__tests__/rpcMethods/getUtxos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/

import { PromptRejectedError } from '../../src/errors';
import { PromptRejectedError, InvalidParamsError } from '../../src/errors';
import { mockPromptHandler, mockGetUtxosRequest } from '../mocks';
import { HathorWallet } from '@hathor/wallet-lib';
import { getUtxos } from '../../src/rpcMethods/getUtxos';
import { TriggerTypes, TriggerResponseTypes, UtxoDetails } from '../../src/types';
import { TriggerTypes, TriggerResponseTypes, UtxoDetails, RpcMethods, GetUtxosRpcRequest } from '../../src/types';

const mockResponse: UtxoDetails = {
total_amount_available: 50,
Expand All @@ -35,6 +35,80 @@ describe('getUtxos', () => {
} as unknown as HathorWallet;
});

describe('parameter validation', () => {
it('should reject when method is missing', async () => {
const invalidRequest = {
params: mockGetUtxosRequest.params,
} as GetUtxosRpcRequest;

await expect(
getUtxos(invalidRequest, wallet, {}, mockPromptHandler)
).rejects.toThrow(InvalidParamsError);
});

it('should reject when method is invalid', async () => {
const invalidRequest = {
method: 'invalid_method',
params: mockGetUtxosRequest.params,
} as unknown as GetUtxosRpcRequest;

await expect(
getUtxos(invalidRequest, wallet, {}, mockPromptHandler)
).rejects.toThrow(InvalidParamsError);
});

it('should reject when network is missing', async () => {
const invalidRequest = {
method: RpcMethods.GetUtxos,
params: {
...mockGetUtxosRequest.params,
network: undefined,
},
} as unknown as GetUtxosRpcRequest;

await expect(
getUtxos(invalidRequest, wallet, {}, mockPromptHandler)
).rejects.toThrow(InvalidParamsError);
});

it('should reject when filterAddress is missing', async () => {
const invalidRequest = {
method: RpcMethods.GetUtxos,
params: {
...mockGetUtxosRequest.params,
filterAddress: undefined,
},
} as unknown as GetUtxosRpcRequest;

await expect(
getUtxos(invalidRequest, wallet, {}, mockPromptHandler)
).rejects.toThrow(InvalidParamsError);
});

it('should use default values for optional parameters', async () => {
mockPromptHandler.mockResolvedValue({
type: TriggerResponseTypes.GetUtxosConfirmationResponse,
data: true,
});

const request = {
method: RpcMethods.GetUtxos,
params: {
network: 'mainnet',
filterAddress: 'mock_address',
},
} as GetUtxosRpcRequest;

await getUtxos(request, wallet, {}, mockPromptHandler);

expect(wallet.getUtxos).toHaveBeenCalledWith(expect.objectContaining({
token: 'HTR',
max_utxos: 255,
only_available_utxos: true,
}));
});
});

it('should return UTXO details if user confirms', async () => {
mockPromptHandler.mockResolvedValue({
type: TriggerResponseTypes.GetUtxosConfirmationResponse,
Expand Down
89 changes: 58 additions & 31 deletions packages/hathor-rpc-handler/src/rpcMethods/getUtxos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { z } from 'zod';
import type { HathorWallet } from '@hathor/wallet-lib';
import {
GetUtxosConfirmationResponse,
Expand All @@ -13,11 +14,27 @@ import {
UtxoDetails,
RpcResponseTypes,
RpcResponse,
RpcMethods,
} from '../types';
import { PromptRejectedError } from '../errors';
import { PromptRejectedError, InvalidParamsError } from '../errors';
import { TriggerTypes } from '../types';
import { validateNetwork } from '../helpers';

const getUtxosSchema = z.object({
method: z.literal(RpcMethods.GetUtxos),
params: z.object({
network: z.string(),
maxUtxos: z.number().default(255),
token: z.string().default('HTR'),
filterAddress: z.string(),
authorities: z.number().nullable().optional(),
amountSmallerThan: z.number().nullable().optional(),
amountBiggerThan: z.number().nullable().optional(),
maximumAmount: z.number().nullable().optional(),
onlyAvailableUtxos: z.boolean().default(true),
}),
});

/**
* Handles the 'htr_getUtxos' RPC request by prompting the user for confirmation
* and returning the UTXO details if confirmed.
Expand All @@ -29,7 +46,7 @@ import { validateNetwork } from '../helpers';
*
* @returns The UTXO details from the wallet if the user confirms.
*
* @throws {InvalidRpcMethod} If the RPC request method is not 'htr_getUtxos'.
* @throws {InvalidParamsError} If the RPC request parameters are invalid.
* @throws {Error} If the method is not implemented in the wallet-service facade.
* @throws {PromptRejectedError} If the user rejects the prompt.
*/
Expand All @@ -39,38 +56,48 @@ export async function getUtxos(
requestMetadata: RequestMetadata,
promptHandler: TriggerHandler,
) {
validateNetwork(wallet, rpcRequest.params.network);
try {
const validatedRequest = getUtxosSchema.parse(rpcRequest);
const { params } = validatedRequest;

const options = {
'token': rpcRequest.params.token,
// Defaults to 0 otherwise the lib fails
'authorities': rpcRequest.params.authorities || 0,
'max_utxos': rpcRequest.params.maxUtxos,
'filter_address': rpcRequest.params.filterAddress,
'amount_smaller_than': rpcRequest.params.amountSmallerThan,
'amount_bigger_than': rpcRequest.params.amountBiggerThan,
'max_amount': rpcRequest.params.maximumAmount,
'only_available_utxos': rpcRequest.params.onlyAvailableUtxos,
};
validateNetwork(wallet, params.network);

// We have the same issues here that we do have in the headless wallet:
// TODO: Memory usage enhancements are required here as wallet.getUtxos can cause issues on
// wallets with a huge amount of utxos.
// TODO: This needs to be paginated.
const utxoDetails: UtxoDetails[] = await wallet.getUtxos(options);
const options = {
'token': params.token,
// Defaults to 0 otherwise the lib fails
'authorities': params.authorities || 0,
'max_utxos': params.maxUtxos,
'filter_address': params.filterAddress,
'amount_smaller_than': params.amountSmallerThan,
'amount_bigger_than': params.amountBiggerThan,
'max_amount': params.maximumAmount,
'only_available_utxos': params.onlyAvailableUtxos,
};

const confirmed = await promptHandler({
type: TriggerTypes.GetUtxosConfirmationPrompt,
method: rpcRequest.method,
data: utxoDetails
}, requestMetadata) as GetUtxosConfirmationResponse;
// We have the same issues here that we do have in the headless wallet:
// TODO: Memory usage enhancements are required here as wallet.getUtxos can cause issues on
// wallets with a huge amount of utxos.
// TODO: This needs to be paginated.
const utxoDetails: UtxoDetails[] = await wallet.getUtxos(options);

if (!confirmed.data) {
throw new PromptRejectedError();
}
const confirmed = await promptHandler({
type: TriggerTypes.GetUtxosConfirmationPrompt,
method: rpcRequest.method,
data: utxoDetails
}, requestMetadata) as GetUtxosConfirmationResponse;

return {
type: RpcResponseTypes.GetUtxosResponse,
response: utxoDetails,
} as RpcResponse;
if (!confirmed.data) {
throw new PromptRejectedError();
}

return {
type: RpcResponseTypes.GetUtxosResponse,
response: utxoDetails,
} as RpcResponse;
} catch (err) {
if (err instanceof z.ZodError) {
throw new InvalidParamsError(err.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '));
}
throw err;
}
}

0 comments on commit a7a11b6

Please sign in to comment.