@framers/agentos-ext-wallet 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +98 -0
  2. package/dist/AgentWalletManager.d.ts +68 -0
  3. package/dist/AgentWalletManager.d.ts.map +1 -0
  4. package/dist/AgentWalletManager.js +238 -0
  5. package/dist/AgentWalletManager.js.map +1 -0
  6. package/dist/SpendingPolicyEnforcer.d.ts +33 -0
  7. package/dist/SpendingPolicyEnforcer.d.ts.map +1 -0
  8. package/dist/SpendingPolicyEnforcer.js +128 -0
  9. package/dist/SpendingPolicyEnforcer.js.map +1 -0
  10. package/dist/cards/CardManager.d.ts +71 -0
  11. package/dist/cards/CardManager.d.ts.map +1 -0
  12. package/dist/cards/CardManager.js +212 -0
  13. package/dist/cards/CardManager.js.map +1 -0
  14. package/dist/cards/LithicCardAdapter.d.ts +40 -0
  15. package/dist/cards/LithicCardAdapter.d.ts.map +1 -0
  16. package/dist/cards/LithicCardAdapter.js +116 -0
  17. package/dist/cards/LithicCardAdapter.js.map +1 -0
  18. package/dist/cards/MccCategoryMap.d.ts +36 -0
  19. package/dist/cards/MccCategoryMap.d.ts.map +1 -0
  20. package/dist/cards/MccCategoryMap.js +113 -0
  21. package/dist/cards/MccCategoryMap.js.map +1 -0
  22. package/dist/cards/types.d.ts +131 -0
  23. package/dist/cards/types.d.ts.map +1 -0
  24. package/dist/cards/types.js +12 -0
  25. package/dist/cards/types.js.map +1 -0
  26. package/dist/chains/EvmWalletAdapter.d.ts +30 -0
  27. package/dist/chains/EvmWalletAdapter.d.ts.map +1 -0
  28. package/dist/chains/EvmWalletAdapter.js +135 -0
  29. package/dist/chains/EvmWalletAdapter.js.map +1 -0
  30. package/dist/chains/SolanaWalletAdapter.d.ts +32 -0
  31. package/dist/chains/SolanaWalletAdapter.d.ts.map +1 -0
  32. package/dist/chains/SolanaWalletAdapter.js +128 -0
  33. package/dist/chains/SolanaWalletAdapter.js.map +1 -0
  34. package/dist/index.d.ts +61 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +228 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/tools/CardSpendingSummaryTool.d.ts +14 -0
  39. package/dist/tools/CardSpendingSummaryTool.d.ts.map +1 -0
  40. package/dist/tools/CardSpendingSummaryTool.js +40 -0
  41. package/dist/tools/CardSpendingSummaryTool.js.map +1 -0
  42. package/dist/tools/CardStatusTool.d.ts +21 -0
  43. package/dist/tools/CardStatusTool.d.ts.map +1 -0
  44. package/dist/tools/CardStatusTool.js +48 -0
  45. package/dist/tools/CardStatusTool.js.map +1 -0
  46. package/dist/tools/CheckBalanceTool.d.ts +23 -0
  47. package/dist/tools/CheckBalanceTool.d.ts.map +1 -0
  48. package/dist/tools/CheckBalanceTool.js +116 -0
  49. package/dist/tools/CheckBalanceTool.js.map +1 -0
  50. package/dist/tools/FreezeCardTool.d.ts +15 -0
  51. package/dist/tools/FreezeCardTool.d.ts.map +1 -0
  52. package/dist/tools/FreezeCardTool.js +39 -0
  53. package/dist/tools/FreezeCardTool.js.map +1 -0
  54. package/dist/tools/IssueCardTool.d.ts +22 -0
  55. package/dist/tools/IssueCardTool.d.ts.map +1 -0
  56. package/dist/tools/IssueCardTool.js +55 -0
  57. package/dist/tools/IssueCardTool.js.map +1 -0
  58. package/dist/tools/PayWithCardTool.d.ts +29 -0
  59. package/dist/tools/PayWithCardTool.d.ts.map +1 -0
  60. package/dist/tools/PayWithCardTool.js +79 -0
  61. package/dist/tools/PayWithCardTool.js.map +1 -0
  62. package/dist/tools/SendCryptoTool.d.ts +33 -0
  63. package/dist/tools/SendCryptoTool.d.ts.map +1 -0
  64. package/dist/tools/SendCryptoTool.js +136 -0
  65. package/dist/tools/SendCryptoTool.js.map +1 -0
  66. package/dist/tools/UnfreezeCardTool.d.ts +15 -0
  67. package/dist/tools/UnfreezeCardTool.d.ts.map +1 -0
  68. package/dist/tools/UnfreezeCardTool.js +39 -0
  69. package/dist/tools/UnfreezeCardTool.js.map +1 -0
  70. package/dist/tools/WalletHistoryTool.d.ts +35 -0
  71. package/dist/tools/WalletHistoryTool.d.ts.map +1 -0
  72. package/dist/tools/WalletHistoryTool.js +75 -0
  73. package/dist/tools/WalletHistoryTool.js.map +1 -0
  74. package/dist/types.d.ts +146 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +29 -0
  77. package/dist/types.js.map +1 -0
  78. package/manifest.json +98 -0
  79. package/package.json +78 -0
  80. package/src/AgentWalletManager.ts +324 -0
  81. package/src/SpendingPolicyEnforcer.ts +169 -0
  82. package/src/cards/CardManager.ts +274 -0
  83. package/src/cards/LithicCardAdapter.ts +159 -0
  84. package/src/cards/MccCategoryMap.ts +138 -0
  85. package/src/cards/types.ts +169 -0
  86. package/src/chains/EvmWalletAdapter.ts +170 -0
  87. package/src/chains/SolanaWalletAdapter.ts +184 -0
  88. package/src/index.ts +315 -0
  89. package/src/tools/CardSpendingSummaryTool.ts +52 -0
  90. package/src/tools/CardStatusTool.ts +68 -0
  91. package/src/tools/CheckBalanceTool.ts +145 -0
  92. package/src/tools/FreezeCardTool.ts +52 -0
  93. package/src/tools/IssueCardTool.ts +76 -0
  94. package/src/tools/PayWithCardTool.ts +109 -0
  95. package/src/tools/SendCryptoTool.ts +185 -0
  96. package/src/tools/UnfreezeCardTool.ts +52 -0
  97. package/src/tools/WalletHistoryTool.ts +113 -0
  98. package/src/types.ts +230 -0
  99. package/test/AgentWalletManager.spec.ts +301 -0
  100. package/test/CardManager.spec.ts +428 -0
  101. package/test/LithicCardAdapter.spec.ts +224 -0
  102. package/test/MccCategoryMap.spec.ts +153 -0
  103. package/test/SpendingPolicyEnforcer.spec.ts +254 -0
  104. package/test/card-integration.spec.ts +242 -0
  105. package/test/card-tools.spec.ts +285 -0
  106. package/test/integration.spec.ts +171 -0
  107. package/test/wallet-tools.spec.ts +275 -0
  108. package/tsconfig.json +22 -0
@@ -0,0 +1,138 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @fileoverview Merchant Category Code (MCC) → SpendCategory mapping.
4
+ *
5
+ * Maps 4-digit MCC codes used by card networks to the wallet extension's
6
+ * SpendCategory enum. Uses range-based matching for efficient lookup.
7
+ *
8
+ * @module wallet/cards/MccCategoryMap
9
+ */
10
+
11
+ import type { SpendCategory } from '../types.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // MCC range definitions
15
+ // ---------------------------------------------------------------------------
16
+
17
+ interface MccRange {
18
+ min: number;
19
+ max: number;
20
+ category: SpendCategory;
21
+ }
22
+
23
+ /**
24
+ * Ordered list of MCC ranges → SpendCategory mappings.
25
+ * Ranges are inclusive on both ends.
26
+ */
27
+ const MCC_RANGES: MccRange[] = [
28
+ // Travel: airlines, hotels, rental cars, travel agencies
29
+ { min: 3000, max: 3999, category: 'travel' },
30
+ { min: 4511, max: 4511, category: 'travel' }, // airlines
31
+ { min: 4722, max: 4722, category: 'travel' }, // travel agencies
32
+ { min: 7011, max: 7012, category: 'travel' }, // hotels, timeshares
33
+ { min: 7033, max: 7033, category: 'travel' }, // trailer parks
34
+ { min: 7512, max: 7519, category: 'travel' }, // car rental
35
+
36
+ // Utilities: telecom, electric, gas, water
37
+ { min: 4812, max: 4816, category: 'utilities' }, // telecom
38
+ { min: 4899, max: 4900, category: 'utilities' }, // cable, electric/gas/water
39
+
40
+ // Shopping: general retail
41
+ { min: 5200, max: 5499, category: 'shopping' }, // home supply, dept stores
42
+ { min: 5511, max: 5599, category: 'shopping' }, // auto dealers, auto parts
43
+ { min: 5600, max: 5699, category: 'shopping' }, // apparel
44
+ { min: 5700, max: 5799, category: 'shopping' }, // furniture, electronics
45
+ { min: 5900, max: 5999, category: 'shopping' }, // retail stores, misc
46
+
47
+ // Dining: restaurants, bars, fast food
48
+ { min: 5811, max: 5811, category: 'dining' }, // caterers
49
+ { min: 5812, max: 5812, category: 'dining' }, // eating places/restaurants
50
+ { min: 5813, max: 5813, category: 'dining' }, // bars/taverns
51
+ { min: 5814, max: 5814, category: 'dining' }, // fast food
52
+
53
+ // Subscriptions: digital goods/services
54
+ { min: 5815, max: 5818, category: 'subscriptions' }, // digital goods, games, apps, streaming
55
+
56
+ // Entertainment: movies, theaters, events, recreation
57
+ { min: 7832, max: 7833, category: 'entertainment' }, // movie theaters
58
+ { min: 7841, max: 7841, category: 'entertainment' }, // video tape rental
59
+ { min: 7911, max: 7911, category: 'entertainment' }, // dance halls/studios
60
+ { min: 7922, max: 7922, category: 'entertainment' }, // theatrical producers
61
+ { min: 7929, max: 7929, category: 'entertainment' }, // bands/orchestras
62
+ { min: 7932, max: 7933, category: 'entertainment' }, // billiards, bowling
63
+ { min: 7941, max: 7941, category: 'entertainment' }, // sports clubs/fields
64
+ { min: 7991, max: 7999, category: 'entertainment' }, // tourist attractions, recreation
65
+
66
+ // Web services: computer programming, data processing, IT
67
+ { min: 7372, max: 7379, category: 'web_services' }, // computer services
68
+ { min: 4816, max: 4816, category: 'web_services' }, // computer network/info services (re-mapped from utilities for precision)
69
+
70
+ // API costs: SaaS / cloud compute (commonly billed under computer services)
71
+ // Note: most SaaS bills under 7372 (web_services). If needed, specific
72
+ // merchant descriptors can override at the CardManager level.
73
+ ];
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Lookup class
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export class MccCategoryMap {
80
+ private readonly ranges: MccRange[];
81
+
82
+ constructor() {
83
+ // Sort by min for binary-search-friendly access
84
+ this.ranges = [...MCC_RANGES].sort((a, b) => a.min - b.min);
85
+ }
86
+
87
+ /**
88
+ * Resolve a 4-digit MCC string to a SpendCategory.
89
+ * Returns 'other' if no matching range is found.
90
+ */
91
+ mccToCategory(mcc: string): SpendCategory {
92
+ const code = parseInt(mcc, 10);
93
+ if (isNaN(code)) return 'other';
94
+
95
+ for (const range of this.ranges) {
96
+ if (code >= range.min && code <= range.max) {
97
+ return range.category;
98
+ }
99
+ }
100
+ return 'other';
101
+ }
102
+
103
+ /**
104
+ * Get all MCC ranges that map to a given SpendCategory.
105
+ * Useful for building Lithic auth rules.
106
+ */
107
+ categoryToMccRanges(category: SpendCategory): Array<{ min: number; max: number }> {
108
+ return this.ranges
109
+ .filter(r => r.category === category)
110
+ .map(({ min, max }) => ({ min, max }));
111
+ }
112
+
113
+ /**
114
+ * Get all individual MCC codes for a category (expands ranges).
115
+ * Useful for Lithic's blocked_mcc list which expects individual codes.
116
+ */
117
+ categoryToMccCodes(category: SpendCategory): string[] {
118
+ const codes: string[] = [];
119
+ for (const range of this.ranges) {
120
+ if (range.category !== category) continue;
121
+ for (let code = range.min; code <= range.max; code++) {
122
+ codes.push(code.toString().padStart(4, '0'));
123
+ }
124
+ }
125
+ return codes;
126
+ }
127
+
128
+ /**
129
+ * Get all categories that have MCC mappings.
130
+ */
131
+ getMappedCategories(): SpendCategory[] {
132
+ const categories = new Set<SpendCategory>();
133
+ for (const range of this.ranges) {
134
+ categories.add(range.category);
135
+ }
136
+ return [...categories];
137
+ }
138
+ }
@@ -0,0 +1,169 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @fileoverview Types for the virtual card subsystem (Lithic integration).
4
+ * @module wallet/cards/types
5
+ */
6
+
7
+ import type { SpendCategory } from '../types.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Card primitives
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export type CardType = 'VIRTUAL' | 'PHYSICAL';
14
+ export type CardState = 'OPEN' | 'PAUSED' | 'CLOSED' | 'PENDING_FULFILLMENT';
15
+ export type CardNetwork = 'VISA' | 'MASTERCARD';
16
+ export type CardTxStatus = 'PENDING' | 'SETTLED' | 'DECLINED' | 'VOIDED';
17
+ export type SpendLimitDuration = 'TRANSACTION' | 'MONTHLY' | 'ANNUALLY' | 'FOREVER';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Config (lives in WalletConfig.card)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface CardConfig {
24
+ enabled: boolean;
25
+ /** Card provider. Only 'lithic' is supported. */
26
+ provider: 'lithic';
27
+ /** Default per-card spending limit in USD. */
28
+ defaultSpendLimitUsd: number;
29
+ /** Whether physical card issuance is allowed. */
30
+ allowPhysical: boolean;
31
+ }
32
+
33
+ export const DEFAULT_CARD_CONFIG: CardConfig = {
34
+ enabled: false,
35
+ provider: 'lithic',
36
+ defaultSpendLimitUsd: 500,
37
+ allowPhysical: false,
38
+ };
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Records (DB / runtime)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export interface AgentCardRecord {
45
+ id: string;
46
+ agentId: string;
47
+ /** Lithic's card token identifier. */
48
+ lithicCardToken: string;
49
+ last4: string;
50
+ cardType: CardType;
51
+ state: CardState;
52
+ spendLimitUsd: number;
53
+ spendLimitDuration: SpendLimitDuration;
54
+ network: CardNetwork;
55
+ /** Agent-assigned label for the card. */
56
+ memo?: string;
57
+ createdAt: number;
58
+ }
59
+
60
+ export interface CardTransactionRecord {
61
+ id: string;
62
+ /** FK to AgentCardRecord.id */
63
+ cardId: string;
64
+ /** Lithic's transaction token. */
65
+ lithicTxToken: string;
66
+ merchantName?: string;
67
+ /** 4-digit Merchant Category Code. */
68
+ merchantMcc: string;
69
+ /** Resolved spend category. */
70
+ category: SpendCategory;
71
+ amountUsd: number;
72
+ status: CardTxStatus;
73
+ createdAt: number;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Card store interface (agnostic — callers provide SQLite/memory impl)
78
+ // ---------------------------------------------------------------------------
79
+
80
+ export interface ICardStore {
81
+ getCard(agentId: string): Promise<AgentCardRecord | null>;
82
+ saveCard(record: AgentCardRecord): Promise<void>;
83
+ updateCardState(agentId: string, state: CardState): Promise<void>;
84
+ insertTransaction(record: CardTransactionRecord): Promise<void>;
85
+ updateTransactionStatus(id: string, status: CardTxStatus): Promise<void>;
86
+ getTransactions(cardId: string, limit?: number): Promise<CardTransactionRecord[]>;
87
+ getTransactionsByPeriod(cardId: string, since: number): Promise<CardTransactionRecord[]>;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Lithic API types (subset we need)
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export interface CreateCardParams {
95
+ type: CardType;
96
+ memo?: string;
97
+ spend_limit: number;
98
+ spend_limit_duration: SpendLimitDuration;
99
+ state?: CardState;
100
+ }
101
+
102
+ export interface UpdateCardParams {
103
+ state?: CardState;
104
+ spend_limit?: number;
105
+ spend_limit_duration?: SpendLimitDuration;
106
+ memo?: string;
107
+ }
108
+
109
+ export interface LithicCardResponse {
110
+ token: string;
111
+ last_four: string;
112
+ type: CardType;
113
+ state: CardState;
114
+ spend_limit: number;
115
+ spend_limit_duration: SpendLimitDuration;
116
+ pan?: string;
117
+ cvv?: string;
118
+ exp_month?: string;
119
+ exp_year?: string;
120
+ funding: { token: string; type: string };
121
+ }
122
+
123
+ export interface LithicTransaction {
124
+ token: string;
125
+ card_token: string;
126
+ amount: number;
127
+ merchant: {
128
+ descriptor: string;
129
+ mcc: string;
130
+ city?: string;
131
+ state?: string;
132
+ country?: string;
133
+ };
134
+ status: 'PENDING' | 'SETTLED' | 'DECLINED' | 'VOIDED';
135
+ created: string;
136
+ }
137
+
138
+ export interface ListTxOpts {
139
+ begin?: string;
140
+ end?: string;
141
+ page_size?: number;
142
+ }
143
+
144
+ export interface AuthRuleParams {
145
+ allowed_mcc?: string[];
146
+ blocked_mcc?: string[];
147
+ }
148
+
149
+ export interface ICardAdapter {
150
+ createCard(params: CreateCardParams): Promise<LithicCardResponse>;
151
+ getCard(cardToken: string): Promise<LithicCardResponse>;
152
+ updateCard(cardToken: string, updates: UpdateCardParams): Promise<LithicCardResponse>;
153
+ pauseCard(cardToken: string): Promise<void>;
154
+ resumeCard(cardToken: string): Promise<void>;
155
+ closeCard(cardToken: string): Promise<void>;
156
+ listTransactions(cardToken: string, opts?: ListTxOpts): Promise<LithicTransaction[]>;
157
+ createAuthRule(cardToken: string, rule: AuthRuleParams): Promise<void>;
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Spending summary
162
+ // ---------------------------------------------------------------------------
163
+
164
+ export interface SpendingSummary {
165
+ totalUsd: number;
166
+ period: 'day' | 'month';
167
+ byCategory: Array<{ category: SpendCategory; amountUsd: number }>;
168
+ transactionCount: number;
169
+ }
@@ -0,0 +1,170 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @fileoverview EVM chain adapter for the Agent Personal Wallet.
4
+ *
5
+ * Supports Ethereum, Base, and Polygon via ethers.js v6 (dynamic import).
6
+ *
7
+ * @module wunderland/wallet/chains/EvmWalletAdapter
8
+ */
9
+
10
+ import type { ChainId, IChainWalletAdapter, WalletTxStatus } from '../types.js';
11
+
12
+ // Lazily resolved ethers module (dynamic import — not a hard dependency)
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ let _ethers: any = null;
15
+
16
+ async function getEthers(): Promise<any> {
17
+ if (!_ethers) {
18
+ try {
19
+ _ethers = await import('ethers' as string);
20
+ } catch {
21
+ throw new Error(
22
+ 'Missing dependency: ethers is required for EVM wallet support. '
23
+ + 'Install it with: npm install ethers',
24
+ );
25
+ }
26
+ }
27
+ return _ethers;
28
+ }
29
+
30
+ /** Default RPC endpoints per chain. */
31
+ const DEFAULT_RPC: Record<string, string> = {
32
+ ethereum: 'https://eth.llamarpc.com',
33
+ base: 'https://mainnet.base.org',
34
+ polygon: 'https://polygon-rpc.com',
35
+ };
36
+
37
+ /** Well-known ERC-20 addresses per chain. */
38
+ export const KNOWN_TOKENS: Record<string, Record<string, string>> = {
39
+ ethereum: {
40
+ USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
41
+ USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
42
+ },
43
+ base: {
44
+ USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
45
+ USDT: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2',
46
+ },
47
+ polygon: {
48
+ USDC: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
49
+ USDT: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
50
+ },
51
+ };
52
+
53
+ /** Minimal ERC-20 ABI for balance + transfer. */
54
+ const ERC20_ABI = [
55
+ 'function balanceOf(address owner) view returns (uint256)',
56
+ 'function transfer(address to, uint256 amount) returns (bool)',
57
+ 'function decimals() view returns (uint8)',
58
+ ];
59
+
60
+ export interface EvmWalletAdapterOptions {
61
+ chain: ChainId;
62
+ rpcUrl?: string;
63
+ }
64
+
65
+ export class EvmWalletAdapter implements IChainWalletAdapter {
66
+ readonly chain: ChainId;
67
+ private rpcUrl: string;
68
+
69
+ constructor(opts: EvmWalletAdapterOptions) {
70
+ this.chain = opts.chain;
71
+ this.rpcUrl = opts.rpcUrl || DEFAULT_RPC[opts.chain] || DEFAULT_RPC.ethereum;
72
+ }
73
+
74
+ async generateKeypair(): Promise<{ publicKey: string; secretKey: Uint8Array }> {
75
+ const { Wallet } = await getEthers();
76
+ const wallet = Wallet.createRandom();
77
+ // ethers v6 returns hex private key — convert to Uint8Array
78
+ const privKeyHex = wallet.privateKey.slice(2); // remove 0x
79
+ const secretKey = hexToBytes(privKeyHex);
80
+ return {
81
+ publicKey: wallet.address,
82
+ secretKey,
83
+ };
84
+ }
85
+
86
+ async getBalance(address: string): Promise<bigint> {
87
+ const { JsonRpcProvider } = await getEthers();
88
+ const provider = new JsonRpcProvider(this.rpcUrl);
89
+ return await provider.getBalance(address);
90
+ }
91
+
92
+ async getTokenBalance(address: string, tokenAddress: string): Promise<bigint> {
93
+ const { JsonRpcProvider, Contract } = await getEthers();
94
+ const provider = new JsonRpcProvider(this.rpcUrl);
95
+ const contract = new Contract(tokenAddress, ERC20_ABI, provider);
96
+ const balance: bigint = await contract.balanceOf(address);
97
+ return balance;
98
+ }
99
+
100
+ async signTransfer(
101
+ secretKey: Uint8Array,
102
+ to: string,
103
+ amountRaw: bigint,
104
+ ): Promise<Uint8Array> {
105
+ const { JsonRpcProvider, Wallet } = await getEthers();
106
+ const provider = new JsonRpcProvider(this.rpcUrl);
107
+ const wallet = new Wallet(bytesToHex(secretKey), provider);
108
+
109
+ const tx = await wallet.populateTransaction({
110
+ to,
111
+ value: amountRaw,
112
+ });
113
+
114
+ const signed = await wallet.signTransaction(tx);
115
+ return hexToBytes(signed.slice(2));
116
+ }
117
+
118
+ async signTokenTransfer(
119
+ secretKey: Uint8Array,
120
+ to: string,
121
+ tokenAddress: string,
122
+ amountRaw: bigint,
123
+ ): Promise<Uint8Array> {
124
+ const { JsonRpcProvider, Wallet, Contract } = await getEthers();
125
+ const provider = new JsonRpcProvider(this.rpcUrl);
126
+ const wallet = new Wallet(bytesToHex(secretKey), provider);
127
+ const contract = new Contract(tokenAddress, ERC20_ABI, wallet);
128
+
129
+ const tx = await contract.transfer.populateTransaction(to, amountRaw);
130
+ const populated = await wallet.populateTransaction(tx);
131
+ const signed = await wallet.signTransaction(populated);
132
+ return hexToBytes(signed.slice(2));
133
+ }
134
+
135
+ async broadcast(signedTx: Uint8Array): Promise<string> {
136
+ const { JsonRpcProvider } = await getEthers();
137
+ const provider = new JsonRpcProvider(this.rpcUrl);
138
+ const txResponse = await provider.broadcastTransaction('0x' + bytesToHex(signedTx));
139
+ await txResponse.wait(1);
140
+ return txResponse.hash;
141
+ }
142
+
143
+ async getTransactionStatus(txHash: string): Promise<WalletTxStatus> {
144
+ const { JsonRpcProvider } = await getEthers();
145
+ const provider = new JsonRpcProvider(this.rpcUrl);
146
+ const receipt = await provider.getTransactionReceipt(txHash);
147
+
148
+ if (!receipt) return 'pending';
149
+ if (receipt.status === 1) return 'confirmed';
150
+ return 'failed';
151
+ }
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Hex <-> bytes helpers
156
+ // ---------------------------------------------------------------------------
157
+
158
+ function hexToBytes(hex: string): Uint8Array {
159
+ const bytes = new Uint8Array(hex.length / 2);
160
+ for (let i = 0; i < bytes.length; i++) {
161
+ bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
162
+ }
163
+ return bytes;
164
+ }
165
+
166
+ function bytesToHex(bytes: Uint8Array): string {
167
+ return Array.from(bytes)
168
+ .map((b) => b.toString(16).padStart(2, '0'))
169
+ .join('');
170
+ }
@@ -0,0 +1,184 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @fileoverview Solana chain adapter for the Agent Personal Wallet.
4
+ *
5
+ * Uses dynamic imports for @solana/web3.js so it remains an optional dependency.
6
+ * Patterns derived from the existing SolanaProvider in anchor-providers.
7
+ *
8
+ * @module wunderland/wallet/chains/SolanaWalletAdapter
9
+ */
10
+
11
+ import type { IChainWalletAdapter, WalletTxStatus } from '../types.js';
12
+
13
+ // Lazily resolved modules
14
+ let _web3: typeof import('@solana/web3.js') | null = null;
15
+
16
+ async function getWeb3() {
17
+ if (!_web3) {
18
+ try {
19
+ _web3 = await import('@solana/web3.js');
20
+ } catch {
21
+ throw new Error(
22
+ 'Missing dependency: @solana/web3.js is required for Solana wallet support. '
23
+ + 'Install it with: npm install @solana/web3.js',
24
+ );
25
+ }
26
+ }
27
+ return _web3;
28
+ }
29
+
30
+ export interface SolanaWalletAdapterOptions {
31
+ /** RPC endpoint URL. Defaults to mainnet-beta. */
32
+ rpcUrl?: string;
33
+ /** Commitment level for queries. */
34
+ commitment?: 'processed' | 'confirmed' | 'finalized';
35
+ }
36
+
37
+ export class SolanaWalletAdapter implements IChainWalletAdapter {
38
+ readonly chain = 'solana' as const;
39
+ private rpcUrl: string;
40
+ private commitment: 'processed' | 'confirmed' | 'finalized';
41
+
42
+ constructor(opts: SolanaWalletAdapterOptions = {}) {
43
+ this.rpcUrl = opts.rpcUrl || 'https://api.mainnet-beta.solana.com';
44
+ this.commitment = opts.commitment || 'confirmed';
45
+ }
46
+
47
+ async generateKeypair(): Promise<{ publicKey: string; secretKey: Uint8Array }> {
48
+ const { Keypair } = await getWeb3();
49
+ const kp = Keypair.generate();
50
+ return {
51
+ publicKey: kp.publicKey.toBase58(),
52
+ secretKey: kp.secretKey,
53
+ };
54
+ }
55
+
56
+ async getBalance(address: string): Promise<bigint> {
57
+ const { Connection, PublicKey } = await getWeb3();
58
+ const conn = new Connection(this.rpcUrl, this.commitment);
59
+ const lamports = await conn.getBalance(new PublicKey(address));
60
+ return BigInt(lamports);
61
+ }
62
+
63
+ async getTokenBalance(address: string, tokenMint: string): Promise<bigint> {
64
+ const { Connection, PublicKey } = await getWeb3();
65
+ const conn = new Connection(this.rpcUrl, this.commitment);
66
+ const ownerPk = new PublicKey(address);
67
+ const mintPk = new PublicKey(tokenMint);
68
+
69
+ // Find the associated token account
70
+ const accounts = await conn.getTokenAccountsByOwner(ownerPk, { mint: mintPk });
71
+ if (accounts.value.length === 0) return 0n;
72
+
73
+ // Parse account data to get amount
74
+ const info = await conn.getTokenAccountBalance(accounts.value[0].pubkey);
75
+ return BigInt(info.value.amount);
76
+ }
77
+
78
+ async signTransfer(
79
+ secretKey: Uint8Array,
80
+ to: string,
81
+ amountRaw: bigint,
82
+ ): Promise<Uint8Array> {
83
+ const { Keypair, PublicKey, SystemProgram, Transaction, Connection } = await getWeb3();
84
+ const conn = new Connection(this.rpcUrl, this.commitment);
85
+ const signer = Keypair.fromSecretKey(secretKey);
86
+
87
+ const tx = new Transaction().add(
88
+ SystemProgram.transfer({
89
+ fromPubkey: signer.publicKey,
90
+ toPubkey: new PublicKey(to),
91
+ lamports: Number(amountRaw),
92
+ }),
93
+ );
94
+
95
+ tx.feePayer = signer.publicKey;
96
+ tx.recentBlockhash = (await conn.getLatestBlockhash()).blockhash;
97
+ tx.sign(signer);
98
+
99
+ return tx.serialize();
100
+ }
101
+
102
+ async signTokenTransfer(
103
+ secretKey: Uint8Array,
104
+ to: string,
105
+ tokenMint: string,
106
+ amountRaw: bigint,
107
+ ): Promise<Uint8Array> {
108
+ // SPL token transfers require @solana/spl-token — use dynamic import
109
+ const { Keypair, PublicKey, Transaction, Connection } = await getWeb3();
110
+ let splToken: any;
111
+ try {
112
+ splToken = await import('@solana/spl-token' as string);
113
+ } catch {
114
+ throw new Error(
115
+ 'Missing dependency: @solana/spl-token is required for SPL token transfers. '
116
+ + 'Install it with: npm install @solana/spl-token',
117
+ );
118
+ }
119
+
120
+ const conn = new Connection(this.rpcUrl, this.commitment);
121
+ const signer = Keypair.fromSecretKey(secretKey);
122
+ const mintPk = new PublicKey(tokenMint);
123
+ const destPk = new PublicKey(to);
124
+
125
+ // Get or create associated token accounts
126
+ const sourceAta = splToken.getAssociatedTokenAddressSync(mintPk, signer.publicKey);
127
+ const destAta = splToken.getAssociatedTokenAddressSync(mintPk, destPk);
128
+
129
+ const tx = new Transaction();
130
+
131
+ // Create destination ATA if it doesn't exist
132
+ const destAccount = await conn.getAccountInfo(destAta);
133
+ if (!destAccount) {
134
+ tx.add(
135
+ splToken.createAssociatedTokenAccountInstruction(
136
+ signer.publicKey,
137
+ destAta,
138
+ destPk,
139
+ mintPk,
140
+ ),
141
+ );
142
+ }
143
+
144
+ tx.add(
145
+ splToken.createTransferInstruction(
146
+ sourceAta,
147
+ destAta,
148
+ signer.publicKey,
149
+ Number(amountRaw),
150
+ ),
151
+ );
152
+
153
+ tx.feePayer = signer.publicKey;
154
+ tx.recentBlockhash = (await conn.getLatestBlockhash()).blockhash;
155
+ tx.sign(signer);
156
+
157
+ return tx.serialize();
158
+ }
159
+
160
+ async broadcast(signedTx: Uint8Array): Promise<string> {
161
+ const { Connection } = await getWeb3();
162
+ const conn = new Connection(this.rpcUrl, this.commitment);
163
+ const sig = await conn.sendRawTransaction(signedTx, {
164
+ skipPreflight: false,
165
+ preflightCommitment: this.commitment,
166
+ });
167
+ // Wait for confirmation
168
+ await conn.confirmTransaction(sig, this.commitment);
169
+ return sig;
170
+ }
171
+
172
+ async getTransactionStatus(txHash: string): Promise<WalletTxStatus> {
173
+ const { Connection } = await getWeb3();
174
+ const conn = new Connection(this.rpcUrl, this.commitment);
175
+ const status = await conn.getSignatureStatus(txHash);
176
+
177
+ if (!status.value) return 'pending';
178
+ if (status.value.err) return 'failed';
179
+ if (status.value.confirmationStatus === 'finalized' || status.value.confirmationStatus === 'confirmed') {
180
+ return 'confirmed';
181
+ }
182
+ return 'pending';
183
+ }
184
+ }