@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.
- package/LICENSE +98 -0
- package/dist/AgentWalletManager.d.ts +68 -0
- package/dist/AgentWalletManager.d.ts.map +1 -0
- package/dist/AgentWalletManager.js +238 -0
- package/dist/AgentWalletManager.js.map +1 -0
- package/dist/SpendingPolicyEnforcer.d.ts +33 -0
- package/dist/SpendingPolicyEnforcer.d.ts.map +1 -0
- package/dist/SpendingPolicyEnforcer.js +128 -0
- package/dist/SpendingPolicyEnforcer.js.map +1 -0
- package/dist/cards/CardManager.d.ts +71 -0
- package/dist/cards/CardManager.d.ts.map +1 -0
- package/dist/cards/CardManager.js +212 -0
- package/dist/cards/CardManager.js.map +1 -0
- package/dist/cards/LithicCardAdapter.d.ts +40 -0
- package/dist/cards/LithicCardAdapter.d.ts.map +1 -0
- package/dist/cards/LithicCardAdapter.js +116 -0
- package/dist/cards/LithicCardAdapter.js.map +1 -0
- package/dist/cards/MccCategoryMap.d.ts +36 -0
- package/dist/cards/MccCategoryMap.d.ts.map +1 -0
- package/dist/cards/MccCategoryMap.js +113 -0
- package/dist/cards/MccCategoryMap.js.map +1 -0
- package/dist/cards/types.d.ts +131 -0
- package/dist/cards/types.d.ts.map +1 -0
- package/dist/cards/types.js +12 -0
- package/dist/cards/types.js.map +1 -0
- package/dist/chains/EvmWalletAdapter.d.ts +30 -0
- package/dist/chains/EvmWalletAdapter.d.ts.map +1 -0
- package/dist/chains/EvmWalletAdapter.js +135 -0
- package/dist/chains/EvmWalletAdapter.js.map +1 -0
- package/dist/chains/SolanaWalletAdapter.d.ts +32 -0
- package/dist/chains/SolanaWalletAdapter.d.ts.map +1 -0
- package/dist/chains/SolanaWalletAdapter.js +128 -0
- package/dist/chains/SolanaWalletAdapter.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +228 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/CardSpendingSummaryTool.d.ts +14 -0
- package/dist/tools/CardSpendingSummaryTool.d.ts.map +1 -0
- package/dist/tools/CardSpendingSummaryTool.js +40 -0
- package/dist/tools/CardSpendingSummaryTool.js.map +1 -0
- package/dist/tools/CardStatusTool.d.ts +21 -0
- package/dist/tools/CardStatusTool.d.ts.map +1 -0
- package/dist/tools/CardStatusTool.js +48 -0
- package/dist/tools/CardStatusTool.js.map +1 -0
- package/dist/tools/CheckBalanceTool.d.ts +23 -0
- package/dist/tools/CheckBalanceTool.d.ts.map +1 -0
- package/dist/tools/CheckBalanceTool.js +116 -0
- package/dist/tools/CheckBalanceTool.js.map +1 -0
- package/dist/tools/FreezeCardTool.d.ts +15 -0
- package/dist/tools/FreezeCardTool.d.ts.map +1 -0
- package/dist/tools/FreezeCardTool.js +39 -0
- package/dist/tools/FreezeCardTool.js.map +1 -0
- package/dist/tools/IssueCardTool.d.ts +22 -0
- package/dist/tools/IssueCardTool.d.ts.map +1 -0
- package/dist/tools/IssueCardTool.js +55 -0
- package/dist/tools/IssueCardTool.js.map +1 -0
- package/dist/tools/PayWithCardTool.d.ts +29 -0
- package/dist/tools/PayWithCardTool.d.ts.map +1 -0
- package/dist/tools/PayWithCardTool.js +79 -0
- package/dist/tools/PayWithCardTool.js.map +1 -0
- package/dist/tools/SendCryptoTool.d.ts +33 -0
- package/dist/tools/SendCryptoTool.d.ts.map +1 -0
- package/dist/tools/SendCryptoTool.js +136 -0
- package/dist/tools/SendCryptoTool.js.map +1 -0
- package/dist/tools/UnfreezeCardTool.d.ts +15 -0
- package/dist/tools/UnfreezeCardTool.d.ts.map +1 -0
- package/dist/tools/UnfreezeCardTool.js +39 -0
- package/dist/tools/UnfreezeCardTool.js.map +1 -0
- package/dist/tools/WalletHistoryTool.d.ts +35 -0
- package/dist/tools/WalletHistoryTool.d.ts.map +1 -0
- package/dist/tools/WalletHistoryTool.js +75 -0
- package/dist/tools/WalletHistoryTool.js.map +1 -0
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/manifest.json +98 -0
- package/package.json +78 -0
- package/src/AgentWalletManager.ts +324 -0
- package/src/SpendingPolicyEnforcer.ts +169 -0
- package/src/cards/CardManager.ts +274 -0
- package/src/cards/LithicCardAdapter.ts +159 -0
- package/src/cards/MccCategoryMap.ts +138 -0
- package/src/cards/types.ts +169 -0
- package/src/chains/EvmWalletAdapter.ts +170 -0
- package/src/chains/SolanaWalletAdapter.ts +184 -0
- package/src/index.ts +315 -0
- package/src/tools/CardSpendingSummaryTool.ts +52 -0
- package/src/tools/CardStatusTool.ts +68 -0
- package/src/tools/CheckBalanceTool.ts +145 -0
- package/src/tools/FreezeCardTool.ts +52 -0
- package/src/tools/IssueCardTool.ts +76 -0
- package/src/tools/PayWithCardTool.ts +109 -0
- package/src/tools/SendCryptoTool.ts +185 -0
- package/src/tools/UnfreezeCardTool.ts +52 -0
- package/src/tools/WalletHistoryTool.ts +113 -0
- package/src/types.ts +230 -0
- package/test/AgentWalletManager.spec.ts +301 -0
- package/test/CardManager.spec.ts +428 -0
- package/test/LithicCardAdapter.spec.ts +224 -0
- package/test/MccCategoryMap.spec.ts +153 -0
- package/test/SpendingPolicyEnforcer.spec.ts +254 -0
- package/test/card-integration.spec.ts +242 -0
- package/test/card-tools.spec.ts +285 -0
- package/test/integration.spec.ts +171 -0
- package/test/wallet-tools.spec.ts +275 -0
- package/tsconfig.json +22 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Wallet types, spending policy, and category definitions
|
|
3
|
+
* for the Agent Personal Wallet system.
|
|
4
|
+
*
|
|
5
|
+
* Enables Wunderland agents to autonomously spend within user-configured
|
|
6
|
+
* guardrails — covering crypto (Solana + EVM) and virtual cards (Lithic).
|
|
7
|
+
*
|
|
8
|
+
* @module wunderland/wallet/types
|
|
9
|
+
*/
|
|
10
|
+
export type ChainId = 'solana' | 'ethereum' | 'base' | 'polygon';
|
|
11
|
+
export type TokenSymbol = 'SOL' | 'ETH' | 'USDC' | 'USDT';
|
|
12
|
+
/** Token metadata for a specific chain deployment. */
|
|
13
|
+
export interface TokenInfo {
|
|
14
|
+
symbol: TokenSymbol;
|
|
15
|
+
chain: ChainId;
|
|
16
|
+
/** Mint address (Solana SPL) or contract address (ERC-20). Empty for native tokens. */
|
|
17
|
+
address: string;
|
|
18
|
+
decimals: number;
|
|
19
|
+
/** CoinGecko ID for price lookups. */
|
|
20
|
+
coingeckoId?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Spend categories used for budget enforcement and card MCC mapping.
|
|
24
|
+
*
|
|
25
|
+
* Crypto categories: api_costs, web_services, subscriptions, transfers, defi
|
|
26
|
+
* Card categories: shopping, dining, travel, entertainment, utilities
|
|
27
|
+
* Catch-all: other
|
|
28
|
+
*/
|
|
29
|
+
export type SpendCategory = 'api_costs' | 'web_services' | 'shopping' | 'subscriptions' | 'transfers' | 'defi' | 'dining' | 'travel' | 'entertainment' | 'utilities' | 'other';
|
|
30
|
+
export interface CategoryBudget {
|
|
31
|
+
category: SpendCategory;
|
|
32
|
+
/** Max daily spend in USD for this category. */
|
|
33
|
+
dailyLimitUsd: number;
|
|
34
|
+
/** Max monthly spend in USD for this category. */
|
|
35
|
+
monthlyLimitUsd: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SpendingPolicyConfig {
|
|
38
|
+
/** Global daily spending cap in USD across all categories. */
|
|
39
|
+
dailyLimitUsd: number;
|
|
40
|
+
/** Max single transaction amount in USD. */
|
|
41
|
+
perTransactionLimitUsd: number;
|
|
42
|
+
/** Global monthly spending cap in USD. */
|
|
43
|
+
monthlyLimitUsd: number;
|
|
44
|
+
/** Per-category budget overrides. Categories not listed inherit global limits. */
|
|
45
|
+
categoryBudgets: CategoryBudget[];
|
|
46
|
+
/** Transactions above this USD amount require human approval. */
|
|
47
|
+
requireApprovalAboveUsd: number;
|
|
48
|
+
/** Categories the agent is completely blocked from spending in. */
|
|
49
|
+
blockedCategories: SpendCategory[];
|
|
50
|
+
/** Crypto address whitelist — if set, only these addresses can receive funds. */
|
|
51
|
+
allowedAddresses?: string[];
|
|
52
|
+
/** Crypto address blacklist — these addresses are always blocked. */
|
|
53
|
+
blockedAddresses?: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface WalletConfig {
|
|
56
|
+
/** Master switch for wallet functionality. */
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
/** Which chains the agent has wallets on. */
|
|
59
|
+
chains: ChainId[];
|
|
60
|
+
/**
|
|
61
|
+
* Key custody mode:
|
|
62
|
+
* - hot: plaintext in memory (dev/testing only)
|
|
63
|
+
* - encrypted-hot: AES-256-GCM encrypted at rest, decrypted on sign
|
|
64
|
+
*/
|
|
65
|
+
custodyMode: 'hot' | 'encrypted-hot';
|
|
66
|
+
/** Tokens the agent is allowed to hold/send. */
|
|
67
|
+
allowedTokens: TokenSymbol[];
|
|
68
|
+
/** Spending guardrails. */
|
|
69
|
+
spendingPolicy: SpendingPolicyConfig;
|
|
70
|
+
/** Virtual card configuration (Lithic). */
|
|
71
|
+
card?: import('./cards/types.js').CardConfig;
|
|
72
|
+
}
|
|
73
|
+
export type WalletDirection = 'inbound' | 'outbound';
|
|
74
|
+
export type WalletTxStatus = 'pending' | 'confirmed' | 'failed';
|
|
75
|
+
export interface AgentWalletRecord {
|
|
76
|
+
id: string;
|
|
77
|
+
agentId: string;
|
|
78
|
+
chain: ChainId;
|
|
79
|
+
/** Public address (base58 for Solana, 0x-prefixed hex for EVM). */
|
|
80
|
+
address: string;
|
|
81
|
+
/** AES-256-GCM encrypted private key (base64). */
|
|
82
|
+
encryptedKey: string;
|
|
83
|
+
/** Salt used for key derivation (hex). */
|
|
84
|
+
keyDerivationSalt: string;
|
|
85
|
+
createdAt: number;
|
|
86
|
+
}
|
|
87
|
+
export interface WalletTransactionRecord {
|
|
88
|
+
id: string;
|
|
89
|
+
walletId: string;
|
|
90
|
+
txHash?: string;
|
|
91
|
+
direction: WalletDirection;
|
|
92
|
+
toAddress?: string;
|
|
93
|
+
fromAddress?: string;
|
|
94
|
+
/** Raw token amount as string (lamports, wei, etc.). */
|
|
95
|
+
amountRaw: string;
|
|
96
|
+
/** USD equivalent at time of transaction. */
|
|
97
|
+
amountUsd?: number;
|
|
98
|
+
token: TokenSymbol;
|
|
99
|
+
category?: SpendCategory;
|
|
100
|
+
status: WalletTxStatus;
|
|
101
|
+
description?: string;
|
|
102
|
+
createdAt: number;
|
|
103
|
+
}
|
|
104
|
+
export interface SpendingLedgerEntry {
|
|
105
|
+
id: string;
|
|
106
|
+
agentId: string;
|
|
107
|
+
category: SpendCategory;
|
|
108
|
+
amountUsd: number;
|
|
109
|
+
/** Date key: 'YYYY-MM-DD' for daily, 'YYYY-MM' for monthly. */
|
|
110
|
+
periodKey: string;
|
|
111
|
+
createdAt: number;
|
|
112
|
+
}
|
|
113
|
+
export interface SpendCheckResult {
|
|
114
|
+
allowed: boolean;
|
|
115
|
+
/** Human-readable reason if blocked. */
|
|
116
|
+
reason?: string;
|
|
117
|
+
/** True if the transaction needs explicit human approval (above threshold but within limits). */
|
|
118
|
+
requiresApproval?: boolean;
|
|
119
|
+
/** Remaining daily budget in USD after this transaction (if allowed). */
|
|
120
|
+
remainingDailyUsd?: number;
|
|
121
|
+
/** Remaining monthly budget in USD after this transaction (if allowed). */
|
|
122
|
+
remainingMonthlyUsd?: number;
|
|
123
|
+
}
|
|
124
|
+
export interface IChainWalletAdapter {
|
|
125
|
+
readonly chain: ChainId;
|
|
126
|
+
/** Generate a new keypair. Returns { publicKey, secretKey (raw bytes) }. */
|
|
127
|
+
generateKeypair(): Promise<{
|
|
128
|
+
publicKey: string;
|
|
129
|
+
secretKey: Uint8Array;
|
|
130
|
+
}>;
|
|
131
|
+
/** Get native token balance in raw units (lamports, wei). */
|
|
132
|
+
getBalance(address: string): Promise<bigint>;
|
|
133
|
+
/** Get SPL/ERC-20 token balance in raw units. */
|
|
134
|
+
getTokenBalance(address: string, tokenMint: string): Promise<bigint>;
|
|
135
|
+
/** Build and sign a native transfer transaction. Returns serialized signed tx. */
|
|
136
|
+
signTransfer(secretKey: Uint8Array, to: string, amountRaw: bigint): Promise<Uint8Array>;
|
|
137
|
+
/** Build and sign a token transfer. Returns serialized signed tx. */
|
|
138
|
+
signTokenTransfer(secretKey: Uint8Array, to: string, tokenMint: string, amountRaw: bigint): Promise<Uint8Array>;
|
|
139
|
+
/** Broadcast a signed transaction and return the tx hash. */
|
|
140
|
+
broadcast(signedTx: Uint8Array): Promise<string>;
|
|
141
|
+
/** Check if a transaction is confirmed. */
|
|
142
|
+
getTransactionStatus(txHash: string): Promise<WalletTxStatus>;
|
|
143
|
+
}
|
|
144
|
+
export declare const DEFAULT_SPENDING_POLICY: SpendingPolicyConfig;
|
|
145
|
+
export declare const DEFAULT_WALLET_CONFIG: WalletConfig;
|
|
146
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Wallet types, spending policy, and category definitions
|
|
4
|
+
* for the Agent Personal Wallet system.
|
|
5
|
+
*
|
|
6
|
+
* Enables Wunderland agents to autonomously spend within user-configured
|
|
7
|
+
* guardrails — covering crypto (Solana + EVM) and virtual cards (Lithic).
|
|
8
|
+
*
|
|
9
|
+
* @module wunderland/wallet/types
|
|
10
|
+
*/
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Default spending policy
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export const DEFAULT_SPENDING_POLICY = {
|
|
15
|
+
dailyLimitUsd: 50,
|
|
16
|
+
perTransactionLimitUsd: 20,
|
|
17
|
+
monthlyLimitUsd: 500,
|
|
18
|
+
categoryBudgets: [],
|
|
19
|
+
requireApprovalAboveUsd: 10,
|
|
20
|
+
blockedCategories: [],
|
|
21
|
+
};
|
|
22
|
+
export const DEFAULT_WALLET_CONFIG = {
|
|
23
|
+
enabled: false,
|
|
24
|
+
chains: ['solana'],
|
|
25
|
+
custodyMode: 'encrypted-hot',
|
|
26
|
+
allowedTokens: ['SOL', 'USDC'],
|
|
27
|
+
spendingPolicy: DEFAULT_SPENDING_POLICY,
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,cAAc;AACd;;;;;;;;GAQG;AAyMH,8EAA8E;AAC9E,0BAA0B;AAC1B,8EAA8E;AAE9E,MAAM,CAAC,MAAM,uBAAuB,GAAyB;IAC3D,aAAa,EAAE,EAAE;IACjB,sBAAsB,EAAE,EAAE;IAC1B,eAAe,EAAE,GAAG;IACpB,eAAe,EAAE,EAAE;IACnB,uBAAuB,EAAE,EAAE;IAC3B,iBAAiB,EAAE,EAAE;CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAiB;IACjD,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,WAAW,EAAE,eAAe;IAC5B,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,uBAAuB;CACxC,CAAC"}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
|
|
3
|
+
"id": "com.framers.tools.wallet",
|
|
4
|
+
"name": "Agent Personal Wallet",
|
|
5
|
+
"version": "0.2.0",
|
|
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
|
+
"author": {
|
|
8
|
+
"name": "Frame.dev",
|
|
9
|
+
"email": "support@frame.dev",
|
|
10
|
+
"url": "https://github.com/framersai"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": ["wallet", "crypto", "solana", "ethereum", "spending-policy", "guardrails", "defi", "card", "lithic", "virtual-card"],
|
|
14
|
+
"agentosVersion": "^2.0.0",
|
|
15
|
+
"categories": ["tool", "finance"],
|
|
16
|
+
"platforms": [],
|
|
17
|
+
"sdk": "none",
|
|
18
|
+
"extensions": [
|
|
19
|
+
{
|
|
20
|
+
"kind": "tool",
|
|
21
|
+
"id": "check_wallet_balance",
|
|
22
|
+
"displayName": "Check Wallet Balance",
|
|
23
|
+
"description": "Check the balance of the agent's personal wallet on a given blockchain",
|
|
24
|
+
"entry": "./dist/tools/CheckBalanceTool.js"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"kind": "tool",
|
|
28
|
+
"id": "send_crypto",
|
|
29
|
+
"displayName": "Send Crypto",
|
|
30
|
+
"description": "Send cryptocurrency with spending policy enforcement",
|
|
31
|
+
"entry": "./dist/tools/SendCryptoTool.js"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"kind": "tool",
|
|
35
|
+
"id": "wallet_transaction_history",
|
|
36
|
+
"displayName": "Wallet Transaction History",
|
|
37
|
+
"description": "View recent transaction history for the agent's wallet",
|
|
38
|
+
"entry": "./dist/tools/WalletHistoryTool.js"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"kind": "tool",
|
|
42
|
+
"id": "issue_virtual_card",
|
|
43
|
+
"displayName": "Issue Virtual Card",
|
|
44
|
+
"description": "Issue a new Lithic virtual debit card for the agent with configurable spending limits",
|
|
45
|
+
"entry": "./dist/tools/IssueCardTool.js"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"kind": "tool",
|
|
49
|
+
"id": "card_status",
|
|
50
|
+
"displayName": "Card Status",
|
|
51
|
+
"description": "Get the current status of the agent's virtual card",
|
|
52
|
+
"entry": "./dist/tools/CardStatusTool.js"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"kind": "tool",
|
|
56
|
+
"id": "freeze_card",
|
|
57
|
+
"displayName": "Freeze Card",
|
|
58
|
+
"description": "Temporarily freeze the agent's virtual card to pause all transactions",
|
|
59
|
+
"entry": "./dist/tools/FreezeCardTool.js"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"kind": "tool",
|
|
63
|
+
"id": "unfreeze_card",
|
|
64
|
+
"displayName": "Unfreeze Card",
|
|
65
|
+
"description": "Resume the agent's virtual card after it was frozen",
|
|
66
|
+
"entry": "./dist/tools/UnfreezeCardTool.js"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"kind": "tool",
|
|
70
|
+
"id": "card_spending_summary",
|
|
71
|
+
"displayName": "Card Spending Summary",
|
|
72
|
+
"description": "Get a spending breakdown by category for the agent's virtual card",
|
|
73
|
+
"entry": "./dist/tools/CardSpendingSummaryTool.js"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"kind": "tool",
|
|
77
|
+
"id": "pay_with_card",
|
|
78
|
+
"displayName": "Pay With Card",
|
|
79
|
+
"description": "Retrieve card details (PAN, CVV, expiry) for online payments with spending policy enforcement",
|
|
80
|
+
"entry": "./dist/tools/PayWithCardTool.js"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"requiredSecrets": ["WALLET_MASTER_SECRET"],
|
|
84
|
+
"optionalSecrets": ["LITHIC_API_KEY", "LITHIC_WEBHOOK_SECRET"],
|
|
85
|
+
"features": [
|
|
86
|
+
"Multi-chain support (Solana, Ethereum, Base, Polygon)",
|
|
87
|
+
"AES-256-GCM encrypted key storage",
|
|
88
|
+
"Configurable spending guardrails per category",
|
|
89
|
+
"Daily, monthly, and per-transaction limits",
|
|
90
|
+
"Human-in-the-loop approval for high-value transactions",
|
|
91
|
+
"Address whitelists and blacklists",
|
|
92
|
+
"Native token and SPL/ERC-20 token transfers",
|
|
93
|
+
"Lithic virtual debit card issuance",
|
|
94
|
+
"MCC-based category mapping for card transactions",
|
|
95
|
+
"Card freeze/unfreeze controls",
|
|
96
|
+
"Card spending summary by category"
|
|
97
|
+
]
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@framers/agentos-ext-wallet",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Agent Personal Wallet — autonomous crypto spending with guardrails, multi-chain support (Solana + EVM), and virtual card integration.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": true,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./manifest.json": "./manifest.json",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"agentos",
|
|
19
|
+
"extension",
|
|
20
|
+
"wallet",
|
|
21
|
+
"crypto",
|
|
22
|
+
"solana",
|
|
23
|
+
"ethereum",
|
|
24
|
+
"spending-policy",
|
|
25
|
+
"guardrails"
|
|
26
|
+
],
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Frame.dev",
|
|
29
|
+
"email": "support@frame.dev",
|
|
30
|
+
"url": "https://github.com/framersai"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"uuid": "^9.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@framers/agentos": "^0.5.2",
|
|
38
|
+
"@solana/web3.js": "^1.0.0",
|
|
39
|
+
"@solana/spl-token": "^0.3.0",
|
|
40
|
+
"ethers": "^6.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@solana/web3.js": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"@solana/spl-token": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"ethers": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.10.0",
|
|
55
|
+
"@types/uuid": "^9.0.0",
|
|
56
|
+
"typescript": "^5.4.0",
|
|
57
|
+
"vitest": "^1.3.0"
|
|
58
|
+
},
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/framersai/agentos-extensions.git",
|
|
62
|
+
"directory": "registry/curated/tools/wallet"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/framersai/agentos-extensions/tree/master/registry/curated/tools/wallet#readme",
|
|
65
|
+
"bugs": {
|
|
66
|
+
"url": "https://github.com/framersai/agentos-extensions/issues"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsc",
|
|
70
|
+
"dev": "tsc --watch",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"test:watch": "vitest",
|
|
73
|
+
"test:coverage": "vitest run --coverage",
|
|
74
|
+
"lint": "eslint src --ext .ts",
|
|
75
|
+
"typecheck": "tsc --noEmit",
|
|
76
|
+
"clean": "rimraf dist"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
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
|
+
}
|