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
2 changes: 1 addition & 1 deletion eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@
"count": 1
},
"jest/unbound-method": {
"count": 4
"count": 2
}
},
"packages/stellar-wallet-snap/src/services/asset-metadata/__mocks__/assets.fixtures.ts": {
Expand Down
2 changes: 2 additions & 0 deletions packages/stellar-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **BREAKING:** Stop falling back to Soroban RPC and Horizon for asset metadata ([#505](https://github.com/MetaMask/internal-snaps/pull/505))
- Fetch asset metadata from the local catalog or the mainnet token API.
- **BREAKING:** Return classic keyring balance metadata `limit` in integer stroop string ([#495](https://github.com/MetaMask/internal-snaps/pull/495))
- Keyring method `getAccountBalances` and the Keyring event `AccountBalancesUpdated` now return this `limit` in stroops.

Expand Down
1 change: 0 additions & 1 deletion packages/stellar-wallet-snap/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ const assetsService = new AssetsService({
remoteFeatureFlagsProvider,
});
const assetMetadataService = new AssetMetadataService({
networkService,
assetMetadataRepository,
logger,
assetsService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import type {
import { AssetType, KnownCaip2ChainId } 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 { TESTNET_SEP41_USDC, USDC_SEP41 } from './__mocks__/assets.fixtures';
import type {
ClassicStellarAssetMetadata,
Sep41StellarAssetMetadata,
} from './api';
import type { AssetMetadataRepository } from './AssetMetadataRepository';
import { AssetMetadataService } from './AssetMetadataService';
import { AssetMetadataServiceException } from './exceptions';
import type { TokenMetadata } from './token-api/api';
import { TokenApiClient } from './token-api/TokenApiClient';
import { toStellarAssetMetadata } from './utils';
Expand All @@ -23,6 +23,9 @@ import { toStellarAssetMetadata } from './utils';
const MAINNET_CLASSIC_USDC =
'stellar:pubnet/asset:USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' as KnownCaip19ClassicAssetId;

const TESTNET_CLASSIC_USDC =
'stellar:testnet/asset:USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' as KnownCaip19ClassicAssetId;

jest.mock('../../config', () => ({
AppConfig: {
api: {
Expand Down Expand Up @@ -67,10 +70,7 @@ function createCachedRow(
};
}

function createService(deps: {
repo?: Partial<AssetMetadataRepository>;
network?: Partial<NetworkService>;
}) {
function createService(deps: { repo?: Partial<AssetMetadataRepository> }) {
const defaults = {
getByAssetIds: jest.fn().mockResolvedValue([]),
saveMany: jest.fn().mockResolvedValue(undefined),
Expand All @@ -86,20 +86,13 @@ function createService(deps: {
...deps.repo,
} as unknown as AssetMetadataRepository;

const network = {
getSep41AssetsData: jest.fn().mockResolvedValue([]),
getClassicAssetData: jest.fn(),
...deps.network,
} as unknown as NetworkService;

(TokenApiClient as jest.Mock).mockImplementation(() => ({
getAssetsByAssetIds: mockGetAssetsByAssetIds,
getAssetsByChainId: mockGetAssetsByChainId,
}));

const { service: assetsService } = createMockAssetsService();
const service = new AssetMetadataService({
networkService: network,
assetMetadataRepository: repo,
logger,
assetsService,
Expand All @@ -114,12 +107,6 @@ function createService(deps: {
saveMany: repo.saveMany as jest.MockedFunction<
AssetMetadataRepository['saveMany']
>,
getSep41AssetsData: network.getSep41AssetsData as jest.MockedFunction<
NetworkService['getSep41AssetsData']
>,
getClassicAssetData: network.getClassicAssetData as jest.MockedFunction<
NetworkService['getClassicAssetData']
>,
};
}

Expand All @@ -144,29 +131,76 @@ describe('AssetMetadataService', () => {
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
});

it('loads mainnet classic from Horizon when token API and cache miss', async () => {
const classicId = MAINNET_CLASSIC_USDC;
const rpcRow = {
assetId: classicId,
symbol: 'USDC',
decimals: 7,
name: 'USD Coin',
};
const { service, getClassicAssetData } = createService({
network: {
getClassicAssetData: jest.fn().mockResolvedValue(rpcRow),
it('throws for an uncached testnet classic asset when the token API returns no row', async () => {
const { service, saveMany } = createService({});

await expect(service.resolve(TESTNET_CLASSIC_USDC)).rejects.toThrow(
AssetMetadataServiceException,
);
expect(mockGetAssetsByAssetIds).toHaveBeenCalledWith([
TESTNET_CLASSIC_USDC,
]);
expect(saveMany).not.toHaveBeenCalled();
});

it('returns a cached testnet classic asset without calling the token API', async () => {
const cached = createCachedRow(
TESTNET_CLASSIC_USDC,
KnownCaip2ChainId.Testnet,
);
const { service, getByAssetIds, saveMany } = createService({
repo: {
getByAssetIds: jest.fn().mockResolvedValue([cached]),
},
});

const result = await service.resolve(classicId);
const result = await service.resolve(TESTNET_CLASSIC_USDC);

expect(result.assetId).toBe(classicId);
expect(result.symbol).toBe('USDC');
expect(mockGetAssetsByAssetIds).toHaveBeenCalled();
expect(getClassicAssetData).toHaveBeenCalledWith(
classicId,
KnownCaip2ChainId.Mainnet,
expect(result).toStrictEqual(cached);
expect(getByAssetIds).toHaveBeenCalledWith([TESTNET_CLASSIC_USDC]);
expect(mockGetAssetsByAssetIds).not.toHaveBeenCalled();
expect(saveMany).not.toHaveBeenCalled();
});

it('persists only the ids returned by the token API', 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,
TESTNET_SEP41_USDC,
]);

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

it('throws when the token API does not return metadata for a mainnet classic asset', async () => {
const { service } = createService({});

await expect(service.resolve(MAINNET_CLASSIC_USDC)).rejects.toThrow(
AssetMetadataServiceException,
);
expect(mockGetAssetsByAssetIds).toHaveBeenCalledWith([
MAINNET_CLASSIC_USDC,
]);
});

it('returns cached mainnet classic asset without calling token API', async () => {
Expand Down Expand Up @@ -250,7 +284,7 @@ describe('AssetMetadataService', () => {
name: 'USD Coin',
symbol: 'USDC',
};
const { service, getClassicAssetData, saveMany } = createService({});
const { service, saveMany } = createService({});
mockGetAssetsByAssetIds.mockResolvedValueOnce([tokenRow]);

const result = await service.resolve(classicId);
Expand All @@ -272,7 +306,6 @@ describe('AssetMetadataService', () => {
encodePathParams: false,
}),
});
expect(getClassicAssetData).not.toHaveBeenCalled();
expect(saveMany).toHaveBeenCalledWith([
expect.objectContaining({ assetId: classicId }),
]);
Expand Down
Loading
Loading