Skip to content

Commit

Permalink
Add new snap_getCurrencyRate RPC method (#2763)
Browse files Browse the repository at this point in the history
This PR adds a new permitted RPC method called `snap_getCurrencyRate`.

It takes a cryptocurrency symbol under the `currency` param. (We
currently only support `btc`) and return a rate object if the rate is
available.

Fixes: #2762
  • Loading branch information
GuillaumeRx authored Sep 27, 2024
1 parent 63ea955 commit d4e1403
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 9 deletions.
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.
* @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.
*/
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

0 comments on commit d4e1403

Please sign in to comment.