Skip to content

Commit 5dd2c2a

Browse files
chore(stellar): read asset metadata from controller and gated by FF and local config (#493)
## Explanation This PR does - Adds gated AssetsController metadata reads to the Stellar Snap, retaining snap-state fallback. <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/internal-snaps/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/internal-snaps/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them
1 parent d51bdc0 commit 5dd2c2a

17 files changed

Lines changed: 439 additions & 17 deletions

‎packages/stellar-wallet-snap/CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Read asset metadata from AssetsController when feature flag `enableAssetMigration` and local flag `enableAssetMigration` is on ([#493](https://github.com/MetaMask/internal-snaps/pull/493))
13+
- `enableAssetMigration` set to off.
14+
1015
## [1.3.0]
1116

1217
### Added

‎packages/stellar-wallet-snap/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
"@metamask/key-tree": "^10.1.1",
5454
"@metamask/keyring-api": "^24.1.0",
5555
"@metamask/keyring-snap-sdk": "^10.0.0",
56+
"@metamask/messenger": "^2.0.0",
57+
"@metamask/remote-feature-flag-controller": "^7.0.0",
5658
"@metamask/snap-networks-utils": "^3.0.0",
5759
"@metamask/snaps-cli": "^8.4.1",
5860
"@metamask/snaps-jest": "^10.2.1",

‎packages/stellar-wallet-snap/snap.manifest.json‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@
6161
},
6262
"endowment:assets": {
6363
"scopes": ["stellar:pubnet"]
64+
},
65+
"endowment:messenger": {
66+
"actions": [
67+
"RemoteFeatureFlagController:getState",
68+
"AssetsController:getAccountAssetByID",
69+
"AssetsController:getAccountAssetsByIDs",
70+
"AssetsController:getAccountAssetsByScope",
71+
"AssetsController:getAssetMetadata"
72+
]
6473
}
6574
},
6675
"platformVersion": "12.0.1",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type {
2+
AssetsControllerGetAccountAssetByIDAction,
3+
AssetsControllerGetAccountAssetsByIDsAction,
4+
AssetsControllerGetAccountAssetsByScopeAction,
5+
AssetsControllerGetAssetMetadataAction,
6+
} from '@metamask/assets-controller';
7+
import type { Messenger } from '@metamask/messenger';
8+
import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';
9+
import type { AsyncMessenger } from '@metamask/snaps-sdk';
10+
11+
/**
12+
* Namespace for this Snap's Core messenger endowment.
13+
*/
14+
export const STELLAR_WALLET_SNAP_MESSENGER_NAMESPACE =
15+
'StellarWalletSnap' as const;
16+
17+
export type CoreMessengerActions =
18+
| RemoteFeatureFlagControllerGetStateAction
19+
| AssetsControllerGetAccountAssetByIDAction
20+
| AssetsControllerGetAccountAssetsByIDsAction
21+
| AssetsControllerGetAccountAssetsByScopeAction
22+
| AssetsControllerGetAssetMetadataAction;
23+
24+
/**
25+
* Messenger type passed to `getMessenger` for Core controller actions.
26+
*/
27+
export type CoreMessenger = Messenger<
28+
typeof STELLAR_WALLET_SNAP_MESSENGER_NAMESPACE,
29+
CoreMessengerActions
30+
>;
31+
32+
/**
33+
* Async messenger returned by `getMessenger` for Core controller actions
34+
* available to this Snap via `endowment:messenger`.
35+
*/
36+
export type CoreMessengerClient = AsyncMessenger<CoreMessenger>;
37+
38+
/**
39+
* Narrow dependency for services that only need to invoke Core actions.
40+
*/
41+
export type CoreMessengerCaller = Pick<CoreMessengerClient, 'call'>;

‎packages/stellar-wallet-snap/src/api/integer.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const ValidStellarAmountStruct = refine(
4444
if (
4545
(decimalPlaces && decimalPlaces > STELLAR_DECIMAL_PLACES) ||
4646
// > Max value
47-
toSmallestUnit(amount).gt(new BigNumber(MAX_INT64).toString())
47+
toSmallestUnit(amount).gt(MAX_INT64)
4848
) {
4949
return 'Invalid amount';
5050
}

‎packages/stellar-wallet-snap/src/config.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const FULL_ENVIRONMENT = {
2424
},
2525
},
2626
selectedNetwork: KnownCaip2ChainId.Mainnet,
27+
enableAssetMigration: false,
2728
transaction: {
2829
timeout: '120',
2930
pollingAttempts: '5',
@@ -125,6 +126,7 @@ describe('ConfigProvider', () => {
125126
},
126127
});
127128
expect(config.selectedNetwork).toBe(KnownCaip2ChainId.Mainnet);
129+
expect(config.enableAssetMigration).toBe(false);
128130
});
129131

130132
it.each([

‎packages/stellar-wallet-snap/src/config.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
UrlStruct,
77
} from '@metamask/snap-networks-utils';
88
import type { Infer } from '@metamask/superstruct';
9-
import { enums, object } from '@metamask/superstruct';
9+
import { boolean, enums, object } from '@metamask/superstruct';
1010

1111
/* eslint-disable no-restricted-globals */
1212
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
@@ -49,6 +49,7 @@ export const ConfigStruct = object({
4949
[KnownCaip2ChainId.Mainnet]: networkConfigStruct,
5050
[KnownCaip2ChainId.Testnet]: networkConfigStruct,
5151
}),
52+
enableAssetMigration: boolean(),
5253
selectedNetwork: enums(getSupportedScopes()),
5354
transaction: object({
5455
timeout: parseIntegerStruct(100, 180),
@@ -147,6 +148,7 @@ const ENVIRONMENT = {
147148
},
148149
},
149150
selectedNetwork: KnownCaip2ChainId.Mainnet,
151+
enableAssetMigration: false,
150152
transaction: {
151153
timeout: process.env.STELLAR_TRANSACTION_TIMEOUT,
152154
pollingAttempts: process.env.STELLAR_TRANSACTION_POLLING_ATTEMPTS,

‎packages/stellar-wallet-snap/src/context.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import {
22
AnalyticsService,
33
State,
4+
AssetsProvider,
5+
RemoteFeatureFlagsProvider,
46
InMemoryCache,
57
} from '@metamask/snap-networks-utils';
8+
import type {
9+
AssetsProviderMessenger,
10+
RemoteFeatureFlagsProviderMessenger,
11+
} from '@metamask/snap-networks-utils';
12+
import { getMessenger } from '@metamask/snaps-sdk';
613
import { assert, object } from '@metamask/superstruct';
714

15+
import type { CoreMessenger } from './api/core-messenger';
816
import { AppConfig } from './config';
917
import { KeyringHandler, CronjobHandler, UserInputHandler } from './handlers';
1018
import { AccountResolver } from './handlers/accountResolver';
@@ -44,6 +52,7 @@ import {
4452
AssetMetadataRepository,
4553
AssetMetadataService,
4654
} from './services/asset-metadata';
55+
import { AssetsService, CoreAssetsAdapter } from './services/assets';
4756
import { NetworkService } from './services/network';
4857
import {
4958
OnChainAccountRepository,
@@ -86,10 +95,31 @@ const analyticsService = new AnalyticsService({
8695
const appCache = new InMemoryCache(noOpLogger);
8796
const networkService = new NetworkService({ logger, cache: appCache });
8897

98+
const coreMessenger = getMessenger<CoreMessenger>();
99+
const remoteFeatureFlagsProvider = new RemoteFeatureFlagsProvider({
100+
messenger: coreMessenger as RemoteFeatureFlagsProviderMessenger,
101+
});
102+
const assetsProvider = new AssetsProvider({
103+
messenger: coreMessenger as AssetsProviderMessenger,
104+
});
105+
const coreAssetsAdapter = new CoreAssetsAdapter({
106+
logger,
107+
getAccountAssetByID: assetsProvider.getAccountAssetByID.bind(assetsProvider),
108+
getAccountAssetsByIDs:
109+
assetsProvider.getAccountAssetsByIDs.bind(assetsProvider),
110+
getAccountAssetsByScope:
111+
assetsProvider.getAccountAssetsByScope.bind(assetsProvider),
112+
getAssetMetadata: assetsProvider.getAssetMetadata.bind(assetsProvider),
113+
});
114+
const assetsService = new AssetsService({
115+
coreAdapter: coreAssetsAdapter,
116+
remoteFeatureFlagsProvider,
117+
});
89118
const assetMetadataService = new AssetMetadataService({
90119
networkService,
91120
assetMetadataRepository,
92121
logger,
122+
assetsService,
93123
});
94124

95125
const transactionBuilder = new TransactionBuilder();

‎packages/stellar-wallet-snap/src/services/asset-metadata/AssetMetadataService.test.ts‎

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import { buildUrl } from '@metamask/snap-networks-utils';
33
import { AssetType, KnownCaip2ChainId } from '../../api';
44
import type { KnownCaip19AssetId } from '../../api';
55
import { getSlip44AssetId, logger } from '../../utils';
6+
import { createMockAssetsService } from '../assets/__mocks__/assetsService.fixtures';
67
import type { NetworkService } from '../network';
8+
import { USDC_SEP41 } from './__mocks__/assets.fixtures';
79
import type { StellarAssetMetadata } from './api';
810
import type { AssetMetadataRepository } from './AssetMetadataRepository';
911
import { AssetMetadataService } from './AssetMetadataService';
1012
import type { TokenMetadata } from './token-api/api';
1113
import { TokenApiClient } from './token-api/TokenApiClient';
14+
import { toStellarAssetMetadata } from './utils';
1215

1316
/** Mainnet classic USDC (matches CAIP-19 pattern used across Stellar fixtures). */
1417
const MAINNET_CLASSIC_USDC =
@@ -88,14 +91,17 @@ function createService(deps: {
8891
getAssetsByChainId: mockGetAssetsByChainId,
8992
}));
9093

94+
const { service: assetsService } = createMockAssetsService();
9195
const service = new AssetMetadataService({
9296
networkService: network,
9397
assetMetadataRepository: repo,
9498
logger,
99+
assetsService,
95100
});
96101

97102
return {
98103
service,
104+
assetsService,
99105
getByAssetIds: repo.getByAssetIds as jest.MockedFunction<
100106
AssetMetadataRepository['getByAssetIds']
101107
>,
@@ -128,7 +134,7 @@ describe('AssetMetadataService', () => {
128134
const result = await service.resolve(slipId);
129135

130136
expect(result.assetId).toBe(slipId);
131-
expect(getByAssetIds).toHaveBeenCalledWith([]);
137+
expect(getByAssetIds).not.toHaveBeenCalled();
132138
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
133139
});
134140

@@ -160,20 +166,58 @@ describe('AssetMetadataService', () => {
160166
it('returns cached mainnet classic asset without calling token API', async () => {
161167
const classicId = MAINNET_CLASSIC_USDC;
162168
const cached = createCachedRow(classicId, KnownCaip2ChainId.Mainnet);
163-
const { service, getByAssetIds, saveMany } = createService({
169+
const { service, assetsService, getByAssetIds, saveMany } = createService({
164170
repo: {
165171
getByAssetIds: jest.fn().mockResolvedValue([cached]),
166172
},
167173
});
174+
jest.spyOn(assetsService, 'isMigrationEnabled').mockResolvedValue(false);
175+
const getAssetMetadata = jest
176+
.spyOn(assetsService, 'getAssetMetadata')
177+
.mockResolvedValue({
178+
symbol: 'USDC',
179+
decimals: 7,
180+
name: 'USD Coin',
181+
});
168182

169183
const result = await service.resolve(classicId);
170184

171185
expect(result).toStrictEqual(cached);
186+
expect(getAssetMetadata).not.toHaveBeenCalled();
172187
expect(getByAssetIds).toHaveBeenCalledWith([classicId]);
173188
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
174189
expect(saveMany).not.toHaveBeenCalled();
175190
});
176191

192+
it('resolves requested SEP-41 metadata and persists it', async () => {
193+
const tokenRow: TokenMetadata = {
194+
assetId: USDC_SEP41,
195+
decimals: 7,
196+
name: 'USDC',
197+
symbol: 'USDC',
198+
};
199+
const { service, saveMany } = createService({});
200+
mockGetAssetsByAssetIds.mockResolvedValueOnce([tokenRow]);
201+
202+
const result = await service.fetchAssetsByAssetIds([USDC_SEP41]);
203+
204+
expect(result).toStrictEqual([
205+
expect.objectContaining({
206+
assetId: USDC_SEP41,
207+
name: 'USDC',
208+
symbol: 'USDC',
209+
assetType: AssetType.Sep41,
210+
}),
211+
]);
212+
expect(mockGetAssetsByAssetIds).toHaveBeenCalledWith([USDC_SEP41]);
213+
expect(saveMany).toHaveBeenCalledWith([
214+
expect.objectContaining({
215+
assetId: USDC_SEP41,
216+
symbol: 'USDC',
217+
}),
218+
]);
219+
});
220+
177221
it('delegates getPersistedSep41AssetsMetadata to repository', async () => {
178222
const sepRows: StellarAssetMetadata[] = [];
179223
const getByAssetType = jest.fn().mockResolvedValue(sepRows);
@@ -362,4 +406,90 @@ describe('AssetMetadataService', () => {
362406
expect(result).toBe(catalog);
363407
});
364408
});
409+
410+
it('persists catalog during synchronize when assets migration is on', async () => {
411+
const sep41AssetId =
412+
'stellar:pubnet/sep41:CAUP7NFABXE5TJRL3FKTPMWRLC7IAXYDCTHQRFSCLR5TMGKHOOQO772J' as KnownCaip19AssetId;
413+
const { service, assetsService, saveMany } = createService({});
414+
jest.spyOn(assetsService, 'isMigrationEnabled').mockResolvedValue(true);
415+
mockGetAssetsByChainId.mockResolvedValueOnce({
416+
data: [
417+
{
418+
assetId: sep41AssetId,
419+
decimals: 7,
420+
name: 'Token A',
421+
symbol: 'TA',
422+
},
423+
],
424+
count: 1,
425+
totalCount: 1,
426+
});
427+
428+
await service.synchronize(KnownCaip2ChainId.Mainnet);
429+
430+
expect(mockGetAssetsByChainId).toHaveBeenCalledWith(
431+
KnownCaip2ChainId.Mainnet,
432+
);
433+
expect(saveMany).toHaveBeenCalledWith(
434+
[
435+
expect.objectContaining({
436+
assetId: sep41AssetId,
437+
name: 'Token A',
438+
symbol: 'TA',
439+
}),
440+
],
441+
true,
442+
);
443+
});
444+
445+
it('maps AssetsController catalog metadata into StellarAssetMetadata without reading snap state', async () => {
446+
const classicId = MAINNET_CLASSIC_USDC;
447+
const { service, assetsService, getByAssetIds, saveMany } = createService(
448+
{},
449+
);
450+
jest.spyOn(assetsService, 'isMigrationEnabled').mockResolvedValue(true);
451+
const getAssetMetadata = jest
452+
.spyOn(assetsService, 'getAssetMetadata')
453+
.mockResolvedValue({
454+
symbol: 'USDC',
455+
decimals: 7,
456+
name: 'USD Coin',
457+
});
458+
459+
const result = await service.resolve(classicId);
460+
461+
expect(result).toStrictEqual(
462+
toStellarAssetMetadata({
463+
assetId: classicId,
464+
decimals: 7,
465+
symbol: 'USDC',
466+
name: 'USD Coin',
467+
}),
468+
);
469+
expect(getAssetMetadata).toHaveBeenCalledWith(classicId);
470+
expect(getByAssetIds).not.toHaveBeenCalled();
471+
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
472+
expect(saveMany).not.toHaveBeenCalled();
473+
});
474+
475+
it('falls back to snap catalog when AssetsController misses', async () => {
476+
const classicId = MAINNET_CLASSIC_USDC;
477+
const cached = createCachedRow(classicId, KnownCaip2ChainId.Mainnet);
478+
const { service, assetsService, getByAssetIds } = createService({
479+
repo: {
480+
getByAssetIds: jest.fn().mockResolvedValue([cached]),
481+
},
482+
});
483+
jest.spyOn(assetsService, 'isMigrationEnabled').mockResolvedValue(true);
484+
const getAssetMetadata = jest
485+
.spyOn(assetsService, 'getAssetMetadata')
486+
.mockResolvedValue(null);
487+
488+
const result = await service.resolve(classicId);
489+
490+
expect(result).toStrictEqual(cached);
491+
expect(getAssetMetadata).toHaveBeenCalledWith(classicId);
492+
expect(getByAssetIds).toHaveBeenCalledWith([classicId]);
493+
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
494+
});
365495
});

0 commit comments

Comments
 (0)