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
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quorum Plugin for Eliza
|
|
3
|
+
*
|
|
4
|
+
* Enables any Eliza agent to participate in multi-agent wallets via Quorum.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { quorumService } from './services/quorum.js';
|
|
8
|
+
import { createMultisigAction } from './actions/createMultisig.js';
|
|
9
|
+
import { joinMultisigAction } from './actions/joinMultisig.js';
|
|
10
|
+
import { signProposalAction } from './actions/signProposal.js';
|
|
11
|
+
import { listProposalsAction } from './actions/listProposals.js';
|
|
12
|
+
import { createProposalAction } from './actions/createProposal.js';
|
|
13
|
+
import { multisigProvider } from './providers/multisigInfo.js';
|
|
14
|
+
|
|
15
|
+
export const quorumPlugin = {
|
|
16
|
+
name: 'quorum',
|
|
17
|
+
description: 'Multi-agent wallet coordination via Quorum',
|
|
18
|
+
|
|
19
|
+
// Initialize on plugin load
|
|
20
|
+
async init(runtime: any) {
|
|
21
|
+
await quorumService.initialize(runtime);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
actions: [
|
|
25
|
+
createMultisigAction,
|
|
26
|
+
joinMultisigAction,
|
|
27
|
+
signProposalAction,
|
|
28
|
+
listProposalsAction,
|
|
29
|
+
createProposalAction,
|
|
30
|
+
],
|
|
31
|
+
|
|
32
|
+
providers: [multisigProvider],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default quorumPlugin;
|
|
36
|
+
|
|
37
|
+
// Re-export for convenience
|
|
38
|
+
export { quorumService } from './services/quorum.js';
|
|
39
|
+
export * from './actions/createMultisig.js';
|
|
40
|
+
export * from './actions/joinMultisig.js';
|
|
41
|
+
export * from './actions/signProposal.js';
|
|
42
|
+
export * from './actions/listProposals.js';
|
|
43
|
+
export * from './actions/createProposal.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { quorumService } from '../services/quorum.js';
|
|
2
|
+
|
|
3
|
+
export const multisigProvider = {
|
|
4
|
+
name: 'QUORUM_MULTISIG_INFO',
|
|
5
|
+
description: 'Provides context about multi-agent wallets and pending proposals',
|
|
6
|
+
|
|
7
|
+
get: async (runtime: any, message: any, state?: any): Promise<any> => {
|
|
8
|
+
try {
|
|
9
|
+
const agentId = quorumService.getAgentId();
|
|
10
|
+
if (!agentId) {
|
|
11
|
+
return ''; // Not initialized
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const multisigs = await quorumService.listMultisigs();
|
|
15
|
+
const proposals = await quorumService.listPendingProposals();
|
|
16
|
+
|
|
17
|
+
if (multisigs.length === 0 && proposals.length === 0) {
|
|
18
|
+
return ''; // No relevant context
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let context = '## Quorum Multi-Agent Wallets\n\n';
|
|
22
|
+
|
|
23
|
+
if (multisigs.length > 0) {
|
|
24
|
+
context += '### My Wallets\n';
|
|
25
|
+
for (const ms of multisigs) {
|
|
26
|
+
context += `- **${ms.name}**: ${ms.address.slice(0, 12)}... (${ms.threshold}-of-${ms.agents.length} on ${ms.chainId})\n`;
|
|
27
|
+
}
|
|
28
|
+
context += '\n';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (proposals.length > 0) {
|
|
32
|
+
context += '### ⚠️ Pending Proposals Requiring Signature\n';
|
|
33
|
+
for (const p of proposals) {
|
|
34
|
+
const amount = p.outputs.reduce((sum, o) => sum + parseInt(o.amount), 0);
|
|
35
|
+
context += `- **${amount} sats** to ${p.outputs[0]?.address.slice(0, 12)}... [${p.signatures.length} sigs] - ID: ${p.id.slice(0, 8)}...\n`;
|
|
36
|
+
}
|
|
37
|
+
context += '\nUse "sign proposal <id>" to approve these transactions.\n';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return context;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error('[Quorum Provider] Error:', err);
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quorum Service for Eliza
|
|
3
|
+
*
|
|
4
|
+
* Manages connection to Quorum API and handles signing operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { schnorr } from '@noble/curves/secp256k1';
|
|
8
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
9
|
+
|
|
10
|
+
const QUORUM_API = process.env.QUORUM_API_URL || 'https://quorumclaw.com';
|
|
11
|
+
|
|
12
|
+
export interface QuorumAgent {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
publicKey: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QuorumMultisig {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
address: string;
|
|
22
|
+
chainId: string;
|
|
23
|
+
threshold: number;
|
|
24
|
+
agents: QuorumAgent[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface QuorumProposal {
|
|
28
|
+
id: string;
|
|
29
|
+
multisigId: string;
|
|
30
|
+
status: 'pending' | 'finalized' | 'expired';
|
|
31
|
+
outputs: Array<{ address: string; amount: string }>;
|
|
32
|
+
signatures: Array<{ agentId: string; signature: string }>;
|
|
33
|
+
sighashes: Array<{ inputIndex: number; sighash: string }>;
|
|
34
|
+
note?: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class QuorumService {
|
|
39
|
+
private agentId: string | null = null;
|
|
40
|
+
private publicKey: string | null = null;
|
|
41
|
+
private privateKey: Uint8Array | null = null;
|
|
42
|
+
|
|
43
|
+
get capabilityDescription(): string {
|
|
44
|
+
return 'Multi-agent wallet coordination via Quorum';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async initialize(runtime: any): Promise<void> {
|
|
48
|
+
// Try to get private key from runtime settings
|
|
49
|
+
const privateKeyHex = runtime.getSetting?.('QUORUM_PRIVATE_KEY') ||
|
|
50
|
+
runtime.getSetting?.('WALLET_PRIVATE_KEY') ||
|
|
51
|
+
process.env.QUORUM_PRIVATE_KEY;
|
|
52
|
+
|
|
53
|
+
if (!privateKeyHex) {
|
|
54
|
+
console.warn('[Quorum] No private key configured. Set QUORUM_PRIVATE_KEY or WALLET_PRIVATE_KEY.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const keyHex = String(privateKeyHex).replace('0x', '');
|
|
60
|
+
this.privateKey = hexToBytes(keyHex);
|
|
61
|
+
this.publicKey = bytesToHex(schnorr.getPublicKey(this.privateKey));
|
|
62
|
+
|
|
63
|
+
// Register with Quorum
|
|
64
|
+
await this.register(runtime.character?.name || 'Eliza Agent');
|
|
65
|
+
|
|
66
|
+
console.log(`[Quorum] Initialized. Agent ID: ${this.agentId}`);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error('[Quorum] Failed to initialize:', err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async register(name: string): Promise<QuorumAgent> {
|
|
73
|
+
if (!this.publicKey) throw new Error('No public key available');
|
|
74
|
+
|
|
75
|
+
const res = await fetch(`${QUORUM_API}/v1/agents`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
name,
|
|
80
|
+
publicKey: this.publicKey,
|
|
81
|
+
provider: 'eliza',
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const json = await res.json();
|
|
86
|
+
if (!json.success) throw new Error(json.error?.message || 'Registration failed');
|
|
87
|
+
|
|
88
|
+
this.agentId = json.data.id;
|
|
89
|
+
return json.data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async createMultisig(params: {
|
|
93
|
+
name: string;
|
|
94
|
+
chainId: string;
|
|
95
|
+
threshold: number;
|
|
96
|
+
totalSigners: number;
|
|
97
|
+
}): Promise<{ multisig: QuorumMultisig; inviteCode: string }> {
|
|
98
|
+
if (!this.agentId) throw new Error('Not registered with Quorum');
|
|
99
|
+
|
|
100
|
+
const res = await fetch(`${QUORUM_API}/v1/invites`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
name: params.name,
|
|
105
|
+
chainId: params.chainId,
|
|
106
|
+
threshold: params.threshold,
|
|
107
|
+
slots: params.totalSigners,
|
|
108
|
+
creatorAgentId: this.agentId,
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const json = await res.json();
|
|
113
|
+
if (!json.success) throw new Error(json.error?.message || 'Create failed');
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
multisig: json.data.multisig,
|
|
117
|
+
inviteCode: json.data.code,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async joinMultisig(inviteCode: string): Promise<QuorumMultisig> {
|
|
122
|
+
if (!this.agentId) throw new Error('Not registered with Quorum');
|
|
123
|
+
|
|
124
|
+
const res = await fetch(`${QUORUM_API}/v1/invites/${inviteCode}/join`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({ agentId: this.agentId }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const json = await res.json();
|
|
131
|
+
if (!json.success) throw new Error(json.error?.message || 'Join failed');
|
|
132
|
+
|
|
133
|
+
return json.data.multisig;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async listMultisigs(): Promise<QuorumMultisig[]> {
|
|
137
|
+
if (!this.agentId) return [];
|
|
138
|
+
|
|
139
|
+
const res = await fetch(`${QUORUM_API}/v1/agents/${this.agentId}/multisigs`);
|
|
140
|
+
const json = await res.json();
|
|
141
|
+
|
|
142
|
+
return json.success ? json.data : [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async listPendingProposals(): Promise<QuorumProposal[]> {
|
|
146
|
+
const multisigs = await this.listMultisigs();
|
|
147
|
+
const proposals: QuorumProposal[] = [];
|
|
148
|
+
|
|
149
|
+
for (const ms of multisigs) {
|
|
150
|
+
const res = await fetch(`${QUORUM_API}/v1/proposals?multisigId=${ms.id}&status=pending`);
|
|
151
|
+
const json = await res.json();
|
|
152
|
+
if (json.success) {
|
|
153
|
+
proposals.push(...json.data);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return proposals;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async getProposal(proposalId: string): Promise<QuorumProposal> {
|
|
161
|
+
const res = await fetch(`${QUORUM_API}/v1/proposals/${proposalId}`);
|
|
162
|
+
const json = await res.json();
|
|
163
|
+
if (!json.success) throw new Error(json.error?.message || 'Proposal not found');
|
|
164
|
+
return json.data;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async signProposal(proposalId: string): Promise<{ success: boolean; status: string; txid?: string }> {
|
|
168
|
+
if (!this.agentId || !this.privateKey) {
|
|
169
|
+
throw new Error('Not initialized');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get proposal to get sighash
|
|
173
|
+
const proposal = await this.getProposal(proposalId);
|
|
174
|
+
|
|
175
|
+
if (proposal.status !== 'pending') {
|
|
176
|
+
throw new Error(`Proposal is ${proposal.status}, not pending`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if we already signed
|
|
180
|
+
if (proposal.signatures.some(s => s.agentId === this.agentId)) {
|
|
181
|
+
throw new Error('Already signed this proposal');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Sign each input's sighash
|
|
185
|
+
for (const sh of proposal.sighashes) {
|
|
186
|
+
const sighashBytes = hexToBytes(sh.sighash);
|
|
187
|
+
const signature = schnorr.sign(sighashBytes, this.privateKey);
|
|
188
|
+
|
|
189
|
+
const res = await fetch(`${QUORUM_API}/v1/proposals/${proposalId}/sign`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
agentId: this.agentId,
|
|
194
|
+
signature: bytesToHex(signature),
|
|
195
|
+
inputIndex: sh.inputIndex,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const json = await res.json();
|
|
200
|
+
if (!json.success) throw new Error(json.error?.message || 'Sign failed');
|
|
201
|
+
|
|
202
|
+
if (json.data.thresholdMet) {
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
status: 'finalized',
|
|
206
|
+
txid: json.data.txid,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { success: true, status: 'pending' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async createProposal(params: {
|
|
215
|
+
multisigId: string;
|
|
216
|
+
recipient: string;
|
|
217
|
+
amount: number;
|
|
218
|
+
note?: string;
|
|
219
|
+
}): Promise<QuorumProposal> {
|
|
220
|
+
const res = await fetch(`${QUORUM_API}/v1/proposals`, {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { 'Content-Type': 'application/json' },
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
multisigId: params.multisigId,
|
|
225
|
+
outputs: [{ address: params.recipient, amount: params.amount.toString() }],
|
|
226
|
+
note: params.note,
|
|
227
|
+
createdBy: this.agentId,
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const json = await res.json();
|
|
232
|
+
if (!json.success) throw new Error(json.error?.message || 'Create proposal failed');
|
|
233
|
+
return json.data;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getAgentId(): string | null {
|
|
237
|
+
return this.agentId;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
getPublicKey(): string | null {
|
|
241
|
+
return this.publicKey;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const quorumService = new QuorumService();
|
|
246
|
+
export default quorumService;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": false,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noImplicitAny": false
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|