Skip to content

Commit 302cd79

Browse files
committed
[BREAKING] Add onAssetsMarketData handler
1 parent 38b152a commit 302cd79

File tree

19 files changed

+522
-206
lines changed

19 files changed

+522
-206
lines changed

packages/snaps-controllers/src/snaps/SnapController.test.tsx

Lines changed: 150 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4512,8 +4512,10 @@ describe('SnapController', () => {
45124512

45134513
snapController.destroy();
45144514
});
4515+
});
45154516

4516-
it('returns the value when `onAssetsConversion` returns a valid response with market data', async () => {
4517+
describe('onAssetsMarketData', () => {
4518+
it('throws if `onAssetsMarketData` handler returns an invalid response', async () => {
45174519
const rootMessenger = getControllerMessenger();
45184520
const messenger = getSnapControllerMessenger(rootMessenger);
45194521
const snapController = getSnapController(
@@ -4552,19 +4554,147 @@ describe('SnapController', () => {
45524554
'ExecutionService:handleRpcRequest',
45534555
async () =>
45544556
Promise.resolve({
4555-
conversionRates: {
4557+
marketData: { foo: {} },
4558+
}),
4559+
);
4560+
4561+
await expect(
4562+
snapController.handleRequest({
4563+
snapId: MOCK_SNAP_ID,
4564+
origin: METAMASK_ORIGIN,
4565+
handler: HandlerType.OnAssetsMarketData,
4566+
request: {
4567+
jsonrpc: '2.0',
4568+
method: ' ',
4569+
params: {},
4570+
id: 1,
4571+
},
4572+
}),
4573+
).rejects.toThrow(
4574+
`Assertion failed: At path: marketData.foo -- Expected a value of type \`CaipAssetType\`, but received: \`"foo"\`.`,
4575+
);
4576+
4577+
snapController.destroy();
4578+
});
4579+
4580+
it('filters out assets that are out of scope for `onAssetsMarketData`', async () => {
4581+
const rootMessenger = getControllerMessenger();
4582+
const messenger = getSnapControllerMessenger(rootMessenger);
4583+
const snapController = getSnapController(
4584+
getSnapControllerOptions({
4585+
messenger,
4586+
state: {
4587+
snaps: getPersistedSnapsState(),
4588+
},
4589+
}),
4590+
);
4591+
4592+
rootMessenger.registerActionHandler(
4593+
'PermissionController:getPermissions',
4594+
() => ({
4595+
[SnapEndowments.Assets]: {
4596+
caveats: [
4597+
{
4598+
type: SnapCaveatType.ChainIds,
4599+
value: ['bip122:000000000019d6689c085ae165831e93'],
4600+
},
4601+
],
4602+
date: 1664187844588,
4603+
id: 'izn0WGUO8cvq_jqvLQuQP',
4604+
invoker: MOCK_SNAP_ID,
4605+
parentCapability: SnapEndowments.Assets,
4606+
},
4607+
}),
4608+
);
4609+
4610+
rootMessenger.registerActionHandler(
4611+
'SubjectMetadataController:getSubjectMetadata',
4612+
() => MOCK_SNAP_SUBJECT_METADATA,
4613+
);
4614+
4615+
rootMessenger.registerActionHandler(
4616+
'ExecutionService:handleRpcRequest',
4617+
async () =>
4618+
Promise.resolve({
4619+
marketData: {
4620+
'bip122:000000000019d6689c085ae165831e93/slip44:0': {
4621+
'eip155:1/slip44:60': {
4622+
fungible: true,
4623+
},
4624+
},
4625+
},
4626+
}),
4627+
);
4628+
4629+
expect(
4630+
await snapController.handleRequest({
4631+
snapId: MOCK_SNAP_ID,
4632+
origin: METAMASK_ORIGIN,
4633+
handler: HandlerType.OnAssetsMarketData,
4634+
request: {
4635+
jsonrpc: '2.0',
4636+
method: ' ',
4637+
params: {
4638+
assets: [
4639+
{
4640+
asset: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
4641+
unit: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
4642+
},
4643+
],
4644+
},
4645+
id: 1,
4646+
},
4647+
}),
4648+
).toStrictEqual({ marketData: {} });
4649+
4650+
snapController.destroy();
4651+
});
4652+
4653+
it('returns the value when `onAssetsMarketData` returns a valid response', async () => {
4654+
const rootMessenger = getControllerMessenger();
4655+
const messenger = getSnapControllerMessenger(rootMessenger);
4656+
const snapController = getSnapController(
4657+
getSnapControllerOptions({
4658+
messenger,
4659+
state: {
4660+
snaps: getPersistedSnapsState(),
4661+
},
4662+
}),
4663+
);
4664+
4665+
rootMessenger.registerActionHandler(
4666+
'PermissionController:getPermissions',
4667+
() => ({
4668+
[SnapEndowments.Assets]: {
4669+
caveats: [
4670+
{
4671+
type: SnapCaveatType.ChainIds,
4672+
value: ['bip122:000000000019d6689c085ae165831e93'],
4673+
},
4674+
],
4675+
date: 1664187844588,
4676+
id: 'izn0WGUO8cvq_jqvLQuQP',
4677+
invoker: MOCK_SNAP_ID,
4678+
parentCapability: SnapEndowments.Assets,
4679+
},
4680+
}),
4681+
);
4682+
4683+
rootMessenger.registerActionHandler(
4684+
'SubjectMetadataController:getSubjectMetadata',
4685+
() => MOCK_SNAP_SUBJECT_METADATA,
4686+
);
4687+
4688+
rootMessenger.registerActionHandler(
4689+
'ExecutionService:handleRpcRequest',
4690+
async () =>
4691+
Promise.resolve({
4692+
marketData: {
45564693
'bip122:000000000019d6689c085ae165831e93/slip44:0': {
45574694
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
4558-
rate: '400',
4559-
conversionTime: 1737548790,
4560-
marketData: {
4561-
marketCap: '123',
4562-
totalVolume: '123',
4563-
circulatingSupply: '123',
4564-
allTimeHigh: '123',
4565-
allTimeLow: '123',
4566-
pricePercentChange: { all: 1.23 },
4567-
},
4695+
fungible: true,
4696+
marketCap: '10000',
4697+
totalVolume: '100000000',
45684698
},
45694699
},
45704700
},
@@ -4575,36 +4705,28 @@ describe('SnapController', () => {
45754705
await snapController.handleRequest({
45764706
snapId: MOCK_SNAP_ID,
45774707
origin: METAMASK_ORIGIN,
4578-
handler: HandlerType.OnAssetsConversion,
4708+
handler: HandlerType.OnAssetsMarketData,
45794709
request: {
45804710
jsonrpc: '2.0',
45814711
method: ' ',
45824712
params: {
4583-
conversions: [
4713+
assets: [
45844714
{
4585-
from: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
4586-
to: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
4715+
asset: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
4716+
unit: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
45874717
},
45884718
],
4589-
includeMarketData: true,
45904719
},
45914720
id: 1,
45924721
},
45934722
}),
45944723
).toStrictEqual({
4595-
conversionRates: {
4724+
marketData: {
45964725
'bip122:000000000019d6689c085ae165831e93/slip44:0': {
45974726
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
4598-
rate: '400',
4599-
conversionTime: 1737548790,
4600-
marketData: {
4601-
marketCap: '123',
4602-
totalVolume: '123',
4603-
circulatingSupply: '123',
4604-
allTimeHigh: '123',
4605-
allTimeLow: '123',
4606-
pricePercentChange: { all: 1.23 },
4607-
},
4727+
fungible: true,
4728+
marketCap: '10000',
4729+
totalVolume: '100000000',
46084730
},
46094731
},
46104732
},

packages/snaps-controllers/src/snaps/SnapController.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ import type {
5555
OnAssetsConversionArguments,
5656
AssetConversion,
5757
OnAssetsLookupArguments,
58+
OnAssetsMarketDataArguments,
59+
FungibleAssetMarketData,
60+
OnAssetsMarketDataResponse,
5861
} from '@metamask/snaps-sdk';
5962
import {
6063
AuxiliaryFileEncoding,
@@ -105,6 +108,7 @@ import {
105108
isValidUrl,
106109
OnAssetHistoricalPriceResponseStruct,
107110
OnAssetsConversionResponseStruct,
111+
OnAssetsMarketDataResponseStruct,
108112
} from '@metamask/snaps-utils';
109113
import type {
110114
Json,
@@ -3780,6 +3784,14 @@ export class SnapController extends BaseController<
37803784
},
37813785
result as OnAssetsConversionResponse,
37823786
);
3787+
3788+
case HandlerType.OnAssetsMarketData:
3789+
// We can cast since the request and result have already been validated.
3790+
return this.#transformOnAssetsMarketDataResult(
3791+
request as { params: OnAssetsMarketDataArguments },
3792+
result as OnAssetsMarketDataResponse,
3793+
);
3794+
37833795
default:
37843796
return result;
37853797
}
@@ -3864,6 +3876,38 @@ export class SnapController extends BaseController<
38643876
return { conversionRates: filteredConversionRates };
38653877
}
38663878

3879+
/**
3880+
* Transforms an RPC response coming from the `onAssetsMarketData` handler.
3881+
*
3882+
* This filters out responses that are out of scope for the Snap based on
3883+
* the incoming request.
3884+
*
3885+
* @param request - The request that returned the result.
3886+
* @param request.params - The parameters for the request.
3887+
* @param result - The result.
3888+
* @param result.marketData - The market data returned by the Snap.
3889+
* @returns The transformed result.
3890+
*/
3891+
#transformOnAssetsMarketDataResult(
3892+
{ params: requestedParams }: { params: OnAssetsMarketDataArguments },
3893+
{ marketData }: OnAssetsMarketDataResponse,
3894+
) {
3895+
const { assets: requestedAssets } = requestedParams;
3896+
3897+
const filteredMarketData = requestedAssets.reduce<
3898+
Record<CaipAssetType, Record<CaipAssetType, FungibleAssetMarketData>>
3899+
>((accumulator, assets) => {
3900+
const result = marketData[assets.asset]?.[assets.unit];
3901+
// Only include rates that were actually requested.
3902+
if (result) {
3903+
accumulator[assets.asset] ??= {};
3904+
accumulator[assets.asset][assets.unit] = result;
3905+
}
3906+
return accumulator;
3907+
}, {});
3908+
return { marketData: filteredMarketData };
3909+
}
3910+
38673911
/**
38683912
* Transforms a JSON-RPC request before sending it to the Snap, if required for a given handler.
38693913
*
@@ -3961,6 +4005,9 @@ export class SnapController extends BaseController<
39614005
case HandlerType.OnAssetHistoricalPrice:
39624006
assertStruct(result, OnAssetHistoricalPriceResponseStruct);
39634007
break;
4008+
case HandlerType.OnAssetsMarketData:
4009+
assertStruct(result, OnAssetsMarketDataResponseStruct);
4010+
break;
39644011
default:
39654012
break;
39664013
}

packages/snaps-controllers/src/snaps/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ export const CLIENT_ONLY_HANDLERS = Object.freeze([
4343
HandlerType.OnAssetsLookup,
4444
HandlerType.OnAssetsConversion,
4545
HandlerType.OnAssetHistoricalPrice,
46+
HandlerType.OnAssetsMarketData,
4647
HandlerType.OnWebSocketEvent,
4748
]);

packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,9 +1608,9 @@ describe('BaseSnapExecutor', () => {
16081608
});
16091609
});
16101610

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

16161616
const executor = new TestSnapExecutor();
@@ -1628,19 +1628,18 @@ describe('BaseSnapExecutor', () => {
16281628
method: 'snapRpc',
16291629
params: [
16301630
MOCK_SNAP_ID,
1631-
HandlerType.OnAssetsConversion,
1631+
HandlerType.OnAssetsMarketData,
16321632
MOCK_ORIGIN,
16331633
{
16341634
jsonrpc: '2.0',
16351635
method: '',
16361636
params: {
1637-
conversions: [
1637+
assets: [
16381638
{
1639-
from: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
1640-
to: 'eip155:1/slip44:60',
1639+
asset: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
1640+
unit: 'eip155:1/slip44:60',
16411641
},
16421642
],
1643-
includeMarketData: true,
16441643
},
16451644
},
16461645
],
@@ -1649,7 +1648,7 @@ describe('BaseSnapExecutor', () => {
16491648
expect(await executor.readCommand()).toStrictEqual({
16501649
id: 2,
16511650
jsonrpc: '2.0',
1652-
result: { conversionRates: {} },
1651+
result: { marketData: {} },
16531652
});
16541653
});
16551654

packages/snaps-execution-environments/src/common/commands.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
assertIsOnProtocolRequestArguments,
2121
assertIsOnAssetHistoricalPriceRequestArguments,
2222
assertIsOnWebSocketEventArguments,
23+
assertIsOnAssetsMarketDataRequestArguments,
2324
} from './validation';
2425

2526
export type CommandMethodsMapping = {
@@ -75,9 +76,16 @@ export function getHandlerArguments(
7576
}
7677
case HandlerType.OnAssetsConversion: {
7778
assertIsOnAssetsConversionRequestArguments(request.params);
78-
const { conversions, includeMarketData } = request.params;
79-
return { conversions, includeMarketData };
79+
const { conversions } = request.params;
80+
return { conversions };
8081
}
82+
83+
case HandlerType.OnAssetsMarketData: {
84+
assertIsOnAssetsMarketDataRequestArguments(request.params);
85+
const { assets } = request.params;
86+
return { assets };
87+
}
88+
8189
case HandlerType.OnNameLookup: {
8290
assertIsOnNameLookupRequestArguments(request.params);
8391

0 commit comments

Comments
 (0)