Skip to content

Commit e05cd80

Browse files
committed
feat(deploy): allow sequenced tx batching
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
1 parent 7054f73 commit e05cd80

3 files changed

Lines changed: 86 additions & 21 deletions

File tree

packages/deployment/config/arbitrumOne.json5

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
eligibilityOracle: 'RewardsEligibilityOracleA',
66
},
77

8+
RecurringAgreementManager: {
9+
// Wired to the same REO as RewardsManager. RAM stays dormant at launch
10+
// (0 issuance + RecurringCollector paused) — this only pre-configures its
11+
// eligibility oracle so it matches RM.
12+
eligibilityOracle: 'RewardsEligibilityOracleA',
13+
},
14+
815
IssuanceAllocator: {
916
// Explicit issuance allocation table, by target contract name. The rates must
1017
// sum to issuancePerBlock, which must equal RM's on-chain issuance rate — the

packages/deployment/deploy/gip/0088/eligibility_integrate.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ import {
1010
import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js'
1111
import { getResolvedSettingsForEnv } from '@graphprotocol/deployment/lib/deployment-config.js'
1212
import { assumeUpgraded, ComponentTags, GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js'
13+
import {
14+
createGovernanceTxBuilder,
15+
executeTxBatchDirect,
16+
saveGovernanceTx,
17+
} from '@graphprotocol/deployment/lib/execute-governance.js'
1318
import { requireContracts } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js'
1419
import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js'
1520
import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js'
21+
import type { TxBuilder } from '@graphprotocol/deployment/lib/tx-builder.js'
1622
import { graph } from '@graphprotocol/deployment/rocketh/deploy.js'
1723
import type { Environment } from '@rocketh/core/types'
1824
import type { PublicClient } from 'viem'
@@ -30,34 +36,41 @@ import type { PublicClient } from 'viem'
3036
async function integrateOracle(
3137
env: Environment,
3238
client: PublicClient,
39+
builder: TxBuilder,
40+
governor: string,
41+
canSign: boolean,
3342
targetLabel: string,
3443
targetEntry: RegistryEntry,
3544
oracleName: EligibilityOracleContractName | undefined,
36-
): Promise<void> {
45+
): Promise<boolean> {
3746
if (!oracleName) {
3847
env.showMessage(`\n ○ ${targetLabel}: no eligibility oracle configured — skipping\n`)
39-
return
48+
return false
4049
}
4150

4251
const reoEntry = eligibilityOracleContract(oracleName)
4352
await syncComponentsFromRegistry(env, [reoEntry, targetEntry])
4453
const [reo, target] = requireContracts(env, [reoEntry, targetEntry])
4554

46-
const { governor, canSign } = await canSignAsGovernor(env)
55+
const applyOpts = {
56+
contractName: `${targetEntry.name}-REO`,
57+
contractAddress: target.address,
58+
canExecuteDirectly: canSign,
59+
executor: governor,
60+
// Append to the shared batch; the caller executes/saves once for all targets.
61+
builder,
62+
}
4763

4864
// Sequenced-bundle generation: the target proxy isn't upgraded yet, so the
4965
// oracle getter would revert. Skip the probe/idempotency read and emit the
5066
// set-oracle TX unconditionally. The resulting bundle is sequenced-only —
5167
// execute it after the upgrade bundle (nonce order enforces this).
5268
if (assumeUpgraded()) {
53-
await applyConfiguration(env, client, [createRMIntegrationCondition(reo.address)], {
54-
contractName: `${target.name}-REO`,
55-
contractAddress: target.address,
56-
canExecuteDirectly: canSign,
57-
executor: governor,
69+
const result = await applyConfiguration(env, client, [createRMIntegrationCondition(reo.address)], {
70+
...applyOpts,
5871
assumeUndone: true,
5972
})
60-
return
73+
return result.changesNeeded
6174
}
6275

6376
// Skip only if the target isn't upgraded yet (no oracle getter). Once it
@@ -73,15 +86,11 @@ async function integrateOracle(
7386
} catch {
7487
// Function not available — target not upgraded, skip
7588
env.showMessage(`\n ○ ${targetLabel} does not support getProviderEligibilityOracle — skipping\n`)
76-
return
89+
return false
7790
}
7891

79-
await applyConfiguration(env, client, [createRMIntegrationCondition(reo.address)], {
80-
contractName: `${target.name}-REO`,
81-
contractAddress: target.address,
82-
canExecuteDirectly: canSign,
83-
executor: governor,
84-
})
92+
const result = await applyConfiguration(env, client, [createRMIntegrationCondition(reo.address)], applyOpts)
93+
return result.changesNeeded
8594
}
8695

8796
/**
@@ -103,21 +112,48 @@ export default createActionModule(
103112
async (env) => {
104113
const settings = await getResolvedSettingsForEnv(env)
105114
const client = graph.getPublicClient(env) as PublicClient
115+
const { governor, canSign } = await canSignAsGovernor(env)
116+
117+
// One shared batch for every configured target (RM and/or RAM), so both
118+
// setProviderEligibilityOracle TXs land in a single governance bundle.
119+
const builder = await createGovernanceTxBuilder(env, 'gip-0088-eligibility-integrate', {
120+
name: 'GIP-0088 Eligibility Integration',
121+
description: 'Set the provider eligibility oracle on RewardsManager and RecurringAgreementManager',
122+
})
106123

107-
await integrateOracle(
124+
const rmChanged = await integrateOracle(
108125
env,
109126
client,
127+
builder,
128+
governor,
129+
canSign,
110130
'RM',
111131
Contracts.horizon.RewardsManager,
112132
settings.rewardsManager.eligibilityOracle,
113133
)
114-
await integrateOracle(
134+
const ramChanged = await integrateOracle(
115135
env,
116136
client,
137+
builder,
138+
governor,
139+
canSign,
117140
'RAM',
118141
Contracts.issuance.RecurringAgreementManager,
119142
settings.recurringAgreementManager.eligibilityOracle,
120143
)
144+
145+
if (!rmChanged && !ramChanged) {
146+
env.showMessage('\n✅ Eligibility oracles already match config — nothing to do\n')
147+
return
148+
}
149+
150+
if (canSign) {
151+
env.showMessage('\n🔨 Executing eligibility integration batch...\n')
152+
await executeTxBatchDirect(env, builder, governor)
153+
env.showMessage('\n✅ Eligibility integration complete\n')
154+
} else {
155+
saveGovernanceTx(env, builder, 'GIP-0088 Eligibility Integration')
156+
}
121157
},
122158
{
123159
// Ordering anchor for a combined `--tags GIP-0088` run: REO-A always deploys

packages/deployment/lib/apply-configuration.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
type RoleCondition,
1919
} from './contract-checks.js'
2020
import { createGovernanceTxBuilder, executeTxBatchDirect, saveGovernanceTx } from './execute-governance.js'
21+
import type { TxBuilder } from './tx-builder.js'
2122

2223
/**
2324
* Options for applyConfiguration
@@ -44,6 +45,14 @@ export interface ApplyConfigurationOptions {
4445
* valid only after the upgrade bundle executes.
4546
*/
4647
assumeUndone?: boolean
48+
49+
/**
50+
* Optional shared TX builder. When provided, configuration TXs are appended to
51+
* it and NOT executed or saved here — the caller owns executing/saving the
52+
* combined batch once. Lets multiple applyConfiguration calls (e.g. across
53+
* several contracts) contribute to a single governance bundle.
54+
*/
55+
builder?: TxBuilder
4756
}
4857

4958
/**
@@ -87,7 +96,14 @@ export async function applyConfiguration<T>(
8796
conditions: ConfigCondition<T>[],
8897
options: ApplyConfigurationOptions,
8998
): Promise<ApplyConfigurationResult<T>> {
90-
const { contractName, contractAddress, canExecuteDirectly, executor, assumeUndone } = options
99+
const {
100+
contractName,
101+
contractAddress,
102+
canExecuteDirectly,
103+
executor,
104+
assumeUndone,
105+
builder: externalBuilder,
106+
} = options
91107

92108
// 1. Check all conditions — or, in sequenced-generation mode, skip the read and
93109
// treat every condition as un-applied (the target proxy isn't upgraded yet, so
@@ -126,7 +142,7 @@ export async function applyConfiguration<T>(
126142
// 3. Build TX batch for failing conditions
127143
env.showMessage('\n🔨 Building configuration TX batch...\n')
128144

129-
const builder = await createGovernanceTxBuilder(env, `configure-${contractName}`)
145+
const builder = externalBuilder ?? (await createGovernanceTxBuilder(env, `configure-${contractName}`))
130146

131147
const failingConditions = conditions.filter((_, i) => !status.conditions[i].ok)
132148

@@ -166,7 +182,13 @@ export async function applyConfiguration<T>(
166182
}
167183
}
168184

169-
// 4/5. Execute or save based on access
185+
// 4/5. When a shared builder was supplied, the caller owns executing/saving the
186+
// combined batch — return with the TXs appended.
187+
if (externalBuilder) {
188+
return { status, changesNeeded: true, executedDirectly: false }
189+
}
190+
191+
// Otherwise execute or save based on access.
170192
if (canExecuteDirectly && executor) {
171193
env.showMessage('\n🔨 Executing configuration TX batch...\n')
172194
await executeTxBatchDirect(env, builder, executor)

0 commit comments

Comments
 (0)