feat: make read-only accounts fake signers while using chopsticks

This commit is contained in:
Victor Oliva
2025-08-29 22:37:32 +08:00
parent ecdaa592e0
commit ce17147d65
5 changed files with 45 additions and 20 deletions

View File

@@ -118,5 +118,5 @@ export const createLocalStorageState = <T>(
},
)
return [state$(), setValue] as const
return [state$, setValue] as const
}

View File

@@ -2,7 +2,7 @@ import { AccountIdDisplay } from "@/components/AccountIdDisplay"
import { AccountIdInput } from "@/components/AccountIdInput"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
import { readOnlyAddresses$, setAddresses } from "@/state/accounts.state"
import { readOnlyAccounts$, setAddresses } from "@/state/accounts.state"
import { useStateObservable } from "@react-rxjs/core"
import { Eye, Trash2 } from "lucide-react"
import { SS58String } from "polkadot-api"
@@ -26,7 +26,7 @@ export const AddressProvider = () => (
const ManageAddresses = () => {
const [addressInput, setAddressInput] = useState<SS58String | null>(null)
const readOnlyAddresses = useStateObservable(readOnlyAddresses$)
const accounts = useStateObservable(readOnlyAccounts$)
return (
<div className="space-y-4 overflow-hidden">
@@ -36,7 +36,9 @@ const ManageAddresses = () => {
if (!addressInput) return
setAddresses([
...readOnlyAddresses.filter((v) => v !== addressInput),
...accounts
.map((v) => v.accountId)
.filter((v) => v !== addressInput),
addressInput,
])
setAddressInput(null)
@@ -56,23 +58,27 @@ const ManageAddresses = () => {
</Button>
</div>
</form>
{readOnlyAddresses.length ? (
{accounts.length ? (
<div>
<h3 className="font-medium text-muted-foreground">Added addresses</h3>
<ul className="space-y-3">
{readOnlyAddresses.map((addr) => (
<li key={addr} className="flex gap-2 items-center">
{accounts.map((account) => (
<li key={account.accountId} className="flex gap-2 items-center">
<Button
variant="outline"
className="text-destructive border-destructive"
type="button"
onClick={() =>
setAddresses(readOnlyAddresses.filter((v) => addr !== v))
setAddresses(
accounts
.map((v) => v.accountId)
.filter((v) => account.accountId !== v),
)
}
>
<Trash2 />
</Button>
<AccountIdDisplay value={addr} />
<AccountIdDisplay value={account.accountId} />
</li>
))}
</ul>

View File

@@ -70,7 +70,7 @@ const groupedAccounts$ = allAccounts$.pipeState(
map((v) =>
groupBy(
// Accept only signer accounts for submitting transactions
v.filter((v) => "signer" in v),
v.filter((v) => v.signer != null),
(acc) => (acc.type === "extension" ? acc.extensionId : acc.type),
),
),

View File

@@ -22,11 +22,13 @@ const SignAndSubmit: FC<{ callData: string; onClose: () => void }> = ({
return (
<ActionButton
onClick={async () => {
if (!account?.signer) return
setIsSigning(true)
try {
const unsafeApi = await firstValueFrom(unsafeApi$)
const tx = await unsafeApi.txFromCallData(Binary.fromHex(callData))
const signedExtrinsic = await tx.sign(account!.signer)
const signedExtrinsic = await tx.sign(account.signer)
trackSignedTx(signedExtrinsic)
onClose()
} catch (ex) {

View File

@@ -3,7 +3,9 @@ import { state } from "@react-rxjs/core"
import type { HexString, PolkadotSigner, SS58String } from "polkadot-api"
import type { InjectedPolkadotAccount } from "polkadot-api/pjs-signer"
import { combineLatest, defer, from, map, switchMap } from "rxjs"
import { accountsByExtension$ } from "./extension-accounts.state"
import { accountsByExtension$, getPublicKey } from "./extension-accounts.state"
import { getPolkadotSigner } from "polkadot-api/signer"
import { canSetStorage$ } from "./chains/chain.state"
export type AccountSource = "extension" | "walletconnect" | "readonly"
export const accountSourceTypeToName: Record<AccountSource, string> = {
@@ -73,20 +75,35 @@ export const walletConnectAccounts$ = state(
export interface ReadOnlyAccount extends BaseAccount {
type: "readonly"
// Chopsticks can fake a signer
signer?: PolkadotSigner
}
export const [readOnlyAddresses$, setAddresses] = createLocalStorageState(
const [readOnlyAddresses$, setAddresses] = createLocalStorageState(
"read-only-addr",
[] as string[],
)
export const readOnlyAccounts$ = readOnlyAddresses$.pipe(
map((v) =>
v.map(
(address): ReadOnlyAccount => ({
type: "readonly",
accountId: address,
}),
export { setAddresses }
export const readOnlyAccounts$ = state(
combineLatest([defer(readOnlyAddresses$), canSetStorage$]).pipe(
map(([v, isChopsticks]) =>
v.map(
(address): ReadOnlyAccount => ({
type: "readonly",
accountId: address,
signer: isChopsticks
? getPolkadotSigner(getPublicKey(address)!, "Sr25519", () => {
// From https://wiki.acala.network/build/sdks/homa
const signature = new Uint8Array(64)
signature.fill(0xcd)
signature.set([0xde, 0xad, 0xbe, 0xef])
return signature
})
: undefined,
}),
),
),
),
[],
)
export type Account = ExtensionAccount | WalletConnectAccount | ReadOnlyAccount