Skip to content

Commit b8a78b5

Browse files
committed
unified storage
1 parent 43d90d4 commit b8a78b5

File tree

6 files changed

+117
-83
lines changed

6 files changed

+117
-83
lines changed

examples/cointoss/.env.example

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,4 @@ NETWORK_ID=base-sepolia # base-mainnet or others
66
OPENAI_API_KEY= # the OpenAI API key
77
CDP_API_KEY_NAME= # the name of the CDP API key
88
CDP_API_KEY_PRIVATE_KEY= # the private key for the CDP API key
9-
XMTP_ENV=local # the environment to use for XMTP
10-
REDIS_URL= # the URL for the Redis database
9+
XMTP_ENV=local # the environment to use for XMTP

examples/cointoss/README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ This CoinToss agent combines several technologies:
5252

5353
1. **XMTP Protocol**: For group chat messaging interface
5454
2. **Coinbase AgentKit**: For wallet management and payments
55-
3. **Storage Options**: Redis or local file storage for toss and wallet data
55+
3. **Storage Options**: Local storage for toss and wallet data
5656
4. **LLM Integration**: For natural language toss parsing
5757

5858
The agent workflow:
@@ -102,7 +102,6 @@ rm -rf .data/wallets
102102
- **Toss Logic**:
103103
- Random selection of winning option
104104
- Fair prize distribution among winners
105-
- **Storage Options**: Local file storage or Redis
106105

107106
## Security
108107

examples/cointoss/src/cdp.ts

+74-52
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "@coinbase/coinbase-sdk";
99
import { isAddress } from "viem";
1010
import { storage } from "./storage";
11-
import type { AgentWalletData, UserWallet } from "./types";
11+
import type { AgentWalletData } from "./types";
1212

1313
const coinbaseApiKeyName = process.env.CDP_API_KEY_NAME;
1414
let coinbaseApiKeyPrivateKey = process.env.CDP_API_KEY_PRIVATE_KEY;
@@ -55,78 +55,94 @@ export class WalletService {
5555
this.senderAddress = sender.toLowerCase();
5656
}
5757

58-
async createWallet(key: string): Promise<AgentWalletData> {
58+
async createWallet(inboxId: string): Promise<AgentWalletData> {
5959
try {
60-
key = key.toLowerCase();
60+
console.log(`Creating new wallet for key ${inboxId}...`);
6161

62+
// Initialize SDK if not already done
6263
if (!sdkInitialized) {
6364
sdkInitialized = initializeCoinbaseSDK();
6465
}
6566

67+
// Log the network we're using
68+
console.log(`Creating wallet on network: ${networkId}`);
69+
70+
// Create wallet
6671
const wallet = await Wallet.create({
67-
networkId: Coinbase.networks.BaseSepolia,
72+
networkId: networkId,
73+
}).catch((err: unknown) => {
74+
const errorDetails =
75+
typeof err === "object" ? JSON.stringify(err, null, 2) : err;
76+
console.error("Detailed wallet creation error:", errorDetails);
77+
throw err;
6878
});
6979

80+
console.log("Wallet created successfully, exporting data...");
7081
const data = wallet.export();
82+
83+
console.log("Getting default address...");
7184
const address = await wallet.getDefaultAddress();
7285
const walletAddress = address.getId();
7386

74-
await storage.saveUserWallet({
75-
userId: `wallet:${key}`,
76-
walletData: data,
77-
});
87+
// Make the wallet address visible in the logs for funding
88+
console.log("-----------------------------------------------------");
89+
console.log(`NEW WALLET CREATED FOR USER: ${inboxId}`);
90+
console.log(`WALLET ADDRESS: ${walletAddress}`);
91+
console.log(`NETWORK: ${networkId}`);
92+
console.log(`SEND FUNDS TO THIS ADDRESS TO TEST: ${walletAddress}`);
93+
console.log("-----------------------------------------------------");
7894

79-
return {
95+
const walletInfo: AgentWalletData = {
8096
id: walletAddress,
8197
wallet: wallet,
82-
data: data,
83-
human_address: this.senderAddress,
98+
walletData: data,
99+
human_address: inboxId,
84100
agent_address: walletAddress,
85-
inboxId: key,
101+
inboxId: inboxId,
86102
};
87-
} catch (error) {
103+
104+
const walletInfoToStore: AgentWalletData = {
105+
id: walletAddress,
106+
//no wallet
107+
walletData: data,
108+
human_address: inboxId,
109+
agent_address: walletAddress,
110+
inboxId: inboxId,
111+
};
112+
113+
await storage.saveUserWallet(inboxId, JSON.stringify(walletInfoToStore));
114+
console.log("Wallet created and saved successfully");
115+
return walletInfo;
116+
} catch (error: unknown) {
117+
console.error("Failed to create wallet:", error);
118+
119+
// Provide more detailed error information
88120
if (error instanceof Error) {
89121
throw new Error(`Wallet creation failed: ${error.message}`);
90122
}
123+
91124
throw new Error(`Failed to create wallet: ${String(error)}`);
92125
}
93126
}
94127

95-
async getWallet(
96-
inboxId: string,
97-
createIfNotFound: boolean = true,
98-
): Promise<AgentWalletData | undefined> {
128+
async getWallet(inboxId: string): Promise<AgentWalletData | undefined> {
99129
// Try to retrieve existing wallet data
100130
const walletData = await storage.getUserWallet(inboxId);
101-
if (!walletData) {
102-
return undefined;
131+
if (walletData === null) {
132+
console.log(`No wallet found for ${inboxId}, creating new one`);
133+
return this.createWallet(inboxId);
103134
}
104135

105-
try {
106-
const importedWallet = await Wallet.import(walletData.walletData);
107-
108-
return {
109-
id: importedWallet.getId() ?? "",
110-
wallet: importedWallet,
111-
data: walletData.walletData,
112-
human_address: walletData.userId,
113-
agent_address: walletData.userId,
114-
inboxId: walletData.userId,
115-
};
116-
} catch (error) {
117-
// If wallet data exists but is corrupted/invalid and we're allowed to create a new one
118-
if (createIfNotFound) {
119-
console.warn(
120-
`Failed to import existing wallet for ${inboxId}, creating new one`,
121-
);
122-
return this.createWallet(inboxId);
123-
}
136+
const importedWallet = await Wallet.import(walletData.walletData);
124137

125-
// Otherwise, fail explicitly
126-
throw new Error(
127-
`Invalid wallet data: ${error instanceof Error ? error.message : String(error)}`,
128-
);
129-
}
138+
return {
139+
id: importedWallet.getId() ?? "",
140+
wallet: importedWallet,
141+
walletData: walletData.walletData,
142+
human_address: walletData.human_address,
143+
agent_address: walletData.agent_address,
144+
inboxId: walletData.inboxId,
145+
};
130146
}
131147

132148
async transfer(
@@ -160,7 +176,7 @@ export class WalletService {
160176
console.log(
161177
`💰 Checking balance for source wallet: ${from.agent_address}...`,
162178
);
163-
const balance = await from.wallet.getBalance(Coinbase.assets.Usdc);
179+
const balance = await from.wallet?.getBalance(Coinbase.assets.Usdc);
164180
console.log(`💵 Available balance: ${Number(balance)} USDC`);
165181

166182
if (Number(balance) < amount) {
@@ -179,7 +195,7 @@ export class WalletService {
179195
// Get or validate destination wallet
180196
let destinationAddress = toAddress;
181197
console.log(`🔑 Validating destination: ${toAddress}...`);
182-
const to = await this.getWallet(toAddress, false);
198+
const to = await this.getWallet(toAddress);
183199
if (to) {
184200
destinationAddress = to.agent_address;
185201
console.log(`✅ Destination wallet found: ${destinationAddress}`);
@@ -198,7 +214,7 @@ export class WalletService {
198214
console.log(
199215
`🚀 Executing transfer of ${amount} USDC from ${from.agent_address} to ${destinationAddress}...`,
200216
);
201-
const transfer = await from.wallet.createTransfer({
217+
const transfer = await from.wallet?.createTransfer({
202218
amount,
203219
assetId: Coinbase.assets.Usdc,
204220
destination: destinationAddress,
@@ -207,7 +223,7 @@ export class WalletService {
207223

208224
console.log(`⏳ Waiting for transfer to complete...`);
209225
try {
210-
await transfer.wait();
226+
await transfer?.wait();
211227
console.log(`✅ Transfer completed successfully!`);
212228
console.log(
213229
`📝 Transaction details: ${JSON.stringify(transfer, null, 2)}`,
@@ -245,7 +261,7 @@ export class WalletService {
245261
return { address: undefined, balance: 0 };
246262
}
247263

248-
const balance = await walletData.wallet.getBalance(Coinbase.assets.Usdc);
264+
const balance = await walletData.wallet?.getBalance(Coinbase.assets.Usdc);
249265
return {
250266
address: walletData.agent_address,
251267
balance: Number(balance),
@@ -262,12 +278,14 @@ export class WalletService {
262278
const walletData = await this.getWallet(address);
263279
if (!walletData) return undefined;
264280

265-
const trade = await walletData.wallet.createTrade({
281+
const trade = await walletData.wallet?.createTrade({
266282
amount,
267283
fromAssetId,
268284
toAssetId,
269285
});
270286

287+
if (!trade) return undefined;
288+
271289
try {
272290
await trade.wait();
273291
} catch (err) {
@@ -283,12 +301,16 @@ export class WalletService {
283301
key = key.toLowerCase();
284302
const encryptedKey = `wallet:${key}`;
285303

286-
const emptyWallet: UserWallet = {
287-
userId: encryptedKey,
304+
const emptyWallet: AgentWalletData = {
305+
id: "",
306+
wallet: {} as Wallet,
288307
walletData: {} as WalletData,
308+
human_address: encryptedKey,
309+
agent_address: encryptedKey,
310+
inboxId: encryptedKey,
289311
};
290312

291-
await storage.saveUserWallet(emptyWallet);
313+
await storage.saveUserWallet(encryptedKey, JSON.stringify(emptyWallet));
292314
return true;
293315
}
294316
}

examples/cointoss/src/storage.ts

+38-21
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { existsSync, mkdirSync } from "fs";
22
import * as fs from "fs/promises";
33
import * as path from "path";
4-
import { TossStatus, type CoinTossGame, type UserWallet } from "./types";
4+
import { TossStatus, type AgentWalletData, type CoinTossGame } from "./types";
55

6-
const WALLET_STORAGE_DIR = ".data/wallet_data";
7-
const XMTP_STORAGE_DIR = ".data/xmtp";
8-
const TOSS_STORAGE_DIR = ".data/tosses";
6+
const networkId = process.env.NETWORK_ID;
7+
export const WALLET_KEY_PREFIX = "wallet_data:";
8+
export const WALLET_STORAGE_DIR = ".data/wallet_data";
9+
export const XMTP_STORAGE_DIR = ".data/xmtp";
10+
export const TOSS_STORAGE_DIR = ".data/tosses";
911

1012
/**
1113
* Storage service for coin toss game data and user wallets
@@ -40,15 +42,16 @@ class StorageService {
4042
*/
4143
private async saveToFile(
4244
directory: string,
43-
key: string,
44-
data: any,
45+
inboxId: string,
46+
data: string,
4547
): Promise<boolean> {
48+
const toRead = `${WALLET_KEY_PREFIX}${inboxId}-${networkId}`;
4649
try {
47-
const filePath = path.join(directory, `${key}.json`);
48-
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
50+
const filePath = path.join(directory, `${toRead}.json`);
51+
await fs.writeFile(filePath, data);
4952
return true;
5053
} catch (error) {
51-
console.error(`Error writing to file ${key}:`, error);
54+
console.error(`Error writing to file ${toRead}:`, error);
5255
return false;
5356
}
5457
}
@@ -58,19 +61,33 @@ class StorageService {
5861
*/
5962
private async readFromFile<T>(
6063
directory: string,
61-
key: string,
64+
inboxId: string,
6265
): Promise<T | null> {
63-
const filePath = path.join(directory, `${key}.json`);
64-
const data = await fs.readFile(filePath, "utf-8");
65-
return JSON.parse(data) as T;
66+
try {
67+
const key = `${WALLET_KEY_PREFIX}${inboxId}-${networkId}`;
68+
const filePath = path.join(directory, `${key}.json`);
69+
const data = await fs.readFile(filePath, "utf-8");
70+
return JSON.parse(data) as T;
71+
} catch (error) {
72+
// If file doesn't exist, return null
73+
if (
74+
error instanceof Error &&
75+
(error.message.includes("ENOENT") ||
76+
error.message.includes("no such file or directory"))
77+
) {
78+
return null;
79+
}
80+
// For other errors, rethrow
81+
throw error;
82+
}
6683
}
6784

6885
/**
6986
* Save a coin toss game
7087
*/
7188
public async saveGame(toss: CoinTossGame): Promise<void> {
7289
if (!this.initialized) this.initialize();
73-
await this.saveToFile(TOSS_STORAGE_DIR, toss.id, toss);
90+
await this.saveToFile(TOSS_STORAGE_DIR, toss.id, JSON.stringify(toss));
7491
}
7592

7693
/**
@@ -121,17 +138,20 @@ class StorageService {
121138
/**
122139
* Save user wallet data
123140
*/
124-
public async saveUserWallet(wallet: UserWallet): Promise<void> {
141+
public async saveUserWallet(
142+
inboxId: string,
143+
walletData: string,
144+
): Promise<void> {
125145
if (!this.initialized) this.initialize();
126-
await this.saveToFile(WALLET_STORAGE_DIR, wallet.userId, wallet);
146+
await this.saveToFile(WALLET_STORAGE_DIR, inboxId, walletData);
127147
}
128148

129149
/**
130150
* Get user wallet data by user ID
131151
*/
132-
public async getUserWallet(userId: string): Promise<UserWallet | null> {
152+
public async getUserWallet(inboxId: string): Promise<AgentWalletData | null> {
133153
if (!this.initialized) this.initialize();
134-
return this.readFromFile<UserWallet>(WALLET_STORAGE_DIR, userId);
154+
return this.readFromFile<AgentWalletData>(WALLET_STORAGE_DIR, inboxId);
135155
}
136156

137157
/**
@@ -167,6 +187,3 @@ const storage = new StorageService();
167187

168188
// Export the storage instance
169189
export { storage };
170-
171-
// Export constants for backward compatibility
172-
export { WALLET_STORAGE_DIR, XMTP_STORAGE_DIR, TOSS_STORAGE_DIR };

examples/cointoss/src/toss.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class GameManager {
3939
// Get a player's wallet address from their user ID
4040
async getPlayerWalletAddress(userId: string): Promise<string | undefined> {
4141
try {
42-
const walletData = await this.walletService.getWallet(userId, false);
42+
const walletData = await this.walletService.getWallet(userId);
4343
return walletData?.agent_address;
4444
} catch (error) {
4545
console.error(`Error getting wallet address for ${userId}:`, error);
@@ -392,7 +392,6 @@ export class GameManager {
392392
// Get the winner's wallet address
393393
const winnerWalletData = await this.walletService.getWallet(
394394
winner.userId,
395-
false,
396395
);
397396
if (!winnerWalletData) {
398397
console.error(

examples/cointoss/src/types.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,11 @@ export interface XMTPUser {
6161
// Agent wallet data
6262
export type AgentWalletData = {
6363
id: string;
64-
wallet: Wallet;
65-
data: WalletData;
64+
walletData: WalletData;
6665
human_address: string;
6766
agent_address: string;
68-
blockchain?: string;
69-
state?: string;
7067
inboxId: string;
68+
wallet?: Wallet;
7169
};
7270

7371
export interface AgentConfig {

0 commit comments

Comments
 (0)