From 5b722e975a78bbed071f0735ee3caa0d5efacf55 Mon Sep 17 00:00:00 2001 From: Nevermore-Ray Date: Wed, 22 Jan 2025 13:42:19 +0800 Subject: [PATCH] btcfun plugin support --- package.json | 4 + packages/plugin-btcfun/README.md | 16 +++ packages/plugin-btcfun/eslint.config.mjs | 3 + packages/plugin-btcfun/package.json | 24 ++++ packages/plugin-btcfun/src/actions/btcfun.ts | 135 ++++++++++++++++++ packages/plugin-btcfun/src/index.ts | 16 +++ .../plugin-btcfun/src/providers/btcfun.ts | 95 ++++++++++++ packages/plugin-btcfun/src/templates/index.ts | 24 ++++ packages/plugin-btcfun/tsconfig.json | 15 ++ packages/plugin-btcfun/tsup.config.ts | 22 +++ 10 files changed, 354 insertions(+) create mode 100644 packages/plugin-btcfun/README.md create mode 100644 packages/plugin-btcfun/eslint.config.mjs create mode 100644 packages/plugin-btcfun/package.json create mode 100644 packages/plugin-btcfun/src/actions/btcfun.ts create mode 100644 packages/plugin-btcfun/src/index.ts create mode 100644 packages/plugin-btcfun/src/providers/btcfun.ts create mode 100644 packages/plugin-btcfun/src/templates/index.ts create mode 100644 packages/plugin-btcfun/tsconfig.json create mode 100644 packages/plugin-btcfun/tsup.config.ts diff --git a/package.json b/package.json index da91787ad2..3631352611 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,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", @@ -66,6 +68,8 @@ "sharp": "0.33.5", "tslog": "4.9.3", "bs58": "4.0.0" + "tiny-secp256k1": "2.2.3", + "tslog": "4.9.3" }, "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee", "workspaces": [ diff --git a/packages/plugin-btcfun/README.md b/packages/plugin-btcfun/README.md new file mode 100644 index 0000000000..12e3c2419a --- /dev/null +++ b/packages/plugin-btcfun/README.md @@ -0,0 +1,16 @@ +# `@elizaos/plugin-btcfun` + +This plugin provides actions and providers for interacting with bitcoin chains. + +--- + +## 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 diff --git a/packages/plugin-btcfun/eslint.config.mjs b/packages/plugin-btcfun/eslint.config.mjs new file mode 100644 index 0000000000..92fe5bbebe --- /dev/null +++ b/packages/plugin-btcfun/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-btcfun/package.json b/packages/plugin-btcfun/package.json new file mode 100644 index 0000000000..e1f1bb64c3 --- /dev/null +++ b/packages/plugin-btcfun/package.json @@ -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" + } +} diff --git a/packages/plugin-btcfun/src/actions/btcfun.ts b/packages/plugin-btcfun/src/actions/btcfun.ts new file mode 100644 index 0000000000..45a186a7ae --- /dev/null +++ b/packages/plugin-btcfun/src/actions/btcfun.ts @@ -0,0 +1,135 @@ +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 }; + +export const btcfunMintAction = { + name: "mint", + description: "btcfun mint brc20", + 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 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) + //validateBrc20 + await btcfunProvider.validateBrc20(address, tick); + console.log("validateBrc20 success") + + try { + let {order_id, psbt_hex} = await btcfunProvider.createBrc20Order( + 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 ${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 Party", + action: "MINT_BRC20", + }, + }, + { + user: "user", + content: { + text: "import token BRC20 `Party`, mintcap 100000, addressFundraisingCap 10 mintdeadline 864000", + action: "MINT_BRC20", + }, + }, + ], + ], + similes: ["MINT_BRC20","MINT_RUNES"], +}; diff --git a/packages/plugin-btcfun/src/index.ts b/packages/plugin-btcfun/src/index.ts new file mode 100644 index 0000000000..d80ab99bdb --- /dev/null +++ b/packages/plugin-btcfun/src/index.ts @@ -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; diff --git a/packages/plugin-btcfun/src/providers/btcfun.ts b/packages/plugin-btcfun/src/providers/btcfun.ts new file mode 100644 index 0000000000..82c5cfd03a --- /dev/null +++ b/packages/plugin-btcfun/src/providers/btcfun.ts @@ -0,0 +1,95 @@ +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 validateBrc20(address: string, ticker: string) { + const response = await fetch(`${this.apiUrl}/api/v1/import/brc20_validate`, { + 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 createBrc20Order(paymentFromPubKey: string, paymentFrom: string, ordinalsFromPubKey: string, ordinalsFrom: string, feeRate: number, tick: string, addressFundraisingCap: string, mintDeadline: number, mintCap: string) { + const response = await fetch(`${this.apiUrl}/api/v1/import/brc20_order`, { + 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(); + + if (result.code === "OK" && result.data) { + return result.data; + } + } +} diff --git a/packages/plugin-btcfun/src/templates/index.ts b/packages/plugin-btcfun/src/templates/index.ts new file mode 100644 index 0000000000..71678a6fb3 --- /dev/null +++ b/packages/plugin-btcfun/src/templates/index.ts @@ -0,0 +1,24 @@ +export const mintTemplate = `Given the recent messages and wallet information below: + +{{recentMessages}} + +{{walletInfo}} + +Extract the following information about the requested token swap: +- 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 +{ + "inputToken": string | null, + "mintcap": string | 1000, + "addressFundraisingCap": string | 10, + "mintdeadline" : number | 864000, +} +\`\`\` +`; diff --git a/packages/plugin-btcfun/tsconfig.json b/packages/plugin-btcfun/tsconfig.json new file mode 100644 index 0000000000..2d8d3fe818 --- /dev/null +++ b/packages/plugin-btcfun/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ], + "declaration": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/plugin-btcfun/tsup.config.ts b/packages/plugin-btcfun/tsup.config.ts new file mode 100644 index 0000000000..3e511a86db --- /dev/null +++ b/packages/plugin-btcfun/tsup.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "viem", + "@lifi/sdk", + "@okxweb3/crypto-lib", + ], +});