Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions packages/snaps-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- **BREAKING:** Move assets market data to `onAssetsMarketData` handler ([#3496](https://github.com/MetaMask/snaps/pull/3496))
- This handler replaces the `marketData` field of `onAssetsConversion`, which is now removed.

## [13.1.1]

### Fixed
Expand Down
178 changes: 150 additions & 28 deletions packages/snaps-controllers/src/snaps/SnapController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4512,8 +4512,10 @@ describe('SnapController', () => {

snapController.destroy();
});
});

it('returns the value when `onAssetsConversion` returns a valid response with market data', async () => {
describe('onAssetsMarketData', () => {
it('throws if `onAssetsMarketData` handler returns an invalid response', async () => {
const rootMessenger = getControllerMessenger();
const messenger = getSnapControllerMessenger(rootMessenger);
const snapController = getSnapController(
Expand Down Expand Up @@ -4552,19 +4554,147 @@ describe('SnapController', () => {
'ExecutionService:handleRpcRequest',
async () =>
Promise.resolve({
conversionRates: {
marketData: { foo: {} },
}),
);

await expect(
snapController.handleRequest({
snapId: MOCK_SNAP_ID,
origin: METAMASK_ORIGIN,
handler: HandlerType.OnAssetsMarketData,
request: {
jsonrpc: '2.0',
method: ' ',
params: {},
id: 1,
},
}),
).rejects.toThrow(
`Assertion failed: At path: marketData.foo -- Expected a value of type \`CaipAssetType\`, but received: \`"foo"\`.`,
);

snapController.destroy();
});

it('filters out assets that are out of scope for `onAssetsMarketData`', async () => {
const rootMessenger = getControllerMessenger();
const messenger = getSnapControllerMessenger(rootMessenger);
const snapController = getSnapController(
getSnapControllerOptions({
messenger,
state: {
snaps: getPersistedSnapsState(),
},
}),
);

rootMessenger.registerActionHandler(
'PermissionController:getPermissions',
() => ({
[SnapEndowments.Assets]: {
caveats: [
{
type: SnapCaveatType.ChainIds,
value: ['bip122:000000000019d6689c085ae165831e93'],
},
],
date: 1664187844588,
id: 'izn0WGUO8cvq_jqvLQuQP',
invoker: MOCK_SNAP_ID,
parentCapability: SnapEndowments.Assets,
},
}),
);

rootMessenger.registerActionHandler(
'SubjectMetadataController:getSubjectMetadata',
() => MOCK_SNAP_SUBJECT_METADATA,
);

rootMessenger.registerActionHandler(
'ExecutionService:handleRpcRequest',
async () =>
Promise.resolve({
marketData: {
'bip122:000000000019d6689c085ae165831e93/slip44:0': {
'eip155:1/slip44:60': {
fungible: true,
},
},
},
}),
);

expect(
await snapController.handleRequest({
snapId: MOCK_SNAP_ID,
origin: METAMASK_ORIGIN,
handler: HandlerType.OnAssetsMarketData,
request: {
jsonrpc: '2.0',
method: ' ',
params: {
assets: [
{
asset: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
unit: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
},
],
},
id: 1,
},
}),
).toStrictEqual({ marketData: {} });

snapController.destroy();
});

it('returns the value when `onAssetsMarketData` returns a valid response', async () => {
const rootMessenger = getControllerMessenger();
const messenger = getSnapControllerMessenger(rootMessenger);
const snapController = getSnapController(
getSnapControllerOptions({
messenger,
state: {
snaps: getPersistedSnapsState(),
},
}),
);

rootMessenger.registerActionHandler(
'PermissionController:getPermissions',
() => ({
[SnapEndowments.Assets]: {
caveats: [
{
type: SnapCaveatType.ChainIds,
value: ['bip122:000000000019d6689c085ae165831e93'],
},
],
date: 1664187844588,
id: 'izn0WGUO8cvq_jqvLQuQP',
invoker: MOCK_SNAP_ID,
parentCapability: SnapEndowments.Assets,
},
}),
);

rootMessenger.registerActionHandler(
'SubjectMetadataController:getSubjectMetadata',
() => MOCK_SNAP_SUBJECT_METADATA,
);

rootMessenger.registerActionHandler(
'ExecutionService:handleRpcRequest',
async () =>
Promise.resolve({
marketData: {
'bip122:000000000019d6689c085ae165831e93/slip44:0': {
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
rate: '400',
conversionTime: 1737548790,
marketData: {
marketCap: '123',
totalVolume: '123',
circulatingSupply: '123',
allTimeHigh: '123',
allTimeLow: '123',
pricePercentChange: { all: 1.23 },
},
fungible: true,
marketCap: '10000',
totalVolume: '100000000',
},
},
},
Expand All @@ -4575,36 +4705,28 @@ describe('SnapController', () => {
await snapController.handleRequest({
snapId: MOCK_SNAP_ID,
origin: METAMASK_ORIGIN,
handler: HandlerType.OnAssetsConversion,
handler: HandlerType.OnAssetsMarketData,
request: {
jsonrpc: '2.0',
method: ' ',
params: {
conversions: [
assets: [
{
from: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
to: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
asset: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
unit: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
},
],
includeMarketData: true,
},
id: 1,
},
}),
).toStrictEqual({
conversionRates: {
marketData: {
'bip122:000000000019d6689c085ae165831e93/slip44:0': {
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
rate: '400',
conversionTime: 1737548790,
marketData: {
marketCap: '123',
totalVolume: '123',
circulatingSupply: '123',
allTimeHigh: '123',
allTimeLow: '123',
pricePercentChange: { all: 1.23 },
},
fungible: true,
marketCap: '10000',
totalVolume: '100000000',
},
},
},
Expand Down
47 changes: 47 additions & 0 deletions packages/snaps-controllers/src/snaps/SnapController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ import type {
OnAssetsConversionArguments,
AssetConversion,
OnAssetsLookupArguments,
OnAssetsMarketDataArguments,
FungibleAssetMarketData,
OnAssetsMarketDataResponse,
} from '@metamask/snaps-sdk';
import {
AuxiliaryFileEncoding,
Expand Down Expand Up @@ -105,6 +108,7 @@ import {
isValidUrl,
OnAssetHistoricalPriceResponseStruct,
OnAssetsConversionResponseStruct,
OnAssetsMarketDataResponseStruct,
} from '@metamask/snaps-utils';
import type {
Json,
Expand Down Expand Up @@ -3780,6 +3784,14 @@ export class SnapController extends BaseController<
},
result as OnAssetsConversionResponse,
);

case HandlerType.OnAssetsMarketData:
// We can cast since the request and result have already been validated.
return this.#transformOnAssetsMarketDataResult(
request as { params: OnAssetsMarketDataArguments },
result as OnAssetsMarketDataResponse,
);

default:
return result;
}
Expand Down Expand Up @@ -3864,6 +3876,38 @@ export class SnapController extends BaseController<
return { conversionRates: filteredConversionRates };
}

/**
* Transforms an RPC response coming from the `onAssetsMarketData` handler.
*
* This filters out responses that are out of scope for the Snap based on
* the incoming request.
*
* @param request - The request that returned the result.
* @param request.params - The parameters for the request.
* @param result - The result.
* @param result.marketData - The market data returned by the Snap.
* @returns The transformed result.
*/
#transformOnAssetsMarketDataResult(
{ params: requestedParams }: { params: OnAssetsMarketDataArguments },
{ marketData }: OnAssetsMarketDataResponse,
) {
const { assets: requestedAssets } = requestedParams;

const filteredMarketData = requestedAssets.reduce<
Record<CaipAssetType, Record<CaipAssetType, FungibleAssetMarketData>>
>((accumulator, assets) => {
const result = marketData[assets.asset]?.[assets.unit];
// Only include rates that were actually requested.
if (result) {
accumulator[assets.asset] ??= {};
accumulator[assets.asset][assets.unit] = result;
}
return accumulator;
}, {});
return { marketData: filteredMarketData };
}

/**
* Transforms a JSON-RPC request before sending it to the Snap, if required for a given handler.
*
Expand Down Expand Up @@ -3961,6 +4005,9 @@ export class SnapController extends BaseController<
case HandlerType.OnAssetHistoricalPrice:
assertStruct(result, OnAssetHistoricalPriceResponseStruct);
break;
case HandlerType.OnAssetsMarketData:
assertStruct(result, OnAssetsMarketDataResponseStruct);
break;
default:
break;
}
Expand Down
1 change: 1 addition & 0 deletions packages/snaps-controllers/src/snaps/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ export const CLIENT_ONLY_HANDLERS = Object.freeze([
HandlerType.OnAssetsLookup,
HandlerType.OnAssetsConversion,
HandlerType.OnAssetHistoricalPrice,
HandlerType.OnAssetsMarketData,
HandlerType.OnWebSocketEvent,
]);
3 changes: 3 additions & 0 deletions packages/snaps-execution-environments/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- **BREAKING:** Move assets market data to `onAssetsMarketData` handler ([#3496](https://github.com/MetaMask/snaps/pull/3496))
- This handler replaces the `marketData` field of `onAssetsConversion`, which is now removed.

## [9.1.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1608,9 +1608,9 @@ describe('BaseSnapExecutor', () => {
});
});

it('supports `onAssetsConversion` export with the market data flag', async () => {
it('supports `onAssetsMarketData` export', async () => {
const CODE = `
module.exports.onAssetsConversion = () => ({ conversionRates: {} });
module.exports.onAssetsMarketData = () => ({ marketData: {} });
`;

const executor = new TestSnapExecutor();
Expand All @@ -1628,19 +1628,18 @@ describe('BaseSnapExecutor', () => {
method: 'snapRpc',
params: [
MOCK_SNAP_ID,
HandlerType.OnAssetsConversion,
HandlerType.OnAssetsMarketData,
MOCK_ORIGIN,
{
jsonrpc: '2.0',
method: '',
params: {
conversions: [
assets: [
{
from: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
to: 'eip155:1/slip44:60',
asset: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
unit: 'eip155:1/slip44:60',
},
],
includeMarketData: true,
},
},
],
Expand All @@ -1649,7 +1648,7 @@ describe('BaseSnapExecutor', () => {
expect(await executor.readCommand()).toStrictEqual({
id: 2,
jsonrpc: '2.0',
result: { conversionRates: {} },
result: { marketData: {} },
});
});

Expand Down
12 changes: 10 additions & 2 deletions packages/snaps-execution-environments/src/common/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
assertIsOnProtocolRequestArguments,
assertIsOnAssetHistoricalPriceRequestArguments,
assertIsOnWebSocketEventArguments,
assertIsOnAssetsMarketDataRequestArguments,
} from './validation';

export type CommandMethodsMapping = {
Expand Down Expand Up @@ -75,9 +76,16 @@ export function getHandlerArguments(
}
case HandlerType.OnAssetsConversion: {
assertIsOnAssetsConversionRequestArguments(request.params);
const { conversions, includeMarketData } = request.params;
return { conversions, includeMarketData };
const { conversions } = request.params;
return { conversions };
}

case HandlerType.OnAssetsMarketData: {
assertIsOnAssetsMarketDataRequestArguments(request.params);
const { assets } = request.params;
return { assets };
}

case HandlerType.OnNameLookup: {
assertIsOnNameLookupRequestArguments(request.params);

Expand Down
Loading
Loading