-
Notifications
You must be signed in to change notification settings - Fork 0
feat: ledger live support #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
b5e61c0 to
81d88d3
Compare
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.
Pull request overview
Adds Ledger Live (embedded dApp browser) wallet support by abstracting signing behind a Signer interface and adapting the app’s wallet/runtime flow to work with either a browser wallet (Reown/AppKit + wagmi) or Ledger Live APIs.
Changes:
- Introduces
Signerabstraction with separate Browser and Ledger implementations and updatesWalletServiceto use it. - Updates wallet/domain models and UI/atoms to support multi-account switching and to key portfolio/token state by wallet address.
- Adds Ledger Live manifest + dependency updates and refines a few dialog trigger patterns and numeric formatting.
Reviewed changes
Copilot reviewed 40 out of 42 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/wallet/wallet-service.ts | Refactors signing flow to use Signer and emits wallet state from signer account stream. |
| src/services/wallet/signer.ts | Adds Signer context contract (accounts stream, sign tx, switch account). |
| src/services/wallet/ledger-signer/utils.ts | Ledger Live helper utilities (accounts/currencies filtering + embed detection). |
| src/services/wallet/ledger-signer/index.ts | Implements Ledger Live signer layer using @ledgerhq/wallet-api-client. |
| src/services/wallet/browser-signer.ts | Implements browser signer layer with AppKit + wagmi actions. |
| src/services/runtime.ts | Chooses signer implementation at runtime and wires layers together. |
| src/services/config.ts | Makes VITE_REOWN_PROJECT_ID optional via Config.option. |
| src/lib/utils.ts | Adds truncateNumber helper to avoid rounding up in UI amounts. |
| src/domain/wallet.ts | Updates wallet/account domain types for browser vs ledger and adds switchAccount. |
| src/domain/transactions.ts | Reworks transaction schemas and adds TransactionHash branding. |
| src/domain/chains/ledger.ts | Adds Ledger family/currency mapping for supported EVM chains. |
| src/domain/chains/evm.ts | Narrows supported EVM chain list used by the app. |
| src/context/appkit.tsx | Gates wagmi provider usage based on wallet type. |
| src/components/molecules/tp-sl-dialog.tsx | Adjusts dialog trigger usage. |
| src/components/molecules/address-switcher.tsx | Replaces AppKit account UI with in-app account switch + copy/disconnect actions. |
| src/components/molecules/account-value-display.tsx | Keys provider-balance selection by wallet address. |
| src/components/modules/Root/PreloadAtoms.tsx | Preloads connected-state atoms using wallet address keys. |
| src/components/modules/PositionDetails/Overview/index.tsx | Keys positions atom by wallet address. |
| src/components/modules/PositionDetails/Overview/Position/index.tsx | Keys positions/orders atoms by wallet address. |
| src/components/modules/PositionDetails/Overview/Orders/index.tsx | Updates cancel order flow to pass wallet address. |
| src/components/modules/PositionDetails/Close/state.tsx | Refactors close-position state to use wallet address (not wallet object). |
| src/components/modules/PositionDetails/Close/index.tsx | Updates hook usage to pass wallet address. |
| src/components/modules/Order/Overview/state.tsx | Uses wallet address for balance atoms; switches to truncateNumber. |
| src/components/modules/Order/Overview/order-type-dialog.tsx | Adjusts dialog trigger usage. |
| src/components/modules/Order/Overview/limit-price-dialog.tsx | Adjusts dialog trigger usage. |
| src/components/modules/Order/Overview/leverage-dialog.tsx | Adjusts dialog trigger usage. |
| src/components/modules/Home/index.tsx | Adjusts connect/address-switcher behavior for browser vs ledger wallet types. |
| src/components/modules/Home/Positions/index.tsx | Keys portfolio atoms by wallet address. |
| src/components/modules/Account/balance.tsx | Keys providers balances atom by wallet address. |
| src/components/modules/Account/Withdraw/state.tsx | Keys selected balances by address; switches to truncateNumber. |
| src/components/modules/Account/Deposit/state.tsx | Refactors deposit atoms/hooks to be keyed by wallet address; uses truncateNumber. |
| src/components/modules/Account/Deposit/index.tsx | Updates deposit UI hooks to pass wallet address. |
| src/atoms/wallet-atom.ts | Simplifies wallet atom to consume wallet stream and adds switchAccount atom. |
| src/atoms/tokens-atoms.ts | Updates Moralis token balances atom to be keyed by wallet address type. |
| src/atoms/position-pending-actions-atom.ts | Updates pending position action atoms to accept wallet address. |
| src/atoms/portfolio-atoms.ts | Refactors portfolio atoms to be keyed by wallet address (not wallet object). |
| src/atoms/orders-pending-actions-atom.ts | Updates cancel order atom to accept wallet address. |
| src/atoms/actions-atoms.ts | Refactors signing machine atoms and changes post-action invalidation behavior. |
| pnpm-lock.yaml | Adds Ledger Wallet API Client dependency tree. |
| package.json | Adds @ledgerhq/wallet-api-client. |
| ledger-manifest/manifest.dev.json | Adds Ledger Live dApp manifest for local dev embedding. |
| .gitignore | Ignores opensrc/ directory. |
| src/components/molecules/tp-sl-dialog.tsx | Adjusts trigger rendering to children-based usage. |
| src/components/molecules/address-switcher.tsx | Adds account switching UI and disconnect/copy actions. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)
src/services/wallet/wallet-service.ts:29
WalletServicedeclaresdependencies: [ApiClientService.Default], but the runtime layer (src/services/runtime.ts) also mergesApiClientService.Default. This is redundant and makes it unclear which layer composition is intended.
For clarity, prefer one approach: either (a) remove the dependency here and provide ApiClientService.Default from runtime.ts, or (b) keep the dependency here and stop merging ApiClientService.Default separately in runtime.ts.
src/context/appkit.tsx:26
QueryClientProvideris only rendered whenwallet.type === "browser". In ledger mode (or any non-browser wallet),childrenis returned without a React Query provider, which will break any@tanstack/react-queryhooks used in the app.
Consider always wrapping children with QueryClientProvider, and only conditionally wrapping with WagmiProvider when a browser wallet (wagmi config) is available.
if (Result.isSuccess(result) && result.value.wallet.type === "browser") {
return (
<WagmiProvider config={result.value.wallet.wagmiAdapter.wagmiConfig}>
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
</WagmiProvider>
);
}
return children;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const switchAccount = Effect.fn( | ||
| function* ({ account }: { account: WalletAccount }) { | ||
| const connection = yield* Option.fromNullable( | ||
| wagmiAdapter.wagmiConfig.state.connections.get(account.id), | ||
| ); | ||
|
|
||
| yield* Effect.tryPromise(() => | ||
| switchConnection(wagmiAdapter.wagmiConfig, { | ||
| connector: connection.connector, | ||
| }), | ||
| ); |
Copilot
AI
Jan 23, 2026
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.
switchAccount looks up wagmiConfig.state.connections using account.id, but accountsStateRef sets WalletAccount.id to the account address (from connection.value.accounts). connections is keyed by currentConnectionId (see nextState.current), so this lookup will always miss and account switching will fail.
Store the connection id alongside the account (or map from selected address back to the active connection) and use that key when calling switchConnection.
| Stream.onDone(() => | ||
| Reactivity.pipe( | ||
| Effect.andThen((reactivity) => | ||
| reactivity.invalidate(portfolioReactivityKeysArray), | ||
| ), | ||
| ), | ||
| Stream.runForEach((val) => | ||
| Effect.sync(() => ctx.setSelf(Result.success(val))), | ||
| ), | ||
| Effect.runFork, | ||
| ); | ||
|
|
||
| return state; | ||
| }), | ||
| ), | ||
| ), |
Copilot
AI
Jan 23, 2026
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.
After signing completes, the code now invalidates only portfolioReactivityKeysArray. Actions include fund/withdraw (see ActionDtoAction in src/services/api-client/api-schemas.ts), which also affect token balances (Moralis) and token pricing state.
Re-add invalidation for tokensReactivityKeysArray (and any other relevant reactivity keys) so UI balances refresh immediately after these actions.
| export const isBrowserWallet = ( | ||
| wallet: Wallet | null, | ||
| ): wallet is WalletConnected & BrowserWallet => wallet?.type === "browser"; |
Copilot
AI
Jan 23, 2026
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.
isBrowserWallet only checks wallet?.type === "browser" but its return type narrows to WalletConnected & BrowserWallet. This is unsound: a disconnected browser wallet will satisfy the predicate.
Either (a) also check wallet?.status === "connected", or (b) widen the return type to include disconnected browser wallets (e.g. WalletBase & BrowserWallet).
| export const BrowserSignerLayer = Effect.gen(function* () { | ||
| const projectId = yield* ConfigService.pipe( | ||
| Effect.andThen((config) => config.reownProjectId), | ||
| ); | ||
|
|
||
| const networks: NonEmptyArray< | ||
| AppKitNetwork & { skChainName: SupportedSKChains } | ||
| > = [ | ||
| { ...mainnet, skChainName: EvmNetworks.Ethereum }, | ||
| { ...base, skChainName: EvmNetworks.Base }, | ||
| { ...arbitrum, skChainName: EvmNetworks.Arbitrum }, | ||
| { ...optimism, skChainName: EvmNetworks.Optimism }, | ||
| { ...monad, skChainName: EvmNetworks.Monad }, | ||
| { ...hyperLiquidL1, skChainName: EvmNetworks.HyperEVM }, | ||
| ]; | ||
|
|
||
| const chainsMap = Record.fromIterableBy(networks, (network) => | ||
| network.id.toString(), | ||
| ); | ||
|
|
||
| const wagmiAdapter = new WagmiAdapter({ | ||
| networks, | ||
| projectId, | ||
| multiInjectedProviderDiscovery: true, |
Copilot
AI
Jan 23, 2026
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.
ConfigService.reownProjectId is now optional (Config.option), but BrowserSignerLayer uses it directly as projectId for WagmiAdapter/createAppKit. This will pass an Option/optional value where a string is expected and can break wallet initialization.
Unwrap/validate the project id here (fail fast with a clear error when missing), or keep VITE_REOWN_PROJECT_ID required for the browser signer path.
| export const isLedgerDappBrowserProvider = Option.fromNullable(window).pipe( | ||
| Option.map((w) => { | ||
| try { | ||
| const params = new URLSearchParams(w.self.location.search); | ||
|
|
||
| return !!params.get("embed"); | ||
| } catch (_error) { | ||
| return false; | ||
| } | ||
| }), | ||
| Option.getOrElse(() => false), | ||
| ); |
Copilot
AI
Jan 23, 2026
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.
isLedgerDappBrowserProvider references window at module top-level. In non-browser contexts (SSR, tests, build tooling) this can throw a ReferenceError during import.
Guard with typeof window !== "undefined" (or compute lazily inside a function) before accessing window/location.
| export const EvmTx = Schema.Struct({ | ||
| from: Schema.String, | ||
| to: HexString, | ||
| data: HexString, |
Copilot
AI
Jan 23, 2026
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.
EvmTx is now an exact Schema.Struct with a fixed set of fields and no index signature. However the backend exposes TransactionDto.signablePayload as Record<string, unknown> (see src/services/api-client/api-schemas.ts:572), so additional transaction fields will cause decoding to fail and the signing flow will die.
Consider restoring an index signature (allow unknown extra props) or modeling the actual payload shape more permissively.
| value: Schema.optional(Schema.BigInt), | ||
| maxFeePerGas: Schema.optional(Schema.BigInt), | ||
| maxPriorityFeePerGas: Schema.optional(Schema.BigInt), | ||
| gasLimit: Schema.BigInt, | ||
| nonce: Schema.Number, |
Copilot
AI
Jan 23, 2026
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.
EvmTx uses Schema.BigInt for gasLimit/fees/value. Since signablePayload comes from the API as JSON (Record<string, unknown>), these numeric fields will typically arrive as strings or numbers (JSON can’t encode bigint).
Ensure the schema decodes from the JSON representation (e.g., BigInt-from-string/number), otherwise Schema.decodeUnknown(Transaction) may reject valid payloads.
666e04a to
dba2e56
Compare
dba2e56 to
52fbba6
Compare
52fbba6 to
2ca5700
Compare
2ca5700 to
d1f555f
Compare
No description provided.