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..72df59a78 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. ([#166](https://github.com/MetaMask/internal-snaps/pull/166)) + +### 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..9ff931c6d 100644 --- a/packages/snap-networks-utils/src/index.ts +++ b/packages/snap-networks-utils/src/index.ts @@ -8,3 +8,12 @@ export { RemoteFeatureFlagsProvider, 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'; diff --git a/packages/solana-wallet-snap/src/core/utils/safeMerge.test.ts b/packages/snap-networks-utils/src/safeMerge/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/safeMerge.test.ts diff --git a/packages/tron-wallet-snap/src/utils/safeMerge.ts b/packages/snap-networks-utils/src/safeMerge/safeMerge.ts similarity index 54% rename from packages/tron-wallet-snap/src/utils/safeMerge.ts rename to packages/snap-networks-utils/src/safeMerge/safeMerge.ts index b860867f0..f4b95d327 100644 --- a/packages/tron-wallet-snap/src/utils/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, 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 }, - }); - }); -});