-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
542 additions
and
291 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { config } from '@wagmi/test' | ||
import { expect, test } from 'vitest' | ||
|
||
import { ledger } from './ledger.js' | ||
|
||
/* | ||
* To manually test the Ledger connector: | ||
* | ||
* - install the Ledger Live app, https://www.ledger.com/ledger-live | ||
* - install the Ledger Connect extension, currently in beta, it should soon be | ||
* distributed with the Ledger Live app for iOS and macOS | ||
* - run the wagmi playground app following the contributing docs on the main | ||
* wagmi repository | ||
* - open the playground app in a web browser | ||
* - press the "Ledger" button | ||
* - see below for the Ledger Connect and Ledger Live flows | ||
* - after the account is selected on the wallet the dapp state should reflect | ||
* the chosen account information | ||
* | ||
* Ledger Connect | ||
* | ||
* - if you are on a platform supported by the Ledger Connect extension | ||
* (currently Safari on iOS and macOS) but don't have it installed or enabled | ||
* you should see a modal explaining how you can do that | ||
* - if you are on a platform supported by the Ledger Connect extension and | ||
* have it installed and enabled it should pop up allowing you to select an | ||
* account | ||
* - when pressing the "Disconnect" button on the dapp, the dapp shows as | ||
* disconnected but you are not actually disconnected until you press | ||
* the "Disconnect" button on Ledger Connect | ||
* - when pressing the "Disconnect" button on Ledger Connect (press the pill | ||
* shaped button with the Ledger logo, then "Disconnect" on the popup), the | ||
* dapp should also show as disconnected | ||
* | ||
* Testing Ledger Live | ||
* | ||
* - if you are on a platform not yet supported by the Connect extension you | ||
* should see a modal allowing you to use the Ledger Live app; pressing | ||
* "Use Ledger Live" or scanning the QR code should open the app and allow | ||
* you to choose an account | ||
* - when pressing the "Disconnect" button on the dapp, Ledger Live should | ||
* also show as disconnected | ||
* - when pressing the "Disconnect" button on Ledger Live, the dapp should | ||
* also show as disconnected | ||
* - when switching accounts on Ledger Live the dapp should reflect those | ||
* changes | ||
*/ | ||
|
||
test('setup', () => { | ||
const connectorFn = ledger() | ||
const connector = config._internal.setup(connectorFn) | ||
expect(connector.name).toEqual('Ledger') | ||
}) | ||
|
||
test.todo('behavior: connects') | ||
|
||
test.todo('behavior: disconnects via dapp (wagmi Connector)') | ||
|
||
test.todo('behavior: disconnects via wallet (Ledger Live)') | ||
|
||
test.todo('behavior: switch chains via dapp (wagmi Connector)') | ||
|
||
test.todo('behavior: switch chains via wallet (Ledger Live)') | ||
|
||
test.todo('behavior: switch accounts via wallet (Ledger Live)') | ||
|
||
test.todo('behavior: sends a transaction') | ||
|
||
test.todo('behavior: signs a message') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
import { | ||
type EthereumProvider, | ||
SupportedProviders, | ||
loadConnectKit, | ||
} from '@ledgerhq/connect-kit-loader' | ||
import { | ||
ChainNotConfiguredError, | ||
createConnector, | ||
normalizeChainId, | ||
} from '@wagmi/core' | ||
import { | ||
type ProviderConnectInfo, | ||
type ProviderRpcError, | ||
SwitchChainError, | ||
UserRejectedRequestError, | ||
getAddress, | ||
numberToHex, | ||
} from 'viem' | ||
|
||
export type LedgerParameters = { | ||
enableDebugLogs?: boolean | undefined | ||
optionalEvents?: string[] | undefined | ||
optionalMethods?: string[] | undefined | ||
projectId?: string | undefined | ||
requiredChains?: number[] | undefined | ||
requiredEvents?: string[] | undefined | ||
requiredMethods?: string[] | undefined | ||
} | ||
|
||
export function ledger(parameters: LedgerParameters = {}) { | ||
type Provider = EthereumProvider | ||
type Properties = { | ||
onConnect(connectInfo: ProviderConnectInfo): void | ||
onSessionDelete(data: { topic: string }): void | ||
} | ||
|
||
let provider_: Provider | undefined | ||
let providerPromise: Promise<typeof provider_> | ||
|
||
return createConnector<Provider, Properties>((config) => ({ | ||
id: 'ledger', | ||
name: 'Ledger', | ||
async setup() { | ||
const provider = await this.getProvider() | ||
if (!provider) return | ||
provider.on('connect', this.onConnect.bind(this)) | ||
provider.on('session_delete', this.onSessionDelete.bind(this)) | ||
}, | ||
async connect({ chainId } = {}) { | ||
try { | ||
const provider = await this.getProvider() | ||
|
||
// TODO: Update this logic to be more stable (a la WalletConnect connector) | ||
// Don't request accounts if we have a session, like when reloading with | ||
// an active WC v2 session | ||
if (!provider.session) | ||
await provider.request({ | ||
method: 'eth_requestAccounts', | ||
}) | ||
|
||
const accounts = await this.getAccounts() | ||
|
||
provider.removeListener('connect', this.onConnect.bind(this)) | ||
provider.on('accountsChanged', this.onAccountsChanged.bind(this)) | ||
provider.on('chainChanged', this.onChainChanged) | ||
provider.on('disconnect', this.onDisconnect.bind(this)) | ||
provider.on('session_delete', this.onSessionDelete.bind(this)) | ||
|
||
// Switch to chain if provided | ||
let currentChainId = await this.getChainId() | ||
if (chainId && currentChainId !== chainId) { | ||
const chain = await this.switchChain!({ chainId }).catch(() => ({ | ||
id: currentChainId, | ||
})) | ||
currentChainId = chain?.id ?? currentChainId | ||
} | ||
|
||
return { accounts, chainId: currentChainId } | ||
} catch (error) { | ||
if (/user rejected/i.test((error as ProviderRpcError)?.message)) | ||
throw new UserRejectedRequestError(error as Error) | ||
throw error | ||
} | ||
}, | ||
async disconnect() { | ||
const provider = await this.getProvider() | ||
try { | ||
await provider?.disconnect?.() | ||
} catch (error) { | ||
console.log({ error }) | ||
if (!/No matching key/i.test((error as Error).message)) throw error | ||
} finally { | ||
provider.removeListener( | ||
'accountsChanged', | ||
this.onAccountsChanged.bind(this), | ||
) | ||
provider.removeListener('chainChanged', this.onChainChanged) | ||
provider.removeListener('disconnect', this.onDisconnect.bind(this)) | ||
provider.removeListener( | ||
'session_delete', | ||
this.onSessionDelete.bind(this), | ||
) | ||
provider.on('connect', this.onConnect.bind(this)) | ||
} | ||
}, | ||
async getAccounts() { | ||
const provider = await this.getProvider() | ||
return (await provider.request<string[]>({ method: 'eth_accounts' })).map( | ||
getAddress, | ||
) | ||
}, | ||
async getProvider({ chainId } = {}) { | ||
async function initProvider() { | ||
const connectKit = await loadConnectKit() | ||
if (parameters.enableDebugLogs) connectKit.enableDebugLogs() | ||
|
||
const { | ||
optionalEvents, | ||
optionalMethods, | ||
projectId, | ||
requiredChains, | ||
requiredEvents, | ||
requiredMethods, | ||
} = parameters | ||
connectKit.checkSupport({ | ||
chains: requiredChains, | ||
events: requiredEvents, | ||
methods: requiredMethods, | ||
optionalChains: config.chains.map(({ id }) => id), | ||
optionalEvents, | ||
optionalMethods, | ||
projectId, | ||
providerType: SupportedProviders.Ethereum, | ||
rpcMap: Object.fromEntries( | ||
config.chains.map((chain) => [ | ||
chain.id, | ||
chain.rpcUrls.default.http[0]!, | ||
]), | ||
), | ||
walletConnectVersion: 2, | ||
}) | ||
|
||
return (await connectKit.getProvider()) as unknown as EthereumProvider | ||
} | ||
|
||
if (!provider_) { | ||
if (!providerPromise) providerPromise = initProvider() | ||
provider_ = await providerPromise | ||
} | ||
if (chainId) await this.switchChain!({ chainId }) | ||
return provider_! | ||
}, | ||
async getChainId() { | ||
const provider = await this.getProvider() | ||
const chainId = await provider.request({ method: 'eth_chainId' }) | ||
return normalizeChainId(chainId) | ||
}, | ||
async isAuthorized() { | ||
try { | ||
const accounts = await this.getAccounts() | ||
return !!accounts.length | ||
} catch { | ||
return false | ||
} | ||
}, | ||
async switchChain({ chainId }) { | ||
const chain = config.chains.find((chain) => chain.id === chainId) | ||
if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()) | ||
|
||
try { | ||
const provider = await this.getProvider() | ||
await provider.request({ | ||
method: 'wallet_switchEthereumChain', | ||
params: [{ chainId: numberToHex(chainId) }], | ||
}) | ||
return chain | ||
} catch (error) { | ||
const message = | ||
typeof error === 'string' | ||
? error | ||
: (error as ProviderRpcError)?.message | ||
if (/user rejected request/i.test(message)) | ||
throw new UserRejectedRequestError(error as Error) | ||
|
||
throw new SwitchChainError(error as Error) | ||
} | ||
}, | ||
onAccountsChanged(accounts) { | ||
if (accounts.length === 0) config.emitter.emit('disconnect') | ||
else config.emitter.emit('change', { accounts: accounts.map(getAddress) }) | ||
}, | ||
onChainChanged(chain) { | ||
const chainId = normalizeChainId(chain) | ||
config.emitter.emit('change', { chainId }) | ||
}, | ||
async onConnect(connectInfo) { | ||
const chainId = normalizeChainId(connectInfo.chainId) | ||
const accounts = await this.getAccounts() | ||
config.emitter.emit('connect', { accounts, chainId }) | ||
}, | ||
async onDisconnect(_error) { | ||
config.emitter.emit('disconnect') | ||
|
||
const provider = await this.getProvider() | ||
provider.removeListener('accountsChanged', this.onAccountsChanged) | ||
provider.removeListener('chainChanged', this.onChainChanged) | ||
provider.removeListener('disconnect', this.onDisconnect.bind(this)) | ||
}, | ||
onSessionDelete() { | ||
this.onDisconnect() | ||
}, | ||
})) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { config } from '@wagmi/test' | ||
import { expect, test } from 'vitest' | ||
|
||
import { safe } from './safe.js' | ||
|
||
/* | ||
* To manually test the Safe connector: | ||
* | ||
* 1. Run the wagmi playground app (`pnpm dev`) | ||
* 2. Add a custom Safe App with App URL set to `http://localhost:5173` (make sure there is a `manifest.json` file served by the playground) | ||
* 3. Open the playground app at `https://app.safe.global/eth:0x4557B18E779944BFE9d78A672452331C186a9f48/apps?appUrl=http%3A%2F%2Flocalhost%3A5173` | ||
* | ||
* See https://docs.gnosis-safe.io/learn/safe-tools/sdks/safe-apps/releasing-your-safe-app for more info. | ||
*/ | ||
|
||
test('setup', () => { | ||
const connectorFn = safe({ | ||
allowedDomains: [/gnosis-safe.io$/, /app.safe.global$/], | ||
debug: false, | ||
}) | ||
const connector = config._internal.setup(connectorFn) | ||
expect(connector.name).toEqual('Safe') | ||
}) |
Oops, something went wrong.
b18a2c4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
wagmi-v2 – ./docs
wagmi-v2-wagmi-dev.vercel.app
wagmi-v2-git-alpha-wagmi-dev.vercel.app
wagmi-v2.vercel.app
alpha.wagmi.sh