Skip to content

Commit

Permalink
feat(playground): nostr paste bin
Browse files Browse the repository at this point in the history
  • Loading branch information
Hanssen0 committed Jan 12, 2025
1 parent 49d5c23 commit 0095cf6
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 102 deletions.
3 changes: 2 additions & 1 deletion packages/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
70 changes: 0 additions & 70 deletions packages/playground/src/app/examples.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
218 changes: 195 additions & 23 deletions packages/playground/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,180 @@
"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";
import {
Blocks,
BookOpenText,
Bug,
Coins,
FlaskConical,
FlaskConicalOff,
Play,
Printer,
Send,
Share2,
SquareArrowOutUpRight,
SquareTerminal,
StepForward,
} from "lucide-react";
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";
import axios from "axios";
import { execute } from "./execute";
import { Editor } from "./components/Editor";

async function shareToNostr(
client: ccc.Client,
relays: string[],
content: string
): Promise<string> {
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<string | undefined>((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<string | undefined> {
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<string> {
const socket = new WebSocket(relayUrl);

const res = await new Promise<string>((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();
Expand Down Expand Up @@ -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");
Expand All @@ -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);
Expand Down Expand Up @@ -179,13 +345,19 @@ export default function Home() {
</Button>
</>
)}
<Button onClick={() => setSource(DEFAULT_TRANSFER)}>
<Send size="16" />
<span className="ml-1">Example: Transfer</span>
</Button>
<Button onClick={() => setSource(DEFAULT_UDT_TRANSFER)}>
<Coins size="16" />
<span className="ml-1">Example: UDT Transfer</span>
<Button
onClick={async () => {
const id = await shareToNostr(
client,
DEFAULT_NOSTR_RELAYS,
source
);

window.location.href = `/?src=nostr:${id}`;
}}
>
<Share2 size="16" />
<span className="ml-1">Share</span>
</Button>
</div>
</div>
Expand Down
Loading

0 comments on commit 0095cf6

Please sign in to comment.