From 0095cf6724a4c5c4db0e578f7e6d760ea0fb1ab5 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sat, 11 Jan 2025 05:26:22 +0800 Subject: [PATCH] feat(playground): nostr paste bin --- packages/playground/package.json | 3 +- packages/playground/src/app/examples.ts | 70 -------- packages/playground/src/app/page.tsx | 218 +++++++++++++++++++++--- pnpm-lock.yaml | 17 +- 4 files changed, 206 insertions(+), 102 deletions(-) diff --git a/packages/playground/package.json b/packages/playground/package.json index 5ec7ba00..3763a916 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -10,11 +10,12 @@ }, "dependencies": { "@ckb-ccc/ccc": "workspace:*", - "@ckb-ccc/spore": "workspace:*", "@ckb-ccc/connector-react": "workspace:*", "@monaco-editor/react": "^4.6.0", "axios": "^1.7.7", + "bech32": "^2.0.0", "html2canvas": "^1.4.1", + "isomorphic-ws": "^5.0.0", "lucide-react": "^0.438.0", "monaco-editor": "^0.51.0", "next": "14.2.8", diff --git a/packages/playground/src/app/examples.ts b/packages/playground/src/app/examples.ts index 910b5cf0..8d9ba0c1 100644 --- a/packages/playground/src/app/examples.ts +++ b/packages/playground/src/app/examples.ts @@ -1,73 +1,3 @@ -export const DEFAULT_UDT_TRANSFER = `import { ccc } from "@ckb-ccc/ccc"; -import { render, signer } from "@ckb-ccc/playground"; - -console.log("Welcome to CCC Playground!"); - -// Prepare the UDT type script -const type = await ccc.Script.fromKnownScript( - signer.client, - ccc.KnownScript.XUdt, - "0xf8f94a13dfe1b87c10312fb9678ab5276eefbe1e0b2c62b4841b1f393494eff2", -); - -// The receiver is the signer itself on mainnet -const receiver = signer.client.addressPrefix === "ckb" ? - await signer.getRecommendedAddress() : - "ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqflz4emgssc6nqj4yv3nfv2sca7g9dzhscgmg28x"; -console.log(receiver); - -// The sender script for change -const { script: change } = await signer.getRecommendedAddressObj(); -// Parse the receiver script from an address -const { script: lock } = await ccc.Address.fromString( - receiver, - signer.client, -); - -// Describe what we want -const tx = ccc.Transaction.from({ - outputs: [ - { capacity: ccc.fixedPointFrom(242), lock, type }, - ], - outputsData: [ccc.numLeToBytes(ccc.fixedPointFrom(1), 16)], -}); -// Add cell deps for the xUDT script -await tx.addCellDepsOfKnownScripts( - signer.client, - ccc.KnownScript.XUdt, -); -await render(tx); - -// Complete missing parts: Fill UDT inputs -await tx.completeInputsByUdt(signer, type); -await render(tx); - -// Calculate excess UDT in inputs -const balanceDiff = - (await tx.getInputsUdtBalance(signer.client, type)) - - tx.getOutputsUdtBalance(type); -console.log(balanceDiff); -if (balanceDiff > ccc.Zero) { - // Add UDT change - tx.addOutput( - { - lock: change, - type, - }, - ccc.numLeToBytes(balanceDiff, 16), - ); -} -await render(tx); - -// Complete missing parts: Fill inputs -await tx.completeInputsByCapacity(signer); -await render(tx); - -// Complete missing parts: Pay fee -await tx.completeFeeBy(signer, 1000); -await render(tx); -`; - export const DEFAULT_TRANSFER = `import { ccc } from "@ckb-ccc/ccc"; import { render, signer } from "@ckb-ccc/playground"; diff --git a/packages/playground/src/app/page.tsx b/packages/playground/src/app/page.tsx index bee8141e..67437126 100644 --- a/packages/playground/src/app/page.tsx +++ b/packages/playground/src/app/page.tsx @@ -1,5 +1,7 @@ "use client"; +import { bech32 } from "bech32"; +import WebSocket from "isomorphic-ws"; import { ccc } from "@ckb-ccc/connector-react"; import { useEffect, useRef, useState, useCallback } from "react"; import { useApp } from "./context"; @@ -7,12 +9,11 @@ import { Blocks, BookOpenText, Bug, - Coins, FlaskConical, FlaskConicalOff, Play, Printer, - Send, + Share2, SquareArrowOutUpRight, SquareTerminal, StepForward, @@ -20,7 +21,7 @@ import { import { Button } from "./components/Button"; import { Transaction } from "./tabs/Transaction"; import { Scripts } from "./tabs/Scripts"; -import { DEFAULT_TRANSFER, DEFAULT_UDT_TRANSFER } from "./examples"; +import { DEFAULT_TRANSFER } from "./examples"; import html2canvas from "html2canvas"; import { About } from "./tabs/About"; import { Console } from "./tabs/Console"; @@ -28,6 +29,152 @@ import axios from "axios"; import { execute } from "./execute"; import { Editor } from "./components/Editor"; +async function shareToNostr( + client: ccc.Client, + relays: string[], + content: string +): Promise { + const event: ccc.NostrEvent = { + kind: 1050, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["filename", "ccc-playground.ts"], + ["client", "ccc-playground"], + ], + content, + }; + const signer = new ccc.SignerNostrPrivateKey( + client, + Array.from(new Array(32), () => Math.floor(Math.random() * 256)) + ); + const signedEvent = await signer.signNostrEvent(event); + + const sent = ( + await Promise.all( + relays.map(async (relay) => { + const socket = new WebSocket(relay); + const res = await new Promise((resolve) => { + setTimeout(resolve, 5000); + socket.onclose = () => { + resolve(undefined); + }; + socket.onmessage = (event) => { + const data = JSON.parse(event.data as string); + if (data[0] === "OK" && data[1] === signedEvent.id && data[2]) { + resolve(relay); + } else { + resolve(undefined); + } + }; + socket.onopen = () => { + socket.send(JSON.stringify(["EVENT", signedEvent])); + }; + }); + socket.close(); + return res; + }) + ) + ).filter((r) => r !== undefined); + + if (sent.length === 0) { + throw new Error("Failed to send event to relay"); + } + + const id = ccc.bytesFrom(signedEvent.id); + return bech32.encode( + "nevent", + bech32.toWords( + ccc.bytesConcat( + [0, id.length], + id, + ...sent + .map((relay) => { + console.log(relay); + const bytes = ccc.bytesFrom(relay, "ascii"); + return [[1, bytes.length], bytes]; + }) + .flat() + ) + ), + 65536 + ); +} + +function getTLVs(tlv: number[]) { + let i = 0; + + const values = []; + while (i < tlv.length) { + const type = tlv[i]; + const length = tlv[i + 1]; + const value = tlv.slice(i + 2, i + 2 + length); + i += 2 + length; + + values.push({ type, length, value }); + } + + return values; +} + +async function getFromNEvent( + defaultRelays: string[], + id: string +): Promise { + let eventId; + const relays = [...defaultRelays]; + for (const { type, value } of getTLVs( + bech32.fromWords(bech32.decode(id, 65536).words) + )) { + if (type === 0) { + eventId = ccc.hexFrom(value).slice(2); + } + if (type === 1) { + const relay = ccc.bytesTo(value, "ascii"); + if (!relays.includes(relay)) { + relays.push(relay); + } + } + } + if (!eventId) { + throw new Error("Invalid nevent"); + } + + return Promise.any(relays.map((relay) => getFromNostr(relay, eventId))); +} + +async function getFromNostr(relayUrl: string, id: string): Promise { + const socket = new WebSocket(relayUrl); + + const res = await new Promise((resolve, reject) => { + setTimeout(() => reject("Timeout"), 10000); + socket.onclose = () => { + reject("Connection closed"); + }; + socket.onmessage = (event) => { + const data = JSON.parse(event.data as string); + if (data[0] === "EVENT" && data[1] === "1") { + resolve(data[2].content); + } else if (data[0] === "EOSE") { + reject("Event not found"); + } else { + reject(JSON.stringify(event.data)); + } + }; + socket.onopen = () => { + socket.send(JSON.stringify(["REQ", "1", { ids: [id] }])); + }; + }); + socket.close(); + + return res; +} + +const DEFAULT_NOSTR_RELAYS = [ + "wss://relay.nostr.band", + "wss://nostr.oxtr.dev", + "wss://relay.damus.io", +]; + export default function Home() { const { openSigner, openAction, signer, messages, sendMessage } = useApp(); const { setClient, client } = ccc.useCcc(); @@ -97,15 +244,6 @@ export default function Home() { [source, signer, sendMessage] ); - useEffect(() => { - if (next) { - next(true); - } - - window.localStorage.setItem("playgroundSourceCode", source); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [source]); - useEffect(() => { const searchParams = new URLSearchParams(window.location.search); const src = searchParams.get("src"); @@ -119,12 +257,40 @@ export default function Home() { } setIsLoading(true); - axios.get(src).then(({ data }) => { - setSource(data); - setIsLoading(false); - }); + + if (src.startsWith("nostr:")) { + const id = src.slice(6); + getFromNEvent(DEFAULT_NOSTR_RELAYS, id) + .then((res) => { + if (res !== undefined) { + setSource(res); + } + }) + .finally(() => setIsLoading(false)); + } else { + axios + .get(src) + .then(({ data }) => { + setSource(data); + }) + .finally(() => setIsLoading(false)); + } }, []); + useEffect(() => { + if (next) { + next(true); + } + + const searchParams = new URLSearchParams(window.location.search); + const src = searchParams.get("src"); + + if (src == null) { + window.localStorage.setItem("playgroundSourceCode", source); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [source]); + useEffect(() => { if (tab === "Console") { setReadMsgCount(messages.length); @@ -179,13 +345,19 @@ export default function Home() { )} - - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4583603..183dfde8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -755,18 +755,21 @@ importers: '@ckb-ccc/connector-react': specifier: workspace:* version: link:../connector-react - '@ckb-ccc/spore': - specifier: workspace:* - version: link:../spore '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.51.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) axios: specifier: ^1.7.7 version: 1.7.7 + bech32: + specifier: ^2.0.0 + version: 2.0.0 html2canvas: specifier: ^1.4.1 version: 1.4.1 + isomorphic-ws: + specifier: ^5.0.0 + version: 5.0.0(ws@8.18.0) lucide-react: specifier: ^0.438.0 version: 0.438.0(react@18.3.1) @@ -8783,7 +8786,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -8802,7 +8805,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -8883,7 +8886,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0): + eslint-plugin-import@2.29.1(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -8903,8 +8906,6 @@ snapshots: object.values: 1.2.0 semver: 6.3.1 tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack