Skip to content

Commit

Permalink
feat: add Bitcoin plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
tiero committed Dec 29, 2024
1 parent 76d4f42 commit d62b560
Show file tree
Hide file tree
Showing 18 changed files with 1,118 additions and 0 deletions.
29 changes: 29 additions & 0 deletions packages/plugin-bitcoin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@ai16z/plugin-bitcoin",
"version": "0.1.0",
"description": "Bitcoin on-chain plugin for Eliza",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@ai16z/eliza": "workspace:*",
"@arklabs/wallet-sdk": "^0.0.1",
"node-fetch": "^2.6.9"
},
"devDependencies": {
"@types/node": "^20.8.7",
"@types/node-fetch": "^2.6.4",
"@vitest/coverage-v8": "^0.34.6",
"tsup": "^7.2.0",
"typescript": "^5.0.4",
"vitest": "^0.34.6"
}
}
63 changes: 63 additions & 0 deletions packages/plugin-bitcoin/src/actions/addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { IAgentRuntime, Memory, State, HandlerCallback } from "@ai16z/eliza";
import { BitcoinTaprootProvider } from "../providers/bitcoin";

export const showBitcoinAddressesAction = {
name: "SHOW_BITCOIN_ADDRESSES",
description: "Show Bitcoin wallet addresses",
similes: ["SHOW_ADDRESSES", "GET_ADDRESSES", "DISPLAY_ADDRESSES", "SHOW_WALLET"],
handler: async (
runtime: IAgentRuntime,
_message: Memory,
_state: State,
_options: any,
callback?: HandlerCallback
) => {
try {
const bitcoinProvider = runtime.providers.find(
p => p instanceof BitcoinTaprootProvider
) as BitcoinTaprootProvider;

if (!bitcoinProvider) {
return {
text: 'Bitcoin provider not found. Please check your configuration.'
};
}

const addresses = await bitcoinProvider.getAddress();

if (callback) {
callback({
text: `Here are your Bitcoin addresses:
⛓️ Onchain: ${addresses.onchain}
⚡️ Offchain: ${addresses.offchain || 'Not configured'}`
});
}

return {
text: `Here are your Bitcoin addresses:
⛓️ Onchain: ${addresses.onchain}
⚡️ Offchain: ${addresses.offchain || 'Not configured'}`
};
} catch (error) {
console.error('Error fetching addresses:', error);
return {
text: 'Unable to fetch your Bitcoin addresses at the moment. Please try again later.'
};
}
},
validate: async (runtime: IAgentRuntime) => {
return runtime.providers.some(p => p instanceof BitcoinTaprootProvider);
},
examples: [
[
{
user: "{{user1}}",
content: {
text: "Show my Bitcoin addresses"
}
}
]
]
};
67 changes: 67 additions & 0 deletions packages/plugin-bitcoin/src/actions/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Action, IAgentRuntime, Memory, State } from "@ai16z/eliza";
import { BitcoinTaprootProvider } from "../providers/bitcoin";

export const balanceAction: Action = {
name: "balance",
description: "Get Bitcoin wallet balance",
examples: [
[{ user: "{{user1}}", content: { text: "What's my Bitcoin balance?" } }],
[{ user: "{{user1}}", content: { text: "Show me my BTC balance" } }]
],
similes: [
"BALANCE",
"GETBALANCE",
"SHOWBALANCE",
"CHECKBALANCE",
"DISPLAYBALANCE",
"MYBALANCE",
"HOWMUCHBTC",
"WHATSMYBALANCE"
],

async validate(runtime: IAgentRuntime): Promise<boolean> {
return !!runtime.providers.find(p => p instanceof BitcoinTaprootProvider);
},

async handler(runtime: IAgentRuntime, _message: Memory, _state: State, _match: any) {
try {
const provider = runtime.providers.find(
(p): p is BitcoinTaprootProvider => p instanceof BitcoinTaprootProvider
);

if (!provider) {
return { text: "Bitcoin provider not found. Please check your configuration." };
}

const [balance, price] = await Promise.all([
provider.getWalletBalance(),
provider.getBitcoinPrice()
]);

const onchainTotal = balance.onchain.total;
const offchainTotal = balance.offchain.total;
const total = balance.total;

const onchainUSD = (onchainTotal * price.USD.last) / 100_000_000;
const offchainUSD = (offchainTotal * price.USD.last) / 100_000_000;
const totalUSD = (total * price.USD.last) / 100_000_000;

return {
text: `Bitcoin Balance:
Total: ${total.toLocaleString()} sats (≈ $${totalUSD.toFixed(2)} USD)
├─ Onchain: ${onchainTotal.toLocaleString()} sats (≈ $${onchainUSD.toFixed(2)} USD)
│ ├─ Confirmed: ${balance.onchain.confirmed.toLocaleString()} sats
│ └─ Unconfirmed: ${balance.onchain.unconfirmed.toLocaleString()} sats
└─ Offchain: ${offchainTotal.toLocaleString()} sats (≈ $${offchainUSD.toFixed(2)} USD)
├─ Settled: ${balance.offchain.settled.toLocaleString()} sats
├─ Pending: ${balance.offchain.pending.toLocaleString()} sats
└─ Swept: ${balance.offchain.swept.toLocaleString()} sats`
};
} catch (error) {
console.error("Error in balance action:", error);
return {
text: "Unable to fetch your Bitcoin balance at the moment. Please try again later."
};
}
}
};
71 changes: 71 additions & 0 deletions packages/plugin-bitcoin/src/actions/coins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Action, IAgentRuntime } from "@ai16z/eliza";
import { BitcoinTaprootProvider } from "../providers/bitcoin";

export const coinsAction: Action = {
name: "coins",
description: "List your Bitcoin UTXOs",
similes: ["LISTCOINS", "LISTUTXOS", "SHOWUTXOS"],
examples: [
[{ user: "{{user1}}", content: { text: "Show my coins" } }],
[{ user: "{{user1}}", content: { text: "List UTXOs" } }]
],
validate: async (runtime: IAgentRuntime) => {
return !!runtime.providers.find(p => p instanceof BitcoinTaprootProvider);
},
handler: async (runtime: IAgentRuntime) => {
try {
const provider = runtime.providers.find(
(p) => p instanceof BitcoinTaprootProvider
) as BitcoinTaprootProvider;

if (!provider) {
return {
text: "Bitcoin provider not found.\nPlease check your configuration.",
};
}

const coins = await provider.getCoins();

if (!coins || coins.length === 0) {
return {
text: "Bitcoin UTXOs:\n\n└─ No confirmed UTXOs\n└─ No unconfirmed UTXOs",
};
}

const confirmedCoins = coins.filter((coin) => coin.status.confirmed);
const unconfirmedCoins = coins.filter((coin) => !coin.status.confirmed);

const confirmedTotal = confirmedCoins.reduce((sum, coin) => sum + coin.value, 0);
const unconfirmedTotal = unconfirmedCoins.reduce((sum, coin) => sum + coin.value, 0);

let output = "Bitcoin UTXOs:\n\n";

// Add confirmed UTXOs
output += `Confirmed UTXOs (Total: ${confirmedTotal} sats):\n`;
if (confirmedCoins.length === 0) {
output += "└─ No confirmed UTXOs\n";
} else {
confirmedCoins.forEach((coin) => {
output += `└─ ${coin.txid}:${coin.vout} (${coin.value} sats)\n`;
});
}

// Add unconfirmed UTXOs
output += `\nUnconfirmed UTXOs (Total: ${unconfirmedTotal} sats):\n`;
if (unconfirmedCoins.length === 0) {
output += "└─ No unconfirmed UTXOs\n";
} else {
unconfirmedCoins.forEach((coin) => {
output += `└─ ${coin.txid}:${coin.vout} (${coin.value} sats)\n`;
});
}

return { text: output };
} catch (error) {
console.error("Error in coins action:", error);
return {
text: "Unable to fetch your Bitcoin coins at the moment. Please try again later.",
};
}
},
};
6 changes: 6 additions & 0 deletions packages/plugin-bitcoin/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { sendBitcoinAction } from './send';
import { showBitcoinAddressesAction } from './addresses';
import { balanceAction } from './balance';
import { coinsAction } from './coins';

export const actions = [sendBitcoinAction, showBitcoinAddressesAction, balanceAction, coinsAction];
Loading

0 comments on commit d62b560

Please sign in to comment.