mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-13 15:11:05 +00:00
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:
@@ -0,0 +1 @@
|
||||
# @pezkuwi/app-js
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"bugs": "https://github.com/pezkuwichain/pezkuwi-apps/issues",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"homepage": "https://github.com/pezkuwichain/pezkuwi-apps/tree/master/packages/page-js#readme",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@pezkuwi/app-js",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"directory": "packages/page-js",
|
||||
"type": "git",
|
||||
"url": "https://github.com/pezkuwichain/pezkuwi-apps.git"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "0.168.2-4-x",
|
||||
"dependencies": {
|
||||
"@pezkuwi/react-components": "^0.168.2-4-x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*",
|
||||
"react-is": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, Input, Popup } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isCustomExample: boolean;
|
||||
isRunning: boolean;
|
||||
removeSnippet: () => void;
|
||||
runJs: () => void;
|
||||
saveSnippet: (snippetName: string) => void;
|
||||
snippetName?: string;
|
||||
stopJs: () => void;
|
||||
}
|
||||
|
||||
function ActionButtons ({ className = '', isCustomExample, isRunning, removeSnippet, runJs, saveSnippet, stopJs }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [snippetName, setSnippetName] = useState('');
|
||||
|
||||
const _onChangeName = useCallback(
|
||||
(snippetName: string) => setSnippetName(snippetName),
|
||||
[]
|
||||
);
|
||||
|
||||
const _onPopupClose = useCallback(
|
||||
(): void => {
|
||||
setSnippetName('');
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const _saveSnippet = useCallback(
|
||||
(): void => {
|
||||
saveSnippet(snippetName);
|
||||
_onPopupClose();
|
||||
},
|
||||
[_onPopupClose, saveSnippet, snippetName]
|
||||
);
|
||||
|
||||
return (
|
||||
<Button.Group className={`${className} action-button`}>
|
||||
{isCustomExample
|
||||
? (
|
||||
<Button
|
||||
icon='trash'
|
||||
onClick={removeSnippet}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Popup
|
||||
className='popup-local'
|
||||
onCloseAction={_onPopupClose}
|
||||
value={
|
||||
<>
|
||||
<Input
|
||||
autoFocus
|
||||
maxLength={50}
|
||||
min={1}
|
||||
onChange={_onChangeName}
|
||||
onEnter={_saveSnippet}
|
||||
placeholder={t('Name your example')}
|
||||
value={snippetName}
|
||||
withLabel={false}
|
||||
/>
|
||||
<Button
|
||||
icon='save'
|
||||
isDisabled={!snippetName.length}
|
||||
label={t('Save snippet to local storage')}
|
||||
onClick={_saveSnippet}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon='save'
|
||||
isReadOnly={false}
|
||||
/>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
{isRunning
|
||||
? (
|
||||
<Button
|
||||
icon='times'
|
||||
onClick={stopJs}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
className='play-button'
|
||||
icon='play'
|
||||
onClick={runJs}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Button.Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ActionButtons);
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Log } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
import { isError, isNull, isUndefined } from '@pezkuwi/util';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
logs: Log[];
|
||||
}
|
||||
|
||||
const format = (value: unknown): string => {
|
||||
if (isError(value)) {
|
||||
return value.stack
|
||||
? value.stack
|
||||
: value.toString();
|
||||
} else if (isUndefined(value)) {
|
||||
return 'undefined';
|
||||
} else if (isNull(value)) {
|
||||
return 'null';
|
||||
} else if (Array.isArray(value)) {
|
||||
return `[${value.map(format).join(', ')}]`;
|
||||
} else if (value instanceof Map) {
|
||||
return `{${[...value.entries()]
|
||||
.map(([k, v]) => `${(k as string).toString()}: ${format(v)}`)
|
||||
.join(', ')}}`;
|
||||
}
|
||||
|
||||
// This _could_ fail as well, hence the catch below
|
||||
return (value as string).toString();
|
||||
};
|
||||
|
||||
const renderEntry = ({ args, type }: Log, index: number): React.ReactNode => {
|
||||
try {
|
||||
return (
|
||||
<div
|
||||
className={`js--Log ${type}`}
|
||||
key={index}
|
||||
>
|
||||
{args.map(format).join(' ')}
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
// e.g. this would hit here -
|
||||
// console.log(api.createType('ProxyType').__proto__)
|
||||
return (
|
||||
<div
|
||||
className={`js--Log ${type} error`}
|
||||
key={index}
|
||||
>
|
||||
Internal error: {(error as Error).stack || (error as Error).message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function Output ({ children, className = '', logs }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<StyledArticle className={`${className} container`}>
|
||||
<div className='logs-wrapper'>
|
||||
<div className='logs-container'>
|
||||
<pre className='logs-content'>
|
||||
{logs.map(renderEntry)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</StyledArticle>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledArticle = styled.article`
|
||||
background-color: #4e4e4e;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
font: var(--font-mono);
|
||||
font-variant-ligatures: common-ligatures;
|
||||
line-height: 18px;
|
||||
padding: 50px 10px 10px;
|
||||
position: relative;
|
||||
width: 40%;
|
||||
|
||||
.logs-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.logs-content {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.js--Log {
|
||||
animation: fadein 0.2s;
|
||||
margin: 0 0 5px 0;
|
||||
word-break: break-all;
|
||||
|
||||
&.error {
|
||||
color: #f88;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Output);
|
||||
@@ -0,0 +1,463 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { KeyringInstance } from '@pezkuwi/keyring/types';
|
||||
import type { ApiProps } from '@pezkuwi/react-api/types';
|
||||
import type { AppProps as Props } from '@pezkuwi/react-components/types';
|
||||
import type { Log, LogType, Snippet } from './types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Button, Dropdown, Editor, styled, Tabs } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
import * as types from '@pezkuwi/types';
|
||||
import uiKeyring from '@pezkuwi/ui-keyring';
|
||||
import * as util from '@pezkuwi/util';
|
||||
import * as hashing from '@pezkuwi/util-crypto';
|
||||
|
||||
import { allSnippets, makeWrapper } from './snippets/index.js';
|
||||
import ActionButtons from './ActionButtons.js';
|
||||
import { CUSTOM_LABEL, STORE_EXAMPLES, STORE_SELECTED } from './constants.js';
|
||||
import Output from './Output.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Injected {
|
||||
api: ApiPromise;
|
||||
console: {
|
||||
error: (...args: unknown[]) => void;
|
||||
log: (...args: unknown[]) => void;
|
||||
};
|
||||
hashing: typeof hashing;
|
||||
keyring: KeyringInstance | null;
|
||||
setIsRunning: (isRunning: boolean) => void;
|
||||
types: typeof types;
|
||||
util: typeof util;
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
const ALLOWED_GLOBALS = ['atob', 'btoa'];
|
||||
const DEFAULT_NULL = { Atomics: null, Bluetooth: null, Clipboard: null, Document: null, Function: null, Location: null, ServiceWorker: null, SharedWorker: null, USB: null, global: null, window: null };
|
||||
|
||||
const snippets = JSON.parse(JSON.stringify(allSnippets)) as Snippet[];
|
||||
let hasSnippetWrappers = false;
|
||||
|
||||
function setupInjected ({ api, isDevelopment }: ApiProps, setIsRunning: (isRunning: boolean) => void, hookConsole: (type: LogType, args: unknown[]) => void): Injected {
|
||||
return {
|
||||
...Object
|
||||
.keys(window)
|
||||
.filter((key) => !key.includes('-') && !ALLOWED_GLOBALS.includes(key))
|
||||
.reduce((result: Record<string, null>, key): Record<string, null> => {
|
||||
result[key] = null;
|
||||
|
||||
return result;
|
||||
}, { ...DEFAULT_NULL }),
|
||||
api: api.clone(),
|
||||
console: {
|
||||
error: (...args: unknown[]) => hookConsole('error', args),
|
||||
log: (...args: unknown[]) => hookConsole('log', args)
|
||||
},
|
||||
hashing,
|
||||
keyring: isDevelopment
|
||||
? uiKeyring.keyring
|
||||
: null,
|
||||
setIsRunning,
|
||||
types,
|
||||
uiKeyring: isDevelopment
|
||||
? uiKeyring
|
||||
: null,
|
||||
util,
|
||||
window
|
||||
};
|
||||
}
|
||||
|
||||
interface IframeWithInjected extends HTMLIFrameElement {
|
||||
contentWindow: (Window & { injected: Injected });
|
||||
}
|
||||
|
||||
// FIXME This... ladies & gentlemen, is a mess that should be untangled
|
||||
function Playground ({ basePath, className = '' }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const apiProps = useApi();
|
||||
const injectedRef = useRef<Injected | null>(null);
|
||||
const iframeRef = useRef<IframeWithInjected|null>(null);
|
||||
const [code, setCode] = useState('');
|
||||
const [isCustomExample, setIsCustomExample] = useState(false);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isWarnOpen, toggleWarnOpen] = useToggle(true);
|
||||
const [customExamples, setCustomExamples] = useState<Snippet[]>([]);
|
||||
const [logs, setLogs] = useState<Log[]>([]);
|
||||
const [options, setOptions] = useState<Snippet[]>([]);
|
||||
const [selected, setSelected] = useState(snippets[0]);
|
||||
|
||||
const tabsRef = useRef([
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'playground',
|
||||
text: t('Console')
|
||||
}
|
||||
]);
|
||||
|
||||
// initialize all options
|
||||
useEffect((): void => {
|
||||
// add snippets if not already available (global)
|
||||
if (!hasSnippetWrappers) {
|
||||
snippets.forEach((snippet): void => {
|
||||
snippet.code = `${makeWrapper(apiProps.isDevelopment)}${snippet.code}`;
|
||||
});
|
||||
|
||||
hasSnippetWrappers = true;
|
||||
}
|
||||
|
||||
const localData = {
|
||||
examples: localStorage.getItem(STORE_EXAMPLES),
|
||||
selectedValue: localStorage.getItem(STORE_SELECTED)
|
||||
};
|
||||
const customExamples = localData.examples ? JSON.parse(localData.examples) as Snippet[] : [];
|
||||
const options: Snippet[] = [...customExamples, ...snippets];
|
||||
const selected = options.find((option): boolean => option.value === localData.selectedValue);
|
||||
|
||||
setCustomExamples(customExamples);
|
||||
setIsCustomExample((selected && selected.type === 'custom') || false);
|
||||
setOptions(options);
|
||||
setSelected(selected || snippets[0]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect((): void => {
|
||||
setCode(selected.code);
|
||||
}, [selected]);
|
||||
|
||||
const _clearConsole = useCallback(
|
||||
(): void => setLogs([]),
|
||||
[]
|
||||
);
|
||||
|
||||
const _hookConsole = useCallback(
|
||||
(type: LogType, args: unknown[]): void => {
|
||||
logs.push({ args, type });
|
||||
setLogs(logs.slice(0));
|
||||
},
|
||||
[logs]
|
||||
);
|
||||
|
||||
const _stopJs = useCallback(
|
||||
(): void => {
|
||||
if (injectedRef.current) {
|
||||
injectedRef.current.api.disconnect().catch(console.error);
|
||||
injectedRef.current = null;
|
||||
}
|
||||
|
||||
setIsRunning(false);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const _runJs = useCallback(
|
||||
(): void => {
|
||||
async function run () {
|
||||
setIsRunning(true);
|
||||
_clearConsole();
|
||||
|
||||
injectedRef.current = setupInjected(apiProps, setIsRunning, _hookConsole);
|
||||
|
||||
await injectedRef.current.api.isReady;
|
||||
|
||||
try {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
const iframeDoc = iframeRef.current.contentWindow.document;
|
||||
|
||||
iframeDoc.open();
|
||||
iframeDoc.write('<!DOCTYPE html><html><head></head><body></body></html>');
|
||||
iframeDoc.close();
|
||||
|
||||
// Expose injected to iframe window
|
||||
iframeRef.current.contentWindow.injected = injectedRef.current;
|
||||
|
||||
// Build destructured keys from injectedRef
|
||||
const injectedKeys = Object.keys(iframeRef.current.contentWindow.injected).sort().slice(1).join(', ');
|
||||
|
||||
// Build the code to run (scoped with destructured injected keys)
|
||||
const exec = `
|
||||
(async ({ ${injectedKeys} }) => {
|
||||
try {
|
||||
${code}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
if (typeof setIsRunning === 'function') {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}
|
||||
})(injected);
|
||||
`;
|
||||
|
||||
// Create script tag to run code
|
||||
const bridgeScript = iframeDoc.createElement('script');
|
||||
|
||||
// eslint-disable-next-line no-new-func, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-assignment
|
||||
bridgeScript.innerText = new Function('injected', exec).bind({}, iframeRef.current.contentWindow.injected)();
|
||||
|
||||
iframeRef.current.contentWindow.document.body.appendChild(bridgeScript);
|
||||
} else {
|
||||
throw new Error('No window found to run code');
|
||||
}
|
||||
} catch (error) {
|
||||
if (injectedRef.current) {
|
||||
injectedRef.current.console.error(error);
|
||||
}
|
||||
|
||||
setIsRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
}, [_clearConsole, _hookConsole, apiProps, code]);
|
||||
|
||||
const _selectExample = useCallback(
|
||||
(value: string): void => {
|
||||
_stopJs();
|
||||
|
||||
if (value.length) {
|
||||
const option = options.find((option): boolean => option.value === value);
|
||||
|
||||
if (option) {
|
||||
localStorage.setItem(STORE_SELECTED, value);
|
||||
|
||||
_clearConsole();
|
||||
setIsCustomExample(option.type === 'custom');
|
||||
setSelected(option);
|
||||
}
|
||||
}
|
||||
},
|
||||
[_clearConsole, _stopJs, options]
|
||||
);
|
||||
|
||||
const _removeSnippet = useCallback(
|
||||
(): void => {
|
||||
const filtered = customExamples.filter((value): boolean => value.value !== selected.value);
|
||||
const nextOptions = [...filtered, ...snippets];
|
||||
|
||||
setCustomExamples(filtered);
|
||||
setIsCustomExample(nextOptions[0].type === 'custom');
|
||||
setOptions(nextOptions);
|
||||
_selectExample(nextOptions[0].value);
|
||||
localStorage.setItem(STORE_EXAMPLES, JSON.stringify(filtered));
|
||||
},
|
||||
[_selectExample, customExamples, selected.value]
|
||||
);
|
||||
|
||||
const _saveSnippet = useCallback(
|
||||
(snippetName: string): void => {
|
||||
// The <Dropdown> component doesn't take boolean custom props and no
|
||||
// camelCase keys, that's why 'custom' is passed as a string here
|
||||
const snapshot: Snippet = {
|
||||
code,
|
||||
label: CUSTOM_LABEL,
|
||||
text: snippetName,
|
||||
type: 'custom',
|
||||
value: `custom-${Date.now()}`
|
||||
};
|
||||
const options = [snapshot, ...customExamples, ...snippets];
|
||||
|
||||
localStorage.setItem(STORE_EXAMPLES, JSON.stringify([snapshot, ...customExamples]));
|
||||
setCustomExamples([snapshot, ...customExamples]);
|
||||
setIsCustomExample(true);
|
||||
setOptions(options);
|
||||
setSelected(snapshot);
|
||||
},
|
||||
[code, customExamples]
|
||||
);
|
||||
|
||||
const snippetName = selected.type === 'custom' ? selected.text : undefined;
|
||||
|
||||
return (
|
||||
<StyledMain className={`${className} js--App`}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={tabsRef.current}
|
||||
/>
|
||||
<section className='js--Selection'>
|
||||
<Dropdown
|
||||
className='js--Dropdown'
|
||||
isFull
|
||||
label={t('Select example')}
|
||||
onChange={_selectExample}
|
||||
options={options}
|
||||
value={selected.value}
|
||||
/>
|
||||
</section>
|
||||
<section className='js--Content'>
|
||||
<article className='container js--Editor'>
|
||||
<ActionButtons
|
||||
isCustomExample={isCustomExample}
|
||||
isRunning={isRunning}
|
||||
removeSnippet={_removeSnippet}
|
||||
runJs={_runJs}
|
||||
saveSnippet={_saveSnippet}
|
||||
snippetName={snippetName}
|
||||
stopJs={_stopJs}
|
||||
/>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
sandbox='allow-scripts allow-same-origin'
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<Editor
|
||||
code={code}
|
||||
onEdit={setCode}
|
||||
/>
|
||||
</article>
|
||||
<Output
|
||||
className='js--Output'
|
||||
logs={logs}
|
||||
>
|
||||
<Button
|
||||
className='action-button'
|
||||
icon='eraser'
|
||||
onClick={_clearConsole}
|
||||
/>
|
||||
</Output>
|
||||
</section>
|
||||
{isWarnOpen && (
|
||||
<div className='warnOverlay'>
|
||||
<article className='warning centered'>
|
||||
<p>{t('This is a developer tool that allows you to execute selected snippets in a limited context.')}</p>
|
||||
<p>{t('Never execute JS snippets from untrusted sources.')}</p>
|
||||
<p>{t('Unless you are a developer with insight into what the specific script does to your environment (based on reading the code being executed) generally the advice would be to not use this environment.')}</p>
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='times'
|
||||
label={t('Close')}
|
||||
onClick={toggleWarnOpen}
|
||||
/>
|
||||
</Button.Group>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
</StyledMain>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledMain = styled.main`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
|
||||
article {
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.js--Selection {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.js--Content {
|
||||
align-content: stretch;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.js--Dropdown {
|
||||
position: relative;
|
||||
z-index: 200;
|
||||
|
||||
.dropdown .menu > .item {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.js--Editor,
|
||||
.js--Output {
|
||||
min-width: 200px;
|
||||
|
||||
.action-button {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
|
||||
.js--Editor {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
resize: horizontal;
|
||||
width: 60%;
|
||||
|
||||
textarea {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.codeflask {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.codeflask--has-line-numbers {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.codeflask--has-line-numbers .codeflask__flatten {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
min-width: calc(100% - 40px);
|
||||
padding-top: 50px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.codeflask__lines {
|
||||
background: #f2f2f2;
|
||||
line-height: 18px;
|
||||
padding-top: 50px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
content: '↔';
|
||||
cursor: col-resize;
|
||||
font-size: 20px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 22px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ui.popup.popup-local {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.warnOverlay {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -0.25rem;
|
||||
z-index: 202;
|
||||
|
||||
article p:first-child {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.ui--Button-Group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Playground);
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StrictLabelProps } from 'semantic-ui-react';
|
||||
|
||||
export const STORE_EXAMPLES = 'pezkuwi-app-js-examples';
|
||||
export const STORE_SELECTED = 'pezkuwi-app-js-selected';
|
||||
|
||||
export const CUSTOM_LABEL: StrictLabelProps = {
|
||||
children: 'Custom',
|
||||
color: 'orange',
|
||||
size: 'tiny'
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AppProps as Props } from '@pezkuwi/react-components/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Playground from './Playground.js';
|
||||
|
||||
function JsApp (props: Props): React.ReactElement<Props> {
|
||||
return <Playground {...props} />;
|
||||
}
|
||||
|
||||
export default React.memo(JsApp);
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Snippet } from '../types.js';
|
||||
|
||||
// We must fix this :(
|
||||
/* eslint-disable sort-keys */
|
||||
|
||||
export const constsStakingParameters: Snippet = {
|
||||
value: 'constsStakingParameters',
|
||||
text: 'Get staking parameters',
|
||||
label: { color: 'green', children: 'Consts', size: 'tiny' },
|
||||
code: `// Get SRML staking parameters as consts
|
||||
// 'parameter_types' were added to bizinikiwi with spec_version: 101.
|
||||
// This example will throw an error if used with versions before that.
|
||||
|
||||
const bondingDuration = api.consts.staking.bondingDuration;
|
||||
const sessionsPerEra = api.consts.staking.sessionsPerEra;
|
||||
|
||||
console.log('Staking bonding duration: ' + bondingDuration);
|
||||
console.log('Staking sessions per era: ' + sessionsPerEra);`
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Snippet } from '../types.js';
|
||||
|
||||
// We must fix this :(
|
||||
/* eslint-disable sort-keys */
|
||||
|
||||
export const extrinsicMakeTransfer: Snippet = {
|
||||
value: 'extrinsicMakeTransfer',
|
||||
text: 'Make transfer and listen to events',
|
||||
label: { color: 'grey', children: 'Extrinsics', size: 'tiny' },
|
||||
code: `// Make a transfer from Alice to Bob and listen to system events.
|
||||
// You need to be connected to a development chain for this example to work.
|
||||
const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||
const BOB = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
|
||||
|
||||
// Get a random number between 1 and 100000
|
||||
const randomAmount = Math.floor((Math.random() * 100000) + 1);
|
||||
|
||||
// Create a extrinsic, transferring randomAmount units to Bob.
|
||||
const transfer = api.tx.balances.transferAllowDeath(BOB, randomAmount);
|
||||
|
||||
// Sign and Send the transaction
|
||||
await transfer.signAndSend(ALICE, ({ events = [], status }) => {
|
||||
if (status.isInBlock) {
|
||||
console.log('Successful transfer of ' + randomAmount + ' with hash ' + status.asInBlock.toHex());
|
||||
} else {
|
||||
console.log('Status of transfer: ' + status.type);
|
||||
}
|
||||
|
||||
events.forEach(({ phase, event: { data, method, section } }) => {
|
||||
console.log(phase.toString() + ' : ' + section + '.' + method + ' ' + data.toString());
|
||||
});
|
||||
});`
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { constsStakingParameters } from './consts-examples.js';
|
||||
import { extrinsicMakeTransfer } from './extrinsics-examples.js';
|
||||
import { rpcNetworkAuthoring, rpcNewHead, rpcQueryState, rpcSystemInfo } from './rpc-examples.js';
|
||||
import { storageGetInfo, storageKeys, storageListenToBalanceChange, storageListenToMultipleBalancesChange, storageRetrieveInfoOnQueryKeys, storageSystemEvents } from './storage-examples.js';
|
||||
|
||||
export { makeWrapper } from './wrapping.js';
|
||||
|
||||
export const allSnippets = [
|
||||
rpcNetworkAuthoring,
|
||||
rpcNewHead,
|
||||
rpcQueryState,
|
||||
rpcSystemInfo,
|
||||
storageGetInfo,
|
||||
storageSystemEvents,
|
||||
storageListenToBalanceChange,
|
||||
storageListenToMultipleBalancesChange,
|
||||
storageRetrieveInfoOnQueryKeys,
|
||||
storageKeys,
|
||||
constsStakingParameters,
|
||||
extrinsicMakeTransfer
|
||||
] as const;
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Snippet } from '../types.js';
|
||||
|
||||
// We must fix this :(
|
||||
/* eslint-disable sort-keys */
|
||||
|
||||
export const rpcNetworkAuthoring: Snippet = {
|
||||
value: 'rpcNetworkAuthoring',
|
||||
text: 'Get authoring information',
|
||||
label: { color: 'pink', children: 'RPC', size: 'tiny' },
|
||||
code: `// Returns all pending extrinsics, potentially grouped by sender
|
||||
const unsub = await api.rpc.author.pendingExtrinsics((extrinsics) => {
|
||||
if(extrinsics.length === 0){
|
||||
console.log('No pending extrinsics');
|
||||
return;
|
||||
}
|
||||
console.log(extrinsics);
|
||||
});`
|
||||
};
|
||||
|
||||
export const rpcNewHead: Snippet = {
|
||||
value: 'rpcListenToHead',
|
||||
text: 'Listen to new Head',
|
||||
label: { color: 'pink', children: 'RPC', size: 'tiny' },
|
||||
code: `// subscribe to new headers, printing the full info for 5 Blocks
|
||||
let count = 0;
|
||||
const unsub = await api.rpc.chain.subscribeNewHeads((header) => {
|
||||
console.log(\`#\${header.number}:\`, header);
|
||||
|
||||
if (++count === 5) {
|
||||
console.log('5 headers retrieved, unsubscribing');
|
||||
unsub();
|
||||
}
|
||||
});`
|
||||
};
|
||||
|
||||
export const rpcQueryState: Snippet = {
|
||||
value: 'rpcQueryState',
|
||||
text: 'Get state metadata',
|
||||
label: { color: 'pink', children: 'RPC', size: 'tiny' },
|
||||
code: `// retrieve and log the complete metadata of your node
|
||||
const metadata = await api.rpc.state.getMetadata();
|
||||
|
||||
console.log('version: ' + metadata.version);
|
||||
console.log('formatted: ' + JSON.stringify(metadata.asLatest.toHuman(), null, 2));
|
||||
`
|
||||
};
|
||||
|
||||
export const rpcSystemInfo: Snippet = {
|
||||
value: 'rpcSystemInfo',
|
||||
text: 'Get system information',
|
||||
label: { color: 'pink', children: 'RPC', size: 'tiny' },
|
||||
code: `// Retrieve the chain & node information via rpc calls
|
||||
const [chain, nodeName, nodeVersion, properties] = await Promise.all([
|
||||
api.rpc.system.chain(),
|
||||
api.rpc.system.name(),
|
||||
api.rpc.system.version(),
|
||||
api.rpc.system.properties()
|
||||
]);
|
||||
console.log('You are connected to chain ' + chain)
|
||||
console.log('You are using: ' + nodeName + ' v' + nodeVersion);
|
||||
|
||||
if (properties.size > 0) {
|
||||
console.log('Node specific properties:');
|
||||
properties.forEach((value, key) => {
|
||||
console.log(key, value);
|
||||
});
|
||||
} else {
|
||||
console.log('No specific chain properties found.');
|
||||
}`
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StrictLabelProps } from 'semantic-ui-react';
|
||||
import type { Snippet } from '../types.js';
|
||||
|
||||
const label: StrictLabelProps = {
|
||||
children: 'Storage',
|
||||
color: 'blue',
|
||||
size: 'tiny'
|
||||
};
|
||||
|
||||
export const storageGetInfo: Snippet = {
|
||||
code: `// Get chain state information
|
||||
// Make our basic chain state / storage queries, all in one go
|
||||
|
||||
const [now, minimumValidatorCount, validators] = await Promise.all([
|
||||
api.query.timestamp.now(),
|
||||
api.query.staking.minimumValidatorCount(),
|
||||
api.query.session.validators()
|
||||
]);
|
||||
|
||||
console.log('The current date is: ' + now);
|
||||
console.log('The minimum validator count: ' + minimumValidatorCount);
|
||||
|
||||
if (validators && validators.length > 0) {
|
||||
// Retrieve the balances for all validators
|
||||
console.log('Validators');
|
||||
|
||||
const validatorBalances = await Promise.all(
|
||||
validators.map((authorityId) => api.query.system.account(authorityId))
|
||||
);
|
||||
|
||||
validators.forEach((authorityId, index) => {
|
||||
console.log('Validator: ' + authorityId.toString() )
|
||||
console.log('AccountData: ' + validatorBalances[index].toHuman() );
|
||||
});
|
||||
}
|
||||
`,
|
||||
label,
|
||||
text: 'Get chain state information',
|
||||
value: 'storageGetInfo'
|
||||
};
|
||||
|
||||
export const storageSystemEvents: Snippet = {
|
||||
code: `// Subscribe to system events via storage
|
||||
api.query.system.events((events) => {
|
||||
console.log('----- Received ' + events.length + ' event(s): -----');
|
||||
// loop through the Vec<EventRecord>
|
||||
events.forEach((record) => {
|
||||
// extract the phase, event and the event types
|
||||
const { event, phase } = record;
|
||||
const types = event.typeDef;
|
||||
// show what we are busy with
|
||||
console.log(event.section + ':' + event.method + '::' + 'phase=' + phase.toString());
|
||||
console.log(event.meta.docs.toString());
|
||||
// loop through each of the parameters, displaying the type and data
|
||||
event.data.forEach((data, index) => {
|
||||
console.log(types[index].type + ';' + data.toString());
|
||||
});
|
||||
});
|
||||
});`,
|
||||
label,
|
||||
text: 'Listen to system events',
|
||||
value: 'storageSystemEvents'
|
||||
};
|
||||
|
||||
export const storageListenToBalanceChange: Snippet = {
|
||||
code: `// You may leave this example running and make a transfer
|
||||
// of any value from or to Alice address in the 'Transfer' App
|
||||
const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||
|
||||
// Retrieve the initial data
|
||||
let [, { free: previous }] = await api.query.system.account(ALICE);
|
||||
|
||||
console.log('ALICE has a balance of ' + previous);
|
||||
|
||||
// Subscribe and listen to balance changes
|
||||
api.query.system.account(ALICE, ([, { free }]) => {
|
||||
// Calculate the delta
|
||||
const change = free.sub(previous);
|
||||
// Only display positive value changes (Since we are pulling 'previous' above already,
|
||||
// the initial balance change will also be zero)
|
||||
if (!change.isZero()) {
|
||||
previous = free;
|
||||
console.log('New transaction of: '+ change);
|
||||
}
|
||||
});`,
|
||||
label,
|
||||
text: 'Listen to balance changes',
|
||||
value: 'storageListenToBalanceChange'
|
||||
};
|
||||
|
||||
export const storageListenToMultipleBalancesChange: Snippet = {
|
||||
code: `// You may leave this example running and make a transfer
|
||||
// of any value from or to Alice/Bob address in the 'Transfer' App
|
||||
const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||
const BOB = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
|
||||
|
||||
console.log('Tracking balances for:', [ALICE, BOB])
|
||||
|
||||
// Subscribe and listen to several balance changes
|
||||
api.query.system.account.multi([ALICE, BOB], (info) => {
|
||||
console.log('Change detected, new balances: ', info)
|
||||
});`,
|
||||
label,
|
||||
text: 'Listen to multiple balances changes',
|
||||
value: 'storageListenToMultipleBalancesChange'
|
||||
};
|
||||
|
||||
export const storageRetrieveInfoOnQueryKeys: Snippet = {
|
||||
code: `// This example set shows how to make queries at a point
|
||||
const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||
|
||||
// retrieve the balance, once-off at the latest block
|
||||
const { data: { free } } = await api.query.system.account(ALICE);
|
||||
|
||||
console.log('Alice has a current balance of', free.toHuman());
|
||||
|
||||
// retrieve balance updates with an optional value callback
|
||||
const balanceUnsub = await api.query.system.account(ALICE, ({ data: { free } }) => {
|
||||
console.log('Alice has an updated balance of', free.toHuman());
|
||||
});
|
||||
|
||||
// retrieve the balance at a block hash in the past
|
||||
const header = await api.rpc.chain.getHeader();
|
||||
const prevHash = await api.rpc.chain.getBlockHash(header.number.unwrap().subn(42));
|
||||
const { data: { free: prev } } = await api.query.system.account.at(prevHash, ALICE);
|
||||
|
||||
console.log('Alice had a balance of', prev.toHuman(), '(42 blocks ago)');
|
||||
|
||||
// useful in some situations - the value hash and storage entry size
|
||||
const currHash = await api.query.system.account.hash(ALICE);
|
||||
const currSize = await api.query.system.account.size(ALICE);
|
||||
|
||||
console.log('Alice account entry has a value hash of', currHash, 'with a size of', currSize);`,
|
||||
label,
|
||||
text: 'Retrieve historic query data',
|
||||
value: 'storageRetrieveInfoOnQueryKeys'
|
||||
};
|
||||
|
||||
export const storageKeys: Snippet = {
|
||||
code: `// this example shows how to retrieve the hex representation of a storage key
|
||||
|
||||
const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||
|
||||
// show the key for an entry without arguments
|
||||
console.log(api.query.timestamp.now.key());
|
||||
|
||||
// show the key for a map entry (single argument)
|
||||
console.log(api.query.system.account.key(ALICE));
|
||||
|
||||
// show the key prefix for a map
|
||||
console.log(api.query.system.account.keyPrefix());
|
||||
|
||||
// show the key for a double map
|
||||
console.log(api.query.staking.erasStakers.key(0, ALICE));
|
||||
|
||||
// show the key prefix for a doublemap
|
||||
console.log(api.query.staking.erasStakers.keyPrefix());
|
||||
`,
|
||||
label,
|
||||
text: 'Get underlying storage key hex values',
|
||||
value: 'storageKeys'
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export default `// transfer
|
||||
const sender = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||
const recipient = '5F2PCyGDWGDJyLRV15NrBsEa9Y61BS1dfAwzbfk7yR6DBm7P';
|
||||
|
||||
const nonce = await api.query.system.accountNonce(ALICE),
|
||||
|
||||
console.log('Current nonce', nonce);
|
||||
|
||||
const unsub = await api.tx.balances
|
||||
.transferAllowDeath(recipient,12345)
|
||||
.signAndSend(sender, ({ events = [], status }) => {
|
||||
console.log('Transaction status:', status.type);
|
||||
|
||||
if (status.isInBlock) {
|
||||
console.log('Completed at block hash', status.asInBlock.toHex());
|
||||
console.log('Events:');
|
||||
|
||||
events.forEach(({ phase, event: { data, method, section } }) => {
|
||||
console.log('\t', phase.toString(), \`: \${section}.\${method}\`, data.toString());
|
||||
});
|
||||
|
||||
unsub();
|
||||
}
|
||||
});`;
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export function makeWrapper (isDevelopment: boolean): string {
|
||||
const args = `api, hashing, ${isDevelopment ? 'keyring, ' : ''}types, util`;
|
||||
|
||||
return `// All code is wrapped within an async closure,
|
||||
// allowing access to ${args}.
|
||||
// (async ({ ${args} }) => {
|
||||
// ... any user code is executed here ...
|
||||
// })();
|
||||
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useTranslation as useTranslationBase } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
|
||||
return useTranslationBase('app-js');
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-js authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DropdownItemProps, StrictLabelProps } from 'semantic-ui-react';
|
||||
|
||||
export type LogType = 'error' | 'log';
|
||||
|
||||
export interface Log {
|
||||
args: unknown[];
|
||||
type: LogType;
|
||||
}
|
||||
|
||||
export interface Snippet extends DropdownItemProps {
|
||||
text: string;
|
||||
value: string;
|
||||
code: string;
|
||||
label?: StrictLabelProps;
|
||||
type?: 'custom' | 'shared';
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "..",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../react-components/tsconfig.build.json" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user