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

Add new snap_getCurrencyRate RPC method #2763

Merged
merged 13 commits into from
Sep 27, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "+w62Op5ur4nVLmQ0uKA0IsAQN2hkKOsgSm4VK9jxxYY=",
"shasum": "zGgTjLWTEn796eXGvv66p8tGxZSa82yEEGnRtyVutEc=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/browserify/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "SL7kg2vwhtpuzNvOx7uQZNZbccRgfFtTi0xZdEVEP0s=",
"shasum": "qfkidJLew8JNN2Enx4pDUgWNgLPqBkG0k3mGQRR1oaY=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
8 changes: 4 additions & 4 deletions packages/snaps-rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
],
coverageThreshold: {
global: {
branches: 92.68,
functions: 97.17,
lines: 97.71,
statements: 97.21,
branches: 92.77,
functions: 97.2,
lines: 97.76,
statements: 97.26,
},
},
});
159 changes: 159 additions & 0 deletions packages/snaps-rpc-methods/src/permitted/getCurrencyRate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
import { type GetCurrencyRateResult } from '@metamask/snaps-sdk';
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';

import type { GetCurrencyRateParameters } from './getCurrencyRate';
import { getCurrencyRateHandler } from './getCurrencyRate';

describe('snap_getCurrencyRate', () => {
describe('getCurrencyRateHandler', () => {
it('has the expected shape', () => {
expect(getCurrencyRateHandler).toMatchObject({
methodNames: ['snap_getCurrencyRate'],
implementation: expect.any(Function),
hookNames: {
getCurrencyRate: true,
},
});
});
});

describe('implementation', () => {
it('returns the result from the `getCurrencyRate` hook', async () => {
const { implementation } = getCurrencyRateHandler;

const getCurrencyRate = jest.fn().mockReturnValue({
currency: 'usd',
conversionRate: 1,
conversionDate: 1,
usdConversionRate: 1,
});

const hooks = {
getCurrencyRate,
};

const engine = new JsonRpcEngine();

engine.push((request, response, next, end) => {
const result = implementation(
request as JsonRpcRequest<GetCurrencyRateParameters>,
response as PendingJsonRpcResponse<GetCurrencyRateResult>,
next,
end,
hooks,
);

result?.catch(end);
});

const response = await engine.handle({
jsonrpc: '2.0',
id: 1,
method: 'snap_getCurrencyRate',
params: {
currency: 'btc',
},
});

expect(response).toStrictEqual({
jsonrpc: '2.0',
id: 1,
result: {
currency: 'usd',
conversionRate: 1,
conversionDate: 1,
usdConversionRate: 1,
},
});
});

it('returns null if there is no rate available', async () => {
const { implementation } = getCurrencyRateHandler;

const getCurrencyRate = jest.fn().mockReturnValue(undefined);

const hooks = {
getCurrencyRate,
};

const engine = new JsonRpcEngine();

engine.push((request, response, next, end) => {
const result = implementation(
request as JsonRpcRequest<GetCurrencyRateParameters>,
response as PendingJsonRpcResponse<GetCurrencyRateResult>,
next,
end,
hooks,
);

result?.catch(end);
});

const response = await engine.handle({
jsonrpc: '2.0',
id: 1,
method: 'snap_getCurrencyRate',
params: {
currency: 'btc',
},
});

expect(response).toStrictEqual({
jsonrpc: '2.0',
id: 1,
result: null,
});
});

it('throws on invalid params', async () => {
const { implementation } = getCurrencyRateHandler;

const getCurrencyRate = jest.fn().mockReturnValue({
currency: 'usd',
conversionRate: 1,
conversionDate: 1,
usdConversionRate: 1,
});

const hooks = {
getCurrencyRate,
};

const engine = new JsonRpcEngine();

engine.push((request, response, next, end) => {
const result = implementation(
request as JsonRpcRequest<GetCurrencyRateParameters>,
response as PendingJsonRpcResponse<GetCurrencyRateResult>,
next,
end,
hooks,
);

result?.catch(end);
});

const response = await engine.handle({
jsonrpc: '2.0',
id: 1,
method: 'snap_getCurrencyRate',
params: {
currency: 'eth',
},
});

expect(response).toStrictEqual({
error: {
code: -32602,
message:
'Invalid params: At path: currency -- Expected the value to satisfy a union of `literal`, but received: "eth".',
stack: expect.any(String),
},
id: 1,
jsonrpc: '2.0',
});
});
});
});
102 changes: 102 additions & 0 deletions packages/snaps-rpc-methods/src/permitted/getCurrencyRate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';
import type { PermittedHandlerExport } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import type {
AvailableCurrency,
CurrencyRate,
GetCurrencyRateParams,
GetCurrencyRateResult,
JsonRpcRequest,
} from '@metamask/snaps-sdk';
import { currency, type InferMatching } from '@metamask/snaps-utils';
import { StructError, create, object, union } from '@metamask/superstruct';
import type { PendingJsonRpcResponse } from '@metamask/utils';

import type { MethodHooksObject } from '../utils';

const hookNames: MethodHooksObject<GetCurrencyRateMethodHooks> = {
getCurrencyRate: true,
};

export type GetCurrencyRateMethodHooks = {
/**
* @param currency - The currency symbol.
* Currently only 'btc' is supported.
GuillaumeRx marked this conversation as resolved.
Show resolved Hide resolved
* @returns The {@link CurrencyRate} object.
*/
getCurrencyRate: (currency: AvailableCurrency) => CurrencyRate | undefined;
};

export const getCurrencyRateHandler: PermittedHandlerExport<
GetCurrencyRateMethodHooks,
GetCurrencyRateParameters,
GetCurrencyRateResult
> = {
methodNames: ['snap_getCurrencyRate'],
implementation: getGetCurrencyRateImplementation,
hookNames,
};

const GetCurrencyRateParametersStruct = object({
currency: union([currency('btc')]),
});

export type GetCurrencyRateParameters = InferMatching<
typeof GetCurrencyRateParametersStruct,
GetCurrencyRateParams
>;

/**
* The `snap_getCurrencyRate` method implementation.
*
* @param req - The JSON-RPC request object.
* @param res - The JSON-RPC response object.
* @param _next - The `json-rpc-engine` "next" callback. Not used by this
* function.
* @param end - The `json-rpc-engine` "end" callback.
* @param hooks - The RPC method hooks.
* @param hooks.getCurrencyRate - The function to get the rate.
* @returns Nothing.
*/
function getGetCurrencyRateImplementation(
req: JsonRpcRequest<GetCurrencyRateParameters>,
res: PendingJsonRpcResponse<GetCurrencyRateResult>,
_next: unknown,
end: JsonRpcEngineEndCallback,
{ getCurrencyRate }: GetCurrencyRateMethodHooks,
): void {
const { params } = req;

try {
const validatedParams = getValidatedParams(params);

const { currency: selectedCurrency } = validatedParams;

res.result = getCurrencyRate(selectedCurrency) ?? null;
} catch (error) {
return end(error);
}

return end();
}

/**
* Validate the getCurrencyRate method `params` and returns them cast to the correct
* type. Throws if validation fails.
*
* @param params - The unvalidated params object from the method request.
* @returns The validated getCurrencyRate method parameter object.
*/
function getValidatedParams(params: unknown): GetCurrencyRateParameters {
try {
return create(params, GetCurrencyRateParametersStruct);
} catch (error) {
if (error instanceof StructError) {
throw rpcErrors.invalidParams({
message: `Invalid params: ${error.message}.`,
});
}
/* istanbul ignore next */
throw rpcErrors.internal();
}
}
2 changes: 2 additions & 0 deletions packages/snaps-rpc-methods/src/permitted/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createInterfaceHandler } from './createInterface';
import { getAllSnapsHandler } from './getAllSnaps';
import { getClientStatusHandler } from './getClientStatus';
import { getCurrencyRateHandler } from './getCurrencyRate';
import { getFileHandler } from './getFile';
import { getInterfaceStateHandler } from './getInterfaceState';
import { getSnapsHandler } from './getSnaps';
Expand All @@ -23,6 +24,7 @@ export const methodHandlers = {
snap_updateInterface: updateInterfaceHandler,
snap_getInterfaceState: getInterfaceStateHandler,
snap_resolveInterface: resolveInterfaceHandler,
snap_getCurrencyRate: getCurrencyRateHandler,
};
/* eslint-enable @typescript-eslint/naming-convention */

Expand Down
4 changes: 3 additions & 1 deletion packages/snaps-rpc-methods/src/permitted/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CreateInterfaceMethodHooks } from './createInterface';
import type { GetAllSnapsHooks } from './getAllSnaps';
import type { GetClientStatusHooks } from './getClientStatus';
import type { GetCurrencyRateMethodHooks } from './getCurrencyRate';
import type { GetInterfaceStateMethodHooks } from './getInterfaceState';
import type { GetSnapsHooks } from './getSnaps';
import type { RequestSnapsHooks } from './requestSnaps';
Expand All @@ -14,7 +15,8 @@ export type PermittedRpcMethodHooks = GetAllSnapsHooks &
CreateInterfaceMethodHooks &
UpdateInterfaceMethodHooks &
GetInterfaceStateMethodHooks &
ResolveInterfaceMethodHooks;
ResolveInterfaceMethodHooks &
GetCurrencyRateMethodHooks;

export * from './handlers';
export * from './middleware';
34 changes: 34 additions & 0 deletions packages/snaps-sdk/src/types/methods/get-currency-rate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type Currency<Value extends string> =
| Lowercase<Value>
| Uppercase<Value>;

export type AvailableCurrency = Currency<'btc'>;

/**
* The currency rate object.
*
* @property currency - The native currency symbol used for the conversion (e.g 'usd').
* @property conversionRate - The conversion rate from the cryptocurrency to the native currency.
* @property conversionDate - The date of the conversion rate as a UNIX timestamp.
* @property usdConversionRate - The conversion rate to USD.
GuillaumeRx marked this conversation as resolved.
Show resolved Hide resolved
*/
export type CurrencyRate = {
currency: string;
conversionRate: number;
conversionDate: number;
usdConversionRate?: number;
};

/**
* The request parameters for the `snap_getCurrencyRate` method.
*
* @property currency - The currency symbol.
*/
export type GetCurrencyRateParams = {
currency: AvailableCurrency;
};

/**
* The result returned by the `snap_getCurrencyRate` method, which is the {@link CurrencyRate} object.
*/
export type GetCurrencyRateResult = CurrencyRate | null;
1 change: 1 addition & 0 deletions packages/snaps-sdk/src/types/methods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './notify';
export * from './request-snaps';
export * from './update-interface';
export * from './resolve-interface';
export * from './get-currency-rate';
4 changes: 2 additions & 2 deletions packages/snaps-utils/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 99.74,
"functions": 98.9,
"functions": 98.91,
"lines": 99.45,
"statements": 96.29
"statements": 96.3
}
Loading
Loading