Skip to content

Commit dc953e7

Browse files
Ecosystem wallet connector (#271)
* Ecosystem wallet connector (WIP) * Update * Option for ecosystem wallet icon width * Minor style updates * Fix connect component email check * Remove unnecessary prop * Aux data support * Update type * Refactor * Handle account mismatch error * beta version * Merge fixes * Update logo in config * WIP * Update example * beta version * Update beta version * Update initial versions * Remove unnecessary params of ecosystem DefaultWaasConnectorOptions * Beta version "5.0.1-ecosystem-beta.0" * Update initial version in pre * Remove beta version related code/config * Move common function normalizeChainId to utils * Remove other ecosystem beta version changes * Replace isDev with nodesUrl option * Add isBrowser checks
1 parent c5979c2 commit dc953e7

File tree

12 files changed

+604
-30
lines changed

12 files changed

+604
-30
lines changed

packages/connect/src/components/Connect/Connect.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export const Connect = (props: ConnectProps) => {
220220
const walletConnectors = [...baseWalletConnectors, ...injectedConnectors].filter(c =>
221221
hasConnectedSequenceUniversal ? c.name !== SEQUENCE_UNIVERSAL_CONNECTOR_NAME : true
222222
)
223-
const emailConnector = (connectors as ExtendedConnector[]).find(c => c._wallet.id.includes('email'))
223+
const emailConnector = (connectors as ExtendedConnector[]).find(c => c._wallet?.id.includes('email'))
224224

225225
const onChangeEmail: React.ChangeEventHandler<HTMLInputElement> = ev => {
226226
setEmail(ev.target.value)
@@ -353,7 +353,7 @@ export const Connect = (props: ConnectProps) => {
353353
<div
354354
className="flex flex-col justify-center text-primary items-center font-medium"
355355
style={{
356-
marginTop: '4px'
356+
marginTop: '2px'
357357
}}
358358
>
359359
<TitleWrapper isPreview={isPreview}>
@@ -362,7 +362,7 @@ export const Connect = (props: ConnectProps) => {
362362
? `Connecting...`
363363
: hasConnectedSocialOrSequenceUniversal
364364
? 'Wallets'
365-
: `Sign in ${projectName ? `to ${projectName}` : ''}`}
365+
: `Connect ${projectName ? `to ${projectName}` : ''}`}
366366
</Text>
367367
</TitleWrapper>
368368

packages/connect/src/components/ConnectButton/ConnectButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const ConnectButton = (props: ConnectButtonProps) => {
3434

3535
if (isDescriptive) {
3636
return (
37-
<Tooltip message={label || walletProps.name}>
37+
<Tooltip message={label || walletProps.name} side="bottom">
3838
<Card
3939
className="flex gap-1 justify-center items-center w-full"
4040
clickable

packages/connect/src/config/defaultConnectors.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CreateConnectorFn } from 'wagmi'
33
import { apple } from '../connectors/apple'
44
import { appleWaas } from '../connectors/apple/appleWaas'
55
import { coinbaseWallet } from '../connectors/coinbaseWallet'
6+
import { ecosystemWallet, type EcosystemWalletOptions } from '../connectors/ecosystem'
67
import { email } from '../connectors/email'
78
import { emailWaas } from '../connectors/email/emailWaas'
89
import { facebook } from '../connectors/facebook'
@@ -38,6 +39,7 @@ export interface DefaultWaasConnectorOptions extends CommonConnectorOptions {
3839
clientId: string
3940
redirectURI: string
4041
}
42+
ecosystem?: false | Omit<EcosystemWalletOptions, 'projectAccessKey' | 'defaultNetwork'>
4143
coinbase?: boolean
4244
metaMask?: boolean
4345
walletConnect?:
@@ -76,6 +78,7 @@ export interface DefaultUniversalConnectorOptions extends CommonConnectorOptions
7678
apple?: boolean
7779
coinbase?: boolean
7880
metaMask?: boolean
81+
ecosystem?: false | Omit<EcosystemWalletOptions, 'projectAccessKey' | 'defaultNetwork'>
7982
walletConnect?:
8083
| false
8184
| {
@@ -146,6 +149,16 @@ export const getDefaultWaasConnectors = (options: DefaultWaasConnectorOptions):
146149
)
147150
}
148151

152+
if (options.ecosystem) {
153+
wallets.push(
154+
ecosystemWallet({
155+
...options.ecosystem,
156+
projectAccessKey,
157+
defaultNetwork: defaultChainId ?? 1
158+
})
159+
)
160+
}
161+
149162
if (options.metaMask !== false) {
150163
if (typeof window !== 'undefined') {
151164
wallets.push(
@@ -253,6 +266,16 @@ export const getDefaultUniversalConnectors = (options: DefaultUniversalConnector
253266
)
254267
}
255268

269+
if (options.ecosystem) {
270+
wallets.push(
271+
ecosystemWallet({
272+
...options.ecosystem,
273+
projectAccessKey,
274+
defaultNetwork: defaultChainId ?? 1
275+
})
276+
)
277+
}
278+
256279
if (options.metaMask !== false) {
257280
if (typeof window !== 'undefined') {
258281
wallets.push(
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { ethers } from 'ethers'
2+
import { getAddress } from 'viem'
3+
import { createConnector, Connector } from 'wagmi'
4+
5+
import { normalizeChainId } from '../../utils/helpers'
6+
7+
import { EcosystemWalletTransportProvider } from './provider'
8+
9+
export interface EcosystemConnector extends Connector {
10+
type: 'ecosystem-wallet'
11+
auxData?: Record<string, unknown>
12+
}
13+
14+
export interface BaseEcosystemConnectorOptions {
15+
projectAccessKey: string
16+
walletUrl: string
17+
defaultNetwork: number
18+
nodesUrl?: string
19+
}
20+
21+
ecosystemWallet.type = 'ecosystem-wallet' as const
22+
23+
export function ecosystemWallet(params: BaseEcosystemConnectorOptions) {
24+
type Provider = EcosystemWalletTransportProvider
25+
type Properties = {
26+
ecosystemProvider: EcosystemWalletTransportProvider
27+
auxData?: Record<string, unknown>
28+
}
29+
type StorageItem = {}
30+
31+
const nodesUrl = params.nodesUrl ?? 'https://nodes.sequence.app'
32+
33+
const ecosystemProvider = new EcosystemWalletTransportProvider(
34+
params.projectAccessKey,
35+
params.walletUrl,
36+
params.defaultNetwork,
37+
nodesUrl
38+
)
39+
40+
return createConnector<Provider, Properties, StorageItem>(config => ({
41+
id: `ecosystem-wallet`,
42+
name: 'Ecosystem Wallet',
43+
type: ecosystemWallet.type,
44+
ecosystemProvider,
45+
params,
46+
auxData: undefined as Record<string, unknown> | undefined,
47+
48+
async setup() {
49+
if (typeof window !== 'object') {
50+
// (for SSR) only run in browser client
51+
return
52+
}
53+
},
54+
55+
async connect(_connectInfo) {
56+
const provider = await this.getProvider()
57+
let walletAddress = provider.transport.getWalletAddress()
58+
59+
if (!walletAddress) {
60+
try {
61+
const res = await provider.transport.connect(this.auxData)
62+
walletAddress = res.walletAddress
63+
} catch (e) {
64+
console.log(e)
65+
await this.disconnect()
66+
throw e
67+
}
68+
}
69+
70+
return {
71+
accounts: [getAddress(walletAddress)],
72+
chainId: await this.getChainId()
73+
}
74+
},
75+
76+
async disconnect() {
77+
const provider = await this.getProvider()
78+
provider.transport.disconnect()
79+
},
80+
81+
async getAccounts() {
82+
const provider = await this.getProvider()
83+
const address = provider.transport.getWalletAddress()
84+
85+
if (address) {
86+
return [getAddress(address)]
87+
}
88+
89+
return []
90+
},
91+
92+
async getProvider(): Promise<EcosystemWalletTransportProvider> {
93+
return ecosystemProvider
94+
},
95+
96+
async isAuthorized() {
97+
const provider = await this.getProvider()
98+
return provider.transport.getWalletAddress() !== undefined
99+
},
100+
101+
async switchChain({ chainId }) {
102+
const provider = await this.getProvider()
103+
const chain = config.chains.find(c => c.id === chainId) || config.chains[0]
104+
105+
await provider.request({
106+
method: 'wallet_switchEthereumChain',
107+
params: [{ chainId: ethers.toQuantity(chainId) }]
108+
})
109+
110+
config.emitter.emit('change', { chainId })
111+
112+
return chain
113+
},
114+
115+
async getChainId() {
116+
const provider = await this.getProvider()
117+
return Number(provider.getChainId())
118+
},
119+
120+
async onAccountsChanged(accounts) {
121+
return { account: accounts[0] }
122+
},
123+
124+
async onChainChanged(chain) {
125+
config.emitter.emit('change', { chainId: normalizeChainId(chain) })
126+
},
127+
128+
async onConnect(_connectInfo) {},
129+
130+
async onDisconnect() {
131+
await this.disconnect()
132+
}
133+
}))
134+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Wallet, WalletProperties } from '../../types'
2+
3+
import { ecosystemWallet as baseEcosystemWallet, BaseEcosystemConnectorOptions } from './ecosystemWallet'
4+
5+
export type EcosystemWalletOptions = BaseEcosystemConnectorOptions &
6+
Pick<WalletProperties, 'logoDark' | 'logoLight'> & {
7+
name?: string
8+
}
9+
10+
export const ecosystemWallet = (options: EcosystemWalletOptions): Wallet => ({
11+
id: 'ecosystem-wallet',
12+
logoDark: options.logoDark,
13+
logoLight: options.logoLight,
14+
name: options.name || 'Ecosystem Wallet',
15+
type: 'social',
16+
createConnector: () => baseEcosystemWallet(options)
17+
})
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { EIP1193Provider, allNetworks } from '@0xsequence/network'
2+
import { ethers } from 'ethers'
3+
import { getAddress, TransactionRejectedRpcError } from 'viem'
4+
5+
import { normalizeChainId } from '../../utils/helpers'
6+
7+
import { ProviderTransport } from './providerTransport'
8+
9+
export class EcosystemWalletTransportProvider extends ethers.AbstractProvider implements EIP1193Provider {
10+
jsonRpcProvider: ethers.JsonRpcProvider
11+
currentNetwork: ethers.Network
12+
transport: ProviderTransport
13+
14+
constructor(
15+
public projectAccessKey: string,
16+
public walletUrl: string,
17+
public initialChainId: number,
18+
public nodesUrl: string
19+
) {
20+
super(initialChainId)
21+
22+
const initialChainName = allNetworks.find(n => n.chainId === initialChainId)?.name
23+
const initialJsonRpcProvider = new ethers.JsonRpcProvider(`${nodesUrl}/${initialChainName}/${projectAccessKey}`)
24+
25+
this.transport = new ProviderTransport(walletUrl)
26+
this.jsonRpcProvider = initialJsonRpcProvider
27+
this.currentNetwork = ethers.Network.from(initialChainId)
28+
}
29+
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
async request({ method, params }: { method: string; params?: any[] }) {
32+
if (method === 'wallet_switchEthereumChain') {
33+
const chainId = normalizeChainId(params?.[0].chainId)
34+
35+
const networkName = allNetworks.find(n => n.chainId === chainId)?.name
36+
const jsonRpcProvider = new ethers.JsonRpcProvider(`${this.nodesUrl}/${networkName}/${this.projectAccessKey}`)
37+
38+
this.jsonRpcProvider = jsonRpcProvider
39+
this.currentNetwork = ethers.Network.from(chainId)
40+
41+
return null
42+
}
43+
44+
if (method === 'eth_chainId') {
45+
return ethers.toQuantity(this.currentNetwork.chainId)
46+
}
47+
48+
if (method === 'eth_accounts') {
49+
const address = this.transport.getWalletAddress()
50+
if (!address) {
51+
return []
52+
}
53+
const account = getAddress(address)
54+
return [account]
55+
}
56+
57+
if (method === 'eth_sendTransaction') {
58+
if (!params) {
59+
throw new Error('No params')
60+
}
61+
62+
try {
63+
const response = await this.transport.sendRequest(method, params, this.getChainId())
64+
65+
if (response.walletAddress && response.walletAddress !== this.transport.getWalletAddress()) {
66+
this.transport.disconnect()
67+
throw new TransactionRejectedRpcError(new Error('Wallet address mismatch, please reconnect.'))
68+
}
69+
70+
if (response.code === 'transactionFailed') {
71+
throw new TransactionRejectedRpcError(new Error(`Unable to send transaction: ${response.data.error}`))
72+
}
73+
74+
if (response.code === 'transactionReceipt') {
75+
const { txHash } = response.data
76+
return txHash
77+
}
78+
} catch (e) {
79+
console.log('error in sendTransaction', e)
80+
throw new TransactionRejectedRpcError(new Error(`Unable to send transaction: ${e}`))
81+
}
82+
}
83+
84+
if (
85+
method === 'eth_sign' ||
86+
method === 'eth_signTypedData' ||
87+
method === 'eth_signTypedData_v4' ||
88+
method === 'personal_sign'
89+
) {
90+
if (!params) {
91+
throw new Error('No params')
92+
}
93+
try {
94+
const response = await this.transport.sendRequest(method, params, this.getChainId())
95+
96+
if (response.walletAddress && response.walletAddress !== this.transport.getWalletAddress()) {
97+
this.transport.disconnect()
98+
throw new TransactionRejectedRpcError(new Error('Wallet address mismatch, please reconnect.'))
99+
}
100+
101+
return response.data.signature
102+
} catch (e) {
103+
console.log('error in sign', e)
104+
throw new TransactionRejectedRpcError(new Error(`Unable to sign: ${e}`))
105+
}
106+
}
107+
108+
return await this.jsonRpcProvider.send(method, params ?? [])
109+
}
110+
111+
async getTransaction(txHash: string) {
112+
return await this.jsonRpcProvider.getTransaction(txHash)
113+
}
114+
115+
detectNetwork(): Promise<ethers.Network> {
116+
return Promise.resolve(this.currentNetwork)
117+
}
118+
119+
getChainId() {
120+
return Number(this.currentNetwork.chainId)
121+
}
122+
}

0 commit comments

Comments
 (0)