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(new-plugin): support Btcfun #2911

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
125801c
btcfun plugin support
Jan 22, 2025
5c018d9
Merge branch 'develop' into btcfun-dev
Nevermore-Ray Jan 22, 2025
154fc87
Merge remote-tracking branch 'origin/develop' into pr/2643
wtfsayo Jan 27, 2025
63e2a0d
Merge remote-tracking branch 'origin/develop' into pr/2643
wtfsayo Jan 27, 2025
469a350
update ts related
wtfsayo Jan 27, 2025
fddb538
update plugin initialisation
wtfsayo Jan 27, 2025
f821c3d
init trikon plugin
wtfsayo Jan 27, 2025
cdd757d
Update pnpm-lock.yaml
wtfsayo Jan 27, 2025
47c282c
attempt fix build
wtfsayo Jan 27, 2025
0ab0d2e
support btcfun plugin
Jan 28, 2025
ed47d37
support btcfun
Jan 28, 2025
cdc37e9
add others
Jan 28, 2025
cf61179
Merge branch 'develop' into btcfun-dev
wtfsayo Jan 29, 2025
98c5df1
Merge branch 'develop' into btcfun-dev
wtfsayo Jan 29, 2025
71d4a9d
Merge branch 'develop' into btcfun-dev
wtfsayo Jan 30, 2025
80a69c4
Merge branch 'develop' into btcfun-dev
wtfsayo Feb 2, 2025
0492976
init plugin
wtfsayo Feb 2, 2025
de3bab9
clean up
wtfsayo Feb 2, 2025
1cb7c16
fix build issue!
wtfsayo Feb 2, 2025
d72d10e
Merge branch 'develop' into btcfun-dev
Nevermore-Ray Feb 3, 2025
7513843
fix conflict
Feb 3, 2025
c1a4bc7
Merge branch 'develop' into btcfun-dev
Nevermore-Ray Feb 3, 2025
c32e728
Merge remote-tracking branch 'origin/develop' into pr/2911
wtfsayo Feb 3, 2025
7c6740c
Merge branch 'develop' into btcfun-dev
wtfsayo Feb 3, 2025
d5f8549
Merge branch 'develop' into btcfun-dev
wtfsayo Feb 4, 2025
cd9dd13
fix conflict
Feb 6, 2025
4021ca0
fix conflict
Feb 7, 2025
a2774eb
Merge branch 'develop' into btcfun-dev
Nevermore-Ray Feb 7, 2025
3c0a25b
fix conflict
Feb 8, 2025
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
65 changes: 65 additions & 0 deletions packages/plugin-btcfun/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# `@elizaos/plugin-btcfun`

A powerful plugin for Eliza that enables interaction with BTCFun via the Bitcoin network. This plugin provides seamless integration for minting BRC20 and Runes tokens using BTCFun's API services.

## Features

### Provider Features

- **BTCFun Provider**
- Validates token operations for both BRC20 and Runes tokens
- Creates and manages minting orders
- Handles transaction broadcasting
- Manages PSBT (Partially Signed Bitcoin Transactions)

### Action Features

- **Token Minting**
- Supports minting of both BRC20 and Runes tokens
- Configurable mint parameters:
- Mint cap (maximum token supply)
- Address fundraising cap (per-address limit)
- Mint deadline (duration in seconds)
- Handles Bitcoin transaction signing and broadcasting
- Automatic PSBT creation and management

## Configuration

### Default Setup

By default, **Bitcoin mainnet** is enabled. To use it, configure the following environment variables in your `.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
```

### Environment Variables

- `BTC_PRIVATE_KEY_WIF`: Your Bitcoin private key in WIF format
- `ADDRESS`: Your Bitcoin address
- `BTCFUN_API_URL`: BTCFun API endpoint URL
- `MINTCAP`: Default maximum token supply for minting
- `MINTDEADLINE`: Default duration for minting in seconds
- `ADDRESS_FUNDRAISING_CAP`: Default per-address fundraising limit

## API Reference

The plugin integrates with BTCFun's API services through the following endpoints:
- `/api/v1/import/brc20_validate` - BRC20 token validation
- `/api/v1/import/rune_validate` - Runes token validation
- `/api/v1/import/brc20_order` - BRC20 minting order creation
- `/api/v1/import/rune_order` - Runes minting order creation
- `/api/v1/import/broadcast` - Transaction broadcasting

## License

See parent project for license information.

## Contributing

Contributions are welcome! See parent project for contribution guidelines.
27 changes: 27 additions & 0 deletions packages/plugin-btcfun/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"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",
"@okxweb3/coin-bitcoin": "1.2.0",
"@okxweb3/crypto-lib": "1.0.10",
"tiny-secp256k1": "2.2.3"
},
"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,
}
\`\`\`
`;
Loading
Loading