From ccd9f649ce5fce9b48afecc29e2490ff7be3e6b5 Mon Sep 17 00:00:00 2001 From: furiosa Date: Thu, 19 Feb 2026 12:35:14 -0700 Subject: [PATCH 1/3] feat: add multi-wallet multi-account identity types Phase 1 of en-fr0z multi-wallet multi-account architecture. New types: - WalletProviderId, WalletConnectionId, AccountId identity types - WalletConnection, ConnectedAccount, WatchAddress, AccountGroup - AccountPortfolio, WalletPortfolio, GroupPortfolio - AccountSummary, AssetDistribution, AccountAssetEntry - TrackedAddress, AccountMetadata, AddressRequest - AccountBalanceList, AccountBalance, AccountError Updated types: - Asset: added accountId, walletConnectionId - Transaction: added walletConnectionId - DeFiPosition: added accountId - Portfolio: added accountBreakdown, walletBreakdown - FilterOptions: added accountIds, walletConnectionIds, groupIds - IntegrationCredentials: added accountId All new fields are optional for backward compatibility. 58 new unit tests, 424 total passing. --- src/index.ts | 27 +- src/interfaces/AccountAssetEntry.ts | 48 ++ src/interfaces/AccountBalanceList.ts | 97 +++ src/interfaces/AccountGroup.ts | 41 + src/interfaces/AccountMetadata.ts | 64 ++ src/interfaces/AccountPortfolio.ts | 54 ++ src/interfaces/AccountSummary.ts | 57 ++ src/interfaces/AddressRequest.ts | 36 + src/interfaces/Asset.ts | 8 + src/interfaces/AssetDistribution.ts | 49 ++ src/interfaces/ConnectedAccount.ts | 57 ++ src/interfaces/DeFiPosition.ts | 4 + src/interfaces/FilterOptions.ts | 11 + src/interfaces/GroupPortfolio.ts | 44 + src/interfaces/IntegrationCredentials.ts | 4 + src/interfaces/Portfolio.ts | 10 + src/interfaces/TrackedAddress.ts | 54 ++ src/interfaces/Transaction.ts | 5 + src/interfaces/WalletConnection.ts | 75 ++ src/interfaces/WalletPortfolio.ts | 45 + src/interfaces/WatchAddress.ts | 44 + src/types/AccountId.ts | 23 + src/types/WalletConnectionId.ts | 19 + src/types/WalletProviderId.ts | 29 + tests/unit/multi-wallet-identity.test.ts | 1004 ++++++++++++++++++++++ 25 files changed, 1908 insertions(+), 1 deletion(-) create mode 100644 src/interfaces/AccountAssetEntry.ts create mode 100644 src/interfaces/AccountBalanceList.ts create mode 100644 src/interfaces/AccountGroup.ts create mode 100644 src/interfaces/AccountMetadata.ts create mode 100644 src/interfaces/AccountPortfolio.ts create mode 100644 src/interfaces/AccountSummary.ts create mode 100644 src/interfaces/AddressRequest.ts create mode 100644 src/interfaces/AssetDistribution.ts create mode 100644 src/interfaces/ConnectedAccount.ts create mode 100644 src/interfaces/GroupPortfolio.ts create mode 100644 src/interfaces/TrackedAddress.ts create mode 100644 src/interfaces/WalletConnection.ts create mode 100644 src/interfaces/WalletPortfolio.ts create mode 100644 src/interfaces/WatchAddress.ts create mode 100644 src/types/AccountId.ts create mode 100644 src/types/WalletConnectionId.ts create mode 100644 src/types/WalletProviderId.ts create mode 100644 tests/unit/multi-wallet-identity.test.ts diff --git a/src/index.ts b/src/index.ts index 74016c4..bc674ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,31 @@ export { Account } from './interfaces/Account'; export { Portfolio } from './interfaces/Portfolio'; export { PortfolioAsset } from './interfaces/PortfolioAsset'; +// Multi-Wallet Identity Types +export type { WalletProviderId } from './types/WalletProviderId'; +export type { WalletConnectionId } from './types/WalletConnectionId'; +export type { AccountId } from './types/AccountId'; + +// Multi-Wallet Connection Models +export type { WalletConnection } from './interfaces/WalletConnection'; +export type { ConnectedAccount } from './interfaces/ConnectedAccount'; +export type { WatchAddress } from './interfaces/WatchAddress'; +export type { AccountGroup } from './interfaces/AccountGroup'; + +// Multi-Wallet Portfolio Models +export type { AccountPortfolio } from './interfaces/AccountPortfolio'; +export type { WalletPortfolio } from './interfaces/WalletPortfolio'; +export type { GroupPortfolio } from './interfaces/GroupPortfolio'; +export type { AccountSummary } from './interfaces/AccountSummary'; +export type { AssetDistribution } from './interfaces/AssetDistribution'; +export type { AccountAssetEntry } from './interfaces/AccountAssetEntry'; + +// Multi-Wallet Integration Models +export type { TrackedAddress } from './interfaces/TrackedAddress'; +export type { AccountMetadata } from './interfaces/AccountMetadata'; +export type { AddressRequest } from './interfaces/AddressRequest'; +export type { AccountBalanceList, AccountBalance, AccountError } from './interfaces/AccountBalanceList'; + // Transaction Models export { Transaction } from './interfaces/Transaction'; @@ -67,4 +92,4 @@ export { RpcProviderConfig } from './interfaces/RpcProviderConfig'; export { NetworkEnvironment, EnvironmentConfig } from './types/NetworkEnvironment'; // Backward Compatibility -export { PortfolioItem } from './interfaces/PortfolioItem'; \ No newline at end of file +export { PortfolioItem } from './interfaces/PortfolioItem'; diff --git a/src/interfaces/AccountAssetEntry.ts b/src/interfaces/AccountAssetEntry.ts new file mode 100644 index 0000000..e43200a --- /dev/null +++ b/src/interfaces/AccountAssetEntry.ts @@ -0,0 +1,48 @@ +import { AccountId } from '../types/AccountId'; +import { Balance } from './Balance'; +import { Price } from './Price'; + +/** + * A single account's holding of an asset within an asset distribution. + * + * Used as part of {@link AssetDistribution} to show how much of a specific + * asset is held in each account and what percentage of the total it represents. + * + * @example + * ```typescript + * import { AccountAssetEntry } from '@cygnus-wealth/data-models'; + * + * const entry: AccountAssetEntry = { + * accountId: 'metamask:a1b2:0xAbc', + * accountLabel: 'Main DeFi', + * connectionLabel: 'My MetaMask', + * quantity: { assetId: 'eth', asset: ethAsset, amount: '7.0' }, + * value: { value: 14000, currency: 'USD', timestamp: new Date() }, + * percentage: 66.67 + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AssetDistribution} for the parent distribution context + */ +export interface AccountAssetEntry { + /** Account holding this portion of the asset */ + accountId: AccountId; + + /** User-assigned account label */ + accountLabel: string; + + /** Label of the parent wallet connection */ + connectionLabel: string; + + /** Quantity of the asset in this account */ + quantity: Balance; + + /** Value of this account's holding */ + value: Price; + + /** Percentage of total for this asset (e.g., 66.67 = 66.67%) */ + percentage: number; +} diff --git a/src/interfaces/AccountBalanceList.ts b/src/interfaces/AccountBalanceList.ts new file mode 100644 index 0000000..9c8d568 --- /dev/null +++ b/src/interfaces/AccountBalanceList.ts @@ -0,0 +1,97 @@ +import { AccountId } from '../types/AccountId'; +import { Chain } from '../enums/Chain'; +import { Balance } from './Balance'; + +/** + * A single account's balance result for a specific chain. + * + * Contains the native balance and token balances for one account + * on one chain, with the AccountId for attribution. + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AccountBalanceList} for the container type + */ +export interface AccountBalance { + /** Account this balance belongs to */ + accountId: AccountId; + + /** Checksummed address */ + address: string; + + /** Chain this balance was fetched from */ + chainId: Chain; + + /** Native token balance (e.g., ETH on Ethereum) */ + nativeBalance: Balance; + + /** ERC-20 and other token balances */ + tokenBalances: Balance[]; +} + +/** + * Error encountered while fetching data for a specific account. + * + * Supports partial failure: some accounts may succeed while others fail. + * + * @since 1.3.0 + * @stability extended + */ +export interface AccountError { + /** Account that encountered the error */ + accountId: AccountId; + + /** Chain where the error occurred */ + chainId: Chain; + + /** Error message */ + message: string; + + /** Error code for programmatic handling */ + code?: string; +} + +/** + * Account-attributed balance results with support for partial failure. + * + * Returned by integration contracts when fetching balances for multiple + * accounts. Each balance carries its AccountId for attribution, and + * per-account errors are reported separately. + * + * @example + * ```typescript + * import { AccountBalanceList } from '@cygnus-wealth/data-models'; + * + * const result: AccountBalanceList = { + * balances: [ + * { + * accountId: 'metamask:a1b2:0xAbc', + * address: '0xAbc', + * chainId: Chain.ETHEREUM, + * nativeBalance: ethBalance, + * tokenBalances: [usdcBalance, daiBalance] + * } + * ], + * errors: [], + * timestamp: '2026-02-19T08:00:00Z' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AddressRequest} for the request format + * @see {@link AccountBalance} for individual balance results + * @see {@link AccountError} for per-account errors + */ +export interface AccountBalanceList { + /** Successful balance results */ + balances: AccountBalance[]; + + /** Per-account errors (partial failure) */ + errors: AccountError[]; + + /** ISO 8601 timestamp of the fetch */ + timestamp: string; +} diff --git a/src/interfaces/AccountGroup.ts b/src/interfaces/AccountGroup.ts new file mode 100644 index 0000000..0d4d230 --- /dev/null +++ b/src/interfaces/AccountGroup.ts @@ -0,0 +1,41 @@ +import { AccountId } from '../types/AccountId'; + +/** + * User-defined grouping of accounts for organizational purposes. + * + * Allows users to create custom groups like "DeFi Accounts", "Long-term Holdings", + * or "Family" that span across wallet connections and watch addresses. + * + * @example + * ```typescript + * import { AccountGroup } from '@cygnus-wealth/data-models'; + * + * const group: AccountGroup = { + * groupId: 'group-defi-1', + * groupName: 'DeFi Accounts', + * accountIds: [ + * 'metamask:a1b2c3d4:0xAbC...123', + * 'rabby:e5f6g7h8:0xDef...456' + * ], + * createdAt: '2026-02-10T09:00:00Z' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AccountId} for account identifier format + */ +export interface AccountGroup { + /** Unique group identifier */ + groupId: string; + + /** User-assigned group name */ + groupName: string; + + /** References to accounts from any connection or watch addresses */ + accountIds: AccountId[]; + + /** ISO 8601 timestamp when the group was created */ + createdAt: string; +} diff --git a/src/interfaces/AccountMetadata.ts b/src/interfaces/AccountMetadata.ts new file mode 100644 index 0000000..9fb5b91 --- /dev/null +++ b/src/interfaces/AccountMetadata.ts @@ -0,0 +1,64 @@ +import { AccountId } from '../types/AccountId'; +import { WalletConnectionId } from '../types/WalletConnectionId'; +import { WalletProviderId } from '../types/WalletProviderId'; + +/** + * Full metadata for an account, including group membership and lifecycle status. + * + * Provides comprehensive account context for portfolio aggregation and UI + * display, combining identity, labeling, and status information. + * + * @example + * ```typescript + * import { AccountMetadata } from '@cygnus-wealth/data-models'; + * + * const metadata: AccountMetadata = { + * accountId: 'metamask:a1b2c3d4:0xAbCdEf1234567890', + * address: '0xAbCdEf1234567890', + * accountLabel: 'Main DeFi', + * connectionLabel: 'My MetaMask', + * providerId: 'metamask', + * walletConnectionId: 'metamask:a1b2c3d4', + * groups: ['group-defi-1'], + * discoveredAt: '2026-01-15T10:30:00Z', + * isStale: false, + * isActive: true + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link TrackedAddress} for the lightweight address tracking variant + */ +export interface AccountMetadata { + /** Account identifier */ + accountId: AccountId; + + /** Checksummed address */ + address: string; + + /** User-assigned account label */ + accountLabel: string; + + /** Label of the parent wallet connection */ + connectionLabel: string; + + /** Wallet provider identifier, or 'watch' for watch addresses */ + providerId: WalletProviderId | 'watch'; + + /** Wallet connection identifier, or 'watch' for watch addresses */ + walletConnectionId: WalletConnectionId | 'watch'; + + /** Group IDs this account belongs to */ + groups: string[]; + + /** ISO 8601 timestamp when first discovered */ + discoveredAt: string; + + /** Whether the provider no longer exposes this account */ + isStale: boolean; + + /** Whether this is the currently active account in the provider */ + isActive: boolean; +} diff --git a/src/interfaces/AccountPortfolio.ts b/src/interfaces/AccountPortfolio.ts new file mode 100644 index 0000000..9a45a7b --- /dev/null +++ b/src/interfaces/AccountPortfolio.ts @@ -0,0 +1,54 @@ +import { AccountId } from '../types/AccountId'; +import { WalletConnectionId } from '../types/WalletConnectionId'; +import { Asset } from './Asset'; +import { Price } from './Price'; + +/** + * Portfolio slice for a single account. + * + * Represents the holdings attributed to one specific account, whether + * it's a connected wallet account or a watch address. + * + * @example + * ```typescript + * import { AccountPortfolio } from '@cygnus-wealth/data-models'; + * + * const accountPortfolio: AccountPortfolio = { + * accountId: 'metamask:a1b2c3d4:0xAbCdEf1234567890', + * accountLabel: 'Main DeFi', + * walletConnectionId: 'metamask:a1b2c3d4', + * providerName: 'MetaMask', + * assets: [], + * totalValue: { value: 25000, currency: 'USD', timestamp: new Date() }, + * lastUpdated: '2026-02-19T08:00:00Z' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link WalletPortfolio} for per-wallet rollup + * @see {@link Portfolio} for aggregate portfolio + */ +export interface AccountPortfolio { + /** Account this portfolio slice belongs to */ + accountId: AccountId; + + /** User-assigned account label */ + accountLabel: string; + + /** Wallet connection this account belongs to, or 'watch' for watch addresses */ + walletConnectionId: WalletConnectionId | 'watch'; + + /** Human-readable provider name */ + providerName: string; + + /** Assets held in this account */ + assets: Asset[]; + + /** Total value of this account's holdings */ + totalValue: Price; + + /** ISO 8601 timestamp of last data update */ + lastUpdated: string; +} diff --git a/src/interfaces/AccountSummary.ts b/src/interfaces/AccountSummary.ts new file mode 100644 index 0000000..c806037 --- /dev/null +++ b/src/interfaces/AccountSummary.ts @@ -0,0 +1,57 @@ +import { AccountId } from '../types/AccountId'; +import { WalletProviderId } from '../types/WalletProviderId'; +import { Chain } from '../enums/Chain'; +import { Price } from './Price'; + +/** + * Summary view of an account for cross-account analysis. + * + * Lightweight representation containing key metrics for each account, + * suitable for dashboard summary views and account comparison. + * + * @example + * ```typescript + * import { AccountSummary } from '@cygnus-wealth/data-models'; + * + * const summary: AccountSummary = { + * accountId: 'metamask:a1b2c3d4:0xAbCdEf1234567890', + * accountLabel: 'Main DeFi', + * connectionLabel: 'My MetaMask', + * providerId: 'metamask', + * totalValue: { value: 25000, currency: 'USD', timestamp: new Date() }, + * assetCount: 12, + * chains: [Chain.ETHEREUM, Chain.POLYGON], + * lastUpdated: '2026-02-19T08:00:00Z' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AccountPortfolio} for full account portfolio details + */ +export interface AccountSummary { + /** Account identifier */ + accountId: AccountId; + + /** User-assigned account label */ + accountLabel: string; + + /** Label of the parent wallet connection */ + connectionLabel: string; + + /** Wallet provider identifier, or 'watch' for watch addresses */ + providerId: WalletProviderId | 'watch'; + + /** Total value of the account's holdings */ + totalValue: Price; + + /** Number of distinct assets held */ + assetCount: number; + + /** Chains this account has activity on */ + chains: Chain[]; + + /** ISO 8601 timestamp of last data update */ + lastUpdated: string; +} diff --git a/src/interfaces/AddressRequest.ts b/src/interfaces/AddressRequest.ts new file mode 100644 index 0000000..b443215 --- /dev/null +++ b/src/interfaces/AddressRequest.ts @@ -0,0 +1,36 @@ +import { AccountId } from '../types/AccountId'; +import { Chain } from '../enums/Chain'; + +/** + * Request to query data for a specific account address. + * + * Used by integration contracts to request data with account attribution. + * Carries the AccountId so results can be attributed back to the originating + * account, and includes chain scope for per-account chain filtering. + * + * @example + * ```typescript + * import { AddressRequest, Chain } from '@cygnus-wealth/data-models'; + * + * const request: AddressRequest = { + * accountId: 'metamask:a1b2c3d4:0xAbCdEf1234567890', + * address: '0xAbCdEf1234567890', + * chainScope: [Chain.ETHEREUM, Chain.POLYGON, Chain.ARBITRUM] + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AccountBalanceList} for the response format + */ +export interface AddressRequest { + /** Account identifier for result attribution */ + accountId: AccountId; + + /** Checksummed address to query */ + address: string; + + /** Chains to query for this address */ + chainScope: Chain[]; +} diff --git a/src/interfaces/Asset.ts b/src/interfaces/Asset.ts index ded8519..fb49330 100644 --- a/src/interfaces/Asset.ts +++ b/src/interfaces/Asset.ts @@ -1,5 +1,7 @@ import { AssetType } from '../enums/AssetType'; import { Chain } from '../enums/Chain'; +import { AccountId } from '../types/AccountId'; +import { WalletConnectionId } from '../types/WalletConnectionId'; import { Metadata } from './Metadata'; /** @@ -85,6 +87,12 @@ export interface Asset { /** ISIN identifier for international securities */ isin?: string; + /** Account holding this asset (for multi-wallet multi-account attribution) */ + accountId?: AccountId; + + /** Wallet connection this asset belongs to, or 'watch' for watch addresses */ + walletConnectionId?: WalletConnectionId | 'watch'; + /** Source-specific additional data and custom fields */ metadata?: Metadata; } diff --git a/src/interfaces/AssetDistribution.ts b/src/interfaces/AssetDistribution.ts new file mode 100644 index 0000000..b40cf4c --- /dev/null +++ b/src/interfaces/AssetDistribution.ts @@ -0,0 +1,49 @@ +import { Balance } from './Balance'; +import { Price } from './Price'; +import { AccountAssetEntry } from './AccountAssetEntry'; + +/** + * Distribution of a specific asset across multiple accounts. + * + * Shows how holdings of a single asset (by symbol) are distributed + * across the user's accounts, useful for concentration analysis. + * + * @example + * ```typescript + * import { AssetDistribution } from '@cygnus-wealth/data-models'; + * + * const ethDistribution: AssetDistribution = { + * symbol: 'ETH', + * totalQuantity: { assetId: 'eth', asset: ethAsset, amount: '10.5' }, + * totalValue: { value: 21000, currency: 'USD', timestamp: new Date() }, + * distribution: [ + * { + * accountId: 'metamask:a1b2:0xAbc', + * accountLabel: 'Main', + * connectionLabel: 'MetaMask', + * quantity: { assetId: 'eth', asset: ethAsset, amount: '7.0' }, + * value: { value: 14000, currency: 'USD', timestamp: new Date() }, + * percentage: 66.67 + * } + * ] + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AccountAssetEntry} for per-account breakdown + */ +export interface AssetDistribution { + /** Asset trading symbol */ + symbol: string; + + /** Total quantity across all accounts */ + totalQuantity: Balance; + + /** Total value across all accounts */ + totalValue: Price; + + /** Per-account breakdown of this asset's distribution */ + distribution: AccountAssetEntry[]; +} diff --git a/src/interfaces/ConnectedAccount.ts b/src/interfaces/ConnectedAccount.ts new file mode 100644 index 0000000..3ca57e0 --- /dev/null +++ b/src/interfaces/ConnectedAccount.ts @@ -0,0 +1,57 @@ +import { AccountId } from '../types/AccountId'; +import { Chain } from '../enums/Chain'; + +/** + * A single account within a wallet connection. + * + * Represents an address discovered via a wallet provider (EIP-1193) or + * manually added to a connection's account list. Tracks discovery time, + * staleness, and active status relative to the wallet provider. + * + * @example + * ```typescript + * import { ConnectedAccount } from '@cygnus-wealth/data-models'; + * + * const account: ConnectedAccount = { + * accountId: 'metamask:a1b2c3d4:0xAbCdEf1234567890', + * address: '0xAbCdEf1234567890', + * accountLabel: 'Main DeFi', + * chainScope: [Chain.ETHEREUM, Chain.POLYGON], + * source: 'provider', + * discoveredAt: '2026-01-15T10:30:00Z', + * isStale: false, + * isActive: true + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link WalletConnection} for the parent connection + * @see {@link AccountId} for identifier format + */ +export interface ConnectedAccount { + /** Unique account identifier: `{walletConnectionId}:{checksummedAddress}` */ + accountId: AccountId; + + /** Checksummed EVM address */ + address: string; + + /** User-assigned label (default: truncated address) */ + accountLabel: string; + + /** Chains to track this account on (default: all supported by the connection) */ + chainScope: Chain[]; + + /** How this account was added: 'provider' = discovered via EIP-1193; 'manual' = user typed the address */ + source: 'provider' | 'manual'; + + /** ISO 8601 timestamp when first seen */ + discoveredAt: string; + + /** True if the provider no longer includes this account in eth_requestAccounts responses */ + isStale: boolean; + + /** True if this is the currently selected account in the wallet provider */ + isActive: boolean; +} diff --git a/src/interfaces/DeFiPosition.ts b/src/interfaces/DeFiPosition.ts index 44beb89..1763d51 100644 --- a/src/interfaces/DeFiPosition.ts +++ b/src/interfaces/DeFiPosition.ts @@ -2,6 +2,7 @@ import { Chain } from '../enums/Chain'; import { DeFiPositionType } from '../enums/DeFiPositionType'; import { DeFiProtocol } from '../enums/DeFiProtocol'; import { DeFiDiscoverySource } from '../enums/DeFiDiscoverySource'; +import { AccountId } from '../types/AccountId'; import { Balance } from './Balance'; import { Price } from './Price'; import { Metadata } from './Metadata'; @@ -81,6 +82,9 @@ export interface DeFiPosition { /** How this position was discovered during portfolio scanning */ discoverySource?: DeFiDiscoverySource; + /** Account identifier for multi-wallet attribution (alongside ownerAddress) */ + accountId?: AccountId; + /** Protocol-specific metadata (version, contract addresses, TVL, etc.) */ metadata?: Metadata; } diff --git a/src/interfaces/FilterOptions.ts b/src/interfaces/FilterOptions.ts index fcf5465..b952037 100644 --- a/src/interfaces/FilterOptions.ts +++ b/src/interfaces/FilterOptions.ts @@ -1,6 +1,8 @@ import { Chain } from '../enums/Chain'; import { AssetType } from '../enums/AssetType'; import { IntegrationSource } from '../enums/IntegrationSource'; +import { AccountId } from '../types/AccountId'; +import { WalletConnectionId } from '../types/WalletConnectionId'; import { TimeRange } from '../types/TimeRange'; /** @@ -83,4 +85,13 @@ export interface FilterOptions { /** Maximum value threshold (inclusive, in base currency like USD) */ maxValue?: number; + + /** Filter by account identifiers (OR within array) */ + accountIds?: AccountId[]; + + /** Filter by wallet connection identifiers (OR within array) */ + walletConnectionIds?: WalletConnectionId[]; + + /** Filter by account group identifiers (OR within array) */ + groupIds?: string[]; } diff --git a/src/interfaces/GroupPortfolio.ts b/src/interfaces/GroupPortfolio.ts new file mode 100644 index 0000000..91f39ea --- /dev/null +++ b/src/interfaces/GroupPortfolio.ts @@ -0,0 +1,44 @@ +import { AccountPortfolio } from './AccountPortfolio'; +import { Price } from './Price'; + +/** + * Portfolio slice for a user-defined account group. + * + * Aggregates holdings across all accounts in a group, which may span + * multiple wallet connections and watch addresses. + * + * @example + * ```typescript + * import { GroupPortfolio } from '@cygnus-wealth/data-models'; + * + * const groupPortfolio: GroupPortfolio = { + * groupId: 'group-defi-1', + * groupName: 'DeFi Accounts', + * accounts: [], + * totalValue: { value: 75000, currency: 'USD', timestamp: new Date() }, + * lastUpdated: '2026-02-19T08:00:00Z' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AccountGroup} for group definition + * @see {@link AccountPortfolio} for per-account breakdown + */ +export interface GroupPortfolio { + /** Group identifier */ + groupId: string; + + /** User-assigned group name */ + groupName: string; + + /** Per-account portfolio breakdowns within this group */ + accounts: AccountPortfolio[]; + + /** Total value across all accounts in this group */ + totalValue: Price; + + /** ISO 8601 timestamp of last data update */ + lastUpdated: string; +} diff --git a/src/interfaces/IntegrationCredentials.ts b/src/interfaces/IntegrationCredentials.ts index 2766bcd..246b4db 100644 --- a/src/interfaces/IntegrationCredentials.ts +++ b/src/interfaces/IntegrationCredentials.ts @@ -1,4 +1,5 @@ import { IntegrationSource } from '../enums/IntegrationSource'; +import { AccountId } from '../types/AccountId'; import { Metadata } from './Metadata'; /** @@ -67,6 +68,9 @@ export interface IntegrationCredentials { /** Blockchain network ID for wallet connections (e.g., '1' for Ethereum mainnet) */ chainId?: string; + /** Account identifier when this integration is account-specific */ + accountId?: AccountId; + /** Source-specific metadata (permissions, connection settings, etc.) */ metadata?: Metadata; } diff --git a/src/interfaces/Portfolio.ts b/src/interfaces/Portfolio.ts index 4570e5d..4cfb868 100644 --- a/src/interfaces/Portfolio.ts +++ b/src/interfaces/Portfolio.ts @@ -1,4 +1,8 @@ +import { AccountId } from '../types/AccountId'; +import { WalletConnectionId } from '../types/WalletConnectionId'; import { Account } from './Account'; +import { AccountPortfolio } from './AccountPortfolio'; +import { WalletPortfolio } from './WalletPortfolio'; import { Price } from './Price'; import { PortfolioAsset } from './PortfolioAsset'; import { Metadata } from './Metadata'; @@ -133,6 +137,12 @@ export interface Portfolio { /** Timestamp of last portfolio data update */ lastUpdated: Date; + /** Per-account portfolio breakdown for multi-wallet attribution */ + accountBreakdown?: Map; + + /** Per-wallet portfolio breakdown for multi-wallet attribution */ + walletBreakdown?: Map; + /** Portfolio-specific metadata (theme, display preferences, etc.) */ metadata?: Metadata; } diff --git a/src/interfaces/TrackedAddress.ts b/src/interfaces/TrackedAddress.ts new file mode 100644 index 0000000..0eb82e1 --- /dev/null +++ b/src/interfaces/TrackedAddress.ts @@ -0,0 +1,54 @@ +import { AccountId } from '../types/AccountId'; +import { WalletConnectionId } from '../types/WalletConnectionId'; +import { WalletProviderId } from '../types/WalletProviderId'; +import { Chain } from '../enums/Chain'; + +/** + * An address being tracked for portfolio purposes with full account context. + * + * Used by the WalletIntegrationContract to communicate tracked addresses + * to PortfolioAggregation with all necessary attribution metadata. + * + * @example + * ```typescript + * import { TrackedAddress } from '@cygnus-wealth/data-models'; + * + * const tracked: TrackedAddress = { + * accountId: 'metamask:a1b2c3d4:0xAbCdEf1234567890', + * address: '0xAbCdEf1234567890', + * walletConnectionId: 'metamask:a1b2c3d4', + * providerId: 'metamask', + * accountLabel: 'Main DeFi', + * connectionLabel: 'My MetaMask', + * chainScope: [Chain.ETHEREUM, Chain.POLYGON] + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link ConnectedAccount} for the source account data + * @see {@link WatchAddress} for watch-only tracked addresses + */ +export interface TrackedAddress { + /** Account identifier */ + accountId: AccountId; + + /** Checksummed address */ + address: string; + + /** Wallet connection this address belongs to, or 'watch' */ + walletConnectionId: WalletConnectionId | 'watch'; + + /** Wallet provider identifier, or 'watch' */ + providerId: WalletProviderId | 'watch'; + + /** User-assigned account label */ + accountLabel: string; + + /** Label of the parent wallet connection */ + connectionLabel: string; + + /** Chains to query for this address */ + chainScope: Chain[]; +} diff --git a/src/interfaces/Transaction.ts b/src/interfaces/Transaction.ts index 1ec749e..0919987 100644 --- a/src/interfaces/Transaction.ts +++ b/src/interfaces/Transaction.ts @@ -1,5 +1,7 @@ import { TransactionType } from '../enums/TransactionType'; import { Chain } from '../enums/Chain'; +import { AccountId } from '../types/AccountId'; +import { WalletConnectionId } from '../types/WalletConnectionId'; import { Asset } from './Asset'; import { Price } from './Price'; import { Metadata } from './Metadata'; @@ -98,6 +100,9 @@ export interface Transaction { /** Reference to the Account.id where this transaction occurred */ accountId: string; + /** Wallet connection this transaction belongs to, or 'watch' for watch addresses */ + walletConnectionId?: WalletConnectionId | 'watch'; + /** Type of transaction operation */ type: TransactionType; diff --git a/src/interfaces/WalletConnection.ts b/src/interfaces/WalletConnection.ts new file mode 100644 index 0000000..93f2e3e --- /dev/null +++ b/src/interfaces/WalletConnection.ts @@ -0,0 +1,75 @@ +import { WalletConnectionId } from '../types/WalletConnectionId'; +import { WalletProviderId } from '../types/WalletProviderId'; +import { Chain } from '../enums/Chain'; +import { ConnectedAccount } from './ConnectedAccount'; + +/** + * A connected wallet provider session. + * + * Represents a single connection session to a wallet provider. A user may have + * multiple connections to the same provider (e.g., two WalletConnect sessions) + * or connections to different providers simultaneously. + * + * Each connection maintains its own list of accumulated accounts discovered + * via EIP-1193 or manually added by the user. + * + * @example + * ```typescript + * import { WalletConnection } from '@cygnus-wealth/data-models'; + * + * const connection: WalletConnection = { + * connectionId: 'metamask:a1b2c3d4', + * providerId: 'metamask', + * providerName: 'MetaMask', + * providerIcon: 'https://metamask.io/icon.svg', + * connectionLabel: 'My MetaMask', + * accounts: [], + * activeAccountAddress: '0xAbCdEf1234567890', + * supportedChains: [Chain.ETHEREUM, Chain.POLYGON], + * connectedAt: '2026-01-15T10:30:00Z', + * lastActiveAt: '2026-02-19T08:00:00Z', + * sessionStatus: 'active' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link ConnectedAccount} for individual account entries + * @see {@link WalletProviderId} for provider identification + * @see {@link WalletConnectionId} for connection identifier format + */ +export interface WalletConnection { + /** Unique connection identifier: `{providerId}:{randomId}` */ + connectionId: WalletConnectionId; + + /** Canonical wallet provider identifier */ + providerId: WalletProviderId; + + /** Human-readable provider name (e.g., "MetaMask") */ + providerName: string; + + /** URL or identifier for the provider's icon */ + providerIcon: string; + + /** User-assigned label (default: provider name) */ + connectionLabel: string; + + /** All accounts accumulated from this connection */ + accounts: ConnectedAccount[]; + + /** Currently selected account address in the wallet provider, or null */ + activeAccountAddress: string | null; + + /** Chains this wallet connection supports */ + supportedChains: Chain[]; + + /** ISO 8601 timestamp of initial connection */ + connectedAt: string; + + /** ISO 8601 timestamp of last activity */ + lastActiveAt: string; + + /** Session lifecycle status */ + sessionStatus: 'active' | 'stale' | 'disconnected'; +} diff --git a/src/interfaces/WalletPortfolio.ts b/src/interfaces/WalletPortfolio.ts new file mode 100644 index 0000000..5e37a06 --- /dev/null +++ b/src/interfaces/WalletPortfolio.ts @@ -0,0 +1,45 @@ +import { WalletConnectionId } from '../types/WalletConnectionId'; +import { AccountPortfolio } from './AccountPortfolio'; +import { Price } from './Price'; + +/** + * Portfolio slice for all accounts in a wallet connection. + * + * Aggregates holdings across all accounts within a single wallet + * connection session. + * + * @example + * ```typescript + * import { WalletPortfolio } from '@cygnus-wealth/data-models'; + * + * const walletPortfolio: WalletPortfolio = { + * walletConnectionId: 'metamask:a1b2c3d4', + * connectionLabel: 'My MetaMask', + * providerName: 'MetaMask', + * accounts: [], + * totalValue: { value: 50000, currency: 'USD', timestamp: new Date() } + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AccountPortfolio} for per-account breakdown + * @see {@link Portfolio} for aggregate portfolio + */ +export interface WalletPortfolio { + /** Wallet connection this portfolio belongs to */ + walletConnectionId: WalletConnectionId; + + /** User-assigned connection label */ + connectionLabel: string; + + /** Human-readable provider name */ + providerName: string; + + /** Per-account portfolio breakdowns within this wallet */ + accounts: AccountPortfolio[]; + + /** Total value across all accounts in this wallet connection */ + totalValue: Price; +} diff --git a/src/interfaces/WatchAddress.ts b/src/interfaces/WatchAddress.ts new file mode 100644 index 0000000..e7ff47e --- /dev/null +++ b/src/interfaces/WatchAddress.ts @@ -0,0 +1,44 @@ +import { AccountId } from '../types/AccountId'; +import { Chain } from '../enums/Chain'; + +/** + * An address tracked independently of any wallet connection. + * + * Represents a read-only monitoring target that the user added manually + * without connecting a wallet. Watch addresses have no associated provider, + * no session lifecycle, and never receive `accountsChanged` events. + * + * @example + * ```typescript + * import { WatchAddress } from '@cygnus-wealth/data-models'; + * + * const watched: WatchAddress = { + * accountId: 'watch:0xAbCdEf1234567890', + * address: '0xAbCdEf1234567890', + * addressLabel: 'Vitalik.eth', + * chainScope: [Chain.ETHEREUM], + * addedAt: '2026-02-01T12:00:00Z' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link AccountId} for identifier format (watch:{checksummedAddress}) + */ +export interface WatchAddress { + /** Account identifier: `watch:{checksummedAddress}` */ + accountId: AccountId; + + /** Checksummed address being monitored */ + address: string; + + /** User-assigned label */ + addressLabel: string; + + /** Chains to track this address on */ + chainScope: Chain[]; + + /** ISO 8601 timestamp when this watch address was added */ + addedAt: string; +} diff --git a/src/types/AccountId.ts b/src/types/AccountId.ts new file mode 100644 index 0000000..cfd1457 --- /dev/null +++ b/src/types/AccountId.ts @@ -0,0 +1,23 @@ +/** + * Unique identifier for a specific account across the system. + * + * Format: `{walletConnectionId}:{checksummedAddress}` for connected accounts + * (e.g., `metamask:a1b2c3d4:0xAbC...123`), or `watch:{checksummedAddress}` + * for watch addresses. + * + * This is the primary key used throughout the system to reference a specific + * account. It disambiguates the same address appearing in different wallet + * connections (e.g., imported into both MetaMask and Rabby). + * + * @example + * ```typescript + * import { AccountId } from '@cygnus-wealth/data-models'; + * + * const connectedAccountId: AccountId = 'metamask:a1b2c3d4:0xAbCdEf1234567890'; + * const watchAccountId: AccountId = 'watch:0xAbCdEf1234567890'; + * ``` + * + * @since 1.3.0 + * @stability extended + */ +export type AccountId = string; diff --git a/src/types/WalletConnectionId.ts b/src/types/WalletConnectionId.ts new file mode 100644 index 0000000..0c440ec --- /dev/null +++ b/src/types/WalletConnectionId.ts @@ -0,0 +1,19 @@ +/** + * Unique identifier for a specific wallet connection session. + * + * Format: `{providerId}:{randomId}` (e.g., `metamask:a1b2c3d4`). + * The randomId component ensures uniqueness when the same provider + * is connected multiple times (e.g., two WalletConnect sessions). + * + * @example + * ```typescript + * import { WalletConnectionId } from '@cygnus-wealth/data-models'; + * + * const connectionId: WalletConnectionId = 'metamask:a1b2c3d4'; + * const wcConnectionId: WalletConnectionId = 'walletconnect:x9y8z7w6'; + * ``` + * + * @since 1.3.0 + * @stability extended + */ +export type WalletConnectionId = string; diff --git a/src/types/WalletProviderId.ts b/src/types/WalletProviderId.ts new file mode 100644 index 0000000..cee16bc --- /dev/null +++ b/src/types/WalletProviderId.ts @@ -0,0 +1,29 @@ +/** + * Canonical identifier for a wallet provider. + * + * String union type representing supported wallet providers. Extensible + * without breaking changes when new wallets are supported. + * + * @example + * ```typescript + * import { WalletProviderId } from '@cygnus-wealth/data-models'; + * + * const provider: WalletProviderId = 'metamask'; + * ``` + * + * @since 1.3.0 + * @stability extended + */ +export type WalletProviderId = + | 'metamask' + | 'rabby' + | 'walletconnect' + | 'coinbase-wallet' + | 'trust-wallet' + | 'frame' + | 'crypto-com-onchain' + | 'phantom' + | 'solflare' + | 'backpack' + | 'exodus' + | 'manual'; diff --git a/tests/unit/multi-wallet-identity.test.ts b/tests/unit/multi-wallet-identity.test.ts new file mode 100644 index 0000000..a00ad25 --- /dev/null +++ b/tests/unit/multi-wallet-identity.test.ts @@ -0,0 +1,1004 @@ +import { describe, it, expect } from 'vitest'; +import { + Chain, + AssetType, + Asset, + Transaction, + TransactionType, + DeFiPosition, + DeFiPositionType, + DeFiProtocol, + DeFiDiscoverySource, + Portfolio, + FilterOptions, + IntegrationCredentials, + IntegrationSource, +} from '../../src/index'; + +import type { + WalletProviderId, + WalletConnectionId, + AccountId, + WalletConnection, + ConnectedAccount, + WatchAddress, + AccountGroup, + AccountPortfolio, + WalletPortfolio, + GroupPortfolio, + AccountSummary, + AssetDistribution, + AccountAssetEntry, + TrackedAddress, + AccountMetadata, + AddressRequest, + AccountBalanceList, + AccountBalance, + AccountError, +} from '../../src/index'; + +/** + * Unit tests for multi-wallet multi-account identity types. + * Phase 1 of en-fr0z architecture. + */ + +describe('Multi-Wallet Identity Types', () => { + describe('WalletProviderId', () => { + it('should accept all canonical provider values', () => { + const providers: WalletProviderId[] = [ + 'metamask', + 'rabby', + 'walletconnect', + 'coinbase-wallet', + 'trust-wallet', + 'frame', + 'crypto-com-onchain', + 'phantom', + 'solflare', + 'backpack', + 'exodus', + 'manual', + ]; + + expect(providers).toHaveLength(12); + providers.forEach(p => expect(typeof p).toBe('string')); + }); + + it('should support all EVM wallet providers', () => { + const evmProviders: WalletProviderId[] = [ + 'metamask', + 'rabby', + 'walletconnect', + 'coinbase-wallet', + 'trust-wallet', + 'frame', + 'crypto-com-onchain', + ]; + + expect(evmProviders).toHaveLength(7); + }); + + it('should support non-EVM wallet providers', () => { + const nonEvmProviders: WalletProviderId[] = [ + 'phantom', + 'solflare', + 'backpack', + 'exodus', + ]; + + expect(nonEvmProviders).toHaveLength(4); + }); + + it('should support manual provider for imported addresses', () => { + const manual: WalletProviderId = 'manual'; + expect(manual).toBe('manual'); + }); + }); + + describe('WalletConnectionId', () => { + it('should follow {providerId}:{randomId} format', () => { + const connectionId: WalletConnectionId = 'metamask:a1b2c3d4'; + expect(connectionId).toContain(':'); + const [provider, randomId] = connectionId.split(':'); + expect(provider).toBe('metamask'); + expect(randomId).toBeTruthy(); + }); + + it('should support multiple connections from same provider', () => { + const conn1: WalletConnectionId = 'walletconnect:session1'; + const conn2: WalletConnectionId = 'walletconnect:session2'; + expect(conn1).not.toBe(conn2); + }); + }); + + describe('AccountId', () => { + it('should follow {walletConnectionId}:{checksummedAddress} format', () => { + const accountId: AccountId = 'metamask:a1b2c3d4:0xAbCdEf1234567890'; + const parts = accountId.split(':'); + expect(parts).toHaveLength(3); + expect(parts[0]).toBe('metamask'); + expect(parts[2]).toMatch(/^0x/); + }); + + it('should support watch address format', () => { + const watchId: AccountId = 'watch:0xAbCdEf1234567890'; + expect(watchId).toMatch(/^watch:/); + const [prefix, address] = watchId.split(':'); + expect(prefix).toBe('watch'); + expect(address).toMatch(/^0x/); + }); + + it('should disambiguate same address across different connections', () => { + const id1: AccountId = 'metamask:abc:0xSameAddress'; + const id2: AccountId = 'rabby:xyz:0xSameAddress'; + expect(id1).not.toBe(id2); + }); + }); +}); + +describe('Multi-Wallet Connection Models', () => { + const now = new Date().toISOString(); + + describe('ConnectedAccount', () => { + it('should represent a provider-discovered account', () => { + const account: ConnectedAccount = { + accountId: 'metamask:a1b2:0xAbC123', + address: '0xAbC123', + accountLabel: 'Main DeFi', + chainScope: [Chain.ETHEREUM, Chain.POLYGON], + source: 'provider', + discoveredAt: now, + isStale: false, + isActive: true, + }; + + expect(account.source).toBe('provider'); + expect(account.isStale).toBe(false); + expect(account.isActive).toBe(true); + expect(account.chainScope).toContain(Chain.ETHEREUM); + }); + + it('should represent a manually-added account', () => { + const manual: ConnectedAccount = { + accountId: 'metamask:a1b2:0xDef456', + address: '0xDef456', + accountLabel: 'Cold Storage', + chainScope: [Chain.ETHEREUM], + source: 'manual', + discoveredAt: now, + isStale: false, + isActive: false, + }; + + expect(manual.source).toBe('manual'); + expect(manual.isActive).toBe(false); + }); + + it('should support stale accounts', () => { + const stale: ConnectedAccount = { + accountId: 'metamask:a1b2:0xOld789', + address: '0xOld789', + accountLabel: 'Old Account', + chainScope: [Chain.ETHEREUM], + source: 'provider', + discoveredAt: '2025-06-01T00:00:00Z', + isStale: true, + isActive: false, + }; + + expect(stale.isStale).toBe(true); + expect(stale.isActive).toBe(false); + }); + }); + + describe('WalletConnection', () => { + it('should represent a connected wallet session', () => { + const connection: WalletConnection = { + connectionId: 'metamask:a1b2c3d4', + providerId: 'metamask', + providerName: 'MetaMask', + providerIcon: 'https://metamask.io/icon.svg', + connectionLabel: 'My MetaMask', + accounts: [], + activeAccountAddress: '0xAbC123', + supportedChains: [Chain.ETHEREUM, Chain.POLYGON, Chain.ARBITRUM], + connectedAt: now, + lastActiveAt: now, + sessionStatus: 'active', + }; + + expect(connection.providerId).toBe('metamask'); + expect(connection.sessionStatus).toBe('active'); + expect(connection.accounts).toHaveLength(0); + expect(connection.supportedChains).toContain(Chain.ETHEREUM); + }); + + it('should support multiple accounts in a connection', () => { + const accounts: ConnectedAccount[] = [ + { + accountId: 'metamask:a1b2:0xAcc1', + address: '0xAcc1', + accountLabel: 'Account 1', + chainScope: [Chain.ETHEREUM], + source: 'provider', + discoveredAt: now, + isStale: false, + isActive: true, + }, + { + accountId: 'metamask:a1b2:0xAcc2', + address: '0xAcc2', + accountLabel: 'Account 2', + chainScope: [Chain.ETHEREUM], + source: 'provider', + discoveredAt: now, + isStale: false, + isActive: false, + }, + { + accountId: 'metamask:a1b2:0xAcc3', + address: '0xAcc3', + accountLabel: 'Account 3', + chainScope: [Chain.ETHEREUM, Chain.POLYGON], + source: 'provider', + discoveredAt: now, + isStale: false, + isActive: false, + }, + ]; + + const connection: WalletConnection = { + connectionId: 'metamask:a1b2', + providerId: 'metamask', + providerName: 'MetaMask', + providerIcon: 'https://metamask.io/icon.svg', + connectionLabel: 'My MetaMask', + accounts, + activeAccountAddress: '0xAcc1', + supportedChains: [Chain.ETHEREUM, Chain.POLYGON], + connectedAt: now, + lastActiveAt: now, + sessionStatus: 'active', + }; + + expect(connection.accounts).toHaveLength(3); + const active = connection.accounts.filter(a => a.isActive); + expect(active).toHaveLength(1); + expect(active[0].address).toBe('0xAcc1'); + }); + + it('should support stale session status', () => { + const staleConn: WalletConnection = { + connectionId: 'walletconnect:old123', + providerId: 'walletconnect', + providerName: 'WalletConnect', + providerIcon: 'https://walletconnect.org/icon.svg', + connectionLabel: 'Old WC Session', + accounts: [], + activeAccountAddress: null, + supportedChains: [Chain.ETHEREUM], + connectedAt: '2025-01-01T00:00:00Z', + lastActiveAt: '2025-06-01T00:00:00Z', + sessionStatus: 'stale', + }; + + expect(staleConn.sessionStatus).toBe('stale'); + expect(staleConn.activeAccountAddress).toBeNull(); + }); + + it('should support disconnected session status', () => { + const disconnected: WalletConnection = { + connectionId: 'rabby:disc456', + providerId: 'rabby', + providerName: 'Rabby', + providerIcon: 'rabby-icon', + connectionLabel: 'Rabby Wallet', + accounts: [], + activeAccountAddress: null, + supportedChains: [Chain.ETHEREUM], + connectedAt: now, + lastActiveAt: now, + sessionStatus: 'disconnected', + }; + + expect(disconnected.sessionStatus).toBe('disconnected'); + }); + }); + + describe('WatchAddress', () => { + it('should represent a watch-only address', () => { + const watched: WatchAddress = { + accountId: 'watch:0xVitalik', + address: '0xVitalik', + addressLabel: 'Vitalik.eth', + chainScope: [Chain.ETHEREUM, Chain.POLYGON, Chain.ARBITRUM], + addedAt: now, + }; + + expect(watched.accountId).toMatch(/^watch:/); + expect(watched.addressLabel).toBe('Vitalik.eth'); + expect(watched.chainScope).toHaveLength(3); + }); + + it('should support single-chain watch addresses', () => { + const single: WatchAddress = { + accountId: 'watch:0xSingleChain', + address: '0xSingleChain', + addressLabel: 'ETH-only Watch', + chainScope: [Chain.ETHEREUM], + addedAt: now, + }; + + expect(single.chainScope).toHaveLength(1); + }); + }); + + describe('AccountGroup', () => { + it('should group accounts from different connections', () => { + const group: AccountGroup = { + groupId: 'group-defi-1', + groupName: 'DeFi Accounts', + accountIds: [ + 'metamask:a1b2:0xAcc1', + 'rabby:c3d4:0xAcc2', + 'watch:0xAcc3', + ], + createdAt: now, + }; + + expect(group.accountIds).toHaveLength(3); + expect(group.groupName).toBe('DeFi Accounts'); + }); + + it('should support empty groups', () => { + const empty: AccountGroup = { + groupId: 'group-empty', + groupName: 'Future Accounts', + accountIds: [], + createdAt: now, + }; + + expect(empty.accountIds).toHaveLength(0); + }); + }); +}); + +describe('Multi-Wallet Portfolio Models', () => { + const now = new Date(); + + describe('AccountPortfolio', () => { + it('should represent a single account portfolio slice', () => { + const portfolio: AccountPortfolio = { + accountId: 'metamask:a1b2:0xAbc', + accountLabel: 'Main DeFi', + walletConnectionId: 'metamask:a1b2', + providerName: 'MetaMask', + assets: [], + totalValue: { value: 25000, currency: 'USD', timestamp: now }, + lastUpdated: now.toISOString(), + }; + + expect(portfolio.accountId).toContain('metamask'); + expect(portfolio.totalValue.value).toBe(25000); + expect(portfolio.assets).toHaveLength(0); + }); + + it('should support watch address portfolio', () => { + const watchPortfolio: AccountPortfolio = { + accountId: 'watch:0xWatchAddr', + accountLabel: 'Watched Whale', + walletConnectionId: 'watch', + providerName: 'Watch', + assets: [], + totalValue: { value: 1000000, currency: 'USD', timestamp: now }, + lastUpdated: now.toISOString(), + }; + + expect(watchPortfolio.walletConnectionId).toBe('watch'); + }); + }); + + describe('WalletPortfolio', () => { + it('should aggregate accounts within a wallet connection', () => { + const account1: AccountPortfolio = { + accountId: 'metamask:a1b2:0xAcc1', + accountLabel: 'Account 1', + walletConnectionId: 'metamask:a1b2', + providerName: 'MetaMask', + assets: [], + totalValue: { value: 15000, currency: 'USD', timestamp: now }, + lastUpdated: now.toISOString(), + }; + + const account2: AccountPortfolio = { + accountId: 'metamask:a1b2:0xAcc2', + accountLabel: 'Account 2', + walletConnectionId: 'metamask:a1b2', + providerName: 'MetaMask', + assets: [], + totalValue: { value: 10000, currency: 'USD', timestamp: now }, + lastUpdated: now.toISOString(), + }; + + const walletPortfolio: WalletPortfolio = { + walletConnectionId: 'metamask:a1b2', + connectionLabel: 'My MetaMask', + providerName: 'MetaMask', + accounts: [account1, account2], + totalValue: { value: 25000, currency: 'USD', timestamp: now }, + }; + + expect(walletPortfolio.accounts).toHaveLength(2); + expect(walletPortfolio.totalValue.value).toBe(25000); + }); + }); + + describe('GroupPortfolio', () => { + it('should aggregate accounts across wallet connections', () => { + const groupPortfolio: GroupPortfolio = { + groupId: 'group-defi-1', + groupName: 'DeFi Accounts', + accounts: [], + totalValue: { value: 75000, currency: 'USD', timestamp: now }, + lastUpdated: now.toISOString(), + }; + + expect(groupPortfolio.groupId).toBe('group-defi-1'); + expect(groupPortfolio.groupName).toBe('DeFi Accounts'); + }); + }); + + describe('AccountSummary', () => { + it('should provide lightweight account overview', () => { + const summary: AccountSummary = { + accountId: 'metamask:a1b2:0xAbc', + accountLabel: 'Main DeFi', + connectionLabel: 'My MetaMask', + providerId: 'metamask', + totalValue: { value: 25000, currency: 'USD', timestamp: now }, + assetCount: 12, + chains: [Chain.ETHEREUM, Chain.POLYGON], + lastUpdated: now.toISOString(), + }; + + expect(summary.assetCount).toBe(12); + expect(summary.chains).toHaveLength(2); + expect(summary.providerId).toBe('metamask'); + }); + + it('should support watch address summary', () => { + const watchSummary: AccountSummary = { + accountId: 'watch:0xWatchAddr', + accountLabel: 'Watched Whale', + connectionLabel: 'Watch', + providerId: 'watch', + totalValue: { value: 500000, currency: 'USD', timestamp: now }, + assetCount: 30, + chains: [Chain.ETHEREUM, Chain.POLYGON, Chain.ARBITRUM], + lastUpdated: now.toISOString(), + }; + + expect(watchSummary.providerId).toBe('watch'); + }); + }); + + describe('AssetDistribution', () => { + it('should show distribution of an asset across accounts', () => { + const ethAssetObj: Asset = { + id: 'ethereum-eth', + symbol: 'ETH', + name: 'Ethereum', + type: AssetType.CRYPTOCURRENCY, + }; + + const entry1: AccountAssetEntry = { + accountId: 'metamask:a1b2:0xAcc1', + accountLabel: 'Main', + connectionLabel: 'MetaMask', + quantity: { assetId: 'ethereum-eth', asset: ethAssetObj, amount: '7.0' }, + value: { value: 14000, currency: 'USD', timestamp: now }, + percentage: 66.67, + }; + + const entry2: AccountAssetEntry = { + accountId: 'rabby:c3d4:0xAcc2', + accountLabel: 'DeFi', + connectionLabel: 'Rabby', + quantity: { assetId: 'ethereum-eth', asset: ethAssetObj, amount: '3.5' }, + value: { value: 7000, currency: 'USD', timestamp: now }, + percentage: 33.33, + }; + + const dist: AssetDistribution = { + symbol: 'ETH', + totalQuantity: { assetId: 'ethereum-eth', asset: ethAssetObj, amount: '10.5' }, + totalValue: { value: 21000, currency: 'USD', timestamp: now }, + distribution: [entry1, entry2], + }; + + expect(dist.distribution).toHaveLength(2); + const totalPct = dist.distribution.reduce((sum, e) => sum + e.percentage, 0); + expect(totalPct).toBeCloseTo(100, 0); + }); + }); +}); + +describe('Multi-Wallet Integration Models', () => { + const now = new Date().toISOString(); + + describe('TrackedAddress', () => { + it('should represent a connected account tracked address', () => { + const tracked: TrackedAddress = { + accountId: 'metamask:a1b2:0xAbC123', + address: '0xAbC123', + walletConnectionId: 'metamask:a1b2', + providerId: 'metamask', + accountLabel: 'Main DeFi', + connectionLabel: 'My MetaMask', + chainScope: [Chain.ETHEREUM, Chain.POLYGON], + }; + + expect(tracked.providerId).toBe('metamask'); + expect(tracked.chainScope).toContain(Chain.ETHEREUM); + }); + + it('should represent a watch address as tracked', () => { + const watchTracked: TrackedAddress = { + accountId: 'watch:0xWatchAddr', + address: '0xWatchAddr', + walletConnectionId: 'watch', + providerId: 'watch', + accountLabel: 'Watched Whale', + connectionLabel: 'Watch', + chainScope: [Chain.ETHEREUM], + }; + + expect(watchTracked.walletConnectionId).toBe('watch'); + expect(watchTracked.providerId).toBe('watch'); + }); + }); + + describe('AccountMetadata', () => { + it('should provide full account metadata', () => { + const metadata: AccountMetadata = { + accountId: 'metamask:a1b2:0xAbC123', + address: '0xAbC123', + accountLabel: 'Main DeFi', + connectionLabel: 'My MetaMask', + providerId: 'metamask', + walletConnectionId: 'metamask:a1b2', + groups: ['group-defi-1', 'group-main'], + discoveredAt: now, + isStale: false, + isActive: true, + }; + + expect(metadata.groups).toHaveLength(2); + expect(metadata.isStale).toBe(false); + expect(metadata.isActive).toBe(true); + }); + + it('should support watch address metadata', () => { + const watchMeta: AccountMetadata = { + accountId: 'watch:0xWatchAddr', + address: '0xWatchAddr', + accountLabel: 'Watched Whale', + connectionLabel: 'Watch', + providerId: 'watch', + walletConnectionId: 'watch', + groups: [], + discoveredAt: now, + isStale: false, + isActive: false, + }; + + expect(watchMeta.providerId).toBe('watch'); + expect(watchMeta.isActive).toBe(false); + }); + }); + + describe('AddressRequest', () => { + it('should request data for a specific account', () => { + const request: AddressRequest = { + accountId: 'metamask:a1b2:0xAbC123', + address: '0xAbC123', + chainScope: [Chain.ETHEREUM, Chain.POLYGON, Chain.ARBITRUM], + }; + + expect(request.chainScope).toHaveLength(3); + expect(request.address).toBe('0xAbC123'); + }); + }); + + describe('AccountBalanceList', () => { + const ethAssetObj: Asset = { + id: 'ethereum-eth', + symbol: 'ETH', + name: 'Ethereum', + type: AssetType.CRYPTOCURRENCY, + }; + + it('should represent successful balance results', () => { + const balance: AccountBalance = { + accountId: 'metamask:a1b2:0xAbC123', + address: '0xAbC123', + chainId: Chain.ETHEREUM, + nativeBalance: { assetId: 'ethereum-eth', asset: ethAssetObj, amount: '5.0' }, + tokenBalances: [], + }; + + const result: AccountBalanceList = { + balances: [balance], + errors: [], + timestamp: now, + }; + + expect(result.balances).toHaveLength(1); + expect(result.errors).toHaveLength(0); + }); + + it('should support partial failure', () => { + const error: AccountError = { + accountId: 'metamask:a1b2:0xFail', + chainId: Chain.POLYGON, + message: 'RPC timeout', + code: 'TIMEOUT', + }; + + const balance: AccountBalance = { + accountId: 'metamask:a1b2:0xAbC123', + address: '0xAbC123', + chainId: Chain.ETHEREUM, + nativeBalance: { assetId: 'ethereum-eth', asset: ethAssetObj, amount: '5.0' }, + tokenBalances: [], + }; + + const result: AccountBalanceList = { + balances: [balance], + errors: [error], + timestamp: now, + }; + + expect(result.balances).toHaveLength(1); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].code).toBe('TIMEOUT'); + }); + + it('should support error without code', () => { + const error: AccountError = { + accountId: 'metamask:a1b2:0xFail', + chainId: Chain.ETHEREUM, + message: 'Unknown error', + }; + + expect(error.code).toBeUndefined(); + }); + }); +}); + +describe('Updated Existing Types — Multi-Wallet Fields', () => { + describe('Asset — accountId and walletConnectionId', () => { + it('should support accountId field', () => { + const asset: Asset = { + id: 'ethereum-eth', + symbol: 'ETH', + name: 'Ethereum', + type: AssetType.CRYPTOCURRENCY, + accountId: 'metamask:a1b2:0xAbC123', + }; + + expect(asset.accountId).toBe('metamask:a1b2:0xAbC123'); + }); + + it('should support walletConnectionId field', () => { + const asset: Asset = { + id: 'ethereum-eth', + symbol: 'ETH', + name: 'Ethereum', + type: AssetType.CRYPTOCURRENCY, + walletConnectionId: 'metamask:a1b2', + }; + + expect(asset.walletConnectionId).toBe('metamask:a1b2'); + }); + + it('should support watch wallet connection', () => { + const asset: Asset = { + id: 'ethereum-eth', + symbol: 'ETH', + name: 'Ethereum', + type: AssetType.CRYPTOCURRENCY, + accountId: 'watch:0xAbC', + walletConnectionId: 'watch', + }; + + expect(asset.walletConnectionId).toBe('watch'); + }); + + it('should remain backward compatible without new fields', () => { + const asset: Asset = { + id: 'ethereum-eth', + symbol: 'ETH', + name: 'Ethereum', + type: AssetType.CRYPTOCURRENCY, + }; + + expect(asset.accountId).toBeUndefined(); + expect(asset.walletConnectionId).toBeUndefined(); + }); + }); + + describe('Transaction — walletConnectionId', () => { + it('should support walletConnectionId field', () => { + const tx: Transaction = { + id: 'tx-1', + accountId: 'metamask:a1b2:0xAbC123', + walletConnectionId: 'metamask:a1b2', + type: TransactionType.TRANSFER_OUT, + status: 'COMPLETED', + timestamp: new Date(), + }; + + expect(tx.walletConnectionId).toBe('metamask:a1b2'); + }); + + it('should support watch transaction', () => { + const tx: Transaction = { + id: 'tx-2', + accountId: 'watch:0xAbC', + walletConnectionId: 'watch', + type: TransactionType.TRANSFER_IN, + status: 'COMPLETED', + timestamp: new Date(), + }; + + expect(tx.walletConnectionId).toBe('watch'); + }); + + it('should remain backward compatible without walletConnectionId', () => { + const tx: Transaction = { + id: 'tx-3', + accountId: 'wallet-1', + type: TransactionType.SWAP, + status: 'COMPLETED', + timestamp: new Date(), + }; + + expect(tx.walletConnectionId).toBeUndefined(); + }); + }); + + describe('DeFiPosition — accountId', () => { + it('should support accountId alongside existing fields', () => { + const position: DeFiPosition = { + id: 'defi-pos-1', + type: DeFiPositionType.LENDING_SUPPLY, + protocol: DeFiProtocol.AAVE, + chain: Chain.ETHEREUM, + underlyingAssets: [], + rewards: [], + accountId: 'metamask:a1b2:0xAbC123', + }; + + expect(position.accountId).toBe('metamask:a1b2:0xAbC123'); + }); + + it('should remain backward compatible without accountId', () => { + const position: DeFiPosition = { + id: 'defi-pos-2', + type: DeFiPositionType.LIQUIDITY, + protocol: DeFiProtocol.UNISWAP, + chain: Chain.ETHEREUM, + underlyingAssets: [], + rewards: [], + }; + + expect(position.accountId).toBeUndefined(); + }); + }); + + describe('Portfolio — accountBreakdown and walletBreakdown', () => { + const now = new Date(); + + it('should support accountBreakdown map', () => { + const accountPortfolio: AccountPortfolio = { + accountId: 'metamask:a1b2:0xAbC', + accountLabel: 'Main', + walletConnectionId: 'metamask:a1b2', + providerName: 'MetaMask', + assets: [], + totalValue: { value: 25000, currency: 'USD', timestamp: now }, + lastUpdated: now.toISOString(), + }; + + const portfolio: Portfolio = { + id: 'portfolio-1', + name: 'Multi-Wallet Portfolio', + totalValue: { value: 25000, currency: 'USD', timestamp: now }, + lastUpdated: now, + accountBreakdown: new Map([ + ['metamask:a1b2:0xAbC', accountPortfolio], + ]), + }; + + expect(portfolio.accountBreakdown).toBeDefined(); + expect(portfolio.accountBreakdown!.size).toBe(1); + expect(portfolio.accountBreakdown!.get('metamask:a1b2:0xAbC')?.totalValue.value).toBe(25000); + }); + + it('should support walletBreakdown map', () => { + const walletPort: WalletPortfolio = { + walletConnectionId: 'metamask:a1b2', + connectionLabel: 'My MetaMask', + providerName: 'MetaMask', + accounts: [], + totalValue: { value: 50000, currency: 'USD', timestamp: now }, + }; + + const portfolio: Portfolio = { + id: 'portfolio-2', + name: 'Multi-Wallet Portfolio', + totalValue: { value: 50000, currency: 'USD', timestamp: now }, + lastUpdated: now, + walletBreakdown: new Map([ + ['metamask:a1b2', walletPort], + ]), + }; + + expect(portfolio.walletBreakdown).toBeDefined(); + expect(portfolio.walletBreakdown!.size).toBe(1); + }); + + it('should remain backward compatible without breakdowns', () => { + const portfolio: Portfolio = { + id: 'portfolio-3', + name: 'Legacy Portfolio', + totalValue: { value: 10000, currency: 'USD', timestamp: now }, + lastUpdated: now, + }; + + expect(portfolio.accountBreakdown).toBeUndefined(); + expect(portfolio.walletBreakdown).toBeUndefined(); + }); + }); + + describe('FilterOptions — account filtering', () => { + it('should support accountIds filter', () => { + const filter: FilterOptions = { + accountIds: ['metamask:a1b2:0xAcc1', 'rabby:c3d4:0xAcc2'], + }; + + expect(filter.accountIds).toHaveLength(2); + }); + + it('should support walletConnectionIds filter', () => { + const filter: FilterOptions = { + walletConnectionIds: ['metamask:a1b2', 'rabby:c3d4'], + }; + + expect(filter.walletConnectionIds).toHaveLength(2); + }); + + it('should support groupIds filter', () => { + const filter: FilterOptions = { + groupIds: ['group-defi-1', 'group-main'], + }; + + expect(filter.groupIds).toHaveLength(2); + }); + + it('should combine account filters with existing filters', () => { + const filter: FilterOptions = { + chains: [Chain.ETHEREUM], + assetTypes: [AssetType.CRYPTOCURRENCY], + accountIds: ['metamask:a1b2:0xAcc1'], + walletConnectionIds: ['metamask:a1b2'], + groupIds: ['group-defi-1'], + }; + + expect(filter.chains).toHaveLength(1); + expect(filter.accountIds).toHaveLength(1); + expect(filter.walletConnectionIds).toHaveLength(1); + expect(filter.groupIds).toHaveLength(1); + }); + + it('should remain backward compatible without account filters', () => { + const filter: FilterOptions = { + chains: [Chain.ETHEREUM], + minValue: 100, + }; + + expect(filter.accountIds).toBeUndefined(); + expect(filter.walletConnectionIds).toBeUndefined(); + expect(filter.groupIds).toBeUndefined(); + }); + }); + + describe('IntegrationCredentials — accountId', () => { + it('should support accountId for account-specific integrations', () => { + const creds: IntegrationCredentials = { + source: IntegrationSource.METAMASK, + walletAddress: '0xAbC123', + accountId: 'metamask:a1b2:0xAbC123', + }; + + expect(creds.accountId).toBe('metamask:a1b2:0xAbC123'); + }); + + it('should remain backward compatible without accountId', () => { + const creds: IntegrationCredentials = { + source: IntegrationSource.KRAKEN, + apiKey: 'encrypted_key', + }; + + expect(creds.accountId).toBeUndefined(); + }); + }); +}); + +describe('Contract Tests — Multi-Wallet Type Exports', () => { + it('should export all identity types', () => { + // These type imports would fail at compile-time if not exported + const providerId: WalletProviderId = 'metamask'; + const connectionId: WalletConnectionId = 'metamask:a1b2'; + const accountId: AccountId = 'metamask:a1b2:0xAbc'; + expect(providerId).toBeDefined(); + expect(connectionId).toBeDefined(); + expect(accountId).toBeDefined(); + }); + + it('should export all connection model types', () => { + const conn: WalletConnection = { + connectionId: 'metamask:a1b2', + providerId: 'metamask', + providerName: 'MetaMask', + providerIcon: 'icon', + connectionLabel: 'Label', + accounts: [], + activeAccountAddress: null, + supportedChains: [], + connectedAt: '', + lastActiveAt: '', + sessionStatus: 'active', + }; + expect(conn).toBeDefined(); + }); + + it('should export all portfolio breakdown types', () => { + const now = new Date(); + const ap: AccountPortfolio = { + accountId: 'a', accountLabel: 'a', walletConnectionId: 'w', + providerName: 'p', assets: [], + totalValue: { currency: 'USD', timestamp: now }, lastUpdated: '', + }; + const wp: WalletPortfolio = { + walletConnectionId: 'w', connectionLabel: 'c', providerName: 'p', + accounts: [], totalValue: { currency: 'USD', timestamp: now }, + }; + const gp: GroupPortfolio = { + groupId: 'g', groupName: 'n', accounts: [], + totalValue: { currency: 'USD', timestamp: now }, lastUpdated: '', + }; + expect(ap).toBeDefined(); + expect(wp).toBeDefined(); + expect(gp).toBeDefined(); + }); + + it('should export all integration types', () => { + const ta: TrackedAddress = { + accountId: 'a', address: '0x', walletConnectionId: 'w', + providerId: 'metamask', accountLabel: 'l', connectionLabel: 'c', chainScope: [], + }; + const am: AccountMetadata = { + accountId: 'a', address: '0x', accountLabel: 'l', connectionLabel: 'c', + providerId: 'metamask', walletConnectionId: 'w', + groups: [], discoveredAt: '', isStale: false, isActive: true, + }; + const ar: AddressRequest = { + accountId: 'a', address: '0x', chainScope: [], + }; + expect(ta).toBeDefined(); + expect(am).toBeDefined(); + expect(ar).toBeDefined(); + }); +}); From b24a290463bfc19aef9f21edd933b1cbf3a7b312 Mon Sep 17 00:00:00 2001 From: DataModels/refinery Date: Thu, 19 Feb 2026 12:41:24 -0700 Subject: [PATCH 2/3] docs: update API report for multi-wallet identity types --- etc/data-models.api.md | 178 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/etc/data-models.api.md b/etc/data-models.api.md index 8aa7b58..95d7f1e 100644 --- a/etc/data-models.api.md +++ b/etc/data-models.api.md @@ -22,6 +22,88 @@ export interface Account { vaultPositions?: VaultPosition[]; } +// @public +export interface AccountAssetEntry { + accountId: AccountId; + accountLabel: string; + connectionLabel: string; + percentage: number; + quantity: Balance; + value: Price; +} + +// @public +export interface AccountBalance { + accountId: AccountId; + address: string; + chainId: Chain; + nativeBalance: Balance; + tokenBalances: Balance[]; +} + +// @public +export interface AccountBalanceList { + balances: AccountBalance[]; + errors: AccountError[]; + timestamp: string; +} + +// @public +export interface AccountError { + accountId: AccountId; + chainId: Chain; + code?: string; + message: string; +} + +// @public +export interface AccountGroup { + accountIds: AccountId[]; + createdAt: string; + groupId: string; + groupName: string; +} + +// @public +export type AccountId = string; + +// @public +export interface AccountMetadata { + accountId: AccountId; + accountLabel: string; + address: string; + connectionLabel: string; + discoveredAt: string; + groups: string[]; + isActive: boolean; + isStale: boolean; + providerId: WalletProviderId | 'watch'; + walletConnectionId: WalletConnectionId | 'watch'; +} + +// @public +export interface AccountPortfolio { + accountId: AccountId; + accountLabel: string; + assets: Asset[]; + lastUpdated: string; + providerName: string; + totalValue: Price; + walletConnectionId: WalletConnectionId | 'watch'; +} + +// @public +export interface AccountSummary { + accountId: AccountId; + accountLabel: string; + assetCount: number; + chains: Chain[]; + connectionLabel: string; + lastUpdated: string; + providerId: WalletProviderId | 'watch'; + totalValue: Price; +} + // @public export enum AccountType { BROKERAGE = "BROKERAGE", @@ -36,6 +118,13 @@ export enum AccountType { WALLET = "WALLET" } +// @public +export interface AddressRequest { + accountId: AccountId; + address: string; + chainScope: Chain[]; +} + // @public export interface ApiError { code: string; @@ -53,6 +142,7 @@ export interface ApiResponse { // @public export interface Asset { + accountId?: AccountId; chain?: Chain; cmc_id?: string; coingeckoId?: string; @@ -66,6 +156,15 @@ export interface Asset { name: string; symbol: string; type: AssetType; + walletConnectionId?: WalletConnectionId | 'watch'; +} + +// @public +export interface AssetDistribution { + distribution: AccountAssetEntry[]; + symbol: string; + totalQuantity: Balance; + totalValue: Price; } // @public @@ -144,6 +243,18 @@ export interface CircuitBreakerConfig { openDurationMs: number; } +// @public +export interface ConnectedAccount { + accountId: AccountId; + accountLabel: string; + address: string; + chainScope: Chain[]; + discoveredAt: string; + isActive: boolean; + isStale: boolean; + source: 'provider' | 'manual'; +} + // @public export enum DeFiDiscoverySource { CONTRACT_QUERY = "CONTRACT_QUERY", @@ -152,6 +263,7 @@ export enum DeFiDiscoverySource { // @public export interface DeFiPosition { + accountId?: AccountId; apy?: number; chain: Chain; discoverySource?: DeFiDiscoverySource; @@ -199,12 +311,24 @@ export interface EnvironmentConfig { // @public export interface FilterOptions { + accountIds?: AccountId[]; assetTypes?: AssetType[]; chains?: Chain[]; + groupIds?: string[]; maxValue?: number; minValue?: number; sources?: IntegrationSource[]; timeRange?: TimeRange; + walletConnectionIds?: WalletConnectionId[]; +} + +// @public +export interface GroupPortfolio { + accounts: AccountPortfolio[]; + groupId: string; + groupName: string; + lastUpdated: string; + totalValue: Price; } // @public @@ -216,6 +340,7 @@ export interface HealthCheckConfig { // @public export interface IntegrationCredentials { + accountId?: AccountId; apiKey?: string; apiSecret?: string; chainId?: string; @@ -335,6 +460,7 @@ export interface PaginatedResponse { // @public export interface Portfolio { + accountBreakdown?: Map; accounts?: Account[]; id: string; items?: PortfolioAsset[]; @@ -354,6 +480,7 @@ export interface Portfolio { value: Price; }>; userId?: string; + walletBreakdown?: Map; } // @public @@ -462,6 +589,17 @@ export type TimeRange = { end: Date; }; +// @public +export interface TrackedAddress { + accountId: AccountId; + accountLabel: string; + address: string; + chainScope: Chain[]; + connectionLabel: string; + providerId: WalletProviderId | 'watch'; + walletConnectionId: WalletConnectionId | 'watch'; +} + // @public export interface Transaction { accountId: string; @@ -492,6 +630,7 @@ export interface Transaction { timestamp: Date; to?: string; type: TransactionType; + walletConnectionId?: WalletConnectionId | 'watch'; } // @public @@ -544,6 +683,45 @@ export enum VaultStrategyType { YIELD_AGGREGATOR = "YIELD_AGGREGATOR" } +// @public +export interface WalletConnection { + accounts: ConnectedAccount[]; + activeAccountAddress: string | null; + connectedAt: string; + connectionId: WalletConnectionId; + connectionLabel: string; + lastActiveAt: string; + providerIcon: string; + providerId: WalletProviderId; + providerName: string; + sessionStatus: 'active' | 'stale' | 'disconnected'; + supportedChains: Chain[]; +} + +// @public +export type WalletConnectionId = string; + +// @public +export interface WalletPortfolio { + accounts: AccountPortfolio[]; + connectionLabel: string; + providerName: string; + totalValue: Price; + walletConnectionId: WalletConnectionId; +} + +// @public +export type WalletProviderId = 'metamask' | 'rabby' | 'walletconnect' | 'coinbase-wallet' | 'trust-wallet' | 'frame' | 'crypto-com-onchain' | 'phantom' | 'solflare' | 'backpack' | 'exodus' | 'manual'; + +// @public +export interface WatchAddress { + accountId: AccountId; + addedAt: string; + address: string; + addressLabel: string; + chainScope: Chain[]; +} + // (No @packageDocumentation comment for this package) ``` From c03400c403ac95d1f7082574802b16ac0aa0e813 Mon Sep 17 00:00:00 2001 From: DataModels/refinery Date: Thu, 19 Feb 2026 12:43:20 -0700 Subject: [PATCH 3/3] fix: escape TSDoc braces in WatchAddress to pass api-extractor --- src/interfaces/WatchAddress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/WatchAddress.ts b/src/interfaces/WatchAddress.ts index e7ff47e..c2c04b1 100644 --- a/src/interfaces/WatchAddress.ts +++ b/src/interfaces/WatchAddress.ts @@ -24,7 +24,7 @@ import { Chain } from '../enums/Chain'; * @since 1.3.0 * @stability extended * - * @see {@link AccountId} for identifier format (watch:{checksummedAddress}) + * @see {@link AccountId} for identifier format (watch:\{checksummedAddress\}) */ export interface WatchAddress { /** Account identifier: `watch:{checksummedAddress}` */