diff --git a/bun.lockb b/bun.lockb index 41de13e..6b99fd4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.html b/index.html index e4b78ea..6d3905a 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,15 @@ - + - Vite + React + TS + Laos Signature Test - -
+ +
diff --git a/package.json b/package.json index bf210ba..2bf25be 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@tailwindcss/forms": "^0.5.7", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", diff --git a/src/App.tsx b/src/App.tsx index 1446c3e..fbdd7e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,34 +1,137 @@ -import { useState } from "react" +import { ApiPromise, Keyring, WsProvider } from "@polkadot/api" +import { Metadata } from "@polkadot/types" +import { useCallback, useEffect, useState } from "react" -const presetWsUrls = [ - "wss://rpc.laos.laosfoundation.io", - "wss://rpc.laosmercury.gorengine.com", - "wss://wss.api.moonbeam.network", -] +import { presetWsUrls } from "./constants" +import { SeedInput } from "./SeedInput" +import { metadataFromOpaque } from "./util" +import { WsSelect } from "./WsSelect" -function App() { - const [wsUrl, setWsUrl] = useState(presetWsUrls[0]) +const keyring = new Keyring() + +const defaultSeed = "test test test test test test test test test test test junk" +const defaultWsUrl = presetWsUrls[0] + +export const App = () => { + const [seed, setSeed] = useState(defaultSeed) + const [wsUrl, setWsUrl] = useState(defaultWsUrl) + + const [address, setAddress] = useState("") + useEffect(() => { + try { + const keypair = keyring.createFromUri(`${seed}/m/44'/60'/0'/0/0`, {}, "ethereum") + setAddress(keypair.address.toString()) + } catch { + setAddress("") + } + }, [seed]) + + const [balance, setBalance] = useState(null) + useEffect(() => { + try { + const keypair = keyring.createFromUri(`${seed}/m/44'/60'/0'/0/0`, {}, "ethereum") + + const abort = new AbortController() + + const api = new ApiPromise({ provider: new WsProvider(wsUrl) }) + abort.signal.onabort = () => api.disconnect() + + setBalance("loading") + + // + ;(async () => { + try { + await api.isReadyOrError + if (abort.signal.aborted) return + + const balance = await api.query.system.account(keypair.address) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setBalance((balance as any)?.data?.free?.toString?.() ?? "0") + } catch { + setBalance(null) + } + })() + + return () => abort.abort() + } catch { + setBalance(null) + } + }, [seed, wsUrl]) + + const [output, setOutput] = useState("") + const addOutput = useCallback((data: string) => setOutput((o) => o + data + "\n"), []) + + const test = useCallback(() => { + const keypair = keyring.createFromUri(`${seed}/m/44'/60'/0'/0/0`, {}, "ethereum") + + const abort = new AbortController() + + const api = new ApiPromise({ provider: new WsProvider(wsUrl) }) + abort.signal.onabort = () => api.disconnect() + + addOutput("Creating extrinsic...") + + // + ;(async () => { + try { + await api.isReadyOrError + if (abort.signal.aborted) return + + const opaqueMetadata = await api.call.metadata.metadataAtVersion(15) + const metadataRpc = metadataFromOpaque(opaqueMetadata.toU8a()) + // addOutput("metadataRpc: " + metadataRpc) + + const metadata = new Metadata(api.registry, metadataRpc) + // addOutput("metadata: " + JSON.stringify(metadata.toJSON(), null, 2)) + + api.registry.setMetadata(metadata) + + const extrinsic = await api.tx.balances + .transferKeepAlive(keypair.address, 0) + .signAsync(keypair) + + addOutput( + `Extrinsic signature: ${extrinsic.signature}\n(length ${extrinsic.signature.byteLength})` + ) + } catch (cause) { + addOutput(`Failed: ${String(cause)}`) + } + })() + + return () => abort.abort() + }, [addOutput, seed, wsUrl]) return ( - <> - - +

Laos Signature Test

+ setSeed("")} + onChange={(e) => setSeed(e.currentTarget.value)} + onReset={() => setSeed(defaultSeed)} + /> +
+ {address ? "Address: " : "Invalid seed or key"} + {address} +
+
+ {balance !== null ? "Balance: " : "No balance"} + {balance} +
+ setWsUrl("")} onChange={(e) => setWsUrl(e.currentTarget.value)} + onReset={() => setWsUrl(defaultWsUrl)} /> -
-

Vite + React

-
- {/* */} -

- Edit src/App.tsx and save to test HMR -

-
-

Click on the Vite and React logos to learn more

- + + {output &&
{output}
} + ) } - -export default App diff --git a/src/SeedInput.tsx b/src/SeedInput.tsx new file mode 100644 index 0000000..0da28a1 --- /dev/null +++ b/src/SeedInput.tsx @@ -0,0 +1,54 @@ +export const SeedInput = ({ + value, + onClick, + onChange, + onReset, +}: Pick< + React.DetailedHTMLProps, HTMLInputElement>, + "value" | "onClick" | "onChange" +> & { onReset: () => void }) => ( +
+ + +
+ + + + + + + + + + + +
+
+) diff --git a/src/WsSelect.tsx b/src/WsSelect.tsx new file mode 100644 index 0000000..a65fdbf --- /dev/null +++ b/src/WsSelect.tsx @@ -0,0 +1,72 @@ +import { presetWsUrls } from "./constants" + +export const WsSelect = ({ + value, + onClick, + onChange, + onReset, +}: Pick< + React.DetailedHTMLProps, HTMLInputElement>, + "value" | "onClick" | "onChange" +> & { onReset: () => void }) => ( +
+ + +
+ + + + + + + + + + + + + + +
+ + + {presetWsUrls.map((url) => ( + + ))} + +
+) diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..2c3c2a8 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,5 @@ +export const presetWsUrls = [ + "wss://rpc.laos.laosfoundation.io", + "wss://rpc.laosmercury.gorengine.com", + "wss://wss.api.moonbeam.network", +] diff --git a/src/index.css b/src/index.css index 6e07d3e..b5c61c9 100644 --- a/src/index.css +++ b/src/index.css @@ -1,18 +1,3 @@ @tailwind base; @tailwind components; @tailwind utilities; - -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} diff --git a/src/main.tsx b/src/main.tsx index 6f4ac9b..1fd0ef9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,12 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.tsx' -import './index.css' +import { StrictMode } from "react" +import { createRoot } from "react-dom/client" -createRoot(document.getElementById('root')!).render( +import { App } from "./App.tsx" + +import "./index.css" + +createRoot(document.getElementById("root")!).render( - , + ) diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..e08cfbd --- /dev/null +++ b/src/util.ts @@ -0,0 +1,20 @@ +import { TypeRegistry } from "@polkadot/types" +import { u8aToNumber } from "@polkadot/util" + +export const metadataFromOpaque = (opaque: Uint8Array) => { + try { + // pjs codec for OpaqueMetadata doesn't allow us to decode the actual Metadata, find it ourselves + const u8aBytes = opaque + for (let i = 0; i < 20; i++) { + // skip until we find the magic number that is used as prefix of metadata objects (usually in the first 10 bytes) + if (u8aToNumber(u8aBytes.slice(i, i + 4)) !== 0x6174656d) continue + + const metadata = new TypeRegistry().createType("Metadata", u8aBytes.slice(i)) + + return metadata.toHex() + } + throw new Error("Magic number not found") + } catch (cause) { + throw new Error("Failed to decode metadata from OpaqueMetadata", { cause }) + } +} diff --git a/tailwind.config.js b/tailwind.config.js index b6bf855..38302b3 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./src/**/*.{js,ts,jsx,tsx}"], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, },