Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(keyring-utils): add isScopeEqual + isScopeEqualToAny #222

Merged
merged 4 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions packages/keyring-utils/src/scopes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { isScopeEqual, isScopeEqualToAny } from './scopes';

const ETH_EOA = 'eip155:0';
const ETH_MAINNET = 'eip155:1';
const ETH_TESTNET = 'eip155:11155111';
const BTC_MAINNET = 'bip122:000000000019d6689c085ae165831e93';
const BTC_TESTNET = 'bip122:000000000933ea01ad0ee984209779ba';

describe('isScopeEqual', () => {
it('returns true when both scopes are equal', () => {
expect(isScopeEqual(ETH_MAINNET, ETH_MAINNET)).toBe(true);
expect(isScopeEqual(ETH_TESTNET, ETH_TESTNET)).toBe(true);

expect(isScopeEqual(BTC_MAINNET, BTC_MAINNET)).toBe(true);
expect(isScopeEqual(BTC_TESTNET, BTC_TESTNET)).toBe(true);
});

it('returns false when both scopes are not equal', () => {
expect(isScopeEqual(ETH_MAINNET, ETH_TESTNET)).toBe(false);
expect(isScopeEqual(ETH_TESTNET, ETH_MAINNET)).toBe(false);

expect(isScopeEqual(BTC_MAINNET, BTC_TESTNET)).toBe(false);
expect(isScopeEqual(BTC_TESTNET, BTC_MAINNET)).toBe(false);
});

it('supports EVM EOA scopes', () => {
expect(isScopeEqual(ETH_MAINNET, ETH_EOA)).toBe(true);
expect(isScopeEqual(ETH_EOA, ETH_MAINNET)).toBe(true);

expect(isScopeEqual(ETH_TESTNET, ETH_EOA)).toBe(true);
expect(isScopeEqual(ETH_EOA, ETH_TESTNET)).toBe(true);

expect(isScopeEqual(ETH_EOA, BTC_MAINNET)).toBe(false);
expect(isScopeEqual(BTC_MAINNET, ETH_EOA)).toBe(false);
});
});

describe('isScopeEqualToAny', () => {
it('returns true when both scopes are equal', () => {
expect(isScopeEqualToAny(ETH_MAINNET, [ETH_TESTNET, ETH_MAINNET])).toBe(
true,
);
expect(isScopeEqualToAny(BTC_MAINNET, [BTC_MAINNET, BTC_TESTNET])).toBe(
true,
);
});

it('returns false when both scopes are not equal', () => {
expect(isScopeEqualToAny(ETH_MAINNET, [ETH_TESTNET])).toBe(false);
expect(isScopeEqualToAny(ETH_MAINNET, [])).toBe(false);
expect(isScopeEqualToAny(BTC_MAINNET, [ETH_MAINNET])).toBe(false);
});

it('supports EVM EOA scopes', () => {
expect(isScopeEqualToAny(ETH_EOA, [ETH_TESTNET, ETH_MAINNET])).toBe(true);
expect(isScopeEqualToAny(ETH_MAINNET, [ETH_TESTNET, ETH_EOA])).toBe(true);

expect(isScopeEqualToAny(BTC_MAINNET, [ETH_TESTNET, ETH_EOA])).toBe(false);
expect(isScopeEqualToAny(ETH_EOA, [BTC_MAINNET, BTC_TESTNET])).toBe(false);
});
});
50 changes: 50 additions & 0 deletions packages/keyring-utils/src/scopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { CaipChainId } from '@metamask/utils';
import { KnownCaipNamespace } from '@metamask/utils';

// We do not use the `EthScope` from `@metamask/keyring-api` here to avoid pulling it
// as a dependency.
export const ETH_SCOPE_EOA = `${KnownCaipNamespace.Eip155}:0`;

// We use a string prefix when comparing EOAs to avoid unecessary splits.
const ETH_SCOPE_PREFIX = `${KnownCaipNamespace.Eip155}:`;

/**
* Check if scope matches another scope. It also supports the special
* case of `eip155:0` for EVM EOA chain ID which is compatible with any EVM chain
* ID (`eip155:*`).
*
* @param scope - The scope (CAIP-2 chain ID) to check.
* @param other - Another scope to compare to.
* @returns True if both scope are compatible, false otherwise.
*/
export function isScopeEqual(scope: CaipChainId, other: CaipChainId): boolean {
const isScopeEoa = scope === ETH_SCOPE_EOA;
const isOtherEoa = other === ETH_SCOPE_EOA;

// Special case for EOA scopes (we check on both sides).
if (isScopeEoa) {
return other.startsWith(ETH_SCOPE_PREFIX);
}
if (isOtherEoa) {
return scope.startsWith(ETH_SCOPE_PREFIX);
}

// Normal case, if both scopes strictly match, then they are compatible.
return scope === other;
}

/**
* Check if `scope` matches any scope from `scopes`. It also supports the special
* case of `eip155:0` for EVM EOA chain ID which is compatible with any EVM chain
* ID (`eip155:*`).
*
* @param scope - The scope (CAIP-2 chain ID) to check.
* @param scopes - The list of scopes to check against.
* @returns True if `scope` matches any scope from `scopes`, false otherwise.
*/
export function isScopeEqualToAny(
scope: CaipChainId,
scopes: CaipChainId[],
): boolean {
return scopes.some((other) => isScopeEqual(scope, other));
}
Loading