@framers/agentos-ext-wallet 0.2.0 → 0.2.2

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.
package/manifest.json CHANGED
@@ -2,16 +2,16 @@
2
2
  "$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
3
3
  "id": "com.framers.tools.wallet",
4
4
  "name": "Agent Personal Wallet",
5
- "version": "0.2.0",
5
+ "version": "0.2.2",
6
6
  "description": "Give agents their own personal wallet with configurable spending guardrails. Supports Solana, Ethereum, Base, and Polygon with per-category budgets, daily/monthly limits, human-in-the-loop approval thresholds, and Lithic virtual card integration.",
7
7
  "author": {
8
8
  "name": "Frame.dev",
9
9
  "email": "support@frame.dev",
10
- "url": "https://github.com/framersai"
10
+ "url": "https://github.com/framerslab"
11
11
  },
12
- "license": "MIT",
12
+ "license": "Apache-2.0",
13
13
  "keywords": ["wallet", "crypto", "solana", "ethereum", "spending-policy", "guardrails", "defi", "card", "lithic", "virtual-card"],
14
- "agentosVersion": "^2.0.0",
14
+ "agentosVersion": ">=0.10.40",
15
15
  "categories": ["tool", "finance"],
16
16
  "platforms": [],
17
17
  "sdk": "none",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@framers/agentos-ext-wallet",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Agent Personal Wallet — autonomous crypto spending with guardrails, multi-chain support (Solana + EVM), and virtual card integration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,14 +27,14 @@
27
27
  "author": {
28
28
  "name": "Frame.dev",
29
29
  "email": "support@frame.dev",
30
- "url": "https://github.com/framersai"
30
+ "url": "https://github.com/framerslab"
31
31
  },
32
- "license": "MIT",
32
+ "license": "Apache-2.0",
33
33
  "dependencies": {
34
34
  "uuid": "^9.0.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "@framers/agentos": "^0.5.2",
37
+ "@framers/agentos": ">=0.10.40",
38
38
  "@solana/web3.js": "^1.0.0",
39
39
  "@solana/spl-token": "^0.3.0",
40
40
  "ethers": "^6.0.0"
@@ -54,24 +54,27 @@
54
54
  "@types/node": "^20.10.0",
55
55
  "@types/uuid": "^9.0.0",
56
56
  "typescript": "^5.4.0",
57
- "vitest": "^1.3.0"
57
+ "vitest": "^3.2.7"
58
58
  },
59
59
  "repository": {
60
60
  "type": "git",
61
- "url": "git+https://github.com/framersai/agentos-extensions.git",
61
+ "url": "git+https://github.com/framerslab/agentos-extensions.git",
62
62
  "directory": "registry/curated/tools/wallet"
63
63
  },
64
- "homepage": "https://github.com/framersai/agentos-extensions/tree/master/registry/curated/tools/wallet#readme",
64
+ "homepage": "https://github.com/framerslab/agentos-extensions/tree/master/registry/curated/tools/wallet#readme",
65
65
  "bugs": {
66
- "url": "https://github.com/framersai/agentos-extensions/issues"
66
+ "url": "https://github.com/framerslab/agentos-extensions/issues"
67
67
  },
68
+ "files": [
69
+ "dist",
70
+ "manifest.json"
71
+ ],
68
72
  "scripts": {
69
73
  "build": "tsc",
70
74
  "dev": "tsc --watch",
71
- "test": "vitest run",
75
+ "test": "vitest run --passWithNoTests",
72
76
  "test:watch": "vitest",
73
77
  "test:coverage": "vitest run --coverage",
74
- "lint": "eslint src --ext .ts",
75
78
  "typecheck": "tsc --noEmit",
76
79
  "clean": "rimraf dist"
77
80
  }
@@ -1,324 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview Core wallet lifecycle manager for agent personal wallets.
4
- *
5
- * Handles keypair generation, encrypted storage, balance queries, transaction
6
- * signing, and broadcasting across multiple chains. Works with the
7
- * SpendingPolicyEnforcer for guardrailed autonomous spending.
8
- *
9
- * @module wunderland/wallet/AgentWalletManager
10
- */
11
-
12
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
13
- import { v4 as uuidv4 } from 'uuid';
14
-
15
- import type {
16
- AgentWalletRecord,
17
- ChainId,
18
- IChainWalletAdapter,
19
- SpendCategory,
20
- TokenSymbol,
21
- WalletConfig,
22
- WalletTransactionRecord,
23
- WalletTxStatus,
24
- } from './types.js';
25
- import type { SpendingPolicyEnforcer } from './SpendingPolicyEnforcer.js';
26
-
27
- // ---------------------------------------------------------------------------
28
- // DB store interface (agnostic — callers provide SQLite/Postgres impl)
29
- // ---------------------------------------------------------------------------
30
-
31
- export interface IWalletStore {
32
- getWallet(agentId: string, chain: ChainId): Promise<AgentWalletRecord | null>;
33
- getAllWallets(agentId: string): Promise<AgentWalletRecord[]>;
34
- saveWallet(record: AgentWalletRecord): Promise<void>;
35
- insertTransaction(record: WalletTransactionRecord): Promise<void>;
36
- updateTransactionStatus(id: string, status: WalletTxStatus, txHash?: string): Promise<void>;
37
- getTransactions(walletId: string, limit?: number): Promise<WalletTransactionRecord[]>;
38
- }
39
-
40
- // ---------------------------------------------------------------------------
41
- // Encryption helpers
42
- // ---------------------------------------------------------------------------
43
-
44
- const ALGO = 'aes-256-gcm';
45
- const KEY_LEN = 32;
46
- const IV_LEN = 12;
47
- const TAG_LEN = 16;
48
- const SCRYPT_N = 16384;
49
-
50
- /** Derive a 256-bit key from a master secret + salt. */
51
- function deriveKey(masterSecret: string, salt: Buffer): Buffer {
52
- return scryptSync(masterSecret, salt, KEY_LEN, { N: SCRYPT_N }) as Buffer;
53
- }
54
-
55
- /** Encrypt raw bytes → base64 string (IV + ciphertext + authTag). */
56
- function encrypt(data: Uint8Array, masterSecret: string, salt: Buffer): string {
57
- const key = deriveKey(masterSecret, salt);
58
- const iv = randomBytes(IV_LEN);
59
- const cipher = createCipheriv(ALGO, key, iv);
60
- const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
61
- const tag = cipher.getAuthTag();
62
- // Pack: IV (12) + Tag (16) + Ciphertext
63
- return Buffer.concat([iv, tag, encrypted]).toString('base64');
64
- }
65
-
66
- /** Decrypt base64 string → raw bytes. */
67
- function decrypt(packed: string, masterSecret: string, salt: Buffer): Uint8Array {
68
- const buf = Buffer.from(packed, 'base64');
69
- const iv = buf.subarray(0, IV_LEN);
70
- const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
71
- const ciphertext = buf.subarray(IV_LEN + TAG_LEN);
72
- const key = deriveKey(masterSecret, salt);
73
- const decipher = createDecipheriv(ALGO, key, iv);
74
- decipher.setAuthTag(tag);
75
- const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
76
- return new Uint8Array(decrypted);
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // AgentWalletManager
81
- // ---------------------------------------------------------------------------
82
-
83
- export interface AgentWalletManagerOptions {
84
- /** Master secret for key encryption. Should come from env (WALLET_MASTER_SECRET). */
85
- masterSecret: string;
86
- walletConfig: WalletConfig;
87
- store: IWalletStore;
88
- policyEnforcer: SpendingPolicyEnforcer;
89
- /** Chain adapters keyed by ChainId. */
90
- adapters: Map<ChainId, IChainWalletAdapter>;
91
- }
92
-
93
- export class AgentWalletManager {
94
- private readonly masterSecret: string;
95
- readonly walletConfig: WalletConfig;
96
- private readonly store: IWalletStore;
97
- private readonly policy: SpendingPolicyEnforcer;
98
- private readonly adapters: Map<ChainId, IChainWalletAdapter>;
99
-
100
- /** In-memory balance cache: `${agentId}:${chain}:${token}` → { value, ts }. */
101
- private balanceCache = new Map<string, { value: bigint; ts: number }>();
102
- private readonly cacheTtlMs = 30_000;
103
-
104
- constructor(opts: AgentWalletManagerOptions) {
105
- this.masterSecret = opts.masterSecret;
106
- this.walletConfig = opts.walletConfig;
107
- this.store = opts.store;
108
- this.policy = opts.policyEnforcer;
109
- this.adapters = opts.adapters;
110
- }
111
-
112
- // -----------------------------------------------------------------------
113
- // Wallet lifecycle
114
- // -----------------------------------------------------------------------
115
-
116
- /** Create a new wallet for the agent on the specified chain. */
117
- async createWallet(agentId: string, chain: ChainId): Promise<AgentWalletRecord> {
118
- const adapter = this.getAdapter(chain);
119
-
120
- // Check if wallet already exists
121
- const existing = await this.store.getWallet(agentId, chain);
122
- if (existing) {
123
- throw new Error(`Agent ${agentId} already has a ${chain} wallet at ${existing.address}`);
124
- }
125
-
126
- const { publicKey, secretKey } = await adapter.generateKeypair();
127
-
128
- // Encrypt the secret key
129
- const salt = randomBytes(32);
130
- const encryptedKey = encrypt(secretKey, this.masterSecret, salt);
131
-
132
- const record: AgentWalletRecord = {
133
- id: uuidv4(),
134
- agentId,
135
- chain,
136
- address: publicKey,
137
- encryptedKey,
138
- keyDerivationSalt: salt.toString('hex'),
139
- createdAt: Date.now(),
140
- };
141
-
142
- await this.store.saveWallet(record);
143
- return record;
144
- }
145
-
146
- /** Get an existing wallet or null. */
147
- async getWallet(agentId: string, chain: ChainId): Promise<AgentWalletRecord | null> {
148
- return this.store.getWallet(agentId, chain);
149
- }
150
-
151
- /** Get all wallets for an agent. */
152
- async getAllWallets(agentId: string): Promise<AgentWalletRecord[]> {
153
- return this.store.getAllWallets(agentId);
154
- }
155
-
156
- // -----------------------------------------------------------------------
157
- // Balance queries (cached)
158
- // -----------------------------------------------------------------------
159
-
160
- /** Get native token balance (e.g. SOL, ETH). */
161
- async getBalance(agentId: string, chain: ChainId): Promise<bigint> {
162
- const wallet = await this.requireWallet(agentId, chain);
163
- const cacheKey = `${agentId}:${chain}:native`;
164
- const cached = this.balanceCache.get(cacheKey);
165
- if (cached && Date.now() - cached.ts < this.cacheTtlMs) return cached.value;
166
-
167
- const adapter = this.getAdapter(chain);
168
- const balance = await adapter.getBalance(wallet.address);
169
- this.balanceCache.set(cacheKey, { value: balance, ts: Date.now() });
170
- return balance;
171
- }
172
-
173
- /** Get token balance (e.g. USDC on Solana). */
174
- async getTokenBalance(agentId: string, chain: ChainId, tokenMint: string): Promise<bigint> {
175
- const wallet = await this.requireWallet(agentId, chain);
176
- const cacheKey = `${agentId}:${chain}:${tokenMint}`;
177
- const cached = this.balanceCache.get(cacheKey);
178
- if (cached && Date.now() - cached.ts < this.cacheTtlMs) return cached.value;
179
-
180
- const adapter = this.getAdapter(chain);
181
- const balance = await adapter.getTokenBalance(wallet.address, tokenMint);
182
- this.balanceCache.set(cacheKey, { value: balance, ts: Date.now() });
183
- return balance;
184
- }
185
-
186
- // -----------------------------------------------------------------------
187
- // Spending (with policy enforcement)
188
- // -----------------------------------------------------------------------
189
-
190
- /**
191
- * Send native token with spending policy enforcement.
192
- * Returns the transaction record (pending until broadcast confirms).
193
- */
194
- async sendNative(
195
- agentId: string,
196
- chain: ChainId,
197
- to: string,
198
- amountRaw: bigint,
199
- amountUsd: number,
200
- category: SpendCategory,
201
- description?: string,
202
- ): Promise<WalletTransactionRecord> {
203
- // Enforce spending policy
204
- const check = await this.policy.canSpend(agentId, amountUsd, category, to);
205
- if (!check.allowed) {
206
- throw new SpendingPolicyViolation(check.reason || 'Spending policy violation');
207
- }
208
- if (check.requiresApproval) {
209
- throw new ApprovalRequiredError(amountUsd, category);
210
- }
211
-
212
- const wallet = await this.requireWallet(agentId, chain);
213
- const adapter = this.getAdapter(chain);
214
- const secretKey = this.decryptKey(wallet);
215
-
216
- // Create pending transaction record
217
- const txRecord: WalletTransactionRecord = {
218
- id: uuidv4(),
219
- walletId: wallet.id,
220
- direction: 'outbound',
221
- toAddress: to,
222
- fromAddress: wallet.address,
223
- amountRaw: amountRaw.toString(),
224
- amountUsd,
225
- token: nativeTokenForChain(chain),
226
- category,
227
- status: 'pending',
228
- description,
229
- createdAt: Date.now(),
230
- };
231
- await this.store.insertTransaction(txRecord);
232
-
233
- try {
234
- // Sign and broadcast
235
- const signedTx = await adapter.signTransfer(secretKey, to, amountRaw);
236
- const txHash = await adapter.broadcast(signedTx);
237
-
238
- // Update record
239
- await this.store.updateTransactionStatus(txRecord.id, 'confirmed', txHash);
240
- txRecord.status = 'confirmed';
241
- txRecord.txHash = txHash;
242
-
243
- // Record in spending ledger
244
- await this.policy.recordSpend(agentId, amountUsd, category);
245
-
246
- // Invalidate balance cache
247
- this.balanceCache.delete(`${agentId}:${chain}:native`);
248
- } catch (err) {
249
- await this.store.updateTransactionStatus(txRecord.id, 'failed');
250
- txRecord.status = 'failed';
251
- throw err;
252
- }
253
-
254
- return txRecord;
255
- }
256
-
257
- /** Get recent transactions for a wallet. */
258
- async getTransactionHistory(agentId: string, chain: ChainId, limit = 20): Promise<WalletTransactionRecord[]> {
259
- const wallet = await this.requireWallet(agentId, chain);
260
- return this.store.getTransactions(wallet.id, limit);
261
- }
262
-
263
- // -----------------------------------------------------------------------
264
- // Internal helpers
265
- // -----------------------------------------------------------------------
266
-
267
- private getAdapter(chain: ChainId): IChainWalletAdapter {
268
- const adapter = this.adapters.get(chain);
269
- if (!adapter) {
270
- throw new Error(`No wallet adapter registered for chain: ${chain}`);
271
- }
272
- return adapter;
273
- }
274
-
275
- private async requireWallet(agentId: string, chain: ChainId): Promise<AgentWalletRecord> {
276
- const wallet = await this.store.getWallet(agentId, chain);
277
- if (!wallet) {
278
- throw new Error(`No ${chain} wallet found for agent ${agentId}. Create one first.`);
279
- }
280
- return wallet;
281
- }
282
-
283
- private decryptKey(wallet: AgentWalletRecord): Uint8Array {
284
- const salt = Buffer.from(wallet.keyDerivationSalt, 'hex');
285
- return decrypt(wallet.encryptedKey, this.masterSecret, salt);
286
- }
287
- }
288
-
289
- // ---------------------------------------------------------------------------
290
- // Error classes
291
- // ---------------------------------------------------------------------------
292
-
293
- export class SpendingPolicyViolation extends Error {
294
- constructor(reason: string) {
295
- super(reason);
296
- this.name = 'SpendingPolicyViolation';
297
- }
298
- }
299
-
300
- export class ApprovalRequiredError extends Error {
301
- constructor(
302
- public readonly amountUsd: number,
303
- public readonly category: SpendCategory,
304
- ) {
305
- super(
306
- `Transaction of $${amountUsd.toFixed(2)} in category "${category}" requires human approval.`,
307
- );
308
- this.name = 'ApprovalRequiredError';
309
- }
310
- }
311
-
312
- // ---------------------------------------------------------------------------
313
- // Utility
314
- // ---------------------------------------------------------------------------
315
-
316
- function nativeTokenForChain(chain: ChainId): TokenSymbol {
317
- switch (chain) {
318
- case 'solana': return 'SOL';
319
- case 'ethereum': return 'ETH';
320
- case 'base': return 'ETH';
321
- case 'polygon': return 'ETH'; // MATIC wraps to ETH for simplicity
322
- default: return 'ETH';
323
- }
324
- }
@@ -1,169 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview Pre-transaction guardrail that enforces spending limits,
4
- * category budgets, address whitelists/blacklists, and approval thresholds.
5
- *
6
- * All amounts are in USD. The enforcer tracks running totals via a
7
- * simple ledger interface — callers provide the persistence layer.
8
- *
9
- * @module wunderland/wallet/SpendingPolicyEnforcer
10
- */
11
-
12
- import type {
13
- SpendCategory,
14
- SpendCheckResult,
15
- SpendingLedgerEntry,
16
- SpendingPolicyConfig,
17
- } from './types.js';
18
-
19
- /** Minimal interface for querying the spending ledger (DB-agnostic). */
20
- export interface ISpendingLedgerStore {
21
- /** Sum all spending for the agent in the given period key (e.g. '2026-03-05'). */
22
- sumByPeriod(agentId: string, periodKey: string): Promise<number>;
23
- /** Sum spending for a specific category in the given period key. */
24
- sumByCategoryAndPeriod(agentId: string, category: SpendCategory, periodKey: string): Promise<number>;
25
- /** Record a new ledger entry after a successful spend. */
26
- insert(entry: SpendingLedgerEntry): Promise<void>;
27
- }
28
-
29
- export class SpendingPolicyEnforcer {
30
- constructor(
31
- private readonly policy: SpendingPolicyConfig,
32
- private readonly ledger: ISpendingLedgerStore,
33
- ) {}
34
-
35
- /**
36
- * Check whether a proposed spend is allowed under the current policy.
37
- */
38
- async canSpend(
39
- agentId: string,
40
- amountUsd: number,
41
- category: SpendCategory,
42
- recipientAddress?: string,
43
- ): Promise<SpendCheckResult> {
44
- // 1. Blocked category check
45
- if (this.policy.blockedCategories.includes(category)) {
46
- return { allowed: false, reason: `Category "${category}" is blocked by spending policy.` };
47
- }
48
-
49
- // 2. Address blacklist check
50
- if (recipientAddress && this.policy.blockedAddresses?.includes(recipientAddress)) {
51
- return { allowed: false, reason: `Recipient address is blocked.` };
52
- }
53
-
54
- // 3. Address whitelist check (if whitelist is set, only whitelisted addresses are allowed)
55
- if (
56
- recipientAddress
57
- && this.policy.allowedAddresses
58
- && this.policy.allowedAddresses.length > 0
59
- && !this.policy.allowedAddresses.includes(recipientAddress)
60
- ) {
61
- return { allowed: false, reason: `Recipient address is not in the allowed list.` };
62
- }
63
-
64
- // 4. Per-transaction limit
65
- if (amountUsd > this.policy.perTransactionLimitUsd) {
66
- return {
67
- allowed: false,
68
- reason: `Amount $${amountUsd.toFixed(2)} exceeds per-transaction limit of $${this.policy.perTransactionLimitUsd.toFixed(2)}.`,
69
- };
70
- }
71
-
72
- const today = getDailyKey();
73
- const month = getMonthlyKey();
74
-
75
- // 5. Daily global limit
76
- const dailySpent = await this.ledger.sumByPeriod(agentId, today);
77
- const remainingDailyUsd = this.policy.dailyLimitUsd - dailySpent;
78
- if (amountUsd > remainingDailyUsd) {
79
- return {
80
- allowed: false,
81
- reason: `Daily limit reached. Spent $${dailySpent.toFixed(2)} of $${this.policy.dailyLimitUsd.toFixed(2)} today.`,
82
- remainingDailyUsd: Math.max(0, remainingDailyUsd),
83
- };
84
- }
85
-
86
- // 6. Monthly global limit
87
- const monthlySpent = await this.ledger.sumByPeriod(agentId, month);
88
- const remainingMonthlyUsd = this.policy.monthlyLimitUsd - monthlySpent;
89
- if (amountUsd > remainingMonthlyUsd) {
90
- return {
91
- allowed: false,
92
- reason: `Monthly limit reached. Spent $${monthlySpent.toFixed(2)} of $${this.policy.monthlyLimitUsd.toFixed(2)} this month.`,
93
- remainingMonthlyUsd: Math.max(0, remainingMonthlyUsd),
94
- };
95
- }
96
-
97
- // 7. Per-category budget check
98
- const catBudget = this.policy.categoryBudgets.find((b) => b.category === category);
99
- if (catBudget) {
100
- const catDailySpent = await this.ledger.sumByCategoryAndPeriod(agentId, category, today);
101
- if (amountUsd > catBudget.dailyLimitUsd - catDailySpent) {
102
- return {
103
- allowed: false,
104
- reason: `Daily "${category}" budget exceeded. Spent $${catDailySpent.toFixed(2)} of $${catBudget.dailyLimitUsd.toFixed(2)}.`,
105
- };
106
- }
107
- const catMonthlySpent = await this.ledger.sumByCategoryAndPeriod(agentId, category, month);
108
- if (amountUsd > catBudget.monthlyLimitUsd - catMonthlySpent) {
109
- return {
110
- allowed: false,
111
- reason: `Monthly "${category}" budget exceeded. Spent $${catMonthlySpent.toFixed(2)} of $${catBudget.monthlyLimitUsd.toFixed(2)}.`,
112
- };
113
- }
114
- }
115
-
116
- // 8. Human approval threshold
117
- const requiresApproval = amountUsd > this.policy.requireApprovalAboveUsd;
118
-
119
- return {
120
- allowed: true,
121
- requiresApproval,
122
- remainingDailyUsd: remainingDailyUsd - amountUsd,
123
- remainingMonthlyUsd: remainingMonthlyUsd - amountUsd,
124
- };
125
- }
126
-
127
- /**
128
- * Record a completed spend in the ledger (call after successful tx).
129
- */
130
- async recordSpend(
131
- agentId: string,
132
- amountUsd: number,
133
- category: SpendCategory,
134
- ): Promise<void> {
135
- const now = Date.now();
136
- const dailyKey = getDailyKey();
137
- const monthlyKey = getMonthlyKey();
138
-
139
- // Record both daily and monthly entries
140
- await this.ledger.insert({
141
- id: `spend-${agentId}-${now}`,
142
- agentId,
143
- category,
144
- amountUsd,
145
- periodKey: dailyKey,
146
- createdAt: now,
147
- });
148
- await this.ledger.insert({
149
- id: `spend-${agentId}-${now}-m`,
150
- agentId,
151
- category,
152
- amountUsd,
153
- periodKey: monthlyKey,
154
- createdAt: now,
155
- });
156
- }
157
- }
158
-
159
- // ---------------------------------------------------------------------------
160
- // Date key helpers
161
- // ---------------------------------------------------------------------------
162
-
163
- function getDailyKey(): string {
164
- return new Date().toISOString().slice(0, 10); // YYYY-MM-DD
165
- }
166
-
167
- function getMonthlyKey(): string {
168
- return new Date().toISOString().slice(0, 7); // YYYY-MM
169
- }