diff --git a/src/atoms/wallet-atom.ts b/src/atoms/wallet-atom.ts index d3a9fb3..02ce412 100644 --- a/src/atoms/wallet-atom.ts +++ b/src/atoms/wallet-atom.ts @@ -1,6 +1,9 @@ import { Atom } from "@effect-atom/atom-react"; import { Effect, Stream } from "effect"; -import type { WalletConnected } from "@/domain/wallet"; +import type { + BrowserWalletConnected, + LedgerWalletConnected, +} from "@/domain/wallet"; import { runtimeAtom } from "@/services/runtime"; import { WalletService } from "@/services/wallet/wallet-service"; @@ -14,7 +17,18 @@ export const walletAtom = runtimeAtom.atom( } if (a.status === "connected" && b.status === "connected") { - return a.currentAccount.id === b.currentAccount.id; + const addressMatch = + a.currentAccount.address === b.currentAccount.address; + + if (a.type === "ledger" && b.type === "ledger") { + const aAccount = a.currentAccount; + const bAccount = b.currentAccount; + return addressMatch && aAccount.id === bAccount.id; + } + + if (a.type === "browser") { + return addressMatch; + } } return true; @@ -22,6 +36,10 @@ export const walletAtom = runtimeAtom.atom( ), ); -export const switchAccountAtom = Atom.family((wallet: WalletConnected) => - runtimeAtom.fn(wallet.switchAccount), +export const switchLedgerAccountAtom = Atom.family( + (wallet: LedgerWalletConnected) => runtimeAtom.fn(wallet.switchAccount), +); + +export const switchBrowserAccountAtom = Atom.family( + (wallet: BrowserWalletConnected) => runtimeAtom.fn(wallet.switchAccount), ); diff --git a/src/components/modules/Account/Deposit/state.tsx b/src/components/modules/Account/Deposit/state.tsx index 89feb57..35bc4c6 100644 --- a/src/components/modules/Account/Deposit/state.tsx +++ b/src/components/modules/Account/Deposit/state.tsx @@ -11,6 +11,7 @@ import { Number as _Number, Effect, Option, + Predicate, Record, Schema, } from "effect"; @@ -22,6 +23,7 @@ import { } from "@/atoms/tokens-atoms"; import { walletAtom } from "@/atoms/wallet-atom"; import { AmountField } from "@/components/molecules/forms"; +import { isArbUsdcToken, isEthNativeToken, makeToken } from "@/domain/tokens"; import type { TokenBalance } from "@/domain/types"; import { isWalletConnected, @@ -147,8 +149,10 @@ export const depositFormBuilder = FormBuilder.empty const cryptoAmount = values.Amount / tokenBalance.price; - if (values.Amount < 10) { - return { path: ["Amount"], message: "Minimum deposit is $10" }; + const usdMin = isArbUsdcToken(makeToken(tokenBalance.token)) ? 5 : 10; + + if (values.Amount < usdMin) { + return { path: ["Amount"], message: `Minimum deposit is $${usdMin}` }; } if (Number(tokenBalance.amount) < cryptoAmount) { @@ -191,8 +195,7 @@ export const DepositForm = FormReact.make(depositFormBuilder, { amount: cryptoAmount.toString(), fromToken: { network: selectedTokenBalance.token.network, - ...(selectedTokenBalance.token.address !== - "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" && { + ...(!isEthNativeToken(makeToken(selectedTokenBalance.token)) && { address: selectedTokenBalance.token.address, }), }, @@ -217,19 +220,24 @@ export const useDepositPercentage = ( const setAmount = useAtomSet(setAmountFieldAtom); - const tokenBalance = useAtomValue( + const availableBalanceUsd = useAtomValue( selectedTokenBalanceAtom(walletAddress), - ).pipe(Result.getOrElse(() => null)); - - const availableBalanceUsd = tokenBalance - ? Number(tokenBalance.amount) * tokenBalance.price - : 0; + ).pipe( + Result.value, + Option.filter(Predicate.isNotNull), + Option.map((balance) => Number(balance.amount) * balance.price), + Option.getOrElse(() => 0), + ); const percentage = _Number.clamp({ minimum: 0, maximum: 100 })( (amount / availableBalanceUsd) * 100, ); const handlePercentageChange = (newPercentage: number) => { + if (newPercentage >= 100) { + return setAmount(availableBalanceUsd.toString()); + } + const amount = (availableBalanceUsd * newPercentage) / 100; setAmount(truncateNumber(amount).toString()); }; diff --git a/src/components/modules/Account/Withdraw/state.tsx b/src/components/modules/Account/Withdraw/state.tsx index e289fb7..1ff5fa1 100644 --- a/src/components/modules/Account/Withdraw/state.tsx +++ b/src/components/modules/Account/Withdraw/state.tsx @@ -174,15 +174,21 @@ export const useWithdrawPercentage = (wallet: WalletConnected) => { const setAmount = useAtomSet(setAmountFieldAtom); - const providerBalance = useAtomValue( + const availableBalance = useAtomValue( selectedProviderBalancesAtom(wallet.currentAccount.address), - ).pipe(Result.getOrElse(() => null)); - - const availableBalance = providerBalance?.availableBalance ?? 0; + ).pipe( + Result.value, + Option.map((v) => v.availableBalance), + Option.getOrElse(() => 0), + ); const handlePercentageChange = (newPercentage: number) => { + if (newPercentage >= 100) { + return setAmount(availableBalance.toString()); + } + const amount = (availableBalance * newPercentage) / 100; - setAmount(truncateNumber(amount).toString()); + setAmount(truncateNumber(amount, 6).toString()); }; const percentage = _Number.clamp({ minimum: 0, maximum: 100 })( diff --git a/src/components/molecules/address-switcher.tsx b/src/components/molecules/address-switcher.tsx index cdeb364..73518a2 100644 --- a/src/components/molecules/address-switcher.tsx +++ b/src/components/molecules/address-switcher.tsx @@ -1,92 +1,118 @@ import { useAtomSet } from "@effect-atom/atom-react"; import { useDisconnect } from "@reown/appkit/react"; +import { Match } from "effect"; import { Check, ChevronDown, Copy, LogOut, Wallet } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { switchAccountAtom } from "@/atoms/wallet-atom"; +import { + switchBrowserAccountAtom, + switchLedgerAccountAtom, +} from "@/atoms/wallet-atom"; import { Button } from "@/components/ui/button"; import { Dialog } from "@/components/ui/dialog"; -import { isBrowserWallet, type WalletConnected } from "@/domain/wallet"; +import { + type BrowserWalletConnected, + isBrowserWalletConnected, + isLedgerWalletConnected, + type LedgerWalletConnected, + type WalletConnected, +} from "@/domain/wallet"; import { cn, truncateAddress } from "@/lib/utils"; -export const AddressSwitcher = ({ wallet }: { wallet: WalletConnected }) => { - const [open, setOpen] = useState(false); - const switchAccount = useAtomSet(switchAccountAtom(wallet)); +const LedgerAccountList = ({ + wallet, + onAccountSwitch, +}: { + wallet: LedgerWalletConnected; + onAccountSwitch: () => void; +}) => { + const switchAccount = useAtomSet(switchLedgerAccountAtom(wallet)); - const handleAccountSwitch = (account: (typeof wallet.accounts)[number]) => { + const handleAccountSwitch = ( + account: LedgerWalletConnected["accounts"][number], + ) => { switchAccount({ account }); - setOpen(false); + onAccountSwitch(); }; - return ( - - ( - + return wallet.accounts.map((account) => { + const currentAccount = wallet.currentAccount; + const isCurrentAccount = + account.address === currentAccount.address && + account.id === currentAccount.id; + + return ( + handleAccountSwitch(account)} + className={cn( + "flex items-center justify-between p-3 rounded-xl transition-colors cursor-pointer", + "hover:bg-gray-5", + isCurrentAccount && "bg-gray-5", + )} + > + + - - {truncateAddress(wallet.currentAccount.address)} + + + + Account ID: {truncateAddress(account.id)} - - - )} - /> - - - - - - Switch Account - + + Address: {truncateAddress(account.address)} + + + + {isCurrentAccount && } + + ); + }); +}; - - {wallet.accounts.map((account) => { - const isCurrentAccount = - account.address === wallet.currentAccount.address && - account.id === wallet.currentAccount.id; - - return ( - handleAccountSwitch(account)} - className={cn( - "flex items-center justify-between p-3 rounded-xl transition-colors cursor-pointer", - "hover:bg-gray-5", - isCurrentAccount && "bg-gray-5", - )} - > - - - - - - {truncateAddress(account.address)} - - - {isCurrentAccount && ( - - )} - - ); - })} - +const BrowserAccountList = ({ + wallet, + onAccountSwitch, +}: { + wallet: BrowserWalletConnected; + onAccountSwitch: () => void; +}) => { + const switchAccount = useAtomSet(switchBrowserAccountAtom(wallet)); - setOpen(false)} - /> - - - - - ); + const handleAccountSwitch = ( + account: BrowserWalletConnected["accounts"][number], + ) => { + switchAccount({ account }); + onAccountSwitch(); + }; + + return wallet.accounts.map((account) => { + const isCurrentAccount = account.address === wallet.currentAccount.address; + + return ( + handleAccountSwitch(account)} + className={cn( + "flex items-center justify-between p-3 rounded-xl transition-colors cursor-pointer", + "hover:bg-gray-5", + isCurrentAccount && "bg-gray-5", + )} + > + + + + + + {truncateAddress(account.address)} + + + {isCurrentAccount && } + + ); + }); }; const CopyButton = ({ @@ -177,7 +203,7 @@ const WalletActions = ({ - {isBrowserWallet(wallet) && ( + {isBrowserWalletConnected(wallet) && ( )} @@ -199,3 +225,62 @@ const DisconnectButton = ({ onDisconnect }: { onDisconnect: () => void }) => { ); }; + +export const AddressSwitcher = ({ wallet }: { wallet: WalletConnected }) => { + const [open, setOpen] = useState(false); + + return ( + + ( + + + + {truncateAddress(wallet.currentAccount.address)} + + + + )} + /> + + + + + + + Switch Account + + + + {Match.value(wallet).pipe( + Match.when(isLedgerWalletConnected, (w) => ( + setOpen(false)} + /> + )), + Match.orElse((w) => ( + setOpen(false)} + /> + )), + )} + + + setOpen(false)} + /> + + + + + ); +}; diff --git a/src/domain/signer.ts b/src/domain/signer.ts new file mode 100644 index 0000000..3a01379 --- /dev/null +++ b/src/domain/signer.ts @@ -0,0 +1,69 @@ +import type { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; +import { Data, type Effect, Schema, type Stream } from "effect"; +import type { Transaction, TransactionHash } from "@/domain/transactions"; + +const BrowserWalletAccount = Schema.Struct({ + address: Schema.String.pipe(Schema.brand("WalletAccountAddress")), +}).pipe(Schema.attachPropertySignature("_kind", "browserWalletAccount")); + +const LedgerWalletAccount = Schema.Struct({ + id: Schema.String.pipe(Schema.brand("WalletAccountId")), + address: Schema.String.pipe(Schema.brand("WalletAccountAddress")), +}).pipe(Schema.attachPropertySignature("_kind", "ledgerWalletAccount")); + +export const makeBrowserWalletAccount = Schema.decodeSync(BrowserWalletAccount); +export const makeLedgerWalletAccount = Schema.decodeSync(LedgerWalletAccount); + +export type BrowserWalletAccount = typeof BrowserWalletAccount.Type; +export type LedgerWalletAccount = typeof LedgerWalletAccount.Type; + +export const WalletAccount = Schema.Union( + BrowserWalletAccount, + LedgerWalletAccount, +); + +export type WalletAccount = typeof WalletAccount.Type; + +export type AccountsState = + | { status: "disconnected" } + | { + status: "connected"; + currentAccount: T; + accounts: T[]; + }; + +type SignerCommon = { + signTransaction: (args: { + transaction: Transaction; + }) => Effect.Effect; + switchAccount: (args: { + account: T; + }) => Effect.Effect; + accountsStream: Stream.Stream>; + getAccountState: Effect.Effect>; +}; + +export type BrowserSigner = { + type: "browser"; + wagmiAdapter: WagmiAdapter; +} & SignerCommon; + +export type LedgerSigner = { + type: "ledger"; +} & SignerCommon; + +export type Signer = BrowserSigner | LedgerSigner; + +export class SwitchAccountError extends Data.TaggedError("SwitchAccountError")<{ + cause: unknown; +}> {} + +export class SignTransactionError extends Data.TaggedError( + "SignTransactionError", +)<{ + cause: unknown; +}> {} + +export class SwitchChainError extends Data.TaggedError("SwitchChainError")<{ + cause: unknown; +}> {} diff --git a/src/domain/tokens.ts b/src/domain/tokens.ts new file mode 100644 index 0000000..0f75b60 --- /dev/null +++ b/src/domain/tokens.ts @@ -0,0 +1,27 @@ +import { Equal, Schema } from "effect"; +import { Networks } from "@/services/api-client/api-schemas"; + +const Token = Schema.Struct({ + network: Networks, + address: Schema.optional(Schema.Lowercase), +}).pipe(Schema.Data, Schema.brand("Token")); + +export const makeToken = Schema.decodeSync(Token); + +export const arbUsdcToken = makeToken({ + network: "arbitrum", + address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", +}); + +export const ethNativeToken = makeToken({ + network: "ethereum", + address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", +}); + +export const tokenIsSame = Equal.equivalence(); + +export const isArbUsdcToken = (otherToken: typeof Token.Type) => + tokenIsSame(otherToken, arbUsdcToken); + +export const isEthNativeToken = (otherToken: typeof Token.Type) => + tokenIsSame(otherToken, ethNativeToken); diff --git a/src/domain/transactions.ts b/src/domain/transactions.ts index 7e99e93..b72475d 100644 --- a/src/domain/transactions.ts +++ b/src/domain/transactions.ts @@ -7,10 +7,7 @@ export const EvmTx = Schema.Struct({ to: HexString, data: HexString, value: Schema.optional(Schema.BigInt), - maxFeePerGas: Schema.optional(Schema.BigInt), - maxPriorityFeePerGas: Schema.optional(Schema.BigInt), gasLimit: Schema.BigInt, - nonce: Schema.Number, chainId: Schema.Number, }); diff --git a/src/domain/wallet.ts b/src/domain/wallet.ts index 60035d2..0c2425b 100644 --- a/src/domain/wallet.ts +++ b/src/domain/wallet.ts @@ -1,7 +1,16 @@ import type { HttpClientError } from "@effect/platform"; import type { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; -import { Data, type Effect, Schema, type Stream } from "effect"; +import { Data, type Effect, type Stream } from "effect"; import type { ParseError } from "effect/ParseResult"; +import type { + BrowserSigner, + BrowserWalletAccount, + LedgerSigner, + LedgerWalletAccount, + SignTransactionError, + SwitchAccountError, + WalletAccount, +} from "@/domain/signer"; import type { ActionDto, TransactionDto, @@ -42,28 +51,27 @@ export type SignTransactionsState = { } ); -export const WalletAccount = Schema.Struct({ - id: Schema.String, - address: Schema.String.pipe(Schema.brand("WalletAccountAddress")), -}); - -export type WalletAccount = typeof WalletAccount.Type; - -type BrowserWallet = { type: "browser"; wagmiAdapter: WagmiAdapter }; -type LedgerWallet = { type: "ledger" }; - -type WalletBase = BrowserWallet | LedgerWallet; +export type BrowserWalletDisconnected = { + type: BrowserSigner["type"]; + wagmiAdapter: WagmiAdapter; + status: "disconnected"; +}; -export type WalletDisconnected = WalletBase & { +export type LedgerWalletDisconnected = { + type: LedgerSigner["type"]; status: "disconnected"; }; -export type WalletConnected = WalletBase & { +export type WalletDisconnected = + | BrowserWalletDisconnected + | LedgerWalletDisconnected; + +type WalletConnectedCommon = { status: "connected"; - currentAccount: WalletAccount; - accounts: WalletAccount[]; + currentAccount: T; + accounts: T[]; switchAccount: (args: { - account: WalletAccount; + account: T; }) => Effect.Effect; signTransactions: (action: ActionDto) => Effect.Effect< { @@ -75,38 +83,39 @@ export type WalletConnected = WalletBase & { >; }; +// Connected wallet types +export type BrowserWalletConnected = Omit< + BrowserWalletDisconnected, + "status" +> & { status: "connected" } & WalletConnectedCommon; + +export type LedgerWalletConnected = Omit & { + status: "connected"; +} & WalletConnectedCommon; + +export type WalletConnected = BrowserWalletConnected | LedgerWalletConnected; + export type Wallet = WalletDisconnected | WalletConnected; export class DeserializeTransactionError extends Data.TaggedError( "DeserializeTransactionError", )<{ cause: unknown }> {} -export class SignTransactionError extends Data.TaggedError( - "SignTransactionError", -)<{ - cause: unknown; -}> {} - -export class WalletNotConnectedError extends Data.TaggedError( - "WalletNotConnectedError", -) {} - -export class ChainNotFoundError extends Data.TaggedError( - "ChainNotFoundError", -) {} - -export class SwitchChainError extends Data.TaggedError("SwitchChainError")<{ - cause: unknown; -}> {} - -export class SwitchAccountError extends Data.TaggedError("SwitchAccountError")<{ - cause: unknown; -}> {} - export const isWalletConnected = ( wallet: Wallet | null, ): wallet is WalletConnected => wallet?.status === "connected"; -export const isBrowserWallet = ( +export const isBrowserWallet = (wallet: Wallet | null) => + wallet?.type === "browser"; + +export const isBrowserWalletConnected = ( wallet: Wallet | null, -): wallet is WalletConnected & BrowserWallet => wallet?.type === "browser"; +): wallet is BrowserWalletConnected => + isWalletConnected(wallet) && wallet.type === "browser"; + +export const isLedgerWalletConnected = ( + wallet: Wallet | null, +): wallet is LedgerWalletConnected => + isWalletConnected(wallet) && wallet.type === "ledger"; + +export type { WalletAccount } from "@/domain/signer"; diff --git a/src/services/wallet/browser-signer.ts b/src/services/wallet/browser-signer.ts index 6619175..905c56b 100644 --- a/src/services/wallet/browser-signer.ts +++ b/src/services/wallet/browser-signer.ts @@ -22,19 +22,21 @@ import { switchChain as wagmiSwitchChain, } from "wagmi/actions"; import type { SupportedSKChains } from "@/domain/chains"; +import { + type AccountsState, + type BrowserWalletAccount, + makeBrowserWalletAccount, + SignTransactionError, + SwitchAccountError, + SwitchChainError, +} from "@/domain/signer"; import { EIP712Tx, type Transaction, TransactionHash, } from "@/domain/transactions"; -import { - SignTransactionError, - SwitchAccountError, - SwitchChainError, - WalletAccount, -} from "@/domain/wallet"; import { ConfigService } from "@/services/config"; -import { type AccountsState, Signer } from "@/services/wallet/signer"; +import { SignerService } from "@/services/wallet/signer"; const hyperLiquidL1 = defineChain({ id: 1337, @@ -128,7 +130,9 @@ export const BrowserSignerLayer = Effect.gen(function* () { }).pipe(Effect.andThen(Schema.decodeSync(TransactionHash))); }); - const accountsStateRef = yield* SubscriptionRef.make({ + const accountsStateRef = yield* SubscriptionRef.make< + AccountsState + >({ status: "disconnected", }); @@ -140,7 +144,7 @@ export const BrowserSignerLayer = Effect.gen(function* () { SubscriptionRef.update( accountsStateRef, - (prevWallet): AccountsState => { + (prevWallet): AccountsState => { const connection = Option.fromNullable(currentConnectionId).pipe( Option.flatMapNullable((connectionId) => nextState.connections.get(connectionId), @@ -168,13 +172,11 @@ export const BrowserSignerLayer = Effect.gen(function* () { return { status: "connected", - currentAccount: Schema.decodeSync(WalletAccount)({ - id: currentAccount.value, + currentAccount: makeBrowserWalletAccount({ address: currentAccount.value, }), accounts: connection.value.accounts.map((acc) => - Schema.decodeSync(WalletAccount)({ - id: acc, + makeBrowserWalletAccount({ address: acc, }), ), @@ -187,9 +189,9 @@ export const BrowserSignerLayer = Effect.gen(function* () { ); const switchAccount = Effect.fn( - function* ({ account }: { account: WalletAccount }) { + function* ({ account }: { account: BrowserWalletAccount }) { const connection = yield* Option.fromNullable( - wagmiAdapter.wagmiConfig.state.connections.get(account.id), + wagmiAdapter.wagmiConfig.state.connections.get(account.address), ); yield* Effect.tryPromise(() => @@ -201,7 +203,7 @@ export const BrowserSignerLayer = Effect.gen(function* () { Effect.mapError((e) => new SwitchAccountError({ cause: e })), ); - return Signer.of({ + return SignerService.of({ type: "browser", signTransaction, switchAccount, @@ -209,4 +211,4 @@ export const BrowserSignerLayer = Effect.gen(function* () { accountsStream: accountsStateRef.changes, getAccountState: accountsStateRef.get, }); -}).pipe(Layer.scoped(Signer)); +}).pipe(Layer.scoped(SignerService)); diff --git a/src/services/wallet/ledger-signer/index.ts b/src/services/wallet/ledger-signer/index.ts index 4a0ce5d..30c96d8 100644 --- a/src/services/wallet/ledger-signer/index.ts +++ b/src/services/wallet/ledger-signer/index.ts @@ -7,30 +7,31 @@ import { Array as _Array, Effect, Layer, - pipe, Schema, SubscriptionRef, } from "effect"; import { evmChainsMap } from "@/domain/chains/evm"; import type { SupportedLedgerLiveFamilies } from "@/domain/chains/ledger"; +import { + type AccountsState, + type LedgerWalletAccount, + makeLedgerWalletAccount, + SignTransactionError, + SwitchAccountError, +} from "@/domain/signer"; import { EIP712Tx, EvmTx, type Transaction, TransactionHash, } from "@/domain/transactions"; -import { - SignTransactionError, - SwitchAccountError, - WalletAccount, -} from "@/domain/wallet"; import { getFilteredSupportedLedgerFamiliesWithCurrency, getLedgerAccounts, getLedgerCurrencies, NoAccountsFoundError, } from "@/services/wallet/ledger-signer/utils"; -import { type AccountsState, Signer } from "@/services/wallet/signer"; +import { SignerService } from "@/services/wallet/signer"; export const LedgerSignerLayer = Effect.gen(function* () { const transport = new WindowMessageTransport(); @@ -54,8 +55,8 @@ export const LedgerSignerLayer = Effect.gen(function* () { enabledChainsMap: { evm: evmChainsMap }, }); - const uniqueByAddressAccounts = pipe( - parentAccounts.reduce((acc, next) => { + const uniqueByAddressAccounts = parentAccounts.reduce( + (acc, next) => { const family = ledgerCurrencies.get(next.currency); if (!family) return acc; @@ -70,7 +71,7 @@ export const LedgerSignerLayer = Effect.gen(function* () { if (chainItem) { acc.push( - Schema.decodeSync(WalletAccount)({ + makeLedgerWalletAccount({ address: next.address, id: next.id, }), @@ -78,15 +79,17 @@ export const LedgerSignerLayer = Effect.gen(function* () { } return acc; - }, []), - (val) => [...new Map(val.map((a) => [a.address, a])).values()], + }, + [], ); const accountWithChain = yield* _Array .head(uniqueByAddressAccounts) .pipe(Effect.mapError(() => new NoAccountsFoundError())); - const accountsStateRef = yield* SubscriptionRef.make({ + const accountsStateRef = yield* SubscriptionRef.make< + AccountsState + >({ status: "connected", currentAccount: accountWithChain, accounts: uniqueByAddressAccounts, @@ -96,7 +99,7 @@ export const LedgerSignerLayer = Effect.gen(function* () { account, transaction, }: { - account: WalletAccount; + account: LedgerWalletAccount; transaction: typeof EIP712Tx.Type; }) => Effect.try(() => Buffer.from(JSON.stringify(transaction))).pipe( @@ -113,7 +116,7 @@ export const LedgerSignerLayer = Effect.gen(function* () { account, transaction, }: { - account: WalletAccount; + account: LedgerWalletAccount; transaction: typeof EvmTx.Type; }) => Schema.encode(EvmTx)(transaction).pipe( @@ -138,19 +141,26 @@ export const LedgerSignerLayer = Effect.gen(function* () { ); const signTransaction = Effect.fn(function* ({ - account, transaction, }: { transaction: Transaction; - account: WalletAccount; }) { + const currentAccount = yield* accountsStateRef.get; + + if (currentAccount.status === "disconnected") { + return yield* Effect.dieMessage("Wallet is disconnected"); + } + return yield* Schema.is(EIP712Tx)(transaction) - ? signMessage({ account, transaction }) - : signEVMTransaction({ account, transaction }); + ? signMessage({ account: currentAccount.currentAccount, transaction }) + : signEVMTransaction({ + account: currentAccount.currentAccount, + transaction, + }); }); const switchAccount = Effect.fn( - function* ({ account }: { account: WalletAccount }) { + function* ({ account }: { account: LedgerWalletAccount }) { const newAccount = yield* _Array.findFirst( uniqueByAddressAccounts, (a) => a.id === account.id, @@ -164,7 +174,7 @@ export const LedgerSignerLayer = Effect.gen(function* () { Effect.mapError((e) => new SwitchAccountError({ cause: e })), ); - return Layer.succeed(Signer, { + return Layer.succeed(SignerService, { type: "ledger", signTransaction, switchAccount, diff --git a/src/services/wallet/signer.ts b/src/services/wallet/signer.ts index d61ae74..b616e39 100644 --- a/src/services/wallet/signer.ts +++ b/src/services/wallet/signer.ts @@ -1,39 +1,6 @@ -import type { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; -import { Context, type Effect, type Stream } from "effect"; -import type { Transaction, TransactionHash } from "@/domain/transactions"; -import type { - SignTransactionError, - SwitchAccountError, - WalletAccount, -} from "@/domain/wallet"; +import { Context } from "effect"; +import type { Signer } from "@/domain/signer"; -export type AccountsState = - | { status: "disconnected" } - | { - status: "connected"; - currentAccount: WalletAccount; - accounts: WalletAccount[]; - }; - -export class Signer extends Context.Tag("Signer")< - Signer, - { - signTransaction: (args: { - transaction: Transaction; - account: WalletAccount; - }) => Effect.Effect; - switchAccount: (args: { - account: WalletAccount; - }) => Effect.Effect; - accountsStream: Stream.Stream; - getAccountState: Effect.Effect; - } & ( - | { - type: "browser"; - wagmiAdapter: WagmiAdapter; - } - | { - type: "ledger"; - } - ) ->() {} +export class SignerService extends Context.Tag( + "perps/services/wallet/signer/SignerService", +)() {} diff --git a/src/services/wallet/wallet-service.ts b/src/services/wallet/wallet-service.ts index 80c8bfd..4d46f2a 100644 --- a/src/services/wallet/wallet-service.ts +++ b/src/services/wallet/wallet-service.ts @@ -9,6 +9,7 @@ import { Stream, SubscriptionRef, } from "effect"; +import type { BrowserSigner, LedgerSigner } from "@/domain/signer"; import { Transaction } from "@/domain/transactions"; import { type SignTransactionsState, @@ -18,7 +19,7 @@ import { } from "@/domain/wallet"; import { ApiClientService } from "@/services/api-client"; import type { ActionDto } from "@/services/api-client/api-schemas"; -import { type AccountsState, Signer } from "@/services/wallet/signer"; +import { SignerService } from "@/services/wallet/signer"; export class WalletService extends Effect.Service()( "perps/services/wallet-service/WalletService", @@ -26,7 +27,7 @@ export class WalletService extends Effect.Service()( dependencies: [ApiClientService.Default], scoped: Effect.gen(function* () { const apiClient = yield* ApiClientService; - const signer = yield* Signer; + const signer = yield* SignerService; type SignAction = Data.TaggedEnum<{ MachineStart: {}; @@ -161,7 +162,6 @@ export class WalletService extends Effect.Service()( const txHash = yield* signer.signTransaction({ transaction: decodedTx, - account: accountState.currentAccount, }); yield* updateState(SignAction.SignDone({ txHash })); @@ -265,37 +265,47 @@ export class WalletService extends Effect.Service()( return { stream, retry }; }); - const getWalletState = Match.type<{ - signer: Signer["Type"]; - accountsState: AccountsState; - }>().pipe( + const getWalletState = Match.type< + | { + type: BrowserSigner["type"]; + signer: BrowserSigner; + accountsState: Effect.Effect.Success< + BrowserSigner["getAccountState"] + >; + } + | { + type: LedgerSigner["type"]; + signer: LedgerSigner; + accountsState: Effect.Effect.Success< + LedgerSigner["getAccountState"] + >; + } + >().pipe( Match.withReturnType(), - Match.when( - { signer: { type: "browser" } }, - ({ signer, accountsState }) => - Match.value(accountsState).pipe( - Match.withReturnType(), - Match.when({ status: "connected" }, (connectedState) => { - return { - type: "browser", - wagmiAdapter: signer.wagmiAdapter, - status: "connected", - accounts: connectedState.accounts, - currentAccount: connectedState.currentAccount, - signTransactions, - switchAccount: signer.switchAccount, - }; - }), - Match.orElse(() => { - return { - type: "browser", - wagmiAdapter: signer.wagmiAdapter, - status: "disconnected", - }; - }), - ), + Match.when({ type: "browser" }, ({ signer, accountsState }) => + Match.value(accountsState).pipe( + Match.withReturnType(), + Match.when({ status: "connected" }, (connectedState) => { + return { + type: "browser", + wagmiAdapter: signer.wagmiAdapter, + status: "connected", + accounts: connectedState.accounts, + currentAccount: connectedState.currentAccount, + signTransactions, + switchAccount: signer.switchAccount, + }; + }), + Match.orElse(() => { + return { + type: "browser", + wagmiAdapter: signer.wagmiAdapter, + status: "disconnected", + }; + }), + ), ), - Match.orElse(({ accountsState }) => + Match.orElse(({ accountsState, signer }) => Match.value(accountsState).pipe( Match.withReturnType(), Match.when({ status: "connected" }, (connectedState) => { @@ -318,10 +328,22 @@ export class WalletService extends Effect.Service()( ), ); - const walletStream = signer.accountsStream.pipe( - Stream.map((accountsState) => - getWalletState({ signer, accountsState }), + const walletStream = Match.value(signer).pipe( + Match.when({ type: "browser" }, (s) => + s.accountsStream.pipe( + Stream.map((accountsState) => + getWalletState({ type: "browser", signer: s, accountsState }), + ), + ), + ), + Match.when({ type: "ledger" }, (s) => + s.accountsStream.pipe( + Stream.map((accountsState) => + getWalletState({ type: "ledger", signer: s, accountsState }), + ), + ), ), + Match.exhaustive, ); return { walletStream };