quorum-eliza-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Quorum Service for Eliza
3
+ *
4
+ * Manages connection to Quorum API and handles signing operations.
5
+ */
6
+ export interface QuorumAgent {
7
+ id: string;
8
+ name: string;
9
+ publicKey: string;
10
+ }
11
+ export interface QuorumMultisig {
12
+ id: string;
13
+ name: string;
14
+ address: string;
15
+ chainId: string;
16
+ threshold: number;
17
+ agents: QuorumAgent[];
18
+ }
19
+ export interface QuorumProposal {
20
+ id: string;
21
+ multisigId: string;
22
+ status: 'pending' | 'finalized' | 'expired';
23
+ outputs: Array<{
24
+ address: string;
25
+ amount: string;
26
+ }>;
27
+ signatures: Array<{
28
+ agentId: string;
29
+ signature: string;
30
+ }>;
31
+ sighashes: Array<{
32
+ inputIndex: number;
33
+ sighash: string;
34
+ }>;
35
+ note?: string;
36
+ createdAt: string;
37
+ }
38
+ declare class QuorumService {
39
+ private agentId;
40
+ private publicKey;
41
+ private privateKey;
42
+ get capabilityDescription(): string;
43
+ initialize(runtime: any): Promise<void>;
44
+ register(name: string): Promise<QuorumAgent>;
45
+ createMultisig(params: {
46
+ name: string;
47
+ chainId: string;
48
+ threshold: number;
49
+ totalSigners: number;
50
+ }): Promise<{
51
+ multisig: QuorumMultisig;
52
+ inviteCode: string;
53
+ }>;
54
+ joinMultisig(inviteCode: string): Promise<QuorumMultisig>;
55
+ listMultisigs(): Promise<QuorumMultisig[]>;
56
+ listPendingProposals(): Promise<QuorumProposal[]>;
57
+ getProposal(proposalId: string): Promise<QuorumProposal>;
58
+ signProposal(proposalId: string): Promise<{
59
+ success: boolean;
60
+ status: string;
61
+ txid?: string;
62
+ }>;
63
+ createProposal(params: {
64
+ multisigId: string;
65
+ recipient: string;
66
+ amount: number;
67
+ note?: string;
68
+ }): Promise<QuorumProposal>;
69
+ getAgentId(): string | null;
70
+ getPublicKey(): string | null;
71
+ }
72
+ export declare const quorumService: QuorumService;
73
+ export default quorumService;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Quorum Service for Eliza
3
+ *
4
+ * Manages connection to Quorum API and handles signing operations.
5
+ */
6
+ import { schnorr } from '@noble/curves/secp256k1';
7
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
8
+ const QUORUM_API = process.env.QUORUM_API_URL || 'https://quorumclaw.com';
9
+ class QuorumService {
10
+ agentId = null;
11
+ publicKey = null;
12
+ privateKey = null;
13
+ get capabilityDescription() {
14
+ return 'Multi-agent wallet coordination via Quorum';
15
+ }
16
+ async initialize(runtime) {
17
+ // Try to get private key from runtime settings
18
+ const privateKeyHex = runtime.getSetting?.('QUORUM_PRIVATE_KEY') ||
19
+ runtime.getSetting?.('WALLET_PRIVATE_KEY') ||
20
+ process.env.QUORUM_PRIVATE_KEY;
21
+ if (!privateKeyHex) {
22
+ console.warn('[Quorum] No private key configured. Set QUORUM_PRIVATE_KEY or WALLET_PRIVATE_KEY.');
23
+ return;
24
+ }
25
+ try {
26
+ const keyHex = String(privateKeyHex).replace('0x', '');
27
+ this.privateKey = hexToBytes(keyHex);
28
+ this.publicKey = bytesToHex(schnorr.getPublicKey(this.privateKey));
29
+ // Register with Quorum
30
+ await this.register(runtime.character?.name || 'Eliza Agent');
31
+ console.log(`[Quorum] Initialized. Agent ID: ${this.agentId}`);
32
+ }
33
+ catch (err) {
34
+ console.error('[Quorum] Failed to initialize:', err);
35
+ }
36
+ }
37
+ async register(name) {
38
+ if (!this.publicKey)
39
+ throw new Error('No public key available');
40
+ const res = await fetch(`${QUORUM_API}/v1/agents`, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({
44
+ name,
45
+ publicKey: this.publicKey,
46
+ provider: 'eliza',
47
+ }),
48
+ });
49
+ const json = await res.json();
50
+ if (!json.success)
51
+ throw new Error(json.error?.message || 'Registration failed');
52
+ this.agentId = json.data.id;
53
+ return json.data;
54
+ }
55
+ async createMultisig(params) {
56
+ if (!this.agentId)
57
+ throw new Error('Not registered with Quorum');
58
+ const res = await fetch(`${QUORUM_API}/v1/invites`, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({
62
+ name: params.name,
63
+ chainId: params.chainId,
64
+ threshold: params.threshold,
65
+ slots: params.totalSigners,
66
+ creatorAgentId: this.agentId,
67
+ }),
68
+ });
69
+ const json = await res.json();
70
+ if (!json.success)
71
+ throw new Error(json.error?.message || 'Create failed');
72
+ return {
73
+ multisig: json.data.multisig,
74
+ inviteCode: json.data.code,
75
+ };
76
+ }
77
+ async joinMultisig(inviteCode) {
78
+ if (!this.agentId)
79
+ throw new Error('Not registered with Quorum');
80
+ const res = await fetch(`${QUORUM_API}/v1/invites/${inviteCode}/join`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ agentId: this.agentId }),
84
+ });
85
+ const json = await res.json();
86
+ if (!json.success)
87
+ throw new Error(json.error?.message || 'Join failed');
88
+ return json.data.multisig;
89
+ }
90
+ async listMultisigs() {
91
+ if (!this.agentId)
92
+ return [];
93
+ const res = await fetch(`${QUORUM_API}/v1/agents/${this.agentId}/multisigs`);
94
+ const json = await res.json();
95
+ return json.success ? json.data : [];
96
+ }
97
+ async listPendingProposals() {
98
+ const multisigs = await this.listMultisigs();
99
+ const proposals = [];
100
+ for (const ms of multisigs) {
101
+ const res = await fetch(`${QUORUM_API}/v1/proposals?multisigId=${ms.id}&status=pending`);
102
+ const json = await res.json();
103
+ if (json.success) {
104
+ proposals.push(...json.data);
105
+ }
106
+ }
107
+ return proposals;
108
+ }
109
+ async getProposal(proposalId) {
110
+ const res = await fetch(`${QUORUM_API}/v1/proposals/${proposalId}`);
111
+ const json = await res.json();
112
+ if (!json.success)
113
+ throw new Error(json.error?.message || 'Proposal not found');
114
+ return json.data;
115
+ }
116
+ async signProposal(proposalId) {
117
+ if (!this.agentId || !this.privateKey) {
118
+ throw new Error('Not initialized');
119
+ }
120
+ // Get proposal to get sighash
121
+ const proposal = await this.getProposal(proposalId);
122
+ if (proposal.status !== 'pending') {
123
+ throw new Error(`Proposal is ${proposal.status}, not pending`);
124
+ }
125
+ // Check if we already signed
126
+ if (proposal.signatures.some(s => s.agentId === this.agentId)) {
127
+ throw new Error('Already signed this proposal');
128
+ }
129
+ // Sign each input's sighash
130
+ for (const sh of proposal.sighashes) {
131
+ const sighashBytes = hexToBytes(sh.sighash);
132
+ const signature = schnorr.sign(sighashBytes, this.privateKey);
133
+ const res = await fetch(`${QUORUM_API}/v1/proposals/${proposalId}/sign`, {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify({
137
+ agentId: this.agentId,
138
+ signature: bytesToHex(signature),
139
+ inputIndex: sh.inputIndex,
140
+ }),
141
+ });
142
+ const json = await res.json();
143
+ if (!json.success)
144
+ throw new Error(json.error?.message || 'Sign failed');
145
+ if (json.data.thresholdMet) {
146
+ return {
147
+ success: true,
148
+ status: 'finalized',
149
+ txid: json.data.txid,
150
+ };
151
+ }
152
+ }
153
+ return { success: true, status: 'pending' };
154
+ }
155
+ async createProposal(params) {
156
+ const res = await fetch(`${QUORUM_API}/v1/proposals`, {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify({
160
+ multisigId: params.multisigId,
161
+ outputs: [{ address: params.recipient, amount: params.amount.toString() }],
162
+ note: params.note,
163
+ createdBy: this.agentId,
164
+ }),
165
+ });
166
+ const json = await res.json();
167
+ if (!json.success)
168
+ throw new Error(json.error?.message || 'Create proposal failed');
169
+ return json.data;
170
+ }
171
+ getAgentId() {
172
+ return this.agentId;
173
+ }
174
+ getPublicKey() {
175
+ return this.publicKey;
176
+ }
177
+ }
178
+ export const quorumService = new QuorumService();
179
+ export default quorumService;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "quorum-eliza-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Quorum multi-agent wallet plugin for Eliza",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsc --watch"
17
+ },
18
+ "keywords": ["eliza", "quorum", "multisig", "ai-agents", "bitcoin", "wallet"],
19
+ "author": "The House of Set",
20
+ "license": "MIT",
21
+ "homepage": "https://quorumclaw.com",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/aetos53t/agent-multisig-api"
25
+ },
26
+ "peerDependencies": {
27
+ "@elizaos/core": ">=1.0.0"
28
+ },
29
+ "dependencies": {
30
+ "quorum-sdk": "^0.1.0",
31
+ "@noble/curves": "^1.4.0",
32
+ "@noble/hashes": "^1.4.0"
33
+ },
34
+ "devDependencies": {
35
+ "@elizaos/core": "^1.0.0",
36
+ "typescript": "^5.3.3"
37
+ }
38
+ }
@@ -0,0 +1,74 @@
1
+
2
+ import { quorumService } from '../services/quorum.js';
3
+
4
+ export const createMultisigAction = {
5
+ name: 'QUORUM_CREATE_MULTISIG',
6
+ description: 'Create a new multi-agent wallet via Quorum',
7
+
8
+ similes: [
9
+ 'create multisig',
10
+ 'create multi-agent wallet',
11
+ 'setup shared wallet',
12
+ 'create treasury',
13
+ 'create quorum wallet',
14
+ ],
15
+
16
+ examples: [
17
+ [
18
+ { user: '{{user1}}', content: { text: 'Create a 2-of-3 Bitcoin multisig called "Team Treasury"' } },
19
+ { user: '{{agent}}', content: { text: 'Created multisig "Team Treasury" (2-of-3). Invite code: abc123. Share this with other signers to join.' } },
20
+ ],
21
+ ],
22
+
23
+ validate: async (runtime: any, message: any): Promise<any> => {
24
+ const text = message.content?.text?.toLowerCase() || '';
25
+ return text.includes('create') && (text.includes('multisig') || text.includes('wallet') || text.includes('treasury'));
26
+ },
27
+
28
+ handler: async (
29
+ runtime: any,
30
+ message: any,
31
+ state: any,
32
+ options: Record<string, unknown>,
33
+ callback?: any
34
+ ): Promise<any> => {
35
+ try {
36
+ const text = message.content?.text || '';
37
+
38
+ // Parse parameters from message
39
+ const thresholdMatch = text.match(/(\d+)[- ]of[- ](\d+)/i);
40
+ const threshold = thresholdMatch ? parseInt(thresholdMatch[1]) : 2;
41
+ const totalSigners = thresholdMatch ? parseInt(thresholdMatch[2]) : 3;
42
+
43
+ const nameMatch = text.match(/(?:called|named)\s+["']?([^"']+)["']?/i);
44
+ const name = nameMatch ? nameMatch[1].trim() : `Multisig ${Date.now()}`;
45
+
46
+ // Detect chain
47
+ let chainId = 'bitcoin-mainnet';
48
+ if (text.includes('ethereum') || text.includes('eth')) chainId = 'ethereum';
49
+ if (text.includes('solana') || text.includes('sol')) chainId = 'solana-mainnet';
50
+ if (text.includes('base')) chainId = 'base';
51
+ if (text.includes('stacks') || text.includes('stx')) chainId = 'stacks-mainnet';
52
+
53
+ const result = await quorumService.createMultisig({
54
+ name,
55
+ chainId,
56
+ threshold,
57
+ totalSigners,
58
+ });
59
+
60
+ const response = `✅ Created multisig "${name}" (${threshold}-of-${totalSigners} on ${chainId})
61
+
62
+ **Invite Code:** \`${result.inviteCode}\`
63
+ **Join Link:** https://quorumclaw.com/join/${result.inviteCode}
64
+
65
+ Share this with other signers to join the wallet.`;
66
+
67
+ callback?.({ text: response });
68
+ return true;
69
+ } catch (err: any) {
70
+ callback?.({ text: `❌ Failed to create multisig: ${err.message}` });
71
+ return false;
72
+ }
73
+ },
74
+ };
@@ -0,0 +1,99 @@
1
+
2
+ import { quorumService } from '../services/quorum.js';
3
+
4
+ export const createProposalAction = {
5
+ name: 'QUORUM_CREATE_PROPOSAL',
6
+ description: 'Create a new spending proposal in a multi-agent wallet',
7
+
8
+ similes: [
9
+ 'send from multisig',
10
+ 'create proposal',
11
+ 'propose spend',
12
+ 'propose transaction',
13
+ 'send from treasury',
14
+ ],
15
+
16
+ examples: [
17
+ [
18
+ { user: '{{user1}}', content: { text: 'Send 5000 sats to bc1q... from our treasury' } },
19
+ { user: '{{agent}}', content: { text: 'Created proposal! ID: abc123. Waiting for 2 more signatures.' } },
20
+ ],
21
+ ],
22
+
23
+ validate: async (runtime: any, message: any): Promise<any> => {
24
+ const text = message.content?.text?.toLowerCase() || '';
25
+ return (text.includes('send') || text.includes('propose') || text.includes('transfer')) &&
26
+ (text.includes('multisig') || text.includes('treasury') || text.includes('proposal') || text.includes('sats'));
27
+ },
28
+
29
+ handler: async (
30
+ runtime: any,
31
+ message: any,
32
+ state: any,
33
+ options: Record<string, unknown>,
34
+ callback?: any
35
+ ): Promise<any> => {
36
+ try {
37
+ const text = message.content?.text || '';
38
+
39
+ // Parse amount
40
+ const amountMatch = text.match(/(\d+(?:,\d+)?)\s*(?:sats?|satoshis?)/i);
41
+ if (!amountMatch) {
42
+ callback?.({ text: '❌ Please specify an amount in sats. Example: "Send 5000 sats to bc1q..."' });
43
+ return false;
44
+ }
45
+ const amount = parseInt(amountMatch[1].replace(/,/g, ''));
46
+
47
+ // Parse recipient address
48
+ const addressMatch = text.match(/(bc1[a-z0-9]{39,87})/i) || // Bech32
49
+ text.match(/(tb1[a-z0-9]{39,87})/i) || // Testnet
50
+ text.match(/(0x[a-fA-F0-9]{40})/i); // EVM
51
+
52
+ if (!addressMatch) {
53
+ callback?.({ text: '❌ Please provide a recipient address. Example: "Send 5000 sats to bc1q..."' });
54
+ return false;
55
+ }
56
+ const recipient = addressMatch[1];
57
+
58
+ // Get multisigs
59
+ const multisigs = await quorumService.listMultisigs();
60
+ if (multisigs.length === 0) {
61
+ callback?.({ text: '❌ You are not part of any multisigs. Create or join one first.' });
62
+ return false;
63
+ }
64
+
65
+ // Use first multisig or parse from message
66
+ // TODO: Allow specifying which multisig
67
+ const multisig = multisigs[0];
68
+
69
+ // Parse note
70
+ const noteMatch = text.match(/(?:note|memo|for|reason)[:\s]+["']?([^"']+)["']?/i);
71
+ const note = noteMatch ? noteMatch[1].trim() : undefined;
72
+
73
+ const proposal = await quorumService.createProposal({
74
+ multisigId: multisig.id,
75
+ recipient,
76
+ amount,
77
+ note,
78
+ });
79
+
80
+ callback?.({
81
+ text: `✅ **Proposal Created**
82
+
83
+ **ID:** \`${proposal.id}\`
84
+ **Amount:** ${amount.toLocaleString()} sats
85
+ **To:** ${recipient}
86
+ **From:** ${multisig.name}
87
+
88
+ Proposal needs ${multisig.threshold} signatures. Share the proposal ID with other signers.
89
+
90
+ https://quorumclaw.com/p/${proposal.id}`
91
+ });
92
+
93
+ return true;
94
+ } catch (err: any) {
95
+ callback?.({ text: `❌ Failed to create proposal: ${err.message}` });
96
+ return false;
97
+ }
98
+ },
99
+ };
@@ -0,0 +1,65 @@
1
+
2
+ import { quorumService } from '../services/quorum.js';
3
+
4
+ export const joinMultisigAction = {
5
+ name: 'QUORUM_JOIN_MULTISIG',
6
+ description: 'Join an existing multi-agent wallet via invite code',
7
+
8
+ similes: [
9
+ 'join multisig',
10
+ 'join wallet',
11
+ 'accept invite',
12
+ 'join treasury',
13
+ ],
14
+
15
+ examples: [
16
+ [
17
+ { user: '{{user1}}', content: { text: 'Join multisig with code abc123' } },
18
+ { user: '{{agent}}', content: { text: 'Joined multisig "Team Treasury"! Address: bc1p...' } },
19
+ ],
20
+ ],
21
+
22
+ validate: async (runtime: any, message: any): Promise<any> => {
23
+ const text = message.content?.text?.toLowerCase() || '';
24
+ return text.includes('join') && (text.includes('multisig') || text.includes('wallet') || text.includes('code'));
25
+ },
26
+
27
+ handler: async (
28
+ runtime: any,
29
+ message: any,
30
+ state: any,
31
+ options: Record<string, unknown>,
32
+ callback?: any
33
+ ): Promise<any> => {
34
+ try {
35
+ const text = message.content?.text || '';
36
+
37
+ // Extract invite code
38
+ const codeMatch = text.match(/(?:code|invite)?\s*([a-f0-9]{8})/i) ||
39
+ text.match(/join\/([a-f0-9]{8})/i);
40
+
41
+ if (!codeMatch) {
42
+ callback?.({ text: '❌ Please provide an invite code. Example: "Join multisig with code abc12345"' });
43
+ return false;
44
+ }
45
+
46
+ const inviteCode = codeMatch[1];
47
+ const multisig = await quorumService.joinMultisig(inviteCode);
48
+
49
+ const response = `✅ Joined multisig "${multisig.name}"!
50
+
51
+ **Address:** \`${multisig.address}\`
52
+ **Threshold:** ${multisig.threshold}-of-${multisig.agents.length}
53
+ **Chain:** ${multisig.chainId}
54
+ **Signers:** ${multisig.agents.map(a => a.name).join(', ')}
55
+
56
+ The wallet is ready to receive funds.`;
57
+
58
+ callback?.({ text: response });
59
+ return true;
60
+ } catch (err: any) {
61
+ callback?.({ text: `❌ Failed to join multisig: ${err.message}` });
62
+ return false;
63
+ }
64
+ },
65
+ };
@@ -0,0 +1,62 @@
1
+
2
+ import { quorumService } from '../services/quorum.js';
3
+
4
+ export const listProposalsAction = {
5
+ name: 'QUORUM_LIST_PROPOSALS',
6
+ description: 'List pending proposals across all multi-agent wallets',
7
+
8
+ similes: [
9
+ 'list proposals',
10
+ 'show proposals',
11
+ 'pending transactions',
12
+ 'what needs signing',
13
+ 'check proposals',
14
+ ],
15
+
16
+ examples: [
17
+ [
18
+ { user: '{{user1}}', content: { text: 'Show pending proposals' } },
19
+ { user: '{{agent}}', content: { text: 'You have 2 pending proposals:\n- 5000 sats to bc1q... (1/2 sigs)\n- 10000 sats to bc1p... (0/3 sigs)' } },
20
+ ],
21
+ ],
22
+
23
+ validate: async (runtime: any, message: any): Promise<any> => {
24
+ const text = message.content?.text?.toLowerCase() || '';
25
+ return (text.includes('list') || text.includes('show') || text.includes('pending') || text.includes('check')) &&
26
+ (text.includes('proposal') || text.includes('transaction') || text.includes('signing'));
27
+ },
28
+
29
+ handler: async (
30
+ runtime: any,
31
+ message: any,
32
+ state: any,
33
+ options: Record<string, unknown>,
34
+ callback?: any
35
+ ): Promise<any> => {
36
+ try {
37
+ const proposals = await quorumService.listPendingProposals();
38
+
39
+ if (proposals.length === 0) {
40
+ callback?.({ text: '✅ No pending proposals. All caught up!' });
41
+ return true;
42
+ }
43
+
44
+ const list = proposals.map(p => {
45
+ const amount = p.outputs.reduce((sum, o) => sum + parseInt(o.amount), 0);
46
+ const recipient = p.outputs[0]?.address || 'unknown';
47
+ const shortRecipient = `${recipient.slice(0, 8)}...${recipient.slice(-6)}`;
48
+ return `• **${amount.toLocaleString()} sats** → ${shortRecipient}
49
+ ID: \`${p.id.slice(0, 8)}...\` | Sigs: ${p.signatures.length}/? | ${p.note || 'No note'}`;
50
+ }).join('\n\n');
51
+
52
+ callback?.({
53
+ text: `📋 **Pending Proposals (${proposals.length})**\n\n${list}\n\nSay "sign proposal <id>" to approve.`
54
+ });
55
+
56
+ return true;
57
+ } catch (err: any) {
58
+ callback?.({ text: `❌ Failed to list proposals: ${err.message}` });
59
+ return false;
60
+ }
61
+ },
62
+ };
@@ -0,0 +1,92 @@
1
+
2
+ import { quorumService } from '../services/quorum.js';
3
+
4
+ export const signProposalAction = {
5
+ name: 'QUORUM_SIGN_PROPOSAL',
6
+ description: 'Sign a pending proposal in a multi-agent wallet',
7
+
8
+ similes: [
9
+ 'sign proposal',
10
+ 'approve transaction',
11
+ 'sign tx',
12
+ 'approve proposal',
13
+ 'co-sign',
14
+ ],
15
+
16
+ examples: [
17
+ [
18
+ { user: '{{user1}}', content: { text: 'Sign proposal abc123' } },
19
+ { user: '{{agent}}', content: { text: 'Signed! 2/3 signatures collected. Waiting for one more signer.' } },
20
+ ],
21
+ ],
22
+
23
+ validate: async (runtime: any, message: any): Promise<any> => {
24
+ const text = message.content?.text?.toLowerCase() || '';
25
+ return (text.includes('sign') || text.includes('approve')) &&
26
+ (text.includes('proposal') || text.includes('transaction') || text.includes('tx'));
27
+ },
28
+
29
+ handler: async (
30
+ runtime: any,
31
+ message: any,
32
+ state: any,
33
+ options: Record<string, unknown>,
34
+ callback?: any
35
+ ): Promise<any> => {
36
+ try {
37
+ const text = message.content?.text || '';
38
+
39
+ // Extract proposal ID (UUID format)
40
+ const idMatch = text.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i) ||
41
+ text.match(/proposal\s+([a-f0-9-]+)/i);
42
+
43
+ if (!idMatch) {
44
+ // Try to find pending proposals
45
+ const pending = await quorumService.listPendingProposals();
46
+ if (pending.length === 0) {
47
+ callback?.({ text: '❌ No pending proposals found. Provide a proposal ID or create a new proposal.' });
48
+ return false;
49
+ }
50
+
51
+ if (pending.length === 1) {
52
+ // Auto-sign the only pending proposal
53
+ const result = await quorumService.signProposal(pending[0].id);
54
+
55
+ if (result.txid) {
56
+ callback?.({ text: `✅ Signed and broadcast! txid: ${result.txid}\n\nhttps://mempool.space/tx/${result.txid}` });
57
+ } else {
58
+ callback?.({ text: `✅ Signed! Proposal status: ${result.status}. Waiting for more signatures.` });
59
+ }
60
+ return true;
61
+ }
62
+
63
+ // List pending proposals
64
+ const list = pending.map(p =>
65
+ `- \`${p.id.slice(0,8)}...\`: ${p.outputs.map(o => `${o.amount} sats`).join(', ')} (${p.signatures.length} sigs)`
66
+ ).join('\n');
67
+
68
+ callback?.({ text: `Multiple pending proposals. Please specify which one:\n\n${list}` });
69
+ return false;
70
+ }
71
+
72
+ const proposalId = idMatch[1];
73
+ const result = await quorumService.signProposal(proposalId);
74
+
75
+ if (result.txid) {
76
+ callback?.({
77
+ text: `✅ **Threshold met! Transaction broadcast.**\n\n**txid:** \`${result.txid}\`\n\nhttps://mempool.space/tx/${result.txid}`
78
+ });
79
+ } else {
80
+ const proposal = await quorumService.getProposal(proposalId);
81
+ callback?.({
82
+ text: `✅ Signed! ${proposal.signatures.length}/${proposal.sighashes.length + 1} signatures collected. Waiting for more signers.`
83
+ });
84
+ }
85
+
86
+ return true;
87
+ } catch (err: any) {
88
+ callback?.({ text: `❌ Failed to sign: ${err.message}` });
89
+ return false;
90
+ }
91
+ },
92
+ };