From f88e9c475e30688e317e444f0ea55e265a01203f Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Tue, 18 Aug 2026 19:40:31 +0200 Subject: [PATCH 1/3] chore: add safeMerge shared util --- eslint-suppressions.json | 16 +-- packages/snap-networks-utils/CHANGELOG.md | 7 +- packages/snap-networks-utils/src/index.ts | 1 + .../src}/safeMerge.test.ts | 0 .../src}/safeMerge.ts | 0 .../src/core/services/state/State.test.ts | 6 + .../src/core/services/state/State.ts | 2 +- .../src/core/utils/safeMerge.ts | 28 ----- .../src/services/state/State.test.ts | 6 + .../src/services/state/State.ts | 2 +- .../src/utils/safeMerge.test.ts | 119 ------------------ 11 files changed, 28 insertions(+), 159 deletions(-) rename packages/{solana-wallet-snap/src/core/utils => snap-networks-utils/src}/safeMerge.test.ts (100%) rename packages/{tron-wallet-snap/src/utils => snap-networks-utils/src}/safeMerge.ts (100%) delete mode 100644 packages/solana-wallet-snap/src/core/utils/safeMerge.ts delete mode 100644 packages/tron-wallet-snap/src/utils/safeMerge.test.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 0bcb524fa..f82d6d77e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -344,14 +344,6 @@ "count": 2 } }, - "packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts": { - "@typescript-eslint/await-thenable": { - "count": 1 - }, - "no-unused-private-class-members": { - "count": 2 - } - }, "packages/solana-wallet-snap/src/core/services/assets/TokenHelper.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -368,6 +360,14 @@ "count": 1 } }, + "packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts": { + "@typescript-eslint/await-thenable": { + "count": 1 + }, + "no-unused-private-class-members": { + "count": 2 + } + }, "packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 diff --git a/packages/snap-networks-utils/CHANGELOG.md b/packages/snap-networks-utils/CHANGELOG.md index dac68b80a..89923972f 100644 --- a/packages/snap-networks-utils/CHANGELOG.md +++ b/packages/snap-networks-utils/CHANGELOG.md @@ -7,10 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed +### Added -- **BREAKING** Replace the logger utilities with a configurable `Logger` class that requires a log level and supports level filtering, per-instance prefixes, and method decorators. +- Add a `safeMerge` utility for shallowly merging objects. + +### Changed +- **BREAKING** Replace the logger utilities with a configurable `Logger` class that requires a log level and supports level filtering, per-instance prefixes, and method decorators. ([#136](https://github.com/MetaMask/internal-snaps/pull/136)) - Bump `@metamask/utils` from `^11.9.0` to `^11.11.9` ([#161](https://github.com/MetaMask/internal-snaps/pull/161)) ## [1.0.0] diff --git a/packages/snap-networks-utils/src/index.ts b/packages/snap-networks-utils/src/index.ts index ee84b9d6e..bdac7e8e7 100644 --- a/packages/snap-networks-utils/src/index.ts +++ b/packages/snap-networks-utils/src/index.ts @@ -8,3 +8,4 @@ export { RemoteFeatureFlagsProvider, type RemoteFeatureFlagsProviderMessenger, } from './providers/remote-feature-flags/RemoteFeatureFlagsProvider'; +export { safeMerge } from './safeMerge'; diff --git a/packages/solana-wallet-snap/src/core/utils/safeMerge.test.ts b/packages/snap-networks-utils/src/safeMerge.test.ts similarity index 100% rename from packages/solana-wallet-snap/src/core/utils/safeMerge.test.ts rename to packages/snap-networks-utils/src/safeMerge.test.ts diff --git a/packages/tron-wallet-snap/src/utils/safeMerge.ts b/packages/snap-networks-utils/src/safeMerge.ts similarity index 100% rename from packages/tron-wallet-snap/src/utils/safeMerge.ts rename to packages/snap-networks-utils/src/safeMerge.ts diff --git a/packages/solana-wallet-snap/src/core/services/state/State.test.ts b/packages/solana-wallet-snap/src/core/services/state/State.test.ts index 4c675b348..5785ac4a0 100644 --- a/packages/solana-wallet-snap/src/core/services/state/State.test.ts +++ b/packages/solana-wallet-snap/src/core/services/state/State.test.ts @@ -86,6 +86,12 @@ describe('State', () => { expect(stateValue).toStrictEqual(DEFAULT_STATE); }); + it('preserves defaults when persisted state values are undefined', async () => { + snap.request.mockResolvedValue({ users: undefined }); + + expect(await state.get()).toStrictEqual(DEFAULT_STATE); + }); + describe('when getting serialized non-JSON values', () => { it('deserializes undefined values', async () => { const mockUnderlyingState = { diff --git a/packages/solana-wallet-snap/src/core/services/state/State.ts b/packages/solana-wallet-snap/src/core/services/state/State.ts index 2271a5ce4..465224fa9 100644 --- a/packages/solana-wallet-snap/src/core/services/state/State.ts +++ b/packages/solana-wallet-snap/src/core/services/state/State.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { Transaction } from '@metamask/keyring-api'; +import { safeMerge } from '@metamask/snap-networks-utils'; import type { Address, Signature } from '@solana/kit'; import type { MutexInterface } from 'async-mutex'; import { Mutex } from 'async-mutex'; @@ -16,7 +17,6 @@ import type { SpotPrices } from '../../clients/price-api/types'; import { deserialize } from '../../serialization/deserialize'; import { serialize } from '../../serialization/serialize'; import type { Serializable } from '../../serialization/types'; -import { safeMerge } from '../../utils/safeMerge'; import type { IStateManager } from './IStateManager'; export type AccountId = string; diff --git a/packages/solana-wallet-snap/src/core/utils/safeMerge.ts b/packages/solana-wallet-snap/src/core/utils/safeMerge.ts deleted file mode 100644 index 69a12472e..000000000 --- a/packages/solana-wallet-snap/src/core/utils/safeMerge.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Merges two objects, keeping values from the overridee object when the overrider's corresponding properties are undefined, - * null, or empty objects. Non-undefined values from the overrider take precedence. Empty objects in the overrider are - * filtered out to preserve the overridee's values. - * - * @param overridee - The object to override. - * @param overrider - The object to override with. - * @returns The merged object. - * @example - * const overridee = { name: 'John' }; - * const overrider = { name: undefined, age: 30 }; - * const merged = safeMerge(overridee, overrider); - * // merged is { name: 'John' } - */ -export const safeMerge = ( - overridee: TOverridee, - overrider: TOverrider, -): TOverridee & TOverrider => ({ - ...overridee, - ...(Object.fromEntries( - Object.entries(overrider).filter( - ([_, value]) => - value !== undefined && - value !== null && - (!value || typeof value !== 'object' || Object.keys(value).length > 0), - ), - ) as TOverrider), -}); diff --git a/packages/tron-wallet-snap/src/services/state/State.test.ts b/packages/tron-wallet-snap/src/services/state/State.test.ts index 8070f0885..76147538e 100644 --- a/packages/tron-wallet-snap/src/services/state/State.test.ts +++ b/packages/tron-wallet-snap/src/services/state/State.test.ts @@ -113,6 +113,12 @@ describe('State', () => { expect(stateValue).toStrictEqual(DEFAULT_STATE); }); + it('preserves defaults when persisted state values are undefined', async () => { + snap.request.mockResolvedValue({ users: undefined }); + + expect(await state.get()).toStrictEqual(DEFAULT_STATE); + }); + describe('when getting serialized non-JSON values', () => { it('deserializes undefined values', async () => { const mockUnderlyingState = { diff --git a/packages/tron-wallet-snap/src/services/state/State.ts b/packages/tron-wallet-snap/src/services/state/State.ts index ae71e9921..a6eaa294c 100644 --- a/packages/tron-wallet-snap/src/services/state/State.ts +++ b/packages/tron-wallet-snap/src/services/state/State.ts @@ -1,4 +1,5 @@ import type { Transaction } from '@metamask/keyring-api'; +import { safeMerge } from '@metamask/snap-networks-utils'; import type { MutexInterface } from 'async-mutex'; import { Mutex } from 'async-mutex'; import { unset } from 'lodash'; @@ -6,7 +7,6 @@ import { unset } from 'lodash'; import type { SpotPrices } from '../../clients/price-api/types'; import type { AssetEntity } from '../../entities/assets'; import type { TronKeyringAccount } from '../../entities/keyring-account'; -import { safeMerge } from '../../utils/safeMerge'; import { deserialize } from '../../utils/serialization/deserialize'; import { serialize } from '../../utils/serialization/serialize'; import type { Serializable } from '../../utils/serialization/types'; diff --git a/packages/tron-wallet-snap/src/utils/safeMerge.test.ts b/packages/tron-wallet-snap/src/utils/safeMerge.test.ts deleted file mode 100644 index 93666ab1c..000000000 --- a/packages/tron-wallet-snap/src/utils/safeMerge.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { safeMerge } from './safeMerge'; - -describe('safeMerge', () => { - it('merges two objects and keeps existing values when overrider has undefined', () => { - const overridee = { name: 'John', age: 25 }; - const overrider = { name: undefined, title: 'Developer' }; - - const result = safeMerge(overridee, overrider); - - expect(result).toStrictEqual({ - name: 'John', - age: 25, - title: 'Developer', - }); - }); - - it('overrides values when overrider has non-undefined values', () => { - const overridee = { name: 'John', age: 25 }; - const overrider = { name: 'Jane', title: 'Engineer' }; - - const result = safeMerge(overridee, overrider); - - expect(result).toStrictEqual({ - name: 'Jane', - age: 25, - title: 'Engineer', - }); - }); - - it('handles empty objects', () => { - const overridee = {}; - const overrider = {}; - - const result = safeMerge(overridee, overrider); - - expect(result).toStrictEqual({}); - }); - - it('handles objects with null values', () => { - const overridee = { name: 'John', age: null }; - const overrider = { name: null, title: 'Developer' }; - - const result = safeMerge(overridee, overrider); - - expect(result).toStrictEqual({ - name: 'John', - age: null, - title: 'Developer', - }); - }); - - it('handles nested objects', () => { - const overridee = { - user: { - name: 'John', - details: { age: 25 }, - }, - }; - const overrider = { - user: { - name: undefined, - details: { location: 'NYC' }, - }, - }; - - const result = safeMerge(overridee, overrider); - - expect(result).toStrictEqual({ - user: { - name: undefined, - details: { location: 'NYC' }, - }, - }); - }); - - it('filters out empty objects in overrider', () => { - const overridee = { name: 'John', settings: { theme: 'dark' } }; - const overrider = { name: 'Jane', settings: {} }; - - const result = safeMerge(overridee, overrider); - - expect(result).toStrictEqual({ - name: 'Jane', - settings: { theme: 'dark' }, - }); - }); - - it('keeps non-empty objects in overrider', () => { - const overridee = { settings: { theme: 'dark' } }; - const overrider = { settings: { language: 'en' } }; - - const result = safeMerge(overridee, overrider); - - expect(result).toStrictEqual({ - settings: { language: 'en' }, - }); - }); - - it('merges multiple empty and non-empty nested objects', () => { - const overridee = { - a: { x: 1 }, - b: { y: 2 }, - c: { z: 3 }, - }; - const overrider = { - a: {}, - b: { y: 5 }, - c: {}, - }; - - const result = safeMerge(overridee, overrider); - - expect(result).toStrictEqual({ - a: { x: 1 }, - b: { y: 5 }, - c: { z: 3 }, - }); - }); -}); From 5059efdb60a59a0ce6195d185960dba883061e7a Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Wed, 19 Aug 2026 12:58:30 +0200 Subject: [PATCH 2/3] chore: update jsdoc for safeMerge util and move files to folder --- packages/snap-networks-utils/CHANGELOG.md | 2 +- packages/snap-networks-utils/src/index.ts | 2 +- .../src/{ => safeMerge}/safeMerge.test.ts | 0 .../src/{ => safeMerge}/safeMerge.ts | 11 ++++++----- 4 files changed, 8 insertions(+), 7 deletions(-) rename packages/snap-networks-utils/src/{ => safeMerge}/safeMerge.test.ts (100%) rename packages/snap-networks-utils/src/{ => safeMerge}/safeMerge.ts (54%) diff --git a/packages/snap-networks-utils/CHANGELOG.md b/packages/snap-networks-utils/CHANGELOG.md index 89923972f..72df59a78 100644 --- a/packages/snap-networks-utils/CHANGELOG.md +++ b/packages/snap-networks-utils/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add a `safeMerge` utility for shallowly merging objects. +- Add a `safeMerge` utility for shallowly merging objects. ([#166](https://github.com/MetaMask/internal-snaps/pull/166)) ### Changed diff --git a/packages/snap-networks-utils/src/index.ts b/packages/snap-networks-utils/src/index.ts index bdac7e8e7..7652027da 100644 --- a/packages/snap-networks-utils/src/index.ts +++ b/packages/snap-networks-utils/src/index.ts @@ -8,4 +8,4 @@ export { RemoteFeatureFlagsProvider, type RemoteFeatureFlagsProviderMessenger, } from './providers/remote-feature-flags/RemoteFeatureFlagsProvider'; -export { safeMerge } from './safeMerge'; +export { safeMerge } from './safeMerge/safeMerge'; diff --git a/packages/snap-networks-utils/src/safeMerge.test.ts b/packages/snap-networks-utils/src/safeMerge/safeMerge.test.ts similarity index 100% rename from packages/snap-networks-utils/src/safeMerge.test.ts rename to packages/snap-networks-utils/src/safeMerge/safeMerge.test.ts diff --git a/packages/snap-networks-utils/src/safeMerge.ts b/packages/snap-networks-utils/src/safeMerge/safeMerge.ts similarity index 54% rename from packages/snap-networks-utils/src/safeMerge.ts rename to packages/snap-networks-utils/src/safeMerge/safeMerge.ts index b860867f0..f4b95d327 100644 --- a/packages/snap-networks-utils/src/safeMerge.ts +++ b/packages/snap-networks-utils/src/safeMerge/safeMerge.ts @@ -1,16 +1,17 @@ /** - * Merges two objects, keeping values from the overridee object when the overrider's corresponding properties are undefined, - * null, or empty objects. Non-undefined values from the overrider take precedence. Empty objects in the overrider are - * filtered out to preserve the overridee's values. + * Shallowly merges two objects at the top level, keeping values from the overridee object when the overrider's + * corresponding top-level properties are undefined, null, or empty objects. Non-undefined values from the overrider + * take precedence. Empty objects in the overrider are filtered out to preserve the overridee's values. Nested objects + * are not merged recursively: if a nested property exists in the overrider, it replaces the overridee's value entirely. * * @param overridee - The object to override. * @param overrider - The object to override with. - * @returns The merged object. + * @returns The merged object (using shallow, top-level merge semantics). * @example * const overridee = { name: 'John' }; * const overrider = { name: undefined, age: 30 }; * const merged = safeMerge(overridee, overrider); - * // merged is { name: 'John' } + * // merged is { name: 'John', age: 30 } */ export const safeMerge = ( overridee: TOverridee, From a6c85303f3adc0ee16dbbbc6c4e7565f24d953e1 Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Wed, 19 Aug 2026 13:01:52 +0200 Subject: [PATCH 3/3] chore: add index export for logger --- packages/snap-networks-utils/src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/snap-networks-utils/src/index.ts b/packages/snap-networks-utils/src/index.ts index 7652027da..9ff931c6d 100644 --- a/packages/snap-networks-utils/src/index.ts +++ b/packages/snap-networks-utils/src/index.ts @@ -9,3 +9,11 @@ export { type RemoteFeatureFlagsProviderMessenger, } from './providers/remote-feature-flags/RemoteFeatureFlagsProvider'; export { safeMerge } from './safeMerge/safeMerge'; +export { Logger, LogLevel } from './logger'; +export type { + LoggerOptions, + LoggerMethod, + LogMethod, + LogMethodDecorator, + LoggerDecorators, +} from './logger';