diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8fa71f4b2..1492f5ed9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -456,6 +456,9 @@ importers:
'@polkadot-api/client':
specifier: 0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0
version: 0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0(rxjs@7.8.1)
+ '@polkadot-api/metadata-builders':
+ specifier: 0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0
+ version: 0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0
'@polkadot-api/substrate-client':
specifier: 0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0
version: 0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0
diff --git a/projects/wallet-template/assets/wallet-popup.html b/projects/wallet-template/assets/wallet-popup.html
new file mode 100644
index 000000000..6f2698bdb
--- /dev/null
+++ b/projects/wallet-template/assets/wallet-popup.html
@@ -0,0 +1,23 @@
+
+
+
+ Substrate Connect
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/wallet-template/package.json b/projects/wallet-template/package.json
index 2a7e3e5fc..72712a60c 100644
--- a/projects/wallet-template/package.json
+++ b/projects/wallet-template/package.json
@@ -55,6 +55,7 @@
},
"dependencies": {
"@polkadot-api/client": "0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0",
+ "@polkadot-api/metadata-builders": "0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0",
"@polkadot-api/tx-helper": "0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0",
"@polkadot-api/substrate-client": "0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0",
"@polkadot-api/utils": "0.0.1-86c37582599a5aaa7c2dde1d765f8af5ef6def2a.1.0",
diff --git a/projects/wallet-template/src/background/createBackgroundRpc.ts b/projects/wallet-template/src/background/createBackgroundRpc.ts
index fba050ceb..255919856 100644
--- a/projects/wallet-template/src/background/createBackgroundRpc.ts
+++ b/projects/wallet-template/src/background/createBackgroundRpc.ts
@@ -1,7 +1,9 @@
import {
type RpcMethodHandlers,
type RpcMessage,
+ type RpcMethodMiddleware,
createRpc,
+ RpcError,
} from "@substrate/light-client-extension-helpers/utils"
import type { LightClientPageHelper } from "@substrate/light-client-extension-helpers/background"
import { sr25519CreateDerive } from "@polkadot-labs/hdkd"
@@ -14,7 +16,7 @@ import {
import { toHex, fromHex } from "@polkadot-api/utils"
import { UserSignedExtensions, getTxCreator } from "@polkadot-api/tx-helper"
-import type { BackgroundRpcSpec } from "./types"
+import type { BackgroundRpcSpec, SignRequest } from "./types"
const entropy = mnemonicToEntropy(DEV_PHRASE)
const miniSecret = entropyToMiniSecret(entropy)
@@ -27,10 +29,21 @@ const keypairs = [
derive("//westend//2"),
]
+type InternalSignRequest = {
+ resolve: () => void
+ reject: (reason?: any) => void
+} & SignRequest
+
+let nextSignRequestId = 0
+
export const createBackgroundRpc = (
sendMessage: (message: RpcMessage) => void,
) => {
- type Context = { lightClientPageHelper: LightClientPageHelper }
+ type Context = {
+ lightClientPageHelper: LightClientPageHelper
+ signRequests: Record
+ port: chrome.runtime.Port
+ }
const handlers: RpcMethodHandlers = {
async getAccounts([chainId], { lightClientPageHelper }) {
const chains = await lightClientPageHelper.getChains()
@@ -40,7 +53,12 @@ export const createBackgroundRpc = (
address: ss58Address(publicKey, chain.ss58Format),
}))
},
- async createTx([chainId, from, callData], { lightClientPageHelper }) {
+ async createTx(
+ [chainId, from, callData],
+ { lightClientPageHelper, signRequests, port },
+ ) {
+ const url = port.sender?.url
+ if (!url) throw new Error("unknown url")
const chains = await lightClientPageHelper.getChains()
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
if (!chain) throw new Error("unknown chain")
@@ -48,7 +66,38 @@ export const createBackgroundRpc = (
({ publicKey }) => toHex(publicKey) === from,
)
if (!keypair) throw new Error("unknown account")
- // FIXME: trigger prompt to show the decoded transaction details
+
+ const id = nextSignRequestId++
+ const signRequest = new Promise(
+ (resolve, reject) =>
+ (signRequests[id] = {
+ resolve,
+ reject,
+ chainId,
+ url,
+ address: ss58Address(from, chain.ss58Format),
+ callData,
+ }),
+ )
+
+ const window = await chrome.windows.create({
+ focused: true,
+ height: 600,
+ left: 150,
+ top: 150,
+ type: "popup",
+ url: chrome.runtime.getURL(
+ `ui/assets/wallet-popup.html#signRequest/${id}`,
+ ),
+ width: 560,
+ })
+ try {
+ await signRequest
+ } finally {
+ delete signRequests[id]
+ chrome.windows.remove(window.id!)
+ }
+
const txCreator = getTxCreator(
chain.provider,
({ userSingedExtensionsName }, callback) => {
@@ -71,6 +120,7 @@ export const createBackgroundRpc = (
callback({
userSignedExtensionsData,
overrides: {},
+ // FIXME: this should be inferred from the keypair signature scheme
signingType: "Sr25519",
signer: async (value) => keypair.sign(value),
})
@@ -82,6 +132,31 @@ export const createBackgroundRpc = (
txCreator.destroy()
return tx
},
+ async getSignRequests([], { signRequests }) {
+ return signRequests
+ },
+ async approveSignRequest([id], { signRequests }) {
+ signRequests[id]?.resolve()
+ },
+ async cancelSignRequest([id], { signRequests }) {
+ signRequests[id]?.reject()
+ },
+ }
+
+ type Method = keyof BackgroundRpcSpec
+ const ALLOWED_WEB_METHODS: Method[] = ["createTx", "getAccounts"]
+ const allowedMethodsMiddleware: RpcMethodMiddleware = async (
+ next,
+ request,
+ context,
+ ) => {
+ const { port } = context
+ if (
+ port.name === "substrate-wallet-template" &&
+ !ALLOWED_WEB_METHODS.includes(request.method as Method)
+ )
+ throw new RpcError("Method not found", -32601)
+ return next(request, context)
}
- return createRpc(sendMessage, handlers)
+ return createRpc(sendMessage, handlers, [allowedMethodsMiddleware])
}
diff --git a/projects/wallet-template/src/background/index.ts b/projects/wallet-template/src/background/index.ts
index 7ab66ec93..d2516c89a 100644
--- a/projects/wallet-template/src/background/index.ts
+++ b/projects/wallet-template/src/background/index.ts
@@ -24,10 +24,16 @@ const { lightClientPageHelper } = register({
),
})
+const signRequests = {}
+
chrome.runtime.onConnect.addListener((port) => {
- if (port.name !== "substrate-wallet-template") return
+ if (!port.name.startsWith("substrate-wallet-template")) return
const rpc = createBackgroundRpc((msg) => port.postMessage(msg))
port.onMessage.addListener((msg) =>
- rpc.handle(msg, { lightClientPageHelper }),
+ rpc.handle(msg, {
+ lightClientPageHelper,
+ signRequests,
+ port,
+ }),
)
})
diff --git a/projects/wallet-template/src/background/types.ts b/projects/wallet-template/src/background/types.ts
index cfa3ce648..bca90a6d6 100644
--- a/projects/wallet-template/src/background/types.ts
+++ b/projects/wallet-template/src/background/types.ts
@@ -2,7 +2,18 @@ export type Account = {
address: string
}
+export type SignRequest = {
+ url: string
+ chainId: string
+ address: string
+ callData: string
+}
+
export type BackgroundRpcSpec = {
getAccounts(chainId: string): Promise
createTx(chainId: string, from: string, callData: string): Promise
+ // private methods
+ getSignRequests(): Promise>
+ approveSignRequest(id: string): Promise
+ cancelSignRequest(id: string): Promise
}
diff --git a/projects/wallet-template/src/containers/WalletPopup.tsx b/projects/wallet-template/src/containers/WalletPopup.tsx
new file mode 100644
index 000000000..a711af88f
--- /dev/null
+++ b/projects/wallet-template/src/containers/WalletPopup.tsx
@@ -0,0 +1,82 @@
+import { createRpc } from "@substrate/light-client-extension-helpers/utils"
+import { useEffect, useState } from "react"
+import type { BackgroundRpcSpec, SignRequest } from "../background/types"
+import { DecodedCallData } from "./WalletPopup/components"
+
+// FIXME: use hook
+const port = chrome.runtime.connect({
+ name: "substrate-wallet-template/wallet-popup",
+})
+const rpc = createRpc((msg) =>
+ port.postMessage(msg),
+).withClient()
+port.onMessage.addListener(rpc.handle)
+
+export const WalletPopup = () => {
+ const [signRequest, setSignRequest] = useState()
+ const [signRequestId, setSignRequestId] = useState()
+ useEffect(() => {
+ const init = async () => {
+ const signRequests = await rpc.client.getSignRequests()
+ // TODO: handle many signRequests
+ const keys = Object.keys(signRequests)
+ if (keys.length === 0) return
+ setSignRequestId(keys[0])
+ setSignRequest(signRequests[keys[0]])
+ }
+ init()
+ }, [])
+ if (!signRequestId || !signRequest) return null
+ return (
+
+
+
Sign Request #{signRequestId}
+
+
+
Origin
+
{signRequest.url}
+
+
+
Chain Id
+
{signRequest.chainId}
+
+
+
From
+
{signRequest.address}
+
+
+
Call data
+
{signRequest.callData}
+
+
+
+
Signed extensions
+
coming soon...
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/projects/wallet-template/src/containers/WalletPopup/components/DecodedCallData.tsx b/projects/wallet-template/src/containers/WalletPopup/components/DecodedCallData.tsx
new file mode 100644
index 000000000..bfa8160bd
--- /dev/null
+++ b/projects/wallet-template/src/containers/WalletPopup/components/DecodedCallData.tsx
@@ -0,0 +1,26 @@
+import { useDecodedCallData } from "../hooks"
+
+type Props = {
+ chainId: string
+ callData: string
+}
+
+const jsonStringify = (value: any) =>
+ JSON.stringify(
+ value,
+ (_, value) => (typeof value === "bigint" ? value.toString() : value),
+ 2,
+ )
+
+export const DecodedCallData = ({ chainId, callData }: Props) => {
+ const { decodedCallData } = useDecodedCallData(chainId, callData)
+ if (!decodedCallData) return decoding...
+ return (
+
+
Pallet: {decodedCallData.pallet.value.name}
+
Call: {decodedCallData.call.value.name}
+
Args
+
{jsonStringify(decodedCallData?.args.value)}
+
+ )
+}
diff --git a/projects/wallet-template/src/containers/WalletPopup/components/index.ts b/projects/wallet-template/src/containers/WalletPopup/components/index.ts
new file mode 100644
index 000000000..acb187d12
--- /dev/null
+++ b/projects/wallet-template/src/containers/WalletPopup/components/index.ts
@@ -0,0 +1 @@
+export * from "./DecodedCallData"
diff --git a/projects/wallet-template/src/containers/WalletPopup/hooks/index.ts b/projects/wallet-template/src/containers/WalletPopup/hooks/index.ts
new file mode 100644
index 000000000..464cce4e0
--- /dev/null
+++ b/projects/wallet-template/src/containers/WalletPopup/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./useDecodedCallData"
diff --git a/projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts b/projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts
new file mode 100644
index 000000000..ab54f7c45
--- /dev/null
+++ b/projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts
@@ -0,0 +1,30 @@
+import { helper } from "@substrate/light-client-extension-helpers/extension-page"
+import { getObservableClient } from "@polkadot-api/client"
+import { createClient } from "@polkadot-api/substrate-client"
+import { DecodedCall, getViewBuilder } from "@polkadot-api/metadata-builders"
+import { useEffect, useState } from "react"
+import { filter, firstValueFrom } from "rxjs"
+import { useIsMounted } from "../../../hooks/useIsMounted"
+
+export const useDecodedCallData = (chainId: string, callData: string) => {
+ const [decodedCallData, setDecodedCallData] = useState()
+ const isMounted = useIsMounted()
+
+ useEffect(() => {
+ if (!chainId || !callData) return
+ helper.getChains().then(async (chains) => {
+ const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
+ if (!chain) return
+ const client = getObservableClient(createClient(chain.provider))
+ const { metadata$, unfollow } = client.chainHead$()
+ const metadata = await firstValueFrom(metadata$.pipe(filter(Boolean)))
+ unfollow()
+ client.destroy()
+ const builder = getViewBuilder(metadata)
+ if (!isMounted()) return
+ setDecodedCallData(builder.callDecoder(callData))
+ })
+ }, [chainId, callData])
+
+ return { decodedCallData }
+}
diff --git a/projects/wallet-template/src/containers/index.tsx b/projects/wallet-template/src/containers/index.tsx
index 71c1fd1a5..d86ca7c0c 100644
--- a/projects/wallet-template/src/containers/index.tsx
+++ b/projects/wallet-template/src/containers/index.tsx
@@ -1,3 +1,4 @@
export { Options } from "./Options"
export { default as Popup } from "./Popup"
export { default as Logo } from "../components/Logo"
+export { WalletPopup } from "./WalletPopup"
diff --git a/projects/wallet-template/src/wallet-popup.tsx b/projects/wallet-template/src/wallet-popup.tsx
new file mode 100644
index 000000000..385be8ce4
--- /dev/null
+++ b/projects/wallet-template/src/wallet-popup.tsx
@@ -0,0 +1,5 @@
+import { render } from "react-dom"
+import { WalletPopup } from "./containers"
+import "./style.css"
+
+render(, document.getElementById("popup"))
diff --git a/projects/wallet-template/vite.ui.config.js b/projects/wallet-template/vite.ui.config.js
index 1cbd7f91c..85c0ed890 100644
--- a/projects/wallet-template/vite.ui.config.js
+++ b/projects/wallet-template/vite.ui.config.js
@@ -12,6 +12,7 @@ export default defineConfig(({ mode }) => ({
input: {
popup: "assets/popup.html",
options: "assets/options.html",
+ walletPopup: "assets/wallet-popup.html",
},
},
},