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/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
+ }