Skip to content

Commit b864cca

Browse files
authored
feat(provider-sdk, deploy-sdk): initial fee support on multi-vm (#8627)
1 parent 1f918d0 commit b864cca

20 files changed

Lines changed: 1972 additions & 2 deletions

File tree

.changeset/fee-type-support.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@hyperlane-xyz/provider-sdk": minor
3+
"@hyperlane-xyz/deploy-sdk": minor
4+
"@hyperlane-xyz/sealevel-sdk": patch
5+
"@hyperlane-xyz/cosmos-sdk": patch
6+
"@hyperlane-xyz/radix-sdk": patch
7+
"@hyperlane-xyz/starknet-sdk": patch
8+
"@hyperlane-xyz/aleo-sdk": patch
9+
"@hyperlane-xyz/tron-sdk": patch
10+
---
11+
12+
Multi-VM fee type support was added to provider-sdk and deploy-sdk. Fee types (linear, regressive, progressive, offchainQuotedLinear, routing, crossCollateralRouting) were defined with Config API and Artifact API variants. FeeReader and FeeWriter with required FeeReadContext were added to deploy-sdk. Fee was integrated into warp types and the warp writer update flow. All protocol providers received createFeeArtifactManager stubs.

typescript/aleo-sdk/src/clients/protocol.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
type TxReceipt,
2020
} from '@hyperlane-xyz/provider-sdk/module';
2121
import { type IRawWarpArtifactManager } from '@hyperlane-xyz/provider-sdk/warp';
22+
import { type IRawFeeArtifactManager } from '@hyperlane-xyz/provider-sdk/fee';
2223
import { type IRawValidatorAnnounceArtifactManager } from '@hyperlane-xyz/provider-sdk/validator-announce';
2324
import { assert } from '@hyperlane-xyz/utils';
2425

@@ -190,6 +191,12 @@ export class AleoProtocolProvider implements ProtocolProvider {
190191
);
191192
}
192193

194+
createFeeArtifactManager(
195+
_chainMetadata: ChainMetadataForAltVM,
196+
): IRawFeeArtifactManager | null {
197+
return null;
198+
}
199+
193200
getMinGas(): MinimumRequiredGasByAction {
194201
return {
195202
CORE_DEPLOY_GAS: 0n,

typescript/cosmos-sdk/src/clients/protocol.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
type TxReceipt,
1717
} from '@hyperlane-xyz/provider-sdk/module';
1818
import { type IRawWarpArtifactManager } from '@hyperlane-xyz/provider-sdk/warp';
19+
import { type IRawFeeArtifactManager } from '@hyperlane-xyz/provider-sdk/fee';
1920
import { type IRawValidatorAnnounceArtifactManager } from '@hyperlane-xyz/provider-sdk/validator-announce';
2021
import { assert } from '@hyperlane-xyz/utils';
2122

@@ -118,6 +119,12 @@ export class CosmosNativeProtocolProvider implements ProtocolProvider {
118119
return null;
119120
}
120121

122+
createFeeArtifactManager(
123+
_chainMetadata: ChainMetadataForAltVM,
124+
): IRawFeeArtifactManager | null {
125+
return null;
126+
}
127+
121128
getMinGas(): MinimumRequiredGasByAction {
122129
return {
123130
CORE_DEPLOY_GAS: BigInt(1e6),

typescript/deploy-sdk/src/core/core-artifact-reader.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const mockProtocolProvider: ProtocolProvider = {
3333
createHookArtifactManager: sinon.stub(),
3434
createMailboxArtifactManager: sinon.stub(),
3535
createValidatorAnnounceArtifactManager: sinon.stub(),
36+
createFeeArtifactManager: sinon.stub(),
3637
getMinGas: sinon.stub(),
3738
createWarpArtifactManager: sinon.stub(),
3839
};

typescript/deploy-sdk/src/core/core-writer.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ describe('CoreWriter', () => {
152152
createHookArtifactManager: sinon.stub().returns(mockHookArtifactManager),
153153
createMailboxArtifactManager: sinon.stub(),
154154
createValidatorAnnounceArtifactManager: sinon.stub(),
155+
createFeeArtifactManager: sinon.stub(),
155156
getMinGas: sinon.stub(),
156157
createWarpArtifactManager: sinon.stub(),
157158
} satisfies ProtocolProvider;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {
2+
ChainMetadataForAltVM,
3+
getProtocolProvider,
4+
} from '@hyperlane-xyz/provider-sdk';
5+
import { ArtifactReader } from '@hyperlane-xyz/provider-sdk/artifact';
6+
import {
7+
DeployedFeeAddress,
8+
DeployedFeeArtifact,
9+
FeeArtifactConfig,
10+
FeeReadContext,
11+
IRawFeeArtifactManager,
12+
} from '@hyperlane-xyz/provider-sdk/fee';
13+
14+
/**
15+
* Factory function to create a FeeReader instance.
16+
* Returns null if the protocol does not support fee programs.
17+
*
18+
* @param chainMetadata Chain metadata for the target chain
19+
* @param context Required fee read context with domains and routers to check
20+
* @returns A FeeReader instance, or null if the protocol does not support fees
21+
*/
22+
export function createFeeReader(
23+
chainMetadata: ChainMetadataForAltVM,
24+
context: FeeReadContext,
25+
): FeeReader | null {
26+
const protocolProvider = getProtocolProvider(chainMetadata.protocol);
27+
const artifactManager: IRawFeeArtifactManager | null =
28+
protocolProvider.createFeeArtifactManager(chainMetadata);
29+
30+
if (!artifactManager) {
31+
return null;
32+
}
33+
34+
return new FeeReader(artifactManager, context);
35+
}
36+
37+
/**
38+
* Generic Fee Reader that reads fee configurations from on-chain state.
39+
*
40+
* The FeeReadContext is required at construction time to ensure the reader always
41+
* knows which domains and routers to check (some fee contracts are not enumerable).
42+
*/
43+
export class FeeReader implements ArtifactReader<
44+
FeeArtifactConfig,
45+
DeployedFeeAddress
46+
> {
47+
constructor(
48+
protected readonly artifactManager: IRawFeeArtifactManager,
49+
protected readonly context: FeeReadContext,
50+
) {}
51+
52+
async read(address: string): Promise<DeployedFeeArtifact> {
53+
return this.artifactManager.readFee(address, this.context);
54+
}
55+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
ChainMetadataForAltVM,
3+
getProtocolProvider,
4+
} from '@hyperlane-xyz/provider-sdk';
5+
import { ISigner } from '@hyperlane-xyz/provider-sdk/altvm';
6+
import {
7+
ArtifactNew,
8+
ArtifactWriter,
9+
} from '@hyperlane-xyz/provider-sdk/artifact';
10+
import {
11+
DeployedFeeAddress,
12+
DeployedFeeArtifact,
13+
FeeArtifactConfig,
14+
FeeReadContext,
15+
IRawFeeArtifactManager,
16+
} from '@hyperlane-xyz/provider-sdk/fee';
17+
import { AnnotatedTx, TxReceipt } from '@hyperlane-xyz/provider-sdk/module';
18+
19+
import { FeeReader } from './fee-reader.js';
20+
21+
/**
22+
* Factory function to create a FeeWriter instance.
23+
* Returns null if the protocol does not support fee programs.
24+
*
25+
* @param chainMetadata Chain metadata for the target chain
26+
* @param signer Signer interface for signing transactions
27+
* @param context Required fee read context with domains and routers to check
28+
* @returns A FeeWriter instance, or null if the protocol does not support fees
29+
*/
30+
export function createFeeWriter(
31+
chainMetadata: ChainMetadataForAltVM,
32+
signer: ISigner<AnnotatedTx, TxReceipt>,
33+
context: FeeReadContext,
34+
): FeeWriter | null {
35+
const protocolProvider = getProtocolProvider(chainMetadata.protocol);
36+
const artifactManager: IRawFeeArtifactManager | null =
37+
protocolProvider.createFeeArtifactManager(chainMetadata);
38+
39+
if (!artifactManager) {
40+
return null;
41+
}
42+
43+
return new FeeWriter(artifactManager, context, signer);
44+
}
45+
46+
/**
47+
* FeeWriter handles creation and updates of fee configurations using the Artifact API.
48+
* It delegates to protocol-specific artifact writers for individual fee types.
49+
*
50+
* Extends FeeReader to inherit read() functionality.
51+
* The FeeReadContext is required at construction time to ensure the reader always
52+
* knows which domains and routers to check (some fee contracts are not enumerable).
53+
*/
54+
export class FeeWriter
55+
extends FeeReader
56+
implements ArtifactWriter<FeeArtifactConfig, DeployedFeeAddress>
57+
{
58+
constructor(
59+
artifactManager: IRawFeeArtifactManager,
60+
context: FeeReadContext,
61+
private readonly signer: ISigner<AnnotatedTx, TxReceipt>,
62+
) {
63+
super(artifactManager, context);
64+
}
65+
66+
async create(
67+
artifact: ArtifactNew<FeeArtifactConfig>,
68+
): Promise<[DeployedFeeArtifact, TxReceipt[]]> {
69+
const { config } = artifact;
70+
const writer = this.artifactManager.createWriter(config.type, this.signer);
71+
return writer.create(artifact);
72+
}
73+
74+
async update(artifact: DeployedFeeArtifact): Promise<AnnotatedTx[]> {
75+
const { config } = artifact;
76+
const writer = this.artifactManager.createWriter(config.type, this.signer);
77+
return writer.update(artifact);
78+
}
79+
}

typescript/deploy-sdk/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// import { AltVMFileSubmitter } from '@hyperlane-xyz/deploy-sdk/AltVMFileSubmitter';
44

55
export { AltVMJsonRpcSubmitter } from './AltVMJsonRpcSubmitter.js';
6+
export { createFeeReader, FeeReader } from './fee/fee-reader.js';
7+
export { createFeeWriter, FeeWriter } from './fee/fee-writer.js';
68
export {
79
CoreArtifactReader,
810
createCoreReader,

typescript/deploy-sdk/src/warp/warp-reader.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@ import {
22
ChainMetadataForAltVM,
33
getProtocolProvider,
44
} from '@hyperlane-xyz/provider-sdk';
5+
import { assert } from '@hyperlane-xyz/utils';
56
import {
67
Artifact,
78
ArtifactReader,
89
isArtifactDeployed,
910
isArtifactUnderived,
1011
} from '@hyperlane-xyz/provider-sdk/artifact';
1112
import { ChainLookup } from '@hyperlane-xyz/provider-sdk/chain';
13+
import {
14+
DeployedFeeAddress,
15+
DeployedFeeArtifact,
16+
FeeArtifactConfig,
17+
} from '@hyperlane-xyz/provider-sdk/fee';
1218
import {
1319
DeployedHookAddress,
1420
DeployedHookArtifact,
@@ -25,9 +31,11 @@ import {
2531
DerivedWarpConfig,
2632
IRawWarpArtifactManager,
2733
WarpArtifactConfig,
34+
buildFeeReadContextFromWarpArtifactConfig,
2835
warpArtifactToDerivedConfig,
2936
} from '@hyperlane-xyz/provider-sdk/warp';
3037

38+
import { createFeeReader } from '../fee/fee-reader.js';
3139
import { HookReader, createHookReader } from '../hook/hook-reader.js';
3240
import { IsmReader, createIsmReader } from '../ism/generic-ism.js';
3341

@@ -69,12 +77,19 @@ export class WarpTokenReader implements ArtifactReader<
6977
rawArtifact.config.hook,
7078
);
7179

80+
// Expand nested Fee artifact if present
81+
const expandedFeeArtifact = await this.expandFeeArtifact(
82+
rawArtifact.config,
83+
rawArtifact.config.fee,
84+
);
85+
7286
return {
7387
...rawArtifact,
7488
config: {
7589
...rawArtifact.config,
7690
interchainSecurityModule: expandedIsmArtifact,
7791
hook: expandedHookArtifact,
92+
fee: expandedFeeArtifact,
7893
},
7994
};
8095
}
@@ -134,6 +149,40 @@ export class WarpTokenReader implements ArtifactReader<
134149
);
135150
}
136151

152+
/**
153+
* Expands a Fee artifact by reading it if underived.
154+
* Builds FeeReadContext from the warp config's remote routers and CC routers.
155+
* Returns undefined only when no feeArtifact is provided. Deployed artifacts
156+
* are returned as-is. Underived artifacts are read via the fee reader.
157+
* Throws if a fee artifact exists but the protocol has no fee artifact manager.
158+
*/
159+
private async expandFeeArtifact(
160+
warpConfig: WarpArtifactConfig,
161+
feeArtifact?: Artifact<FeeArtifactConfig, DeployedFeeAddress>,
162+
): Promise<DeployedFeeArtifact | undefined> {
163+
if (!feeArtifact) {
164+
return undefined;
165+
}
166+
167+
if (isArtifactDeployed(feeArtifact)) {
168+
return feeArtifact;
169+
}
170+
171+
if (isArtifactUnderived(feeArtifact)) {
172+
const context = buildFeeReadContextFromWarpArtifactConfig(warpConfig);
173+
const feeReader = createFeeReader(this.chainMetadata, context);
174+
assert(
175+
feeReader,
176+
`Fee artifact present on warp config but protocol ${this.chainMetadata.protocol} has no fee artifact manager`,
177+
);
178+
return feeReader.read(feeArtifact.deployed.address);
179+
}
180+
181+
throw new Error(
182+
`Unexpected Fee artifact state 'new' when reading warp token Fee configuration`,
183+
);
184+
}
185+
137186
/**
138187
* Backward compatibility method that converts DeployedWarpArtifact to DerivedWarpConfig.
139188
* This allows WarpTokenReader to be used as a drop-in replacement for the old AltVMWarpRouteReader.

0 commit comments

Comments
 (0)