Skip to content

Commit 38d7df0

Browse files
committed
common: fix 'one-bad-apple' execution; allow partial batches
1 parent 6af4aa6 commit 38d7df0

File tree

1 file changed

+113
-23
lines changed

1 file changed

+113
-23
lines changed

packages/indexer-common/src/indexer-management/allocations.ts

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ export interface ReallocateTransactionParams {
7777
proof: BytesLike
7878
}
7979

80+
export interface PreparedTransaction {
81+
action: Action
82+
result: PopulateTransactionResult
83+
}
84+
8085
// An Action with resolved Allocation and Unallocation values
8186
export interface ActionStakeUsageSummary {
8287
action: Action
@@ -91,11 +96,21 @@ export type PopulateTransactionResult =
9196
| PopulatedTransaction[]
9297
| ActionFailure
9398

99+
// Result when we attempt to execute a batch
100+
export interface BatchExecutionResult {
101+
receipt: ContractReceipt | 'paused' | 'unauthorized'
102+
prepared: PreparedTransaction[] // Actions that were prepared and ready to execute
103+
failed: PreparedTransaction[] // Actions that failed during preparation
104+
}
105+
94106
export type TransactionResult =
95-
| ContractReceipt
96-
| 'paused'
97-
| 'unauthorized'
98-
| ActionFailure[]
107+
| BatchExecutionResult // We attempted execution (even if paused/unauthorized)
108+
| ActionFailure[] // All failed during preparation, nothing to execute
109+
110+
// Type guard to check if all preparations failed
111+
const isAllFailures = (result: TransactionResult): result is ActionFailure[] => {
112+
return Array.isArray(result)
113+
}
99114

100115
export class AllocationManager {
101116
constructor(
@@ -112,11 +127,48 @@ export class AllocationManager {
112127
const logger = this.logger.child({ function: 'executeBatch' })
113128
logger.trace('Executing action batch', { actions })
114129
const result = await this.executeTransactions(actions, onFinishedDeploying)
115-
if (Array.isArray(result)) {
116-
logger.trace('Execute batch transaction failed', { actionBatchResult: result })
117-
return result as ActionFailure[]
130+
131+
// Handle the case where all preparations failed
132+
if (isAllFailures(result)) {
133+
logger.trace('All transaction preparations failed', { failures: result })
134+
return result
135+
}
136+
137+
// Handle BatchExecutionResult - we have some prepared transactions
138+
const allResults: AllocationResult[] = []
139+
140+
// Add failures to results
141+
for (const failed of result.failed) {
142+
allResults.push(failed.result as ActionFailure)
143+
}
144+
145+
// Process prepared transactions if we have a valid receipt
146+
if (result.receipt !== 'paused' && result.receipt !== 'unauthorized') {
147+
const confirmedResults = await this.confirmTransactions(
148+
result.receipt,
149+
result.prepared.map((p) => p.action),
150+
)
151+
allResults.push(...confirmedResults)
152+
} else {
153+
// If paused or unauthorized, mark prepared actions as failed
154+
logger.info(`Execution skipped: ${result.receipt}`, {
155+
preparedCount: result.prepared.length,
156+
failedCount: result.failed.length,
157+
})
158+
159+
for (const prepared of result.prepared) {
160+
allResults.push({
161+
actionID: prepared.action.id,
162+
failureReason:
163+
result.receipt === 'paused'
164+
? 'Network is paused'
165+
: 'Not authorized as operator',
166+
protocolNetwork: prepared.action.protocolNetwork,
167+
} as ActionFailure)
168+
}
118169
}
119-
return await this.confirmTransactions(result, actions)
170+
171+
return allResults
120172
}
121173

122174
private async executeTransactions(
@@ -135,37 +187,72 @@ export class AllocationManager {
135187
await this.deployBeforeAllocating(logger, validatedActions)
136188
await onFinishedDeploying(validatedActions)
137189

138-
const populateTransactionsResults = await this.prepareTransactions(validatedActions)
190+
const preparedTransactions = await this.prepareTransactions(validatedActions)
191+
192+
// Separate successful and failed preparations
193+
const failedPreparations = preparedTransactions.filter((prepared) =>
194+
isActionFailure(prepared.result),
195+
)
196+
const successfulPreparations = preparedTransactions.filter(
197+
(prepared) => !isActionFailure(prepared.result),
198+
)
139199

140-
const failedTransactionPreparations = populateTransactionsResults
141-
.filter((result) => isActionFailure(result))
142-
.map((result) => result as ActionFailure)
200+
// Log the preparation results
201+
logger.info('Transaction preparation complete', {
202+
total: preparedTransactions.length,
203+
successful: successfulPreparations.length,
204+
failed: failedPreparations.length,
205+
})
143206

144-
if (failedTransactionPreparations.length > 0) {
145-
logger.trace('Failed to prepare transactions', { failedTransactionPreparations })
146-
return failedTransactionPreparations
207+
// If all failed, return early with failures
208+
if (successfulPreparations.length === 0) {
209+
logger.warn('All transaction preparations failed', {
210+
failures: failedPreparations.map((f) => ({
211+
actionId: f.action.id,
212+
reason: (f.result as ActionFailure).failureReason,
213+
})),
214+
})
215+
return failedPreparations.map((f) => f.result as ActionFailure)
147216
}
148217

149-
logger.trace('Prepared transactions ', {
150-
preparedTransactions: populateTransactionsResults,
151-
})
218+
// If some failed, log details
219+
if (failedPreparations.length > 0) {
220+
logger.warn('Some transaction preparations failed', {
221+
failures: failedPreparations.map((f) => ({
222+
actionId: f.action.id,
223+
type: f.action.type,
224+
deploymentId: f.action.deploymentID,
225+
reason: (f.result as ActionFailure).failureReason,
226+
})),
227+
})
228+
}
152229

153-
const callData = populateTransactionsResults
230+
// Build multicall only for successful preparations
231+
const callData = successfulPreparations
232+
.map((prepared) => prepared.result)
154233
.flat()
155234
.map((tx) => tx as PopulatedTransaction)
156235
.filter((tx: PopulatedTransaction) => !!tx.data)
157236
.map((tx) => tx.data as string)
158237
logger.trace('Prepared transaction calldata', { callData })
159238

160-
return await this.network.transactionManager.executeTransaction(
239+
// Execute multicall for successful preparations only
240+
const receipt = await this.network.transactionManager.executeTransaction(
161241
async () => this.network.contracts.staking.estimateGas.multicall(callData),
162242
async (gasLimit) =>
163243
this.network.contracts.staking.multicall(callData, { gasLimit }),
164244
this.logger.child({
165-
actions: `${JSON.stringify(validatedActions.map((action) => action.id))}`,
245+
actions: successfulPreparations.map((p) => p.action.id),
166246
function: 'staking.multicall',
167247
}),
168248
)
249+
250+
// Return result with both prepared and failed transactions
251+
return {
252+
receipt,
253+
prepared: successfulPreparations,
254+
failed: failedPreparations,
255+
} as BatchExecutionResult
169256
}
170257

171258
async confirmTransactions(
@@ -246,7 +333,7 @@ export class AllocationManager {
246333
}
247334
}
248335

249-
async prepareTransactions(actions: Action[]): Promise<PopulateTransactionResult[]> {
336+
async prepareTransactions(actions: Action[]): Promise<PreparedTransaction[]> {
250337
const currentEpoch = await this.network.contracts.epochManager.currentEpoch()
251338
const context: TransactionPreparationContext = {
252339
activeAllocations: await this.network.networkMonitor.allocations(
@@ -264,7 +351,10 @@ export class AllocationManager {
264351
}
265352
return await pMap(
266353
actions,
267-
async (action: Action) => await this.prepareTransaction(action, context),
354+
async (action: Action) => ({
355+
action,
356+
result: await this.prepareTransaction(action, context),
357+
}),
268358
{
269359
stopOnError: false,
270360
},

0 commit comments

Comments
 (0)