@recoillink/deploy-sdk 0.0.0-stage → 11.0.1

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 (103) hide show
  1. package/LICENSE.md +193 -0
  2. package/dist/AltVMFileSubmitter.d.ts +12 -0
  3. package/dist/AltVMFileSubmitter.d.ts.map +1 -0
  4. package/dist/AltVMFileSubmitter.js +35 -0
  5. package/dist/AltVMFileSubmitter.js.map +1 -0
  6. package/dist/AltVMFileSubmitter.test.d.ts +2 -0
  7. package/dist/AltVMFileSubmitter.test.d.ts.map +1 -0
  8. package/dist/AltVMFileSubmitter.test.js +46 -0
  9. package/dist/AltVMFileSubmitter.test.js.map +1 -0
  10. package/dist/AltVMJsonRpcSubmitter.d.ts +2 -0
  11. package/dist/AltVMJsonRpcSubmitter.d.ts.map +1 -0
  12. package/dist/AltVMJsonRpcSubmitter.js +2 -0
  13. package/dist/AltVMJsonRpcSubmitter.js.map +1 -0
  14. package/dist/core/core-artifact-reader.d.ts +67 -0
  15. package/dist/core/core-artifact-reader.d.ts.map +1 -0
  16. package/dist/core/core-artifact-reader.js +118 -0
  17. package/dist/core/core-artifact-reader.js.map +1 -0
  18. package/dist/core/core-artifact-reader.test.d.ts +2 -0
  19. package/dist/core/core-artifact-reader.test.d.ts.map +1 -0
  20. package/dist/core/core-artifact-reader.test.js +310 -0
  21. package/dist/core/core-artifact-reader.test.js.map +1 -0
  22. package/dist/core/core-writer.d.ts +43 -0
  23. package/dist/core/core-writer.d.ts.map +1 -0
  24. package/dist/core/core-writer.js +238 -0
  25. package/dist/core/core-writer.js.map +1 -0
  26. package/dist/core/core-writer.test.d.ts +2 -0
  27. package/dist/core/core-writer.test.d.ts.map +1 -0
  28. package/dist/core/core-writer.test.js +1315 -0
  29. package/dist/core/core-writer.test.js.map +1 -0
  30. package/dist/fee/fee-reader.d.ts +25 -0
  31. package/dist/fee/fee-reader.d.ts.map +1 -0
  32. package/dist/fee/fee-reader.js +35 -0
  33. package/dist/fee/fee-reader.js.map +1 -0
  34. package/dist/fee/fee-writer.d.ts +31 -0
  35. package/dist/fee/fee-writer.d.ts.map +1 -0
  36. package/dist/fee/fee-writer.js +45 -0
  37. package/dist/fee/fee-writer.js.map +1 -0
  38. package/dist/hook/hook-reader.d.ts +41 -0
  39. package/dist/hook/hook-reader.d.ts.map +1 -0
  40. package/dist/hook/hook-reader.js +52 -0
  41. package/dist/hook/hook-reader.js.map +1 -0
  42. package/dist/hook/hook-writer.d.ts +101 -0
  43. package/dist/hook/hook-writer.d.ts.map +1 -0
  44. package/dist/hook/hook-writer.js +140 -0
  45. package/dist/hook/hook-writer.js.map +1 -0
  46. package/dist/hook/hook-writer.test.d.ts +2 -0
  47. package/dist/hook/hook-writer.test.d.ts.map +1 -0
  48. package/dist/hook/hook-writer.test.js +70 -0
  49. package/dist/hook/hook-writer.test.js.map +1 -0
  50. package/dist/index.d.ts +14 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +17 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/ism/generic-ism-context.test.d.ts +2 -0
  55. package/dist/ism/generic-ism-context.test.d.ts.map +1 -0
  56. package/dist/ism/generic-ism-context.test.js +112 -0
  57. package/dist/ism/generic-ism-context.test.js.map +1 -0
  58. package/dist/ism/generic-ism-writer.d.ts +62 -0
  59. package/dist/ism/generic-ism-writer.d.ts.map +1 -0
  60. package/dist/ism/generic-ism-writer.js +100 -0
  61. package/dist/ism/generic-ism-writer.js.map +1 -0
  62. package/dist/ism/generic-ism.d.ts +43 -0
  63. package/dist/ism/generic-ism.d.ts.map +1 -0
  64. package/dist/ism/generic-ism.js +105 -0
  65. package/dist/ism/generic-ism.js.map +1 -0
  66. package/dist/ism/generic-ism.test.d.ts +2 -0
  67. package/dist/ism/generic-ism.test.d.ts.map +1 -0
  68. package/dist/ism/generic-ism.test.js +145 -0
  69. package/dist/ism/generic-ism.test.js.map +1 -0
  70. package/dist/ism/routing-ism.d.ts +21 -0
  71. package/dist/ism/routing-ism.d.ts.map +1 -0
  72. package/dist/ism/routing-ism.js +149 -0
  73. package/dist/ism/routing-ism.js.map +1 -0
  74. package/dist/protocol.d.ts +3 -0
  75. package/dist/protocol.d.ts.map +1 -0
  76. package/dist/protocol.js +38 -0
  77. package/dist/protocol.js.map +1 -0
  78. package/dist/protocol.test.d.ts +2 -0
  79. package/dist/protocol.test.d.ts.map +1 -0
  80. package/dist/protocol.test.js +14 -0
  81. package/dist/protocol.test.js.map +1 -0
  82. package/dist/utils/validation.d.ts +46 -0
  83. package/dist/utils/validation.d.ts.map +1 -0
  84. package/dist/utils/validation.js +132 -0
  85. package/dist/utils/validation.js.map +1 -0
  86. package/dist/utils/validation.test.d.ts +2 -0
  87. package/dist/utils/validation.test.d.ts.map +1 -0
  88. package/dist/utils/validation.test.js +160 -0
  89. package/dist/utils/validation.test.js.map +1 -0
  90. package/dist/warp/warp-reader.d.ts +57 -0
  91. package/dist/warp/warp-reader.d.ts.map +1 -0
  92. package/dist/warp/warp-reader.js +135 -0
  93. package/dist/warp/warp-reader.js.map +1 -0
  94. package/dist/warp/warp-writer.d.ts +67 -0
  95. package/dist/warp/warp-writer.d.ts.map +1 -0
  96. package/dist/warp/warp-writer.js +381 -0
  97. package/dist/warp/warp-writer.js.map +1 -0
  98. package/dist/warp/warp-writer.test.d.ts +2 -0
  99. package/dist/warp/warp-writer.test.d.ts.map +1 -0
  100. package/dist/warp/warp-writer.test.js +1855 -0
  101. package/dist/warp/warp-writer.test.js.map +1 -0
  102. package/package.json +54 -3
  103. package/README.md +0 -3
@@ -0,0 +1,1855 @@
1
+ import chai, { expect } from 'chai';
2
+ import chaiAsPromised from 'chai-as-promised';
3
+ import Sinon from 'sinon';
4
+ import { ArtifactState, isArtifactDeployed, isArtifactUnderived, } from '@recoillink/provider-sdk/artifact';
5
+ import { CompositeIsmNodeType, IsmType } from '@recoillink/provider-sdk/ism';
6
+ import { FeeParamsType, FeeStrategyType, FeeType, } from '@recoillink/provider-sdk/fee';
7
+ import { ProtocolType, hasProtocol, registerProtocol, } from '@recoillink/provider-sdk';
8
+ import { TokenType, } from '@recoillink/provider-sdk/warp';
9
+ import { assert } from '@recoillink/utils';
10
+ import { WarpTokenWriter } from './warp-writer.js';
11
+ chai.use(chaiAsPromised);
12
+ const TEST_CHAIN = 'test1';
13
+ const TEST_DOMAIN_ID = 1;
14
+ const REMOTE_DOMAIN_ID_1 = 1234;
15
+ const REMOTE_DOMAIN_ID_2 = 4321;
16
+ const REMOTE_DOMAIN_ID_3 = 5321;
17
+ const TOKEN_ADDRESS = '0x726f757465725f61707000000000000000000000000000010000000000000000';
18
+ const OWNER_ADDRESS = 'hyp1jq304cthpx0lwhpqzrdjrcza559ukyy3sc4dw5';
19
+ const MAILBOX_ADDRESS = '0x68797065726c616e650000000000000000000000000000000000000000000000';
20
+ const ISM_ADDRESS = '0x1234';
21
+ const HOOK_ADDRESS = '0x5678';
22
+ const FEE_ADDRESS = '0x9abc';
23
+ describe('WarpTokenWriter', () => {
24
+ let writer;
25
+ let mockArtifactManager;
26
+ let mockSigner;
27
+ let mockIsmWriter;
28
+ let mockHookWriter;
29
+ let mockChainLookup;
30
+ let readStub;
31
+ const actualConfig = {
32
+ type: TokenType.collateral,
33
+ owner: OWNER_ADDRESS,
34
+ mailbox: MAILBOX_ADDRESS,
35
+ token: 'uhyp',
36
+ remoteRouters: {
37
+ [REMOTE_DOMAIN_ID_1]: {
38
+ address: TOKEN_ADDRESS,
39
+ },
40
+ },
41
+ destinationGas: {
42
+ [REMOTE_DOMAIN_ID_1]: '200000',
43
+ },
44
+ };
45
+ const baseDeployedArtifact = {
46
+ artifactState: ArtifactState.DEPLOYED,
47
+ config: actualConfig,
48
+ deployed: { address: TOKEN_ADDRESS },
49
+ };
50
+ const chainMetadata = {
51
+ name: TEST_CHAIN,
52
+ chainId: 1,
53
+ domainId: TEST_DOMAIN_ID,
54
+ protocol: ProtocolType.Ethereum,
55
+ rpcUrls: [{ http: 'http://localhost:8545' }],
56
+ };
57
+ beforeEach(() => {
58
+ // Create mock artifact manager
59
+ mockArtifactManager = {
60
+ readWarpToken: Sinon.stub(),
61
+ createWriter: Sinon.stub(),
62
+ supportsHookUpdates: Sinon.stub().returns(true),
63
+ };
64
+ // Create minimal mock signer
65
+ mockSigner = {
66
+ getSignerAddress: () => OWNER_ADDRESS,
67
+ };
68
+ // Create mock chain lookup
69
+ mockChainLookup = {
70
+ getChainMetadata: Sinon.stub().returns({
71
+ name: TEST_CHAIN,
72
+ domainId: TEST_DOMAIN_ID,
73
+ protocol: ProtocolType.Ethereum,
74
+ }),
75
+ getChainName: Sinon.stub().returns(TEST_CHAIN),
76
+ getDomainId: Sinon.stub().returns(TEST_DOMAIN_ID),
77
+ };
78
+ // Create mock ISM and Hook writers FIRST
79
+ mockIsmWriter = {
80
+ create: Sinon.stub(),
81
+ update: Sinon.stub(),
82
+ read: Sinon.stub(),
83
+ };
84
+ mockHookWriter = {
85
+ create: Sinon.stub(),
86
+ update: Sinon.stub(),
87
+ read: Sinon.stub(),
88
+ };
89
+ // Create writer instance - manually to bypass protocol provider
90
+ writer = Object.create(WarpTokenWriter.prototype);
91
+ Object.assign(writer, {
92
+ artifactManager: mockArtifactManager,
93
+ chainMetadata,
94
+ chainLookup: mockChainLookup,
95
+ signer: mockSigner,
96
+ ismWriter: mockIsmWriter,
97
+ hookWriterFactory: () => mockHookWriter,
98
+ });
99
+ // Default read stub - returns current config
100
+ readStub = Sinon.stub(writer, 'read').resolves(baseDeployedArtifact);
101
+ });
102
+ afterEach(() => {
103
+ Sinon.restore();
104
+ });
105
+ describe('update() - Router Management', () => {
106
+ const createMockTx = (annotation) => ({
107
+ annotation,
108
+ to: TOKEN_ADDRESS,
109
+ data: '0x',
110
+ });
111
+ const routerTestCases = [
112
+ {
113
+ name: 'no updates needed if config is the same',
114
+ configOverrides: {},
115
+ expectedTxCount: 0,
116
+ },
117
+ {
118
+ name: 'new remote router',
119
+ configOverrides: {
120
+ remoteRouters: {
121
+ ...actualConfig.remoteRouters,
122
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER' },
123
+ },
124
+ destinationGas: {
125
+ ...actualConfig.destinationGas,
126
+ [REMOTE_DOMAIN_ID_2]: '300000',
127
+ },
128
+ },
129
+ expectedTxCount: 1,
130
+ assertion: (txs) => {
131
+ const [tx] = txs;
132
+ assert(tx, 'expected router update transaction');
133
+ expect(tx.annotation).to.include('router');
134
+ },
135
+ },
136
+ {
137
+ name: 'multiple new remote routers',
138
+ configOverrides: {
139
+ remoteRouters: {
140
+ ...actualConfig.remoteRouters,
141
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER1' },
142
+ [REMOTE_DOMAIN_ID_3]: { address: '0xNEWROUTER2' },
143
+ },
144
+ destinationGas: {
145
+ ...actualConfig.destinationGas,
146
+ [REMOTE_DOMAIN_ID_2]: '300000',
147
+ [REMOTE_DOMAIN_ID_3]: '400000',
148
+ },
149
+ },
150
+ expectedTxCount: 2,
151
+ assertion: (txs) => {
152
+ expect(txs).to.have.lengthOf(2);
153
+ txs.forEach((tx) => {
154
+ expect(tx.annotation).to.include('router');
155
+ });
156
+ },
157
+ },
158
+ {
159
+ name: 'remove existing remote router',
160
+ configOverrides: {
161
+ remoteRouters: {},
162
+ destinationGas: {},
163
+ },
164
+ expectedTxCount: 1,
165
+ },
166
+ {
167
+ name: 'update existing router address',
168
+ configOverrides: {
169
+ remoteRouters: {
170
+ [REMOTE_DOMAIN_ID_1]: { address: '0xUPDATEDROUTER' },
171
+ },
172
+ },
173
+ expectedTxCount: 2, // unenroll + enroll
174
+ },
175
+ {
176
+ name: 'update existing router gas',
177
+ configOverrides: {
178
+ destinationGas: {
179
+ [REMOTE_DOMAIN_ID_1]: '999999',
180
+ },
181
+ },
182
+ expectedTxCount: 2, // unenroll + enroll with new gas
183
+ },
184
+ {
185
+ name: 'remove and add remote router at the same time',
186
+ configOverrides: {
187
+ remoteRouters: {
188
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER' },
189
+ },
190
+ destinationGas: {
191
+ [REMOTE_DOMAIN_ID_2]: '300000',
192
+ },
193
+ },
194
+ expectedTxCount: 2, // remove old + add new
195
+ },
196
+ ];
197
+ routerTestCases.forEach(({ name, configOverrides, expectedTxCount, assertion }) => {
198
+ it(name, async () => {
199
+ // Setup mock writer
200
+ const mockWriter = {
201
+ read: Sinon.stub(),
202
+ create: Sinon.stub(),
203
+ update: Sinon.stub().resolves(Array(expectedTxCount)
204
+ .fill(null)
205
+ .map((_, i) => createMockTx(`Update router ${i}`))),
206
+ };
207
+ mockArtifactManager.createWriter.returns(mockWriter);
208
+ // Execute update
209
+ const artifact = {
210
+ ...baseDeployedArtifact,
211
+ config: { ...actualConfig, ...configOverrides },
212
+ };
213
+ const updateTxs = await writer.update(artifact);
214
+ // Assertions
215
+ expect(updateTxs).to.have.lengthOf(expectedTxCount);
216
+ if (assertion) {
217
+ assertion(updateTxs);
218
+ }
219
+ });
220
+ });
221
+ });
222
+ describe('update() - Ownership Changes', () => {
223
+ it('should update ownership', async () => {
224
+ const newOwner = '0x9999999999999999999999999999999999999999';
225
+ const configWithNewOwner = {
226
+ ...actualConfig,
227
+ owner: newOwner,
228
+ };
229
+ const mockWriter = {
230
+ read: Sinon.stub(),
231
+ create: Sinon.stub(),
232
+ update: Sinon.stub().resolves([
233
+ {
234
+ annotation: 'Transfer ownership',
235
+ to: TOKEN_ADDRESS,
236
+ data: '0x',
237
+ },
238
+ ]),
239
+ };
240
+ mockArtifactManager.createWriter.returns(mockWriter);
241
+ const artifact = {
242
+ ...baseDeployedArtifact,
243
+ config: configWithNewOwner,
244
+ };
245
+ const updateTxs = await writer.update(artifact);
246
+ expect(updateTxs).to.have.lengthOf(1);
247
+ const [updateTx] = updateTxs;
248
+ assert(updateTx, 'expected ownership update transaction');
249
+ expect(updateTx.annotation).to.match(/ownership/i);
250
+ });
251
+ });
252
+ describe('update() - ISM Updates', () => {
253
+ const createIsmConfig = (type, validators) => ({
254
+ type,
255
+ validators,
256
+ threshold: 1,
257
+ });
258
+ it('should deploy new ISM', async () => {
259
+ const newIsmConfig = createIsmConfig(IsmType.MESSAGE_ID_MULTISIG, [
260
+ '0xVALIDATOR',
261
+ ]);
262
+ const configWithIsm = {
263
+ ...actualConfig,
264
+ interchainSecurityModule: {
265
+ artifactState: ArtifactState.NEW,
266
+ config: newIsmConfig,
267
+ },
268
+ };
269
+ // Mock ISM creation
270
+ const deployedIsm = {
271
+ artifactState: ArtifactState.DEPLOYED,
272
+ config: newIsmConfig,
273
+ deployed: { address: ISM_ADDRESS },
274
+ };
275
+ mockIsmWriter.create.resolves([deployedIsm, []]);
276
+ const mockWriter = {
277
+ read: Sinon.stub(),
278
+ create: Sinon.stub(),
279
+ update: Sinon.stub().resolves([
280
+ {
281
+ annotation: 'Set ISM',
282
+ to: TOKEN_ADDRESS,
283
+ data: '0x',
284
+ },
285
+ ]),
286
+ };
287
+ mockArtifactManager.createWriter.returns(mockWriter);
288
+ const artifact = {
289
+ ...baseDeployedArtifact,
290
+ config: configWithIsm,
291
+ };
292
+ const updateTxs = await writer.update(artifact);
293
+ expect(mockIsmWriter.create.callCount).to.equal(1);
294
+ expect(updateTxs.length).to.be.greaterThan(0);
295
+ });
296
+ it('should update existing ISM in-place when type is unchanged', async () => {
297
+ const ismConfig = createIsmConfig(IsmType.MESSAGE_ID_MULTISIG, [
298
+ '0xVALIDATOR1',
299
+ ]);
300
+ const currentArtifactWithIsm = {
301
+ ...baseDeployedArtifact,
302
+ config: {
303
+ ...actualConfig,
304
+ interchainSecurityModule: {
305
+ artifactState: ArtifactState.DEPLOYED,
306
+ config: ismConfig,
307
+ deployed: { address: ISM_ADDRESS },
308
+ },
309
+ },
310
+ };
311
+ readStub.restore();
312
+ readStub = Sinon.stub(writer, 'read').resolves(currentArtifactWithIsm);
313
+ mockIsmWriter.update.resolves([]);
314
+ const mockWriter = {
315
+ read: Sinon.stub(),
316
+ create: Sinon.stub(),
317
+ update: Sinon.stub().resolves([]),
318
+ };
319
+ mockArtifactManager.createWriter.returns(mockWriter);
320
+ const artifact = {
321
+ ...baseDeployedArtifact,
322
+ config: {
323
+ ...actualConfig,
324
+ interchainSecurityModule: {
325
+ artifactState: ArtifactState.DEPLOYED,
326
+ config: ismConfig,
327
+ deployed: { address: ISM_ADDRESS },
328
+ },
329
+ },
330
+ };
331
+ await writer.update(artifact);
332
+ expect(mockIsmWriter.create.callCount).to.equal(0);
333
+ expect(mockIsmWriter.update.callCount).to.equal(1);
334
+ });
335
+ it('should replace existing ISM when type changes', async () => {
336
+ // Setup current artifact with existing ISM
337
+ const currentIsmConfig = createIsmConfig(IsmType.MESSAGE_ID_MULTISIG, [
338
+ '0xVALIDATOR1',
339
+ ]);
340
+ const currentArtifactWithIsm = {
341
+ ...baseDeployedArtifact,
342
+ config: {
343
+ ...actualConfig,
344
+ interchainSecurityModule: {
345
+ artifactState: ArtifactState.DEPLOYED,
346
+ config: currentIsmConfig,
347
+ deployed: { address: ISM_ADDRESS },
348
+ },
349
+ },
350
+ };
351
+ readStub.restore();
352
+ readStub = Sinon.stub(writer, 'read').resolves(currentArtifactWithIsm);
353
+ // New ISM config
354
+ const newIsmConfig = {
355
+ type: IsmType.MERKLE_ROOT_MULTISIG,
356
+ validators: ['0xVALIDATOR2'],
357
+ threshold: 1,
358
+ };
359
+ const configWithNewIsm = {
360
+ ...actualConfig,
361
+ interchainSecurityModule: {
362
+ artifactState: ArtifactState.NEW,
363
+ config: newIsmConfig,
364
+ },
365
+ };
366
+ // Mock ISM creation (new ISM type)
367
+ const newIsmAddress = '0x0000000000000000000000000000000000000004';
368
+ const deployedNewIsm = {
369
+ artifactState: ArtifactState.DEPLOYED,
370
+ config: newIsmConfig,
371
+ deployed: { address: newIsmAddress },
372
+ };
373
+ mockIsmWriter.create.resolves([deployedNewIsm, []]);
374
+ const mockWriter = {
375
+ read: Sinon.stub(),
376
+ create: Sinon.stub(),
377
+ update: Sinon.stub().resolves([
378
+ {
379
+ annotation: 'Update ISM',
380
+ to: TOKEN_ADDRESS,
381
+ data: '0x',
382
+ },
383
+ ]),
384
+ };
385
+ mockArtifactManager.createWriter.returns(mockWriter);
386
+ const artifact = {
387
+ ...baseDeployedArtifact,
388
+ config: configWithNewIsm,
389
+ };
390
+ const updateTxs = await writer.update(artifact);
391
+ expect(mockIsmWriter.create.callCount).to.equal(1);
392
+ expect(updateTxs.length).to.be.greaterThan(0);
393
+ });
394
+ it('should treat omitted ISM and zero-address ISM equivalently', async () => {
395
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
396
+ const mockWriter = {
397
+ read: Sinon.stub(),
398
+ create: Sinon.stub(),
399
+ update: Sinon.stub().resolves([]),
400
+ };
401
+ mockArtifactManager.createWriter.returns(mockWriter);
402
+ // Case 1: no ISM
403
+ const artifactNoIsm = {
404
+ ...baseDeployedArtifact,
405
+ config: { ...actualConfig, interchainSecurityModule: undefined },
406
+ };
407
+ await writer.update(artifactNoIsm);
408
+ const createCountAfterNoIsm = mockIsmWriter.create.callCount;
409
+ // Case 2: zero-address ISM (UNDERIVED — treated as pass-through)
410
+ const artifactZeroIsm = {
411
+ ...baseDeployedArtifact,
412
+ config: {
413
+ ...actualConfig,
414
+ interchainSecurityModule: {
415
+ artifactState: ArtifactState.UNDERIVED,
416
+ deployed: { address: ZERO_ADDRESS },
417
+ },
418
+ },
419
+ };
420
+ await writer.update(artifactZeroIsm);
421
+ // Neither case should trigger ISM creation
422
+ expect(mockIsmWriter.create.callCount).to.equal(createCountAfterNoIsm);
423
+ expect(mockIsmWriter.create.callCount).to.equal(0);
424
+ });
425
+ });
426
+ describe('rateLimited recipient resolution', () => {
427
+ const COMPOSITE_OWNER = OWNER_ADDRESS;
428
+ const OTHER_RECIPIENT = `0x${'9999'.padStart(64, '0')}`;
429
+ const rateLimited = (recipient) => recipient === undefined
430
+ ? {
431
+ type: CompositeIsmNodeType.RATE_LIMITED,
432
+ maxCapacity: '86400',
433
+ mailbox: MAILBOX_ADDRESS,
434
+ }
435
+ : {
436
+ type: CompositeIsmNodeType.RATE_LIMITED,
437
+ maxCapacity: '86400',
438
+ mailbox: MAILBOX_ADDRESS,
439
+ recipient,
440
+ };
441
+ // Nested one level down so the tests exercise the recursive walk rather
442
+ // than a root-only special case.
443
+ const compositeWith = (node) => ({
444
+ type: IsmType.COMPOSITE,
445
+ owner: COMPOSITE_OWNER,
446
+ root: {
447
+ type: CompositeIsmNodeType.AGGREGATION,
448
+ threshold: 1,
449
+ subIsms: [{ type: CompositeIsmNodeType.TEST, accept: true }, node],
450
+ },
451
+ });
452
+ const extractRateLimited = (config) => {
453
+ assert(config.type === IsmType.COMPOSITE, 'expected compositeIsm');
454
+ assert(config.root.type === CompositeIsmNodeType.AGGREGATION, 'expected aggregation root');
455
+ const rateLimitedIsm = config.root.subIsms[1];
456
+ assert(rateLimitedIsm, 'expected rate limited ISM');
457
+ return rateLimitedIsm;
458
+ };
459
+ const deployedCompositeIsm = (config) => ({
460
+ artifactState: ArtifactState.DEPLOYED,
461
+ config,
462
+ deployed: { address: ISM_ADDRESS },
463
+ });
464
+ const routingWith = (nestedConfig) => ({
465
+ type: IsmType.ROUTING,
466
+ owner: COMPOSITE_OWNER,
467
+ domains: {
468
+ [REMOTE_DOMAIN_ID_1]: {
469
+ artifactState: ArtifactState.NEW,
470
+ config: nestedConfig,
471
+ },
472
+ },
473
+ });
474
+ const extractRoutingDomain = (config) => {
475
+ assert(config.type === IsmType.ROUTING, 'expected domainRoutingIsm');
476
+ const domainIsm = config.domains[REMOTE_DOMAIN_ID_1];
477
+ assert(domainIsm, `expected domain ${REMOTE_DOMAIN_ID_1}`);
478
+ assert(!isArtifactUnderived(domainIsm), 'expected expanded domain ISM');
479
+ return domainIsm.config;
480
+ };
481
+ let warpWriterStub;
482
+ let sendStub;
483
+ beforeEach(() => {
484
+ sendStub = Sinon.stub().resolves({});
485
+ Object.assign(mockSigner, { sendAndConfirmTransaction: sendStub });
486
+ warpWriterStub = {
487
+ read: Sinon.stub(),
488
+ create: Sinon.stub().resolves([
489
+ {
490
+ artifactState: ArtifactState.DEPLOYED,
491
+ config: actualConfig,
492
+ deployed: { address: TOKEN_ADDRESS },
493
+ },
494
+ [],
495
+ ]),
496
+ update: Sinon.stub().resolves([]),
497
+ };
498
+ mockArtifactManager.createWriter.returns(warpWriterStub);
499
+ mockIsmWriter.update.resolves([]);
500
+ });
501
+ describe('create()', () => {
502
+ it('deploys the ISM after the router and points every recipient at it', async () => {
503
+ const ismConfig = compositeWith(rateLimited());
504
+ mockIsmWriter.create.resolves([deployedCompositeIsm(ismConfig), []]);
505
+ await writer.create({
506
+ artifactState: ArtifactState.NEW,
507
+ config: {
508
+ ...actualConfig,
509
+ interchainSecurityModule: {
510
+ artifactState: ArtifactState.NEW,
511
+ config: ismConfig,
512
+ },
513
+ },
514
+ });
515
+ expect(mockIsmWriter.create.callCount).to.equal(1);
516
+ expect(warpWriterStub.create.calledBefore(mockIsmWriter.create)).to.be
517
+ .true;
518
+ expect(extractRateLimited(mockIsmWriter.create.firstCall.args[0].config)).to.deep.equal(rateLimited(TOKEN_ADDRESS));
519
+ });
520
+ it('defers and resolves a composite nested inside a domainRoutingIsm artifact', async () => {
521
+ const routingConfig = routingWith(compositeWith(rateLimited()));
522
+ mockIsmWriter.create.resolves([
523
+ {
524
+ artifactState: ArtifactState.DEPLOYED,
525
+ config: routingConfig,
526
+ deployed: { address: ISM_ADDRESS },
527
+ },
528
+ [],
529
+ ]);
530
+ await writer.create({
531
+ artifactState: ArtifactState.NEW,
532
+ config: {
533
+ ...actualConfig,
534
+ interchainSecurityModule: {
535
+ artifactState: ArtifactState.NEW,
536
+ config: routingConfig,
537
+ },
538
+ },
539
+ });
540
+ expect(warpWriterStub.create.calledBefore(mockIsmWriter.create)).to.be
541
+ .true;
542
+ const rawConfig = warpWriterStub.create.firstCall.args[0].config;
543
+ expect(rawConfig.interchainSecurityModule).to.be.undefined;
544
+ expect(rawConfig.remoteRouters).to.deep.equal(actualConfig.remoteRouters);
545
+ const resolvedRouting = mockIsmWriter.create.firstCall.args[0].config;
546
+ const nestedComposite = extractRoutingDomain(resolvedRouting);
547
+ expect(extractRateLimited(nestedComposite)).to.deep.equal(rateLimited(TOKEN_ADDRESS));
548
+ });
549
+ it('preserves a DEPLOYED descendant of a NEW routing ISM', async () => {
550
+ const deployedDomainIsm = deployedCompositeIsm(compositeWith(rateLimited(TOKEN_ADDRESS)));
551
+ const routingConfig = {
552
+ type: IsmType.ROUTING,
553
+ owner: COMPOSITE_OWNER,
554
+ domains: { [REMOTE_DOMAIN_ID_1]: deployedDomainIsm },
555
+ };
556
+ mockIsmWriter.create.resolves([
557
+ {
558
+ artifactState: ArtifactState.DEPLOYED,
559
+ config: routingConfig,
560
+ deployed: { address: ISM_ADDRESS },
561
+ },
562
+ [],
563
+ ]);
564
+ await writer.create({
565
+ artifactState: ArtifactState.NEW,
566
+ config: {
567
+ ...actualConfig,
568
+ interchainSecurityModule: {
569
+ artifactState: ArtifactState.NEW,
570
+ config: routingConfig,
571
+ },
572
+ },
573
+ });
574
+ const resolved = mockIsmWriter.create.firstCall.args[0].config;
575
+ assert(resolved.type === IsmType.ROUTING, 'expected routing ISM');
576
+ expect(resolved.domains[REMOTE_DOMAIN_ID_1]).to.equal(deployedDomainIsm);
577
+ });
578
+ it('preserves an UNDERIVED descendant of a NEW routing ISM', async () => {
579
+ const underivedDomainIsm = {
580
+ artifactState: ArtifactState.UNDERIVED,
581
+ deployed: { address: ISM_ADDRESS },
582
+ };
583
+ const routingConfig = {
584
+ type: IsmType.ROUTING,
585
+ owner: COMPOSITE_OWNER,
586
+ domains: { [REMOTE_DOMAIN_ID_1]: underivedDomainIsm },
587
+ };
588
+ mockIsmWriter.create.resolves([
589
+ {
590
+ artifactState: ArtifactState.DEPLOYED,
591
+ config: routingConfig,
592
+ deployed: { address: ISM_ADDRESS },
593
+ },
594
+ [],
595
+ ]);
596
+ await writer.create({
597
+ artifactState: ArtifactState.NEW,
598
+ config: {
599
+ ...actualConfig,
600
+ interchainSecurityModule: {
601
+ artifactState: ArtifactState.NEW,
602
+ config: routingConfig,
603
+ },
604
+ },
605
+ });
606
+ const resolved = mockIsmWriter.create.firstCall.args[0].config;
607
+ assert(resolved.type === IsmType.ROUTING, 'expected routing ISM');
608
+ expect(resolved.domains[REMOTE_DOMAIN_ID_1]).to.equal(underivedDomainIsm);
609
+ });
610
+ it('validates a rateLimited descendant in a DEPLOYED routing ISM', async () => {
611
+ const deployedDomainIsm = deployedCompositeIsm(compositeWith(rateLimited(TOKEN_ADDRESS)));
612
+ const routingIsm = {
613
+ artifactState: ArtifactState.DEPLOYED,
614
+ config: {
615
+ type: IsmType.ROUTING,
616
+ owner: COMPOSITE_OWNER,
617
+ domains: { [REMOTE_DOMAIN_ID_1]: deployedDomainIsm },
618
+ },
619
+ deployed: { address: ISM_ADDRESS },
620
+ };
621
+ const [deployed] = await writer.create({
622
+ artifactState: ArtifactState.NEW,
623
+ config: {
624
+ ...actualConfig,
625
+ interchainSecurityModule: routingIsm,
626
+ },
627
+ });
628
+ expect(mockIsmWriter.create.callCount).to.equal(0);
629
+ expect(mockIsmWriter.update.callCount).to.equal(0);
630
+ expect(deployed.config.interchainSecurityModule).to.equal(routingIsm);
631
+ });
632
+ it('rejects a mismatched rateLimited descendant in a DEPLOYED routing ISM', async () => {
633
+ const routingIsm = {
634
+ artifactState: ArtifactState.DEPLOYED,
635
+ config: {
636
+ type: IsmType.ROUTING,
637
+ owner: COMPOSITE_OWNER,
638
+ domains: {
639
+ [REMOTE_DOMAIN_ID_1]: deployedCompositeIsm(compositeWith(rateLimited(OTHER_RECIPIENT))),
640
+ },
641
+ },
642
+ deployed: { address: ISM_ADDRESS },
643
+ };
644
+ await expect(writer.create({
645
+ artifactState: ArtifactState.NEW,
646
+ config: {
647
+ ...actualConfig,
648
+ interchainSecurityModule: routingIsm,
649
+ },
650
+ })).to.be.rejectedWith(/does not match/);
651
+ expect(warpWriterStub.create.callCount).to.equal(1);
652
+ expect(mockIsmWriter.create.callCount).to.equal(0);
653
+ });
654
+ it('withholds the NEW ISM but preserves the router config', async () => {
655
+ const ismConfig = compositeWith(rateLimited());
656
+ mockIsmWriter.create.resolves([deployedCompositeIsm(ismConfig), []]);
657
+ const [deployed] = await writer.create({
658
+ artifactState: ArtifactState.NEW,
659
+ config: {
660
+ ...actualConfig,
661
+ interchainSecurityModule: {
662
+ artifactState: ArtifactState.NEW,
663
+ config: ismConfig,
664
+ },
665
+ },
666
+ });
667
+ const rawConfig = warpWriterStub.create.firstCall.args[0].config;
668
+ expect(rawConfig.interchainSecurityModule).to.be.undefined;
669
+ expect(rawConfig.remoteRouters).to.deep.equal(actualConfig.remoteRouters);
670
+ expect(rawConfig.destinationGas).to.deep.equal(actualConfig.destinationGas);
671
+ expect(deployed.config.remoteRouters).to.deep.equal(actualConfig.remoteRouters);
672
+ expect(deployed.config.destinationGas).to.deep.equal(actualConfig.destinationGas);
673
+ expect(deployed.config.interchainSecurityModule).to.deep.equal(deployedCompositeIsm(ismConfig));
674
+ });
675
+ it('rejects a hand-written recipient before deploying anything', async () => {
676
+ const ismConfig = compositeWith(rateLimited(TOKEN_ADDRESS));
677
+ await expect(writer.create({
678
+ artifactState: ArtifactState.NEW,
679
+ config: {
680
+ ...actualConfig,
681
+ interchainSecurityModule: {
682
+ artifactState: ArtifactState.NEW,
683
+ config: ismConfig,
684
+ },
685
+ },
686
+ })).to.be.rejectedWith(/recipient/);
687
+ expect(mockIsmWriter.create.callCount).to.equal(0);
688
+ expect(warpWriterStub.create.callCount).to.equal(0);
689
+ });
690
+ it('post-deploys a composite ISM without a rateLimited node', async () => {
691
+ const ismConfig = {
692
+ type: IsmType.COMPOSITE,
693
+ owner: COMPOSITE_OWNER,
694
+ root: { type: CompositeIsmNodeType.TEST, accept: true },
695
+ };
696
+ mockIsmWriter.create.resolves([deployedCompositeIsm(ismConfig), []]);
697
+ await writer.create({
698
+ artifactState: ArtifactState.NEW,
699
+ config: {
700
+ ...actualConfig,
701
+ interchainSecurityModule: {
702
+ artifactState: ArtifactState.NEW,
703
+ config: ismConfig,
704
+ },
705
+ },
706
+ });
707
+ expect(warpWriterStub.create.calledBefore(mockIsmWriter.create)).to.be
708
+ .true;
709
+ const rawConfig = warpWriterStub.create.firstCall.args[0].config;
710
+ expect(rawConfig.interchainSecurityModule).to.be.undefined;
711
+ expect(rawConfig.remoteRouters).to.deep.equal(actualConfig.remoteRouters);
712
+ });
713
+ it('post-deploys a non-composite ISM', async () => {
714
+ const ismConfig = { type: IsmType.TEST_ISM };
715
+ mockIsmWriter.create.resolves([
716
+ {
717
+ artifactState: ArtifactState.DEPLOYED,
718
+ config: ismConfig,
719
+ deployed: { address: ISM_ADDRESS },
720
+ },
721
+ [],
722
+ ]);
723
+ await writer.create({
724
+ artifactState: ArtifactState.NEW,
725
+ config: {
726
+ ...actualConfig,
727
+ interchainSecurityModule: {
728
+ artifactState: ArtifactState.NEW,
729
+ config: ismConfig,
730
+ },
731
+ },
732
+ });
733
+ expect(warpWriterStub.create.calledBefore(mockIsmWriter.create)).to.be
734
+ .true;
735
+ const rawConfig = warpWriterStub.create.firstCall.args[0].config;
736
+ expect(rawConfig.interchainSecurityModule).to.be.undefined;
737
+ expect(rawConfig.destinationGas).to.deep.equal(actualConfig.destinationGas);
738
+ });
739
+ });
740
+ describe('update()', () => {
741
+ it('fills an unset recipient with the existing router address', async () => {
742
+ const ismConfig = compositeWith(rateLimited());
743
+ mockIsmWriter.create.resolves([deployedCompositeIsm(ismConfig), []]);
744
+ await writer.update({
745
+ ...baseDeployedArtifact,
746
+ config: {
747
+ ...actualConfig,
748
+ interchainSecurityModule: {
749
+ artifactState: ArtifactState.NEW,
750
+ config: ismConfig,
751
+ },
752
+ },
753
+ });
754
+ expect(mockIsmWriter.create.callCount).to.equal(1);
755
+ expect(extractRateLimited(mockIsmWriter.create.firstCall.args[0].config)).to.deep.equal(rateLimited(TOKEN_ADDRESS));
756
+ });
757
+ it('resolves a composite nested inside a domainRoutingIsm artifact', async () => {
758
+ const routingConfig = routingWith(compositeWith(rateLimited()));
759
+ mockIsmWriter.create.resolves([
760
+ {
761
+ artifactState: ArtifactState.DEPLOYED,
762
+ config: routingConfig,
763
+ deployed: { address: ISM_ADDRESS },
764
+ },
765
+ [],
766
+ ]);
767
+ await writer.update({
768
+ ...baseDeployedArtifact,
769
+ config: {
770
+ ...actualConfig,
771
+ interchainSecurityModule: {
772
+ artifactState: ArtifactState.NEW,
773
+ config: routingConfig,
774
+ },
775
+ },
776
+ });
777
+ const resolvedRouting = mockIsmWriter.create.firstCall.args[0].config;
778
+ const nestedComposite = extractRoutingDomain(resolvedRouting);
779
+ expect(extractRateLimited(nestedComposite)).to.deep.equal(rateLimited(TOKEN_ADDRESS));
780
+ });
781
+ it('accepts attaching a new composite ISM whose recipient names the existing router', async () => {
782
+ const ismConfig = compositeWith(rateLimited(TOKEN_ADDRESS));
783
+ mockIsmWriter.create.resolves([deployedCompositeIsm(ismConfig), []]);
784
+ await writer.update({
785
+ ...baseDeployedArtifact,
786
+ config: {
787
+ ...actualConfig,
788
+ interchainSecurityModule: {
789
+ artifactState: ArtifactState.NEW,
790
+ config: ismConfig,
791
+ },
792
+ },
793
+ });
794
+ expect(mockIsmWriter.create.callCount).to.equal(1);
795
+ expect(extractRateLimited(mockIsmWriter.create.firstCall.args[0].config)).to.deep.equal(rateLimited(TOKEN_ADDRESS));
796
+ });
797
+ it('accepts an already-deployed composite ISM whose recipient names the router', async () => {
798
+ const ismConfig = compositeWith(rateLimited(TOKEN_ADDRESS));
799
+ readStub.resolves({
800
+ ...baseDeployedArtifact,
801
+ config: {
802
+ ...actualConfig,
803
+ interchainSecurityModule: deployedCompositeIsm(ismConfig),
804
+ },
805
+ });
806
+ await writer.update({
807
+ ...baseDeployedArtifact,
808
+ config: {
809
+ ...actualConfig,
810
+ interchainSecurityModule: deployedCompositeIsm(ismConfig),
811
+ },
812
+ });
813
+ expect(mockIsmWriter.create.callCount).to.equal(0);
814
+ expect(mockIsmWriter.update.callCount).to.equal(1);
815
+ expect(extractRateLimited(mockIsmWriter.update.firstCall.args[0].config)).to.deep.equal(rateLimited(TOKEN_ADDRESS));
816
+ });
817
+ it('rejects a recipient naming a different address', async () => {
818
+ const ismConfig = compositeWith(rateLimited(OTHER_RECIPIENT));
819
+ await expect(writer.update({
820
+ ...baseDeployedArtifact,
821
+ config: {
822
+ ...actualConfig,
823
+ interchainSecurityModule: {
824
+ artifactState: ArtifactState.NEW,
825
+ config: ismConfig,
826
+ },
827
+ },
828
+ })).to.be.rejectedWith(TOKEN_ADDRESS);
829
+ expect(mockIsmWriter.create.callCount).to.equal(0);
830
+ expect(mockIsmWriter.update.callCount).to.equal(0);
831
+ });
832
+ it('leaves an ISM referenced only by address alone', async () => {
833
+ const updateTxs = await writer.update({
834
+ ...baseDeployedArtifact,
835
+ config: {
836
+ ...actualConfig,
837
+ interchainSecurityModule: {
838
+ artifactState: ArtifactState.UNDERIVED,
839
+ deployed: { address: ISM_ADDRESS },
840
+ },
841
+ },
842
+ });
843
+ expect(mockIsmWriter.create.callCount).to.equal(0);
844
+ expect(mockIsmWriter.update.callCount).to.equal(0);
845
+ expect(updateTxs).to.deep.equal([]);
846
+ });
847
+ });
848
+ });
849
+ describe('update() - Hook Updates', () => {
850
+ const merkleTreeHookConfig = {
851
+ type: 'merkleTreeHook',
852
+ };
853
+ it('should deploy new hook when none existed before', async () => {
854
+ const configWithHook = {
855
+ ...actualConfig,
856
+ hook: {
857
+ artifactState: ArtifactState.NEW,
858
+ config: merkleTreeHookConfig,
859
+ },
860
+ };
861
+ const deployedHook = {
862
+ artifactState: ArtifactState.DEPLOYED,
863
+ config: merkleTreeHookConfig,
864
+ deployed: { address: HOOK_ADDRESS },
865
+ };
866
+ mockHookWriter.create.resolves([deployedHook, []]);
867
+ const mockWriter = {
868
+ read: Sinon.stub(),
869
+ create: Sinon.stub(),
870
+ update: Sinon.stub().resolves([
871
+ { annotation: 'Set hook', to: TOKEN_ADDRESS, data: '0x' },
872
+ ]),
873
+ };
874
+ mockArtifactManager.createWriter.returns(mockWriter);
875
+ const artifact = {
876
+ ...baseDeployedArtifact,
877
+ config: configWithHook,
878
+ };
879
+ const updateTxs = await writer.update(artifact);
880
+ expect(mockHookWriter.create.callCount).to.equal(1);
881
+ expect(updateTxs.length).to.be.greaterThan(0);
882
+ });
883
+ it('should skip hook deployment when hook is underived (address reference)', async () => {
884
+ const configWithUnderivedHook = {
885
+ ...actualConfig,
886
+ hook: {
887
+ artifactState: ArtifactState.UNDERIVED,
888
+ deployed: { address: HOOK_ADDRESS },
889
+ },
890
+ };
891
+ const mockWriter = {
892
+ read: Sinon.stub(),
893
+ create: Sinon.stub(),
894
+ update: Sinon.stub().resolves([]),
895
+ };
896
+ mockArtifactManager.createWriter.returns(mockWriter);
897
+ const artifact = {
898
+ ...baseDeployedArtifact,
899
+ config: configWithUnderivedHook,
900
+ };
901
+ await writer.update(artifact);
902
+ expect(mockHookWriter.create.called).to.be.false;
903
+ expect(mockHookWriter.update.called).to.be.false;
904
+ });
905
+ it('should skip hook deployment when protocol does not support hook updates', async () => {
906
+ mockArtifactManager.supportsHookUpdates.returns(false);
907
+ const configWithHook = {
908
+ ...actualConfig,
909
+ hook: {
910
+ artifactState: ArtifactState.NEW,
911
+ config: merkleTreeHookConfig,
912
+ },
913
+ };
914
+ const mockWriter = {
915
+ read: Sinon.stub(),
916
+ create: Sinon.stub(),
917
+ update: Sinon.stub().resolves([]),
918
+ };
919
+ mockArtifactManager.createWriter.returns(mockWriter);
920
+ const artifact = {
921
+ ...baseDeployedArtifact,
922
+ config: configWithHook,
923
+ };
924
+ await writer.update(artifact);
925
+ expect(mockHookWriter.create.called).to.be.false;
926
+ expect(mockHookWriter.update.called).to.be.false;
927
+ });
928
+ it('should handle hook + router updates in single call', async () => {
929
+ const configWithHookAndRouter = {
930
+ ...actualConfig,
931
+ hook: {
932
+ artifactState: ArtifactState.NEW,
933
+ config: merkleTreeHookConfig,
934
+ },
935
+ remoteRouters: {
936
+ ...actualConfig.remoteRouters,
937
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER' },
938
+ },
939
+ destinationGas: {
940
+ ...actualConfig.destinationGas,
941
+ [REMOTE_DOMAIN_ID_2]: '300000',
942
+ },
943
+ };
944
+ const deployedHook = {
945
+ artifactState: ArtifactState.DEPLOYED,
946
+ config: merkleTreeHookConfig,
947
+ deployed: { address: HOOK_ADDRESS },
948
+ };
949
+ mockHookWriter.create.resolves([deployedHook, []]);
950
+ const mockWriter = {
951
+ read: Sinon.stub(),
952
+ create: Sinon.stub(),
953
+ update: Sinon.stub().resolves([
954
+ { annotation: 'Set hook', to: TOKEN_ADDRESS, data: '0x' },
955
+ { annotation: 'Enroll router', to: TOKEN_ADDRESS, data: '0x' },
956
+ ]),
957
+ };
958
+ mockArtifactManager.createWriter.returns(mockWriter);
959
+ const artifact = {
960
+ ...baseDeployedArtifact,
961
+ config: configWithHookAndRouter,
962
+ };
963
+ const updateTxs = await writer.update(artifact);
964
+ expect(mockHookWriter.create.callCount).to.equal(1);
965
+ expect(updateTxs.length).to.equal(2);
966
+ });
967
+ });
968
+ describe('update() - Validation', () => {
969
+ it('should reject changing token type', async () => {
970
+ // Current artifact is collateral
971
+ const currentArtifact = {
972
+ ...baseDeployedArtifact,
973
+ config: {
974
+ ...actualConfig,
975
+ type: TokenType.collateral,
976
+ },
977
+ };
978
+ readStub.restore();
979
+ readStub = Sinon.stub(writer, 'read').resolves(currentArtifact);
980
+ // Try to change to synthetic
981
+ const syntheticConfig = {
982
+ type: TokenType.synthetic,
983
+ owner: OWNER_ADDRESS,
984
+ mailbox: MAILBOX_ADDRESS,
985
+ name: 'Synthetic Token',
986
+ symbol: 'SYN',
987
+ decimals: 18,
988
+ remoteRouters: {},
989
+ destinationGas: {},
990
+ };
991
+ const artifact = {
992
+ ...baseDeployedArtifact,
993
+ config: syntheticConfig,
994
+ };
995
+ await expect(writer.update(artifact)).to.be.rejectedWith(/Cannot change warp token type/);
996
+ });
997
+ });
998
+ describe('update() - Complex Scenarios', () => {
999
+ it('should handle ISM + router updates in single call', async () => {
1000
+ const newIsmConfig = {
1001
+ type: IsmType.MESSAGE_ID_MULTISIG,
1002
+ validators: ['0xVALIDATOR'],
1003
+ threshold: 1,
1004
+ };
1005
+ const configWithIsmAndRouter = {
1006
+ ...actualConfig,
1007
+ interchainSecurityModule: {
1008
+ artifactState: ArtifactState.NEW,
1009
+ config: newIsmConfig,
1010
+ },
1011
+ remoteRouters: {
1012
+ ...actualConfig.remoteRouters,
1013
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER' },
1014
+ },
1015
+ destinationGas: {
1016
+ ...actualConfig.destinationGas,
1017
+ [REMOTE_DOMAIN_ID_2]: '300000',
1018
+ },
1019
+ };
1020
+ // Mock ISM creation
1021
+ const deployedIsm = {
1022
+ artifactState: ArtifactState.DEPLOYED,
1023
+ config: newIsmConfig,
1024
+ deployed: { address: ISM_ADDRESS },
1025
+ };
1026
+ mockIsmWriter.create.resolves([deployedIsm, []]);
1027
+ const mockWriter = {
1028
+ read: Sinon.stub(),
1029
+ create: Sinon.stub(),
1030
+ update: Sinon.stub().resolves([
1031
+ {
1032
+ annotation: 'Set ISM',
1033
+ to: TOKEN_ADDRESS,
1034
+ data: '0x',
1035
+ },
1036
+ {
1037
+ annotation: 'Enroll router',
1038
+ to: TOKEN_ADDRESS,
1039
+ data: '0x',
1040
+ },
1041
+ ]),
1042
+ };
1043
+ mockArtifactManager.createWriter.returns(mockWriter);
1044
+ const artifact = {
1045
+ ...baseDeployedArtifact,
1046
+ config: configWithIsmAndRouter,
1047
+ };
1048
+ const updateTxs = await writer.update(artifact);
1049
+ expect(mockIsmWriter.create.callCount).to.equal(1);
1050
+ expect(updateTxs.length).to.be.greaterThan(1);
1051
+ });
1052
+ it('should handle ownership + ISM + router updates', async () => {
1053
+ const newOwner = '0x9999999999999999999999999999999999999999';
1054
+ const newIsmConfig = {
1055
+ type: IsmType.MESSAGE_ID_MULTISIG,
1056
+ validators: ['0xVALIDATOR'],
1057
+ threshold: 1,
1058
+ };
1059
+ const complexConfig = {
1060
+ ...actualConfig,
1061
+ owner: newOwner,
1062
+ interchainSecurityModule: {
1063
+ artifactState: ArtifactState.NEW,
1064
+ config: newIsmConfig,
1065
+ },
1066
+ remoteRouters: {},
1067
+ destinationGas: {},
1068
+ };
1069
+ // Mock ISM creation
1070
+ const deployedIsm = {
1071
+ artifactState: ArtifactState.DEPLOYED,
1072
+ config: newIsmConfig,
1073
+ deployed: { address: ISM_ADDRESS },
1074
+ };
1075
+ mockIsmWriter.create.resolves([deployedIsm, []]);
1076
+ const mockWriter = {
1077
+ read: Sinon.stub(),
1078
+ create: Sinon.stub(),
1079
+ update: Sinon.stub().resolves([
1080
+ {
1081
+ annotation: 'Transfer ownership',
1082
+ to: TOKEN_ADDRESS,
1083
+ data: '0x',
1084
+ },
1085
+ {
1086
+ annotation: 'Set ISM',
1087
+ to: TOKEN_ADDRESS,
1088
+ data: '0x',
1089
+ },
1090
+ {
1091
+ annotation: 'Unenroll router',
1092
+ to: TOKEN_ADDRESS,
1093
+ data: '0x',
1094
+ },
1095
+ ]),
1096
+ };
1097
+ mockArtifactManager.createWriter.returns(mockWriter);
1098
+ const artifact = {
1099
+ ...baseDeployedArtifact,
1100
+ config: complexConfig,
1101
+ };
1102
+ const updateTxs = await writer.update(artifact);
1103
+ expect(mockIsmWriter.create.callCount).to.equal(1);
1104
+ expect(updateTxs.length).to.equal(3);
1105
+ });
1106
+ });
1107
+ describe('create()', () => {
1108
+ it('transfers ownership after create when no artifacts are deferred', async () => {
1109
+ const signerAddress = '0x1111111111111111111111111111111111111111';
1110
+ const finalOwner = '0x2222222222222222222222222222222222222222';
1111
+ const transferOwnerTx = {
1112
+ annotation: 'Transfer ownership',
1113
+ to: TOKEN_ADDRESS,
1114
+ data: '0x02',
1115
+ };
1116
+ const sendAndConfirmTransaction = Sinon.stub().resolves({});
1117
+ Object.assign(mockSigner, {
1118
+ getSignerAddress: () => signerAddress,
1119
+ sendAndConfirmTransaction,
1120
+ });
1121
+ const rawConfig = {
1122
+ ...actualConfig,
1123
+ owner: signerAddress,
1124
+ };
1125
+ const currentArtifact = {
1126
+ ...baseDeployedArtifact,
1127
+ config: rawConfig,
1128
+ };
1129
+ readStub.resolves(currentArtifact);
1130
+ const mockWriter = {
1131
+ read: Sinon.stub(),
1132
+ create: Sinon.stub().resolves([currentArtifact, []]),
1133
+ update: Sinon.stub().resolves([transferOwnerTx]),
1134
+ };
1135
+ mockArtifactManager.createWriter.returns(mockWriter);
1136
+ const [deployed] = await writer.create({
1137
+ artifactState: ArtifactState.NEW,
1138
+ config: { ...actualConfig, owner: finalOwner },
1139
+ });
1140
+ expect(mockWriter.create.firstCall.args[0].config.owner).to.equal(signerAddress);
1141
+ expect(mockWriter.update.firstCall.args[0].config.owner).to.equal(finalOwner);
1142
+ expect(sendAndConfirmTransaction.firstCall.args[0]).to.equal(transferOwnerTx);
1143
+ expect(deployed.config.owner).to.equal(finalOwner);
1144
+ });
1145
+ it('keeps signer ownership through ISM attachment and transfers it last', async () => {
1146
+ const signerAddress = '0x1111111111111111111111111111111111111111';
1147
+ const finalOwner = '0x2222222222222222222222222222222222222222';
1148
+ const setIsmTx = {
1149
+ annotation: 'Set ISM',
1150
+ to: TOKEN_ADDRESS,
1151
+ data: '0x01',
1152
+ };
1153
+ const transferOwnerTx = {
1154
+ annotation: 'Transfer ownership',
1155
+ to: TOKEN_ADDRESS,
1156
+ data: '0x02',
1157
+ };
1158
+ const sendAndConfirmTransaction = Sinon.stub().resolves({});
1159
+ Object.assign(mockSigner, {
1160
+ getSignerAddress: () => signerAddress,
1161
+ sendAndConfirmTransaction,
1162
+ });
1163
+ const rawConfig = {
1164
+ ...actualConfig,
1165
+ owner: signerAddress,
1166
+ };
1167
+ const currentArtifact = {
1168
+ ...baseDeployedArtifact,
1169
+ config: rawConfig,
1170
+ };
1171
+ readStub.resolves(currentArtifact);
1172
+ const mockWriter = {
1173
+ read: Sinon.stub(),
1174
+ create: Sinon.stub().resolves([currentArtifact, []]),
1175
+ update: Sinon.stub().resolves([setIsmTx, transferOwnerTx]),
1176
+ };
1177
+ mockArtifactManager.createWriter.returns(mockWriter);
1178
+ const [deployed] = await writer.create({
1179
+ artifactState: ArtifactState.NEW,
1180
+ config: {
1181
+ ...actualConfig,
1182
+ owner: finalOwner,
1183
+ interchainSecurityModule: {
1184
+ artifactState: ArtifactState.UNDERIVED,
1185
+ deployed: { address: ISM_ADDRESS },
1186
+ },
1187
+ },
1188
+ });
1189
+ expect(mockWriter.create.firstCall.args[0].config.owner).to.equal(signerAddress);
1190
+ expect(mockWriter.update.firstCall.args[0].config.owner).to.equal(finalOwner);
1191
+ expect(mockWriter.update.firstCall.args[0].config.interchainSecurityModule).to.deep.equal({
1192
+ artifactState: ArtifactState.UNDERIVED,
1193
+ deployed: { address: ISM_ADDRESS },
1194
+ });
1195
+ expect(sendAndConfirmTransaction.firstCall.args[0]).to.equal(setIsmTx);
1196
+ expect(sendAndConfirmTransaction.secondCall.args[0]).to.equal(transferOwnerTx);
1197
+ expect(deployed.config.owner).to.equal(finalOwner);
1198
+ });
1199
+ it('should create warp token without ISM', async () => {
1200
+ const mockWriter = {
1201
+ read: Sinon.stub(),
1202
+ create: Sinon.stub().resolves([
1203
+ {
1204
+ artifactState: ArtifactState.DEPLOYED,
1205
+ config: actualConfig,
1206
+ deployed: { address: TOKEN_ADDRESS },
1207
+ },
1208
+ [],
1209
+ ]),
1210
+ update: Sinon.stub(),
1211
+ };
1212
+ mockArtifactManager.createWriter.returns(mockWriter);
1213
+ const artifact = {
1214
+ artifactState: ArtifactState.NEW,
1215
+ config: actualConfig,
1216
+ };
1217
+ const [deployed, receipts] = await writer.create(artifact);
1218
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
1219
+ expect(deployed.deployed.address).to.equal(TOKEN_ADDRESS);
1220
+ expect(receipts).to.be.an('array');
1221
+ expect(mockWriter.create.callCount).to.equal(1);
1222
+ });
1223
+ it('should create warp token with new ISM', async () => {
1224
+ const newIsmConfig = {
1225
+ type: IsmType.MESSAGE_ID_MULTISIG,
1226
+ validators: ['0xVALIDATOR'],
1227
+ threshold: 1,
1228
+ };
1229
+ const configWithIsm = {
1230
+ ...actualConfig,
1231
+ interchainSecurityModule: {
1232
+ artifactState: ArtifactState.NEW,
1233
+ config: newIsmConfig,
1234
+ },
1235
+ };
1236
+ // Mock ISM creation
1237
+ const deployedIsm = {
1238
+ artifactState: ArtifactState.DEPLOYED,
1239
+ config: newIsmConfig,
1240
+ deployed: { address: ISM_ADDRESS },
1241
+ };
1242
+ mockIsmWriter.create.resolves([deployedIsm, []]);
1243
+ mockIsmWriter.update.resolves([]);
1244
+ const mockWriter = {
1245
+ read: Sinon.stub(),
1246
+ create: Sinon.stub().resolves([
1247
+ {
1248
+ artifactState: ArtifactState.DEPLOYED,
1249
+ config: actualConfig,
1250
+ deployed: { address: TOKEN_ADDRESS },
1251
+ },
1252
+ [],
1253
+ ]),
1254
+ update: Sinon.stub().resolves([]),
1255
+ };
1256
+ mockArtifactManager.createWriter.returns(mockWriter);
1257
+ const artifact = {
1258
+ artifactState: ArtifactState.NEW,
1259
+ config: configWithIsm,
1260
+ };
1261
+ const [deployed, receipts] = await writer.create(artifact);
1262
+ expect(mockIsmWriter.create.callCount).to.equal(1);
1263
+ expect(mockWriter.create.calledBefore(mockIsmWriter.create)).to.be.true;
1264
+ expect(mockWriter.create.firstCall.args[0].config.interchainSecurityModule).to.be.undefined;
1265
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
1266
+ expect(deployed.deployed.address).to.equal(TOKEN_ADDRESS);
1267
+ expect(receipts).to.be.an('array');
1268
+ });
1269
+ it('should create warp token with existing ISM', async () => {
1270
+ const existingIsmConfig = {
1271
+ type: IsmType.MESSAGE_ID_MULTISIG,
1272
+ validators: ['0xVALIDATOR'],
1273
+ threshold: 1,
1274
+ };
1275
+ const configWithExistingIsm = {
1276
+ ...actualConfig,
1277
+ interchainSecurityModule: {
1278
+ artifactState: ArtifactState.DEPLOYED,
1279
+ config: existingIsmConfig,
1280
+ deployed: { address: ISM_ADDRESS },
1281
+ },
1282
+ };
1283
+ mockIsmWriter.update.resolves([]);
1284
+ const mockWriter = {
1285
+ read: Sinon.stub(),
1286
+ create: Sinon.stub().resolves([
1287
+ {
1288
+ artifactState: ArtifactState.DEPLOYED,
1289
+ config: actualConfig,
1290
+ deployed: { address: TOKEN_ADDRESS },
1291
+ },
1292
+ [],
1293
+ ]),
1294
+ update: Sinon.stub().resolves([]),
1295
+ };
1296
+ mockArtifactManager.createWriter.returns(mockWriter);
1297
+ const artifact = {
1298
+ artifactState: ArtifactState.NEW,
1299
+ config: configWithExistingIsm,
1300
+ };
1301
+ const [deployed, receipts] = await writer.create(artifact);
1302
+ // Should not create new ISM
1303
+ expect(mockIsmWriter.create.called).to.be.false;
1304
+ expect(mockIsmWriter.update.called).to.be.false;
1305
+ expect(mockWriter.create.firstCall.args[0].config.interchainSecurityModule).to.be.undefined;
1306
+ expect(mockWriter.update.callCount).to.equal(1);
1307
+ expect(mockWriter.update.firstCall.args[0].config.interchainSecurityModule).to.deep.equal({
1308
+ artifactState: ArtifactState.UNDERIVED,
1309
+ deployed: { address: ISM_ADDRESS },
1310
+ });
1311
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
1312
+ expect(deployed.deployed.address).to.equal(TOKEN_ADDRESS);
1313
+ expect(receipts).to.be.an('array');
1314
+ });
1315
+ });
1316
+ describe('create() - Hook', () => {
1317
+ const merkleTreeHookConfig = {
1318
+ type: 'merkleTreeHook',
1319
+ };
1320
+ it('should deploy hook before warp token when hook is new', async () => {
1321
+ const configWithHook = {
1322
+ ...actualConfig,
1323
+ hook: {
1324
+ artifactState: ArtifactState.NEW,
1325
+ config: merkleTreeHookConfig,
1326
+ },
1327
+ };
1328
+ const hookReceipt = { transactionHash: '0xHOOKTX' };
1329
+ const deployedHook = {
1330
+ artifactState: ArtifactState.DEPLOYED,
1331
+ config: merkleTreeHookConfig,
1332
+ deployed: { address: HOOK_ADDRESS },
1333
+ };
1334
+ mockHookWriter.create.resolves([deployedHook, [hookReceipt]]);
1335
+ const mockWriter = {
1336
+ read: Sinon.stub(),
1337
+ create: Sinon.stub().resolves([
1338
+ {
1339
+ artifactState: ArtifactState.DEPLOYED,
1340
+ config: configWithHook,
1341
+ deployed: { address: TOKEN_ADDRESS },
1342
+ },
1343
+ [],
1344
+ ]),
1345
+ update: Sinon.stub(),
1346
+ };
1347
+ mockArtifactManager.createWriter.returns(mockWriter);
1348
+ const artifact = {
1349
+ artifactState: ArtifactState.NEW,
1350
+ config: configWithHook,
1351
+ };
1352
+ const [deployed, receipts] = await writer.create(artifact);
1353
+ expect(mockHookWriter.create.callCount).to.equal(1);
1354
+ expect(receipts).to.include(hookReceipt);
1355
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
1356
+ expect(deployed.deployed.address).to.equal(TOKEN_ADDRESS);
1357
+ });
1358
+ it('should reuse hook address when hook is already deployed', async () => {
1359
+ const configWithDeployedHook = {
1360
+ ...actualConfig,
1361
+ hook: {
1362
+ artifactState: ArtifactState.DEPLOYED,
1363
+ config: merkleTreeHookConfig,
1364
+ deployed: { address: HOOK_ADDRESS },
1365
+ },
1366
+ };
1367
+ const mockWriter = {
1368
+ read: Sinon.stub(),
1369
+ create: Sinon.stub().resolves([
1370
+ {
1371
+ artifactState: ArtifactState.DEPLOYED,
1372
+ config: configWithDeployedHook,
1373
+ deployed: { address: TOKEN_ADDRESS },
1374
+ },
1375
+ [],
1376
+ ]),
1377
+ update: Sinon.stub(),
1378
+ };
1379
+ mockArtifactManager.createWriter.returns(mockWriter);
1380
+ const artifact = {
1381
+ artifactState: ArtifactState.NEW,
1382
+ config: configWithDeployedHook,
1383
+ };
1384
+ const [deployed] = await writer.create(artifact);
1385
+ expect(mockHookWriter.create.called).to.be.false;
1386
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
1387
+ });
1388
+ it('should skip hook deployment when protocol does not support hooks', async () => {
1389
+ mockArtifactManager.supportsHookUpdates.returns(false);
1390
+ const configWithHook = {
1391
+ ...actualConfig,
1392
+ hook: {
1393
+ artifactState: ArtifactState.NEW,
1394
+ config: merkleTreeHookConfig,
1395
+ },
1396
+ };
1397
+ const mockWriter = {
1398
+ read: Sinon.stub(),
1399
+ create: Sinon.stub().resolves([
1400
+ {
1401
+ artifactState: ArtifactState.DEPLOYED,
1402
+ config: configWithHook,
1403
+ deployed: { address: TOKEN_ADDRESS },
1404
+ },
1405
+ [],
1406
+ ]),
1407
+ update: Sinon.stub(),
1408
+ };
1409
+ mockArtifactManager.createWriter.returns(mockWriter);
1410
+ const artifact = {
1411
+ artifactState: ArtifactState.NEW,
1412
+ config: configWithHook,
1413
+ };
1414
+ const [deployed] = await writer.create(artifact);
1415
+ expect(mockHookWriter.create.called).to.be.false;
1416
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
1417
+ });
1418
+ });
1419
+ describe('update() - Idempotency', () => {
1420
+ it('should return empty array when no changes needed', async () => {
1421
+ const mockWriter = {
1422
+ read: Sinon.stub(),
1423
+ create: Sinon.stub(),
1424
+ update: Sinon.stub().resolves([]),
1425
+ };
1426
+ mockArtifactManager.createWriter.returns(mockWriter);
1427
+ const artifact = {
1428
+ ...baseDeployedArtifact,
1429
+ config: actualConfig,
1430
+ };
1431
+ const updateTxs = await writer.update(artifact);
1432
+ expect(updateTxs).to.be.an('array').that.is.empty;
1433
+ });
1434
+ });
1435
+ describe('create() - Fee', () => {
1436
+ const FEE_CREATE_PROTOCOL = 'fee-create-protocol';
1437
+ const feeCreateChainMetadata = {
1438
+ name: TEST_CHAIN,
1439
+ chainId: 1,
1440
+ domainId: TEST_DOMAIN_ID,
1441
+ protocol: FEE_CREATE_PROTOCOL,
1442
+ rpcUrls: [{ http: 'http://localhost:8545' }],
1443
+ };
1444
+ let feeCreateWriter;
1445
+ let mockFeeCreateStub;
1446
+ let mockFeeUpdateStubForCreate;
1447
+ let currentFeeCreateManager;
1448
+ const linearFeeConfig = {
1449
+ type: FeeType.linear,
1450
+ owner: '0xowner',
1451
+ beneficiary: '0xbeneficiary',
1452
+ params: { type: FeeParamsType.raw, maxFee: '1000', halfAmount: '500' },
1453
+ };
1454
+ const deployedFee = {
1455
+ artifactState: ArtifactState.DEPLOYED,
1456
+ config: linearFeeConfig,
1457
+ deployed: { address: FEE_ADDRESS },
1458
+ };
1459
+ before(() => {
1460
+ if (!hasProtocol(FEE_CREATE_PROTOCOL)) {
1461
+ registerProtocol(FEE_CREATE_PROTOCOL, () => ({
1462
+ createProvider: Sinon.stub(),
1463
+ createSigner: Sinon.stub(),
1464
+ createSubmitter: Sinon.stub(),
1465
+ createIsmArtifactManager: Sinon.stub(),
1466
+ createHookArtifactManager: Sinon.stub(),
1467
+ createMailboxArtifactManager: Sinon.stub(),
1468
+ createValidatorAnnounceArtifactManager: Sinon.stub(),
1469
+ createFeeArtifactManager: () => currentFeeCreateManager,
1470
+ getMinGas: Sinon.stub(),
1471
+ createWarpArtifactManager: Sinon.stub(),
1472
+ }));
1473
+ }
1474
+ });
1475
+ beforeEach(() => {
1476
+ // Singleton stubs for the fee deployment performed by create().
1477
+ mockFeeCreateStub = Sinon.stub().resolves([deployedFee, []]);
1478
+ mockFeeUpdateStubForCreate = Sinon.stub().resolves([]);
1479
+ currentFeeCreateManager = {
1480
+ readFee: Sinon.stub().resolves(deployedFee),
1481
+ createReader: Sinon.stub(),
1482
+ createWriter: Sinon.stub().returns({
1483
+ read: Sinon.stub(),
1484
+ create: mockFeeCreateStub,
1485
+ update: mockFeeUpdateStubForCreate,
1486
+ }),
1487
+ };
1488
+ feeCreateWriter = Object.create(WarpTokenWriter.prototype);
1489
+ Object.assign(feeCreateWriter, {
1490
+ artifactManager: mockArtifactManager,
1491
+ chainMetadata: feeCreateChainMetadata,
1492
+ chainLookup: mockChainLookup,
1493
+ signer: mockSigner,
1494
+ ismWriter: mockIsmWriter,
1495
+ hookWriterFactory: () => mockHookWriter,
1496
+ });
1497
+ // this.update() inside create() reads current warp state; return a
1498
+ // warp with no fee so the diff emits exactly the SetFee attach.
1499
+ Sinon.stub(feeCreateWriter, 'read').resolves(baseDeployedArtifact);
1500
+ });
1501
+ it('should deploy warp without fee, then deploy fee and attach via update', async () => {
1502
+ const configWithFee = {
1503
+ ...actualConfig,
1504
+ fee: {
1505
+ artifactState: ArtifactState.NEW,
1506
+ config: linearFeeConfig,
1507
+ },
1508
+ };
1509
+ const createStub = Sinon.stub().resolves([
1510
+ {
1511
+ artifactState: ArtifactState.DEPLOYED,
1512
+ config: actualConfig,
1513
+ deployed: { address: TOKEN_ADDRESS },
1514
+ },
1515
+ [],
1516
+ ]);
1517
+ const updateStub = Sinon.stub().resolves([]);
1518
+ mockArtifactManager.createWriter.returns({
1519
+ read: Sinon.stub(),
1520
+ create: createStub,
1521
+ update: updateStub,
1522
+ });
1523
+ const artifact = {
1524
+ artifactState: ArtifactState.NEW,
1525
+ config: configWithFee,
1526
+ };
1527
+ const [deployed] = await feeCreateWriter.create(artifact);
1528
+ // Warp should be deployed with fee=undefined; fee is attached after.
1529
+ const rawArtifactArg = createStub.firstCall.args[0];
1530
+ expect(rawArtifactArg.config.fee).to.be.undefined;
1531
+ // Fee writer's create should run post-warp with the warp's settlement
1532
+ // asset resolved from deployed.config.
1533
+ expect(mockFeeCreateStub.calledOnce).to.be.true;
1534
+ const feeArtifactArg = mockFeeCreateStub.firstCall.args[0];
1535
+ expect(feeArtifactArg.config.token).to.equal('uhyp');
1536
+ // Attach references the deployed fee by address. It must not mutate the
1537
+ // freshly deployed fee program through feeWriter.update().
1538
+ expect(mockFeeUpdateStubForCreate.called).to.be.false;
1539
+ expect(updateStub.calledOnce).to.be.true;
1540
+ expect(updateStub.firstCall.args[0].config.fee).to.deep.equal({
1541
+ artifactState: ArtifactState.UNDERIVED,
1542
+ deployed: { address: FEE_ADDRESS },
1543
+ });
1544
+ // Returned artifact should include the deployed fee.
1545
+ expect(deployed.config.fee).to.not.be.undefined;
1546
+ });
1547
+ it('should attach existing deployed fee via update without re-creating', async () => {
1548
+ const configWithDeployedFee = {
1549
+ ...actualConfig,
1550
+ fee: {
1551
+ artifactState: ArtifactState.DEPLOYED,
1552
+ config: linearFeeConfig,
1553
+ deployed: { address: FEE_ADDRESS },
1554
+ },
1555
+ };
1556
+ const createStub = Sinon.stub().resolves([
1557
+ {
1558
+ artifactState: ArtifactState.DEPLOYED,
1559
+ config: actualConfig,
1560
+ deployed: { address: TOKEN_ADDRESS },
1561
+ },
1562
+ [],
1563
+ ]);
1564
+ mockArtifactManager.createWriter.returns({
1565
+ read: Sinon.stub(),
1566
+ create: createStub,
1567
+ update: Sinon.stub().resolves([]),
1568
+ });
1569
+ const artifact = {
1570
+ artifactState: ArtifactState.NEW,
1571
+ config: configWithDeployedFee,
1572
+ };
1573
+ const [deployed] = await feeCreateWriter.create(artifact);
1574
+ // Warp deployed without fee
1575
+ const rawArtifactArg = createStub.firstCall.args[0];
1576
+ expect(rawArtifactArg.config.fee).to.be.undefined;
1577
+ // No fresh fee deploy — user supplied a DEPLOYED artifact
1578
+ expect(mockFeeCreateStub.called).to.be.false;
1579
+ // Returned artifact passes the user-supplied DEPLOYED fee through
1580
+ const returnedFee = deployed.config.fee;
1581
+ assert(returnedFee && isArtifactDeployed(returnedFee), 'Expected DEPLOYED fee artifact in returned config');
1582
+ expect(returnedFee.deployed.address).to.equal(FEE_ADDRESS);
1583
+ });
1584
+ });
1585
+ describe('update() - Fee', () => {
1586
+ const FEE_TEST_PROTOCOL = 'fee-test-protocol';
1587
+ const feeChainMetadata = {
1588
+ name: TEST_CHAIN,
1589
+ chainId: 1,
1590
+ domainId: TEST_DOMAIN_ID,
1591
+ protocol: FEE_TEST_PROTOCOL,
1592
+ rpcUrls: [{ http: 'http://localhost:8545' }],
1593
+ };
1594
+ let feeWriter;
1595
+ let feeReadStub;
1596
+ let mockFeeUpdateStub;
1597
+ // Shared reference so the protocol factory always reads the latest mock
1598
+ let currentMockFeeArtifactManager;
1599
+ const linearFeeConfig = {
1600
+ type: FeeType.linear,
1601
+ owner: '0xowner',
1602
+ beneficiary: '0xbeneficiary',
1603
+ params: { type: FeeParamsType.raw, maxFee: '1000', halfAmount: '500' },
1604
+ };
1605
+ const deployedFee = {
1606
+ artifactState: ArtifactState.DEPLOYED,
1607
+ config: linearFeeConfig,
1608
+ deployed: { address: FEE_ADDRESS },
1609
+ };
1610
+ // Register once - factory reads currentMockFeeArtifactManager each time
1611
+ before(() => {
1612
+ if (!hasProtocol(FEE_TEST_PROTOCOL)) {
1613
+ registerProtocol(FEE_TEST_PROTOCOL, () => ({
1614
+ createProvider: Sinon.stub(),
1615
+ createSigner: Sinon.stub(),
1616
+ createSubmitter: Sinon.stub(),
1617
+ createIsmArtifactManager: Sinon.stub(),
1618
+ createHookArtifactManager: Sinon.stub(),
1619
+ createMailboxArtifactManager: Sinon.stub(),
1620
+ createValidatorAnnounceArtifactManager: Sinon.stub(),
1621
+ createFeeArtifactManager: () => currentMockFeeArtifactManager,
1622
+ getMinGas: Sinon.stub(),
1623
+ createWarpArtifactManager: Sinon.stub(),
1624
+ }));
1625
+ }
1626
+ });
1627
+ beforeEach(() => {
1628
+ mockFeeUpdateStub = Sinon.stub().resolves([]);
1629
+ const mockFeeArtifactWriter = {
1630
+ read: Sinon.stub().resolves(deployedFee),
1631
+ create: Sinon.stub().resolves([deployedFee, []]),
1632
+ update: mockFeeUpdateStub,
1633
+ };
1634
+ currentMockFeeArtifactManager = {
1635
+ readFee: Sinon.stub().resolves(deployedFee),
1636
+ createReader: Sinon.stub(),
1637
+ createWriter: Sinon.stub().returns(mockFeeArtifactWriter),
1638
+ };
1639
+ feeWriter = Object.create(WarpTokenWriter.prototype);
1640
+ Object.assign(feeWriter, {
1641
+ artifactManager: mockArtifactManager,
1642
+ chainMetadata: feeChainMetadata,
1643
+ chainLookup: mockChainLookup,
1644
+ signer: mockSigner,
1645
+ ismWriter: mockIsmWriter,
1646
+ hookWriterFactory: () => mockHookWriter,
1647
+ });
1648
+ feeReadStub = Sinon.stub(feeWriter, 'read').resolves(baseDeployedArtifact);
1649
+ });
1650
+ it('should deploy new fee when fee is NEW', async () => {
1651
+ const configWithFee = {
1652
+ ...actualConfig,
1653
+ fee: {
1654
+ artifactState: ArtifactState.NEW,
1655
+ config: linearFeeConfig,
1656
+ },
1657
+ };
1658
+ const updateStub = Sinon.stub().resolves([]);
1659
+ mockArtifactManager.createWriter.returns({
1660
+ read: Sinon.stub(),
1661
+ create: Sinon.stub(),
1662
+ update: updateStub,
1663
+ });
1664
+ const artifact = {
1665
+ ...baseDeployedArtifact,
1666
+ config: configWithFee,
1667
+ };
1668
+ await feeWriter.update(artifact);
1669
+ // Fee should have been deployed - raw artifact should have fee address
1670
+ const rawArtifactArg = updateStub.firstCall.args[0];
1671
+ expect(rawArtifactArg.config.fee).to.not.be.undefined;
1672
+ expect(rawArtifactArg.config.fee?.deployed.address).to.equal(FEE_ADDRESS);
1673
+ });
1674
+ it('should update existing routing fee in-place when DEPLOYED', async () => {
1675
+ const feeTx = {
1676
+ annotation: 'Update fee routes',
1677
+ to: FEE_ADDRESS,
1678
+ data: '0x',
1679
+ };
1680
+ mockFeeUpdateStub.resolves([feeTx]);
1681
+ const currentRoutingFee = {
1682
+ artifactState: ArtifactState.DEPLOYED,
1683
+ config: {
1684
+ type: FeeType.routing,
1685
+ owner: '0xowner',
1686
+ beneficiary: '0xbeneficiary',
1687
+ routes: {},
1688
+ },
1689
+ deployed: { address: FEE_ADDRESS },
1690
+ };
1691
+ // Current on-chain state has a deployed routing fee
1692
+ feeReadStub.resolves({
1693
+ ...baseDeployedArtifact,
1694
+ config: {
1695
+ ...actualConfig,
1696
+ fee: currentRoutingFee,
1697
+ },
1698
+ });
1699
+ const configWithFee = {
1700
+ ...actualConfig,
1701
+ fee: {
1702
+ artifactState: ArtifactState.DEPLOYED,
1703
+ config: {
1704
+ type: FeeType.routing,
1705
+ owner: '0xnewowner',
1706
+ beneficiary: '0xbeneficiary',
1707
+ routes: {
1708
+ [REMOTE_DOMAIN_ID_1]: {
1709
+ type: FeeStrategyType.linear,
1710
+ params: {
1711
+ type: FeeParamsType.raw,
1712
+ maxFee: '1000',
1713
+ halfAmount: '500',
1714
+ },
1715
+ },
1716
+ },
1717
+ },
1718
+ deployed: { address: FEE_ADDRESS },
1719
+ },
1720
+ };
1721
+ const warpUpdateStub = Sinon.stub().resolves([]);
1722
+ mockArtifactManager.createWriter.returns({
1723
+ read: Sinon.stub(),
1724
+ create: Sinon.stub(),
1725
+ update: warpUpdateStub,
1726
+ });
1727
+ const artifact = {
1728
+ ...baseDeployedArtifact,
1729
+ config: configWithFee,
1730
+ };
1731
+ const updateTxs = await feeWriter.update(artifact);
1732
+ // Fee update tx should be included
1733
+ expect(updateTxs).to.include(feeTx);
1734
+ // Fee writer should receive the warp's settlement asset on its config
1735
+ const feeArtifactArg = mockFeeUpdateStub.firstCall.args[0];
1736
+ expect(feeArtifactArg.config.token).to.equal('uhyp');
1737
+ });
1738
+ it('should pass through UNDERIVED fee without creating fee writer', async () => {
1739
+ const configWithUnderivedFee = {
1740
+ ...actualConfig,
1741
+ fee: {
1742
+ artifactState: ArtifactState.UNDERIVED,
1743
+ deployed: { address: FEE_ADDRESS },
1744
+ },
1745
+ };
1746
+ const warpUpdateStub = Sinon.stub().resolves([]);
1747
+ mockArtifactManager.createWriter.returns({
1748
+ read: Sinon.stub(),
1749
+ create: Sinon.stub(),
1750
+ update: warpUpdateStub,
1751
+ });
1752
+ const artifact = {
1753
+ ...baseDeployedArtifact,
1754
+ config: configWithUnderivedFee,
1755
+ };
1756
+ await feeWriter.update(artifact);
1757
+ // UNDERIVED fee should be passed through as-is
1758
+ const rawArtifactArg = warpUpdateStub.firstCall.args[0];
1759
+ expect(rawArtifactArg.config.fee?.deployed.address).to.equal(FEE_ADDRESS);
1760
+ });
1761
+ });
1762
+ describe('Fee - unsupported protocol', () => {
1763
+ const NO_FEE_PROTOCOL = 'no-fee-protocol';
1764
+ const noFeeChainMetadata = {
1765
+ name: TEST_CHAIN,
1766
+ chainId: 1,
1767
+ domainId: TEST_DOMAIN_ID,
1768
+ protocol: NO_FEE_PROTOCOL,
1769
+ rpcUrls: [{ http: 'http://localhost:8545' }],
1770
+ };
1771
+ let noFeeWriter;
1772
+ before(() => {
1773
+ if (!hasProtocol(NO_FEE_PROTOCOL)) {
1774
+ registerProtocol(NO_FEE_PROTOCOL, () => ({
1775
+ createProvider: Sinon.stub(),
1776
+ createSigner: Sinon.stub(),
1777
+ createSubmitter: Sinon.stub(),
1778
+ createIsmArtifactManager: Sinon.stub(),
1779
+ createHookArtifactManager: Sinon.stub(),
1780
+ createMailboxArtifactManager: Sinon.stub(),
1781
+ createValidatorAnnounceArtifactManager: Sinon.stub(),
1782
+ createFeeArtifactManager: Sinon.stub().returns(null),
1783
+ getMinGas: Sinon.stub(),
1784
+ createWarpArtifactManager: Sinon.stub(),
1785
+ }));
1786
+ }
1787
+ });
1788
+ beforeEach(() => {
1789
+ noFeeWriter = Object.create(WarpTokenWriter.prototype);
1790
+ Object.assign(noFeeWriter, {
1791
+ artifactManager: mockArtifactManager,
1792
+ chainMetadata: noFeeChainMetadata,
1793
+ chainLookup: mockChainLookup,
1794
+ signer: mockSigner,
1795
+ ismWriter: mockIsmWriter,
1796
+ hookWriterFactory: () => mockHookWriter,
1797
+ });
1798
+ Sinon.stub(noFeeWriter, 'read').resolves(baseDeployedArtifact);
1799
+ });
1800
+ const feeConfig = {
1801
+ type: FeeType.linear,
1802
+ owner: '0xowner',
1803
+ beneficiary: '0xbeneficiary',
1804
+ params: { type: FeeParamsType.raw, maxFee: '1000', halfAmount: '500' },
1805
+ };
1806
+ const configWithFee = {
1807
+ ...actualConfig,
1808
+ fee: {
1809
+ artifactState: ArtifactState.NEW,
1810
+ config: feeConfig,
1811
+ },
1812
+ };
1813
+ it('should ignore fee config on create when protocol does not support fees', async () => {
1814
+ const createStub = Sinon.stub().resolves([
1815
+ {
1816
+ artifactState: ArtifactState.DEPLOYED,
1817
+ config: actualConfig,
1818
+ deployed: { address: TOKEN_ADDRESS },
1819
+ },
1820
+ [],
1821
+ ]);
1822
+ mockArtifactManager.createWriter.returns({
1823
+ read: Sinon.stub(),
1824
+ create: createStub,
1825
+ update: Sinon.stub(),
1826
+ });
1827
+ const artifact = {
1828
+ artifactState: ArtifactState.NEW,
1829
+ config: configWithFee,
1830
+ };
1831
+ // Should not throw - fee is ignored with a warning
1832
+ const [deployed] = await noFeeWriter.create(artifact);
1833
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
1834
+ // Fee should be undefined in raw artifact since protocol doesn't support it
1835
+ const rawArtifactArg = createStub.firstCall.args[0];
1836
+ expect(rawArtifactArg.config.fee).to.be.undefined;
1837
+ });
1838
+ it('should ignore fee config on update when protocol does not support fees', async () => {
1839
+ const updateStub = Sinon.stub().resolves([]);
1840
+ mockArtifactManager.createWriter.returns({
1841
+ read: Sinon.stub(),
1842
+ create: Sinon.stub(),
1843
+ update: updateStub,
1844
+ });
1845
+ const artifact = {
1846
+ ...baseDeployedArtifact,
1847
+ config: configWithFee,
1848
+ };
1849
+ // Should not throw - fee is ignored with a warning
1850
+ const updateTxs = await noFeeWriter.update(artifact);
1851
+ expect(updateTxs).to.be.an('array');
1852
+ });
1853
+ });
1854
+ });
1855
+ //# sourceMappingURL=warp-writer.test.js.map