feat: initial Pezkuwi Apps rebrand from polkadot-apps

Rebranded terminology:
- Polkadot → Pezkuwi
- Kusama → Dicle
- Westend → Zagros
- Rococo → PezkuwiChain
- Substrate → Bizinikiwi
- parachain → teyrchain

Custom logos with Kurdistan brand colors (#e6007a → #86e62a):
- bizinikiwi-hexagon.svg
- sora-bizinikiwi.svg
- hezscanner.svg
- heztreasury.svg
- pezkuwiscan.svg
- pezkuwistats.svg
- pezkuwiassembly.svg
- pezkuwiholic.svg
This commit is contained in:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
@@ -0,0 +1,13 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
export interface AccountStoreApi {
all: () => Promise<{ key: string, value: KeyringJson }[]>
get: (key: string) => Promise<KeyringJson>
remove: (key: string) => Promise<void>
set: (key: string, value: KeyringJson) => Promise<void>
}
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountStoreApi } from './account-store-api.js';
export interface ElectronMainApi {
accountStore: AccountStoreApi
}
@@ -0,0 +1,12 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ElectronMainApi } from './electron-main-api.js';
declare global {
interface Window {
ElectronMain: ElectronMainApi
}
}
export const electronMainApi = window.ElectronMain;
@@ -0,0 +1,18 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AppUpdater } from 'electron-updater';
export async function setupAutoUpdater (): Promise<void> {
const { autoUpdater } = await import('electron-updater');
await setLogger(autoUpdater);
autoUpdater.checkForUpdatesAndNotify().catch(console.error);
}
async function setLogger (autoUpdater: AppUpdater): Promise<void> {
const log = await import('electron-log');
log.transports.file.level = 'debug';
autoUpdater.logger = log;
}
@@ -0,0 +1,29 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeadersReceivedResponse } from 'electron';
import { session } from 'electron';
export function setupContentSecurityPolicy (_: string): void {
session.defaultSession.webRequest.onHeadersReceived((details, respond: (response: HeadersReceivedResponse) => void) => {
respond({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self';" +
" style-src-elem 'self' https://fonts.googleapis.com/css 'unsafe-inline';" +
" font-src data: 'self' https://fonts.gstatic.com;" +
" style-src 'unsafe-inline';" +
" connect-src 'self' wss: ws:;" +
" img-src 'self' data:;" +
// react-qr-reader uses an embedded blob
" worker-src 'self' blob: filesystem:;" +
// unsafe-eval is needed for the WASM content - same as the extension
// script hashes here are for the window.top script (not technically needed)
" script-src 'self' 'unsafe-eval' 'sha256-02/ejyoV/iwRdJ4NAsxjzF6WVUtLMPM6Nv96EbAm6u8=' 'sha256-wW/WsLudCDaPo/ibpeK0KslHqYpCzcAKNFxFBXwCHJg='"
]
}
});
});
}
@@ -0,0 +1,26 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { app } from 'electron';
import { registerAccountStoreHandlers } from '../main/account-store.js';
import { setupAutoUpdater } from './autoUpdater.js';
import { setupContentSecurityPolicy } from './contentSecurityPolicy.js';
import { createWindow } from './window.js';
const ENV = process.env.NODE_ENV || 'production';
app.on('web-contents-created', (_, webContents): void => {
webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
});
app
.whenReady()
.then(async (): Promise<void> => {
registerAccountStoreHandlers();
setupContentSecurityPolicy(ENV);
await createWindow(ENV);
await setupAutoUpdater();
})
.catch(console.error);
@@ -0,0 +1,50 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { BrowserWindow, screen, shell } from 'electron';
import path from 'path';
export function createWindow (environment: string): Promise<unknown> {
const { height, width } = screen.getPrimaryDisplay().workAreaSize;
const win = new BrowserWindow({
height,
icon: path.join(__dirname, 'icon.png'),
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js')
},
width
});
if (environment === 'development') {
win.webContents.openDevTools();
return win.loadURL('http://127.0.0.1:3000/');
}
// Handle attempts to open a new window via window.open()
win.webContents.setWindowOpenHandler(({ url }) => {
// Open all http/https URLs externally
if (url.startsWith('http://') || url.startsWith('https://')) {
shell.openExternal(url).catch(console.log);
return { action: 'deny' };
}
return { action: 'allow' };
});
// Handle in-app navigation attempts, such as clicking on <a href="...">
win.webContents.on('will-navigate', (event, url) => {
if (url.startsWith('http://') || url.startsWith('https://')) {
event.preventDefault();
shell.openExternal(url).catch(console.log);
}
});
const mainFilePath = path.resolve(__dirname, 'index.html');
return win.loadFile(mainFilePath);
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
// setup these right at front
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import '@pezkuwi/apps/initSettings';
import 'semantic-ui-css/semantic.min.css';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import '@pezkuwi/react-components/i18n';
import React from 'react';
import { createRoot } from 'react-dom/client';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import Root from '@pezkuwi/apps/Root';
import { electronMainApi } from './api/global-exported-api.js';
import { RemoteElectronStore } from './renderer/remote-electron-store.js';
const rootId = 'root';
const rootElement = document.getElementById(rootId);
if (!rootElement) {
throw new Error(`Unable to find element with id '${rootId}'`);
}
const store = new RemoteElectronStore(electronMainApi.accountStore);
createRoot(rootElement).render(
<Root
isElectron
store={store}
/>
);
@@ -0,0 +1,72 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
import type { IpcMainHandler } from './ipc-main-handler.js';
import * as tmp from 'tmp';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import { FileStore } from '@pezkuwi/ui-keyring/stores';
import { accountStoreIpcHandler } from './account-store.js';
const exampleAccount = (address: string): KeyringJson => ({
address,
meta: {}
});
describe('Account store', () => {
let accountStore: IpcMainHandler;
let tmpDir: tmp.DirResult;
beforeEach(() => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
accountStore = accountStoreIpcHandler(new FileStore(tmpDir.name));
});
afterEach(() => {
tmpDir.removeCallback();
});
it('all returns empty array at first', () => {
expect(accountStore['account-store-all']()).toEqual([]);
});
it('after adding accounts, they become visible', async () => {
await accountStore['account-store-set']('1', exampleAccount('a'));
await accountStore['account-store-set']('2', exampleAccount('b'));
expect(accountStore['account-store-all']()).toEqual([{
key: '1', value: exampleAccount('a')
}, {
key: '2', value: exampleAccount('b')
}]);
});
it('get returns account if exists', async () => {
await accountStore['account-store-set']('1', exampleAccount('a'));
expect(await accountStore['account-store-get']('1')).toEqual(exampleAccount('a'));
});
it('get returns null if account does not exist', async () => {
// jest.spyOn(console, 'error').mockImplementationOnce(() => { /**/ });
expect(await accountStore['account-store-get']('1')).toEqual(null);
});
it('account disappears from list after it is removed', async () => {
// jest.spyOn(console, 'error').mockImplementationOnce(() => { /**/ });
await accountStore['account-store-set']('1', exampleAccount('a'));
await accountStore['account-store-remove']('1');
expect(await accountStore['account-store-get']('1')).toEqual(null);
expect(accountStore['account-store-all']()).toEqual([]);
});
});
@@ -0,0 +1,60 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
import type { IpcMainHandler } from './ipc-main-handler.js';
import electron from 'electron';
import path from 'path';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import { FileStore } from '@pezkuwi/ui-keyring/stores';
import { registerIpcHandler } from './register-ipc-handler.js';
const ACCOUNTS_SUBFOLDER = 'pezkuwi-accounts';
function safeWriteKey (key: string) {
return key.replace(/:/g, '-');
}
function safeReadKey (key: string) {
return key.replace(/-/g, ':');
}
export const accountStoreIpcHandler = (fileStore: FileStore): IpcMainHandler => ({
'account-store-all': () => {
let result: { key: string, value: KeyringJson }[] = [];
const collect = (key: string, value: KeyringJson) => {
result = [...result, { key: safeReadKey(key), value }];
};
fileStore.all(collect);
return result;
},
'account-store-get': async (key: string) => new Promise((resolve) => {
try {
fileStore.get(safeWriteKey(key), resolve);
} catch {
resolve(null);
}
}),
'account-store-remove': async (key: string) => new Promise((resolve) =>
fileStore.remove(safeWriteKey(key), () => resolve(undefined))
),
'account-store-set': async (key: string, value: KeyringJson) => new Promise((resolve) =>
fileStore.set(safeWriteKey(key), value, () => resolve(undefined))
)
});
export const registerAccountStoreHandlers = (): void => {
const defaultStorePath = path.join(electron.app.getPath('userData'), ACCOUNTS_SUBFOLDER);
const fileStore = new FileStore(defaultStorePath);
registerIpcHandler(accountStoreIpcHandler(fileStore));
};
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
export type IpcMainHandler = Record<string, (...args: any[]) => unknown>;
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { IpcMainHandler } from './ipc-main-handler.js';
import electron from 'electron';
export const registerIpcHandler = (ipcHandler: IpcMainHandler): void => {
for (const [channel, listener] of Object.entries(ipcHandler)) {
electron.ipcMain.handle(channel, (_, ...args: unknown[]) => {
return listener(...args);
});
}
};
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('ElectronMain', {
accountStore: {
all: () => ipcRenderer.invoke('account-store-all'),
get: (key: string) => ipcRenderer.invoke('account-store-get', key),
remove: (key: string) => ipcRenderer.invoke('account-store-remove', key),
set: (key: string, value: KeyringJson) => ipcRenderer.invoke('account-store-set', key, value)
}
});
@@ -0,0 +1,95 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import type { KeyringJson } from '@pezkuwi/ui-keyring/types';
import { RemoteElectronStore } from './remote-electron-store.js';
describe('Remote Electron Store', () => {
const accountStore = {
all: jest.fn(),
get: jest.fn(),
remove: jest.fn(),
set: jest.fn()
};
const remoteStore = new RemoteElectronStore(accountStore);
beforeEach(() => {
accountStore.all.mockClear();
accountStore.get.mockClear();
accountStore.remove.mockClear();
accountStore.set.mockClear();
});
describe('all', () => {
it('calls callback for each returned account', async () => {
accountStore.all.mockResolvedValue([{
key: 1,
value: 'a'
}, {
key: 2,
value: 'b'
}]);
const cb = jest.fn();
remoteStore.all(cb);
await Promise.resolve();
expect(cb).toHaveBeenNthCalledWith(1, 1, 'a');
expect(cb).toHaveBeenNthCalledWith(2, 2, 'b');
});
});
describe('get', () => {
it('calls callback with returned account', async () => {
accountStore.get.mockResolvedValue('a');
const cb = jest.fn();
remoteStore.get('1', cb);
await Promise.resolve();
expect(accountStore.get).toHaveBeenCalledWith('1');
expect(cb).toHaveBeenCalledWith('a');
});
it('calls callback with null if no accounts found', async () => {
accountStore.get.mockResolvedValue(null);
const cb = jest.fn();
remoteStore.get('1', cb);
await Promise.resolve();
expect(cb).toHaveBeenCalledWith(null);
});
});
describe('remove', () => {
it('calls callback after success', async () => {
accountStore.remove.mockResolvedValue(null);
const cb = jest.fn();
remoteStore.remove('1', cb);
await Promise.resolve();
expect(accountStore.remove).toHaveBeenCalledWith('1');
expect(cb).toHaveBeenCalledTimes(1);
});
});
describe('set', () => {
it('calls callback after success', async () => {
accountStore.set.mockResolvedValue(null);
const cb = jest.fn();
remoteStore.set('1', 'a' as unknown as KeyringJson, cb);
await Promise.resolve();
expect(accountStore.set).toHaveBeenCalledWith('1', 'a');
expect(cb).toHaveBeenCalledTimes(1);
});
});
});
@@ -0,0 +1,42 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Warned on by nodenext resolution (while package does build in bundler mode)
import type { KeyringJson, KeyringStore } from '@pezkuwi/ui-keyring/types';
import type { AccountStoreApi } from '../api/account-store-api.js';
export class RemoteElectronStore implements KeyringStore {
readonly #accountStore: AccountStoreApi;
constructor (accountStore: AccountStoreApi) {
this.#accountStore = accountStore;
}
all (cb: (key: string, value: KeyringJson) => void): void {
this.#accountStore.all()
.then((result: { key: string, value: KeyringJson }[]) => result?.forEach(({ key, value }) => cb(key, value)))
.catch((e: Error) => {
throw new Error(`error getting all accounts: ${e.message}`);
});
}
get (key: string, cb: (value: KeyringJson) => void): void {
this.#accountStore.get(key)
.then(cb).catch((e: Error) => {
throw new Error(`error storing account: ${e.message}`);
});
}
remove (key: string, cb: (() => void) | undefined): void {
this.#accountStore.remove(key).then(cb).catch((e: Error) => {
throw new Error(`error removing account: ${e.message}`);
});
}
set (key: string, value: KeyringJson, cb: (() => void) | undefined): void {
this.#accountStore.set(key, value).then(cb).catch((e: Error) => {
throw new Error(`error saving account: ${e.message}`);
});
}
}