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

btcfun Plugin for Eliza #2797

Merged
merged 3 commits into from
Jan 27, 2025
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
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
"@0glabs/0g-ts-sdk": "0.2.1",
"@coinbase/coinbase-sdk": "0.10.0",
"@deepgram/sdk": "^3.9.0",
"@okxweb3/coin-bitcoin": "1.2.0",
"@okxweb3/crypto-lib": "1.0.10",
"@injectivelabs/sdk-ts": "^1.14.33",
"@vitest/eslint-plugin": "1.0.1",
"amqplib": "0.10.5",
Expand All @@ -74,7 +76,9 @@
"pnpm": "9.14.4",
"sharp": "0.33.5",
"tslog": "4.9.3",
"bs58": "4.0.0"
"bs58": "4.0.0",
"tiny-secp256k1": "2.2.3",
"tslog": "4.9.3"
},
"packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee",
"workspaces": [
Expand Down
19 changes: 19 additions & 0 deletions packages/plugin-btcfun/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# `@elizaos/plugin-btcfun`

This plugin provides actions and providers to interact with btcfun via bitcoin network.

---

## Configuration

### Default Setup

By default, **Bitcoin mainnet** is enabled. To use it, simply add your private key to the `.env` file:

```env
BTC_PRIVATE_KEY_WIF=your-private-key-here
ADDRESS=your-address-here
BTCFUN_API_URL=https://api-testnet-new.btc.fun
MINTCAP=10000
MINTDEADLINE=864000
ADDRESS_FUNDRAISING_CAP=100
3 changes: 3 additions & 0 deletions packages/plugin-btcfun/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import eslintGlobalConfig from "../../eslint.config.mjs";

export default [...eslintGlobalConfig];
24 changes: 24 additions & 0 deletions packages/plugin-btcfun/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@elizaos/plugin-btcfun",
"version": "0.1.7-alpha.2",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@elizaos/core": "workspace:*",
"@lifi/data-types": "5.15.5",
"@lifi/sdk": "3.4.1",
"@lifi/types": "16.3.0",
"tsup": "8.3.5",
"viem": "2.21.53"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"test": "vitest run",
"lint": "eslint --fix --cache ."
},
"peerDependencies": {
"whatwg-url": "7.1.0"
}
}
156 changes: 156 additions & 0 deletions packages/plugin-btcfun/src/actions/btcfun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { ByteArray, formatEther, parseEther, type Hex } from "viem";
import {
composeContext,
generateObjectDeprecated,
HandlerCallback,
ModelClass,
type IAgentRuntime,
type Memory,
type State,
} from "@elizaos/core";

import { networks, Psbt } from 'bitcoinjs-lib';
import { BIP32Factory } from 'bip32';
import {randomBytes} from 'crypto';
import * as ecc from 'tiny-secp256k1';
import { BtcWallet, privateKeyFromWIF } from "@okxweb3/coin-bitcoin";
import { base } from "@okxweb3/crypto-lib";
import { mintTemplate } from "../templates";
import {initBtcFunProvider} from "../providers/btcfun.ts";
export { mintTemplate };

function checkTokenType(tokenType: string) {
if (tokenType.toLowerCase() !== "brc20" && tokenType.toLowerCase() !== "runes") {
throw new Error("Invalid token type");
}
}

export const btcfunMintAction = {
name: "mint",
description: "btcfun mint brc20/runes",
handler: async (
runtime: IAgentRuntime,
_message: Memory,
state: State,
_options: any,
callback?: HandlerCallback
) => {
console.log("btcfun action handler called");
const btcfunProvider = initBtcFunProvider(runtime);

const chainCode = randomBytes(32);
const bip32Factory = BIP32Factory(ecc);
const network = networks.bitcoin;
const privateKeyWif = runtime.getSetting("BTC_PRIVATE_KEY_WIF") ?? process.env.BTC_PRIVATE_KEY_WIF;
let address = runtime.getSetting("ADDRESS") ?? process.env.ADDRESS;

const privateKey = base.fromHex(privateKeyFromWIF(privateKeyWif, network));
const privateKeyHex = base.toHex(privateKey);
const privateKeyBuffer = Buffer.from(privateKeyHex, 'hex');
const keyPair = bip32Factory.fromPrivateKey(privateKeyBuffer, chainCode, network);
const publicKeyBuffer = Buffer.from(keyPair.publicKey);
const publicKeyHex = publicKeyBuffer.toString('hex');

// Compose mint context
const mintContext = composeContext({
state,
template: mintTemplate,
});
const content = await generateObjectDeprecated({
runtime,
context: mintContext,
modelClass: ModelClass.LARGE,
});
let tokenType = content.tokenType;
let tick = content.inputToken;
let mintcap = content.mintcap ?? runtime.getSetting("MINTCAP");
let mintdeadline = content.mintdeadline ?? runtime.getSetting("MINTDEADLINE");
let addressFundraisingCap = content.addressFundraisingCap ?? runtime.getSetting("ADDRESS_FUNDRAISING_CAP");
console.log("begin to mint token", tick, content)
checkTokenType(tokenType)
//validateToken
await btcfunProvider.validateToken(tokenType, address, tick);
console.log("validate token success")

try {
let {order_id, psbt_hex} = await btcfunProvider.createOrder(
tokenType, publicKeyHex, address, publicKeyHex, address, 10,
tick, addressFundraisingCap, mintdeadline, mintcap)
const psbt = Psbt.fromHex(psbt_hex)
let wallet = new BtcWallet()
const toSignInputs = [];
psbt.data.inputs.forEach((input, index)=>{
toSignInputs.push({
index: index,
address: address,
sighashTypes: [0],
disableTweakSigner: false,
});
})

let params = {
type: 3,
psbt: psbt_hex,
autoFinalized: false,
toSignInputs: toSignInputs,
};

let signParams = {
privateKey: privateKeyWif,
data: params,
};
let signedPsbtHex = await wallet.signTransaction(signParams);
const txHash = await btcfunProvider.broadcastOrder(order_id, signedPsbtHex)
console.log('signedPsbtHex: ', signedPsbtHex, 'orderID: ', order_id, 'txhash', txHash)
if (callback) {
callback({
text: `Successfully mint ${tokenType} ${tick} tokens, mintcap ${mintcap}, mintdeadline ${mintdeadline}, addressFundraisingCap ${addressFundraisingCap} ,txhash ${txHash}`,
content: {
success: true,
orderID: order_id,
},
});
}
} catch (error) {
console.error('Error:', error);
}
},
template: mintTemplate,
validate: async (runtime: IAgentRuntime) => {
const privateKey = runtime.getSetting("BTC_PRIVATE_KEY_WIF");
return typeof privateKey === "string" && privateKey.length > 0;
},
examples: [
[
{
user: "assistant",
content: {
text: "I'll help you mint 100000000 BRC20 Party",
action: "MINT_BRC20",
},
},
{
user: "assistant",
content: {
text: "I'll help you mint 100000000 RUNES Party",
action: "MINT_RUNES",
},
},
{
user: "user",
content: {
text: "import token BRC20 `Party`, mintcap 100000, addressFundraisingCap 10 mintdeadline 864000",
action: "MINT_BRC20",
},
},
{
user: "user",
content: {
text: "import token RUNES `Party2`, mintcap 100000, addressFundraisingCap 10 mintdeadline 864000",
action: "MINT_RUNES",
},
},
],
],
similes: ["MINT_BRC20","MINT_RUNES"],
};
16 changes: 16 additions & 0 deletions packages/plugin-btcfun/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {btcfunMintAction} from "./actions/btcfun.ts";

export * from "./providers/btcfun";

import type { Plugin } from "@elizaos/core";

export const btcfunPlugin: Plugin = {
name: "btcfun",
description: "btcfun plugin",
providers: [],
evaluators: [],
services: [],
actions: [btcfunMintAction],
};

export default btcfunPlugin;
102 changes: 102 additions & 0 deletions packages/plugin-btcfun/src/providers/btcfun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import fetch from 'node-fetch';
import type {IAgentRuntime} from "@elizaos/core";

export const initBtcFunProvider = (runtime: IAgentRuntime) => {

const btcfunApiURL = runtime.getSetting("BTCFUN_API_URL") ?? process.env.BTCFUN_API_URL
if (!btcfunApiURL) {
throw new Error("BTCFUN_API_URL is not set");
}

return new BtcfunProvider(btcfunApiURL);
};

export class BtcfunProvider {
private apiUrl: string;

constructor(apiUrl: string) {
this.apiUrl = apiUrl;
}

async validateToken(tokenType: string, address: string, ticker: string) {
const url = tokenType === "runes"
? `${this.apiUrl}/api/v1/import/rune_validate`
: `${this.apiUrl}/api/v1/import/brc20_validate`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
address: address,
ticker: ticker,
}),
});

if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}

return response.json();
}

async createOrder(tokenType: string, paymentFromPubKey: string, paymentFrom: string, ordinalsFromPubKey: string, ordinalsFrom: string, feeRate: number, tick: string, addressFundraisingCap: string, mintDeadline: number, mintCap: string) {
const url = tokenType === "runes"
? `${this.apiUrl}/api/v1/import/rune_order`
: `${this.apiUrl}/api/v1/import/brc20_order`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
payment_from_pub_key: paymentFromPubKey,
payment_from: paymentFrom,
ordinals_from_pub_key: ordinalsFromPubKey,
ordinals_from: ordinalsFrom,
fee_rate: feeRate,
tick: tick,
address_fundraising_cap: addressFundraisingCap,
mint_deadline: mintDeadline,
mint_cap: mintCap,
}),
});

if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}

const result = await response.json();

if (result.code === "OK" && result.data) {
const { order_id, psbt_hex } = result.data;
return { order_id, psbt_hex };
} else {
console.log("Invalid response", result)
throw new Error("Invalid response");
}
}

async broadcastOrder(orderId: string, signedPsbtHex: string) {
const response = await fetch(`${this.apiUrl}/api/v1/import/broadcast`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
order_id: orderId,
signed_psbt_hex: signedPsbtHex,
}),
});

if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const result = await response.json();
console.log("broadcastOrder result", result);

if (result.code === "OK" && result.data) {
return result.data;
}
}
}
26 changes: 26 additions & 0 deletions packages/plugin-btcfun/src/templates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const mintTemplate = `Given the recent messages and wallet information below:

{{recentMessages}}

{{walletInfo}}

Extract the following information about the requested token swap:
- Input token type, eg:runes or brc20, should convert BRC20,brc20 to brc20, runes or RUNES to runes
- Input token symbol (the token being mint), eg: mint token abc
- Input token mintcap eg: "10000"
- Input token addressFundraisingCap everyone can offer eg: "10"
- Input token mintdeadline ,duration Using seconds as the unit eg: 864000


Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined:

\`\`\`json
{
"tokenType": string | null,
"inputToken": string | null,
"mintcap": string | 1000,
"addressFundraisingCap": string | 10,
"mintdeadline" : number | 864000,
}
\`\`\`
`;
15 changes: 15 additions & 0 deletions packages/plugin-btcfun/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../core/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "./src",
"typeRoots": [
"./node_modules/@types",
"./src/types"
],
"declaration": true
},
"include": [
"src"
]
}
Loading
Loading