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

## [Unreleased]

### Added

- Read asset metadata from AssetsController when feature flag `enableAssetMigration` and local flag `enableAssetMigration` is on ([#493](https://github.com/MetaMask/internal-snaps/pull/493))
- `enableAssetMigration` set to off.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is the goal of this sub-point to specify that by default the current changes make it off?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, we only switch to on, when full set of code ready to use controller
becoz in stellar we keep shipping new feature, i dont wanna this block / change any behavior when the migration is not ready


## [1.3.0]

### Added
Expand Down
2 changes: 2 additions & 0 deletions packages/stellar-wallet-snap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"@metamask/key-tree": "^10.1.1",
"@metamask/keyring-api": "^24.1.0",
"@metamask/keyring-snap-sdk": "^10.0.0",
"@metamask/messenger": "^2.0.0",
"@metamask/remote-feature-flag-controller": "^7.0.0",
"@metamask/snap-networks-utils": "^3.0.0",
"@metamask/snaps-cli": "^8.4.1",
"@metamask/snaps-jest": "^10.2.1",
Expand Down
9 changes: 9 additions & 0 deletions packages/stellar-wallet-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@
},
"endowment:assets": {
"scopes": ["stellar:pubnet"]
},
"endowment:messenger": {
"actions": [
"RemoteFeatureFlagController:getState",
"AssetsController:getAccountAssetByID",
"AssetsController:getAccountAssetsByIDs",
"AssetsController:getAccountAssetsByScope",
"AssetsController:getAssetMetadata"
]
}
},
"platformVersion": "12.0.1",
Expand Down
41 changes: 41 additions & 0 deletions packages/stellar-wallet-snap/src/api/core-messenger.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: just like Tron's, very nice 👏

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {
AssetsControllerGetAccountAssetByIDAction,
AssetsControllerGetAccountAssetsByIDsAction,
AssetsControllerGetAccountAssetsByScopeAction,
AssetsControllerGetAssetMetadataAction,
} from '@metamask/assets-controller';
import type { Messenger } from '@metamask/messenger';
import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';
import type { AsyncMessenger } from '@metamask/snaps-sdk';

/**
* Namespace for this Snap's Core messenger endowment.
*/
export const STELLAR_WALLET_SNAP_MESSENGER_NAMESPACE =
'StellarWalletSnap' as const;

export type CoreMessengerActions =
| RemoteFeatureFlagControllerGetStateAction
| AssetsControllerGetAccountAssetByIDAction
| AssetsControllerGetAccountAssetsByIDsAction
| AssetsControllerGetAccountAssetsByScopeAction
| AssetsControllerGetAssetMetadataAction;

/**
* Messenger type passed to `getMessenger` for Core controller actions.
*/
export type CoreMessenger = Messenger<
typeof STELLAR_WALLET_SNAP_MESSENGER_NAMESPACE,
CoreMessengerActions
>;

/**
* Async messenger returned by `getMessenger` for Core controller actions
* available to this Snap via `endowment:messenger`.
*/
export type CoreMessengerClient = AsyncMessenger<CoreMessenger>;

/**
* Narrow dependency for services that only need to invoke Core actions.
*/
export type CoreMessengerCaller = Pick<CoreMessengerClient, 'call'>;
2 changes: 1 addition & 1 deletion packages/stellar-wallet-snap/src/api/integer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ValidStellarAmountStruct = refine(
if (
(decimalPlaces && decimalPlaces > STELLAR_DECIMAL_PLACES) ||
// > Max value
toSmallestUnit(amount).gt(new BigNumber(MAX_INT64).toString())
toSmallestUnit(amount).gt(MAX_INT64)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not related to this PR, but just a small refactor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very worthwhile!

) {
return 'Invalid amount';
}
Expand Down
2 changes: 2 additions & 0 deletions packages/stellar-wallet-snap/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const FULL_ENVIRONMENT = {
},
},
selectedNetwork: KnownCaip2ChainId.Mainnet,
enableAssetMigration: false,
transaction: {
timeout: '120',
pollingAttempts: '5',
Expand Down Expand Up @@ -125,6 +126,7 @@ describe('ConfigProvider', () => {
},
});
expect(config.selectedNetwork).toBe(KnownCaip2ChainId.Mainnet);
expect(config.enableAssetMigration).toBe(false);
});

it.each([
Expand Down
4 changes: 3 additions & 1 deletion packages/stellar-wallet-snap/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
UrlStruct,
} from '@metamask/snap-networks-utils';
import type { Infer } from '@metamask/superstruct';
import { enums, object } from '@metamask/superstruct';
import { boolean, enums, object } from '@metamask/superstruct';

/* eslint-disable no-restricted-globals */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
Expand Down Expand Up @@ -49,6 +49,7 @@ export const ConfigStruct = object({
[KnownCaip2ChainId.Mainnet]: networkConfigStruct,
[KnownCaip2ChainId.Testnet]: networkConfigStruct,
}),
enableAssetMigration: boolean(),
selectedNetwork: enums(getSupportedScopes()),
transaction: object({
timeout: parseIntegerStruct(100, 180),
Expand Down Expand Up @@ -147,6 +148,7 @@ const ENVIRONMENT = {
},
},
selectedNetwork: KnownCaip2ChainId.Mainnet,
enableAssetMigration: false,
transaction: {
timeout: process.env.STELLAR_TRANSACTION_TIMEOUT,
pollingAttempts: process.env.STELLAR_TRANSACTION_POLLING_ATTEMPTS,
Expand Down
30 changes: 30 additions & 0 deletions packages/stellar-wallet-snap/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import {
AnalyticsService,
State,
AssetsProvider,
RemoteFeatureFlagsProvider,
InMemoryCache,
} from '@metamask/snap-networks-utils';
import type {
AssetsProviderMessenger,
RemoteFeatureFlagsProviderMessenger,
} from '@metamask/snap-networks-utils';
import { getMessenger } from '@metamask/snaps-sdk';
import { assert, object } from '@metamask/superstruct';

import type { CoreMessenger } from './api/core-messenger';
import { AppConfig } from './config';
import { KeyringHandler, CronjobHandler, UserInputHandler } from './handlers';
import { AccountResolver } from './handlers/accountResolver';
Expand Down Expand Up @@ -44,6 +52,7 @@ import {
AssetMetadataRepository,
AssetMetadataService,
} from './services/asset-metadata';
import { AssetsService, CoreAssetsAdapter } from './services/assets';
import { NetworkService } from './services/network';
import {
OnChainAccountRepository,
Expand Down Expand Up @@ -86,10 +95,31 @@ const analyticsService = new AnalyticsService({
const appCache = new InMemoryCache(noOpLogger);
const networkService = new NetworkService({ logger, cache: appCache });

const coreMessenger = getMessenger<CoreMessenger>();
const remoteFeatureFlagsProvider = new RemoteFeatureFlagsProvider({
messenger: coreMessenger as RemoteFeatureFlagsProviderMessenger,
});
const assetsProvider = new AssetsProvider({
messenger: coreMessenger as AssetsProviderMessenger,
});
const coreAssetsAdapter = new CoreAssetsAdapter({
logger,
getAccountAssetByID: assetsProvider.getAccountAssetByID.bind(assetsProvider),
getAccountAssetsByIDs:
assetsProvider.getAccountAssetsByIDs.bind(assetsProvider),
getAccountAssetsByScope:
assetsProvider.getAccountAssetsByScope.bind(assetsProvider),
getAssetMetadata: assetsProvider.getAssetMetadata.bind(assetsProvider),
});
const assetsService = new AssetsService({
coreAdapter: coreAssetsAdapter,
remoteFeatureFlagsProvider,
});
const assetMetadataService = new AssetMetadataService({
networkService,
assetMetadataRepository,
logger,
assetsService,
});

const transactionBuilder = new TransactionBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { buildUrl } from '@metamask/snap-networks-utils';
import { AssetType, KnownCaip2ChainId } from '../../api';
import type { KnownCaip19AssetId } from '../../api';
import { getSlip44AssetId, logger } from '../../utils';
import { createMockAssetsService } from '../assets/__mocks__/assetsService.fixtures';
import type { NetworkService } from '../network';
import { USDC_SEP41 } from './__mocks__/assets.fixtures';
import type { StellarAssetMetadata } from './api';
import type { AssetMetadataRepository } from './AssetMetadataRepository';
import { AssetMetadataService } from './AssetMetadataService';
import type { TokenMetadata } from './token-api/api';
import { TokenApiClient } from './token-api/TokenApiClient';
import { toStellarAssetMetadata } from './utils';

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

const { service: assetsService } = createMockAssetsService();
const service = new AssetMetadataService({
networkService: network,
assetMetadataRepository: repo,
logger,
assetsService,
});

return {
service,
assetsService,
getByAssetIds: repo.getByAssetIds as jest.MockedFunction<
AssetMetadataRepository['getByAssetIds']
>,
Expand Down Expand Up @@ -128,7 +134,7 @@ describe('AssetMetadataService', () => {
const result = await service.resolve(slipId);

expect(result.assetId).toBe(slipId);
expect(getByAssetIds).toHaveBeenCalledWith([]);
expect(getByAssetIds).not.toHaveBeenCalled();
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
});

Expand Down Expand Up @@ -160,20 +166,58 @@ describe('AssetMetadataService', () => {
it('returns cached mainnet classic asset without calling token API', async () => {
const classicId = MAINNET_CLASSIC_USDC;
const cached = createCachedRow(classicId, KnownCaip2ChainId.Mainnet);
const { service, getByAssetIds, saveMany } = createService({
const { service, assetsService, getByAssetIds, saveMany } = createService({
repo: {
getByAssetIds: jest.fn().mockResolvedValue([cached]),
},
});
jest.spyOn(assetsService, 'isMigrationEnabled').mockResolvedValue(false);
const getAssetMetadata = jest
.spyOn(assetsService, 'getAssetMetadata')
.mockResolvedValue({
symbol: 'USDC',
decimals: 7,
name: 'USD Coin',
});

const result = await service.resolve(classicId);

expect(result).toStrictEqual(cached);
expect(getAssetMetadata).not.toHaveBeenCalled();
expect(getByAssetIds).toHaveBeenCalledWith([classicId]);
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
expect(saveMany).not.toHaveBeenCalled();
});

it('resolves requested SEP-41 metadata and persists it', async () => {
const tokenRow: TokenMetadata = {
assetId: USDC_SEP41,
decimals: 7,
name: 'USDC',
symbol: 'USDC',
};
const { service, saveMany } = createService({});
mockGetAssetsByAssetIds.mockResolvedValueOnce([tokenRow]);

const result = await service.fetchAssetsByAssetIds([USDC_SEP41]);

expect(result).toStrictEqual([
expect.objectContaining({
assetId: USDC_SEP41,
name: 'USDC',
symbol: 'USDC',
assetType: AssetType.Sep41,
}),
]);
expect(mockGetAssetsByAssetIds).toHaveBeenCalledWith([USDC_SEP41]);
expect(saveMany).toHaveBeenCalledWith([
expect.objectContaining({
assetId: USDC_SEP41,
symbol: 'USDC',
}),
]);
});

it('delegates getPersistedSep41AssetsMetadata to repository', async () => {
const sepRows: StellarAssetMetadata[] = [];
const getByAssetType = jest.fn().mockResolvedValue(sepRows);
Expand Down Expand Up @@ -362,4 +406,90 @@ describe('AssetMetadataService', () => {
expect(result).toBe(catalog);
});
});

it('persists catalog during synchronize when assets migration is on', async () => {
const sep41AssetId =
'stellar:pubnet/sep41:CAUP7NFABXE5TJRL3FKTPMWRLC7IAXYDCTHQRFSCLR5TMGKHOOQO772J' as KnownCaip19AssetId;
const { service, assetsService, saveMany } = createService({});
jest.spyOn(assetsService, 'isMigrationEnabled').mockResolvedValue(true);
mockGetAssetsByChainId.mockResolvedValueOnce({
data: [
{
assetId: sep41AssetId,
decimals: 7,
name: 'Token A',
symbol: 'TA',
},
],
count: 1,
totalCount: 1,
});

await service.synchronize(KnownCaip2ChainId.Mainnet);

expect(mockGetAssetsByChainId).toHaveBeenCalledWith(
KnownCaip2ChainId.Mainnet,
);
expect(saveMany).toHaveBeenCalledWith(
[
expect.objectContaining({
assetId: sep41AssetId,
name: 'Token A',
symbol: 'TA',
}),
],
true,
);
});

it('maps AssetsController catalog metadata into StellarAssetMetadata without reading snap state', async () => {
const classicId = MAINNET_CLASSIC_USDC;
const { service, assetsService, getByAssetIds, saveMany } = createService(
{},
);
jest.spyOn(assetsService, 'isMigrationEnabled').mockResolvedValue(true);
const getAssetMetadata = jest
.spyOn(assetsService, 'getAssetMetadata')
.mockResolvedValue({
symbol: 'USDC',
decimals: 7,
name: 'USD Coin',
});

const result = await service.resolve(classicId);

expect(result).toStrictEqual(
toStellarAssetMetadata({
assetId: classicId,
decimals: 7,
symbol: 'USDC',
name: 'USD Coin',
}),
);
expect(getAssetMetadata).toHaveBeenCalledWith(classicId);
expect(getByAssetIds).not.toHaveBeenCalled();
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
expect(saveMany).not.toHaveBeenCalled();
});

it('falls back to snap catalog when AssetsController misses', async () => {
const classicId = MAINNET_CLASSIC_USDC;
const cached = createCachedRow(classicId, KnownCaip2ChainId.Mainnet);
const { service, assetsService, getByAssetIds } = createService({
repo: {
getByAssetIds: jest.fn().mockResolvedValue([cached]),
},
});
jest.spyOn(assetsService, 'isMigrationEnabled').mockResolvedValue(true);
const getAssetMetadata = jest
.spyOn(assetsService, 'getAssetMetadata')
.mockResolvedValue(null);

const result = await service.resolve(classicId);

expect(result).toStrictEqual(cached);
expect(getAssetMetadata).toHaveBeenCalledWith(classicId);
expect(getByAssetIds).toHaveBeenCalledWith([classicId]);
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
});
});
Loading
Loading