@0xsequence/catapult 1.3.6 → 1.3.8

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.
@@ -1492,7 +1492,7 @@ describe('ExecutionEngine', () => {
1492
1492
  }
1493
1493
 
1494
1494
  await expect((engine as any).executeAction(action, context, new Map())).rejects.toThrow(new Error(`Nick's method test failed for action "nick-test-fail-twice"`))
1495
- await expect((engine as any).executeAction(action, context, new Map())).rejects.toThrow(new Error('Nick\'s method test already performed this run'))
1495
+ await expect((engine as any).executeAction(action, context, new Map())).rejects.toThrow(new Error('Nick\'s method test already failed this run'))
1496
1496
  })
1497
1497
 
1498
1498
  it('should prevent passing Nick\'s method from being tested twice', async () => {
@@ -29,7 +29,7 @@ export class ExecutionEngine {
29
29
  private readonly noPostCheckConditions: boolean
30
30
  private readonly allowMultipleNicksMethodTests: boolean
31
31
  private readonly ignoreVerifyErrors: boolean
32
- private nicksMethodTested: boolean = false
32
+ private nicksMethodResult: boolean | undefined
33
33
  private verificationWarnings: Array<{
34
34
  actionName: string
35
35
  address: string
@@ -464,17 +464,17 @@ export class ExecutionEngine {
464
464
 
465
465
  // Handle gas limit with optional multiplier
466
466
  const network = context.getNetwork()
467
+ const signer = await context.getResolvedSigner()
467
468
  if (network.gasLimit) {
468
469
  const baseGasLimit = network.gasLimit
469
470
  txParams.gasLimit = gasMultiplier ? Math.floor(baseGasLimit * gasMultiplier) : baseGasLimit
470
471
  } else if (gasMultiplier) {
471
472
  // If gasMultiplier is specified but no network gasLimit, estimate gas first
472
- const signer = await context.getResolvedSigner()
473
473
  const estimatedGas = await signer.estimateGas({ to, data, value })
474
474
  txParams.gasLimit = Math.floor(Number(estimatedGas) * gasMultiplier)
475
475
  }
476
476
 
477
- const signer = await context.getResolvedSigner()
477
+ await this.checkFundsForTransaction(actionName, txParams, context, signer)
478
478
  const tx = await signer.sendTransaction(txParams)
479
479
 
480
480
  this.events.emitEvent({
@@ -795,17 +795,17 @@ export class ExecutionEngine {
795
795
 
796
796
  // Handle gas limit with optional multiplier
797
797
  const network = context.getNetwork()
798
+ const signer = await context.getResolvedSigner()
798
799
  if (network.gasLimit) {
799
800
  const baseGasLimit = network.gasLimit
800
801
  txParams.gasLimit = gasMultiplier ? Math.floor(baseGasLimit * gasMultiplier) : baseGasLimit
801
802
  } else if (gasMultiplier) {
802
803
  // If gasMultiplier is specified but no network gasLimit, estimate gas first
803
- const signer = await context.getResolvedSigner()
804
- const estimatedGas = await signer.estimateGas({ to: null, data, value })
804
+ const estimatedGas = await signer.estimateGas(txParams)
805
805
  txParams.gasLimit = Math.floor(Number(estimatedGas) * gasMultiplier)
806
806
  }
807
-
808
- const signer = await context.getResolvedSigner()
807
+
808
+ await this.checkFundsForTransaction(actionName, txParams, context, signer)
809
809
  const tx = await signer.sendTransaction(txParams)
810
810
 
811
811
  this.events.emitEvent({
@@ -855,17 +855,19 @@ export class ExecutionEngine {
855
855
  break
856
856
  }
857
857
  case 'test-nicks-method': {
858
- if (this.nicksMethodTested && !this.allowMultipleNicksMethodTests) {
859
- try {
860
- if (context.getOutput(`${action.name}.success`) === true) {
861
- // Return previous result
862
- break
863
- }
864
- } catch (e) {
865
- throw new Error(`Nick's method test already performed this run`)
858
+ if (this.nicksMethodResult !== undefined && !this.allowMultipleNicksMethodTests) {
859
+ if (this.nicksMethodResult === false) {
860
+ throw new Error(`Nick's method test already failed this run`)
866
861
  }
862
+ this.events.emitEvent({
863
+ type: 'debug_info',
864
+ level: 'debug',
865
+ data: {
866
+ message: `Nick's method test already passed this run`,
867
+ },
868
+ })
869
+ break
867
870
  }
868
- this.nicksMethodTested = true
869
871
 
870
872
  // Default bytecode if none provided
871
873
  const defaultBytecode = '0x608060405234801561001057600080fd5b5061013d806100206000396000f3fe60806040526004361061001e5760003560e01c80639c4ae2d014610023575b600080fd5b6100cb6004803603604081101561003957600080fd5b81019060208101813564010000000081111561005457600080fd5b82018360208201111561006657600080fd5b8035906020019184600183028401116401000000008311171561008857600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955050913592506100cd915050565b005b60008183516020850134f56040805173ffffffffffffffffffffffffffffffffffffffff83168152905191925081900360200190a050505056fea264697066735822122033609f614f03931b92d88c309d698449bb77efcd517328d341fa4f923c5d8c7964736f6c63430007060033'
@@ -884,6 +886,7 @@ export class ExecutionEngine {
884
886
  const fundingAmount = resolvedFundingAmount ? validateBigNumberish(resolvedFundingAmount, actionName, 'fundingAmount') : undefined
885
887
 
886
888
  const success = await this.testNicksMethod(bytecode, context, gasPrice, gasLimit, fundingAmount)
889
+ this.nicksMethodResult = success
887
890
 
888
891
  if (!success) {
889
892
  throw new Error(`Nick's method test failed for action "${actionName}"`)
@@ -1629,7 +1632,7 @@ export class ExecutionEngine {
1629
1632
  /**
1630
1633
  * Evaluates a list of conditions and returns true if any of them are met.
1631
1634
  */
1632
- private async evaluateSkipConditions(
1635
+ public async evaluateSkipConditions(
1633
1636
  conditions: Condition[] | undefined,
1634
1637
  context: ExecutionContext,
1635
1638
  scope: ResolutionScope,
@@ -1700,6 +1703,61 @@ export class ExecutionEngine {
1700
1703
  return sorted
1701
1704
  }
1702
1705
 
1706
+ /**
1707
+ * Checks if the signer has enough funds to cover the estimated cost of the transaction.
1708
+ * Returns true if the signer has enough funds, false if the signer does not have enough funds, and null if no gas price is available.
1709
+ */
1710
+ private async checkFundsForTransaction(actionName: string, txParams: ethers.TransactionRequest, context: ExecutionContext, signer: ethers.Signer): Promise<boolean | null> {
1711
+ try {
1712
+ const gasPrice = txParams.gasPrice || await context.provider.getFeeData().then(data => data.gasPrice)
1713
+ if (!gasPrice) {
1714
+ this.events.emitEvent({
1715
+ type: 'debug_info',
1716
+ level: 'warn',
1717
+ data: {
1718
+ actionName: actionName,
1719
+ message: `No gas price available`
1720
+ }
1721
+ })
1722
+ return null
1723
+ }
1724
+ const gasLimit = txParams.gasLimit || await signer.estimateGas(txParams)
1725
+ const requiredETH = BigInt(gasLimit) * BigInt(gasPrice)
1726
+ const signerBalance = await context.provider.getBalance(await signer.getAddress())
1727
+ this.events.emitEvent({
1728
+ type: 'debug_info',
1729
+ level: 'debug',
1730
+ data: {
1731
+ actionName: actionName,
1732
+ message: `Transaction ${txParams.gasLimit ? 'set' : 'estimated'} gas limit: ${gasLimit}, ${txParams.gasPrice ? 'set' : 'estimated'} gas price: ${ethers.formatUnits(gasPrice, 'gwei')} gwei, required ETH: ${ethers.formatEther(requiredETH)}`
1733
+ }
1734
+ })
1735
+ if (signerBalance < requiredETH) {
1736
+ this.events.emitEvent({
1737
+ type: 'debug_info',
1738
+ level: 'warn',
1739
+ data: {
1740
+ actionName: actionName,
1741
+ message: `Insufficient funds: signer has ${ethers.formatEther(signerBalance)} ETH but estimated cost is ${ethers.formatEther(requiredETH)} ETH`
1742
+ }
1743
+ })
1744
+ return false
1745
+ } else {
1746
+ return true
1747
+ }
1748
+ } catch (error) {
1749
+ this.events.emitEvent({
1750
+ type: 'debug_info',
1751
+ level: 'warn',
1752
+ data: {
1753
+ actionName: actionName,
1754
+ message: "Error checking signer balance: " + (error instanceof Error ? error.message : String(error))
1755
+ }
1756
+ })
1757
+ }
1758
+ return null
1759
+ }
1760
+
1703
1761
  /**
1704
1762
  * Get all verification warnings that were collected when ignoreVerifyErrors is enabled
1705
1763
  */
@@ -16,31 +16,31 @@ import type { RunSummaryEvent } from './events'
16
16
  export interface DeployerOptions {
17
17
  /** The root directory of the deployment project. */
18
18
  projectRoot: string
19
-
19
+
20
20
  /** The private key of the EOA to be used as the signer/relayer. Optional if an implicit sender from RPC is desired. */
21
21
  privateKey?: string
22
-
22
+
23
23
  /** An array of network configurations to use for deployment. */
24
24
  networks: Network[]
25
-
25
+
26
26
  /** Optional: An array of job names to execute. If not provided, all jobs are considered. */
27
27
  runJobs?: string[]
28
-
28
+
29
29
  /** Optional: An array of chain IDs to run on. If not provided, all configured networks are used. */
30
30
  runOnNetworks?: number[]
31
-
31
+
32
32
  /** Optional: Custom event emitter instance. If not provided, uses the global singleton. */
33
33
  eventEmitter?: DeploymentEventEmitter
34
-
34
+
35
35
  /** Optional: Project loader options (e.g., whether to load standard templates). */
36
36
  loaderOptions?: ProjectLoaderOptions
37
-
37
+
38
38
  /** Optional Etherscan API key for contract verification. */
39
39
  etherscanApiKey?: string
40
-
40
+
41
41
  /** Optional: Stop execution as soon as any job fails. Defaults to false. */
42
42
  failEarly?: boolean
43
-
43
+
44
44
  /** Optional: Skip post-execution check of skip conditions. Defaults to false (post-check enabled). */
45
45
  noPostCheckConditions?: boolean
46
46
 
@@ -68,11 +68,11 @@ export class Deployer {
68
68
  private readonly loader: ProjectLoader
69
69
  private readonly noPostCheckConditions: boolean
70
70
  private readonly showSummary: boolean
71
-
71
+
72
72
  // Store both successful and failed execution results
73
73
  private readonly results = new Map<string, {
74
74
  job: Job;
75
- outputs: Map<number, { status: 'success' | 'error'; data: Map<string, unknown> | string }>
75
+ outputs: Map<number, { status: 'success' | 'error' | 'skipped'; data: Map<string, unknown> | string }>
76
76
  }>()
77
77
  private graph?: DependencyGraph
78
78
 
@@ -97,7 +97,7 @@ export class Deployer {
97
97
  projectRoot: this.options.projectRoot
98
98
  }
99
99
  })
100
-
100
+
101
101
  try {
102
102
  // 1. Load all project artifacts, templates, and jobs.
103
103
  this.events.emitEvent({
@@ -107,9 +107,9 @@ export class Deployer {
107
107
  projectRoot: this.options.projectRoot
108
108
  }
109
109
  })
110
-
110
+
111
111
  await this.loader.load()
112
-
112
+
113
113
  this.events.emitEvent({
114
114
  type: 'project_loaded',
115
115
  level: 'info',
@@ -118,7 +118,7 @@ export class Deployer {
118
118
  templateCount: this.loader.templates.size
119
119
  }
120
120
  })
121
-
121
+
122
122
  // 2. Build the dependency graph and determine execution order.
123
123
  const graph = new DependencyGraph(this.loader.jobs, this.loader.templates)
124
124
  this.graph = graph
@@ -142,7 +142,7 @@ export class Deployer {
142
142
  }
143
143
  }
144
144
  const targetNetworks = this.getTargetNetworks()
145
-
145
+
146
146
  this.events.emitEvent({
147
147
  type: 'execution_plan',
148
148
  level: 'info',
@@ -163,12 +163,12 @@ export class Deployer {
163
163
  noPostCheckConditions: this.noPostCheckConditions,
164
164
  ignoreVerifyErrors: this.options.ignoreVerifyErrors ?? false
165
165
  })
166
-
166
+
167
167
  // Track if any jobs have failed
168
168
  let hasFailures = false
169
169
  // Emit signer info once per network (chainId)
170
170
  const signerInfoPrintedForChain = new Set<number>()
171
-
171
+
172
172
  for (const network of targetNetworks) {
173
173
  this.events.emitEvent({
174
174
  type: 'network_started',
@@ -180,7 +180,12 @@ export class Deployer {
180
180
  })
181
181
  for (const jobName of jobsToRun) {
182
182
  const job = this.loader.jobs.get(jobName)!
183
-
183
+
184
+ // Initialize results storage for this job if not exists
185
+ if (!this.results.has(job.name)) {
186
+ this.results.set(job.name, { job, outputs: new Map() })
187
+ }
188
+
184
189
  if (this.shouldSkipJobOnNetwork(job, network)) {
185
190
  this.events.emitEvent({
186
191
  type: 'job_skipped',
@@ -191,14 +196,16 @@ export class Deployer {
191
196
  reason: 'configuration'
192
197
  }
193
198
  })
199
+
200
+ // Store skipped result
201
+ this.results.get(job.name)!.outputs.set(network.chainId, {
202
+ status: 'skipped',
203
+ data: 'Job skipped due to network configuration'
204
+ })
205
+
194
206
  continue
195
207
  }
196
-
197
- // Initialize results storage for this job if not exists
198
- if (!this.results.has(job.name)) {
199
- this.results.set(job.name, { job, outputs: new Map() })
200
- }
201
-
208
+
202
209
  let context: ExecutionContext | undefined
203
210
  try {
204
211
  context = new ExecutionContext(
@@ -212,7 +219,7 @@ export class Deployer {
212
219
  if (typeof (context as unknown as { setJobConstants?: (constants: unknown) => void }).setJobConstants === 'function') {
213
220
  (context as unknown as { setJobConstants: (constants: unknown) => void }).setJobConstants(job.constants)
214
221
  }
215
-
222
+
216
223
  // Emit signer info once per network using the first job's context
217
224
  if (!signerInfoPrintedForChain.has(network.chainId)) {
218
225
  try {
@@ -253,11 +260,35 @@ export class Deployer {
253
260
  }
254
261
  }
255
262
 
263
+ // Check job-level skip conditions before execution
264
+ if (job.skip_condition) {
265
+ const shouldSkip = await engine.evaluateSkipConditions(job.skip_condition, context, new Map())
266
+ if (shouldSkip) {
267
+ // Store skipped result
268
+ this.results.get(job.name)!.outputs.set(network.chainId, {
269
+ status: 'skipped',
270
+ data: `Job "${job.name}" skipped due to skip condition`
271
+ })
272
+
273
+ this.events.emitEvent({
274
+ type: 'job_skipped',
275
+ level: 'warn',
276
+ data: {
277
+ jobName: job.name,
278
+ networkName: network.name,
279
+ reason: 'skip_condition'
280
+ }
281
+ })
282
+
283
+ continue // Skip to next job
284
+ }
285
+ }
286
+
256
287
  // Populate context with outputs from previously executed dependent jobs
257
288
  this.populateContextWithDependentJobOutputs(job, context, network)
258
-
289
+
259
290
  await engine.executeJob(job, context)
260
-
291
+
261
292
  // Store successful results
262
293
  this.results.get(job.name)!.outputs.set(network.chainId, {
263
294
  status: 'success',
@@ -270,7 +301,7 @@ export class Deployer {
270
301
  status: 'error',
271
302
  data: errorMessage
272
303
  })
273
-
304
+
274
305
  this.events.emitEvent({
275
306
  type: 'job_execution_failed',
276
307
  level: 'error',
@@ -281,15 +312,15 @@ export class Deployer {
281
312
  error: errorMessage
282
313
  }
283
314
  })
284
-
315
+
285
316
  // Mark that we have failures
286
317
  hasFailures = true
287
-
318
+
288
319
  // If fail-early is enabled, throw the error immediately
289
320
  if (this.options.failEarly) {
290
321
  throw error
291
322
  }
292
-
323
+
293
324
  // Otherwise, continue to next job/network
294
325
  } finally {
295
326
  // Clean up the context to prevent hanging connections
@@ -312,7 +343,7 @@ export class Deployer {
312
343
  }
313
344
  }
314
345
  }
315
-
346
+
316
347
  // 5. Write results to output files.
317
348
  await this.writeOutputFiles()
318
349
 
@@ -386,18 +417,13 @@ export class Deployer {
386
417
  const jobCount = this.results.size
387
418
  let successCount = 0
388
419
  let failedCount = 0
389
- const skippedCount = 0
390
-
391
- // Detect skipped by comparing planned jobs across networks vs executed entries
392
- // Here we approximate: an entry exists per job and per network outcome. We count
393
- // successes/errors; skips were emitted as events during execution and are not persisted
394
- // in results. We cannot perfectly reconstruct skipped count without tracking, so we
395
- // expose it as 0 for now; could be improved by tracking per-network skip events.
420
+ let skippedCount = 0
396
421
 
397
422
  for (const [, result] of this.results) {
398
423
  for (const [, netResult] of result.outputs) {
399
424
  if (netResult.status === 'success') successCount++
400
- else failedCount++
425
+ else if (netResult.status === 'skipped') skippedCount++
426
+ else if (netResult.status === 'error') failedCount++
401
427
  }
402
428
  }
403
429
 
@@ -437,7 +463,7 @@ export class Deployer {
437
463
  */
438
464
  private emitVerificationWarningsReport(engine: ExecutionEngine): void {
439
465
  const warnings = engine.getVerificationWarnings()
440
-
466
+
441
467
  if (warnings.length > 0) {
442
468
  this.events.emitEvent({
443
469
  type: 'verification_warnings_report',
@@ -530,7 +556,7 @@ export class Deployer {
530
556
  const allowed = new Set<string>([...nonDeprecatedJobs, ...requiredDeprecated])
531
557
  return fullOrder.filter(name => allowed.has(name))
532
558
  }
533
-
559
+
534
560
  // Expand patterns to concrete names
535
561
  const expandedRunJobs = expandRunJobs(this.options.runJobs)
536
562
  const explicitlyRequested = new Set<string>(expandedRunJobs)
@@ -557,7 +583,7 @@ export class Deployer {
557
583
  return this.options.runDeprecated === true
558
584
  })
559
585
  const allowedSet = new Set(filtered)
560
-
586
+
561
587
  // Filter the original execution order to only include the required jobs, preserving the correct sequence.
562
588
  return fullOrder.filter(jobName => allowedSet.has(jobName))
563
589
  }
@@ -569,10 +595,10 @@ export class Deployer {
569
595
  if (!this.options.runOnNetworks || this.options.runOnNetworks.length === 0) {
570
596
  return this.options.networks // Run on all configured networks
571
597
  }
572
-
598
+
573
599
  const targetChainIds = new Set(this.options.runOnNetworks)
574
600
  const filteredNetworks = this.options.networks.filter(n => targetChainIds.has(n.chainId))
575
-
601
+
576
602
  if (filteredNetworks.length !== this.options.runOnNetworks.length) {
577
603
  const foundIds = new Set(filteredNetworks.map(n => n.chainId))
578
604
  const missingIds = this.options.runOnNetworks.filter(id => !foundIds.has(id))
@@ -584,7 +610,7 @@ export class Deployer {
584
610
  }
585
611
  })
586
612
  }
587
-
613
+
588
614
  return filteredNetworks
589
615
  }
590
616
 
@@ -611,7 +637,7 @@ export class Deployer {
611
637
  }
612
638
  }
613
639
  }
614
-
640
+
615
641
  // Check minimal EVM hardfork requirement if present on job and network declares an EVM version
616
642
  if (jobWithNetworkFilters.min_evm_version) {
617
643
  const jobMin = this.normalizeEvmVersion(jobWithNetworkFilters.min_evm_version)
@@ -622,7 +648,7 @@ export class Deployer {
622
648
  }
623
649
  // If network has no evmVersion declared, do not skip (assume compatible)
624
650
  }
625
-
651
+
626
652
  return false // Run by default
627
653
  }
628
654
 
@@ -688,16 +714,31 @@ export class Deployer {
688
714
 
689
715
  /**
690
716
  * Populates the execution context with outputs from previously executed dependent jobs.
717
+ * Throws an error if any dependency failed.
691
718
  */
692
719
  private populateContextWithDependentJobOutputs(job: Job, context: ExecutionContext, network: Network): void {
693
720
  if (!job.depends_on) return
694
721
 
695
722
  for (const dependentJobName of job.depends_on) {
696
723
  const dependentJobResults = this.results.get(dependentJobName)
697
- if (!dependentJobResults) continue
724
+ if (!dependentJobResults) {
725
+ throw new Error(`Job "${job.name}" depends on "${dependentJobName}", but "${dependentJobName}" has not been executed yet.`)
726
+ }
698
727
 
699
728
  const networkResult = dependentJobResults.outputs.get(network.chainId)
700
- if (!networkResult || networkResult.status !== 'success') continue
729
+ if (!networkResult) {
730
+ throw new Error(`Job "${job.name}" depends on "${dependentJobName}", but "${dependentJobName}" has not been executed on network ${network.name} (chainId: ${network.chainId}).`)
731
+ }
732
+
733
+ // Skip jobs don't provide outputs, but they don't prevent dependent jobs from running
734
+ if (networkResult.status === 'skipped') {
735
+ continue
736
+ }
737
+
738
+ if (networkResult.status !== 'success') {
739
+ const errorMessage = typeof networkResult.data === 'string' ? networkResult.data : 'Unknown error'
740
+ throw new Error(`Job "${job.name}" depends on "${dependentJobName}", but "${dependentJobName}" failed: ${errorMessage}`)
741
+ }
701
742
 
702
743
  // Add outputs with job name prefixes for cross-job access
703
744
  const outputs = networkResult.data as Map<string, unknown>
@@ -726,7 +767,7 @@ export class Deployer {
726
767
 
727
768
  const outputRoot = path.join(this.options.projectRoot, 'output')
728
769
  await fs.mkdir(outputRoot, { recursive: true })
729
-
770
+
730
771
  this.events.emitEvent({
731
772
  type: 'output_writing_started',
732
773
  level: 'info'
@@ -755,7 +796,7 @@ export class Deployer {
755
796
  const outputFilePath = path.join(outputRoot, relativeJobSubpath)
756
797
  const outputFileDir = path.dirname(outputFilePath)
757
798
  await fs.mkdir(outputFileDir, { recursive: true })
758
-
799
+
759
800
  // Group networks by identical status and outputs
760
801
  const groupedResults = this.groupNetworkResults(resultData.outputs, resultData.job)
761
802
 
@@ -765,7 +806,7 @@ export class Deployer {
765
806
  lastRun: new Date().toISOString(),
766
807
  networks: groupedResults
767
808
  }
768
-
809
+
769
810
  await fs.writeFile(outputFilePath, JSON.stringify(fileContent, null, 2))
770
811
  this.events.emitEvent({
771
812
  type: 'output_file_written',
@@ -865,20 +906,20 @@ export class Deployer {
865
906
  */
866
907
  private filterOutDependencyOutputs(outputs: Map<string, unknown>, job: Job): Record<string, unknown> {
867
908
  const filtered = new Map<string, unknown>()
868
-
909
+
869
910
  // Get list of dependency job names
870
911
  const dependencyNames = job.depends_on || []
871
-
912
+
872
913
  for (const [key, value] of outputs) {
873
914
  // Check if this output key starts with any dependency job name prefix
874
915
  const isDependencyOutput = dependencyNames.some(depName => key.startsWith(`${depName}.`))
875
-
916
+
876
917
  // Only include outputs that are NOT from dependencies
877
918
  if (!isDependencyOutput) {
878
919
  filtered.set(key, value)
879
920
  }
880
921
  }
881
-
922
+
882
923
  return Object.fromEntries(filtered)
883
924
  }
884
925
 
@@ -887,8 +928,8 @@ export class Deployer {
887
928
  * - Success states with identical outputs are grouped together with chainIds array
888
929
  * - Error states are kept separate (one entry per network)
889
930
  */
890
- private groupNetworkResults(outputs: Map<number, { status: 'success' | 'error'; data: Map<string, unknown> | string }>, job: Job): Array<{
891
- status: 'success' | 'error';
931
+ private groupNetworkResults(outputs: Map<number, { status: 'success' | 'error' | 'skipped'; data: Map<string, unknown> | string }>, job: Job): Array<{
932
+ status: 'success' | 'error' | 'skipped';
892
933
  chainIds?: string[];
893
934
  chainId?: string;
894
935
  outputs?: Record<string, unknown>;
@@ -900,20 +941,20 @@ export class Deployer {
900
941
  chainId: string;
901
942
  error: string;
902
943
  }> = []
903
-
944
+
904
945
  for (const [chainId, result] of outputs.entries()) {
905
946
  if (result.status === 'success') {
906
947
  // Group successful results by identical outputs, filtered by action output flags
907
948
  const outputsObj = result.data instanceof Map ? this.filterOutputsByActionFlags(result.data, job) : {}
908
949
  const key = JSON.stringify(outputsObj)
909
-
950
+
910
951
  if (!successGroups.has(key)) {
911
952
  successGroups.set(key, {
912
953
  chainIds: [],
913
954
  outputs: outputsObj
914
955
  })
915
956
  }
916
-
957
+
917
958
  successGroups.get(key)!.chainIds.push(chainId.toString())
918
959
  } else {
919
960
  // Keep error results separate - one entry per network
@@ -924,14 +965,14 @@ export class Deployer {
924
965
  })
925
966
  }
926
967
  }
927
-
968
+
928
969
  // Convert success groups to array format
929
970
  const successEntries = Array.from(successGroups.values()).map(group => ({
930
971
  status: 'success' as const,
931
972
  chainIds: group.chainIds.sort(), // Sort for consistent output
932
973
  outputs: group.outputs
933
974
  }))
934
-
975
+
935
976
  // Return all entries: successes first, then errors
936
977
  return [...successEntries, ...errorEntries]
937
978
  }
@@ -1,3 +0,0 @@
1
- import { Network } from './types';
2
- export declare function resolveRequestedNetworks(requests: string[], networks: Network[]): number[];
3
- //# sourceMappingURL=network-match.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"network-match.d.ts","sourceRoot":"","sources":["../../src/lib/network-match.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AA8BjC,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAyC1F"}
@@ -1,62 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveRequestedNetworks = resolveRequestedNetworks;
4
- function normalize(input) {
5
- return input
6
- .toLowerCase()
7
- .replace(/[^a-z0-9]+/g, ' ')
8
- .replace(/\s+/g, ' ')
9
- .trim();
10
- }
11
- function scoreNameMatch(candidateName, query) {
12
- const c = normalize(candidateName);
13
- const q = normalize(query);
14
- if (!q || !c)
15
- return 0;
16
- if (c === q)
17
- return 100;
18
- if (c.startsWith(q))
19
- return 90;
20
- if (c.includes(q))
21
- return 75;
22
- const qTokens = q.split(' ');
23
- const allTokensPresent = qTokens.every(t => c.includes(t));
24
- if (allTokensPresent)
25
- return 70;
26
- return 0;
27
- }
28
- function resolveRequestedNetworks(requests, networks) {
29
- const chainIds = [];
30
- for (const req of requests) {
31
- const asNumber = Number(req);
32
- if (!Number.isNaN(asNumber)) {
33
- const found = networks.find(n => n.chainId === asNumber);
34
- if (found) {
35
- chainIds.push(found.chainId);
36
- continue;
37
- }
38
- }
39
- let bestScore = -1;
40
- let bestMatches = [];
41
- for (const n of networks) {
42
- const s = scoreNameMatch(n.name, req);
43
- if (s > bestScore) {
44
- bestScore = s;
45
- bestMatches = [n];
46
- }
47
- else if (s === bestScore && s > 0) {
48
- bestMatches.push(n);
49
- }
50
- }
51
- if (bestScore < 60 || bestMatches.length === 0) {
52
- throw new Error(`No network matched "${req}". Available: ${networks.map(n => `${n.name} (${n.chainId})`).join(', ')}`);
53
- }
54
- if (bestMatches.length > 1) {
55
- const candidates = bestMatches.map(n => `${n.name} (${n.chainId})`).join(', ');
56
- throw new Error(`Ambiguous network "${req}". Possible matches: ${candidates}`);
57
- }
58
- chainIds.push(bestMatches[0].chainId);
59
- }
60
- return chainIds;
61
- }
62
- //# sourceMappingURL=network-match.js.map