Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add wallet history (transactions, balances) to coinbase providers #658

Merged
merged 3 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/plugin-coinbase/src/plugins/commerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
} from "@ai16z/eliza";
import { ChargeContent, ChargeSchema, isChargeContent } from "../types";
import { chargeTemplate, getChargeTemplate } from "../templates";
import { getWalletDetails } from "../utils";
import { Coinbase } from "@coinbase/coinbase-sdk";

const url = "https://api.commerce.coinbase.com/charges";
interface ChargeRequest {
Expand Down Expand Up @@ -420,7 +422,26 @@ export const chargeProvider: Provider = {
const charges = await getAllCharges(
runtime.getSetting("COINBASE_COMMERCE_KEY")
);
return charges.data;
// Ensure API key is available
const coinbaseAPIKey =
runtime.getSetting("COINBASE_API_KEY") ??
process.env.COINBASE_API_KEY;
const coinbasePrivateKey =
runtime.getSetting("COINBASE_PRIVATE_KEY") ??
process.env.COINBASE_PRIVATE_KEY;
let balances = [];
let transactions = [];
if (coinbaseAPIKey && coinbasePrivateKey) {
Coinbase.configure({
apiKeyName: coinbaseAPIKey,
privateKey: coinbasePrivateKey,
});
const { balances, transactions } = await getWalletDetails(runtime);
elizaLogger.log("Current Balances:", balances);
elizaLogger.log("Last Transactions:", transactions);
}
elizaLogger.log("Charges:", charges);
return { charges: charges.data, balances, transactions };
},
};

Expand Down
41 changes: 29 additions & 12 deletions packages/plugin-coinbase/src/plugins/massPayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import { createArrayCsvWriter } from "csv-writer";
import { initializeWallet } from "../utils";
import { getWalletDetails, initializeWallet } from "../utils";

// Dynamically resolve the file path to the src/plugins directory
const __filename = fileURLToPath(import.meta.url);
Expand All @@ -34,11 +34,19 @@ const baseDir = path.resolve(__dirname, "../../plugin-coinbase/src/plugins");
const csvFilePath = path.join(baseDir, "transactions.csv");

export const massPayoutProvider: Provider = {
get: async (_runtime: IAgentRuntime, _message: Memory) => {
get: async (runtime: IAgentRuntime, _message: Memory) => {
try {
Coinbase.configure({
apiKeyName:
runtime.getSetting("COINBASE_API_KEY") ??
process.env.COINBASE_API_KEY,
privateKey:
runtime.getSetting("COINBASE_PRIVATE_KEY") ??
process.env.COINBASE_PRIVATE_KEY,
});
elizaLogger.log("Reading CSV file from:", csvFilePath);

// Check if the file exists; if not, create it with headers
// Ensure the CSV file exists
if (!fs.existsSync(csvFilePath)) {
elizaLogger.warn("CSV file not found. Creating a new one.");
const csvWriter = createArrayCsvWriter({
Expand All @@ -62,17 +70,26 @@ export const massPayoutProvider: Provider = {
skip_empty_lines: true,
});

const { balances, transactions } = await getWalletDetails(runtime);

elizaLogger.log("Parsed CSV records:", records);
return records.map((record: any) => ({
address: record["Address"] || undefined,
amount: parseFloat(record["Amount"]) || undefined,
status: record["Status"] || undefined,
errorCode: record["Error Code"] || "",
transactionUrl: record["Transaction URL"] || "",
}));
elizaLogger.log("Current Balances:", balances);
elizaLogger.log("Last Transactions:", transactions);

return {
currentTransactions: records.map((record: any) => ({
address: record["Address"] || undefined,
amount: parseFloat(record["Amount"]) || undefined,
status: record["Status"] || undefined,
errorCode: record["Error Code"] || "",
transactionUrl: record["Transaction URL"] || "",
})),
balances,
transactionHistory: transactions,
};
} catch (error) {
elizaLogger.error("Error in massPayoutProvider:", error);
return [];
return { csvRecords: [], balances: [], transactions: [] };
}
},
};
Expand Down Expand Up @@ -367,7 +384,7 @@ Check the CSV file for full details.`,
{
user: "{{user1}}",
content: {
text: "Distribute 0.0001 ETH on base network to 0xA0ba2ACB5846A54834173fB0DD9444F756810f06 and 0xF14F2c49aa90BaFA223EE074C1C33b59891826bF",
text: "Distribute 0.0001 ETH on base to 0xA0ba2ACB5846A54834173fB0DD9444F756810f06 and 0xF14F2c49aa90BaFA223EE074C1C33b59891826bF",
},
},
{
Expand Down
39 changes: 27 additions & 12 deletions packages/plugin-coinbase/src/plugins/trade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ModelClass,
Provider,
} from "@ai16z/eliza";
import { initializeWallet } from "../utils";
import { getWalletDetails, initializeWallet } from "../utils";
import { tradeTemplate } from "../templates";
import { isTradeContent, TradeContent, TradeSchema } from "../types";
import { readFile } from "fs/promises";
Expand All @@ -29,8 +29,16 @@ const baseDir = path.resolve(__dirname, "../../plugin-coinbase/src/plugins");
const tradeCsvFilePath = path.join(baseDir, "trades.csv");

export const tradeProvider: Provider = {
get: async (_runtime: IAgentRuntime, _message: Memory) => {
get: async (runtime: IAgentRuntime, _message: Memory) => {
try {
Coinbase.configure({
apiKeyName:
runtime.getSetting("COINBASE_API_KEY") ??
process.env.COINBASE_API_KEY,
privateKey:
runtime.getSetting("COINBASE_PRIVATE_KEY") ??
process.env.COINBASE_PRIVATE_KEY,
});
elizaLogger.log("Reading CSV file from:", tradeCsvFilePath);

// Check if the file exists; if not, create it with headers
Expand Down Expand Up @@ -60,15 +68,22 @@ export const tradeProvider: Provider = {
});

elizaLogger.log("Parsed CSV records:", records);
return records.map((record: any) => ({
network: record["Network"] || undefined,
amount: parseFloat(record["From Amount"]) || undefined,
sourceAsset: record["Source Asset"] || undefined,
toAmount: parseFloat(record["To Amount"]) || undefined,
targetAsset: record["Target Asset"] || undefined,
status: record["Status"] || undefined,
transactionUrl: record["Transaction URL"] || "",
}));
const { balances, transactions } = await getWalletDetails(runtime);
elizaLogger.log("Current Balances:", balances);
elizaLogger.log("Last Transactions:", transactions);
return {
currentTrades: records.map((record: any) => ({
network: record["Network"] || undefined,
amount: parseFloat(record["From Amount"]) || undefined,
sourceAsset: record["Source Asset"] || undefined,
toAmount: parseFloat(record["To Amount"]) || undefined,
targetAsset: record["Target Asset"] || undefined,
status: record["Status"] || undefined,
transactionUrl: record["Transaction URL"] || "",
})),
balances,
transactions,
};
} catch (error) {
elizaLogger.error("Error in tradeProvider:", error);
return [];
Expand Down Expand Up @@ -232,7 +247,7 @@ export const executeTradeAction: Action = {
{
user: "{{user1}}",
content: {
text: "Trade 0.00001 ETH for USDC on the base",
text: "Trade 0.00001 ETH for USDC on base",
},
},
{
Expand Down
82 changes: 80 additions & 2 deletions packages/plugin-coinbase/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Wallet, WalletData } from "@coinbase/coinbase-sdk";
import { Coinbase, Wallet, WalletData } from "@coinbase/coinbase-sdk";
import { elizaLogger, IAgentRuntime } from "@ai16z/eliza";
import fs from "fs";
import path from "path";
import { EthereumTransaction } from "@coinbase/coinbase-sdk/dist/client";

export async function initializeWallet(
runtime: IAgentRuntime,
networkId: string
networkId: string = Coinbase.networks.EthereumMainnet
) {
let wallet: Wallet;
const storedSeed =
Expand Down Expand Up @@ -123,3 +124,80 @@ export async function updateCharacterSecrets(
}
return true;
}

export const getAssetType = (transaction: EthereumTransaction) => {
// Check for ETH
if (transaction.value && transaction.value !== "0") {
return "ETH";
}

// Check for ERC-20 tokens
if (transaction.token_transfers && transaction.token_transfers.length > 0) {
return transaction.token_transfers
.map((transfer) => {
return transfer.token_id;
})
.join(", ");
}

return "N/A";
};

/**
* Fetches and formats wallet balances and recent transactions.
*
* @param {IAgentRuntime} runtime - The runtime for wallet initialization.
* @param {string} networkId - The network ID (optional, defaults to ETH mainnet).
* @returns {Promise<{balances: Array<{asset: string, amount: string}>, transactions: Array<any>}>} - An object with formatted balances and transactions.
*/
export async function getWalletDetails(
runtime: IAgentRuntime,
networkId: string = Coinbase.networks.EthereumMainnet
): Promise<{
balances: Array<{ asset: string; amount: string }>;
transactions: Array<{
timestamp: string;
amount: string;
asset: string; // Ensure getAssetType is implemented
status: string;
transactionUrl: string;
}>;
}> {
try {
// Initialize the wallet, defaulting to the specified network or ETH mainnet
const wallet = await initializeWallet(runtime, networkId);

// Fetch balances
const balances = await wallet.listBalances();
const formattedBalances = Array.from(balances, (balance) => ({
asset: balance[0],
amount: balance[1].toString(),
}));

// Fetch the wallet's recent transactions
const walletAddress = await wallet.getDefaultAddress();
const transactions = (
await walletAddress.listTransactions({ limit: 10 })
).data;

const formattedTransactions = transactions.map((transaction) => {
const content = transaction.content();
return {
timestamp: content.block_timestamp || "N/A",
amount: content.value || "N/A",
asset: getAssetType(content) || "N/A", // Ensure getAssetType is implemented
status: transaction.getStatus(),
transactionUrl: transaction.getTransactionLink() || "N/A",
};
});

// Return formatted data
return {
balances: formattedBalances,
transactions: formattedTransactions,
};
} catch (error) {
console.error("Error fetching wallet details:", error);
throw new Error("Unable to retrieve wallet details.");
}
}