@agirails/sdk 2.3.1 → 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 (77) 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 +1 -1
  11. package/dist/cli/commands/init.d.ts.map +1 -1
  12. package/dist/cli/commands/init.js +82 -237
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/cli/commands/publish.d.ts +11 -3
  15. package/dist/cli/commands/publish.d.ts.map +1 -1
  16. package/dist/cli/commands/publish.js +319 -80
  17. package/dist/cli/commands/publish.js.map +1 -1
  18. package/dist/cli/commands/register.d.ts.map +1 -1
  19. package/dist/cli/commands/register.js +10 -0
  20. package/dist/cli/commands/register.js.map +1 -1
  21. package/dist/cli/utils/config.d.ts +17 -2
  22. package/dist/cli/utils/config.d.ts.map +1 -1
  23. package/dist/cli/utils/config.js +9 -1
  24. package/dist/cli/utils/config.js.map +1 -1
  25. package/dist/cli/utils/wallet.d.ts +31 -0
  26. package/dist/cli/utils/wallet.d.ts.map +1 -0
  27. package/dist/cli/utils/wallet.js +114 -0
  28. package/dist/cli/utils/wallet.js.map +1 -0
  29. package/dist/config/pendingPublish.d.ts +79 -0
  30. package/dist/config/pendingPublish.d.ts.map +1 -0
  31. package/dist/config/pendingPublish.js +167 -0
  32. package/dist/config/pendingPublish.js.map +1 -0
  33. package/dist/config/publishPipeline.d.ts +33 -0
  34. package/dist/config/publishPipeline.d.ts.map +1 -1
  35. package/dist/config/publishPipeline.js +33 -2
  36. package/dist/config/publishPipeline.js.map +1 -1
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +7 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/wallet/AutoWalletProvider.d.ts +2 -1
  42. package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
  43. package/dist/wallet/AutoWalletProvider.js +6 -2
  44. package/dist/wallet/AutoWalletProvider.js.map +1 -1
  45. package/dist/wallet/IWalletProvider.d.ts +4 -2
  46. package/dist/wallet/IWalletProvider.d.ts.map +1 -1
  47. package/dist/wallet/aa/BundlerClient.d.ts.map +1 -1
  48. package/dist/wallet/aa/BundlerClient.js +2 -1
  49. package/dist/wallet/aa/BundlerClient.js.map +1 -1
  50. package/dist/wallet/aa/DualNonceManager.d.ts +2 -1
  51. package/dist/wallet/aa/DualNonceManager.d.ts.map +1 -1
  52. package/dist/wallet/aa/DualNonceManager.js +16 -5
  53. package/dist/wallet/aa/DualNonceManager.js.map +1 -1
  54. package/dist/wallet/aa/PaymasterClient.d.ts.map +1 -1
  55. package/dist/wallet/aa/PaymasterClient.js +2 -1
  56. package/dist/wallet/aa/PaymasterClient.js.map +1 -1
  57. package/dist/wallet/aa/TransactionBatcher.d.ts +54 -0
  58. package/dist/wallet/aa/TransactionBatcher.d.ts.map +1 -1
  59. package/dist/wallet/aa/TransactionBatcher.js +67 -1
  60. package/dist/wallet/aa/TransactionBatcher.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/ACTPClient.ts +265 -49
  63. package/src/adapters/BasicAdapter.ts +48 -12
  64. package/src/cli/commands/init.ts +81 -281
  65. package/src/cli/commands/publish.ts +354 -87
  66. package/src/cli/commands/register.ts +14 -0
  67. package/src/cli/utils/config.ts +32 -2
  68. package/src/cli/utils/wallet.ts +109 -0
  69. package/src/config/pendingPublish.ts +226 -0
  70. package/src/config/publishPipeline.ts +82 -1
  71. package/src/index.ts +8 -0
  72. package/src/wallet/AutoWalletProvider.ts +7 -2
  73. package/src/wallet/IWalletProvider.ts +4 -2
  74. package/src/wallet/aa/BundlerClient.ts +2 -1
  75. package/src/wallet/aa/DualNonceManager.ts +19 -9
  76. package/src/wallet/aa/PaymasterClient.ts +2 -1
  77. 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
  }
@@ -233,8 +233,9 @@ export class BundlerClient {
233
233
  const json = (await response.json()) as JsonRpcResponse<T>;
234
234
 
235
235
  if (json.error) {
236
+ const dataStr = json.error.data ? ` | data: ${JSON.stringify(json.error.data)}` : '';
236
237
  const err = new Error(
237
- `Bundler RPC error ${json.error.code}: ${json.error.message}`
238
+ `Bundler RPC error ${json.error.code}: ${json.error.message}${dataStr}`
238
239
  );
239
240
  (err as any).code = json.error.code;
240
241
  (err as any).data = json.error.data;
@@ -141,17 +141,27 @@ export class DualNonceManager {
141
141
 
142
142
  /**
143
143
  * Read current ACTP nonce for the requester.
144
- * requesterNonces is public on ACTPKernel.
144
+ * requesterNonces is public on ACTPKernel (added in v2).
145
+ * Older deployments may not expose this — fall back to 0n.
145
146
  */
146
147
  private async readActpNonce(): Promise<bigint> {
147
- const kernel = new ethers.Contract(
148
- this.actpKernelAddress,
149
- ACTP_KERNEL_NONCE_ABI,
150
- this.provider
151
- );
152
- const nonce = await kernel.requesterNonces(this.senderAddress);
153
- this.cachedActpNonce = nonce;
154
- return nonce;
148
+ try {
149
+ const kernel = new ethers.Contract(
150
+ this.actpKernelAddress,
151
+ ACTP_KERNEL_NONCE_ABI,
152
+ this.provider
153
+ );
154
+ const nonce = await kernel.requesterNonces(this.senderAddress);
155
+ this.cachedActpNonce = nonce;
156
+ return nonce;
157
+ } catch {
158
+ // Older ACTPKernel deployments don't expose requesterNonces.
159
+ // Return 0n — registration doesn't need it, and payment batches
160
+ // will fail at the contract level anyway if nonces are wrong.
161
+ sdkLogger.warn('requesterNonces not available on ACTPKernel — using 0 (older deployment?)');
162
+ this.cachedActpNonce = 0n;
163
+ return 0n;
164
+ }
155
165
  }
156
166
 
157
167
  /**
@@ -160,8 +160,9 @@ export class PaymasterClient {
160
160
  const json = (await response.json()) as JsonRpcResponse<T>;
161
161
 
162
162
  if (json.error) {
163
+ const dataStr = json.error.data ? ` | data: ${JSON.stringify(json.error.data)}` : '';
163
164
  throw new Error(
164
- `Paymaster RPC error ${json.error.code}: ${json.error.message}`
165
+ `Paymaster RPC error ${json.error.code}: ${json.error.message}${dataStr}`
165
166
  );
166
167
  }
167
168
 
@@ -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
+ }