Skip to content

Commit 104b940

Browse files
authored
packages/chain-ethereum: add sign in with farcaster (#438)
* add siwfsigner, verify fc message to farcasterSignerAddress * use requestId to encode canvas pubkey, update siwf message encoding, add integration test for siwf verification * examples/chat: prototype siwf login to chat example * add static methods for generating requestId, update fixtures, tests pass * fix parsing of delegate did, return custody address * workaround for creating messages * examples/chat: default back to burner wallet * examples/chat: handle signer switching * factor out newSIWFSession * add eip712signer to chat * sync fixes: add eip712signer and siwfsigner to cli, add siwfsigner to chat init
1 parent 5cd864e commit 104b940

File tree

14 files changed

+969
-40
lines changed

14 files changed

+969
-40
lines changed

examples/chat/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@canvas-js/interfaces": "0.14.0-next.0",
2222
"@canvas-js/modeldb-sqlite-wasm": "0.14.0-next.0",
2323
"@cosmjs/encoding": "^0.32.3",
24+
"@farcaster/auth-kit": "^0.6.0",
2425
"@keplr-wallet/types": "^0.11.64",
2526
"@libp2p/interface": "^2.2.1",
2627
"@magic-ext/auth": "^4.3.2",

examples/chat/src/App.tsx

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useRef, useState } from "react"
22

33
import type { SessionSigner } from "@canvas-js/interfaces"
4-
import { SIWESigner } from "@canvas-js/chain-ethereum"
4+
import { SIWESigner, SIWFSigner, Eip712Signer } from "@canvas-js/chain-ethereum"
55
import { ATPSigner } from "@canvas-js/chain-atp"
66
import { CosmosSigner } from "@canvas-js/chain-cosmos"
77
import { SubstrateSigner } from "@canvas-js/chain-substrate"
@@ -11,6 +11,9 @@ import type { Contract } from "@canvas-js/core"
1111

1212
import { useCanvas } from "@canvas-js/hooks"
1313

14+
import { AuthKitProvider } from "@farcaster/auth-kit"
15+
import { JsonRpcProvider } from "ethers"
16+
1417
import { AppContext } from "./AppContext.js"
1518
import { Messages } from "./Chat.js"
1619
import { MessageComposer } from "./MessageComposer.js"
@@ -21,11 +24,21 @@ import { Connect } from "./connect/index.js"
2124
import { LogStatus } from "./LogStatus.js"
2225
import { contract } from "./contract.js"
2326

24-
const topic = "chat-example.canvas.xyz"
27+
export const topic = "chat-example.canvas.xyz"
2528

2629
const wsURL = import.meta.env.VITE_CANVAS_WS_URL ?? null
2730
console.log("websocket API URL:", wsURL)
2831

32+
const config = {
33+
// For a production app, replace this with an Optimism Mainnet
34+
// RPC URL from a provider like Alchemy or Infura.
35+
relay: "https://relay.farcaster.xyz",
36+
rpcUrl: "https://mainnet.optimism.io",
37+
domain: "chat-example.canvas.xyz",
38+
siweUri: "https://chat-example.canvas.xyz",
39+
provider: new JsonRpcProvider(undefined, 10),
40+
}
41+
2942
export const App: React.FC<{}> = ({}) => {
3043
const [sessionSigner, setSessionSigner] = useState<SessionSigner | null>(null)
3144
const [address, setAddress] = useState<string | null>(null)
@@ -35,32 +48,42 @@ export const App: React.FC<{}> = ({}) => {
3548
const { app } = useCanvas(wsURL, {
3649
topic: topicRef.current,
3750
contract: contract,
38-
signers: [new SIWESigner(), new ATPSigner(), new CosmosSigner(), new SubstrateSigner({}), new SolanaSigner()],
51+
signers: [
52+
new SIWESigner(),
53+
new Eip712Signer(),
54+
new SIWFSigner(),
55+
new ATPSigner(),
56+
new CosmosSigner(),
57+
new SubstrateSigner({}),
58+
new SolanaSigner(),
59+
],
3960
})
4061

4162
return (
4263
<AppContext.Provider value={{ address, setAddress, sessionSigner, setSessionSigner, app: app ?? null }}>
43-
{app ? (
44-
<main>
45-
<div className="flex flex-row gap-4 h-full">
46-
<div className="min-w-[480px] flex-1 flex flex-col justify-stretch gap-2">
47-
<div className="flex-1 border rounded px-2 overflow-y-scroll">
48-
<Messages address={address} />
64+
<AuthKitProvider config={config}>
65+
{app ? (
66+
<main>
67+
<div className="flex flex-row gap-4 h-full">
68+
<div className="min-w-[480px] flex-1 flex flex-col justify-stretch gap-2">
69+
<div className="flex-1 border rounded px-2 overflow-y-scroll">
70+
<Messages address={address} />
71+
</div>
72+
<MessageComposer />
73+
</div>
74+
<div className="flex flex-col gap-4 w-[480px] break-all">
75+
<Connect />
76+
<SessionStatus />
77+
<ConnectionStatus topic={topicRef.current} />
78+
<LogStatus />
79+
<ControlPanel />
4980
</div>
50-
<MessageComposer />
51-
</div>
52-
<div className="flex flex-col gap-4 w-[480px] break-all">
53-
<Connect />
54-
<SessionStatus />
55-
<ConnectionStatus topic={topicRef.current} />
56-
<LogStatus />
57-
<ControlPanel />
5881
</div>
59-
</div>
60-
</main>
61-
) : (
62-
<div className="text-center my-20">Connecting to {wsURL}...</div>
63-
)}
82+
</main>
83+
) : (
84+
<div className="text-center my-20">Connecting to {wsURL}...</div>
85+
)}
86+
</AuthKitProvider>
6487
</AppContext.Provider>
6588
)
6689
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import "@farcaster/auth-kit/styles.css"
2+
3+
import React, { useContext, useEffect, useRef, useState } from "react"
4+
import { hexlify, getBytes } from "ethers"
5+
import { SIWFSigner } from "@canvas-js/chain-ethereum"
6+
import { ed25519 } from "@canvas-js/signatures"
7+
import { SignInButton, useProfile } from "@farcaster/auth-kit"
8+
9+
import { topic } from "../App.js"
10+
import { AppContext } from "../AppContext.js"
11+
12+
export interface ConnectSIWFProps {}
13+
14+
export const ConnectSIWF: React.FC<ConnectSIWFProps> = ({}) => {
15+
const { app, setSessionSigner, setAddress } = useContext(AppContext)
16+
17+
const profile = useProfile()
18+
const {
19+
isAuthenticated,
20+
profile: { fid, displayName, custody, verifications },
21+
} = profile
22+
23+
const [error, setError] = useState<Error | null>(null)
24+
const [requestId, setRequestId] = useState<string | null>(null)
25+
const [privateKey, setPrivateKey] = useState<string | null>(null)
26+
27+
const initialRef = useRef(false)
28+
useEffect(() => {
29+
if (initialRef.current) {
30+
return
31+
}
32+
33+
initialRef.current = true
34+
35+
const { requestId, privateKey } = SIWFSigner.newSIWFRequestId(topic)
36+
setRequestId(requestId)
37+
setPrivateKey(hexlify(privateKey))
38+
}, [])
39+
40+
if (error !== null) {
41+
return (
42+
<div className="p-2 border rounded bg-red-100 text-sm">
43+
<code>{error.message}</code>
44+
</div>
45+
)
46+
} else if (!privateKey || !requestId || !app) {
47+
return (
48+
<div className="p-2 border rounded bg-gray-200">
49+
<button disabled>Loading...</button>
50+
</div>
51+
)
52+
} else {
53+
return (
54+
<div style={{ marginTop: "12px", right: "12px" }}>
55+
{isAuthenticated && (
56+
<div>
57+
<p>
58+
Hello, {displayName}! Your FID is {fid}.
59+
</p>
60+
<p>Your custody address is: </p>
61+
<pre>{custody}</pre>
62+
<p>Your connected signers: </p>
63+
{verifications?.map((v, i) => <pre key={i}>{v}</pre>)}
64+
</div>
65+
)}
66+
<SignInButton
67+
requestId={requestId}
68+
onSuccess={async (result) => {
69+
const { signature, message } = result
70+
if (!message || !signature) {
71+
setError(new Error("login succeeded but did not return a valid SIWF message"))
72+
return
73+
}
74+
75+
const { authorizationData, topic, custodyAddress } = SIWFSigner.parseSIWFMessage(message, signature)
76+
const signer = new SIWFSigner({ custodyAddress, privateKey: privateKey.slice(2) })
77+
const address = await signer.getDid()
78+
79+
const timestamp = new Date(authorizationData.siweIssuedAt).valueOf()
80+
const { payload, signer: delegateSigner } = await signer.newSIWFSession(
81+
topic,
82+
authorizationData,
83+
timestamp,
84+
getBytes(privateKey),
85+
)
86+
setAddress(address)
87+
setSessionSigner(signer)
88+
app.updateSigners([
89+
signer,
90+
...app.signers.getAll().filter((signer) => signer.key !== "chain-ethereum-farcaster"),
91+
])
92+
app.messageLog.append(payload, { signer: delegateSigner })
93+
console.log("started SIWF chat session", authorizationData)
94+
}}
95+
onError={(...args) => {
96+
console.log("received SIWF error", args)
97+
}}
98+
onSignOut={(...args) => {
99+
setAddress(null)
100+
setSessionSigner(null)
101+
}}
102+
/>
103+
</div>
104+
)
105+
}
106+
}

examples/chat/src/connect/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState } from "react"
22

33
import { ConnectSIWEBurner } from "./ConnectSIWEBurner.js"
44
import { ConnectSIWE } from "./ConnectSIWE.js"
5+
import { ConnectSIWF } from "./ConnectSIWF.js"
56
import { ConnectSIWEViem } from "./ConnectSIWEViem.js"
67
import { ConnectEIP712Burner } from "./ConnectEIP712Burner.js"
78
import { ConnectEIP712 } from "./ConnectEIP712.js"
@@ -27,6 +28,7 @@ export const Connect: React.FC<{}> = ({}) => {
2728
>
2829
<option value="burner">Burner Wallet</option>
2930
<option value="burner-eip712">Burner Wallet - EIP712</option>
31+
<option value="farcaster">Sign in with Farcaster</option>
3032
<option value="ethereum">Ethereum</option>
3133
<option value="ethereum-viem">Ethereum (Viem)</option>
3234
<option value="ethereum-eip712">Ethereum (EIP712)</option>
@@ -37,7 +39,7 @@ export const Connect: React.FC<{}> = ({}) => {
3739
{/* <option value="near">NEAR</option> */}
3840
<option value="terra">Terra</option>
3941
<option value="cosmos-evm">Cosmos/EVM</option>
40-
<option value="bluesky">BlueSky</option>
42+
<option value="bluesky">Bluesky</option>
4143
<option value="leap">Leap</option>
4244
<option value="magic">Magic</option>
4345
</select>
@@ -48,6 +50,8 @@ export const Connect: React.FC<{}> = ({}) => {
4850

4951
const Method: React.FC<{ method: string }> = (props) => {
5052
switch (props.method) {
53+
case "farcaster":
54+
return <ConnectSIWF />
5155
case "burner":
5256
return <ConnectSIWEBurner />
5357
case "burner-eip712":

0 commit comments

Comments
 (0)