Skip to content

Commit

Permalink
Add WalletConnect support (#2133)
Browse files Browse the repository at this point in the history
  • Loading branch information
jribbink authored Feb 21, 2025
1 parent f691a8c commit b010de2
Show file tree
Hide file tree
Showing 25 changed files with 1,801 additions and 508 deletions.
542 changes: 342 additions & 200 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions packages/fcl-core/src/current-user/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,10 @@ const getAuthenticate =
* @param {object} [opts] - Options
* @param {object} [opts.service] - Optional service to use for authentication
* @param {boolean} [opts.redir] - Optional redirect flag
* @param {boolean} [opts.forceReauth] - Optional force re-authentication flag
* @returns
*/
async ({service, redir = false} = {}) => {
async ({service, redir = false, forceReauth = false} = {}) => {
if (
service &&
!service?.provider?.is_installed &&
Expand All @@ -185,7 +186,7 @@ const getAuthenticate =
const refreshService = serviceOfType(user.services, "authn-refresh")
let accountProofData

if (user.loggedIn) {
if (user.loggedIn && !forceReauth) {
if (refreshService) {
try {
const response = await execService({
Expand Down
7 changes: 6 additions & 1 deletion packages/fcl-ethereum-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,14 @@
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/hash": "^5.7.0",
"@noble/hashes": "^1.7.1",
"@onflow/fcl-wc": "^5.5.4",
"@onflow/rlp": "^1.2.3",
"@walletconnect/ethereum-provider": "^2.18.0",
"@walletconnect/jsonrpc-http-connection": "^1.0.8",
"@walletconnect/jsonrpc-provider": "^1.0.14"
"@walletconnect/jsonrpc-provider": "^1.0.14",
"@walletconnect/types": "^2.18.0",
"@walletconnect/universal-provider": "^2.18.0",
"@walletconnect/utils": "^2.18.0"
},
"peerDependencies": {
"@onflow/fcl": "^1.13.4"
Expand Down
12 changes: 6 additions & 6 deletions packages/fcl-ethereum-provider/src/accounts/account-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,18 @@ export class AccountManager {

$user
.pipe(
// Only listen bind to users matching the current authz service
// Only listen bind to users matching the current authn service
map(snapshot => {
const addr = snapshot?.addr || null
if (!addr) {
return null
}

const authzService = snapshot?.services?.find(
service => service.type === "authz"
const authnService = snapshot?.services?.find(
service => service.type === "authn"
)
const matchingAuthzService = authzService?.uid === this.service?.uid
return matchingAuthzService ? addr : null
const matchingAuthnService = authnService?.uid === this.service?.uid
return matchingAuthnService ? addr : null
}),
distinctUntilChanged(),
switchMap(addr =>
Expand Down Expand Up @@ -110,7 +110,7 @@ export class AccountManager {
}

public async authenticate(): Promise<string[]> {
await this.user.authenticate({service: this.service})
await this.user.authenticate({service: this.service, forceReauth: true})
return this.getAccounts()
}

Expand Down
16 changes: 9 additions & 7 deletions packages/fcl-ethereum-provider/src/create-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import {AccountManager} from "./accounts/account-manager"
import {FLOW_CHAINS} from "./constants"
import {Gateway} from "./gateway/gateway"
import {NetworkManager} from "./network/network-manager"
import {Subject} from "./util/observable"

export type FclProviderConfig = {
user: typeof fcl.currentUser
config: typeof fcl.config
service?: Service
gateway?: string
rpcUrls?: {[chainId: string]: number}
}

/**
* Create a new FCL Ethereum provider
Expand All @@ -29,12 +36,7 @@ import {Subject} from "./util/observable"
* })
* ```
*/
export function createProvider(config: {
user: typeof fcl.currentUser
config: typeof fcl.config
service?: Service
rpcUrls?: {[chainId: string]: number}
}): Eip1193Provider {
export function createProvider(config: FclProviderConfig): Eip1193Provider {
const defaultRpcUrls = Object.values(FLOW_CHAINS).reduce(
(acc, chain) => {
acc[chain.eip155ChainId] = chain.publicRpcUrl
Expand Down
1 change: 1 addition & 0 deletions packages/fcl-ethereum-provider/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export {createProvider} from "./create-provider"
export {WalletConnectEthereumProvider} from "./wc-provider"
221 changes: 221 additions & 0 deletions packages/fcl-ethereum-provider/src/wc-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import {
EthereumProvider,
EthereumProviderOptions,
OPTIONAL_EVENTS,
OPTIONAL_METHODS,
REQUIRED_EVENTS,
REQUIRED_METHODS,
} from "@walletconnect/ethereum-provider"
import {
NamespaceConfig,
UniversalProvider,
} from "@walletconnect/universal-provider"
import {SessionTypes} from "@walletconnect/types"
import {FLOW_CHAINS, FlowNetwork} from "./constants"
import {formatChainId} from "./util/eth"
import {getAccountsFromNamespaces} from "@walletconnect/utils"
import {FLOW_METHODS} from "@onflow/fcl-wc"
import * as fcl from "@onflow/fcl"
import {Service} from "@onflow/typedefs"

const BASE_WC_SERVICE = (
externalProvider: InstanceType<typeof UniversalProvider>
) =>
({
f_type: "Service",
f_vsn: "1.0.0",
type: "authn",
method: "WC/RPC",
uid: "cross-vm-walletconnect#authn",
endpoint: "flow_authn",
optIn: true,
provider: {
address: null,
name: "WalletConnect",
icon: "https://avatars.githubusercontent.com/u/37784886",
description: "WalletConnect Base Service",
website: "https://walletconnect.com",
color: null,
supportEmail: null,
},
params: {
externalProvider,
},
}) as unknown as Service

export class WalletConnectEthereumProvider extends EthereumProvider {
static async init(
opts: EthereumProviderOptions
): Promise<WalletConnectEthereumProvider> {
const provider = new WalletConnectEthereumProvider()
await provider.initialize(opts)

// Refresh the FCL user to align with the WalletConnect session
async function refreshFclUser() {
const fclUser = fcl.currentUser()
const wcService = BASE_WC_SERVICE(provider.signer)
const snapshot = await fclUser.snapshot()

// Find the authentication service from the current FCL user snapshot
const authnService = snapshot?.services.find(
service => service.type === "authn"
)

// If there’s no auth service or the auth service
if (authnService && authnService.uid !== wcService.uid) {
// Another FCL user is already authenticated, we need to unauthenticate it
if (provider.signer.session) {
await fclUser.authenticate({service: wcService, forceReauth: true})
}
} else {
// Determine the external provider's topic from the auth service params
const externalProvider = authnService?.params?.externalProvider as
| string
| InstanceType<typeof UniversalProvider>
| undefined
const externalProviderTopic =
typeof externalProvider === "string"
? externalProvider
: (externalProvider?.session?.topic ?? null)

// If the provider is already connected with a matching session, re-authenticate the user
if (
provider.signer.session &&
(externalProviderTopic == null ||
externalProviderTopic === provider.signer.session.topic)
) {
await fclUser.authenticate({service: wcService, forceReauth: true})
} else if (!provider.signer.session) {
// If no session is set but FCL is still authenticated, unauthenticate the user
await fclUser.unauthenticate()
}
}
}

// Set up event listeners regardless of the current authentication state
provider.on("connect", async () => {
try {
await refreshFclUser()
} catch (error) {
console.error("Error during authentication on connect:", error)
}
})

provider.on("disconnect", async () => {
try {
await refreshFclUser()
} catch (error) {
console.error("Error during unauthentication on disconnect:", error)
}
})

return provider
}

async connect(
opts?: Parameters<InstanceType<typeof EthereumProvider>["connect"]>[0]
) {
if (!this.signer.client) {
throw new Error("Provider not initialized. Call init() first")
}

this.loadConnectOpts(opts)

const chains = new Set(opts?.chains ?? [])
const optionalChains = new Set(opts?.optionalChains ?? [])
const chainIds = Array.from(chains).concat(Array.from(optionalChains))

const flowNetwork = Object.entries(FLOW_CHAINS).find(
([, {eip155ChainId}]) => {
if (chainIds.includes(eip155ChainId)) {
return true
}
return false
}
)?.[0]
if (!flowNetwork) {
throw new Error(
`Unsupported chainId: ${chainIds.join(", ")}, expected one of ${Object.values(
FLOW_CHAINS
)
.map(({eip155ChainId}) => eip155ChainId)
.join(", ")}`
)
}

const {required, optional} = buildNamespaces(flowNetwork as FlowNetwork)
try {
const session = await new Promise<SessionTypes.Struct | undefined>(
async (resolve, reject) => {
if (this.rpc.showQrModal) {
this.modal?.subscribeModal((state: {open: boolean}) => {
// the modal was closed so reject the promise
if (!state.open && !this.signer.session) {
this.signer.abortPairingAttempt()
reject(new Error("Connection request reset. Please try again."))
}
})
}
await this.signer
.connect({
namespaces: required,
optionalNamespaces: optional,
pairingTopic: opts?.pairingTopic,
})
.then((session?: SessionTypes.Struct) => {
resolve(session)
})
.catch((error: unknown) => {
var newErr = new Error("Failed to connect")
if (error instanceof Error)
newErr.stack += "\nCaused by: " + error.stack
throw newErr
})
}
)
if (!session) return

const accounts = getAccountsFromNamespaces(session.namespaces, [
this.namespace,
])
// if no required chains are set, use the approved accounts to fetch chainIds
this.setChainIds(this.rpc.chains.length ? this.rpc.chains : accounts)
this.setAccounts(accounts)
this.events.emit("connect", {chainId: formatChainId(this.chainId)})
} catch (error) {
this.signer.logger.error(error)
throw error
} finally {
if (this.modal) this.modal.closeModal()
}
}
}

function buildNamespaces(network: FlowNetwork): {
required: NamespaceConfig
optional: NamespaceConfig
} {
const {eip155ChainId} = FLOW_CHAINS[network]

return {
required: {
eip155: {
methods: REQUIRED_METHODS,
chains: [`eip155:${eip155ChainId}`],
events: REQUIRED_EVENTS,
},
},
optional: {
eip155: {
methods: OPTIONAL_METHODS,
chains: [`eip155:${eip155ChainId}`],
events: OPTIONAL_EVENTS,
},
flow: {
methods: Object.values(FLOW_METHODS),
events: ["chainChanged", "accountsChanged"],
chains: [`flow:${network}`],
},
},
}
}
4 changes: 3 additions & 1 deletion packages/fcl-rainbowkit-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
"@wagmi/core": "^2.16.3",
"@walletconnect/jsonrpc-http-connection": "^1.0.8",
"@walletconnect/jsonrpc-provider": "^1.0.14",
"viem": "^2.22.21"
"mipd": "^0.0.7",
"viem": "^2.22.21",
"wagmi": "^2.14.11"
},
"peerDependencies": {
"@onflow/fcl": "^1.13.4",
Expand Down
Loading

0 comments on commit b010de2

Please sign in to comment.