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.
- package/README.md +113 -0
- package/dist/actions/createMultisig.d.ts +13 -0
- package/dist/actions/createMultisig.js +61 -0
- package/dist/actions/createProposal.d.ts +13 -0
- package/dist/actions/createProposal.js +79 -0
- package/dist/actions/joinMultisig.d.ts +13 -0
- package/dist/actions/joinMultisig.js +49 -0
- package/dist/actions/listProposals.d.ts +13 -0
- package/dist/actions/listProposals.js +47 -0
- package/dist/actions/signProposal.d.ts +13 -0
- package/dist/actions/signProposal.js +72 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +36 -0
- package/dist/providers/multisigInfo.d.ts +5 -0
- package/dist/providers/multisigInfo.js +39 -0
- package/dist/services/quorum.d.ts +73 -0
- package/dist/services/quorum.js +179 -0
- package/package.json +38 -0
- package/src/actions/createMultisig.ts +74 -0
- package/src/actions/createProposal.ts +99 -0
- package/src/actions/joinMultisig.ts +65 -0
- package/src/actions/listProposals.ts +62 -0
- package/src/actions/signProposal.ts +92 -0
- package/src/index.ts +43 -0
- package/src/providers/multisigInfo.ts +46 -0
- package/src/services/quorum.ts +246 -0
- package/tsconfig.json +14 -0
|
@@ -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
|
+
};
|