@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
package/src/ACTPClient.ts CHANGED
@@ -11,10 +11,9 @@
11
11
  *
12
12
  * @example
13
13
  * ```typescript
14
- * // Create client in mock mode
14
+ * // Create client (auto-detects wallet from .actp/keystore.json or env vars)
15
15
  * const client = await ACTPClient.create({
16
16
  * mode: 'mock',
17
- * requesterAddress: '0x1234...',
18
17
  * });
19
18
  *
20
19
  * // Basic API - simplest approach
@@ -57,6 +56,9 @@ import { getNetwork } from './config/networks';
57
56
  import { IWalletProvider } from './wallet/IWalletProvider';
58
57
  import { EOAWalletProvider } from './wallet/EOAWalletProvider';
59
58
  import { AutoWalletProvider } from './wallet/AutoWalletProvider';
59
+ import { SmartWalletCall } from './wallet/aa/constants';
60
+ import { buildActivationBatch, ActivationScenario } from './wallet/aa/TransactionBatcher';
61
+ import { loadPendingPublish, deletePendingPublish, PendingPublish } from './config/pendingPublish';
60
62
  import { sdkLogger } from './utils/Logger';
61
63
 
62
64
  // ============================================================================
@@ -74,18 +76,24 @@ import { sdkLogger } from './utils/Logger';
74
76
  * @param stateDirectory - The directory path to validate
75
77
  * @throws Error if path is unsafe
76
78
  */
79
+ /** On-chain agent state from AgentRegistry. */
80
+ export interface OnChainAgentState {
81
+ registeredAt: bigint;
82
+ configHash: string;
83
+ listed: boolean;
84
+ }
85
+
86
+ const ZERO_HASH = '0x' + '0'.repeat(64);
87
+
77
88
  /**
78
- * Check if an agent is registered on AgentRegistry.
79
- * Lightweight read-only check no signer needed.
80
- *
81
- * Uses minimal ABI fragment to avoid importing the full AgentRegistry class.
82
- * Checks registeredAt field of AgentProfile struct (> 0 means registered).
89
+ * Read the on-chain agent state from AgentRegistry.
90
+ * Returns registeredAt, configHash, and listed fields.
83
91
  */
84
- async function checkRegistration(
92
+ export async function getOnChainAgentState(
85
93
  provider: ethers.JsonRpcProvider,
86
94
  registryAddress: string,
87
95
  agentAddress: string
88
- ): Promise<boolean> {
96
+ ): Promise<OnChainAgentState> {
89
97
  const contract = new ethers.Contract(
90
98
  registryAddress,
91
99
  [
@@ -98,7 +106,46 @@ async function checkRegistration(
98
106
  provider
99
107
  );
100
108
  const profile = await contract.getAgent(agentAddress);
101
- return profile.registeredAt > 0n;
109
+ return {
110
+ registeredAt: profile.registeredAt,
111
+ configHash: profile.configHash,
112
+ listed: profile.listed,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Detect the lazy publish activation scenario.
118
+ *
119
+ * Decision matrix:
120
+ * - A: Not registered + has pending → first-time activation
121
+ * - B1: Registered + pending hash != on-chain hash + not listed → re-publish + list
122
+ * - B2: Registered + pending hash != on-chain hash + already listed → re-publish only
123
+ * - C: Pending hash == on-chain hash → stale pending, delete it
124
+ * - none: No pending publish file
125
+ */
126
+ export function detectLazyPublishScenario(
127
+ onChainState: OnChainAgentState,
128
+ pendingPublish: PendingPublish | null
129
+ ): ActivationScenario {
130
+ if (!pendingPublish) return 'none';
131
+
132
+ const isRegistered = onChainState.registeredAt > 0n;
133
+ const pendingHash = pendingPublish.configHash;
134
+ const onChainHash = onChainState.configHash;
135
+
136
+ if (!isRegistered) {
137
+ // Not registered — scenario A: full activation
138
+ return 'A';
139
+ }
140
+
141
+ // Registered — check if pending hash differs from on-chain
142
+ if (pendingHash !== onChainHash) {
143
+ // Config differs — need to publish
144
+ return onChainState.listed ? 'B2' : 'B1';
145
+ }
146
+
147
+ // Hash matches — stale pending
148
+ return 'C';
102
149
  }
103
150
 
104
151
  function validateStateDirectory(stateDirectory: string): void {
@@ -519,6 +566,39 @@ export class ACTPClient {
519
566
  */
520
567
  private readonly walletProvider?: IWalletProvider;
521
568
 
569
+ /**
570
+ * Lazy Publish: Current activation scenario.
571
+ * Set during create(), consumed during first payACTPBatched().
572
+ * @internal
573
+ */
574
+ private lazyScenario: ActivationScenario = 'none';
575
+
576
+ /**
577
+ * Lazy Publish: Cached pending publish data.
578
+ * @internal
579
+ */
580
+ private pendingPublish: PendingPublish | null = null;
581
+
582
+ /**
583
+ * AgentRegistry address (for lazy activation calls).
584
+ * @internal
585
+ */
586
+ private agentRegistryAddress?: string;
587
+
588
+ /**
589
+ * Network identifier (e.g. 'base-sepolia', 'base-mainnet').
590
+ * Used for chain-scoped pending-publish file operations.
591
+ * @internal
592
+ */
593
+ private networkId?: string;
594
+
595
+ /**
596
+ * Whether the pending publish config is stale (AGIRAILS.md changed since last publish).
597
+ * When true, getActivationCalls() returns empty to prevent stale config going on-chain.
598
+ * @internal
599
+ */
600
+ private pendingIsStale = false;
601
+
522
602
  /**
523
603
  * Private constructor - use ACTPClient.create() factory method.
524
604
  */
@@ -530,14 +610,22 @@ export class ACTPClient {
530
610
  erc8004Bridge?: ERC8004Bridge,
531
611
  reputationReporter?: ReputationReporter,
532
612
  walletProvider?: IWalletProvider,
533
- contractAddresses?: { usdc: string; actpKernel: string; escrowVault: string }
613
+ contractAddresses?: { usdc: string; actpKernel: string; escrowVault: string },
614
+ lazyScenario: ActivationScenario = 'none',
615
+ pendingPublish: PendingPublish | null = null,
616
+ agentRegistryAddress?: string,
617
+ networkId?: string,
534
618
  ) {
535
619
  this.runtime = runtime;
536
620
  this.info = info;
537
621
  this.easHelper = easHelper;
538
622
  this.reputationReporter = reputationReporter;
539
623
  this.walletProvider = walletProvider;
540
- this.basic = new BasicAdapter(runtime, requesterAddress, easHelper, walletProvider, contractAddresses);
624
+ this.lazyScenario = lazyScenario;
625
+ this.pendingPublish = pendingPublish;
626
+ this.agentRegistryAddress = agentRegistryAddress;
627
+ this.networkId = networkId;
628
+ this.basic = new BasicAdapter(runtime, requesterAddress, easHelper, walletProvider, contractAddresses, this);
541
629
  this.standard = new StandardAdapter(runtime, requesterAddress, easHelper);
542
630
 
543
631
  // Initialize registry and router
@@ -594,6 +682,10 @@ export class ACTPClient {
594
682
  let walletProvider: IWalletProvider | undefined;
595
683
  let requesterAddress: string;
596
684
  let contractAddresses: { usdc: string; actpKernel: string; escrowVault: string } | undefined;
685
+ let lazyScenario: ActivationScenario = 'none';
686
+ let lazyPending: PendingPublish | null = null;
687
+ let registryAddr: string | undefined;
688
+ let networkId: string | undefined;
597
689
 
598
690
  // If custom runtime provided, use it directly
599
691
  if (config.runtime) {
@@ -613,17 +705,18 @@ export class ACTPClient {
613
705
  // Initialize runtime based on mode
614
706
  switch (config.mode) {
615
707
  case 'mock': {
616
- // Mock mode: requesterAddress is mandatory
617
- if (!config.requesterAddress) {
618
- throw new Error('requesterAddress is required for mock mode');
619
- }
620
- if (!/^0x[a-fA-F0-9]{40}$/.test(config.requesterAddress)) {
621
- throw new Error(
622
- `Invalid requesterAddress: "${config.requesterAddress}". ` +
623
- 'Must be a valid Ethereum address (0x-prefixed, 40 hex chars)'
624
- );
708
+ // Mock mode: requesterAddress is optional (auto-generate if missing)
709
+ if (config.requesterAddress) {
710
+ if (!/^0x[a-fA-F0-9]{40}$/.test(config.requesterAddress)) {
711
+ throw new Error(
712
+ `Invalid requesterAddress: "${config.requesterAddress}". ` +
713
+ 'Must be a valid Ethereum address (0x-prefixed, 40 hex chars)'
714
+ );
715
+ }
716
+ requesterAddress = config.requesterAddress;
717
+ } else {
718
+ requesterAddress = ethers.Wallet.createRandom().address;
625
719
  }
626
- requesterAddress = config.requesterAddress;
627
720
 
628
721
  // SECURITY FIX: Enhanced path validation to prevent path traversal attacks
629
722
  if (config.stateDirectory) {
@@ -640,15 +733,27 @@ export class ACTPClient {
640
733
 
641
734
  case 'testnet':
642
735
  case 'mainnet': {
643
- // Validate required parameters for blockchain modes
736
+ // Auto-detect private key from keystore / env var if not provided
644
737
  if (!config.privateKey) {
645
- throw new Error(
646
- `privateKey is required for ${config.mode} mode`
647
- );
738
+ const { resolvePrivateKey } = await import('./wallet/keystore');
739
+ const resolved = await resolvePrivateKey(config.stateDirectory);
740
+ if (resolved) {
741
+ config = { ...config, privateKey: resolved };
742
+ } else {
743
+ throw new Error(
744
+ `No wallet found for ${config.mode} mode.\n\n` +
745
+ 'Provide a private key via one of:\n' +
746
+ ' 1. ACTP_KEY_PASSWORD env var + .actp/keystore.json (recommended)\n' +
747
+ ' 2. ACTP_PRIVATE_KEY env var\n' +
748
+ ' 3. privateKey option in ACTPClient.create()\n' +
749
+ ' 4. Run "actp publish" to generate a wallet automatically'
750
+ );
751
+ }
648
752
  }
649
753
 
650
754
  // Map mode to network config
651
755
  const network = config.mode === 'testnet' ? 'base-sepolia' : 'base-mainnet';
756
+ networkId = network;
652
757
  const networkConfig = getNetwork(network);
653
758
 
654
759
  // Default RPC URL from network config if not provided
@@ -661,7 +766,8 @@ export class ACTPClient {
661
766
 
662
767
  // Create ethers provider and signer
663
768
  const provider = new ethers.JsonRpcProvider(rpcUrl);
664
- const signer = new ethers.Wallet(config.privateKey, provider);
769
+ const privateKey = config.privateKey!; // Guaranteed by auto-detect above
770
+ const signer = new ethers.Wallet(privateKey, provider);
665
771
 
666
772
  // ====================================================================
667
773
  // AIP-12: Wallet Provider Selection
@@ -705,43 +811,72 @@ export class ACTPClient {
705
811
  },
706
812
  });
707
813
 
708
- // Check AgentRegistry gasless only for registered agents
814
+ // Check AgentRegistry + Lazy Publish scenario
709
815
  const smartWalletAddress = autoWallet.getAddress();
710
- const agentRegistryAddress = config.contracts?.agentRegistry
816
+ registryAddr = config.contracts?.agentRegistry
711
817
  ?? networkConfig.contracts.agentRegistry;
712
818
 
713
- let isRegistered = false;
714
- if (agentRegistryAddress) {
819
+ // Load pending publish (may be null) — chain-scoped
820
+ try {
821
+ lazyPending = loadPendingPublish(network);
822
+ } catch {
823
+ // Ignore file read errors
824
+ }
825
+
826
+ let useAutoWallet = false;
827
+
828
+ if (registryAddr) {
715
829
  try {
716
- isRegistered = await checkRegistration(
717
- provider, agentRegistryAddress, smartWalletAddress
830
+ const onChainState = await getOnChainAgentState(
831
+ provider, registryAddr, smartWalletAddress
718
832
  );
833
+ lazyScenario = detectLazyPublishScenario(onChainState, lazyPending);
834
+
835
+ // Scenario C: stale pending — delete immediately
836
+ if (lazyScenario === 'C') {
837
+ deletePendingPublish(network);
838
+ lazyPending = null;
839
+ lazyScenario = 'none';
840
+ }
841
+
842
+ // Gate: configHash != ZERO || hasPendingPublish → use AutoWallet
843
+ const hasOnChainConfig = onChainState.configHash !== ZERO_HASH;
844
+ const hasPendingPublish = lazyPending !== null;
845
+
846
+ if (hasOnChainConfig || hasPendingPublish) {
847
+ useAutoWallet = true;
848
+ }
719
849
  } catch {
720
- // Registry check failed (e.g. RPC down) — allow AA anyway.
721
- // Rationale: don't punish legit registered agents for infra issues.
722
- // Paymaster contract allowlist + rate limits prevent abuse.
723
- isRegistered = true;
724
- sdkLogger.warn('AgentRegistry check failed, proceeding with AA wallet');
850
+ // Registry check failed (e.g. RPC down).
851
+ // Fail-open only if pending publish exists (agent did `actp publish` → legitimate intent).
852
+ // Fail-closed otherwise to prevent unregistered agents getting free gas.
853
+ if (lazyPending) {
854
+ useAutoWallet = true;
855
+ sdkLogger.warn('AgentRegistry check failed, but pending publish found — proceeding with AA.');
856
+ } else {
857
+ sdkLogger.warn('AgentRegistry check failed and no pending publish — falling back to EOA.');
858
+ }
725
859
  }
726
860
  } else {
727
861
  // No registry deployed — skip check (early testnet)
728
- isRegistered = true;
862
+ useAutoWallet = true;
729
863
  }
730
864
 
731
- if (isRegistered) {
865
+ if (useAutoWallet) {
732
866
  walletProvider = autoWallet;
733
867
  requesterAddress = smartWalletAddress;
734
868
  } else {
735
- // Not registered — fall back to EOA with warning
869
+ // Not published and no pending — fall back to EOA with warning
736
870
  sdkLogger.warn(
737
- 'Agent not registered on AgentRegistry. ' +
871
+ 'Agent not published on AgentRegistry and no pending publish found. ' +
738
872
  'Falling back to EOA wallet (gas not sponsored). ' +
739
- 'Run "actp register" for gas-free transactions.'
873
+ 'Run "actp publish" for gas-free transactions.'
740
874
  );
741
875
  walletProvider = new EOAWalletProvider(signer, networkConfig.chainId);
742
- // Force signer.address — config.requesterAddress may be the Smart Wallet
743
- // address (set by `actp init --wallet auto`), which would be wrong for EOA.
744
876
  requesterAddress = signer.address;
877
+ // Reset since we're not using auto wallet
878
+ lazyScenario = 'none';
879
+ lazyPending = null;
745
880
  }
746
881
  } else {
747
882
  // Tier 2: EOA Wallet (backward compatible)
@@ -824,11 +959,35 @@ export class ACTPClient {
824
959
  walletTier: walletProvider?.getWalletInfo().tier,
825
960
  };
826
961
 
827
- // Pass wallet provider and contract addresses to constructor
962
+ // Staleness check: recompute hash if AGIRAILS.md exists and we have a pending publish
963
+ let pendingIsStale = false;
964
+ if (lazyPending && lazyScenario !== 'none' && lazyScenario !== 'C') {
965
+ try {
966
+ const mdPath = path.join(process.cwd(), 'AGIRAILS.md');
967
+ if (fs.existsSync(mdPath)) {
968
+ const { computeConfigHash } = await import('./config/agirailsmd');
969
+ const content = fs.readFileSync(mdPath, 'utf-8');
970
+ const { configHash: currentHash } = computeConfigHash(content);
971
+ if (currentHash !== lazyPending.configHash) {
972
+ pendingIsStale = true;
973
+ sdkLogger.warn(
974
+ 'AGIRAILS.md changed since last publish. Activation skipped. ' +
975
+ 'Run "actp publish" to update.'
976
+ );
977
+ }
978
+ }
979
+ } catch {
980
+ // Best-effort: staleness check should not block operation
981
+ }
982
+ }
983
+
984
+ // Pass wallet provider, contract addresses, and lazy publish state to constructor
828
985
  const client = new ACTPClient(
829
986
  runtime, normalizedAddress, info, easHelper,
830
- erc8004Bridge, reputationReporter, walletProvider, contractAddresses
987
+ erc8004Bridge, reputationReporter, walletProvider, contractAddresses,
988
+ lazyScenario, lazyPending, registryAddr, networkId,
831
989
  );
990
+ client.pendingIsStale = pendingIsStale;
832
991
 
833
992
  // Drift detection: non-blocking check for AGIRAILS.md sync status
834
993
  if (config.mode !== 'mock') {
@@ -1239,7 +1398,7 @@ export class ACTPClient {
1239
1398
  * @example
1240
1399
  * ```typescript
1241
1400
  * // Register a custom x402 adapter
1242
- * client.registerAdapter(new X402Adapter(client.runtime, requesterAddress));
1401
+ * client.registerAdapter(new X402Adapter(requesterAddress, { expectedNetwork, transferFn }));
1243
1402
  * ```
1244
1403
  */
1245
1404
  registerAdapter(adapter: IAdapter): void {
@@ -1299,6 +1458,56 @@ export class ACTPClient {
1299
1458
  return this.walletProvider;
1300
1459
  }
1301
1460
 
1461
+ /**
1462
+ * Get activation calls for lazy publish.
1463
+ *
1464
+ * Returns SmartWalletCall[] to prepend to the first payment UserOp,
1465
+ * plus an onSuccess callback that deletes pending-publish.json.
1466
+ *
1467
+ * Returns empty calls if no activation is needed (scenario C/none).
1468
+ * @internal
1469
+ */
1470
+ getActivationCalls(): { calls: SmartWalletCall[]; onSuccess: () => void } {
1471
+ if (this.lazyScenario === 'none' || this.lazyScenario === 'C' || !this.agentRegistryAddress) {
1472
+ return { calls: [], onSuccess: () => {} };
1473
+ }
1474
+
1475
+ // Staleness check: AGIRAILS.md changed since last publish → skip activation
1476
+ if (this.pendingIsStale) {
1477
+ return { calls: [], onSuccess: () => {} };
1478
+ }
1479
+
1480
+ const pending = this.pendingPublish;
1481
+ if (!pending) {
1482
+ return { calls: [], onSuccess: () => {} };
1483
+ }
1484
+
1485
+ // Build activation batch params
1486
+ const params: import('./wallet/aa/TransactionBatcher').ActivationBatchParams = {
1487
+ scenario: this.lazyScenario,
1488
+ agentRegistryAddress: this.agentRegistryAddress,
1489
+ cid: pending.cid,
1490
+ configHash: pending.configHash,
1491
+ listed: true,
1492
+ };
1493
+
1494
+ // For scenario A, extract registration params from pending publish
1495
+ if (this.lazyScenario === 'A') {
1496
+ params.endpoint = pending.endpoint;
1497
+ params.serviceDescriptors = pending.serviceDescriptors;
1498
+ }
1499
+
1500
+ const calls = buildActivationBatch(params);
1501
+
1502
+ const onSuccess = () => {
1503
+ deletePendingPublish(this.networkId);
1504
+ this.lazyScenario = 'none';
1505
+ this.pendingPublish = null;
1506
+ };
1507
+
1508
+ return { calls, onSuccess };
1509
+ }
1510
+
1302
1511
  /**
1303
1512
  * Non-blocking drift detection for AGIRAILS.md config.
1304
1513
  * Checks if local AGIRAILS.md matches on-chain config hash.
@@ -1313,7 +1522,14 @@ export class ACTPClient {
1313
1522
  // Look for AGIRAILS.md in cwd
1314
1523
  const agirailsMdPath = join(process.cwd(), 'AGIRAILS.md');
1315
1524
  if (!existsSync(agirailsMdPath)) {
1316
- return; // No local file — nothing to check
1525
+ // No local file — try cached hash from pending-publish.json
1526
+ const { loadPendingPublish: loadPP } = await import('./config/pendingPublish');
1527
+ const driftNetwork = config.mode === 'testnet' ? 'base-sepolia' : 'base-mainnet';
1528
+ const pp = loadPP(driftNetwork);
1529
+ if (pp) {
1530
+ sdkLogger.info('[AGIRAILS] No AGIRAILS.md found, using cached config hash from pending-publish.json');
1531
+ }
1532
+ return;
1317
1533
  }
1318
1534
 
1319
1535
  const network = config.mode === 'testnet' ? 'base-sepolia' : 'base-mainnet';
@@ -23,8 +23,17 @@ import {
23
23
  UnifiedPayResult,
24
24
  } from '../types/adapter';
25
25
  import { IWalletProvider } from '../wallet/IWalletProvider';
26
+ import { SmartWalletCall } from '../wallet/aa/constants';
26
27
  import { ethers } from 'ethers';
27
28
 
29
+ /**
30
+ * Interface for lazy publish activation call provider.
31
+ * ACTPClient implements this to avoid circular dependency.
32
+ */
33
+ export interface IActivationCallProvider {
34
+ getActivationCalls(): { calls: SmartWalletCall[]; onSuccess: () => void };
35
+ }
36
+
28
37
  /**
29
38
  * Parameters for creating a simple payment.
30
39
  *
@@ -126,7 +135,8 @@ export class BasicAdapter extends BaseAdapter implements IAdapter {
126
135
  requesterAddress: string,
127
136
  private easHelper?: EASHelper,
128
137
  private walletProvider?: IWalletProvider,
129
- private contractAddresses?: { usdc: string; actpKernel: string; escrowVault: string }
138
+ private contractAddresses?: { usdc: string; actpKernel: string; escrowVault: string },
139
+ private activationProvider?: IActivationCallProvider,
130
140
  ) {
131
141
  super(requesterAddress);
132
142
  }
@@ -194,25 +204,51 @@ export class BasicAdapter extends BaseAdapter implements IAdapter {
194
204
  }
195
205
 
196
206
  // ====================================================================
197
- // AIP-12: Batched payment via AA wallet (1 UserOp = 3 on-chain calls)
207
+ // AIP-12: Batched payment via AA wallet (1 UserOp = N on-chain calls)
208
+ // With lazy publish: activation calls prepended to first payment.
198
209
  // ====================================================================
199
210
  if (this.walletProvider?.payACTPBatched && this.contractAddresses) {
200
211
  const serviceHash = ethers.ZeroHash;
201
- const result = await this.walletProvider.payACTPBatched({
202
- provider,
203
- requester,
204
- amount: amount.toString(),
205
- deadline,
206
- disputeWindow,
207
- serviceHash,
208
- agentId: agentId || '0',
209
- contracts: this.contractAddresses,
210
- });
212
+
213
+ // Get lazy publish activation calls (if any)
214
+ let prependCalls: SmartWalletCall[] = [];
215
+ let onActivationSuccess: (() => void) | undefined;
216
+
217
+ if (this.activationProvider) {
218
+ const activation = this.activationProvider.getActivationCalls();
219
+ prependCalls = activation.calls;
220
+ if (prependCalls.length > 0) {
221
+ onActivationSuccess = activation.onSuccess;
222
+ }
223
+ }
224
+
225
+ const result = await this.walletProvider.payACTPBatched(
226
+ {
227
+ provider,
228
+ requester,
229
+ amount: amount.toString(),
230
+ deadline,
231
+ disputeWindow,
232
+ serviceHash,
233
+ agentId: agentId || '0',
234
+ contracts: this.contractAddresses,
235
+ },
236
+ prependCalls.length > 0 ? prependCalls : undefined,
237
+ );
211
238
 
212
239
  if (!result.success) {
213
240
  throw new Error(`Batched payment UserOp failed: ${result.hash}`);
214
241
  }
215
242
 
243
+ // Delete pending-publish.json on successful activation (best-effort)
244
+ if (onActivationSuccess) {
245
+ try {
246
+ onActivationSuccess();
247
+ } catch {
248
+ // Best-effort: activation succeeded, don't crash over cleanup
249
+ }
250
+ }
251
+
216
252
  return {
217
253
  txId: result.txId,
218
254
  provider,