diff --git a/package.json b/package.json index c50a31e08f..3dca19b5e4 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,9 @@ "resolutions": { "elliptic@6.5.4": "^6.5.7", "fast-xml-parser@^4.3.4": "^4.4.1", - "ws@7.4.6": "^7.5.10" + "ws@7.4.6": "^7.5.10", + "@metamask/keyring-api": "23.1.0", + "@metamask/keyring-internal-api": "11.0.1" }, "simple-git-hooks": { "pre-push": "yarn lint" diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 07de57bfce..1d2899b5ed 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `MultichainAssetsController`: track Stellar classic (`asset:`) CAIP-19 tokens added via `addAssets` in `stellarClassicTrustlineInactiveAssetIds` until the keyring lists the same id in `AccountsController:accountAssetListUpdated` `added` (or the asset is removed); one-time backfill for classic ids already in `accountsAssets` when `stellarTrustlineInactiveBackfillComplete` is false (legacy imports before this feature) ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- `selectAllMultichainAssets`: optional `isStellarTrustlineInactive` on multichain `Asset` when the id is in that map ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- `isStellarClassicAssetCaip19` helper; `isStellarTrustlineTrackedAsset` now applies only to classic `asset:` ids, not `sep41:` ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + +### Fixed + +- `MultichainAssetsController`: after `addAssets`, re-fetch the Snap `listAccountAssets` list for Stellar classic (`asset:`) ids and clear `stellarClassicTrustlineInactiveAssetIds` when the keyring already reports the same CAIP-19 id (e.g. hide token then re-add while trustline is still active) ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- `MultichainBalancesController`: when `MultichainAssetsController:accountAssetListUpdated` fires while the vault is locked, balance fetches are skipped and were never retried after unlock, leaving multichain token lists empty despite assets in state; subscribe to `KeyringController:stateChange` and refetch balances for snap-backed accounts that still have no cached balances after unlock. ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + ### Changed - Bump `@metamask/account-tree-controller` from `^7.2.0` to `^7.3.0` ([#8722](https://github.com/MetaMask/core/pull/8722)) diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 973ed9e3a9..91f835f987 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/assets-controllers", - "version": "106.0.0", + "version": "106.0.0-dev.2", "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", "keywords": [ "Ethereum", diff --git a/packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.test.ts b/packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.test.ts index f1bcd86cec..658d7d08cf 100644 --- a/packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.test.ts +++ b/packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.test.ts @@ -23,6 +23,7 @@ import type { SubjectPermissions } from '@metamask/permission-controller'; import type { BulkTokenScanResponse } from '@metamask/phishing-controller'; import { TokenScanResultType } from '@metamask/phishing-controller'; import type { Snap } from '@metamask/snaps-utils'; +import { HandlerType } from '@metamask/snaps-utils'; import { v4 as uuidv4 } from 'uuid'; import { @@ -197,6 +198,17 @@ const mockGetPermissionsReturnValue = [ }, ]; +const STELLAR_CLASSIC_USDC = + 'stellar:pubnet/asset:USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' as CaipAssetType; + +const STELLAR_CLASSIC_USDC_METADATA = { + name: 'USD Coin', + symbol: 'USDC', + fungible: true, + iconUrl: '', + units: [{ name: 'USD Coin', symbol: 'USDC', decimals: 7 }], +}; + const mockGetMetadataReturnValue: AssetMetadataResponse | undefined = { assets: { 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501': { @@ -380,9 +392,51 @@ describe('MultichainAssetsController', () => { accountsAssets: {}, assetsMetadata: {}, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); }); + it('backfills Stellar classic trustline-inactive for assets already in accountsAssets on first load', () => { + const accountId = 'stellar-legacy'; + const { controller } = setupController({ + state: { + accountsAssets: { + [accountId]: [STELLAR_CLASSIC_USDC], + }, + assetsMetadata: {}, + allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + } as MultichainAssetsControllerState, + }); + + expect(controller.state.stellarTrustlineInactiveBackfillComplete).toBe( + true, + ); + expect( + controller.state.stellarClassicTrustlineInactiveAssetIds[accountId], + ).toStrictEqual([STELLAR_CLASSIC_USDC]); + }); + + it('skips Stellar trustline backfill when already completed', () => { + const accountId = 'stellar-legacy'; + const { controller } = setupController({ + state: { + accountsAssets: { + [accountId]: [STELLAR_CLASSIC_USDC], + }, + assetsMetadata: {}, + allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, + } as MultichainAssetsControllerState, + }); + + expect( + controller.state.stellarClassicTrustlineInactiveAssetIds[accountId], + ).toBeUndefined(); + }); + it('does not update state when new account added is EVM', async () => { const { controller, messenger } = setupController(); @@ -397,6 +451,8 @@ describe('MultichainAssetsController', () => { accountsAssets: {}, assetsMetadata: {}, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); }); @@ -430,6 +486,8 @@ describe('MultichainAssetsController', () => { }, assetsMetadata: mockGetMetadataReturnValue.assets, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); }); @@ -499,6 +557,8 @@ describe('MultichainAssetsController', () => { ...mockGetMetadataReturnValue.assets, }, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); }); @@ -556,6 +616,8 @@ describe('MultichainAssetsController', () => { ...mockGetMetadataReturnValue.assets, }, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); }); @@ -591,6 +653,8 @@ describe('MultichainAssetsController', () => { assetsMetadata: mockGetMetadataReturnValue.assets, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); // Remove an EVM account messenger.publish('AccountsController:accountRemoved', mockEthAccount.id); @@ -604,6 +668,8 @@ describe('MultichainAssetsController', () => { assetsMetadata: mockGetMetadataReturnValue.assets, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); }); @@ -639,6 +705,8 @@ describe('MultichainAssetsController', () => { assetsMetadata: mockGetMetadataReturnValue.assets, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); // Remove the added solana account messenger.publish( @@ -653,10 +721,46 @@ describe('MultichainAssetsController', () => { assetsMetadata: mockGetMetadataReturnValue.assets, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }); }); describe('handleAccountAssetListUpdated', () => { + it('clears Stellar trustline-inactive when keyring re-announces an existing classic asset', async () => { + const accountId = 'stellar-test-account'; + const { messenger, controller } = setupController({ + state: { + accountsAssets: { + [accountId]: [STELLAR_CLASSIC_USDC], + }, + assetsMetadata: { + ...mockGetMetadataReturnValue.assets, + [STELLAR_CLASSIC_USDC]: STELLAR_CLASSIC_USDC_METADATA, + }, + allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: { + [accountId]: [STELLAR_CLASSIC_USDC], + }, + } as MultichainAssetsControllerState, + }); + + messenger.publish('AccountsController:accountAssetListUpdated', { + assets: { + [accountId]: { + added: [STELLAR_CLASSIC_USDC], + removed: [], + }, + }, + }); + + await jestAdvanceTime({ duration: 1 }); + + expect( + controller.state.stellarClassicTrustlineInactiveAssetIds[accountId], + ).toBeUndefined(); + }); + it('updates the assets list for an account when a new asset is added', async () => { const mockSolanaAccountId1 = 'account1'; const mockSolanaAccountId2 = 'account2'; @@ -1067,6 +1171,55 @@ describe('MultichainAssetsController', () => { ).toStrictEqual([assetToAdd]); }); + it('adds Stellar classic assets to trustline-inactive when added via addAssets', async () => { + const { controller } = setupController({ + state: { + accountsAssets: {}, + assetsMetadata: { + [STELLAR_CLASSIC_USDC]: STELLAR_CLASSIC_USDC_METADATA, + }, + allIgnoredAssets: {}, + } as MultichainAssetsControllerState, + }); + + await controller.addAssets([STELLAR_CLASSIC_USDC], mockSolanaAccount.id); + + expect( + controller.state.stellarClassicTrustlineInactiveAssetIds[ + mockSolanaAccount.id + ], + ).toStrictEqual([STELLAR_CLASSIC_USDC]); + }); + + it('clears Stellar classic trustline-inactive when Snap listAccountAssets includes the asset', async () => { + const { controller, mockSnapHandleRequest } = setupController({ + state: { + accountsAssets: {}, + assetsMetadata: { + [STELLAR_CLASSIC_USDC]: STELLAR_CLASSIC_USDC_METADATA, + }, + allIgnoredAssets: {}, + } as MultichainAssetsControllerState, + }); + + mockSnapHandleRequest.mockImplementation( + (params: { handler: string }) => { + if (params.handler === HandlerType.OnKeyringRequest) { + return Promise.resolve([STELLAR_CLASSIC_USDC]); + } + return Promise.resolve(mockHandleRequestOnAssetsLookupReturnValue); + }, + ); + + await controller.addAssets([STELLAR_CLASSIC_USDC], mockSolanaAccount.id); + + expect( + controller.state.stellarClassicTrustlineInactiveAssetIds[ + mockSolanaAccount.id + ], + ).toBeUndefined(); + }); + it('should publish accountAssetListUpdated event when asset is added', async () => { const { controller, messenger } = setupController({ state: { @@ -1709,7 +1862,9 @@ describe('MultichainAssetsController', () => { const tokens = Array.from( { length: 150 }, (_, i) => - `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Token${String(i).padStart(3, '0')}`, + `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Token${String( + i, + ).padStart(3, '0')}`, ); const { controller, messenger, mockBulkScanTokens } = setupController({ @@ -1778,7 +1933,9 @@ describe('MultichainAssetsController', () => { const tokens = Array.from( { length: 120 }, (_, i) => - `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Token${String(i).padStart(3, '0')}`, + `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Token${String( + i, + ).padStart(3, '0')}`, ); const { controller, messenger, mockBulkScanTokens } = setupController({ @@ -2021,6 +2178,8 @@ describe('MultichainAssetsController', () => { "accountsAssets": {}, "allIgnoredAssets": {}, "assetsMetadata": {}, + "stellarClassicTrustlineInactiveAssetIds": {}, + "stellarTrustlineInactiveBackfillComplete": true, } `); }); @@ -2039,6 +2198,7 @@ describe('MultichainAssetsController', () => { "accountsAssets": {}, "allIgnoredAssets": {}, "assetsMetadata": {}, + "stellarClassicTrustlineInactiveAssetIds": {}, } `); }); diff --git a/packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.ts b/packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.ts index 3bd5ee7e12..0dd0591ef3 100644 --- a/packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.ts +++ b/packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.ts @@ -42,6 +42,7 @@ import type { MutexInterface } from 'async-mutex'; import { Mutex } from 'async-mutex'; import type { MultichainAssetsControllerMethodActions } from './MultichainAssetsController-method-action-types'; +import { isStellarClassicAssetCaip19 } from '../multichain/stellarTrustline'; import { getChainIdsCaveat } from './utils'; const controllerName = 'MultichainAssetsController'; @@ -52,6 +53,19 @@ export type MultichainAssetsControllerState = { }; accountsAssets: { [account: string]: CaipAssetType[] }; allIgnoredAssets: { [account: string]: CaipAssetType[] }; + /** + * Stellar classic (`asset:`) CAIP-19 ids added via import and not yet reported + * by the Snap in `accountAssetListUpdated` `added` — treated as no trustline for UI. + */ + stellarClassicTrustlineInactiveAssetIds: { + [account: string]: CaipAssetType[]; + }; + /** + * After the first run, `true` means legacy Stellar classic (`asset:`) rows in + * `accountsAssets` were merged into `stellarClassicTrustlineInactiveAssetIds` + * so pre-feature imports get the “no trustline” flag without re-importing. + */ + stellarTrustlineInactiveBackfillComplete: boolean; }; // Represents the response of the asset snap's onAssetLookup handler @@ -75,7 +89,13 @@ export type MultichainAssetsControllerAccountAssetListUpdatedEvent = { * @returns The default {@link MultichainAssetsController} state. */ export function getDefaultMultichainAssetsControllerState(): MultichainAssetsControllerState { - return { accountsAssets: {}, assetsMetadata: {}, allIgnoredAssets: {} }; + return { + accountsAssets: {}, + assetsMetadata: {}, + allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: false, + }; } /** @@ -175,6 +195,18 @@ const assetsControllerMetadata: StateMetadata = includeInDebugSnapshot: false, usedInUi: true, }, + stellarClassicTrustlineInactiveAssetIds: { + includeInStateLogs: false, + persist: true, + includeInDebugSnapshot: false, + usedInUi: true, + }, + stellarTrustlineInactiveBackfillComplete: { + includeInStateLogs: false, + persist: true, + includeInDebugSnapshot: false, + usedInUi: false, + }, }; const MESSENGER_EXPOSED_METHODS = [ @@ -256,6 +288,37 @@ export class MultichainAssetsController extends StaticIntervalPollingController< ); messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS); + + this.#backfillStellarTrustlineInactiveIfNeeded(); + } + + /** + * One-time merge: Stellar classic ids already present in `accountsAssets` from + * before `stellarClassicTrustlineInactiveAssetIds` existed are added to that + * map so the UI can show “no trustline” until the keyring clears them. + */ + #backfillStellarTrustlineInactiveIfNeeded(): void { + if (this.state.stellarTrustlineInactiveBackfillComplete) { + return; + } + this.update((state) => { + for (const [accountId, assets] of Object.entries(state.accountsAssets)) { + for (const assetId of assets) { + if (!isStellarClassicAssetCaip19(assetId)) { + continue; + } + if (!state.stellarClassicTrustlineInactiveAssetIds[accountId]) { + state.stellarClassicTrustlineInactiveAssetIds[accountId] = []; + } + const inactive = + state.stellarClassicTrustlineInactiveAssetIds[accountId]; + if (!inactive.includes(assetId)) { + inactive.push(assetId); + } + } + } + state.stellarTrustlineInactiveBackfillComplete = true; + }); } async _executePoll(_input: null): Promise { @@ -341,6 +404,12 @@ export class MultichainAssetsController extends StaticIntervalPollingController< ].filter((asset) => !assetsToIgnore.includes(asset)); } + this.#removeStellarTrustlineInactiveAssetIds( + state, + accountId, + assetsToIgnore, + ); + if (!state.allIgnoredAssets[accountId]) { state.allIgnoredAssets[accountId] = []; } @@ -410,6 +479,19 @@ export class MultichainAssetsController extends StaticIntervalPollingController< delete state.allIgnoredAssets[accountId]; } } + + for (const assetId of addedAssets) { + if (isStellarClassicAssetCaip19(assetId)) { + if (!state.stellarClassicTrustlineInactiveAssetIds[accountId]) { + state.stellarClassicTrustlineInactiveAssetIds[accountId] = []; + } + const inactive = + state.stellarClassicTrustlineInactiveAssetIds[accountId]; + if (!inactive.includes(assetId)) { + inactive.push(assetId); + } + } + } }); // Publish event to notify other controllers (balances, rates) about the new assets @@ -424,6 +506,11 @@ export class MultichainAssetsController extends StaticIntervalPollingController< }); } + await this.#reconcileStellarClassicTrustlineInactiveWithSnap( + accountId, + addedAssets, + ); + return this.state.accountsAssets[accountId] || []; }); } @@ -439,6 +526,90 @@ export class MultichainAssetsController extends StaticIntervalPollingController< return this.state.allIgnoredAssets[accountId]?.includes(asset) ?? false; } + /** + * After `addAssets`, Stellar classic (`asset:`) ids are marked trustline-inactive until + * the keyring confirms them. Re-fetch the account asset list from the Snap and clear + * that flag for any added classic id the Snap already reports (e.g. hide → re-add with + * an existing trustline), without relying on a later `accountAssetListUpdated` event. + * + * @param accountId - Account the assets were added to. + * @param addedAssets - Assets that were newly added in this `addAssets` call. + */ + async #reconcileStellarClassicTrustlineInactiveWithSnap( + accountId: string, + addedAssets: CaipAssetType[], + ): Promise { + const stellarAdded = addedAssets.filter(isStellarClassicAssetCaip19); + if (stellarAdded.length === 0) { + return; + } + + const accounts = this.messenger.call( + 'AccountsController:listMultichainAccounts', + ); + const account = accounts.find((a) => a.id === accountId); + if (!account || !this.#isNonEvmAccount(account)) { + return; + } + + const snapId = account.metadata.snap?.id; + if (!snapId) { + return; + } + + let snapAssets: CaipAssetTypeOrId[]; + try { + snapAssets = await this.#getAssetsList(accountId, snapId); + } catch { + return; + } + + const snapAssetSet = new Set(snapAssets.filter(isCaipAssetType)); + const confirmedTrustline = stellarAdded.filter((asset) => + snapAssetSet.has(asset), + ); + + if (confirmedTrustline.length === 0) { + return; + } + + this.update((state) => { + this.#removeStellarTrustlineInactiveAssetIds( + state, + accountId, + confirmedTrustline, + ); + }); + } + + /** + * Drops CAIP-19 asset ids from the per-account "Stellar import / no trustline" set. + * + * @param state - Controller draft state. + * @param accountId - Account id. + * @param assetIds - Asset ids to remove from the inactive set. + */ + #removeStellarTrustlineInactiveAssetIds( + state: MultichainAssetsControllerState, + accountId: string, + assetIds: readonly CaipAssetType[], + ): void { + if (assetIds.length === 0) { + return; + } + const list = state.stellarClassicTrustlineInactiveAssetIds[accountId]; + if (!list) { + return; + } + const drop = new Set(assetIds); + const next = list.filter((a) => !drop.has(a)); + if (next.length === 0) { + delete state.stellarClassicTrustlineInactiveAssetIds[accountId]; + } else { + state.stellarClassicTrustlineInactiveAssetIds[accountId] = next; + } + } + /** * Function to update the assets list for an account * @@ -453,6 +624,13 @@ export class MultichainAssetsController extends StaticIntervalPollingController< const assetsForMetadataRefresh = new Set([]); const accountsAndAssetsToUpdate: AccountAssetListUpdatedEventPayload['assets'] = {}; + /** + * When the keyring re-announces a Stellar classic asset id the user had already + * imported, `preFiltered` excludes it; we still clear "no trustline" UI state. + */ + const clearStellarTrustlineInactiveByAccount: { + [account: string]: CaipAssetType[]; + } = {}; for (const [accountId, { added, removed }] of Object.entries( event.assets, )) { @@ -472,6 +650,21 @@ export class MultichainAssetsController extends StaticIntervalPollingController< const filteredToBeAddedAssets = await this.#filterBlockaidSpamTokensOnAdd(preFilteredToBeAddedAssets); + const filteredToBeAddedSet = new Set( + filteredToBeAddedAssets, + ); + const snapConfirmsStellarTrustline = added.filter( + (asset): asset is CaipAssetType => + isCaipAssetType(asset) && + !this.#isAssetIgnored(asset, accountId) && + isStellarClassicAssetCaip19(asset) && + (existing.includes(asset) || filteredToBeAddedSet.has(asset)), + ); + if (snapConfirmsStellarTrustline.length > 0) { + clearStellarTrustlineInactiveByAccount[accountId] = + snapConfirmsStellarTrustline; + } + // In case accountsAndAssetsToUpdate event is fired with "removed" assets that don't exist, we don't want to remove them const filteredToBeRemovedAssets = removed.filter( (asset) => existing.includes(asset) && isCaipAssetType(asset), @@ -499,19 +692,39 @@ export class MultichainAssetsController extends StaticIntervalPollingController< } } + const accountIdsToUpdate = new Set([ + ...Object.keys(accountsAndAssetsToUpdate), + ...Object.keys(clearStellarTrustlineInactiveByAccount), + ]); + this.update((state) => { - for (const [accountId, { added, removed }] of Object.entries( - accountsAndAssetsToUpdate, - )) { - const assets = new Set([ - ...(state.accountsAssets[accountId] || []), - ...added, - ]); - for (const asset of removed) { - assets.delete(asset); + for (const accountId of accountIdsToUpdate) { + const toMerge = accountsAndAssetsToUpdate[accountId]; + if (toMerge) { + const { added, removed: removedAssets } = toMerge; + const assets = new Set([ + ...(state.accountsAssets[accountId] || []), + ...added, + ]); + for (const asset of removedAssets) { + assets.delete(asset); + } + + state.accountsAssets[accountId] = Array.from(assets); } - state.accountsAssets[accountId] = Array.from(assets); + this.#removeStellarTrustlineInactiveAssetIds( + state, + accountId, + clearStellarTrustlineInactiveByAccount[accountId] ?? [], + ); + if (toMerge) { + this.#removeStellarTrustlineInactiveAssetIds( + state, + accountId, + toMerge.removed, + ); + } } }); @@ -565,6 +778,7 @@ export class MultichainAssetsController extends StaticIntervalPollingController< await this.#refreshAssetsMetadata(assets); this.update((state) => { state.accountsAssets[account.id] = assets; + delete state.stellarClassicTrustlineInactiveAssetIds[account.id]; }); this.messenger.publish(`${controllerName}:accountAssetListUpdated`, { assets: { @@ -590,6 +804,9 @@ export class MultichainAssetsController extends StaticIntervalPollingController< if (state.allIgnoredAssets[accountId]) { delete state.allIgnoredAssets[accountId]; } + if (state.stellarClassicTrustlineInactiveAssetIds[accountId]) { + delete state.stellarClassicTrustlineInactiveAssetIds[accountId]; + } // TODO: We are not deleting the assetsMetadata because we will soon make this controller extends StaticIntervalPollingController // and update all assetsMetadata once a day. }); @@ -742,7 +959,7 @@ export class MultichainAssetsController extends StaticIntervalPollingController< snapId: string, ): Promise { try { - return (await this.messenger.call('SnapController:handleRequest', { + const response = (await this.messenger.call('SnapController:handleRequest', { snapId: snapId as SnapId, origin: 'metamask', handler: HandlerType.OnAssetsLookup, @@ -753,7 +970,8 @@ export class MultichainAssetsController extends StaticIntervalPollingController< assets, }, }, - })) as Promise; + })) as AssetMetadataResponse; + return response; } catch (error) { // Ignore console.error(error); diff --git a/packages/assets-controllers/src/MultichainAssetsRatesController/constant.ts b/packages/assets-controllers/src/MultichainAssetsRatesController/constant.ts index 2fef0e8155..e0610f49f3 100644 --- a/packages/assets-controllers/src/MultichainAssetsRatesController/constant.ts +++ b/packages/assets-controllers/src/MultichainAssetsRatesController/constant.ts @@ -26,7 +26,7 @@ export const MAP_CAIP_CURRENCIES: { // XRP mainnet xrp: 'xrpl:mainnet/slip44:144', - // Stellar Lumens mainnet + // Stellar Lumens (spot/fiat map uses pubnet; per-chain testnet balances use stellar:testnet/slip44:148 when wired) xlm: 'stellar:pubnet/slip44:148', // Chainlink (ERC20 on Ethereum mainnet) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index 84fec30388..fb0a15208d 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -158,6 +158,7 @@ function getRestrictedMessenger( 'AccountsController:accountRemoved', 'AccountsController:accountBalancesUpdated', 'MultichainAssetsController:accountAssetListUpdated', + 'KeyringController:stateChanged', ], }); return multichainBalancesControllerMessenger; diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index a8c58ed4a6..626051e35f 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -9,6 +9,7 @@ import type { StateMetadata, ControllerGetStateAction, ControllerStateChangeEvent, + ControllerStateChangedEvent, } from '@metamask/base-controller'; import { isEvmAccountType } from '@metamask/keyring-api'; import type { @@ -16,7 +17,10 @@ import type { CaipAssetType, AccountBalancesUpdatedEventPayload, } from '@metamask/keyring-api'; -import type { KeyringControllerGetStateAction } from '@metamask/keyring-controller'; +import type { + KeyringControllerGetStateAction, + KeyringControllerState, +} from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import { KeyringClient } from '@metamask/keyring-snap-client'; import type { Messenger } from '@metamask/messenger'; @@ -105,7 +109,8 @@ type AllowedEvents = | AccountsControllerAccountAddedEvent | AccountsControllerAccountRemovedEvent | AccountsControllerAccountBalancesUpdatesEvent - | MultichainAssetsControllerAccountAssetListUpdatedEvent; + | MultichainAssetsControllerAccountAssetListUpdatedEvent + | ControllerStateChangedEvent<'KeyringController', KeyringControllerState>; /** * Messenger type for the MultichainBalancesController. */ @@ -190,6 +195,47 @@ export class MultichainBalancesController extends BaseController< await this.#handleOnAccountAssetListUpdated(updatedAccountAssets); }, ); + + // When the keyring transitions from locked → unlocked, fetch balances for + // any non-EVM account that had its balance fetch skipped while locked. + // We cannot read KeyringController state in the constructor (restricted), + // so the first `stateChanged` establishes the baseline; if the vault is + // already unlocked on that first event, we refetch once (covers unlock as + // the only keyring update after construction). + let previousKeyringIsUnlocked: boolean | undefined; + this.messenger.subscribe( + 'KeyringController:stateChanged', + (keyringState: KeyringControllerState) => { + const { isUnlocked } = keyringState; + if (previousKeyringIsUnlocked === undefined) { + previousKeyringIsUnlocked = isUnlocked; + if (isUnlocked) { + this.#refetchBalancesForAccountsMissingFromState(); + } + return; + } + const justUnlocked = isUnlocked && !previousKeyringIsUnlocked; + previousKeyringIsUnlocked = isUnlocked; + if (justUnlocked) { + this.#refetchBalancesForAccountsMissingFromState(); + } + }, + ); + } + + /** + * Fetches balances for non-EVM accounts that have no cached balances yet. + */ + #refetchBalancesForAccountsMissingFromState(): void { + for (const account of this.#listAccounts()) { + const hasBalance = + this.state.balances[account.id] && + Object.keys(this.state.balances[account.id]).length > 0; + if (!hasBalance) { + // eslint-disable-next-line no-void + void this.updateBalance(account.id); + } + } } /** @@ -392,7 +438,9 @@ export class MultichainBalancesController extends BaseController< this.update((state: Draft) => { Object.entries(balanceUpdate.balances).forEach( ([accountId, assetBalances]) => { - if (accountId in state.balances) { + if ( + Object.prototype.hasOwnProperty.call(state.balances, accountId) + ) { Object.assign(state.balances[accountId], assetBalances); } }, @@ -406,7 +454,9 @@ export class MultichainBalancesController extends BaseController< * @param accountId - The account ID being removed. */ async #handleOnAccountRemoved(accountId: string): Promise { - if (accountId in this.state.balances) { + if ( + Object.prototype.hasOwnProperty.call(this.state.balances, accountId) + ) { this.update((state: Draft) => { delete state.balances[accountId]; }); diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index 8bc2c17247..55ec907a8f 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -217,6 +217,11 @@ export type { MultichainAssetsControllerAccountAssetListUpdatedEvent, MultichainAssetsControllerMessenger, } from './MultichainAssetsController'; +export { + isStellarCaipChain, + isStellarClassicAssetCaip19, + isStellarTrustlineTrackedAsset, +} from './multichain/stellarTrustline'; export type { MultichainAssetsControllerGetAssetMetadataAction, MultichainAssetsControllerIgnoreAssetsAction, diff --git a/packages/assets-controllers/src/multichain/stellarTrustline.test.ts b/packages/assets-controllers/src/multichain/stellarTrustline.test.ts new file mode 100644 index 0000000000..84a3b73776 --- /dev/null +++ b/packages/assets-controllers/src/multichain/stellarTrustline.test.ts @@ -0,0 +1,69 @@ +import type { CaipAssetType, CaipChainId } from '@metamask/utils'; + +import { + isStellarCaipChain, + isStellarClassicAssetCaip19, + isStellarTrustlineTrackedAsset, +} from './stellarTrustline'; + +describe('stellarTrustline', () => { + describe('isStellarCaipChain', () => { + it('returns true for Stellar pubnet CAIP-2 id', () => { + expect(isStellarCaipChain('stellar:pubnet' as CaipChainId)).toBe(true); + }); + + it('returns false for Solana chain id', () => { + expect( + isStellarCaipChain( + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as CaipChainId, + ), + ).toBe(false); + }); + }); + + describe('isStellarClassicAssetCaip19', () => { + const stellarClassicAsset = + 'stellar:pubnet/asset:USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' as CaipAssetType; + + it('returns true for Stellar classic asset id', () => { + expect(isStellarClassicAssetCaip19(stellarClassicAsset)).toBe(true); + }); + + it('returns false for Stellar native slip44', () => { + const native = 'stellar:pubnet/slip44:148' as CaipAssetType; + expect(isStellarClassicAssetCaip19(native)).toBe(false); + }); + + it('returns false for Stellar sep41 (Soroban) asset', () => { + const sep41 = + 'stellar:pubnet/sep41:CAUP7NFABXE5TJRL3FKTPMWRLC7IAXYDCTHQRFSCLR5TMGKHOOQO772J' as CaipAssetType; + expect(isStellarClassicAssetCaip19(sep41)).toBe(false); + }); + }); + + describe('isStellarTrustlineTrackedAsset', () => { + const stellarClassicAsset = + 'stellar:pubnet/asset:USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' as CaipAssetType; + + it('returns true for Stellar classic asset', () => { + expect(isStellarTrustlineTrackedAsset(stellarClassicAsset)).toBe(true); + }); + + it('returns false for Stellar native slip44', () => { + const native = 'stellar:pubnet/slip44:148' as CaipAssetType; + expect(isStellarTrustlineTrackedAsset(native)).toBe(false); + }); + + it('returns false for Stellar sep41 (Soroban) asset', () => { + const sep41 = + 'stellar:pubnet/sep41:CAUP7NFABXE5TJRL3FKTPMWRLC7IAXYDCTHQRFSCLR5TMGKHOOQO772J' as CaipAssetType; + expect(isStellarTrustlineTrackedAsset(sep41)).toBe(false); + }); + + it('returns false for Solana SPL token', () => { + const spl = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN' as CaipAssetType; + expect(isStellarTrustlineTrackedAsset(spl)).toBe(false); + }); + }); +}); diff --git a/packages/assets-controllers/src/multichain/stellarTrustline.ts b/packages/assets-controllers/src/multichain/stellarTrustline.ts new file mode 100644 index 0000000000..0481109255 --- /dev/null +++ b/packages/assets-controllers/src/multichain/stellarTrustline.ts @@ -0,0 +1,48 @@ +import type { CaipAssetType, CaipChainId } from '@metamask/utils'; +import { KnownCaipNamespace, parseCaipAssetType, parseCaipChainId } from '@metamask/utils'; + +/** + * Returns true when the CAIP-2 chain id uses the Stellar namespace. + * + * @param chainId - CAIP-2 chain identifier. + * @returns Whether this is a Stellar chain. + */ +export function isStellarCaipChain(chainId: CaipChainId): boolean { + try { + return parseCaipChainId(chainId).namespace === KnownCaipNamespace.Stellar; + } catch { + return false; + } +} + +/** + * Returns true for Stellar classic (non-Soroban) fungible assets: `asset:CODE-ISSUER`. + * Excludes native XLM (`slip44`), Soroban SEP-41 (`sep41:`), and other namespaces. + * + * @param caipAssetType - CAIP-19 asset type identifier. + * @returns Whether this is a Stellar classic asset id. + */ +export function isStellarClassicAssetCaip19(caipAssetType: CaipAssetType): boolean { + try { + const parsed = parseCaipAssetType(caipAssetType); + if (!isStellarCaipChain(parsed.chainId)) { + return false; + } + return parsed.assetNamespace === 'asset'; + } catch { + return false; + } +} + +/** + * Stellar trust lines apply to classic (non-Soroban) assets only. Native XLM uses + * slip44. Soroban tokens use `sep41:` and do not use trust-line semantics in this UI. + * + * @param caipAssetType - CAIP-19 asset type identifier. + * @returns Whether trust-line active/inactive should be tracked for this asset. + */ +export function isStellarTrustlineTrackedAsset( + caipAssetType: CaipAssetType, +): boolean { + return isStellarClassicAssetCaip19(caipAssetType); +} diff --git a/packages/assets-controllers/src/selectors/token-selectors.test.ts b/packages/assets-controllers/src/selectors/token-selectors.test.ts index ccbac2f2d2..8b0e4372b4 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.test.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.test.ts @@ -278,6 +278,8 @@ const mockMultichainAssetsControllerState: MultichainAssetsControllerState = { }, }, allIgnoredAssets: {}, + stellarClassicTrustlineInactiveAssetIds: {}, + stellarTrustlineInactiveBackfillComplete: true, }; const mockAccountTreeControllerState = { diff --git a/packages/assets-controllers/src/selectors/token-selectors.ts b/packages/assets-controllers/src/selectors/token-selectors.ts index 4b6e7dc2b7..d82e89d6b9 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.ts @@ -89,6 +89,8 @@ export type Asset = ( conversionRate: number; } | undefined; + /** Stellar classic `asset:` row added via import only (no keyring confirmation yet). */ + isStellarTrustlineInactive?: boolean; rwaData?: TokenRwaData; }; @@ -103,6 +105,7 @@ export type AssetListState = { currencyRates: CurrencyRateState['currencyRates']; accountsAssets: MultichainAssetsControllerState['accountsAssets']; allIgnoredAssets: MultichainAssetsControllerState['allIgnoredAssets']; + stellarClassicTrustlineInactiveAssetIds: MultichainAssetsControllerState['stellarClassicTrustlineInactiveAssetIds']; assetsMetadata: MultichainAssetsControllerState['assetsMetadata']; balances: MultichainBalancesControllerState['balances']; conversionRates: MultichainAssetsRatesControllerState['conversionRates']; @@ -356,6 +359,7 @@ const selectAllMultichainAssets = createAssetListSelector( selectAccountsToGroupIdMap, (state) => state.accountsAssets, (state) => state.allIgnoredAssets, + (state) => state.stellarClassicTrustlineInactiveAssetIds, (state) => state.assetsMetadata, (state) => state.balances, (state) => state.conversionRates, @@ -365,6 +369,7 @@ const selectAllMultichainAssets = createAssetListSelector( accountsMap, multichainTokens, ignoredMultichainAssets, + stellarTrustlineInactiveByAccount, multichainAssetsMetadata, multichainBalances, multichainConversionRates, @@ -430,6 +435,10 @@ const selectAllMultichainAssets = createAssetListSelector( assetId, ); + const isStellarTrustlineInactive = ( + stellarTrustlineInactiveByAccount[accountId] ?? [] + ).includes(assetId); + // TODO: We shouldn't have to rely on fallbacks for name and symbol, they should not be optional groupChainAssets.push({ accountType: type as MultichainAccountType, @@ -450,6 +459,7 @@ const selectAllMultichainAssets = createAssetListSelector( } : undefined, chainId, + ...(isStellarTrustlineInactive && { isStellarTrustlineInactive: true }), }); } } diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 0be7f50c94..9f66feb88f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add Stellar support for bridge token flows: `isStellarChainId`, `ChainId.STELLAR`, native XLM metadata, CAIP/decimal formatting aligned with Bridge API, and Stellar pubnet/testnet in `isNonEvmChainId` ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- Add `StellarTradeDataSchema`, `StellarTradeData`, and `isStellarTrade`; extend `extractTradeData` to read Stellar XDR from `{ xdrBase64 }` or `{ xdr }` objects ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + ## [72.0.0] ### Added diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index ba58313029..b5a9a138b8 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/bridge-controller", - "version": "72.0.0", + "version": "72.0.0-dev.2", "description": "Manages bridge-related quote fetching functionality for MetaMask", "keywords": [ "Ethereum", diff --git a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap index 5dbe5e22f4..b7bbbbc609 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap @@ -19,6 +19,8 @@ exports[`BridgeController SSE should publish validation failures 4`] = ` "lifi|trade.unsignedPsbtBase64", "lifi|trade.inputsToSign", "lifi|trade.raw_data_hex", + "lifi|trade.xdrBase64", + "lifi|trade.xdr", ], "location": "Main View", "refresh_count": 1, diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 0c10142524..b81bdffa3f 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -848,13 +848,13 @@ describe('BridgeController SSE', function () { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ).toBeGreaterThan(t2!); expect(consoleLogSpy.mock.calls).toMatchInlineSnapshot(` - [ - [ - "Failed to stream bridge quotes", - "Network error", - ], - ] - `); + [ + [ + "Failed to stream bridge quotes", + "Network error", + ], + ] + `); expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); expect(getLayer1GasFeeMock).toHaveBeenCalledTimes(2); expect(trackMetaMetricsFn).toHaveBeenCalledTimes(8); @@ -1220,22 +1220,24 @@ describe('BridgeController SSE', function () { t6!, ); expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` - [ - "Quote validation failed", [ - "lifi|trade", - "lifi|trade.chainId", - "lifi|trade.to", - "lifi|trade.from", - "lifi|trade.value", - "lifi|trade.data", - "lifi|trade.gasLimit", - "lifi|trade.unsignedPsbtBase64", - "lifi|trade.inputsToSign", - "lifi|trade.raw_data_hex", - ], - ] - `); + "Quote validation failed", + [ + "lifi|trade", + "lifi|trade.chainId", + "lifi|trade.to", + "lifi|trade.from", + "lifi|trade.value", + "lifi|trade.data", + "lifi|trade.gasLimit", + "lifi|trade.unsignedPsbtBase64", + "lifi|trade.inputsToSign", + "lifi|trade.raw_data_hex", + "lifi|trade.xdrBase64", + "lifi|trade.xdr", + ], + ] + `); // Invalid quote jest.advanceTimersByTime(FOURTH_FETCH_DELAY * 3 - 1000); await flushPromises(); @@ -1250,21 +1252,21 @@ describe('BridgeController SSE', function () { ); expect(consoleWarnSpy.mock.calls).toHaveLength(3); expect(consoleWarnSpy.mock.calls[1]).toMatchInlineSnapshot(` - [ - "Quote validation failed", - [ - "unknown|unknown", - ], - ] - `); + [ + "Quote validation failed", + [ + "unknown|unknown", + ], + ] + `); expect(consoleWarnSpy.mock.calls[2]).toMatchInlineSnapshot(` - [ - "Quote validation failed", - [ - "unknown|quote", - ], - ] - `); + [ + "Quote validation failed", + [ + "unknown|quote", + ], + ] + `); expect(consoleLogSpy).toHaveBeenCalledTimes(1); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(5); @@ -1384,11 +1386,11 @@ describe('BridgeController SSE', function () { expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); expect(consoleLogSpy).toHaveBeenCalledTimes(1); expect(consoleLogSpy.mock.calls[0]).toMatchInlineSnapshot(` - [ - "Failed to stream bridge quotes", - [Error: Bridge-api error: timeout from server], - ] - `); + [ + "Failed to stream bridge quotes", + [Error: Bridge-api error: timeout from server], + ] + `); expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); expect(getLayer1GasFeeMock).toHaveBeenCalledTimes(0); // eslint-disable-next-line jest/no-restricted-matchers diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts index 6ee92f052a..ef632708bd 100644 --- a/packages/bridge-controller/src/constants/bridge.ts +++ b/packages/bridge-controller/src/constants/bridge.ts @@ -1,5 +1,5 @@ import { AddressZero } from '@ethersproject/constants'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import type { Hex } from '@metamask/utils'; import type { @@ -25,6 +25,7 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ SolScope.Mainnet, BtcScope.Mainnet, TrxScope.Mainnet, + XlmScope.Pubnet, ] as const; export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; @@ -56,6 +57,7 @@ export const DEFAULT_CHAIN_RANKING = [ { chainId: 'bip122:000000000019d6689c085ae165831e93', name: 'BTC' }, { chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', name: 'Solana' }, { chainId: 'tron:728126428', name: 'Tron' }, + { chainId: 'stellar:pubnet', name: 'Stellar' }, { chainId: 'eip155:8453', name: 'Base' }, { chainId: 'eip155:42161', name: 'Arbitrum' }, { chainId: 'eip155:59144', name: 'Linea' }, diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index 1c0ec09894..c4f6b85d9d 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -1,4 +1,4 @@ -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import type { AllowedBridgeChainIds } from './bridge'; import { CHAIN_IDS } from './chains'; @@ -59,6 +59,7 @@ const CURRENCY_SYMBOLS = { MON: 'MON', HYPE: 'HYPE', MEGAETH: 'ETH', + XLM: 'XLM', } as const; const ETH_SWAPS_TOKEN_OBJECT = { @@ -169,6 +170,14 @@ const TRX_SWAPS_TOKEN_OBJECT = { iconUrl: '', } as const; +const XLM_SWAPS_TOKEN_OBJECT = { + symbol: CURRENCY_SYMBOLS.XLM, + name: 'Stellar Lumens', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 7, + iconUrl: '', +} as const; + const MONAD_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.MON, name: 'Mon', @@ -210,6 +219,7 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [SolScope.Devnet]: SOLANA_SWAPS_TOKEN_OBJECT, [BtcScope.Mainnet]: BTC_SWAPS_TOKEN_OBJECT, [TrxScope.Mainnet]: TRX_SWAPS_TOKEN_OBJECT, + [XlmScope.Pubnet]: XLM_SWAPS_TOKEN_OBJECT, } as const; export type SupportedSwapsNativeCurrencySymbols = @@ -234,6 +244,7 @@ export const SYMBOL_TO_SLIP44_MAP: Record< TESTETH: 'slip44:60', SEI: 'slip44:19000118', TRX: 'slip44:195', + XLM: 'slip44:148', MON: 'slip44:268435779', HYPE: 'slip44:2457', }; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 5af57fabf3..a9e82cb8af 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -50,6 +50,7 @@ export type { Intent, IntentOrderLike, BitcoinTradeData, + StellarTradeData, TronTradeData, BridgeControllerState, BridgeControllerAction, @@ -140,6 +141,7 @@ export { isSolanaChainId, isBitcoinChainId, isTronChainId, + isStellarChainId, isNonEvmChainId, getNativeAssetForChainId, getDefaultBridgeControllerState, @@ -167,6 +169,7 @@ export { export { extractTradeData, isBitcoinTrade, + isStellarTrade, isTronTrade, isEvmTxData, type Trade, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 5f030bd45e..78ffd46ae3 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -45,6 +45,7 @@ import type { StepSchema, TokenFeatureSchema, QuoteStreamCompleteSchema, + StellarTradeDataSchema, TronTradeDataSchema, TxDataSchema, } from './utils/validators'; @@ -283,13 +284,15 @@ export type IntentOrderLike = Intent['order']; export type BitcoinTradeData = Infer; export type TronTradeData = Infer; + +export type StellarTradeData = Infer; /** * This is the type for the quote response from the bridge-api * TxDataType can be overriden to be a string when the quote is non-evm * ApprovalType can be overriden when you know the specific approval type (e.g., TxData for EVM-only contexts) */ export type QuoteResponse< - TxDataType = TxData | string | BitcoinTradeData | TronTradeData, + TxDataType = TxData | string | BitcoinTradeData | TronTradeData | StellarTradeData, ApprovalType = TxData | TronTradeData, > = Infer & { trade: TxDataType; @@ -322,6 +325,8 @@ export enum ChainId { LINEA = 59144, SOLANA = 1151111081099710, BTC = 20000000000001, + /** Internal bridge / token-list id for Stellar pubnet (Token API chain: stellar:pubnet). */ + STELLAR = 20000000000002, TRON = 728126428, SEI = 1329, MONAD = 143, diff --git a/packages/bridge-controller/src/utils/bridge.test.ts b/packages/bridge-controller/src/utils/bridge.test.ts index 97680af1e2..f24dea769a 100644 --- a/packages/bridge-controller/src/utils/bridge.test.ts +++ b/packages/bridge-controller/src/utils/bridge.test.ts @@ -1,4 +1,4 @@ -import { BtcScope, SolScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, XlmScope } from '@metamask/keyring-api'; import type { Hex } from '@metamask/utils'; import { @@ -15,6 +15,7 @@ import { isEthUsdt, isNonEvmChainId, isSolanaChainId, + isStellarChainId, isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, sumHexes, @@ -185,6 +186,24 @@ describe('Bridge utils', () => { }); }); + describe('isStellarChainId', () => { + it('returns true for Stellar CAIP-2 chain ids', () => { + expect(isStellarChainId(XlmScope.Pubnet)).toBe(true); + expect(isStellarChainId(XlmScope.Testnet)).toBe(true); + }); + + it('returns true for internal Stellar bridge chain id', () => { + expect(isStellarChainId(ChainId.STELLAR)).toBe(true); + expect(isStellarChainId(String(ChainId.STELLAR))).toBe(true); + }); + + it('returns false for other chainIds', () => { + expect(isStellarChainId(SolScope.Mainnet)).toBe(false); + expect(isStellarChainId('0x1')).toBe(false); + expect(isStellarChainId(1)).toBe(false); + }); + }); + describe('isNonEvmChainId', () => { it('returns true for Solana chainIds', () => { expect(isNonEvmChainId(ChainId.SOLANA)).toBe(true); @@ -198,6 +217,12 @@ describe('Bridge utils', () => { expect(isNonEvmChainId('20000000000001')).toBe(true); }); + it('returns true for Stellar chainIds', () => { + expect(isNonEvmChainId(XlmScope.Pubnet)).toBe(true); + expect(isNonEvmChainId(XlmScope.Testnet)).toBe(true); + expect(isNonEvmChainId(ChainId.STELLAR)).toBe(true); + }); + it('returns false for EVM chainIds', () => { expect(isNonEvmChainId('0x1')).toBe(false); expect(isNonEvmChainId(1)).toBe(false); @@ -268,6 +293,15 @@ describe('Bridge utils', () => { }); }); + it('should return native asset for Stellar chainId', () => { + const result = getNativeAssetForChainId(XlmScope.Pubnet); + expect(result).toStrictEqual({ + ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[XlmScope.Pubnet], + chainId: ChainId.STELLAR, + assetId: 'stellar:pubnet/slip44:148', + }); + }); + it('should throw error for unsupported chainId', () => { expect(() => getNativeAssetForChainId('999999')).toThrow( 'No XChain Swaps native asset found for chainId: 999999', diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 204b164d51..9eea963a7f 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -1,6 +1,6 @@ import { AddressZero } from '@ethersproject/constants'; import { Contract } from '@ethersproject/contracts'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import { isCaipChainId, isStrictHexString } from '@metamask/utils'; import type { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; @@ -230,9 +230,27 @@ export const isTronChainId = (chainId: Hex | number | CaipChainId | string) => { return chainId.toString() === ChainId.TRON.toString(); }; +/** + * Checks whether the chainId matches Stellar pubnet or testnet (CAIP-2). + * + * @param chainId - The chainId to check + * @returns Whether the chainId is Stellar + */ +export const isStellarChainId = ( + chainId: Hex | number | CaipChainId | string, +): boolean => { + if (isCaipChainId(chainId)) { + return ( + chainId === XlmScope.Pubnet.toString() || + chainId === XlmScope.Testnet.toString() + ); + } + return chainId.toString() === ChainId.STELLAR.toString(); +}; + /** * Checks if a chain ID represents a non-EVM blockchain supported by swaps - * Currently supports Solana, Bitcoin and Tron + * Currently supports Solana, Bitcoin, Tron, and Stellar * * @param chainId - The chain ID to check * @returns True if the chain is a supported non-EVM chain, false otherwise @@ -243,7 +261,8 @@ export const isNonEvmChainId = ( return ( isSolanaChainId(chainId) || isBitcoinChainId(chainId) || - isTronChainId(chainId) + isTronChainId(chainId) || + isStellarChainId(chainId) ); }; diff --git a/packages/bridge-controller/src/utils/caip-formatters.test.ts b/packages/bridge-controller/src/utils/caip-formatters.test.ts index 6b39b96434..3f91b91bfc 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.test.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.test.ts @@ -1,5 +1,5 @@ import { AddressZero } from '@ethersproject/constants'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../constants/chains'; import { ChainId } from '../types'; @@ -41,6 +41,12 @@ describe('CAIP Formatters', () => { expect(formatChainIdToCaip(TrxScope.Mainnet)).toBe(TrxScope.Mainnet); }); + it('should convert Stellar chainId to XlmScope', () => { + expect(formatChainIdToCaip(ChainId.STELLAR)).toBe(XlmScope.Pubnet); + expect(formatChainIdToCaip(XlmScope.Pubnet)).toBe(XlmScope.Pubnet); + expect(formatChainIdToCaip(XlmScope.Testnet)).toBe(XlmScope.Testnet); + }); + it('should convert number to CAIP format', () => { expect(formatChainIdToCaip(1)).toBe('eip155:1'); }); @@ -68,6 +74,12 @@ describe('CAIP Formatters', () => { expect(formatChainIdToDec(TrxScope.Mainnet)).toBe(ChainId.TRON); }); + it('should handle Stellar mainnet', () => { + expect(formatChainIdToDec(XlmScope.Pubnet)).toBe(ChainId.STELLAR); + expect(formatChainIdToDec(XlmScope.Testnet)).toBe(ChainId.STELLAR); + expect(formatChainIdToDec(ChainId.STELLAR)).toBe(ChainId.STELLAR); + }); + it('should parse CAIP chainId to decimal', () => { expect(formatChainIdToDec('eip155:1')).toBe(1); }); diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts index 450be976b0..eec21f2c74 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.ts @@ -5,7 +5,7 @@ import { convertHexToDecimal, toChecksumHexAddress, } from '@metamask/controller-utils'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { isCaipChainId, @@ -25,6 +25,7 @@ import { isBitcoinChainId, isNativeAddress, isSolanaChainId, + isStellarChainId, isTronChainId, } from './bridge'; @@ -52,6 +53,12 @@ export const formatChainIdToCaip = ( if (isTronChainId(chainId)) { return TrxScope.Mainnet; } + if (isStellarChainId(chainId)) { + if (chainId === XlmScope.Testnet) { + return XlmScope.Testnet; + } + return XlmScope.Pubnet; + } return toEvmCaipChainId(numberToHex(Number(chainId))); }; @@ -76,6 +83,9 @@ export const formatChainIdToDec = ( if (chainId === TrxScope.Mainnet) { return ChainId.TRON; } + if (isStellarChainId(chainId)) { + return ChainId.STELLAR; + } if (isCaipChainId(chainId)) { return Number(chainId.split(':').at(-1)); } diff --git a/packages/bridge-controller/src/utils/trade-utils.test.ts b/packages/bridge-controller/src/utils/trade-utils.test.ts index 0d4a4cf74f..d238910208 100644 --- a/packages/bridge-controller/src/utils/trade-utils.test.ts +++ b/packages/bridge-controller/src/utils/trade-utils.test.ts @@ -3,6 +3,7 @@ import { extractTradeData, isEvmTxData, isBitcoinTrade, + isStellarTrade, isTronTrade, } from './trade-utils'; import type { Trade } from './trade-utils'; @@ -145,12 +146,50 @@ describe('Trade utils', () => { }); }); + describe('isStellarTrade', () => { + it('returns true for xdrBase64 object', () => { + expect( + isStellarTrade({ xdrBase64: 'AAAABg==' } as unknown as Trade), + ).toBe(true); + }); + + it('returns true for xdr object', () => { + expect(isStellarTrade({ xdr: 'AAAABg==' } as unknown as Trade)).toBe( + true, + ); + }); + + it('returns false for Tron trade', () => { + expect( + isStellarTrade({ + raw_data_hex: 'ab', + } as unknown as Trade), + ).toBe(false); + }); + }); + describe('extractTradeData', () => { it('returns string as-is for Solana trades', () => { const solanaTrade = 'base64EncodedSolanaTransaction'; expect(extractTradeData(solanaTrade)).toBe(solanaTrade); }); + it('returns xdrBase64 for Stellar trade object', () => { + expect( + extractTradeData({ + xdrBase64: 'stellarXdrPayload', + } as unknown as Trade), + ).toBe('stellarXdrPayload'); + }); + + it('returns xdr for Stellar trade object with xdr key', () => { + expect( + extractTradeData({ + xdr: 'stellarXdrAlt', + } as unknown as Trade), + ).toBe('stellarXdrAlt'); + }); + it('extracts data property from EVM TxData object', () => { const evmTxData: TxData = { chainId: 1, diff --git a/packages/bridge-controller/src/utils/trade-utils.ts b/packages/bridge-controller/src/utils/trade-utils.ts index 0e78b063da..568b115236 100644 --- a/packages/bridge-controller/src/utils/trade-utils.ts +++ b/packages/bridge-controller/src/utils/trade-utils.ts @@ -1,7 +1,17 @@ -import type { BitcoinTradeData, TronTradeData, TxData } from '../types'; +import type { + BitcoinTradeData, + StellarTradeData, + TronTradeData, + TxData, +} from '../types'; -// Union type representing all possible trade formats (EVM, Solana, Bitcoin, Tron) -export type Trade = TxData | string | BitcoinTradeData | TronTradeData; +// Union type representing all possible trade formats (EVM, Solana, Bitcoin, Tron, Stellar) +export type Trade = + | TxData + | string + | BitcoinTradeData + | TronTradeData + | StellarTradeData; /** * Type guard to check if a trade is an EVM TxData object @@ -41,6 +51,25 @@ export const isTronTrade = (trade: Trade): trade is TronTradeData => { return typeof trade === 'object' && trade !== null && 'raw_data_hex' in trade; }; +/** + * Type guard to check if a trade is a Stellar trade with XDR (base64) payload + */ +export const isStellarTrade = (trade: Trade): trade is StellarTradeData => { + if (typeof trade !== 'object' || trade === null) { + return false; + } + if ( + 'xdrBase64' in trade && + typeof (trade as { xdrBase64: unknown }).xdrBase64 === 'string' + ) { + return true; + } + if ('xdr' in trade && typeof (trade as { xdr: unknown }).xdr === 'string') { + return true; + } + return false; +}; + /** * Extracts the transaction data from different trade formats * @@ -59,6 +88,10 @@ export const extractTradeData = (trade: Trade): string => { return Buffer.from(trade.raw_data_hex, 'hex').toString('base64'); } + if (isStellarTrade(trade)) { + return 'xdrBase64' in trade ? trade.xdrBase64 : trade.xdr; + } + if (typeof trade === 'string') { // Solana txs - assuming already in correct format return trade; diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 12cd1780b3..1307506182 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -462,6 +462,14 @@ export const TronTradeDataSchema = type({ ), }); +/** + * Stellar bridge quote: unsigned transaction envelope as XDR (base64). + */ +export const StellarTradeDataSchema = union([ + type({ xdrBase64: string() }), + type({ xdr: string() }), +]); + export const QuoteResponseSchema = type({ quote: QuoteSchema, estimatedProcessingTimeInSeconds: number(), @@ -470,6 +478,7 @@ export const QuoteResponseSchema = type({ TxDataSchema, BitcoinTradeDataSchema, TronTradeDataSchema, + StellarTradeDataSchema, string(), ]), }); diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 2c06f032ac..ddde466ac7 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -7,38 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed - -- **BREAKING:** The service messenger now requires the `SnapAccountService:ensureReady` action to be declared ([#8715](https://github.com/MetaMask/core/pull/8715)) -- **BREAKING:** Delegate Snap platform readiness to `@metamask/snap-account-service` ([#8715](https://github.com/MetaMask/core/pull/8715)) - - Removed `MultichainAccountService.ensureCanUseSnapPlatform()` method and the corresponding `MultichainAccountService:ensureCanUseSnapPlatform` messenger action. - - Removed the `MultichainAccountServiceEnsureCanUseSnapPlatformAction` type export. - - Removed `MultichainAccountServiceOptions.ensureOnboardingComplete`. Configure it via `SnapAccountService`'s `config.snapPlatformWatcher.ensureOnboardingComplete` instead. - - Removed `MultichainAccountServiceConfig.snapPlatformWatcher` and the `SnapPlatformWatcherConfig` type export. Configure the keyring-wait timeout via `SnapAccountService`'s `config.snapPlatformWatcher.snapKeyringWaitTimeoutMs` instead. - - The service messenger no longer needs `SnapController:getState` or `SnapController:stateChange`. -- **BREAKING:** Rename `SnapAccountProvider.ensureCanUseSnapPlatform()` to `ensureReady()` ([#8715](https://github.com/MetaMask/core/pull/8715)) - -## [9.0.0] - ### Added -- Expose missing `MultichainAccountService:init` action through its messenger ([#8717](https://github.com/MetaMask/core/pull/8717)) - - Corresponding action type is available as well. -- Filter out `KeyringController` locked errors from sentry reporting ([#8619](https://github.com/MetaMask/core/pull/8619)) - -### Changed - -- **BREAKING:** Replace `KeyringController:withKeyring` with `KeyringController:withKeyringV2` for the EVM account provider ([#8491](https://github.com/MetaMask/core/pull/8491)) -- Bump `@metamask/accounts-controller` from `^37.1.1` to `^38.0.0` ([#8363](https://github.com/MetaMask/core/pull/8363), [#8665](https://github.com/MetaMask/core/pull/8665)) -- Bump `@metamask/keyring-controller` from `^25.1.1` to `^25.5.0` ([#8363](https://github.com/MetaMask/core/pull/8363), [#8634](https://github.com/MetaMask/core/pull/8634), [#8665](https://github.com/MetaMask/core/pull/8665), [#8722](https://github.com/MetaMask/core/pull/8722)) -- Bump `@metamask/messenger` from `^1.0.0` to `^1.2.0` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373), [#8632](https://github.com/MetaMask/core/pull/8632)) -- Bump `@metamask/base-controller` from `^9.0.1` to `^9.1.0` ([#8457](https://github.com/MetaMask/core/pull/8457)) -- Bump `@metamask/account-api` from `^1.0.0` to `^1.0.4` ([#8464](https://github.com/MetaMask/core/pull/8464), [#8647](https://github.com/MetaMask/core/pull/8647)) -- Bump `@metamask/eth-snap-keyring` from `^19.0.0` to `^22.0.1` ([#8464](https://github.com/MetaMask/core/pull/8464), [#8584](https://github.com/MetaMask/core/pull/8584), [#8647](https://github.com/MetaMask/core/pull/8647)) -- Bump `@metamask/keyring-api` from `^21.6.0` to `^23.1.0` ([#8464](https://github.com/MetaMask/core/pull/8464), [#8647](https://github.com/MetaMask/core/pull/8647)) -- Bump `@metamask/keyring-internal-api` from `^10.0.0` to `^11.0.1` ([#8464](https://github.com/MetaMask/core/pull/8464), [#8584](https://github.com/MetaMask/core/pull/8584), [#8647](https://github.com/MetaMask/core/pull/8647)) -- Bump `@metamask/keyring-snap-client` from `^8.2.0` to `^9.0.2` ([#8464](https://github.com/MetaMask/core/pull/8464), [#8647](https://github.com/MetaMask/core/pull/8647)) -- Bump `@metamask/keyring-utils` from `^3.1.0` to `^3.2.1` ([#8703](https://github.com/MetaMask/core/pull/8703)) +- Add `XlmAccountProvider` for Stellar (XLM) snap-backed accounts, including `XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG` and package export ([#TODO](https://github.com/MetaMask/core/pull/TODO)) ## [8.0.1] diff --git a/packages/multichain-account-service/package.json b/packages/multichain-account-service/package.json index 7039296e0a..e80c2b70af 100644 --- a/packages/multichain-account-service/package.json +++ b/packages/multichain-account-service/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-account-service", - "version": "9.0.0", + "version": "8.0.1-dev.67", "description": "Service to manage multichain accounts", "keywords": [ "Ethereum", diff --git a/packages/multichain-account-service/src/MultichainAccountService.test.ts b/packages/multichain-account-service/src/MultichainAccountService.test.ts index c3d11af4fe..bdadcc7364 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.test.ts @@ -10,6 +10,7 @@ import { EthAccountType, SolAccountType, TrxAccountType, + XlmAccountType, } from '@metamask/keyring-api'; import type { Keyring } from '@metamask/keyring-api/v2'; import { KeyringType } from '@metamask/keyring-api/v2'; @@ -38,6 +39,11 @@ import { TRX_ACCOUNT_PROVIDER_NAME, TrxAccountProvider, } from './providers/TrxAccountProvider'; +import { + XLM_ACCOUNT_PROVIDER_NAME, + XlmAccountProvider, +} from './providers/XlmAccountProvider'; +import { SnapPlatformWatcher } from './snaps/SnapPlatformWatcher'; import type { RootMessenger, MockAccountProvider } from './tests'; import { MOCK_HARDWARE_ACCOUNT_1, @@ -90,6 +96,12 @@ jest.mock('./providers/TrxAccountProvider', () => { TrxAccountProvider: jest.fn(), }; }); +jest.mock('./providers/XlmAccountProvider', () => { + return { + ...jest.requireActual('./providers/XlmAccountProvider'), + XlmAccountProvider: jest.fn(), + }; +}); type Mocks = { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -119,6 +131,8 @@ type Mocks = { BtcAccountProvider: MockAccountProvider; // eslint-disable-next-line @typescript-eslint/naming-convention TrxAccountProvider: MockAccountProvider; + // eslint-disable-next-line @typescript-eslint/naming-convention + XlmAccountProvider: MockAccountProvider; }; function mockAccountProvider( @@ -160,6 +174,11 @@ function mockAccountProvider( mocks.isAccountCompatible?.mockImplementation( (account: KeyringAccount) => account.type === TrxAccountType.Eoa, ); + } else if (providerClass === (XlmAccountProvider as unknown)) { + mocks.getName.mockReturnValue(XLM_ACCOUNT_PROVIDER_NAME); + mocks.isAccountCompatible?.mockImplementation( + (account: KeyringAccount) => account.type === XlmAccountType.Account, + ); } } @@ -202,6 +221,7 @@ async function setup({ SolAccountProvider: makeMockAccountProvider(), BtcAccountProvider: makeMockAccountProvider(), TrxAccountProvider: makeMockAccountProvider(), + XlmAccountProvider: makeMockAccountProvider(), }; // Required for the `assert` on `MultichainAccountWallet.createMultichainAccountGroup`. @@ -258,6 +278,7 @@ async function setup({ SolAccountProvider.NAME = SOL_ACCOUNT_PROVIDER_NAME; BtcAccountProvider.NAME = BTC_ACCOUNT_PROVIDER_NAME; TrxAccountProvider.NAME = TRX_ACCOUNT_PROVIDER_NAME; + XlmAccountProvider.NAME = XLM_ACCOUNT_PROVIDER_NAME; mockAccountProvider( EvmAccountProvider, @@ -287,6 +308,13 @@ async function setup({ 3, TrxAccountType.Eoa, ); + mockAccountProvider( + XlmAccountProvider, + mocks.XlmAccountProvider, + accounts, + 4, + XlmAccountType.Account, + ); } const messenger = getMultichainAccountServiceMessenger(rootMessenger); diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index aeea53a201..2edfd7c5fc 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -25,10 +25,13 @@ import { EVM_ACCOUNT_PROVIDER_NAME, BtcAccountProviderConfig, TrxAccountProviderConfig, + XlmAccountProviderConfig, BTC_ACCOUNT_PROVIDER_NAME, TRX_ACCOUNT_PROVIDER_NAME, + XLM_ACCOUNT_PROVIDER_NAME, BtcAccountProvider, TrxAccountProvider, + XlmAccountProvider, } from './providers'; import { AccountProviderWrapper, @@ -58,6 +61,7 @@ export type MultichainAccountServiceOptions = { [SOL_ACCOUNT_PROVIDER_NAME]?: SolAccountProviderConfig; [BTC_ACCOUNT_PROVIDER_NAME]?: BtcAccountProviderConfig; [TRX_ACCOUNT_PROVIDER_NAME]?: TrxAccountProviderConfig; + [XLM_ACCOUNT_PROVIDER_NAME]?: XlmAccountProviderConfig; }; config?: MultichainAccountServiceConfig; }; @@ -196,6 +200,14 @@ export class MultichainAccountService { trace, ), ), + new AccountProviderWrapper( + this.#messenger, + new XlmAccountProvider( + this.#messenger, + providerConfigs?.[XLM_ACCOUNT_PROVIDER_NAME], + trace, + ), + ), // Custom account providers that can be provided by the MetaMask client. ...providers, ]; diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts index 705ff8a9cd..57d3c09788 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts @@ -300,7 +300,7 @@ describe('MultichainAccountWallet', () => { ).rejects.toThrow('Unable to create accounts'); expect(captureExceptionSpy).toHaveBeenCalledWith( new Error( - 'Unable to create some accounts with provider "Mocked Provider 0"', + 'Unable to create accounts with provider "Mocked Provider 0" (group indices 1–1)', ), ); expect(captureExceptionSpy.mock.lastCall[0]).toHaveProperty( @@ -609,7 +609,7 @@ describe('MultichainAccountWallet', () => { ).rejects.toThrow(`Bad range, to (${badIndex}) must be >= 0`); }); - it('captures an error with batch mode message when EVM provider fails', async () => { + it('captures an error with group index range message when EVM provider fails', async () => { const { wallet, providers, messenger } = setup({ accounts: [[]], }); @@ -626,7 +626,7 @@ describe('MultichainAccountWallet', () => { expect(captureExceptionSpy).toHaveBeenCalledWith( new Error( - 'Unable to create some accounts (batch) with provider "Mocked Provider 0"', + 'Unable to create accounts with provider "Mocked Provider 0" (group indices 0–2)', ), ); expect(captureExceptionSpy.mock.lastCall[0]).toHaveProperty( diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index d38114eefe..0aca148d89 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -237,8 +237,6 @@ export class MultichainAccountWallet< from: number, to: number, ): Promise[]> { - const isBatching = to > from; - try { return await provider.createAccounts({ type: AccountCreationType.Bip44DeriveIndexRange, @@ -251,12 +249,12 @@ export class MultichainAccountWallet< } catch (error) { reportError( this.#messenger, - `Unable to create ${isBatching ? 'some accounts (batch)' : 'some accounts'} with provider "${provider.getName()}"`, + `Unable to create accounts with provider "${provider.getName()}" (group indices ${from}–${to})`, error, { range: { from, to }, provider: provider.getName(), - isBatching, + spansMultipleGroupIndices: to > from, }, ); throw error; diff --git a/packages/multichain-account-service/src/index.ts b/packages/multichain-account-service/src/index.ts index 2c6dec78e8..512c5c0801 100644 --- a/packages/multichain-account-service/src/index.ts +++ b/packages/multichain-account-service/src/index.ts @@ -35,6 +35,8 @@ export { BtcAccountProvider, TRX_ACCOUNT_PROVIDER_NAME, TrxAccountProvider, + XLM_ACCOUNT_PROVIDER_NAME, + XlmAccountProvider, } from './providers'; export { MultichainAccountWallet } from './MultichainAccountWallet'; export { MultichainAccountGroup } from './MultichainAccountGroup'; diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts index 70a8931ff6..f7a47606a6 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts @@ -13,8 +13,8 @@ import type { CreateAccountOptions, DeleteAccountRequest, GetAccountRequest, - KeyringCapabilities, } from '@metamask/keyring-api'; +import type { KeyringCapabilities } from '@metamask/keyring-api/v2'; import type { EntropySourceId, KeyringAccount } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { JsonRpcRequest, SnapId } from '@metamask/snaps-sdk'; diff --git a/packages/multichain-account-service/src/providers/XlmAccountProvider.test.ts b/packages/multichain-account-service/src/providers/XlmAccountProvider.test.ts new file mode 100644 index 0000000000..0c2d2942ef --- /dev/null +++ b/packages/multichain-account-service/src/providers/XlmAccountProvider.test.ts @@ -0,0 +1,286 @@ +import { isBip44Account } from '@metamask/account-api'; +import type { SnapKeyring } from '@metamask/eth-snap-keyring'; +import { AccountCreationType } from '@metamask/keyring-api'; +import type { KeyringMetadata } from '@metamask/keyring-controller'; +import type { + EthKeyring, + InternalAccount, +} from '@metamask/keyring-internal-api'; +import { SnapControllerState } from '@metamask/snaps-controllers'; +import type { Json } from '@metamask/utils'; +import deepmerge from 'deepmerge'; + +import { + getMultichainAccountServiceMessenger, + getRootMessenger, + MOCK_HD_KEYRING_2, + MOCK_XLM_ACCOUNT_1, + MockAccountBuilder, + toGroupIndexRangeArray, +} from '../tests'; +import type { RootMessenger, DeepPartial } from '../tests'; +import { AccountProviderWrapper } from './AccountProviderWrapper'; +import type { SnapAccountProviderConfig } from './SnapAccountProvider'; +import { + XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG, + XLM_ACCOUNT_PROVIDER_NAME, + XlmAccountProvider, +} from './XlmAccountProvider'; + +function asConfig( + partial: DeepPartial, +): SnapAccountProviderConfig { + return deepmerge( + XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG, + partial, + ) as SnapAccountProviderConfig; +} + +class MockStellarKeyring { + readonly type = 'MockStellarKeyring'; + + readonly metadata: KeyringMetadata = { + id: 'mock-stellar-keyring-id', + name: '', + }; + + readonly accounts: InternalAccount[]; + + constructor(accounts: InternalAccount[]) { + this.accounts = accounts; + } + + createAccount: SnapKeyring['createAccount'] = jest + .fn() + .mockImplementation((_, options: Record) => { + const { index } = options; + if (typeof index === 'number') { + const found = this.accounts.find( + (account) => + isBip44Account(account) && + account.options.entropy.groupIndex === index, + ); + + if (found) { + return found; + } + } + + const account = MockAccountBuilder.from(MOCK_XLM_ACCOUNT_1) + .withUuid() + .withAddressSuffix(`${this.accounts.length}`) + .withGroupIndex(typeof index === 'number' ? index : this.accounts.length) + .get(); + this.accounts.push(account); + + return account; + }); + + createAccounts: SnapKeyring['createAccounts'] = jest + .fn() + .mockImplementation((_, options) => { + const groupIndices = + options.type === 'bip44:derive-index' + ? [options.groupIndex] + : toGroupIndexRangeArray(options.range); + + return groupIndices.map((groupIndex) => { + const found = this.accounts.find( + (account) => + isBip44Account(account) && + account.options.entropy.groupIndex === groupIndex, + ); + + if (found) { + return found; + } + + const account = MockAccountBuilder.from(MOCK_XLM_ACCOUNT_1) + .withUuid() + .withAddressSuffix(`${groupIndex}`) + .withGroupIndex(groupIndex) + .get(); + this.accounts.push(account); + return account; + }); + }); +} + +class MockXlmAccountProvider extends XlmAccountProvider { + override async ensureCanUseSnapPlatform(): Promise { + // Override to avoid waiting during tests. + } +} + +function setup({ + messenger = getRootMessenger(), + accounts = [], + config, +}: { + messenger?: RootMessenger; + accounts?: InternalAccount[]; + config?: SnapAccountProviderConfig; +} = {}): { + provider: AccountProviderWrapper; + messenger: RootMessenger; + keyring: MockStellarKeyring; + mocks: { + handleRequest: jest.Mock; + keyring: { + createAccount: jest.Mock; + createAccounts: jest.Mock; + }; + trace: jest.Mock; + }; +} { + const keyring = new MockStellarKeyring(accounts); + + messenger.registerActionHandler( + 'AccountsController:getAccounts', + () => accounts, + ); + + messenger.registerActionHandler( + 'SnapController:getState', + () => ({ isReady: true }) as SnapControllerState, + ); + + messenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + () => accounts, + ); + + const mockGetAccount = jest.fn().mockImplementation((id) => { + return keyring.accounts.find((account) => account.id === id); + }); + messenger.registerActionHandler( + 'AccountsController:getAccount', + mockGetAccount, + ); + + const mockHandleRequest = jest + .fn() + .mockImplementation((address: string) => + keyring.accounts.find((account) => account.address === address), + ); + + const mockTrace = jest.fn().mockImplementation(async (_request, fn) => { + return await fn(); + }); + + messenger.registerActionHandler( + 'SnapController:handleRequest', + mockHandleRequest, + ); + + messenger.registerActionHandler( + 'KeyringController:withKeyring', + async (_, operation) => + operation({ + keyring: keyring as unknown as EthKeyring, + metadata: keyring.metadata, + }), + ); + + const multichainMessenger = getMultichainAccountServiceMessenger(messenger); + const xlmProvider = new MockXlmAccountProvider( + multichainMessenger, + config, + mockTrace, + ); + const accountIds = accounts.map((account) => account.id); + xlmProvider.init(accountIds); + const provider = new AccountProviderWrapper(multichainMessenger, xlmProvider); + + return { + provider, + messenger, + keyring, + mocks: { + handleRequest: mockHandleRequest, + keyring: { + createAccount: keyring.createAccount as jest.Mock, + createAccounts: keyring.createAccounts as jest.Mock, + }, + trace: mockTrace, + }, + }; +} + +describe('XlmAccountProvider', () => { + it('getName returns Stellar', () => { + const { provider } = setup({ accounts: [] }); + expect(provider.getName()).toBe(XLM_ACCOUNT_PROVIDER_NAME); + }); + + describe('v1', () => { + it('uses createAccount when batching is disabled', async () => { + const accounts = [MOCK_XLM_ACCOUNT_1]; + const { provider, mocks } = setup({ + accounts, + config: asConfig({ createAccounts: { batched: false } }), + }); + + await provider.createAccounts({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: accounts.length, + }); + + expect(mocks.keyring.createAccount).toHaveBeenCalled(); + expect(mocks.keyring.createAccounts).not.toHaveBeenCalled(); + }); + }); + + describe('v2 - batched', () => { + it('creates one account via createAccounts', async () => { + const accounts = [MOCK_XLM_ACCOUNT_1]; + const { provider, mocks } = setup({ + accounts, + config: asConfig({ createAccounts: { batched: true } }), + }); + + const newGroupIndex = accounts.length; + const newAccounts = await provider.createAccounts({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: newGroupIndex, + }); + + expect(newAccounts).toHaveLength(1); + expect(mocks.keyring.createAccounts).toHaveBeenCalledWith( + XlmAccountProvider.XLM_SNAP_ID, + { + type: AccountCreationType.Bip44DeriveIndex, + entropySource: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: newGroupIndex, + }, + ); + expect(mocks.keyring.createAccount).not.toHaveBeenCalled(); + }); + + it('creates multiple accounts using Bip44DeriveIndexRange', async () => { + const accounts = [MOCK_XLM_ACCOUNT_1]; + const { provider, mocks } = setup({ + accounts, + config: asConfig({ createAccounts: { batched: true } }), + }); + + const from = 1; + const newAccounts = await provider.createAccounts({ + type: AccountCreationType.Bip44DeriveIndexRange, + entropySource: MOCK_HD_KEYRING_2.metadata.id, + range: { from, to: 3 }, + }); + + expect(newAccounts).toHaveLength(3); + expect(mocks.keyring.createAccounts).toHaveBeenCalledTimes(1); + expect(mocks.keyring.createAccount).not.toHaveBeenCalled(); + + for (const [index, account] of newAccounts.entries()) { + expect(isBip44Account(account)).toBe(true); + expect(account.options.entropy.groupIndex).toBe(from + index); + } + }); + }); +}); diff --git a/packages/multichain-account-service/src/providers/XlmAccountProvider.ts b/packages/multichain-account-service/src/providers/XlmAccountProvider.ts new file mode 100644 index 0000000000..55e109d5a3 --- /dev/null +++ b/packages/multichain-account-service/src/providers/XlmAccountProvider.ts @@ -0,0 +1,145 @@ +import type { Bip44Account } from '@metamask/account-api'; +import type { TraceCallback } from '@metamask/controller-utils'; +import type { + EntropySourceId, + KeyringAccount, +} from '@metamask/keyring-api'; +import type { KeyringCapabilities } from '@metamask/keyring-api/v2'; +import { + AccountCreationType, + XlmAccountType, + XlmScope, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { SnapId } from '@metamask/snaps-sdk'; + +import { SnapAccountProvider } from './SnapAccountProvider'; +import type { + RestrictedSnapKeyring, + SnapAccountProviderConfig, +} from './SnapAccountProvider'; +import { withRetry, withTimeout } from './utils'; +import { traceFallback } from '../analytics'; +import { TraceName } from '../analytics/traces'; +import type { MultichainAccountServiceMessenger } from '../types'; + +export type XlmAccountProviderConfig = SnapAccountProviderConfig; + +export const XLM_ACCOUNT_PROVIDER_NAME = 'Stellar'; + +export const XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG: XlmAccountProviderConfig = { + maxConcurrency: 3, + discovery: { + enabled: true, + timeoutMs: 2000, + maxAttempts: 3, + backOffMs: 1000, + }, + createAccounts: { + batched: true, + timeoutMs: 10000, + }, + resyncAccounts: { + autoRemoveExtraSnapAccounts: true, + }, +}; + +export class XlmAccountProvider extends SnapAccountProvider { + static NAME = XLM_ACCOUNT_PROVIDER_NAME; + + static XLM_SNAP_ID = 'npm:@metamask/stellar-wallet-snap' as SnapId; + + readonly capabilities: KeyringCapabilities = { + scopes: [XlmScope.Pubnet, XlmScope.Testnet], + bip44: { + deriveIndex: true, + deriveIndexRange: true, + }, + }; + + constructor( + messenger: MultichainAccountServiceMessenger, + config: XlmAccountProviderConfig = XLM_ACCOUNT_PROVIDER_DEFAULT_CONFIG, + trace: TraceCallback = traceFallback, + ) { + super(XlmAccountProvider.XLM_SNAP_ID, messenger, config, trace); + } + + getName(): string { + return XlmAccountProvider.NAME; + } + + isAccountCompatible(account: Bip44Account): boolean { + return ( + account.type === XlmAccountType.Account && + account.metadata.keyring.type === (KeyringTypes.snap as string) + ); + } + + protected override createAccountV1( + keyring: RestrictedSnapKeyring, + { + entropySource, + groupIndex, + }: { entropySource: EntropySourceId; groupIndex: number }, + ): Promise { + return keyring.createAccount({ + entropySource, + index: groupIndex, + addressType: XlmAccountType.Account, + scope: XlmScope.Pubnet, + }); + } + + async discoverAccounts({ + entropySource, + groupIndex, + }: { + entropySource: EntropySourceId; + groupIndex: number; + }): Promise[]> { + return this.withSnap(async ({ client, keyring }) => { + return await super.trace( + { + name: TraceName.SnapDiscoverAccounts, + data: { + provider: this.getName(), + }, + }, + async () => { + if (!this.config.discovery.enabled) { + return []; + } + + const discoveredAccounts = await withRetry( + () => + withTimeout( + () => + client.discoverAccounts( + [XlmScope.Pubnet], + entropySource, + groupIndex, + ), + this.config.discovery.timeoutMs, + ), + { + maxAttempts: this.config.discovery.maxAttempts, + backOffMs: this.config.discovery.backOffMs, + }, + ); + + if (!discoveredAccounts.length) { + return []; + } + + return await this.createBip44Accounts(keyring, { + type: AccountCreationType.Bip44DeriveIndex, + entropySource, + groupIndex, + }); + }, + ); + }); + } +} diff --git a/packages/multichain-account-service/src/providers/index.ts b/packages/multichain-account-service/src/providers/index.ts index 0504f99fea..51f20af248 100644 --- a/packages/multichain-account-service/src/providers/index.ts +++ b/packages/multichain-account-service/src/providers/index.ts @@ -10,3 +10,4 @@ export * from './SolAccountProvider'; export * from './EvmAccountProvider'; export * from './BtcAccountProvider'; export * from './TrxAccountProvider'; +export * from './XlmAccountProvider'; diff --git a/packages/multichain-account-service/src/tests/accounts.ts b/packages/multichain-account-service/src/tests/accounts.ts index 2eab252362..4307de1d0e 100644 --- a/packages/multichain-account-service/src/tests/accounts.ts +++ b/packages/multichain-account-service/src/tests/accounts.ts @@ -19,6 +19,9 @@ import { TrxAccountType, TrxMethod, TrxScope, + XlmAccountType, + XlmMethod, + XlmScope, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; @@ -149,6 +152,31 @@ export const MOCK_SOL_ACCOUNT_1: Bip44Account = { }, }; +const XLM_METHODS = Object.values(XlmMethod); + +export const MOCK_XLM_ACCOUNT_1: Bip44Account = { + id: 'mock-snap-id-1', + address: `G${'A'.repeat(55)}`, + options: { + entropy: { + type: KeyringAccountEntropyTypeOption.Mnemonic, + id: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: 0, + derivationPath: '', + }, + }, + methods: XLM_METHODS, + type: XlmAccountType.Account, + scopes: [XlmScope.Pubnet, XlmScope.Testnet], + metadata: { + name: 'Stellar Account 1', + keyring: { type: KeyringTypes.snap }, + snap: MOCK_SNAP_1, + importTime: 0, + lastSelected: 0, + }, +}; + export const MOCK_TRX_ACCOUNT_1: Bip44Account = { id: 'mock-snap-id-1', address: 'aabbccdd', @@ -185,6 +213,12 @@ export const MOCK_TRX_DISCOVERED_ACCOUNT_1: DiscoveredAccount = { derivationPath: `m/44'/195'/0'/0'`, }; +export const MOCK_XLM_DISCOVERED_ACCOUNT_1: DiscoveredAccount = { + type: 'bip44', + scopes: [XlmScope.Pubnet], + derivationPath: `m/44'/148'/0'`, +}; + export const MOCK_BTC_P2TR_DISCOVERED_ACCOUNT_1: DiscoveredAccount = { type: 'bip44', scopes: [BtcScope.Mainnet], diff --git a/packages/multichain-account-service/src/tests/providers.ts b/packages/multichain-account-service/src/tests/providers.ts index 2c92fca4ba..e88c66256d 100644 --- a/packages/multichain-account-service/src/tests/providers.ts +++ b/packages/multichain-account-service/src/tests/providers.ts @@ -1,5 +1,11 @@ import type { Bip44Account } from '@metamask/account-api'; -import { BtcScope, EthScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { + BtcScope, + EthScope, + SolScope, + TrxScope, + XlmScope, +} from '@metamask/keyring-api'; import type { KeyringAccount } from '@metamask/keyring-api'; import type { KeyringCapabilities } from '@metamask/keyring-api/v2'; @@ -37,6 +43,7 @@ export function makeMockAccountProvider( SolScope.Testnet, BtcScope.Testnet, TrxScope.Shasta, + XlmScope.Testnet, EthScope.Eoa, ], bip44: { deriveIndex: true }, diff --git a/packages/multichain-network-controller/CHANGELOG.md b/packages/multichain-network-controller/CHANGELOG.md index 936bf61f56..1cbe3e0ba9 100644 --- a/packages/multichain-network-controller/CHANGELOG.md +++ b/packages/multichain-network-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add Stellar pubnet (`stellar:pubnet`) and Stellar testnet (`stellar:testnet`) to multichain network configurations (`AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS`), native asset CAIP-19 constants, metadata, tickers, decimal places, and `SupportedCaipChainId`; register testnet in `NON_EVM_TESTNET_IDS` ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + ## [3.1.0] ### Added diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index 9ad189462e..3b312df561 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-network-controller", - "version": "3.1.0", + "version": "3.1.0-dev.2", "description": "Multichain network controller", "keywords": [ "Ethereum", diff --git a/packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts index 240743acec..fefd952ca6 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts @@ -778,6 +778,18 @@ describe('MultichainNetworkController', () => { "name": "Solana Devnet", "nativeCurrency": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501", }, + "stellar:pubnet": { + "chainId": "stellar:pubnet", + "isEvm": false, + "name": "Stellar", + "nativeCurrency": "stellar:pubnet/slip44:148", + }, + "stellar:testnet": { + "chainId": "stellar:testnet", + "isEvm": false, + "name": "Stellar Testnet", + "nativeCurrency": "stellar:testnet/slip44:148", + }, "tron:2494104990": { "chainId": "tron:2494104990", "isEvm": false, @@ -864,6 +876,18 @@ describe('MultichainNetworkController', () => { "name": "Solana Devnet", "nativeCurrency": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501", }, + "stellar:pubnet": { + "chainId": "stellar:pubnet", + "isEvm": false, + "name": "Stellar", + "nativeCurrency": "stellar:pubnet/slip44:148", + }, + "stellar:testnet": { + "chainId": "stellar:testnet", + "isEvm": false, + "name": "Stellar Testnet", + "nativeCurrency": "stellar:testnet/slip44:148", + }, "tron:2494104990": { "chainId": "tron:2494104990", "isEvm": false, @@ -950,6 +974,18 @@ describe('MultichainNetworkController', () => { "name": "Solana Devnet", "nativeCurrency": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501", }, + "stellar:pubnet": { + "chainId": "stellar:pubnet", + "isEvm": false, + "name": "Stellar", + "nativeCurrency": "stellar:pubnet/slip44:148", + }, + "stellar:testnet": { + "chainId": "stellar:testnet", + "isEvm": false, + "name": "Stellar Testnet", + "nativeCurrency": "stellar:testnet/slip44:148", + }, "tron:2494104990": { "chainId": "tron:2494104990", "isEvm": false, @@ -1036,6 +1072,18 @@ describe('MultichainNetworkController', () => { "name": "Solana Devnet", "nativeCurrency": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501", }, + "stellar:pubnet": { + "chainId": "stellar:pubnet", + "isEvm": false, + "name": "Stellar", + "nativeCurrency": "stellar:pubnet/slip44:148", + }, + "stellar:testnet": { + "chainId": "stellar:testnet", + "isEvm": false, + "name": "Stellar Testnet", + "nativeCurrency": "stellar:testnet/slip44:148", + }, "tron:2494104990": { "chainId": "tron:2494104990", "isEvm": false, diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index 262e79aa6d..8fe8205881 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -1,5 +1,5 @@ import type { StateMetadata } from '@metamask/base-controller'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import type { CaipChainId } from '@metamask/keyring-api'; import { NetworkStatus } from '@metamask/network-controller'; @@ -21,6 +21,8 @@ export const SOL_DEVNET_NATIVE_ASSET = `${SolScope.Devnet}/slip44:501`; export const TRX_NATIVE_ASSET = `${TrxScope.Mainnet}/slip44:195`; export const TRX_NILE_NATIVE_ASSET = `${TrxScope.Nile}/slip44:195`; export const TRX_SHASTA_NATIVE_ASSET = `${TrxScope.Shasta}/slip44:195`; +export const XLM_NATIVE_ASSET = `${XlmScope.Pubnet}/slip44:148`; +export const XLM_TESTNET_NATIVE_ASSET = `${XlmScope.Testnet}/slip44:148`; /** * Supported networks by the MultichainNetworkController @@ -95,6 +97,18 @@ export const AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS: Record< nativeCurrency: TRX_SHASTA_NATIVE_ASSET, isEvm: false, }, + [XlmScope.Pubnet]: { + chainId: XlmScope.Pubnet, + name: 'Stellar', + nativeCurrency: XLM_NATIVE_ASSET, + isEvm: false, + }, + [XlmScope.Testnet]: { + chainId: XlmScope.Testnet, + name: 'Stellar Testnet', + nativeCurrency: XLM_TESTNET_NATIVE_ASSET, + isEvm: false, + }, }; /** @@ -111,6 +125,7 @@ export const NON_EVM_TESTNET_IDS: CaipChainId[] = [ SolScope.Devnet, TrxScope.Nile, TrxScope.Shasta, + XlmScope.Testnet, ]; /** @@ -129,6 +144,10 @@ export const NETWORKS_METADATA: Record = { features: [], status: NetworkStatus.Available, }, + [XlmScope.Pubnet]: { + features: [], + status: NetworkStatus.Available, + }, }; /** @@ -195,6 +214,8 @@ export const MULTICHAIN_NETWORK_TICKER: Record = { [TrxScope.Mainnet]: 'TRX', [TrxScope.Nile]: 'tTRX', [TrxScope.Shasta]: 'sTRX', + [XlmScope.Pubnet]: 'XLM', + [XlmScope.Testnet]: 'tXLM', } as const; /** @@ -213,4 +234,6 @@ export const MULTICHAIN_NETWORK_DECIMAL_PLACES: Record = { [TrxScope.Mainnet]: 6, [TrxScope.Nile]: 6, [TrxScope.Shasta]: 6, + [XlmScope.Pubnet]: 7, + [XlmScope.Testnet]: 7, } as const; diff --git a/packages/multichain-network-controller/src/types.ts b/packages/multichain-network-controller/src/types.ts index 6988307298..4b4b42b6d4 100644 --- a/packages/multichain-network-controller/src/types.ts +++ b/packages/multichain-network-controller/src/types.ts @@ -9,6 +9,7 @@ import type { CaipChainId, SolScope, TrxScope, + XlmScope, } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { Messenger } from '@metamask/messenger'; @@ -43,7 +44,9 @@ export type SupportedCaipChainId = | SolScope.Devnet | TrxScope.Mainnet | TrxScope.Nile - | TrxScope.Shasta; + | TrxScope.Shasta + | XlmScope.Pubnet + | XlmScope.Testnet; export type CommonNetworkConfiguration = { /** diff --git a/packages/multichain-network-controller/src/utils.test.ts b/packages/multichain-network-controller/src/utils.test.ts index c865fcf610..83ceaacb05 100644 --- a/packages/multichain-network-controller/src/utils.test.ts +++ b/packages/multichain-network-controller/src/utils.test.ts @@ -1,4 +1,4 @@ -import { BtcScope, SolScope, EthScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, EthScope, XlmScope } from '@metamask/keyring-api'; import type { CaipChainId } from '@metamask/keyring-api'; import type { NetworkConfiguration } from '@metamask/network-controller'; import { KnownCaipNamespace } from '@metamask/utils'; @@ -29,6 +29,11 @@ describe('utils', () => { expect(getChainIdForNonEvm(scopes)).toBe(BtcScope.Testnet); }); + it('returns Stellar chain ID for Stellar scopes', () => { + const scopes = [XlmScope.Pubnet, XlmScope.Testnet]; + expect(getChainIdForNonEvm(scopes)).toBe(XlmScope.Pubnet); + }); + it('throws error if network is not found', () => { const scopes = ['unknown:scope' as CaipChainId]; expect(() => getChainIdForNonEvm(scopes)).toThrow( @@ -41,6 +46,8 @@ describe('utils', () => { it('returns true for supported CAIP chain IDs', () => { expect(checkIfSupportedCaipChainId(SolScope.Mainnet)).toBe(true); expect(checkIfSupportedCaipChainId(BtcScope.Mainnet)).toBe(true); + expect(checkIfSupportedCaipChainId(XlmScope.Pubnet)).toBe(true); + expect(checkIfSupportedCaipChainId(XlmScope.Testnet)).toBe(true); }); it('returns false for non-CAIP IDs', () => { diff --git a/packages/network-enablement-controller/CHANGELOG.md b/packages/network-enablement-controller/CHANGELOG.md index 5bd6f6d763..f42c88093f 100644 --- a/packages/network-enablement-controller/CHANGELOG.md +++ b/packages/network-enablement-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add Stellar network enablement: default `enabledNetworkMap` entries for Stellar namespace (pubnet on, testnet off), enable Stellar pubnet during popular-network init when it exists in `MultichainNetworkController`, and include Stellar pubnet in `listPopularMultichainNetworks` ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + ### Changed - Bump `@metamask/transaction-controller` from `^65.0.0` to `^65.2.0` ([#8691](https://github.com/MetaMask/core/pull/8691), [#8722](https://github.com/MetaMask/core/pull/8722)) diff --git a/packages/network-enablement-controller/package.json b/packages/network-enablement-controller/package.json index 471b4bbf93..785456f9e2 100644 --- a/packages/network-enablement-controller/package.json +++ b/packages/network-enablement-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/network-enablement-controller", - "version": "5.1.0", + "version": "5.1.0-dev.2", "description": "Provides an interface to the currently enabled network using a MetaMask-compatible provider object", "keywords": [ "Ethereum", @@ -61,7 +61,7 @@ "@metamask/network-controller": "^30.1.0", "@metamask/slip44": "^4.3.0", "@metamask/transaction-controller": "^65.2.0", - "@metamask/utils": "^11.9.0", + "@metamask/utils": "^11.11.0", "reselect": "^5.1.1" }, "devDependencies": { diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.test.ts b/packages/network-enablement-controller/src/NetworkEnablementController.test.ts index 8712deadaa..8ca349966c 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.test.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.test.ts @@ -1,6 +1,6 @@ import { deriveStateFromMetadata } from '@metamask/base-controller'; import { BuiltInNetworkName, ChainId } from '@metamask/controller-utils'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MessengerActions, @@ -83,6 +83,7 @@ const defaultMultichainGetState = (): MultichainGetStateReturn => ({ [BtcScope.Mainnet]: { chainId: BtcScope.Mainnet, name: 'Bitcoin' }, [SolScope.Mainnet]: { chainId: SolScope.Mainnet, name: 'Solana' }, [TrxScope.Mainnet]: { chainId: TrxScope.Mainnet, name: 'Tron' }, + [XlmScope.Pubnet]: { chainId: XlmScope.Pubnet, name: 'Stellar' }, }, selectedMultichainNetworkChainId: 'eip155:1', isEvmSelected: true, @@ -211,6 +212,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -267,6 +272,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -330,6 +339,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: expectedNativeAssetIdentifiers, }); @@ -471,6 +484,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: expectedNativeAssetIdentifiersForFallback, }); @@ -571,6 +588,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, // init() populates nativeAssetIdentifiers from NetworkController (EVM networks only) nativeAssetIdentifiers: { @@ -1172,6 +1193,10 @@ describe('NetworkEnablementController', () => { chainId: TrxScope.Mainnet, name: 'Tron Mainnet', }, + [XlmScope.Pubnet]: { + chainId: XlmScope.Pubnet, + name: 'Stellar Mainnet', + }, }, selectedMultichainNetworkChainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', @@ -1213,6 +1238,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1247,6 +1276,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1329,6 +1362,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1510,6 +1547,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1544,6 +1585,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1598,6 +1643,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -1636,6 +1685,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -1674,6 +1727,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -1723,6 +1780,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1764,6 +1825,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1818,6 +1883,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: false, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: { ...getDefaultNativeAssetIdentifiers(), @@ -1864,6 +1933,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -1912,6 +1985,10 @@ describe('NetworkEnablementController', () => { [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, nativeAssetIdentifiers: getDefaultNativeAssetIdentifiers(), }); @@ -2148,14 +2225,15 @@ describe('NetworkEnablementController', () => { const { controller } = setupController(); const result = controller.listPopularNetworks(); - // Default setup: 3 EVM (0x1, 0xe708, 0x2105) + 3 multichain (Btc, Sol, Trx) + // Default setup: 3 EVM (0x1, 0xe708, 0x2105) + 4 multichain (Btc, Sol, Trx, Stellar) expect(result).toContain('eip155:1'); expect(result).toContain('eip155:59144'); expect(result).toContain('eip155:8453'); expect(result).toContain(BtcScope.Mainnet); expect(result).toContain(SolScope.Mainnet); expect(result).toContain(TrxScope.Mainnet); - expect(result).toHaveLength(6); + expect(result).toContain(XlmScope.Pubnet); + expect(result).toHaveLength(7); }); it('excludes multichain mainnets when not in MultichainNetworkController state', () => { @@ -2175,6 +2253,7 @@ describe('NetworkEnablementController', () => { expect(result).not.toContain(BtcScope.Mainnet); expect(result).not.toContain(SolScope.Mainnet); expect(result).not.toContain(TrxScope.Mainnet); + expect(result).not.toContain(XlmScope.Pubnet); expect(result).toHaveLength(3); }); @@ -2222,14 +2301,15 @@ describe('NetworkEnablementController', () => { }); describe('listPopularMultichainNetworks', () => { - it('returns only Bitcoin, Solana, Tron mainnets that exist in MultichainNetworkController state', () => { + it('returns only Bitcoin, Solana, Tron, Stellar mainnets that exist in MultichainNetworkController state', () => { const { controller } = setupController(); const result = controller.listPopularMultichainNetworks(); expect(result).toContain(BtcScope.Mainnet); expect(result).toContain(SolScope.Mainnet); expect(result).toContain(TrxScope.Mainnet); - expect(result).toHaveLength(3); + expect(result).toContain(XlmScope.Pubnet); + expect(result).toHaveLength(4); }); it('returns empty when none of the multichain mainnets are configured', () => { diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.ts b/packages/network-enablement-controller/src/NetworkEnablementController.ts index 8ea80bcc98..39e428e57b 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.ts @@ -4,7 +4,7 @@ import type { ControllerStateChangeEvent, } from '@metamask/base-controller'; import { BuiltInNetworkName, ChainId } from '@metamask/controller-utils'; -import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import type { Messenger } from '@metamask/messenger'; import type { MultichainNetworkControllerGetStateAction } from '@metamask/multichain-network-controller'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; @@ -183,6 +183,10 @@ const getDefaultNetworkEnablementControllerState = [TrxScope.Nile]: false, [TrxScope.Shasta]: false, }, + [KnownCaipNamespace.Stellar]: { + [XlmScope.Pubnet]: true, + [XlmScope.Testnet]: false, + }, }, // nativeAssetIdentifiers is initialized as empty and should be populated // by the client using initNativeAssetIdentifiers() during controller init @@ -418,6 +422,18 @@ export class NetworkEnablementController extends BaseController< // Enable Tron mainnet state.enabledNetworkMap[tronKeys.namespace][tronKeys.storageKey] = true; } + + // Enable Stellar mainnet if it exists in MultichainNetworkController configurations + const stellarKeys = deriveKeys(XlmScope.Pubnet as CaipChainId); + if ( + multichainState.multichainNetworkConfigurationsByChainId[ + XlmScope.Pubnet + ] + ) { + this.#ensureNamespaceBucket(state, stellarKeys.namespace); + state.enabledNetworkMap[stellarKeys.namespace][stellarKeys.storageKey] = + true; + } }); } @@ -768,11 +784,11 @@ export class NetworkEnablementController extends BaseController< } /** - * Returns popular multichain (Bitcoin, Solana, Tron) mainnet chain IDs in + * Returns popular multichain (Bitcoin, Solana, Tron, Stellar) mainnet chain IDs in * CAIP-2 form, restricted to networks that exist in MultichainNetworkController * (multichainNetworkConfigurationsByChainId). * - * @returns CAIP-2 chain IDs for Bitcoin, Solana, and Tron mainnets that are configured. + * @returns CAIP-2 chain IDs for Bitcoin, Solana, Tron, and Stellar mainnets that are configured. */ listPopularMultichainNetworks(): CaipChainId[] { const multichainState = this.messenger.call( @@ -782,6 +798,7 @@ export class NetworkEnablementController extends BaseController< BtcScope.Mainnet, SolScope.Mainnet, TrxScope.Mainnet, + XlmScope.Pubnet, ] as const; return multichainMainnets.filter( (chainId) => @@ -794,7 +811,7 @@ export class NetworkEnablementController extends BaseController< * networks that exist in NetworkController (networkConfigurationsByChainId) and * MultichainNetworkController (multichainNetworkConfigurationsByChainId). EVM * popular networks come from POPULAR_NETWORKS; multichain popular are Bitcoin, - * Solana, and Tron mainnets. + * Solana, Tron, and Stellar mainnets. * * @returns CAIP-2 chain IDs for popular EVM networks and multichain mainnets that are configured. */ diff --git a/yarn.lock b/yarn.lock index 6513749b43..0fc5c3a7f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2567,6 +2567,31 @@ __metadata: languageName: unknown linkType: soft +"@metamask/account-tree-controller@npm:^7.4.0": + version: 7.4.0 + resolution: "@metamask/account-tree-controller@npm:7.4.0" + dependencies: + "@metamask/accounts-controller": "npm:^38.1.1" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/multichain-account-service": "npm:^10.0.0" + "@metamask/profile-sync-controller": "npm:^28.1.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.9.0" + fast-deep-equal: "npm:^3.1.3" + lodash: "npm:^4.17.21" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/8c9967775e4ef3bcb718efb18feb2f94f077c540510c94eb9cc9e814dc91f19b1d4e374e694cbfb6697e162d9c88922acafa1723e727b8912bf4ef3620a8e784 + languageName: node + linkType: hard + "@metamask/accounts-controller@npm:^38.0.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" @@ -2607,6 +2632,34 @@ __metadata: languageName: unknown linkType: soft +"@metamask/accounts-controller@npm:^38.1.0, @metamask/accounts-controller@npm:^38.1.1": + version: 38.1.1 + resolution: "@metamask/accounts-controller@npm:38.1.1" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/eth-snap-keyring": "npm:^22.0.1" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/keyring-sdk": "npm:^2.1.1" + "@metamask/keyring-utils": "npm:^3.2.1" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/network-controller": "npm:^32.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.9.0" + deepmerge: "npm:^4.2.2" + ethereum-cryptography: "npm:^2.1.2" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/176427006cba41f65434722128e975e7d66ef026d9c75fcfe983975db6d8ba63c282fc8a097af3454249a058f61a6b83bf42d8f4714441c93f2811c0502a21b4 + languageName: node + linkType: hard + "@metamask/action-utils@npm:^1.0.0": version: 1.1.1 resolution: "@metamask/action-utils@npm:1.1.1" @@ -2639,6 +2692,18 @@ __metadata: languageName: unknown linkType: soft +"@metamask/address-book-controller@npm:^7.1.2": + version: 7.1.2 + resolution: "@metamask/address-book-controller@npm:7.1.2" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/utils": "npm:^11.9.0" + checksum: 10/e15140418452a80d51fec783003144add72a50a4fe362377481de83c7858860f641a3cfb1575f4b802b1eb1e873b3efd03bb4183afcbee77f95c7525e05aaed4 + languageName: node + linkType: hard + "@metamask/ai-controllers@workspace:packages/ai-controllers": version: 0.0.0-use.local resolution: "@metamask/ai-controllers@workspace:packages/ai-controllers" @@ -2815,7 +2880,158 @@ __metadata: languageName: unknown linkType: soft -"@metamask/assets-controllers@npm:^106.0.0, @metamask/assets-controllers@workspace:packages/assets-controllers": +"@metamask/assets-controller@npm:^7.1.1": + version: 7.1.2 + resolution: "@metamask/assets-controller@npm:7.1.2" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/account-tree-controller": "npm:^7.4.0" + "@metamask/accounts-controller": "npm:^38.1.1" + "@metamask/assets-controllers": "npm:^108.1.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/client-controller": "npm:^1.0.1" + "@metamask/controller-utils": "npm:^12.1.0" + "@metamask/core-backend": "npm:^6.2.2" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/keyring-snap-client": "npm:^9.0.2" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/network-controller": "npm:^32.0.0" + "@metamask/network-enablement-controller": "npm:^5.1.1" + "@metamask/permission-controller": "npm:^13.1.1" + "@metamask/phishing-controller": "npm:^17.1.2" + "@metamask/polling-controller": "npm:^16.0.5" + "@metamask/preferences-controller": "npm:^23.1.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/transaction-controller": "npm:^65.3.0" + "@metamask/utils": "npm:^11.9.0" + async-mutex: "npm:^0.5.0" + bignumber.js: "npm:^9.1.2" + lodash: "npm:^4.17.21" + p-limit: "npm:^3.1.0" + checksum: 10/b6dd422a1c29f67a0a4094907c1b52ad2bda845bbdefc8206601b82868ee8035c600ccdf8f93d61b6d6b84fdf7ad1381415e618c5c29e676721b6b0f2f53fbf2 + languageName: node + linkType: hard + +"@metamask/assets-controllers@npm:^106.0.0": + version: 106.0.1 + resolution: "@metamask/assets-controllers@npm:106.0.1" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/account-tree-controller": "npm:^7.3.0" + "@metamask/accounts-controller": "npm:^38.1.0" + "@metamask/approval-controller": "npm:^9.0.1" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/core-backend": "npm:^6.2.2" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-account-service": "npm:^9.0.0" + "@metamask/network-controller": "npm:^31.0.0" + "@metamask/network-enablement-controller": "npm:^5.1.1" + "@metamask/permission-controller": "npm:^13.1.1" + "@metamask/phishing-controller": "npm:^17.1.2" + "@metamask/polling-controller": "npm:^16.0.5" + "@metamask/preferences-controller": "npm:^23.1.0" + "@metamask/profile-sync-controller": "npm:^28.0.2" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/storage-service": "npm:^1.0.1" + "@metamask/transaction-controller": "npm:^65.3.0" + "@metamask/utils": "npm:^11.9.0" + "@tanstack/query-core": "npm:^5.62.16" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + bn.js: "npm:^5.2.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^9.9.0" + reselect: "npm:^5.1.1" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/784562166d0b74185bb28eb4489892ffb52ab6607e1938df36b882e49c13f9c699f70beeca98a9a5d295709d7d9a8391c4e4e90d6db49170808a818770649cc6 + languageName: node + linkType: hard + +"@metamask/assets-controllers@npm:^108.0.0, @metamask/assets-controllers@npm:^108.1.0": + version: 108.1.0 + resolution: "@metamask/assets-controllers@npm:108.1.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/account-tree-controller": "npm:^7.4.0" + "@metamask/accounts-controller": "npm:^38.1.1" + "@metamask/approval-controller": "npm:^9.0.1" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^12.1.0" + "@metamask/core-backend": "npm:^6.2.2" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-account-service": "npm:^10.0.0" + "@metamask/network-controller": "npm:^32.0.0" + "@metamask/network-enablement-controller": "npm:^5.1.1" + "@metamask/permission-controller": "npm:^13.1.1" + "@metamask/phishing-controller": "npm:^17.1.2" + "@metamask/polling-controller": "npm:^16.0.5" + "@metamask/preferences-controller": "npm:^23.1.0" + "@metamask/profile-sync-controller": "npm:^28.1.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/storage-service": "npm:^1.0.1" + "@metamask/transaction-controller": "npm:^65.3.0" + "@metamask/utils": "npm:^11.9.0" + "@tanstack/query-core": "npm:^5.62.16" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + bn.js: "npm:^5.2.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^9.9.0" + reselect: "npm:^5.1.1" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/b30574058978f594505b6585dff7501b05a1b36591dfe97ea58c915b85f4e534374d8d3fc72420d58f0a44df9d29aa62c526ec5e903482a64ed9384ff4f09f44 + languageName: node + linkType: hard + +"@metamask/assets-controllers@workspace:packages/assets-controllers": version: 0.0.0-use.local resolution: "@metamask/assets-controllers@workspace:packages/assets-controllers" dependencies: @@ -3015,7 +3231,40 @@ __metadata: languageName: unknown linkType: soft -"@metamask/bridge-controller@npm:^72.0.0, @metamask/bridge-controller@workspace:packages/bridge-controller": +"@metamask/bridge-controller@npm:^72.0.0": + version: 72.0.4 + resolution: "@metamask/bridge-controller@npm:72.0.4" + dependencies: + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/accounts-controller": "npm:^38.1.1" + "@metamask/assets-controller": "npm:^7.1.1" + "@metamask/assets-controllers": "npm:^108.0.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.1.0" + "@metamask/gas-fee-controller": "npm:^26.2.1" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-network-controller": "npm:^3.1.1" + "@metamask/network-controller": "npm:^32.0.0" + "@metamask/polling-controller": "npm:^16.0.5" + "@metamask/profile-sync-controller": "npm:^28.0.2" + "@metamask/remote-feature-flag-controller": "npm:^4.2.1" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/transaction-controller": "npm:^65.3.0" + "@metamask/utils": "npm:^11.9.0" + bignumber.js: "npm:^9.1.2" + reselect: "npm:^5.1.1" + uuid: "npm:^8.3.2" + checksum: 10/afba9061ce1df482d6d8a32f084a9784adac443c0e13abd298a8d8c2cd3fd6c7e7e68ae674f5e3e69913d96250ec4cdee2b3514a07cbe3bfd350f600fe540bbf + languageName: node + linkType: hard + +"@metamask/bridge-controller@workspace:packages/bridge-controller": version: 0.0.0-use.local resolution: "@metamask/bridge-controller@workspace:packages/bridge-controller" dependencies: @@ -3347,6 +3596,27 @@ __metadata: languageName: unknown linkType: soft +"@metamask/controller-utils@npm:^12.0.0, @metamask/controller-utils@npm:^12.1.0": + version: 12.1.0 + resolution: "@metamask/controller-utils@npm:12.1.0" + dependencies: + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/ethjs-unit": "npm:^0.3.0" + "@metamask/utils": "npm:^11.9.0" + "@spruceid/siwe-parser": "npm:2.1.0" + "@types/bn.js": "npm:^5.1.5" + bignumber.js: "npm:^9.1.2" + bn.js: "npm:^5.2.1" + cockatiel: "npm:^3.1.2" + eth-ens-namehash: "npm:^2.0.8" + fast-deep-equal: "npm:^3.1.3" + lodash: "npm:^4.17.21" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/421b9685faefa481529e611a11cf005cd94ced8f4a15a6ea1aacd885417be0de6191ee414e0bcb0fff7ecade9e356a4d0f3d7c826e798ba47d65e412aa1df049 + languageName: node + linkType: hard + "@metamask/core-backend@npm:^6.2.1, @metamask/core-backend@workspace:packages/core-backend": version: 0.0.0-use.local resolution: "@metamask/core-backend@workspace:packages/core-backend" @@ -3373,6 +3643,23 @@ __metadata: languageName: unknown linkType: soft +"@metamask/core-backend@npm:^6.2.2": + version: 6.3.0 + resolution: "@metamask/core-backend@npm:6.3.0" + dependencies: + "@metamask/accounts-controller": "npm:^38.1.1" + "@metamask/controller-utils": "npm:^12.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/profile-sync-controller": "npm:^28.1.0" + "@metamask/utils": "npm:^11.9.0" + "@tanstack/query-core": "npm:^5.62.16" + async-mutex: "npm:^0.5.0" + uuid: "npm:^8.3.2" + checksum: 10/4823dc2d9ac77e405190eb7aca87c14ddc1cc2d30092872c48a2dbf45672f64859a36bc298fa2673cfcd14165525216dad4f3d9242c7f2ec743a71c175045928 + languageName: node + linkType: hard + "@metamask/core-monorepo@workspace:.": version: 0.0.0-use.local resolution: "@metamask/core-monorepo@workspace:." @@ -4100,6 +4387,28 @@ __metadata: languageName: unknown linkType: soft +"@metamask/gas-fee-controller@npm:^26.2.1": + version: 26.2.1 + resolution: "@metamask/gas-fee-controller@npm:26.2.1" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/ethjs-unit": "npm:^0.3.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/network-controller": "npm:^31.0.0" + "@metamask/polling-controller": "npm:^16.0.5" + "@metamask/utils": "npm:^11.9.0" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + bn.js: "npm:^5.2.1" + uuid: "npm:^8.3.2" + peerDependencies: + "@babel/runtime": ^7.0.0 + checksum: 10/aa7fe8b2112baef79219d72c8c7ef020846ce1c1f94cb17c55497dcb539d473a1f054365068f05a6afa51a71ca4e810e104f7c0e7260b8c99d856dc59dac2475 + languageName: node + linkType: hard + "@metamask/gator-permissions-controller@npm:^4.1.0, @metamask/gator-permissions-controller@workspace:packages/gator-permissions-controller": version: 0.0.0-use.local resolution: "@metamask/gator-permissions-controller@workspace:packages/gator-permissions-controller" @@ -4176,6 +4485,21 @@ __metadata: languageName: unknown linkType: soft +"@metamask/json-rpc-engine@npm:^10.5.0": + version: 10.5.0 + resolution: "@metamask/json-rpc-engine@npm:10.5.0" + dependencies: + "@metamask/messenger": "npm:^1.2.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^11.9.0" + "@types/deep-freeze-strict": "npm:^1.1.0" + deep-freeze-strict: "npm:^1.1.1" + klona: "npm:^2.0.6" + checksum: 10/2fa42bc57e6e268f0dfcdfe22d15f3c1c593acb5c96df1469403c389fc2a4e4ccb7c75efc00a02d987834d7f0dfb52f31328929ebb37d73b6719aa2e53a75466 + languageName: node + linkType: hard + "@metamask/json-rpc-middleware-stream@npm:^8.0.6, @metamask/json-rpc-middleware-stream@npm:^8.0.8, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": version: 0.0.0-use.local resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" @@ -4213,7 +4537,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^23.1.0": +"@metamask/keyring-api@npm:23.1.0": version: 23.1.0 resolution: "@metamask/keyring-api@npm:23.1.0" dependencies: @@ -4265,7 +4589,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/keyring-internal-api@npm:^11.0.1": +"@metamask/keyring-internal-api@npm:11.0.1": version: 11.0.1 resolution: "@metamask/keyring-internal-api@npm:11.0.1" dependencies: @@ -4537,7 +4861,68 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-account-service@npm:^9.0.0, @metamask/multichain-account-service@workspace:packages/multichain-account-service": +"@metamask/multichain-account-service@npm:^10.0.0": + version: 10.0.0 + resolution: "@metamask/multichain-account-service@npm:10.0.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/accounts-controller": "npm:^38.1.1" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/eth-snap-keyring": "npm:^22.0.1" + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/keyring-snap-client": "npm:^9.0.2" + "@metamask/keyring-utils": "npm:^3.2.1" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/snap-account-service": "npm:^0.1.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.9.0" + async-mutex: "npm:^0.5.0" + lodash: "npm:^4.17.21" + peerDependencies: + "@metamask/account-api": ^1.0.4 + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/07b7e0dc1d4b50bfbd4846fa82598d9a1244a37645a122d00502d710bacb05abcc863a2f569d1c6a33ea656c7f25361b8e1b4de7895be028718195aa2673384b + languageName: node + linkType: hard + +"@metamask/multichain-account-service@npm:^9.0.0": + version: 9.0.0 + resolution: "@metamask/multichain-account-service@npm:9.0.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/accounts-controller": "npm:^38.0.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/eth-snap-keyring": "npm:^22.0.1" + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/keyring-snap-client": "npm:^9.0.2" + "@metamask/keyring-utils": "npm:^3.2.1" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.9.0" + async-mutex: "npm:^0.5.0" + lodash: "npm:^4.17.21" + peerDependencies: + "@metamask/account-api": ^1.0.4 + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/62add4c9a795c5aef4451b5293608d2f0e4df0a48012e7ece67e484100504e228e291516a7b2ac5fb81751d1708c74d93683533adb58e5a8096b540d15c6e4a0 + languageName: node + linkType: hard + +"@metamask/multichain-account-service@workspace:packages/multichain-account-service": version: 0.0.0-use.local resolution: "@metamask/multichain-account-service@workspace:packages/multichain-account-service" dependencies: @@ -4616,7 +5001,26 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-network-controller@npm:^3.1.0, @metamask/multichain-network-controller@workspace:packages/multichain-network-controller": +"@metamask/multichain-network-controller@npm:^3.1.0, @metamask/multichain-network-controller@npm:^3.1.1": + version: 3.1.1 + resolution: "@metamask/multichain-network-controller@npm:3.1.1" + dependencies: + "@metamask/accounts-controller": "npm:^38.1.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/network-controller": "npm:^31.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.9.0" + "@solana/addresses": "npm:^2.0.0" + lodash: "npm:^4.17.21" + checksum: 10/1c17020e9c2d6c8b50f182900730e7c59b23f6ad0e467be1082242ba089b5bf931371bc966f5965fcc611330da1724ddb92062c87a145d3b69a99e7458fe1088 + languageName: node + linkType: hard + +"@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" dependencies: @@ -4752,7 +5156,81 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-enablement-controller@npm:^5.1.0, @metamask/network-enablement-controller@workspace:packages/network-enablement-controller": +"@metamask/network-controller@npm:^31.0.0": + version: 31.1.0 + resolution: "@metamask/network-controller@npm:31.1.0" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/connectivity-controller": "npm:^0.2.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/eth-block-tracker": "npm:^15.0.1" + "@metamask/eth-json-rpc-infura": "npm:^10.3.0" + "@metamask/eth-json-rpc-middleware": "npm:^23.1.3" + "@metamask/eth-json-rpc-provider": "npm:^6.0.1" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/json-rpc-engine": "npm:^10.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/swappable-obj-proxy": "npm:^2.3.0" + "@metamask/utils": "npm:^11.9.0" + async-mutex: "npm:^0.5.0" + fast-deep-equal: "npm:^3.1.3" + immer: "npm:^9.0.6" + loglevel: "npm:^1.8.1" + reselect: "npm:^5.1.1" + uri-js: "npm:^4.4.1" + uuid: "npm:^8.3.2" + checksum: 10/63faa20d97eeca5b1ddac955e82433666dc7af3ec5b141d53a7628e67a6677a4e1757aa551092e890b62cde127c5cd442c312f23f341f886183420a4745e90d0 + languageName: node + linkType: hard + +"@metamask/network-controller@npm:^32.0.0": + version: 32.0.0 + resolution: "@metamask/network-controller@npm:32.0.0" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/connectivity-controller": "npm:^0.2.0" + "@metamask/controller-utils": "npm:^12.1.0" + "@metamask/eth-block-tracker": "npm:^15.0.1" + "@metamask/eth-json-rpc-infura": "npm:^10.3.0" + "@metamask/eth-json-rpc-middleware": "npm:^23.1.3" + "@metamask/eth-json-rpc-provider": "npm:^6.0.1" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/json-rpc-engine": "npm:^10.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/swappable-obj-proxy": "npm:^2.3.0" + "@metamask/utils": "npm:^11.9.0" + async-mutex: "npm:^0.5.0" + fast-deep-equal: "npm:^3.1.3" + immer: "npm:^9.0.6" + loglevel: "npm:^1.8.1" + reselect: "npm:^5.1.1" + uri-js: "npm:^4.4.1" + uuid: "npm:^8.3.2" + checksum: 10/263f6355aac3e2014f2d18c2c10b20b83e12823845cf9a12e74dea09d31d5f81be9bd396d9f3b5b7dbaa99ce5ed7f3b0ec4c537656f884f586c349965569ad07 + languageName: node + linkType: hard + +"@metamask/network-enablement-controller@npm:^5.1.0, @metamask/network-enablement-controller@npm:^5.1.1": + version: 5.1.1 + resolution: "@metamask/network-enablement-controller@npm:5.1.1" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/multichain-network-controller": "npm:^3.1.1" + "@metamask/network-controller": "npm:^31.0.0" + "@metamask/slip44": "npm:^4.3.0" + "@metamask/transaction-controller": "npm:^65.3.0" + "@metamask/utils": "npm:^11.9.0" + reselect: "npm:^5.1.1" + checksum: 10/bcf7443559e161e73a07dc478e89fd2e041b2c32071adb0916de49ebf5e61e630dea41c68f59d252f5bffa62b0f7b15142bb7e8799af97b28adb5cd66918a96e + languageName: node + linkType: hard + +"@metamask/network-enablement-controller@workspace:packages/network-enablement-controller": version: 0.0.0-use.local resolution: "@metamask/network-enablement-controller@workspace:packages/network-enablement-controller" dependencies: @@ -4765,7 +5243,7 @@ __metadata: "@metamask/network-controller": "npm:^30.1.0" "@metamask/slip44": "npm:^4.3.0" "@metamask/transaction-controller": "npm:^65.2.0" - "@metamask/utils": "npm:^11.9.0" + "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" @@ -4920,6 +5398,25 @@ __metadata: languageName: unknown linkType: soft +"@metamask/permission-controller@npm:^13.1.1": + version: 13.1.1 + resolution: "@metamask/permission-controller@npm:13.1.1" + dependencies: + "@metamask/approval-controller": "npm:^9.0.1" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/json-rpc-engine": "npm:^10.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.9.0" + "@types/deep-freeze-strict": "npm:^1.1.0" + deep-freeze-strict: "npm:^1.1.1" + immer: "npm:^9.0.6" + nanoid: "npm:^3.3.8" + checksum: 10/12c6a675217ff510b837543c6e71ea851a3a7bd4592195f9579e76e2779177d2d4f03fcdd4d5c12585598df73250f32d0d49cb114274fc8187cbc7081bbc913b + languageName: node + linkType: hard + "@metamask/permission-log-controller@workspace:packages/permission-log-controller": version: 0.0.0-use.local resolution: "@metamask/permission-log-controller@workspace:packages/permission-log-controller" @@ -5010,6 +5507,24 @@ __metadata: languageName: unknown linkType: soft +"@metamask/phishing-controller@npm:^17.1.2": + version: 17.2.0 + resolution: "@metamask/phishing-controller@npm:17.2.0" + dependencies: + "@metamask/address-book-controller": "npm:^7.1.2" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.1.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/transaction-controller": "npm:^65.4.0" + "@noble/hashes": "npm:^1.8.0" + "@types/punycode": "npm:^2.1.0" + ethereum-cryptography: "npm:^2.1.2" + fastest-levenshtein: "npm:^1.0.16" + punycode: "npm:^2.1.1" + checksum: 10/de271d813043ca535e76cff52084ac9d4e0da762fa02e75425583dfc068c4b4bcb189266cb9cf58baf7ed79ab58f0b003a5643426780bb49192ca30212640b05 + languageName: node + linkType: hard + "@metamask/polling-controller@npm:^16.0.4, @metamask/polling-controller@workspace:packages/polling-controller": version: 0.0.0-use.local resolution: "@metamask/polling-controller@workspace:packages/polling-controller" @@ -5034,6 +5549,21 @@ __metadata: languageName: unknown linkType: soft +"@metamask/polling-controller@npm:^16.0.5": + version: 16.0.5 + resolution: "@metamask/polling-controller@npm:16.0.5" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/network-controller": "npm:^31.0.0" + "@metamask/utils": "npm:^11.9.0" + "@types/uuid": "npm:^8.3.0" + fast-json-stable-stringify: "npm:^2.1.0" + uuid: "npm:^8.3.2" + checksum: 10/0fa32076672860e5bc27ac2720223b0f4199aaae9c952c5ec74b53ad49248705cc63a9b07dd25219512c348a85f87d04038f94a45eb2d2219af14d941f327275 + languageName: node + linkType: hard + "@metamask/post-message-stream@npm:^10.0.0": version: 10.0.0 resolution: "@metamask/post-message-stream@npm:10.0.0" @@ -5135,6 +5665,30 @@ __metadata: languageName: unknown linkType: soft +"@metamask/profile-sync-controller@npm:^28.1.0": + version: 28.1.0 + resolution: "@metamask/profile-sync-controller@npm:28.1.0" + dependencies: + "@metamask/address-book-controller": "npm:^7.1.2" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/utils": "npm:^11.9.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/hashes": "npm:^1.8.0" + immer: "npm:^9.0.6" + loglevel: "npm:^1.8.1" + siwe: "npm:^2.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/33c318fe005e55b38b4d0d603806c9303b8deaddf8f98fc04f89ecb49cf26da2189ceea20ac6a4b1be5da49060a928d6788cb465b4451aa199c8e51172366213 + languageName: node + linkType: hard + "@metamask/providers@npm:^22.1.0, @metamask/providers@npm:^22.1.1": version: 22.1.1 resolution: "@metamask/providers@npm:22.1.1" @@ -5245,6 +5799,19 @@ __metadata: languageName: unknown linkType: soft +"@metamask/remote-feature-flag-controller@npm:^4.2.1": + version: 4.2.1 + resolution: "@metamask/remote-feature-flag-controller@npm:4.2.1" + dependencies: + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.0.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/utils": "npm:^11.9.0" + uuid: "npm:^8.3.2" + checksum: 10/b789836eb03dfe45374a86209d00fad4b1e2284bd61fa7e0342504f577b2227336db01da67063b0166ece24edee7cb0482f63512143f0bbd5710942fba9b1d60 + languageName: node + linkType: hard + "@metamask/rpc-errors@npm:^7.0.2, @metamask/rpc-errors@npm:^7.0.3": version: 7.0.3 resolution: "@metamask/rpc-errors@npm:7.0.3" @@ -5450,6 +6017,23 @@ __metadata: languageName: unknown linkType: soft +"@metamask/snap-account-service@npm:^0.1.0": + version: 0.1.0 + resolution: "@metamask/snap-account-service@npm:0.1.0" + dependencies: + "@metamask/account-api": "npm:^1.0.4" + "@metamask/account-tree-controller": "npm:^7.4.0" + "@metamask/eth-snap-keyring": "npm:^22.0.1" + "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/snaps-controllers": "npm:^19.0.0" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/utils": "npm:^11.9.0" + lodash: "npm:^4.17.21" + checksum: 10/75f07b042cc6223943f663a6ef5cf76553fe7bf5a1132e4b95d2621e749e8c7ec88c47e881f7e914e7017ae810d2380f95825a0aff7583f9ad3c9868116ef08c + languageName: node + linkType: hard + "@metamask/snaps-controllers@npm:^19.0.0, @metamask/snaps-controllers@npm:^19.0.1": version: 19.0.1 resolution: "@metamask/snaps-controllers@npm:19.0.1" @@ -5730,6 +6314,44 @@ __metadata: languageName: unknown linkType: soft +"@metamask/transaction-controller@npm:^65.3.0, @metamask/transaction-controller@npm:^65.4.0": + version: 65.4.0 + resolution: "@metamask/transaction-controller@npm:65.4.0" + dependencies: + "@ethereumjs/common": "npm:^4.4.0" + "@ethereumjs/tx": "npm:^5.4.0" + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@ethersproject/wallet": "npm:^5.7.0" + "@metamask/accounts-controller": "npm:^38.1.1" + "@metamask/approval-controller": "npm:^9.0.1" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.1.0" + "@metamask/core-backend": "npm:^6.2.2" + "@metamask/gas-fee-controller": "npm:^26.2.1" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/network-controller": "npm:^32.0.0" + "@metamask/nonce-tracker": "npm:^6.0.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.1" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.9.0" + async-mutex: "npm:^0.5.0" + bignumber.js: "npm:^9.1.2" + bn.js: "npm:^5.2.1" + eth-method-registry: "npm:^4.0.0" + fast-json-patch: "npm:^3.1.1" + lodash: "npm:^4.17.21" + uuid: "npm:^8.3.2" + peerDependencies: + "@babel/runtime": ^7.0.0 + "@metamask/eth-block-tracker": ">=9" + checksum: 10/e9273b3dec6837dd33a7ed40fa2569794d71f6580691f40950e94ade1ca4e1d9f31178070218398935068f8193cafb06ba4769c77483286c989a42c2c72232fd + languageName: node + linkType: hard + "@metamask/transaction-pay-controller@workspace:packages/transaction-pay-controller": version: 0.0.0-use.local resolution: "@metamask/transaction-pay-controller@workspace:packages/transaction-pay-controller"