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: basic course part-2 #20

Merged
merged 14 commits into from
Apr 7, 2023
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@
"@solid-primitives/storage": "^1.3.7",
"@solidjs/meta": "^0.28.2",
"@solidjs/router": "^0.7.0",
"@srsholmes/solid-code-input": "^0.0.18",
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.9",
"@zag-js/dialog": "^0.2.13",
"@zag-js/hover-card": "^0.2.6",
"@zag-js/solid": "^0.2.10",
"@zag-js/toast": "^0.2.14",
"@zag-js/tooltip": "^0.2.13",
"monaco-editor": "0.33.0",
"axios": "^1.3.4",
"ethers": "^6.2.3",
"highlight.js": "^11.7.0",
"install": "^0.13.0",
"lodash": "^4.17.21",
"motion": "^10.15.5",
"npm": "^9.6.1",
"postcss-import": "^15.1.0",
Expand Down
File renamed without changes
Binary file added public/images/course/lesson2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/assets/css/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@apply placeholder:font-normal;
}

.input-sm{
@apply px-2 py-1 text-xs rounded-md
.input-sm {
@apply px-2 py-1 text-xs rounded-md;
}
}
5 changes: 2 additions & 3 deletions src/assets/css/mrakdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
content: '';
}

:where(blockquote):not(:where([class~="not-prose"] *)){
@apply border-light-border dark:border-dark-border
:where(blockquote):not(:where([class~='not-prose'] *)) {
@apply border-light-border dark:border-dark-border;
}

}
17 changes: 17 additions & 0 deletions src/components/CKBCore/CKBConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { predefined } from '@ckb-lumos/config-manager';

export type CKBConfig = {
LUMOS_CONFIG: typeof predefined.AGGRON4;
CKB_INDEXER_URL: string;
CKB_RPC_URL: string;
SCANNER_URL: string;
NERVOS_FUNCTIONS_URL: string;
};

export const ckbConfig: CKBConfig = {
LUMOS_CONFIG: predefined.AGGRON4,
CKB_INDEXER_URL: 'https://testnet.ckb.dev/indexer',
CKB_RPC_URL: 'https://testnet.ckb.dev',
SCANNER_URL: 'https://pudge.explorer.nervos.org',
NERVOS_FUNCTIONS_URL: 'https://nervos-functions.vercel.app',
};
80 changes: 80 additions & 0 deletions src/components/CKBCore/CKBProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Address, Cell, Script } from '@ckb-lumos/base';
import * as helpers from '@ckb-lumos/helpers';
import { Indexer } from '@ckb-lumos/ckb-indexer';
import { RPC } from '@ckb-lumos/rpc';
import { ckbConfig, CKBConfig } from '~/components/CKBCore/CKBConfig';
import { NervosFunctions } from '~/components/CKBCore/NervosFunctions';
import { BI } from '@ckb-lumos/bi';
import { BrowserProvider, ethers } from 'ethers';

export class CKBProvider {
readonly ethAddress: string;

ckbAddress: Address;

config: CKBConfig;

indexer: Indexer;
ckbRpc: RPC;
private readonly nervosClient: NervosFunctions;
ethProvider: BrowserProvider;

constructor(ethAddress: string) {
this.ethAddress = ethAddress;
this.config = ckbConfig;
this.indexer = new Indexer(this.config.CKB_INDEXER_URL, this.config.CKB_RPC_URL);
this.ckbRpc = new RPC(this.config.CKB_RPC_URL);
this.ckbAddress = this.generateL1Address(this.ethAddress);
this.nervosClient = new NervosFunctions(this.config.NERVOS_FUNCTIONS_URL);
const e: any = window.ethereum;
this.ethProvider = new ethers.BrowserProvider(e);
void this.getTestToken();
}

generateL1Address(account: string) {
const omniLock: Script = {
codeHash: this.config.LUMOS_CONFIG.SCRIPTS.OMNILOCK.CODE_HASH,
hashType: this.config.LUMOS_CONFIG.SCRIPTS.OMNILOCK.HASH_TYPE,
args: `0x01${account.substring(2)}00`,
};
return helpers.encodeToAddress(omniLock, {
config: this.config.LUMOS_CONFIG,
});
}

async getTestToken() {
const liveCells = await this.getLiveCells();
if (liveCells.length == 0) {
await this.nervosClient.faucet(this.ckbAddress);
}
}

async getLiveCells() {
const cells: Cell[] = [];
const collector = this.indexer.collector({
lock: helpers.parseAddress(this.ckbAddress, { config: this.config.LUMOS_CONFIG }),
type: 'empty',
outputDataLenRange: ['0x0', '0x1'],
});
for await (const cell of collector.collect()) {
cells.push(cell);
}
return cells;
}

async getNewBlocks(size?: number) {
size = size || 9;
const numbers: string[] = [];
const blockNumber = await this.ckbRpc.getTipBlockNumber();
numbers.push(blockNumber);
for (let i = 1; i < size; i++) {
const next = BI.from(blockNumber).sub(BI.from(i)).toHexString();
numbers.push(next);
}
return Promise.all(
numbers.map((number) => {
return this.ckbRpc.getBlockByNumber(number);
}),
);
}
}
22 changes: 22 additions & 0 deletions src/components/CKBCore/NervosFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import axios, { AxiosInstance } from 'axios';

export class NervosFunctions {
private readonly axios: AxiosInstance;

constructor(url: string) {
this.axios = axios.create({
baseURL: url,
timeout: 30000,
});
}

async faucet(address: string) {
const { data } = await this.axios.request({
url: '/api/faucet',
params: {
target_ckt_address: address,
},
});
return data;
}
}
18 changes: 18 additions & 0 deletions src/components/CKBCore/WalletContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Accessor, createContext, useContext } from 'solid-js';
import { CKBProvider } from '~/components/CKBCore/CKBProvider';

export interface WalletContext {
connected: Accessor<boolean>;
provider: Accessor<CKBProvider | undefined>;
connect: () => Promise<void>;
}

export const walletContext = createContext<WalletContext>({
connected: () => false,
provider: () => undefined,
connect: () => Promise.reject(void 0),
});

export const useWalletContext = () => {
return useContext(walletContext);
};
70 changes: 70 additions & 0 deletions src/components/CKBCore/WalletProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createEffect, createMemo, createResource, createSignal, ParentComponent } from 'solid-js';
import { WalletContext, walletContext } from '~/components/CKBCore/WalletContext';
import detectEthereum from '@metamask/detect-provider';
import { CKBProvider } from '~/components/CKBCore/CKBProvider';

interface ethereum {
on: (event: string, listen: (data?: never) => void) => void;
once: (event: string, listen: (data?: never) => void) => void;
request: <T>(params: { method: string }) => Promise<T>;
chainId: string;
}

export const WalletProvider: ParentComponent = (props) => {
const [provider, setProvider] = createSignal<CKBProvider>();
const [ethereum] = createResource<ethereum | undefined, unknown>(async () => {
const res = await detectEthereum();
return res as ethereum | undefined;
});

const [ethAccounts] = createResource<string[], ethereum | undefined>(
() => ethereum(),
async (ethereum) => {
return ethereum?.request?.({ method: 'eth_accounts' }) ?? [];
},
);

createEffect(() => {
ethereum()?.on('accountsChanged', (accounts: string[] | undefined) => {
if (accounts && accounts.length > 0) {
setProvider(new CKBProvider(accounts[0]));
} else {
setProvider(undefined);
}
});
});

createEffect(() => {
const accounts = ethAccounts() ?? [];
if (accounts.length > 0) {
setProvider(new CKBProvider(accounts[0]));
}
});

const connect = async () => {
if (provider() === undefined) {
if (ethereum()) {
const accounts = await ethereum()?.request<string[]>({ method: 'eth_requestAccounts' });
if (accounts && accounts.length > 0) {
setProvider(new CKBProvider(accounts[0]));
} else {
setProvider(undefined);
}
} else {
throw new MetaMaskInstallError();
}
}
};

const connected = createMemo(() => provider() != undefined);

const context: WalletContext = {
connected: connected,
provider: provider,
connect: connect,
};

return <walletContext.Provider value={context}>{props.children}</walletContext.Provider>;
};

export class MetaMaskInstallError extends Error {}
111 changes: 111 additions & 0 deletions src/components/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
Component,
createEffect,
createSignal,
mergeProps,
onCleanup,
onMount,
splitProps,
useContext,
} from 'solid-js';
import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import { AppContext } from '~/AppContext';

window.MonacoEnvironment = {
getWorker: function (_moduleId: unknown, label: string) {
switch (label) {
case 'css':
return new cssWorker();
case 'json':
return new jsonWorker();
case 'typescript':
case 'javascript':
return new tsWorker();
default:
return new editorWorker();
}
},
};

export type CodeEditorOption = {
value: string;
onChange: (value: string) => void;
class?: string;
} & monaco.editor.IStandaloneEditorConstructionOptions;

const lightTheme: monaco.editor.IStandaloneThemeData = {
base: 'vs',
inherit: true,
rules: [],
colors: {
'scrollbar.shadow': '#00000000',
'editor.background': '#00000000',
'editor.lineHighlightBackground': '#d5dae1',
},
};

const darkTheme: monaco.editor.IStandaloneThemeData = {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'scrollbar.shadow': '#00000000',
'editor.background': '#00000000',
},
};

monaco.editor.defineTheme('light', lightTheme);
monaco.editor.defineTheme('dark', darkTheme);

export const CodeEditor: Component<CodeEditorOption> = (props) => {
let ref: HTMLElement | any;

const [editor, setEditor] = createSignal<monaco.editor.IStandaloneCodeEditor>();
const appContext = useContext(AppContext);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
props = mergeProps(
{
automaticLayout: true,
lineNumbers: 'off',
minimap: {
enabled: false,
},
},
props,
);
const [local, others] = splitProps(props, ['class']);

onMount(() => {
const _editor = monaco.editor.create(ref as HTMLElement, others);
_editor.onDidChangeModelContent(() => {
props.onChange(_editor.getValue());
});
setEditor(_editor);
});

createEffect(() => {
editor()?.updateOptions({
theme: appContext.isDark ? 'dark' : 'light',
});
});

onCleanup(() => {
editor()?.dispose();
});

return (
<div
class="py-4 rounded-lg bg-light-tertiary/10 dark:bg-dark-background_dark"
classList={{ [`${local?.class ?? ''}`]: local.class != undefined }}
>
<div ref={ref} class="h-full" />
</div>
);
};

export default CodeEditor;
7 changes: 7 additions & 0 deletions src/components/CodeEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { unstable_clientOnly } from 'solid-start';
import { CodeEditorOption } from '~/components/CodeEditor/CodeEditor';

const CodeEditor = unstable_clientOnly(() => import('~/components/CodeEditor/CodeEditor'));
export default function (props: CodeEditorOption) {
return <CodeEditor {...props} />;
}
Loading