Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add wallet-popup #1904

Merged
merged 2 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions projects/wallet-template/assets/wallet-popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html>
<head>
<title>Substrate Connect</title>
<meta charset="utf-8" />
<link
href="https://fonts.googleapis.com/css2?family=Poppins:ital@0;1&display=swap"
rel="stylesheet"
/>
<link
href="http://fonts.googleapis.com/css?family=Roboto:400,100,100italic,300, 500,700,900"
rel="stylesheet"
/>
<link
href="http://fonts.googleapis.com/css?family=Inter:400,100,100italic,300, 500,700,900"
rel="stylesheet"
/>
</head>
<body>
<div id="popup"></div>
<script type="module" src="../src/wallet-popup.tsx"></script>
</body>
</html>
1 change: 1 addition & 0 deletions projects/wallet-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
85 changes: 80 additions & 5 deletions projects/wallet-template/src/background/createBackgroundRpc.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -14,7 +16,7 @@
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)
Expand All @@ -27,10 +29,21 @@
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<string, InternalSignRequest>
port: chrome.runtime.Port
}
const handlers: RpcMethodHandlers<BackgroundRpcSpec, Context> = {
async getAccounts([chainId], { lightClientPageHelper }) {
const chains = await lightClientPageHelper.getChains()
Expand All @@ -40,15 +53,51 @@
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")
const keypair = keypairs.find(
({ 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<void>(
(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) => {
Expand All @@ -71,6 +120,7 @@
callback({
userSignedExtensionsData,
overrides: {},
// FIXME: this should be inferred from the keypair signature scheme
signingType: "Sr25519",
signer: async (value) => keypair.sign(value),
})
Expand All @@ -82,6 +132,31 @@
txCreator.destroy()
return tx
},
async getSignRequests([], { signRequests }) {

Check warning on line 135 in projects/wallet-template/src/background/createBackgroundRpc.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected empty array pattern

Check warning on line 135 in projects/wallet-template/src/background/createBackgroundRpc.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected empty array pattern

Check warning on line 135 in projects/wallet-template/src/background/createBackgroundRpc.ts

View workflow job for this annotation

GitHub Actions / playwright-test-extension

Unexpected empty array pattern

Check warning on line 135 in projects/wallet-template/src/background/createBackgroundRpc.ts

View workflow job for this annotation

GitHub Actions / playwright-test-examples

Unexpected empty array pattern

Check warning on line 135 in projects/wallet-template/src/background/createBackgroundRpc.ts

View workflow job for this annotation

GitHub Actions / connect-flaky-tests

Unexpected empty array pattern

Check warning on line 135 in projects/wallet-template/src/background/createBackgroundRpc.ts

View workflow job for this annotation

GitHub Actions / playwright-test-wallet-template

Unexpected empty array pattern

Check warning on line 135 in projects/wallet-template/src/background/createBackgroundRpc.ts

View workflow job for this annotation

GitHub Actions / zombienet-tests

Unexpected empty array pattern
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<Context> = 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])
}
10 changes: 8 additions & 2 deletions projects/wallet-template/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
)
})
11 changes: 11 additions & 0 deletions projects/wallet-template/src/background/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Account[]>
createTx(chainId: string, from: string, callData: string): Promise<string>
// private methods
getSignRequests(): Promise<Record<string, SignRequest>>
approveSignRequest(id: string): Promise<void>
cancelSignRequest(id: string): Promise<void>
}
82 changes: 82 additions & 0 deletions projects/wallet-template/src/containers/WalletPopup.tsx
Original file line number Diff line number Diff line change
@@ -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<BackgroundRpcSpec>()
port.onMessage.addListener(rpc.handle)

export const WalletPopup = () => {
const [signRequest, setSignRequest] = useState<SignRequest>()
const [signRequestId, setSignRequestId] = useState<string>()
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
Comment on lines +16 to +29
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll refactor this into hooks

return (
<main className="w-[32rem] mx-auto px-6 py-8">
<div>
<h1 className="text-3xl font-bold">Sign Request #{signRequestId}</h1>
<div className="my-4">
<div className="my-2">
<div className="text-sm font-semibold">Origin</div>
<div>{signRequest.url}</div>
</div>
<div className="my-2 overflow-hidden">
<div className="text-sm font-semibold">Chain Id</div>
<pre className="overflow-auto">{signRequest.chainId}</pre>
</div>
<div className="my-2">
<div className="text-sm font-semibold">From</div>
<pre>{signRequest.address}</pre>
</div>
<div className="my-2 overflow-hidden">
<div className="text-sm font-semibold">Call data</div>
<pre className="overflow-auto">{signRequest.callData}</pre>
</div>
<div className="my-2">
<div className="text-sm font-semibold">Decoded Call data</div>
<DecodedCallData
chainId={signRequest.chainId}
callData={signRequest.callData}
/>
</div>
<div className="my-2">
<div className="text-sm font-semibold">Signed extensions</div>
<div>coming soon...</div>
</div>
</div>
<div className="my-4 text-center">
<button
onClick={() => rpc.client.approveSignRequest(signRequestId)}
className="py-1.5 px-8 mr-4 text-sm rounded border border-[#24cc85] text-[#24cc85] hover:text-white
hover:bg-[#24cc85]"
>
Approve
</button>
<button
onClick={() => rpc.client.cancelSignRequest(signRequestId)}
className="py-1.5 px-8 text-sm rounded border border-[#24cc85] text-[#24cc85] hover:text-white
hover:bg-[#24cc85]"
>
Cancel
</button>
</div>
</div>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -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 <div>decoding...</div>
return (
<div>
<div className="text-sm">Pallet: {decodedCallData.pallet.value.name}</div>
<div className="text-sm">Call: {decodedCallData.call.value.name}</div>
<div className="text-sm">Args</div>
<pre>{jsonStringify(decodedCallData?.args.value)}</pre>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./DecodedCallData"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useDecodedCallData"
Original file line number Diff line number Diff line change
@@ -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<DecodedCall>()
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])

Check warning on line 27 in projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

React Hook useEffect has a missing dependency: 'isMounted'. Either include it or remove the dependency array

Check warning on line 27 in projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

React Hook useEffect has a missing dependency: 'isMounted'. Either include it or remove the dependency array

Check warning on line 27 in projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts

View workflow job for this annotation

GitHub Actions / playwright-test-extension

React Hook useEffect has a missing dependency: 'isMounted'. Either include it or remove the dependency array

Check warning on line 27 in projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts

View workflow job for this annotation

GitHub Actions / playwright-test-examples

React Hook useEffect has a missing dependency: 'isMounted'. Either include it or remove the dependency array

Check warning on line 27 in projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts

View workflow job for this annotation

GitHub Actions / connect-flaky-tests

React Hook useEffect has a missing dependency: 'isMounted'. Either include it or remove the dependency array

Check warning on line 27 in projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts

View workflow job for this annotation

GitHub Actions / playwright-test-wallet-template

React Hook useEffect has a missing dependency: 'isMounted'. Either include it or remove the dependency array

Check warning on line 27 in projects/wallet-template/src/containers/WalletPopup/hooks/useDecodedCallData.ts

View workflow job for this annotation

GitHub Actions / zombienet-tests

React Hook useEffect has a missing dependency: 'isMounted'. Either include it or remove the dependency array

return { decodedCallData }
}
1 change: 1 addition & 0 deletions projects/wallet-template/src/containers/index.tsx
Original file line number Diff line number Diff line change
@@ -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"
5 changes: 5 additions & 0 deletions projects/wallet-template/src/wallet-popup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { render } from "react-dom"
import { WalletPopup } from "./containers"
import "./style.css"

render(<WalletPopup />, document.getElementById("popup"))
1 change: 1 addition & 0 deletions projects/wallet-template/vite.ui.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default defineConfig(({ mode }) => ({
input: {
popup: "assets/popup.html",
options: "assets/options.html",
walletPopup: "assets/wallet-popup.html",
},
},
},
Expand Down