From 885f0baeb1a605cf95692321ff8404eecdbf5ffb Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 10 Apr 2026 19:14:31 -0400 Subject: [PATCH] feat: add Linea chain support and WalletAdapter abstraction TASK-120 Phases 1-2: Add Linea mainnet (59144) and Linea Sepolia (59141) to all chain maps (CHAIN_MAP, RPC, env names, native tokens, popular tokens, CoinGecko, Zerion, chain colors/names, Safe prefixes). New WalletAdapter class provides chain-parameterized abstraction over Safe/EOA/UP wallets with immutable withUniversalProfile() and fromSafe/fromEOA/fromUP factories. Co-Authored-By: Claude Opus 4.6 --- .../rwallet/components/folk-wallet-viewer.ts | 4 +- modules/rwallet/lib/defi-positions.ts | 1 + modules/rwallet/lib/price-feed.ts | 2 + modules/rwallet/mod.ts | 15 ++- src/encryptid/server.ts | 2 +- src/encryptid/wallet-adapter.ts | 125 ++++++++++++++++++ 6 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/encryptid/wallet-adapter.ts diff --git a/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts index 450aec87..81750585 100644 --- a/modules/rwallet/components/folk-wallet-viewer.ts +++ b/modules/rwallet/components/folk-wallet-viewer.ts @@ -74,6 +74,8 @@ const CHAIN_COLORS: Record = { "80002": "#a855f7", "43113": "#fb923c", "97": "#fbbf24", + "59144": "#0E76FD", + "59141": "#3b82f6", "local": "#22c55e", }; @@ -82,7 +84,7 @@ const COLORS_TX = { cyan: "#00d4ff", green: "#4ade80", red: "#f87171" }; const CHAIN_NAMES: Record = { "1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon", "8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche", - "56": "BSC", "324": "zkSync", "local": "CRDT", + "56": "BSC", "324": "zkSync", "59144": "Linea", "local": "CRDT", }; const EXAMPLE_WALLETS = [ diff --git a/modules/rwallet/lib/defi-positions.ts b/modules/rwallet/lib/defi-positions.ts index a3988012..3e6a72c5 100644 --- a/modules/rwallet/lib/defi-positions.ts +++ b/modules/rwallet/lib/defi-positions.ts @@ -34,6 +34,7 @@ const ZERION_CHAIN_MAP: Record = { avalanche: "43114", celo: "42220", "zksync-era": "324", + linea: "59144", }; function getApiKey(): string | null { diff --git a/modules/rwallet/lib/price-feed.ts b/modules/rwallet/lib/price-feed.ts index 9c12703c..30182d23 100644 --- a/modules/rwallet/lib/price-feed.ts +++ b/modules/rwallet/lib/price-feed.ts @@ -15,6 +15,7 @@ const CHAIN_PLATFORM: Record = { "43114": "avalanche", "42220": "celo", "324": "zksync", + "59144": "linea", }; // CoinGecko native coin IDs per chain @@ -29,6 +30,7 @@ const NATIVE_COIN_ID: Record = { "43114": "avalanche-2", "42220": "celo", "324": "ethereum", + "59144": "ethereum", }; interface CacheEntry { diff --git a/modules/rwallet/mod.ts b/modules/rwallet/mod.ts index 8570a4bd..14f4e143 100644 --- a/modules/rwallet/mod.ts +++ b/modules/rwallet/mod.ts @@ -151,6 +151,8 @@ const CHAIN_MAP: Record = { "43114": { name: "Avalanche", prefix: "avax" }, "56": { name: "BSC", prefix: "bnb" }, "324": { name: "zkSync", prefix: "zksync" }, + "59144": { name: "Linea", prefix: "linea" }, + "59141": { name: "Linea Sepolia", prefix: "lineasep" }, "11155111": { name: "Sepolia", prefix: "sep" }, "84532": { name: "Base Sepolia", prefix: "basesep" }, "421614": { name: "Arbitrum Sepolia", prefix: "arbsep" }, @@ -160,7 +162,7 @@ const CHAIN_MAP: Record = { "97": { name: "BSC Testnet", prefix: "bsctest" }, }; -const TESTNET_CHAIN_IDS = new Set(["11155111", "84532", "421614", "11155420", "80002", "43113", "97"]); +const TESTNET_CHAIN_IDS = new Set(["11155111", "84532", "421614", "11155420", "80002", "43113", "97", "59141"]); function getChains(includeTestnets: boolean): [string, { name: string; prefix: string }][] { return Object.entries(CHAIN_MAP).filter(([id]) => includeTestnets || !TESTNET_CHAIN_IDS.has(id)); @@ -186,6 +188,8 @@ const DEFAULT_RPC_URLS: Record = { "43114": "https://api.avax.network/ext/bc/C/rpc", "56": "https://bsc-dataseed.binance.org", "324": "https://mainnet.era.zksync.io", + "59144": "https://rpc.linea.build", + "59141": "https://rpc.sepolia.linea.build", "11155111": "https://rpc.sepolia.org", "84532": "https://sepolia.base.org", "421614": "https://sepolia-rollup.arbitrum.io/rpc", @@ -207,6 +211,8 @@ const CHAIN_ENV_NAMES: Record "43114": { envName: "AVALANCHE" }, "56": { envName: "BSC" }, "324": { envName: "ZKSYNC" }, + "59144": { envName: "LINEA", alchemySlug: "linea-mainnet" }, + "59141": { envName: "LINEA_SEPOLIA" }, "11155111": { envName: "SEPOLIA", alchemySlug: "eth-sepolia" }, "84532": { envName: "BASE_SEPOLIA", alchemySlug: "base-sepolia" }, "421614": { envName: "ARB_SEPOLIA", alchemySlug: "arb-sepolia" }, @@ -249,6 +255,8 @@ const NATIVE_TOKENS: Record { const CHAIN_PREFIXES: Record = { 1: 'eth', 10: 'oeth', 100: 'gno', 137: 'pol', 8453: 'base', 42161: 'arb1', 42220: 'celo', 43114: 'avax', - 56: 'bnb', 324: 'zksync', 11155111: 'sep', 84532: 'basesep', + 56: 'bnb', 324: 'zksync', 59144: 'linea', 11155111: 'sep', 84532: 'basesep', }; const prefix = CHAIN_PREFIXES[chain]; if (!prefix) { diff --git a/src/encryptid/wallet-adapter.ts b/src/encryptid/wallet-adapter.ts new file mode 100644 index 00000000..c1e1a4dd --- /dev/null +++ b/src/encryptid/wallet-adapter.ts @@ -0,0 +1,125 @@ +/** + * WalletAdapter — Chain-parameterized abstraction over Safe/EOA/UP wallets. + * + * Provides a unified interface regardless of the underlying wallet backing type + * (Gnosis Safe, raw EOA, or LUKSO Universal Profile). + */ + +import type { SafeWalletEntry } from './wallet-store'; + +export type WalletBackingType = 'safe' | 'eoa' | 'up'; + +export interface WalletInfo { + /** Effective sending address (Safe address, EOA, or UP) */ + address: string; + /** Underlying EOA address (from passkey-derived key) */ + signerAddress: string; + /** EVM chain ID */ + chainId: number; + /** Wallet backing type */ + type: WalletBackingType; + /** User-assigned label */ + label: string; + /** Whether a Universal Profile is associated */ + hasUniversalProfile: boolean; + /** UP address if one is associated */ + upAddress?: string; +} + +interface WalletAdapterParams { + address: string; + signerAddress: string; + chainId: number; + type: WalletBackingType; + label: string; + upAddress?: string; +} + +export class WalletAdapter { + private readonly params: WalletAdapterParams; + + private constructor(params: WalletAdapterParams) { + this.params = params; + } + + /** + * Create a WalletAdapter from an existing Gnosis Safe entry. + */ + static fromSafe(entry: SafeWalletEntry, eoaAddress: string): WalletAdapter { + return new WalletAdapter({ + address: entry.safeAddress, + signerAddress: eoaAddress, + chainId: entry.chainId, + type: 'safe', + label: entry.label, + }); + } + + /** + * Create a WalletAdapter for a raw EOA (passkey-derived secp256k1 key). + */ + static fromEOA(eoaAddress: string, chainId: number, label?: string): WalletAdapter { + return new WalletAdapter({ + address: eoaAddress, + signerAddress: eoaAddress, + chainId, + type: 'eoa', + label: label ?? 'EOA Wallet', + }); + } + + /** + * Create a WalletAdapter for a LUKSO Universal Profile. + */ + static fromUP(upAddress: string, controllerEOA: string, chainId: number, label?: string): WalletAdapter { + return new WalletAdapter({ + address: upAddress, + signerAddress: controllerEOA, + chainId, + type: 'up', + label: label ?? 'Universal Profile', + upAddress, + }); + } + + /** + * Returns a new WalletAdapter with a Universal Profile associated. + * The original instance is not mutated. + */ + withUniversalProfile(upAddress: string): WalletAdapter { + return new WalletAdapter({ + ...this.params, + upAddress, + }); + } + + /** + * Get wallet info, optionally overriding the chain ID. + */ + getInfo(chainId?: number): WalletInfo { + const effectiveChainId = chainId ?? this.params.chainId; + return { + address: this.params.address, + signerAddress: this.params.signerAddress, + chainId: effectiveChainId, + type: this.params.type, + label: this.params.label, + hasUniversalProfile: !!this.params.upAddress, + ...(this.params.upAddress && { upAddress: this.params.upAddress }), + }; + } + + /** + * Serialize to a plain object (no sensitive data). + */ + toJSON(): Record { + return { + address: this.params.address, + signerAddress: this.params.signerAddress, + chainId: this.params.chainId, + type: this.params.type, + label: this.params.label, + upAddress: this.params.upAddress ?? null, + }; + } +}