diff --git a/packages/plugin-merlin/README.md b/packages/plugin-merlin/README.md new file mode 100644 index 00000000000..aed5a1a6218 --- /dev/null +++ b/packages/plugin-merlin/README.md @@ -0,0 +1,16 @@ +# `@elizaos/plugin-merlin` + +This plugin provides actions and providers for interacting with Merlin-compatible chains. + +--- + +## Configuration + +### Default Setup + +By default, **Merlin 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-merlin/eslint.config.mjs b/packages/plugin-merlin/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-merlin/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-merlin/package.json b/packages/plugin-merlin/package.json new file mode 100644 index 00000000000..bedf6c79722 --- /dev/null +++ b/packages/plugin-merlin/package.json @@ -0,0 +1,24 @@ +{ + "name": "@elizaos/plugin-merlin", + "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-merlin/src/actions/btcfun.ts b/packages/plugin-merlin/src/actions/btcfun.ts new file mode 100644 index 00000000000..1a4a84655df --- /dev/null +++ b/packages/plugin-merlin/src/actions/btcfun.ts @@ -0,0 +1,136 @@ +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: "btcfun", + 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 psbtHex = '70736274ff0100dd0200000002079a01841a17ba439545a00c73031db6a2317be7b12b619a4a1239a67e18ffaa0000000000fdffffffa0dffa35eee28e5c1b0c2048f5725f82200c6726337c27160b324e77a9c6ad530100000000fdffffff032202000000000000225120a11f9fa43193da4b9b825cf92d13fde040fc7205076c5a6eebfdb4b6a67a583d805c00000000000022512097162ff9c360ce05c59079a6f34897564528eee056dfcf2549383a3016683b8a4f0200000000000022512097162ff9c360ce05c59079a6f34897564528eee056dfcf2549383a3016683b8a000000000001012b220200000000000022512097162ff9c360ce05c59079a6f34897564528eee056dfcf2549383a3016683b8a011720e2df0c6fced9b8530a46649bc7ec06abfa8636a51bcacde4150c52715b76e9df0001012bc56800000000000022512097162ff9c360ce05c59079a6f34897564528eee056dfcf2549383a3016683b8a011720e2df0c6fced9b8530a46649bc7ec06abfa8636a51bcacde4150c52715b76e9df00000000'; + + const privateKey = base.fromHex(privateKeyFromWIF(privateKeyWif, network)); + const privateKeyHex = base.toHex(privateKey); + console.log('Private key: ', privateKeyHex) + const privateKeyBuffer = Buffer.from(privateKeyHex, 'hex'); + const keyPair = bip32Factory.fromPrivateKey(privateKeyBuffer, chainCode, network); + const publicKeyBuffer = Buffer.from(keyPair.publicKey); + const publicKeyHex = publicKeyBuffer.toString('hex'); + console.log('Public Key: ', publicKeyHex); + + // Compose mint context + const mintContext = composeContext({ + state, + template: mintTemplate, + }); + const content = await generateObjectDeprecated({ + runtime, + context: mintContext, + modelClass: ModelClass.LARGE, + }); + let tick = content.inputToken; + console.log("begin to mint token", tick, content) + //todo remove + tick = "dongj" + + try { + const {order_id, psbt_hex} = await btcfunProvider.createBrc20Order(publicKeyHex, address, publicKeyHex, address, 5, tick,"100",864000,"1000") + console.log("11110",psbt_hex) + 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, + }; + console.log("signParams: ", signParams) + //let signedPsbtHex = await wallet.signTransaction(signParams); + + //todo open + //await btcfunProvider.broadcastOrder(orderID, signedPsbtHex) + //console.log('signedPsbtHex: ', signedPsbtHex, 'orderID: ', order_id) + if (callback) { + callback({ + text: `Successfully mint ${tick} tokens`, + content: { + success: true, + orderID: order_id, + //psbtHex: signedPsbtHex, + }, + }); + } + } 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`", + action: "MINT_BRC20", + }, + }, + ], + ], + similes: ["MINT_BRC20","MINT_RUNES"], +}; diff --git a/packages/plugin-merlin/src/index.ts b/packages/plugin-merlin/src/index.ts new file mode 100644 index 00000000000..1a0beea79df --- /dev/null +++ b/packages/plugin-merlin/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 merlinPlugin: Plugin = { + name: "merlin", + description: "merlin plugin", + providers: [], + evaluators: [], + services: [], + actions: [btcfunMintAction], +}; + +export default merlinPlugin; diff --git a/packages/plugin-merlin/src/providers/btcfun.ts b/packages/plugin-merlin/src/providers/btcfun.ts new file mode 100644 index 00000000000..e9b37dae3ef --- /dev/null +++ b/packages/plugin-merlin/src/providers/btcfun.ts @@ -0,0 +1,92 @@ +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}`); + } + + return response.json(); + } +} diff --git a/packages/plugin-merlin/src/templates/index.ts b/packages/plugin-merlin/src/templates/index.ts new file mode 100644 index 00000000000..f38e6a21b0c --- /dev/null +++ b/packages/plugin-merlin/src/templates/index.ts @@ -0,0 +1,17 @@ +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 + +Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined: + +\`\`\`json +{ + "inputToken": string | null, +} +\`\`\` +`; diff --git a/packages/plugin-merlin/tsconfig.json b/packages/plugin-merlin/tsconfig.json new file mode 100644 index 00000000000..2d8d3fe8181 --- /dev/null +++ b/packages/plugin-merlin/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-merlin/tsup.config.ts b/packages/plugin-merlin/tsup.config.ts new file mode 100644 index 00000000000..3e511a86dbb --- /dev/null +++ b/packages/plugin-merlin/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", + ], +});