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 }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)
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: {},
},