@agirails/sdk 2.3.3 → 2.4.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.
Files changed (63) hide show
  1. package/README.md +10 -12
  2. package/dist/ACTPClient.d.ts +80 -3
  3. package/dist/ACTPClient.d.ts.map +1 -1
  4. package/dist/ACTPClient.js +213 -57
  5. package/dist/ACTPClient.js.map +1 -1
  6. package/dist/adapters/BasicAdapter.d.ts +13 -1
  7. package/dist/adapters/BasicAdapter.d.ts.map +1 -1
  8. package/dist/adapters/BasicAdapter.js +24 -3
  9. package/dist/adapters/BasicAdapter.js.map +1 -1
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +9 -292
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/publish.d.ts +11 -3
  14. package/dist/cli/commands/publish.d.ts.map +1 -1
  15. package/dist/cli/commands/publish.js +319 -80
  16. package/dist/cli/commands/publish.js.map +1 -1
  17. package/dist/cli/commands/register.d.ts.map +1 -1
  18. package/dist/cli/commands/register.js +10 -0
  19. package/dist/cli/commands/register.js.map +1 -1
  20. package/dist/cli/utils/config.d.ts +3 -2
  21. package/dist/cli/utils/config.d.ts.map +1 -1
  22. package/dist/cli/utils/config.js +9 -1
  23. package/dist/cli/utils/config.js.map +1 -1
  24. package/dist/cli/utils/wallet.d.ts +31 -0
  25. package/dist/cli/utils/wallet.d.ts.map +1 -0
  26. package/dist/cli/utils/wallet.js +114 -0
  27. package/dist/cli/utils/wallet.js.map +1 -0
  28. package/dist/config/pendingPublish.d.ts +79 -0
  29. package/dist/config/pendingPublish.d.ts.map +1 -0
  30. package/dist/config/pendingPublish.js +167 -0
  31. package/dist/config/pendingPublish.js.map +1 -0
  32. package/dist/config/publishPipeline.d.ts +33 -0
  33. package/dist/config/publishPipeline.d.ts.map +1 -1
  34. package/dist/config/publishPipeline.js +33 -2
  35. package/dist/config/publishPipeline.js.map +1 -1
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +7 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/wallet/AutoWalletProvider.d.ts +2 -1
  41. package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
  42. package/dist/wallet/AutoWalletProvider.js +6 -2
  43. package/dist/wallet/AutoWalletProvider.js.map +1 -1
  44. package/dist/wallet/IWalletProvider.d.ts +4 -2
  45. package/dist/wallet/IWalletProvider.d.ts.map +1 -1
  46. package/dist/wallet/aa/TransactionBatcher.d.ts +54 -0
  47. package/dist/wallet/aa/TransactionBatcher.d.ts.map +1 -1
  48. package/dist/wallet/aa/TransactionBatcher.js +67 -1
  49. package/dist/wallet/aa/TransactionBatcher.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/ACTPClient.ts +265 -49
  52. package/src/adapters/BasicAdapter.ts +48 -12
  53. package/src/cli/commands/init.ts +7 -348
  54. package/src/cli/commands/publish.ts +354 -87
  55. package/src/cli/commands/register.ts +14 -0
  56. package/src/cli/utils/config.ts +11 -2
  57. package/src/cli/utils/wallet.ts +109 -0
  58. package/src/config/pendingPublish.ts +226 -0
  59. package/src/config/publishPipeline.ts +82 -1
  60. package/src/index.ts +8 -0
  61. package/src/wallet/AutoWalletProvider.ts +7 -2
  62. package/src/wallet/IWalletProvider.ts +4 -2
  63. package/src/wallet/aa/TransactionBatcher.ts +113 -0
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Wallet Utilities — Shared wallet generation and Smart Wallet derivation.
3
+ *
4
+ * Extracted from init.ts for reuse by both `actp init` and `actp publish`.
5
+ *
6
+ * @module cli/utils/wallet
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as readline from 'readline';
12
+ import { Output } from './output';
13
+
14
+ /**
15
+ * Generate a new encrypted wallet keystore.
16
+ *
17
+ * - Creates a random ethers.Wallet
18
+ * - Encrypts with password (from ACTP_KEY_PASSWORD env or interactive prompt)
19
+ * - Saves to `{actpDir}/keystore.json` with 0o600 permissions
20
+ *
21
+ * @param actpDir - Path to .actp directory
22
+ * @param output - CLI output handler
23
+ * @returns The generated wallet's address
24
+ */
25
+ export async function generateWallet(actpDir: string, output: Output): Promise<string> {
26
+ const { Wallet } = await import('ethers');
27
+
28
+ const wallet = Wallet.createRandom();
29
+
30
+ // Get password from env var or interactive prompt
31
+ let password = process.env.ACTP_KEY_PASSWORD;
32
+ if (!password) {
33
+ password = await promptPassword();
34
+ }
35
+
36
+ if (!password || password.length < 8) {
37
+ throw new Error(
38
+ 'Wallet password required (minimum 8 characters).\n' +
39
+ 'Set ACTP_KEY_PASSWORD env var or enter when prompted.'
40
+ );
41
+ }
42
+
43
+ // Encrypt with Keystore V3 (scrypt + AES-128-CTR)
44
+ output.info('Encrypting wallet (this takes a few seconds)...');
45
+ const keystore = await wallet.encrypt(password);
46
+
47
+ // Save with restrictive permissions
48
+ const keystorePath = path.join(actpDir, 'keystore.json');
49
+ fs.writeFileSync(keystorePath, keystore, { mode: 0o600 });
50
+
51
+ output.success('Key securely saved and encrypted');
52
+ output.info(`Address: ${wallet.address}`);
53
+ output.warning('Back up your password — it cannot be recovered.');
54
+
55
+ return wallet.address;
56
+ }
57
+
58
+ /**
59
+ * Compute the Smart Wallet address for an EOA signer.
60
+ * Uses CREATE2 counterfactual derivation — no deployment needed.
61
+ *
62
+ * @param eoaAddress - The EOA signer address
63
+ * @param mode - 'testnet' or 'mainnet'
64
+ * @param output - CLI output handler
65
+ * @returns The derived Smart Wallet address
66
+ */
67
+ export async function computeSmartWalletInit(
68
+ eoaAddress: string,
69
+ mode: string,
70
+ output: Output
71
+ ): Promise<string> {
72
+ const { ethers } = await import('ethers');
73
+ const { getNetwork } = await import('../../config/networks');
74
+ const { computeSmartWalletAddress } = await import('../../wallet/aa/UserOpBuilder');
75
+
76
+ const network = mode === 'testnet' ? 'base-sepolia' : 'base-mainnet';
77
+ const networkConfig = getNetwork(network);
78
+ const rpcUrl = networkConfig.rpcUrl;
79
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
80
+
81
+ output.info('Computing Smart Wallet address...');
82
+ const smartWalletAddress = await computeSmartWalletAddress(eoaAddress, provider);
83
+
84
+ output.success(`Smart Wallet: ${smartWalletAddress}`);
85
+
86
+ return smartWalletAddress;
87
+ }
88
+
89
+ /**
90
+ * Interactive password prompt (TTY only).
91
+ * Returns empty string in non-TTY environments (piped/agent mode).
92
+ */
93
+ async function promptPassword(): Promise<string> {
94
+ if (!process.stdin.isTTY) {
95
+ return '';
96
+ }
97
+
98
+ const rl = readline.createInterface({
99
+ input: process.stdin,
100
+ output: process.stdout,
101
+ });
102
+
103
+ return new Promise((resolve) => {
104
+ rl.question('Enter password for wallet encryption (min 8 chars): ', (answer) => {
105
+ rl.close();
106
+ resolve(answer.trim());
107
+ });
108
+ });
109
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Pending Publish Module — Deferred on-chain activation for Lazy Publish.
3
+ *
4
+ * When `actp publish` runs, it saves a `pending-publish.{network}.json` file
5
+ * instead of making on-chain calls. The first real payment triggers activation
6
+ * (registerAgent, publishConfig, setListed) in a single UserOp alongside the
7
+ * payment calls.
8
+ *
9
+ * Files are chain-scoped: testnet and mainnet pending publishes coexist independently.
10
+ * Legacy `pending-publish.json` (unscoped) is supported for migration.
11
+ *
12
+ * The file is deleted after successful on-chain activation.
13
+ *
14
+ * @module config/pendingPublish
15
+ */
16
+
17
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { ServiceDescriptor } from '../types/agent';
20
+
21
+ // ============================================================================
22
+ // Types
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Serializable representation of a ServiceDescriptor.
27
+ * BigInt fields are stored as strings for JSON compatibility.
28
+ */
29
+ interface SerializedServiceDescriptor {
30
+ serviceTypeHash: string;
31
+ serviceType: string;
32
+ schemaURI: string;
33
+ minPrice: string;
34
+ maxPrice: string;
35
+ avgCompletionTime: number;
36
+ metadataCID: string;
37
+ }
38
+
39
+ /**
40
+ * Pending publish state — saved to `.actp/pending-publish.{network}.json`.
41
+ */
42
+ export interface PendingPublish {
43
+ /** Schema version */
44
+ version: 1;
45
+ /** Canonical config hash (bytes32) */
46
+ configHash: string;
47
+ /** IPFS CID of uploaded AGIRAILS.md */
48
+ cid: string;
49
+ /** Agent endpoint URL */
50
+ endpoint: string;
51
+ /** Service descriptors from AGIRAILS.md frontmatter */
52
+ serviceDescriptors: ServiceDescriptor[];
53
+ /** ISO 8601 timestamp of when pending-publish.json was created */
54
+ createdAt: string;
55
+ /** Network identifier (e.g. 'base-sepolia', 'base-mainnet') */
56
+ network?: string;
57
+ }
58
+
59
+ /**
60
+ * JSON-serializable form of PendingPublish (bigints as strings).
61
+ */
62
+ interface SerializedPendingPublish {
63
+ version: 1;
64
+ configHash: string;
65
+ cid: string;
66
+ endpoint: string;
67
+ serviceDescriptors: SerializedServiceDescriptor[];
68
+ createdAt: string;
69
+ network?: string;
70
+ }
71
+
72
+ // ============================================================================
73
+ // Serialization Helpers
74
+ // ============================================================================
75
+
76
+ function serializeDescriptor(sd: ServiceDescriptor): SerializedServiceDescriptor {
77
+ return {
78
+ serviceTypeHash: sd.serviceTypeHash,
79
+ serviceType: sd.serviceType,
80
+ schemaURI: sd.schemaURI,
81
+ minPrice: sd.minPrice.toString(),
82
+ maxPrice: sd.maxPrice.toString(),
83
+ avgCompletionTime: sd.avgCompletionTime,
84
+ metadataCID: sd.metadataCID,
85
+ };
86
+ }
87
+
88
+ function deserializeDescriptor(sd: SerializedServiceDescriptor): ServiceDescriptor {
89
+ return {
90
+ serviceTypeHash: sd.serviceTypeHash,
91
+ serviceType: sd.serviceType,
92
+ schemaURI: sd.schemaURI,
93
+ minPrice: BigInt(sd.minPrice),
94
+ maxPrice: BigInt(sd.maxPrice),
95
+ avgCompletionTime: sd.avgCompletionTime,
96
+ metadataCID: sd.metadataCID,
97
+ };
98
+ }
99
+
100
+ // ============================================================================
101
+ // Public API
102
+ // ============================================================================
103
+
104
+ /**
105
+ * Get the .actp directory path.
106
+ *
107
+ * Respects `ACTP_DIR` env var for custom locations.
108
+ * Defaults to `{cwd}/.actp`.
109
+ */
110
+ export function getActpDir(): string {
111
+ return process.env.ACTP_DIR || join(process.cwd(), '.actp');
112
+ }
113
+
114
+ /**
115
+ * Get the path to a pending-publish file.
116
+ *
117
+ * @param network - Optional network identifier. If provided, returns
118
+ * `pending-publish.{network}.json`. Otherwise returns legacy `pending-publish.json`.
119
+ */
120
+ export function getPendingPublishPath(network?: string): string {
121
+ if (network) {
122
+ return join(getActpDir(), `pending-publish.${network}.json`);
123
+ }
124
+ return join(getActpDir(), 'pending-publish.json');
125
+ }
126
+
127
+ /**
128
+ * Save a pending publish to `.actp/pending-publish.{network}.json`.
129
+ *
130
+ * Creates the .actp directory if it doesn't exist.
131
+ * File is written atomically with mode 0o600 (owner read/write only).
132
+ *
133
+ * If `pending.network` is set, saves to network-scoped file.
134
+ * Otherwise saves to legacy `pending-publish.json`.
135
+ */
136
+ export function savePendingPublish(pending: PendingPublish): void {
137
+ const dir = getActpDir();
138
+ if (!existsSync(dir)) {
139
+ mkdirSync(dir, { recursive: true });
140
+ }
141
+
142
+ const serialized: SerializedPendingPublish = {
143
+ version: pending.version,
144
+ configHash: pending.configHash,
145
+ cid: pending.cid,
146
+ endpoint: pending.endpoint,
147
+ serviceDescriptors: pending.serviceDescriptors.map(serializeDescriptor),
148
+ createdAt: pending.createdAt,
149
+ ...(pending.network ? { network: pending.network } : {}),
150
+ };
151
+
152
+ const filePath = getPendingPublishPath(pending.network);
153
+ writeFileSync(filePath, JSON.stringify(serialized, null, 2), { mode: 0o600 });
154
+ }
155
+
156
+ /**
157
+ * Load a pending publish from `.actp/pending-publish.{network}.json`.
158
+ *
159
+ * If `network` is provided:
160
+ * 1. Try `pending-publish.{network}.json`
161
+ * 2. Fall back to legacy `pending-publish.json` (migration)
162
+ *
163
+ * If no `network`: loads legacy `pending-publish.json`.
164
+ *
165
+ * Returns null if no file found.
166
+ */
167
+ export function loadPendingPublish(network?: string): PendingPublish | null {
168
+ // Try network-scoped file first
169
+ if (network) {
170
+ const scopedPath = getPendingPublishPath(network);
171
+ if (existsSync(scopedPath)) {
172
+ return deserializePendingPublish(readFileSync(scopedPath, 'utf-8'));
173
+ }
174
+ }
175
+
176
+ // Fall back to legacy file
177
+ const legacyPath = getPendingPublishPath();
178
+ if (!existsSync(legacyPath)) {
179
+ return null;
180
+ }
181
+
182
+ return deserializePendingPublish(readFileSync(legacyPath, 'utf-8'));
183
+ }
184
+
185
+ /**
186
+ * Delete the pending-publish file for a given network.
187
+ *
188
+ * Deletes both the network-scoped file and legacy file (cleanup).
189
+ * No-op if files don't exist. Best-effort — never throws.
190
+ */
191
+ export function deletePendingPublish(network?: string): void {
192
+ try {
193
+ // Delete network-scoped file
194
+ if (network) {
195
+ const scopedPath = getPendingPublishPath(network);
196
+ if (existsSync(scopedPath)) {
197
+ unlinkSync(scopedPath);
198
+ }
199
+ }
200
+
201
+ // Also delete legacy file if it exists (cleanup)
202
+ const legacyPath = getPendingPublishPath();
203
+ if (existsSync(legacyPath)) {
204
+ unlinkSync(legacyPath);
205
+ }
206
+ } catch {
207
+ // Best-effort: file deletion should never crash post-payment UX
208
+ }
209
+ }
210
+
211
+ // ============================================================================
212
+ // Internal Helpers
213
+ // ============================================================================
214
+
215
+ function deserializePendingPublish(raw: string): PendingPublish {
216
+ const serialized: SerializedPendingPublish = JSON.parse(raw);
217
+ return {
218
+ version: serialized.version,
219
+ configHash: serialized.configHash,
220
+ cid: serialized.cid,
221
+ endpoint: serialized.endpoint,
222
+ serviceDescriptors: serialized.serviceDescriptors.map(deserializeDescriptor),
223
+ createdAt: serialized.createdAt,
224
+ ...(serialized.network ? { network: serialized.network } : {}),
225
+ };
226
+ }
@@ -176,7 +176,88 @@ export function extractRegistrationParams(
176
176
  }
177
177
 
178
178
  // ============================================================================
179
- // Pipeline
179
+ // Prepare Publish (offline — no on-chain calls)
180
+ // ============================================================================
181
+
182
+ export interface PreparePublishOptions {
183
+ /** Path to AGIRAILS.md file */
184
+ path: string;
185
+ /** Filebase client for IPFS upload */
186
+ filebaseClient: FilebaseClient;
187
+ /** Arweave client for permanent storage (optional) */
188
+ arweaveClient?: ArweaveClient;
189
+ /** Skip Arweave upload */
190
+ skipArweave?: boolean;
191
+ /** Dry run — compute and show but don't execute */
192
+ dryRun?: boolean;
193
+ }
194
+
195
+ export interface PreparePublishResult {
196
+ /** IPFS CID of uploaded AGIRAILS.md */
197
+ cid: string;
198
+ /** Canonical config hash (bytes32) */
199
+ configHash: string;
200
+ /** Arweave transaction ID (if uploaded) */
201
+ arweaveTxId?: string;
202
+ /** Parsed frontmatter */
203
+ frontmatter: Record<string, unknown>;
204
+ /** Parsed body */
205
+ body: string;
206
+ /** Whether this was a dry run */
207
+ dryRun: boolean;
208
+ }
209
+
210
+ /**
211
+ * Prepare publish — IPFS upload + hash computation only.
212
+ *
213
+ * No on-chain calls. Returns the CID and configHash for
214
+ * saving to pending-publish.json (lazy publish flow).
215
+ */
216
+ export async function preparePublish(options: PreparePublishOptions): Promise<PreparePublishResult> {
217
+ const {
218
+ path,
219
+ filebaseClient,
220
+ arweaveClient,
221
+ skipArweave = false,
222
+ dryRun = false,
223
+ } = options;
224
+
225
+ // Read and parse
226
+ const content = readFileSync(path, 'utf-8');
227
+ const { frontmatter, body } = parseAgirailsMd(content);
228
+ const { configHash } = computeConfigHash(content);
229
+
230
+ if (dryRun) {
231
+ return { cid: '(dry-run)', configHash, frontmatter, body, dryRun: true };
232
+ }
233
+
234
+ // Upload to IPFS
235
+ const ipfsResult = await filebaseClient.uploadBinary(
236
+ Buffer.from(content, 'utf-8'),
237
+ 'text/markdown',
238
+ { metadata: { type: 'agirails-config', version: '1.0' } }
239
+ );
240
+ const cid = ipfsResult.cid;
241
+
242
+ // Arweave (optional)
243
+ let arweaveTxId: string | undefined;
244
+ if (!skipArweave && arweaveClient) {
245
+ const arweaveResult = await arweaveClient.uploadJSON(
246
+ { frontmatter, body, _format: 'agirails.md.v1' },
247
+ [
248
+ { name: 'Type', value: 'agent-config' },
249
+ { name: 'ConfigHash', value: configHash },
250
+ { name: 'IPFS-CID', value: cid },
251
+ ]
252
+ );
253
+ arweaveTxId = arweaveResult.txId;
254
+ }
255
+
256
+ return { cid, configHash, arweaveTxId, frontmatter, body, dryRun: false };
257
+ }
258
+
259
+ // ============================================================================
260
+ // Pipeline (legacy — makes on-chain calls)
180
261
  // ============================================================================
181
262
 
182
263
  /**
package/src/index.ts CHANGED
@@ -64,6 +64,12 @@ export {
64
64
  StandardTransactionParams,
65
65
  } from './adapters/StandardAdapter';
66
66
 
67
+ export {
68
+ X402Adapter,
69
+ X402AdapterConfig,
70
+ X402PayParams,
71
+ } from './adapters/X402Adapter';
72
+
67
73
  export { AdapterRegistry } from './adapters/AdapterRegistry';
68
74
 
69
75
  export {
@@ -353,4 +359,6 @@ export {
353
359
  ArweaveTimeoutError,
354
360
  InvalidArweaveTxIdError,
355
361
  InsufficientBalanceError as StorageInsufficientBalanceError,
362
+ SwapExecutionError,
363
+ QueryCapExceededError,
356
364
  } from './errors';
@@ -191,7 +191,7 @@ export class AutoWalletProvider implements IWalletProvider {
191
191
  * Builds approve + createTransaction + linkEscrow as a single UserOp.
192
192
  * Manages ACTP nonce inside the mutex queue for concurrent safety.
193
193
  */
194
- async payACTPBatched(params: BatchedPayParams): Promise<BatchedPayResult> {
194
+ async payACTPBatched(params: BatchedPayParams, prependCalls?: SmartWalletCall[]): Promise<BatchedPayResult> {
195
195
  return this.nonceManager.enqueue(
196
196
  async ({ entryPointNonce, actpNonce }) => {
197
197
  const batch = buildACTPPayBatch({
@@ -199,7 +199,12 @@ export class AutoWalletProvider implements IWalletProvider {
199
199
  actpNonce,
200
200
  });
201
201
 
202
- const receipt = await this.submitUserOp(batch.calls, entryPointNonce);
202
+ // Combine activation calls (if any) with payment calls
203
+ const allCalls = prependCalls && prependCalls.length > 0
204
+ ? [...prependCalls, ...batch.calls]
205
+ : batch.calls;
206
+
207
+ const receipt = await this.submitUserOp(allCalls, entryPointNonce);
203
208
 
204
209
  return {
205
210
  result: {
@@ -127,7 +127,9 @@ export interface IWalletProvider {
127
127
  * Only available on AA wallets (supportsBatching: true).
128
128
  * Handles ACTP nonce management internally via the DualNonceManager mutex.
129
129
  *
130
- * Returns undefined if batched payments are not supported.
130
+ * @param params - Payment batch parameters
131
+ * @param prependCalls - Optional calls to prepend (e.g., lazy publish activation)
132
+ * @returns undefined if batched payments are not supported.
131
133
  */
132
- payACTPBatched?(params: BatchedPayParams): Promise<BatchedPayResult>;
134
+ payACTPBatched?(params: BatchedPayParams, prependCalls?: import('./aa/constants').SmartWalletCall[]): Promise<BatchedPayResult>;
133
135
  }
@@ -154,6 +154,8 @@ export function buildACTPPayBatch(params: ACTPBatchParams): ACTPBatchResult {
154
154
  */
155
155
  const AGENT_REGISTRY_ABI = [
156
156
  'function registerAgent(string endpoint, (bytes32 serviceTypeHash, string serviceType, string schemaURI, uint256 minPrice, uint256 maxPrice, uint256 avgCompletionTime, string metadataCID)[] serviceDescriptors)',
157
+ 'function publishConfig(string cid, bytes32 configHash)',
158
+ 'function setListed(bool listed)',
157
159
  ];
158
160
 
159
161
  /**
@@ -238,3 +240,114 @@ export function buildTestnetInitBatch(params: {
238
240
  );
239
241
  return [...registerCalls, ...mintCalls];
240
242
  }
243
+
244
+ // ============================================================================
245
+ // Lazy Publish — Activation Batch Builders
246
+ // ============================================================================
247
+
248
+ /**
249
+ * Lazy publish activation scenario.
250
+ *
251
+ * - 'A': First activation — registerAgent + publishConfig + setListed (3 calls)
252
+ * - 'B1': Re-publish with listing change — publishConfig + setListed (2 calls)
253
+ * - 'B2': Re-publish config only — publishConfig (1 call)
254
+ * - 'C': Stale pending — delete pending-publish.json, no calls
255
+ * - 'none': No pending publish, normal flow
256
+ */
257
+ export type ActivationScenario = 'A' | 'B1' | 'B2' | 'C' | 'none';
258
+
259
+ /**
260
+ * Parameters for building an activation batch.
261
+ */
262
+ export interface ActivationBatchParams {
263
+ /** Activation scenario */
264
+ scenario: ActivationScenario;
265
+ /** AgentRegistry contract address */
266
+ agentRegistryAddress: string;
267
+ /** IPFS CID of the published AGIRAILS.md */
268
+ cid: string;
269
+ /** Canonical config hash (bytes32) */
270
+ configHash: string;
271
+ /** Agent endpoint URL (for scenario A registration) */
272
+ endpoint?: string;
273
+ /** Service descriptors (for scenario A registration) */
274
+ serviceDescriptors?: ServiceDescriptor[];
275
+ /** Whether to set listed=true (for scenarios A, B1) */
276
+ listed?: boolean;
277
+ }
278
+
279
+ /**
280
+ * Build a publishConfig batch call for AgentRegistry.
281
+ *
282
+ * @param agentRegistryAddress - AgentRegistry contract address
283
+ * @param cid - IPFS CID of the uploaded AGIRAILS.md
284
+ * @param configHash - Canonical config hash (bytes32)
285
+ */
286
+ export function buildPublishConfigBatch(
287
+ agentRegistryAddress: string,
288
+ cid: string,
289
+ configHash: string
290
+ ): SmartWalletCall[] {
291
+ const iface = new ethers.Interface(AGENT_REGISTRY_ABI);
292
+ const data = iface.encodeFunctionData('publishConfig', [cid, configHash]);
293
+ return [{ target: agentRegistryAddress, value: 0n, data }];
294
+ }
295
+
296
+ /**
297
+ * Build a setListed batch call for AgentRegistry.
298
+ *
299
+ * @param agentRegistryAddress - AgentRegistry contract address
300
+ * @param listed - Whether to list the agent
301
+ */
302
+ export function buildSetListedBatch(
303
+ agentRegistryAddress: string,
304
+ listed: boolean
305
+ ): SmartWalletCall[] {
306
+ const iface = new ethers.Interface(AGENT_REGISTRY_ABI);
307
+ const data = iface.encodeFunctionData('setListed', [listed]);
308
+ return [{ target: agentRegistryAddress, value: 0n, data }];
309
+ }
310
+
311
+ /**
312
+ * Build the full activation batch based on scenario.
313
+ *
314
+ * Scenario call counts:
315
+ * - A: registerAgent + publishConfig + setListed = 3 calls
316
+ * - B1: publishConfig + setListed = 2 calls
317
+ * - B2: publishConfig = 1 call
318
+ * - C/none: empty (0 calls)
319
+ */
320
+ export function buildActivationBatch(params: ActivationBatchParams): SmartWalletCall[] {
321
+ const { scenario, agentRegistryAddress, cid, configHash } = params;
322
+
323
+ switch (scenario) {
324
+ case 'A': {
325
+ // First activation: register + publish + list
326
+ if (!params.endpoint || !params.serviceDescriptors || params.serviceDescriptors.length === 0) {
327
+ throw new Error('Scenario A requires endpoint and serviceDescriptors');
328
+ }
329
+ const registerCalls = buildRegisterAgentBatch(
330
+ agentRegistryAddress,
331
+ params.endpoint,
332
+ params.serviceDescriptors
333
+ );
334
+ const publishCalls = buildPublishConfigBatch(agentRegistryAddress, cid, configHash);
335
+ const listCalls = buildSetListedBatch(agentRegistryAddress, params.listed ?? true);
336
+ return [...registerCalls, ...publishCalls, ...listCalls];
337
+ }
338
+ case 'B1': {
339
+ // Re-publish with listing: publish + list
340
+ const publishCalls = buildPublishConfigBatch(agentRegistryAddress, cid, configHash);
341
+ const listCalls = buildSetListedBatch(agentRegistryAddress, params.listed ?? true);
342
+ return [...publishCalls, ...listCalls];
343
+ }
344
+ case 'B2': {
345
+ // Re-publish config only
346
+ return buildPublishConfigBatch(agentRegistryAddress, cid, configHash);
347
+ }
348
+ case 'C':
349
+ case 'none':
350
+ // Stale or no pending — no activation calls
351
+ return [];
352
+ }
353
+ }