Initial commit: Pezkuwi SDK UI

Comprehensive web interface for interacting with Pezkuwi blockchain.

Features:
- Blockchain explorer
- Wallet management
- Staking interface
- Governance participation
- Developer tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 13:55:36 +03:00
commit d949863789
5831 changed files with 327739 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
const fs = require('node:fs');
const path = require('node:path');
module.exports = function findPackages () {
const pkgRoot = path.join(__dirname, '..', 'packages');
return fs
.readdirSync(pkgRoot)
.filter((entry) => {
const pkgPath = path.join(pkgRoot, entry);
return !['.', '..'].includes(entry) &&
fs.lstatSync(pkgPath).isDirectory() &&
fs.existsSync(path.join(pkgPath, 'package.json'));
})
.map((dir) => {
const jsonPath = path.join(pkgRoot, dir, 'package.json');
const { name } = JSON.parse(
fs.readFileSync(jsonPath).toString('utf-8')
);
return { dir, name };
});
};
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
const fs = require('node:fs');
const path = require('node:path');
/** @type {Record<string, string[]>} */
const defaults = {};
const i18nRoot = path.join(__dirname, '../packages/apps/public/locales');
/**
* @param {string} langRoot
* @returns {string[]}
*/
function getEntries (langRoot) {
return fs
.readdirSync(langRoot)
.filter((entry) =>
!['.', '..'].includes(entry) &&
fs.lstatSync(path.join(langRoot, entry)).isFile() &&
entry.endsWith('.json') &&
!['index.json', 'translation.json'].includes(entry)
)
.sort();
}
/**
* @param {string} lang
*/
function checkLanguage (lang) {
console.log(`*** Checking ${lang}`);
const langRoot = path.join(i18nRoot, lang);
const entries = getEntries(langRoot);
const roots = Object.keys(defaults);
const missing = roots.filter((entry) => !entries.includes(entry));
if (missing.length) {
console.log(`\ttop-level missing ${missing.length}: ${missing.join(', ')}`);
}
entries.forEach((entry) => {
const json = require(path.join(langRoot, entry));
const keys = Object.keys(json);
const root = defaults[entry];
if (!root) {
console.log(`\t> ${entry} not found in default, not checking`);
return;
}
const missing = root.filter((key) => !keys.includes(key));
const extra = keys.filter((key) => !root.includes(key));
if (missing.length) {
console.log(`\t> ${entry} ${missing.length} keys missing`);
missing.forEach((key) =>
console.log(`\t\t${key}`)
);
}
if (extra.length) {
console.log(`\t> ${entry} ${extra.length} keys extra`);
extra.forEach((key) =>
console.log(`\t\t${key}`)
);
}
});
}
function checkLanguages () {
fs
.readdirSync(i18nRoot)
.filter((entry) =>
!['.', '..'].includes(entry) &&
fs.lstatSync(path.join(i18nRoot, entry)).isDirectory() &&
entry !== 'en'
)
.sort()
.forEach(checkLanguage);
}
function initDefault () {
const enRoot = path.join(i18nRoot, 'en');
getEntries(enRoot).forEach((entry) => {
const json = require(path.join(enRoot, entry));
const keys = Object.keys(json);
// if (keys.length > 0) {
// console.log(`${entry} ${keys.length} keys`);
// }
defaults[entry] = keys;
});
}
initDefault();
checkLanguages();
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
const fs = require('node:fs');
const path = require('node:path');
const i18nRoot = path.join(__dirname, '../packages/apps/public/locales');
const SKIP_NS = ['translation'].map((f) => `${f}.json`);
/**
* @param {string} langRoot
* @returns {string[]}
*/
function getEntries (langRoot) {
return fs
.readdirSync(langRoot)
.filter((entry) =>
!['.', '..'].includes(entry) &&
fs.lstatSync(path.join(langRoot, entry)).isFile() &&
entry.endsWith('.json') &&
!['index.json'].includes(entry)
)
.sort();
}
/**
* @param {string} lang
*/
function sortLanguage (lang) {
const langRoot = path.join(i18nRoot, lang);
const entries = getEntries(langRoot);
/** @type {Record<String, boolean>} */
const hasKeys = {};
entries.forEach((entry) => {
const filename = path.join(langRoot, entry);
const json = require(filename);
const sorted = Object
.keys(json)
.sort()
.reduce((/** @type {Record<String, string>} */ result, key) => {
result[key] = json[key];
return result;
}, {});
hasKeys[entry] = Object.keys(sorted).length !== 0;
fs.writeFileSync(filename, JSON.stringify(sorted, null, 2));
});
if (lang === 'en') {
const filtered = entries
.filter((entry) => !SKIP_NS.includes(entry))
.filter((entry) => hasKeys[entry]);
fs.writeFileSync(
path.join(langRoot, 'index.json'),
JSON.stringify(filtered, null, 2)
);
}
}
function checkLanguages () {
const languages = fs
.readdirSync(i18nRoot)
.filter((entry) =>
!['.', '..'].includes(entry) &&
fs.lstatSync(path.join(i18nRoot, entry)).isDirectory()
)
.sort();
languages.forEach(sortLanguage);
fs.writeFileSync(path.join(i18nRoot, 'index.json'), JSON.stringify(languages, null, 2));
}
checkLanguages();
+157
View File
@@ -0,0 +1,157 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import fs from 'node:fs';
import path from 'node:path';
import { formatNumber, stringCamelCase } from '@pezkuwi/util';
const MAX_SIZE = 48 * 1024;
// FIXME The sorting here and the sorting from linting seems like a mismatch...
const HEADER = '// Copyright 2017-2025 @pezkuwi/apps authors & contributors\n// SPDX-License-Identifier: Apache-2.0\n\n// Do not edit. Auto-generated via node scripts/imgConvert.mjs\n\n';
/** @type {Record<string, string>} */
const MIME = {
gif: 'image/gif',
jpeg: 'image/jpeg',
png: 'image/png',
svg: 'image/svg+xml'
};
/**
* @param {string} k
* @param {string} contents
* @returns {string}
*/
function makeContents (k, contents) {
return `${HEADER}export const ${k} = '${contents}';\n`;
}
/** @type {Record<string, string>} */
const all = {};
/** @type {Record<string, number>} */
const oversized = {};
for (const dir of ['extensions', 'external', 'chains', 'nodes']) {
const sub = path.join('packages/apps-config/src/ui/logos', dir);
const generated = path.join(sub, 'generated');
/** @type {Record<string, string>} */
const result = {};
if (fs.existsSync(generated)) {
fs.rmSync(generated, { force: true, recursive: true });
}
fs.mkdirSync(generated);
fs
.readdirSync(sub)
.forEach((file) => {
const full = path.join(sub, file);
if (fs.lstatSync(full).isFile() && !(file.endsWith('.ts') || file.startsWith('.'))) {
const parts = file.split('.');
const ext = parts[parts.length - 1];
const nameParts = parts.slice(0, parts.length - 1);
const mime = MIME[ext];
if (!mime) {
throw new Error(`Unable to determine mime for ${file}`);
} else {
const buf = fs.readFileSync(full);
const data = `data:${mime};base64,${buf.toString('base64')}`;
const k = `${stringCamelCase(`${dir}_${nameParts.join('_')}`)}${ext.toUpperCase()}`;
const fileprefix = `generated/${nameParts.join('.')}${ext.toUpperCase()}`;
fs.writeFileSync(path.join(sub, `${fileprefix}.ts`), makeContents(k, data));
result[k] = fileprefix;
all[k] = data;
if (buf.length > MAX_SIZE) {
oversized[k] = buf.length;
}
}
}
});
if (Object.keys(result).length) {
let srcs = '';
for (const dir of ['endpoints', 'extensions', 'links']) {
const srcroot = path.join('packages/apps-config/src', dir);
fs
.readdirSync(srcroot)
.forEach((file) => {
const full = path.join(srcroot, file);
if (fs.lstatSync(full).isFile() && file.endsWith('.ts')) {
srcs += fs.readFileSync(full).toString();
}
});
}
const notfound = Object
.keys(result)
.filter((k) => !srcs.includes(k));
if (notfound.length) {
console.log('\n', notfound.length.toString().padStart(3), 'not referenced in', dir, '::\n\n\t', notfound.join(', '), '\n');
}
fs.writeFileSync(path.join(sub, 'index.ts'), `${HEADER}${
Object
.keys(result)
.sort((a, b) => result[a].localeCompare(result[b]))
.map((k) => `export { ${k} } from './${result[k]}.js';`)
.join('\n')
}\n`);
}
}
const allKeys = Object.keys(all);
/** @type {Record<string, string[]>} */
const dupes = {};
allKeys.forEach((a) => {
const d = allKeys.filter((b) =>
a !== b &&
all[a] === all[b]
);
if (d.length) {
dupes[a] = d;
}
});
if (Object.keys(dupes).length) {
const errMsg = `${Object.keys(dupes).length.toString().padStart(3)} dupes found`;
console.log('\n', errMsg, '::\n');
for (const [k, d] of Object.entries(dupes)) {
console.log('\t', k.padStart(30), ' >> ', d.join(', '));
}
console.log();
throw new Error(`FATAL: ${errMsg}. Please remove the duplicates.`);
}
const numOversized = Object.keys(oversized).length;
if (numOversized) {
const errMsg = `${numOversized.toString().padStart(3)} files with byte sizes > 48K`;
console.log('\n', errMsg, '::\n');
for (const [k, v] of Object.entries(oversized)) {
console.log('\t', k.padStart(30), formatNumber(v).padStart(15), `(+${formatNumber(v - MAX_SIZE)} bytes)`);
}
console.log();
throw new Error(`FATAL: ${errMsg}. Please resize the images.`);
}
+215
View File
@@ -0,0 +1,215 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck Currently we have a bit too many of these
import CrustPinner from '@crustio/crust-pin';
import PinataSDK from '@pinata/sdk';
// @ts-expect-error No definition file
import cloudflare from 'dnslink-cloudflare';
import fs from 'node:fs';
// @ts-expect-error No definition file
import { execSync } from '@pezkuwi/dev/scripts/util.mjs';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Using ignore since the file won't be there in dev
import { createWsEndpoints } from '../packages/apps-config/build/endpoints/index.js';
console.log('$ scripts/ipfsUpload.mjs', process.argv.slice(2).join(' '));
// https://gateway.pinata.cloud/ipfs/
const GATEWAY = 'https://ipfs.io/ipfs/';
const DOMAIN = 'dotapps.io';
const DST = 'packages/apps/build';
const SRC = 'packages/apps/public';
const WOPTS = { encoding: 'utf8', flag: 'w' };
const PINMETA = { name: DOMAIN };
const repo = `https://${process.env.GH_PAT}@github.com/${process.env.GITHUB_REPOSITORY}.git`;
async function wait (delay = 2500) {
return new Promise((resolve) => {
setTimeout(() => resolve(undefined), delay);
});
}
function createPinata () {
try {
// For 1.2.1
return PinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET_KEY);
// For 2.1.0+
// return new PinataSDK({
// pinataApiKey: process.env.PINATA_API_KEY,
// pinataSecretApiKey: process.env.PINATA_SECRET_KEY
// });
} catch {
console.error('Unable to create Pinata');
}
return null;
}
function createCrust () {
try {
// eslint-disable-next-line new-cap
return new CrustPinner.default(process.env.CRUST_SEEDS);
} catch {
console.error('Unable to create Crust');
}
return null;
}
const pinata = createPinata();
const crust = createCrust();
function writeFiles (name, content) {
[DST, SRC].forEach((root) =>
fs.writeFileSync(`${root}/ipfs/${name}`, content, WOPTS)
);
}
function updateGh (hash) {
execSync('git add --all .');
execSync(`git commit --no-status --quiet -m "[CI Skip] publish/ipfs ${hash}
skip-checks: true"`);
execSync(`git push ${repo} HEAD:${process.env.GITHUB_REF}`, true);
}
async function pin () {
if (!pinata) {
console.error('Pinata not available, cannot pin');
return;
}
// 1. Pin on pinata
const result = await pinata.pinFromFS(DST, { pinataMetadata: PINMETA });
const url = `${GATEWAY}${result.IpfsHash}/`;
const html = `<!DOCTYPE html>
<html>
<head>
<title>Redirecting to ipfs gateway</title>
<meta http-equiv="refresh" content="0; url=${url}" />
<style>
body { font-family: 'Nunito Sans',sans-serif; line-height: 1.5rem; padding: 2rem; text-align: center }
p { margin: 0 }
</style>
</head>
<body>
<p>Redirecting to</p>
<p><a href="${url}">${url}</a></p>
</body>
</html>`;
writeFiles('index.html', html);
writeFiles('pin.json', JSON.stringify(result));
updateGh(result.IpfsHash);
// 2. Decentralized pin on Crust
if (crust) {
await crust.pin(result.IpfsHash).catch(console.error);
}
console.log(`Pinned ${result.IpfsHash}`);
await wait();
return result.IpfsHash;
}
async function unpin (exclude) {
if (!pinata) {
console.error('Pinata not available, cannot unpin');
return;
}
const result = await pinata.pinList({ metadata: PINMETA, status: 'pinned' });
await wait();
console.log('Available Pinata pins', result.rows);
if (result.count > 1) {
const hashes = result.rows
.map((r) => r.ipfs_pin_hash)
.filter((hash) => hash !== exclude);
for (let i = 0, count = hashes.length; i < count; i++) {
const hash = hashes[i];
try {
await pinata.unpin(hash);
console.log(`Unpinned ${hash}`);
} catch (error) {
console.error(`Failed unpinning ${hash}`, error);
}
await wait();
}
}
}
async function dnslink (hash) {
const records = createWsEndpoints(() => '')
.map((e) => e.dnslink)
.reduce((all, dnslink) => {
if (dnslink && !all.includes(dnslink)) {
all.push(dnslink);
}
return all;
}, [null])
.map((sub) =>
['_dnslink', sub, DOMAIN]
.filter((entry) => !!entry)
.join('.')
);
for (let i = 0, count = records.length; i < count; i++) {
const record = records[i];
try {
await cloudflare(
{ token: process.env.CF_API_TOKEN },
{ link: `/ipfs/${hash}`, record, zone: DOMAIN }
);
console.log(`Updated dnslink ${record}`);
} catch (error) {
console.error(`Failed updating dnslink ${record}`, error);
}
await wait();
}
console.log(`Dnslink ${hash} for ${records.join(', ')}`);
}
async function main () {
const pkgJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
// only run on non-beta versions
if (!pkgJson.version.includes('-')) {
console.log('Pinning');
const hash = await pin();
await dnslink(hash);
await unpin(hash);
console.log('Completed');
} else {
console.log('Skipping');
}
}
main()
.catch(console.error)
.finally(() => process.exit(0));
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
const fs = require('node:fs');
const path = require('node:path');
const HEADER = `// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Automatically generated, do not edit
/* eslint-disable simple-import-sort/imports */`;
const PATH = 'packages/react-components/src/IdentityIcon/RoboHash';
/**
* @param {number} index
* @returns {string}
*/
function getCounter (index) {
return `000${index}`.slice(-3);
}
/**
* @param {string} dir
* @returns {string[]}
*/
function getFiles (dir) {
const genpath = path.join(dir, 'generated');
if (!fs.existsSync(genpath)) {
fs.mkdirSync(genpath, { recursive: true });
}
const all = fs
.readdirSync(dir)
.filter((entry) => {
if (entry.endsWith('.ts')) {
fs.rmSync(path.join(dir, entry), { force: true });
return false;
}
return !entry.startsWith('.') && entry !== 'generated';
})
.map((entry) => {
if (entry.includes('#')) {
const newName = entry.replace(/#/g, '-');
fs.renameSync(path.join(dir, entry), path.join(dir, newName));
return newName;
}
return entry;
})
.sort((a, b) =>
(a.includes('-') && b.includes('-'))
? a.split('-')[1].localeCompare(b.split('-')[1])
: 0
);
for (const f of all) {
if (f.endsWith('.png')) {
fs.writeFileSync(path.join(dir, `generated/${f}`).replace('.png', '.ts'), `${HEADER}\n\nexport default 'data:image/png;base64,${fs.readFileSync(path.join(dir, f)).toString('base64')}';\n`);
}
}
return all;
}
function extractBg () {
const root = path.join(__dirname, '..', PATH, 'backgrounds');
/** @type {string[]} */
const files = [];
getFiles(root).forEach((sub) => {
getFiles(path.join(root, sub)).forEach((entry) => files.push(`./${sub}/generated/${entry}`));
});
fs.writeFileSync(path.join(root, 'index.ts'), `${HEADER}\n\n${files.map((file, index) => `import b${getCounter(index)} from '${file.replace('.png', '')}';`).join('\n')}\n\nexport default [${files.map((_, index) => `b${getCounter(index)}`).join(', ')}];\n`);
}
function extractSets () {
const root = path.join(__dirname, '..', PATH, 'sets');
const sets = getFiles(root).map((sub) =>
getFiles(path.join(root, sub)).map((dir) =>
getFiles(path.join(root, sub, dir)).map((entry) => `./${sub}/${dir}/generated/${entry}`)
)
);
/** @type {string[]} */
const imports = [];
let list = '[';
sets.forEach((areas, sindex) => {
list = `${list}${sindex ? ',' : ''}\n [`;
areas.forEach((files, aindex) => {
const indexes = files.map((file, findex) => {
const index = `s${getCounter(sindex)}${getCounter(aindex)}${getCounter(findex)}`;
imports.push(`import ${index} from '${file.replace('.png', '')}';`);
return index;
});
list = `${list}${aindex ? ',' : ''}\n [${indexes.join(', ')}]`;
});
list = `${list}\n ]`;
});
list = `${list}\n];`;
fs.writeFileSync(path.join(root, 'index.ts'), `${HEADER}\n\n${imports.join('\n')}\n\nexport default ${list}\n`);
}
extractBg();
extractSets();