mirror of
https://github.com/pezkuwichain/pezkuwi-extension.git
synced 2026-07-19 03:35:44 +00:00
Update domain references to pezkuwichain.app and rebrand from polkadot
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/* eslint-disable jest/expect-expect */
|
||||
|
||||
import type * as _ from '@pezkuwi/dev-test/globals.d.ts';
|
||||
import type { AccountJson, AccountWithChildren } from '@pezkuwi/extension-base/background/types';
|
||||
|
||||
import { buildHierarchy } from './buildHierarchy.js';
|
||||
|
||||
const genesisExample = {
|
||||
KUSAMA: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe',
|
||||
POLKADOT: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3'
|
||||
} as const;
|
||||
|
||||
const testHierarchy = (accounts: AccountJson[], expected: AccountWithChildren[]): void => {
|
||||
expect(buildHierarchy(accounts)).toEqual(expected);
|
||||
};
|
||||
|
||||
describe('Use Account Hierarchy', () => {
|
||||
const acc = (address: string, parentAddress?: string, whenCreated?: number, name?: string, suri?: string): {
|
||||
address: string;
|
||||
name?: string;
|
||||
parentAddress?: string;
|
||||
suri?: string;
|
||||
whenCreated?: number;
|
||||
} => ({ address, name, parentAddress, suri, whenCreated });
|
||||
|
||||
it('for empty account list, returns empty list', () => {
|
||||
testHierarchy([], []);
|
||||
});
|
||||
|
||||
it('returns one account', () => {
|
||||
testHierarchy([acc('a')], [acc('a')]);
|
||||
});
|
||||
|
||||
it('puts child account into children field of parent: single child', () => {
|
||||
testHierarchy([acc('a'), acc('b', 'a')], [
|
||||
{ address: 'a', children: [acc('b', 'a')], name: undefined, parentAddress: undefined, suri: undefined, whenCreated: undefined }
|
||||
]);
|
||||
});
|
||||
|
||||
it('puts child account into children field of parent: more children', () => {
|
||||
testHierarchy([acc('a'), acc('b', 'a'), acc('c', 'a')], [
|
||||
{ address: 'a', children: [acc('b', 'a'), acc('c', 'a')], name: undefined, parentAddress: undefined, suri: undefined, whenCreated: undefined }
|
||||
]);
|
||||
});
|
||||
|
||||
it('puts child account into children field of parent: 2 roots', () => {
|
||||
testHierarchy([acc('a'), acc('b', 'a'), acc('c', 'a'), acc('d')], [
|
||||
{ address: 'a', children: [acc('b', 'a'), acc('c', 'a')], name: undefined, parentAddress: undefined, suri: undefined, whenCreated: undefined },
|
||||
acc('d')
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles grandchildren', () => {
|
||||
testHierarchy([acc('a'), acc('b', 'a'), acc('c', 'b')], [{
|
||||
address: 'a',
|
||||
children: [{
|
||||
...acc('b', 'a'),
|
||||
children: [acc('c', 'b')]
|
||||
}],
|
||||
name: undefined,
|
||||
parentAddress: undefined,
|
||||
suri: undefined,
|
||||
whenCreated: undefined
|
||||
}]);
|
||||
});
|
||||
|
||||
it('sorts accounts by network', () => {
|
||||
testHierarchy(
|
||||
[{ address: 'b', genesisHash: genesisExample.KUSAMA }, { address: 'a', genesisHash: genesisExample.POLKADOT }, { address: 'c', genesisHash: genesisExample.KUSAMA }],
|
||||
[{ address: 'b', genesisHash: genesisExample.KUSAMA }, { address: 'c', genesisHash: genesisExample.KUSAMA }, { address: 'a', genesisHash: genesisExample.POLKADOT }]
|
||||
);
|
||||
});
|
||||
|
||||
it('sorts accounts by network and name', () => {
|
||||
testHierarchy(
|
||||
[{ address: 'b', genesisHash: genesisExample.KUSAMA, name: 'b-last-kusama' }, { address: 'a', genesisHash: genesisExample.POLKADOT }, { address: 'c', genesisHash: genesisExample.KUSAMA, name: 'a-first-kusama' }],
|
||||
[{ address: 'c', genesisHash: genesisExample.KUSAMA, name: 'a-first-kusama' }, { address: 'b', genesisHash: genesisExample.KUSAMA, name: 'b-last-kusama' }, { address: 'a', genesisHash: genesisExample.POLKADOT }]
|
||||
);
|
||||
});
|
||||
|
||||
it('sorts accounts by name and creation date', () => {
|
||||
testHierarchy(
|
||||
[acc('b', undefined, 2, 'b'), acc('z', undefined, 1, 'b'), acc('a', undefined, 4, 'a')],
|
||||
[{ address: 'a', name: 'a', parentAddress: undefined, suri: undefined, whenCreated: 4 }, { address: 'z', name: 'b', parentAddress: undefined, suri: undefined, whenCreated: 1 }, { address: 'b', name: 'b', parentAddress: undefined, suri: undefined, whenCreated: 2 }]
|
||||
);
|
||||
});
|
||||
|
||||
it('sorts account children by name and path', () => {
|
||||
testHierarchy(
|
||||
[acc('a', undefined, 1, 'a'), acc('b', 'a', 1, 'b', '/2'), acc('b', 'a', 1, 'b', '/0')],
|
||||
[{ address: 'a', children: [acc('b', 'a', 1, 'b', '/0'), acc('b', 'a', 1, 'b', '/2')], name: 'a', parentAddress: undefined, suri: undefined, whenCreated: 1 }]
|
||||
);
|
||||
});
|
||||
|
||||
it('sorts accounts with children by name and creation date', () => {
|
||||
testHierarchy(
|
||||
[acc('b', undefined, 2, 'b'), acc('z', undefined, 1, 'b'), acc('d', 'b', 2, 'd'), acc('c', 'b', 3, 'c'), acc('a', undefined, 4, 'a')],
|
||||
[{ address: 'a', name: 'a', parentAddress: undefined, suri: undefined, whenCreated: 4 }, { address: 'z', name: 'b', parentAddress: undefined, suri: undefined, whenCreated: 1 }, { address: 'b', children: [acc('c', 'b', 3, 'c'), acc('d', 'b', 2, 'd')], name: 'b', parentAddress: undefined, suri: undefined, whenCreated: 2 }]
|
||||
);
|
||||
});
|
||||
|
||||
it('if creation time is missing, puts account at the back of a list', () => {
|
||||
testHierarchy(
|
||||
[acc('a'), acc('b', undefined, 2), acc('c', undefined, 1)],
|
||||
[acc('c', undefined, 1), acc('b', undefined, 2), acc('a')]
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountJson, AccountWithChildren } from '@pezkuwi/extension-base/background/types';
|
||||
|
||||
import getNetworkMap from './getNetworkMap.js';
|
||||
|
||||
type ChildFilter = (account: AccountJson) => AccountWithChildren;
|
||||
|
||||
function compareByCreation (a: AccountJson, b: AccountJson): number {
|
||||
return (a.whenCreated || Infinity) - (b.whenCreated || Infinity);
|
||||
}
|
||||
|
||||
function compareByName (a: AccountJson, b: AccountJson): number {
|
||||
const nameA = a.name?.toUpperCase() || '';
|
||||
const nameB = b.name?.toUpperCase() || '';
|
||||
|
||||
return nameA.localeCompare(nameB);
|
||||
}
|
||||
|
||||
function compareByPath (a: AccountJson, b: AccountJson): number {
|
||||
const suriA = a.suri?.toUpperCase() || '';
|
||||
const suriB = b.suri?.toUpperCase() || '';
|
||||
|
||||
return suriA.localeCompare(suriB);
|
||||
}
|
||||
|
||||
function compareByNetwork (a: AccountJson, b: AccountJson): number {
|
||||
const networkMap = getNetworkMap();
|
||||
const networkA = networkMap.get(a?.genesisHash || '') || '';
|
||||
const networkB = networkMap.get(b?.genesisHash || '') || '';
|
||||
|
||||
return networkA.localeCompare(networkB);
|
||||
}
|
||||
|
||||
function compareByPathThenCreation (a: AccountJson, b: AccountJson): number {
|
||||
// if the paths are equal, compare by creation time
|
||||
return compareByPath(a, b) || compareByCreation(a, b);
|
||||
}
|
||||
|
||||
function compareByNameThenPathThenCreation (a: AccountJson, b: AccountJson): number {
|
||||
// This comparison happens after an initial sorting by network.
|
||||
// if the 2 accounts are from different networks, don't touch their order
|
||||
if (a.genesisHash !== b.genesisHash) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// if the names are equal, compare by path then creation time
|
||||
return compareByName(a, b) || compareByPathThenCreation(a, b);
|
||||
}
|
||||
|
||||
export function accountWithChildren (accounts: AccountJson[]): ChildFilter {
|
||||
return (account: AccountJson): AccountWithChildren => {
|
||||
const children = accounts
|
||||
.filter(({ parentAddress }) => account.address === parentAddress)
|
||||
.map(accountWithChildren(accounts))
|
||||
.sort(compareByNameThenPathThenCreation);
|
||||
|
||||
return children.length === 0
|
||||
? account
|
||||
: { children, ...account };
|
||||
};
|
||||
}
|
||||
|
||||
export function buildHierarchy (accounts: AccountJson[]): AccountWithChildren[] {
|
||||
return accounts
|
||||
.filter(({ parentAddress }) =>
|
||||
// it is a parent
|
||||
!parentAddress ||
|
||||
// we don't have a parent for this one
|
||||
!accounts.some(({ address }) => parentAddress === address)
|
||||
)
|
||||
.map(accountWithChildren(accounts))
|
||||
.sort(compareByNetwork)
|
||||
.sort(compareByNameThenPathThenCreation);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { MetadataDefBase } from '@pezkuwi/extension-inject/types';
|
||||
|
||||
import { selectableNetworks } from '@pezkuwi/networks';
|
||||
|
||||
const hashes: MetadataDefBase[] = selectableNetworks
|
||||
.filter(({ genesisHash }) => !!genesisHash.length)
|
||||
.map((network) => ({
|
||||
chain: network.displayName,
|
||||
genesisHash: network.genesisHash[0],
|
||||
icon: network.icon,
|
||||
ss58Format: network.prefix
|
||||
}));
|
||||
|
||||
export default hashes;
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KeypairType } from '@pezkuwi/util-crypto/types';
|
||||
|
||||
export const DEFAULT_TYPE: KeypairType = 'sr25519';
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface Option {
|
||||
info?: string;
|
||||
isDisabled?: boolean;
|
||||
isHeader?: boolean;
|
||||
text: React.ReactNode;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export default function getLanguageOptions (): Option[] {
|
||||
return [
|
||||
// default/native
|
||||
{
|
||||
text: 'English',
|
||||
value: 'en'
|
||||
},
|
||||
{
|
||||
text: '汉语',
|
||||
value: 'zh'
|
||||
},
|
||||
{
|
||||
text: 'Français',
|
||||
value: 'fr'
|
||||
},
|
||||
{
|
||||
text: 'Türkce',
|
||||
value: 'tr'
|
||||
},
|
||||
{
|
||||
text: 'Polski',
|
||||
value: 'pl'
|
||||
},
|
||||
{
|
||||
text: 'ภาษาไทย',
|
||||
value: 'th'
|
||||
},
|
||||
{
|
||||
text: 'اردو',
|
||||
value: 'ur'
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import chains from './chains.js';
|
||||
|
||||
export default function getNetworkMap (): Map<string, string> {
|
||||
const res = new Map<string, string>();
|
||||
|
||||
chains.forEach((chain) => {
|
||||
res.set(chain.genesisHash, chain.chain);
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export default function (parentName?: string | null, suri?: string): string {
|
||||
return `${parentName || ''} ${suri || ''}`;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { selectableNetworks } from '@pezkuwi/networks';
|
||||
|
||||
export default selectableNetworks.filter((network) => network.hasLedgerSupport);
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type * as _ from '@pezkuwi/dev-test/globals.d.ts';
|
||||
|
||||
import { nextDerivationPath } from './nextDerivationPath.js';
|
||||
|
||||
describe('Generate Derivation Path', () => {
|
||||
const acc = (address: string, parentAddress?: string): {
|
||||
address: string;
|
||||
parentAddress?: string;
|
||||
} => ({
|
||||
address,
|
||||
parentAddress
|
||||
});
|
||||
|
||||
it('generates path for first masters child', () => {
|
||||
expect(nextDerivationPath([acc('a')], 'a')).toEqual('//0');
|
||||
});
|
||||
|
||||
it('generates path for third masters child', () => {
|
||||
expect(nextDerivationPath([acc('a'), acc('b', 'a'), acc('c', 'a')], 'a')).toEqual('//2');
|
||||
});
|
||||
|
||||
it('generates path for masters child when another root exists', () => {
|
||||
expect(nextDerivationPath([acc('a'), acc('b', 'a'), acc('c', 'a'), acc('d')], 'a')).toEqual('//2');
|
||||
});
|
||||
|
||||
it('generates path for masters grandchild', () => {
|
||||
expect(nextDerivationPath([acc('a'), acc('b', 'a'), acc('c', 'b'), acc('d', 'b')], 'b')).toEqual('//2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountJson } from '@pezkuwi/extension-base/background/types';
|
||||
|
||||
export function nextDerivationPath (accounts: AccountJson[], parentAddress: string): string {
|
||||
const siblingsCount = accounts.filter((account) => account.parentAddress === parentAddress).length;
|
||||
|
||||
return `//${siblingsCount}`;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { TFunction } from '../hooks/useTranslation.js';
|
||||
|
||||
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
|
||||
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
|
||||
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
|
||||
|
||||
// Configure zxcvbn with English dictionary and common patterns
|
||||
zxcvbnOptions.setOptions({
|
||||
dictionary: {
|
||||
...zxcvbnCommonPackage.dictionary, // common words across languages
|
||||
...zxcvbnEnPackage.dictionary // english-specific dictionary
|
||||
},
|
||||
graphs: zxcvbnCommonPackage.adjacencyGraphs, // keyboard patterns
|
||||
translations: zxcvbnEnPackage.translations // language translations for feedback messages
|
||||
});
|
||||
|
||||
export interface PasswordStrength {
|
||||
feedback: {
|
||||
warning: string;
|
||||
suggestions: string[];
|
||||
};
|
||||
score: number; // 0-4 (0: very weak, 4: very strong)
|
||||
}
|
||||
|
||||
const MIN_LENGTH = 6; // Minimum password length requirement
|
||||
|
||||
export function validatePasswordStrength (password: string, t: TFunction): PasswordStrength {
|
||||
const result = zxcvbn(password);
|
||||
|
||||
// First check: Minimum length requirement
|
||||
if (password.length < MIN_LENGTH) {
|
||||
return {
|
||||
feedback: {
|
||||
suggestions: [t('Password must be at least {{length}} characters long', { replace: { length: MIN_LENGTH } })],
|
||||
warning: t('Password is too short')
|
||||
},
|
||||
score: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Combine zxcvbn suggestions with our custom ones
|
||||
const suggestions = (result.feedback.suggestions || []).map((suggestion) => t(suggestion));
|
||||
|
||||
return {
|
||||
feedback: {
|
||||
suggestions,
|
||||
warning: result.feedback.warning ? t(result.feedback.warning) : ''
|
||||
},
|
||||
score: result.score
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const sanitizeHtml = (input: string, options?: {
|
||||
allowLineBreaks?: boolean;
|
||||
maxLength?: number;
|
||||
}): string => {
|
||||
if (!input || typeof input !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
let sanitized = input;
|
||||
|
||||
// Remove all HTML tags
|
||||
sanitized = sanitized.replace(/<[^>]*>/g, '');
|
||||
|
||||
// Handle line breaks if specified
|
||||
if (options?.allowLineBreaks) {
|
||||
// Convert <br> to actual line breaks before sanitization
|
||||
sanitized = input
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<[^>]*>/g, ''); // Remove other HTML tags
|
||||
}
|
||||
|
||||
// Normalize whitespace
|
||||
sanitized = sanitized
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ');
|
||||
|
||||
// Apply length limit if specified
|
||||
if (options?.maxLength && sanitized.length > options.maxLength) {
|
||||
sanitized = sanitized.substring(0, options.maxLength) + '...';
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
export const sanitizeOrigin = (origin: string): string => {
|
||||
return sanitizeHtml(origin, {
|
||||
allowLineBreaks: false,
|
||||
maxLength: 50
|
||||
});
|
||||
};
|
||||
|
||||
// Validate that the origin looks legitimate
|
||||
export const validateOrigin = (origin: string): boolean => {
|
||||
const sanitized = sanitizeOrigin(origin);
|
||||
|
||||
// Special case: always allow localhost URLs
|
||||
if (/^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)(?::\d+)?(?:\/|$)/.test(sanitized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for suspicious patterns
|
||||
const suspiciousPatterns = [
|
||||
/[<>]/, // HTML characters
|
||||
/^\s*$/, // Empty or whitespace only
|
||||
/.{50,}/, // Extremely long names
|
||||
/^(?!https?:\/\/).*:\/\//, // Any protocol other than http/https
|
||||
/^https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d+)?(?:\/|$)/ // Raw IP addresses (except localhost)
|
||||
];
|
||||
|
||||
return !suspiciousPatterns.some((pattern) => pattern.test(sanitized));
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KeyringPair$Json } from '@pezkuwi/keyring/types';
|
||||
import type { KeyringPairs$Json } from '@pezkuwi/ui-keyring/types';
|
||||
|
||||
export function isKeyringPairs$Json (json: KeyringPair$Json | KeyringPairs$Json): json is KeyringPairs$Json {
|
||||
return (json.encoding.content).includes('batch-pkcs8');
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019-2025 @pezkuwi/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
interface InputError {
|
||||
errorDescription: string;
|
||||
}
|
||||
|
||||
export type ResultType<T> = { error: InputError } | { ok: T };
|
||||
|
||||
export declare type Validator<T> = (value: T) => ResultType<T> | Promise<ResultType<T>>;
|
||||
|
||||
export const Result = {
|
||||
error: <T>(errorDescription: string): ResultType<T> => ({ error: { errorDescription } }),
|
||||
isError<T> (value: ResultType<T>): value is ({ error: InputError }) {
|
||||
return Object.hasOwnProperty.call(value, 'error');
|
||||
},
|
||||
isOk<T> (value: ResultType<T>): value is ({ ok: T }) {
|
||||
return Object.hasOwnProperty.call(value, 'ok');
|
||||
},
|
||||
ok: <T>(ok: T): ResultType<T> => ({ ok })
|
||||
};
|
||||
|
||||
export function allOf <T> (...validators: Validator<T>[]): Validator<T> {
|
||||
return async (value: T): Promise<ResultType<T>> => {
|
||||
for (const validator of validators) {
|
||||
const validationResult = await validator(value);
|
||||
|
||||
if (Result.isError(validationResult)) {
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok(value);
|
||||
};
|
||||
}
|
||||
|
||||
export function isNotShorterThan (minLength: number, errorText: string): Validator<string> {
|
||||
return (value: string): ResultType<string> => {
|
||||
return value.length < minLength
|
||||
? Result.error(errorText)
|
||||
: Result.ok(value);
|
||||
};
|
||||
}
|
||||
|
||||
export function isSameAs <T> (expectedValue: T, errorText: string): Validator<T> {
|
||||
return (value: T): ResultType<T> => {
|
||||
return value !== expectedValue
|
||||
? Result.error(errorText)
|
||||
: Result.ok(value);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user