@hyperlane-xyz/deploy-sdk 1.4.0 → 2.0.0

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 (44) hide show
  1. package/dist/AltVMCoreModule.d.ts.map +1 -1
  2. package/dist/AltVMCoreModule.js +1 -2
  3. package/dist/AltVMCoreModule.js.map +1 -1
  4. package/dist/index.d.ts +2 -5
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -5
  7. package/dist/index.js.map +1 -1
  8. package/dist/warp/warp-reader.d.ts +51 -0
  9. package/dist/warp/warp-reader.d.ts.map +1 -0
  10. package/dist/warp/warp-reader.js +106 -0
  11. package/dist/warp/warp-reader.js.map +1 -0
  12. package/dist/warp/warp-writer.d.ts +66 -0
  13. package/dist/warp/warp-writer.d.ts.map +1 -0
  14. package/dist/warp/warp-writer.js +231 -0
  15. package/dist/warp/warp-writer.js.map +1 -0
  16. package/dist/warp/warp-writer.test.d.ts +2 -0
  17. package/dist/warp/warp-writer.test.d.ts.map +1 -0
  18. package/dist/warp/warp-writer.test.js +903 -0
  19. package/dist/warp/warp-writer.test.js.map +1 -0
  20. package/package.json +9 -9
  21. package/dist/AltVMWarpDeployer.d.ts +0 -14
  22. package/dist/AltVMWarpDeployer.d.ts.map +0 -1
  23. package/dist/AltVMWarpDeployer.js +0 -69
  24. package/dist/AltVMWarpDeployer.js.map +0 -1
  25. package/dist/AltVMWarpModule.d.ts +0 -96
  26. package/dist/AltVMWarpModule.d.ts.map +0 -1
  27. package/dist/AltVMWarpModule.js +0 -416
  28. package/dist/AltVMWarpModule.js.map +0 -1
  29. package/dist/AltVMWarpModule.test.d.ts +0 -2
  30. package/dist/AltVMWarpModule.test.d.ts.map +0 -1
  31. package/dist/AltVMWarpModule.test.js +0 -508
  32. package/dist/AltVMWarpModule.test.js.map +0 -1
  33. package/dist/AltVMWarpRouteReader.d.ts +0 -53
  34. package/dist/AltVMWarpRouteReader.d.ts.map +0 -1
  35. package/dist/AltVMWarpRouteReader.js +0 -168
  36. package/dist/AltVMWarpRouteReader.js.map +0 -1
  37. package/dist/ism/ism-config-utils.d.ts +0 -37
  38. package/dist/ism/ism-config-utils.d.ts.map +0 -1
  39. package/dist/ism/ism-config-utils.js +0 -72
  40. package/dist/ism/ism-config-utils.js.map +0 -1
  41. package/dist/warp-module.d.ts +0 -5
  42. package/dist/warp-module.d.ts.map +0 -1
  43. package/dist/warp-module.js +0 -28
  44. package/dist/warp-module.js.map +0 -1
@@ -0,0 +1,903 @@
1
+ import chai, { expect } from 'chai';
2
+ import chaiAsPromised from 'chai-as-promised';
3
+ import Sinon from 'sinon';
4
+ import { ArtifactState } from '@hyperlane-xyz/provider-sdk/artifact';
5
+ import { ProtocolType } from '@hyperlane-xyz/provider-sdk/protocol';
6
+ import { TokenType, } from '@hyperlane-xyz/provider-sdk/warp';
7
+ import { WarpTokenWriter } from './warp-writer.js';
8
+ chai.use(chaiAsPromised);
9
+ const TEST_CHAIN = 'test1';
10
+ const TEST_DOMAIN_ID = 1;
11
+ const REMOTE_DOMAIN_ID_1 = 1234;
12
+ const REMOTE_DOMAIN_ID_2 = 4321;
13
+ const REMOTE_DOMAIN_ID_3 = 5321;
14
+ const TOKEN_ADDRESS = '0x726f757465725f61707000000000000000000000000000010000000000000000';
15
+ const OWNER_ADDRESS = 'hyp1jq304cthpx0lwhpqzrdjrcza559ukyy3sc4dw5';
16
+ const MAILBOX_ADDRESS = '0x68797065726c616e650000000000000000000000000000000000000000000000';
17
+ const ISM_ADDRESS = '0x1234';
18
+ const HOOK_ADDRESS = '0x5678';
19
+ describe('WarpTokenWriter', () => {
20
+ let writer;
21
+ let mockArtifactManager;
22
+ let mockSigner;
23
+ let mockIsmWriter;
24
+ let mockHookWriter;
25
+ let mockChainLookup;
26
+ let readStub;
27
+ const actualConfig = {
28
+ type: TokenType.collateral,
29
+ owner: OWNER_ADDRESS,
30
+ mailbox: MAILBOX_ADDRESS,
31
+ token: 'uhyp',
32
+ remoteRouters: {
33
+ [REMOTE_DOMAIN_ID_1]: {
34
+ address: TOKEN_ADDRESS,
35
+ },
36
+ },
37
+ destinationGas: {
38
+ [REMOTE_DOMAIN_ID_1]: '200000',
39
+ },
40
+ };
41
+ const baseDeployedArtifact = {
42
+ artifactState: ArtifactState.DEPLOYED,
43
+ config: actualConfig,
44
+ deployed: { address: TOKEN_ADDRESS },
45
+ };
46
+ const chainMetadata = {
47
+ name: TEST_CHAIN,
48
+ chainId: 1,
49
+ domainId: TEST_DOMAIN_ID,
50
+ protocol: ProtocolType.Ethereum,
51
+ rpcUrls: [{ http: 'http://localhost:8545' }],
52
+ };
53
+ beforeEach(() => {
54
+ // Create mock artifact manager
55
+ mockArtifactManager = {
56
+ readWarpToken: Sinon.stub(),
57
+ createWriter: Sinon.stub(),
58
+ supportsHookUpdates: Sinon.stub().returns(true),
59
+ };
60
+ // Create minimal mock signer
61
+ mockSigner = {
62
+ getSignerAddress: () => OWNER_ADDRESS,
63
+ };
64
+ // Create mock chain lookup
65
+ mockChainLookup = {
66
+ getChainMetadata: Sinon.stub().returns({
67
+ name: TEST_CHAIN,
68
+ domainId: TEST_DOMAIN_ID,
69
+ protocol: ProtocolType.Ethereum,
70
+ }),
71
+ getChainName: Sinon.stub().returns(TEST_CHAIN),
72
+ getDomainId: Sinon.stub().returns(TEST_DOMAIN_ID),
73
+ };
74
+ // Create mock ISM and Hook writers FIRST
75
+ mockIsmWriter = {
76
+ create: Sinon.stub(),
77
+ update: Sinon.stub(),
78
+ read: Sinon.stub(),
79
+ };
80
+ mockHookWriter = {
81
+ create: Sinon.stub(),
82
+ update: Sinon.stub(),
83
+ read: Sinon.stub(),
84
+ };
85
+ // Create writer instance - manually to bypass protocol provider
86
+ writer = Object.create(WarpTokenWriter.prototype);
87
+ Object.assign(writer, {
88
+ artifactManager: mockArtifactManager,
89
+ chainMetadata,
90
+ chainLookup: mockChainLookup,
91
+ signer: mockSigner,
92
+ ismWriter: mockIsmWriter,
93
+ hookWriterFactory: () => mockHookWriter,
94
+ });
95
+ // Default read stub - returns current config
96
+ readStub = Sinon.stub(writer, 'read').resolves(baseDeployedArtifact);
97
+ });
98
+ afterEach(() => {
99
+ Sinon.restore();
100
+ });
101
+ describe('update() - Router Management', () => {
102
+ const createMockTx = (annotation) => ({
103
+ annotation,
104
+ to: TOKEN_ADDRESS,
105
+ data: '0x',
106
+ });
107
+ const routerTestCases = [
108
+ {
109
+ name: 'no updates needed if config is the same',
110
+ configOverrides: {},
111
+ expectedTxCount: 0,
112
+ },
113
+ {
114
+ name: 'new remote router',
115
+ configOverrides: {
116
+ remoteRouters: {
117
+ ...actualConfig.remoteRouters,
118
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER' },
119
+ },
120
+ destinationGas: {
121
+ ...actualConfig.destinationGas,
122
+ [REMOTE_DOMAIN_ID_2]: '300000',
123
+ },
124
+ },
125
+ expectedTxCount: 1,
126
+ assertion: (txs) => {
127
+ expect(txs[0].annotation).to.include('router');
128
+ },
129
+ },
130
+ {
131
+ name: 'multiple new remote routers',
132
+ configOverrides: {
133
+ remoteRouters: {
134
+ ...actualConfig.remoteRouters,
135
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER1' },
136
+ [REMOTE_DOMAIN_ID_3]: { address: '0xNEWROUTER2' },
137
+ },
138
+ destinationGas: {
139
+ ...actualConfig.destinationGas,
140
+ [REMOTE_DOMAIN_ID_2]: '300000',
141
+ [REMOTE_DOMAIN_ID_3]: '400000',
142
+ },
143
+ },
144
+ expectedTxCount: 2,
145
+ assertion: (txs) => {
146
+ expect(txs).to.have.lengthOf(2);
147
+ txs.forEach((tx) => {
148
+ expect(tx.annotation).to.include('router');
149
+ });
150
+ },
151
+ },
152
+ {
153
+ name: 'remove existing remote router',
154
+ configOverrides: {
155
+ remoteRouters: {},
156
+ destinationGas: {},
157
+ },
158
+ expectedTxCount: 1,
159
+ },
160
+ {
161
+ name: 'update existing router address',
162
+ configOverrides: {
163
+ remoteRouters: {
164
+ [REMOTE_DOMAIN_ID_1]: { address: '0xUPDATEDROUTER' },
165
+ },
166
+ },
167
+ expectedTxCount: 2, // unenroll + enroll
168
+ },
169
+ {
170
+ name: 'update existing router gas',
171
+ configOverrides: {
172
+ destinationGas: {
173
+ [REMOTE_DOMAIN_ID_1]: '999999',
174
+ },
175
+ },
176
+ expectedTxCount: 2, // unenroll + enroll with new gas
177
+ },
178
+ {
179
+ name: 'remove and add remote router at the same time',
180
+ configOverrides: {
181
+ remoteRouters: {
182
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER' },
183
+ },
184
+ destinationGas: {
185
+ [REMOTE_DOMAIN_ID_2]: '300000',
186
+ },
187
+ },
188
+ expectedTxCount: 2, // remove old + add new
189
+ },
190
+ ];
191
+ routerTestCases.forEach(({ name, configOverrides, expectedTxCount, assertion }) => {
192
+ it(name, async () => {
193
+ // Setup mock writer
194
+ const mockWriter = {
195
+ read: Sinon.stub(),
196
+ create: Sinon.stub(),
197
+ update: Sinon.stub().resolves(Array(expectedTxCount)
198
+ .fill(null)
199
+ .map((_, i) => createMockTx(`Update router ${i}`))),
200
+ };
201
+ mockArtifactManager.createWriter.returns(mockWriter);
202
+ // Execute update
203
+ const artifact = {
204
+ ...baseDeployedArtifact,
205
+ config: { ...actualConfig, ...configOverrides },
206
+ };
207
+ const updateTxs = await writer.update(artifact);
208
+ // Assertions
209
+ expect(updateTxs).to.have.lengthOf(expectedTxCount);
210
+ if (assertion) {
211
+ assertion(updateTxs);
212
+ }
213
+ });
214
+ });
215
+ });
216
+ describe('update() - Ownership Changes', () => {
217
+ it('should update ownership', async () => {
218
+ const newOwner = '0x9999999999999999999999999999999999999999';
219
+ const configWithNewOwner = {
220
+ ...actualConfig,
221
+ owner: newOwner,
222
+ };
223
+ const mockWriter = {
224
+ read: Sinon.stub(),
225
+ create: Sinon.stub(),
226
+ update: Sinon.stub().resolves([
227
+ {
228
+ annotation: 'Transfer ownership',
229
+ to: TOKEN_ADDRESS,
230
+ data: '0x',
231
+ },
232
+ ]),
233
+ };
234
+ mockArtifactManager.createWriter.returns(mockWriter);
235
+ const artifact = {
236
+ ...baseDeployedArtifact,
237
+ config: configWithNewOwner,
238
+ };
239
+ const updateTxs = await writer.update(artifact);
240
+ expect(updateTxs).to.have.lengthOf(1);
241
+ expect(updateTxs[0].annotation).to.match(/ownership/i);
242
+ });
243
+ });
244
+ describe('update() - ISM Updates', () => {
245
+ const createIsmConfig = (type, validators) => ({
246
+ type,
247
+ validators,
248
+ threshold: 1,
249
+ });
250
+ it('should deploy new ISM', async () => {
251
+ const newIsmConfig = createIsmConfig('messageIdMultisigIsm', [
252
+ '0xVALIDATOR',
253
+ ]);
254
+ const configWithIsm = {
255
+ ...actualConfig,
256
+ interchainSecurityModule: {
257
+ artifactState: ArtifactState.NEW,
258
+ config: newIsmConfig,
259
+ },
260
+ };
261
+ // Mock ISM creation
262
+ const deployedIsm = {
263
+ artifactState: ArtifactState.DEPLOYED,
264
+ config: newIsmConfig,
265
+ deployed: { address: ISM_ADDRESS },
266
+ };
267
+ mockIsmWriter.create.resolves([deployedIsm, []]);
268
+ const mockWriter = {
269
+ read: Sinon.stub(),
270
+ create: Sinon.stub(),
271
+ update: Sinon.stub().resolves([
272
+ {
273
+ annotation: 'Set ISM',
274
+ to: TOKEN_ADDRESS,
275
+ data: '0x',
276
+ },
277
+ ]),
278
+ };
279
+ mockArtifactManager.createWriter.returns(mockWriter);
280
+ const artifact = {
281
+ ...baseDeployedArtifact,
282
+ config: configWithIsm,
283
+ };
284
+ const updateTxs = await writer.update(artifact);
285
+ expect(mockIsmWriter.create.callCount).to.equal(1);
286
+ expect(updateTxs.length).to.be.greaterThan(0);
287
+ });
288
+ it('should update existing ISM in-place when type is unchanged', async () => {
289
+ const ismConfig = createIsmConfig('messageIdMultisigIsm', [
290
+ '0xVALIDATOR1',
291
+ ]);
292
+ const currentArtifactWithIsm = {
293
+ ...baseDeployedArtifact,
294
+ config: {
295
+ ...actualConfig,
296
+ interchainSecurityModule: {
297
+ artifactState: ArtifactState.DEPLOYED,
298
+ config: ismConfig,
299
+ deployed: { address: ISM_ADDRESS },
300
+ },
301
+ },
302
+ };
303
+ readStub.restore();
304
+ readStub = Sinon.stub(writer, 'read').resolves(currentArtifactWithIsm);
305
+ mockIsmWriter.update.resolves([]);
306
+ const mockWriter = {
307
+ read: Sinon.stub(),
308
+ create: Sinon.stub(),
309
+ update: Sinon.stub().resolves([]),
310
+ };
311
+ mockArtifactManager.createWriter.returns(mockWriter);
312
+ const artifact = {
313
+ ...baseDeployedArtifact,
314
+ config: {
315
+ ...actualConfig,
316
+ interchainSecurityModule: {
317
+ artifactState: ArtifactState.DEPLOYED,
318
+ config: ismConfig,
319
+ deployed: { address: ISM_ADDRESS },
320
+ },
321
+ },
322
+ };
323
+ await writer.update(artifact);
324
+ expect(mockIsmWriter.create.callCount).to.equal(0);
325
+ expect(mockIsmWriter.update.callCount).to.equal(1);
326
+ });
327
+ it('should replace existing ISM when type changes', async () => {
328
+ // Setup current artifact with existing ISM
329
+ const currentIsmConfig = createIsmConfig('messageIdMultisigIsm', [
330
+ '0xVALIDATOR1',
331
+ ]);
332
+ const currentArtifactWithIsm = {
333
+ ...baseDeployedArtifact,
334
+ config: {
335
+ ...actualConfig,
336
+ interchainSecurityModule: {
337
+ artifactState: ArtifactState.DEPLOYED,
338
+ config: currentIsmConfig,
339
+ deployed: { address: ISM_ADDRESS },
340
+ },
341
+ },
342
+ };
343
+ readStub.restore();
344
+ readStub = Sinon.stub(writer, 'read').resolves(currentArtifactWithIsm);
345
+ // New ISM config
346
+ const newIsmConfig = {
347
+ type: 'merkleRootMultisigIsm',
348
+ validators: ['0xVALIDATOR2'],
349
+ threshold: 1,
350
+ };
351
+ const configWithNewIsm = {
352
+ ...actualConfig,
353
+ interchainSecurityModule: {
354
+ artifactState: ArtifactState.NEW,
355
+ config: newIsmConfig,
356
+ },
357
+ };
358
+ // Mock ISM creation (new ISM type)
359
+ const newIsmAddress = '0x0000000000000000000000000000000000000004';
360
+ const deployedNewIsm = {
361
+ artifactState: ArtifactState.DEPLOYED,
362
+ config: newIsmConfig,
363
+ deployed: { address: newIsmAddress },
364
+ };
365
+ mockIsmWriter.create.resolves([deployedNewIsm, []]);
366
+ const mockWriter = {
367
+ read: Sinon.stub(),
368
+ create: Sinon.stub(),
369
+ update: Sinon.stub().resolves([
370
+ {
371
+ annotation: 'Update ISM',
372
+ to: TOKEN_ADDRESS,
373
+ data: '0x',
374
+ },
375
+ ]),
376
+ };
377
+ mockArtifactManager.createWriter.returns(mockWriter);
378
+ const artifact = {
379
+ ...baseDeployedArtifact,
380
+ config: configWithNewIsm,
381
+ };
382
+ const updateTxs = await writer.update(artifact);
383
+ expect(mockIsmWriter.create.callCount).to.equal(1);
384
+ expect(updateTxs.length).to.be.greaterThan(0);
385
+ });
386
+ it('should treat omitted ISM and zero-address ISM equivalently', async () => {
387
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
388
+ const mockWriter = {
389
+ read: Sinon.stub(),
390
+ create: Sinon.stub(),
391
+ update: Sinon.stub().resolves([]),
392
+ };
393
+ mockArtifactManager.createWriter.returns(mockWriter);
394
+ // Case 1: no ISM
395
+ const artifactNoIsm = {
396
+ ...baseDeployedArtifact,
397
+ config: { ...actualConfig, interchainSecurityModule: undefined },
398
+ };
399
+ await writer.update(artifactNoIsm);
400
+ const createCountAfterNoIsm = mockIsmWriter.create.callCount;
401
+ // Case 2: zero-address ISM (UNDERIVED — treated as pass-through)
402
+ const artifactZeroIsm = {
403
+ ...baseDeployedArtifact,
404
+ config: {
405
+ ...actualConfig,
406
+ interchainSecurityModule: {
407
+ artifactState: ArtifactState.UNDERIVED,
408
+ deployed: { address: ZERO_ADDRESS },
409
+ },
410
+ },
411
+ };
412
+ await writer.update(artifactZeroIsm);
413
+ // Neither case should trigger ISM creation
414
+ expect(mockIsmWriter.create.callCount).to.equal(createCountAfterNoIsm);
415
+ expect(mockIsmWriter.create.callCount).to.equal(0);
416
+ });
417
+ });
418
+ describe('update() - Hook Updates', () => {
419
+ const merkleTreeHookConfig = {
420
+ type: 'merkleTreeHook',
421
+ };
422
+ it('should deploy new hook when none existed before', async () => {
423
+ const configWithHook = {
424
+ ...actualConfig,
425
+ hook: {
426
+ artifactState: ArtifactState.NEW,
427
+ config: merkleTreeHookConfig,
428
+ },
429
+ };
430
+ const deployedHook = {
431
+ artifactState: ArtifactState.DEPLOYED,
432
+ config: merkleTreeHookConfig,
433
+ deployed: { address: HOOK_ADDRESS },
434
+ };
435
+ mockHookWriter.create.resolves([deployedHook, []]);
436
+ const mockWriter = {
437
+ read: Sinon.stub(),
438
+ create: Sinon.stub(),
439
+ update: Sinon.stub().resolves([
440
+ { annotation: 'Set hook', to: TOKEN_ADDRESS, data: '0x' },
441
+ ]),
442
+ };
443
+ mockArtifactManager.createWriter.returns(mockWriter);
444
+ const artifact = {
445
+ ...baseDeployedArtifact,
446
+ config: configWithHook,
447
+ };
448
+ const updateTxs = await writer.update(artifact);
449
+ expect(mockHookWriter.create.callCount).to.equal(1);
450
+ expect(updateTxs.length).to.be.greaterThan(0);
451
+ });
452
+ it('should skip hook deployment when hook is underived (address reference)', async () => {
453
+ const configWithUnderivedHook = {
454
+ ...actualConfig,
455
+ hook: {
456
+ artifactState: ArtifactState.UNDERIVED,
457
+ deployed: { address: HOOK_ADDRESS },
458
+ },
459
+ };
460
+ const mockWriter = {
461
+ read: Sinon.stub(),
462
+ create: Sinon.stub(),
463
+ update: Sinon.stub().resolves([]),
464
+ };
465
+ mockArtifactManager.createWriter.returns(mockWriter);
466
+ const artifact = {
467
+ ...baseDeployedArtifact,
468
+ config: configWithUnderivedHook,
469
+ };
470
+ await writer.update(artifact);
471
+ expect(mockHookWriter.create.called).to.be.false;
472
+ expect(mockHookWriter.update.called).to.be.false;
473
+ });
474
+ it('should skip hook deployment when protocol does not support hook updates', async () => {
475
+ mockArtifactManager.supportsHookUpdates.returns(false);
476
+ const configWithHook = {
477
+ ...actualConfig,
478
+ hook: {
479
+ artifactState: ArtifactState.NEW,
480
+ config: merkleTreeHookConfig,
481
+ },
482
+ };
483
+ const mockWriter = {
484
+ read: Sinon.stub(),
485
+ create: Sinon.stub(),
486
+ update: Sinon.stub().resolves([]),
487
+ };
488
+ mockArtifactManager.createWriter.returns(mockWriter);
489
+ const artifact = {
490
+ ...baseDeployedArtifact,
491
+ config: configWithHook,
492
+ };
493
+ await writer.update(artifact);
494
+ expect(mockHookWriter.create.called).to.be.false;
495
+ expect(mockHookWriter.update.called).to.be.false;
496
+ });
497
+ it('should handle hook + router updates in single call', async () => {
498
+ const configWithHookAndRouter = {
499
+ ...actualConfig,
500
+ hook: {
501
+ artifactState: ArtifactState.NEW,
502
+ config: merkleTreeHookConfig,
503
+ },
504
+ remoteRouters: {
505
+ ...actualConfig.remoteRouters,
506
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER' },
507
+ },
508
+ destinationGas: {
509
+ ...actualConfig.destinationGas,
510
+ [REMOTE_DOMAIN_ID_2]: '300000',
511
+ },
512
+ };
513
+ const deployedHook = {
514
+ artifactState: ArtifactState.DEPLOYED,
515
+ config: merkleTreeHookConfig,
516
+ deployed: { address: HOOK_ADDRESS },
517
+ };
518
+ mockHookWriter.create.resolves([deployedHook, []]);
519
+ const mockWriter = {
520
+ read: Sinon.stub(),
521
+ create: Sinon.stub(),
522
+ update: Sinon.stub().resolves([
523
+ { annotation: 'Set hook', to: TOKEN_ADDRESS, data: '0x' },
524
+ { annotation: 'Enroll router', to: TOKEN_ADDRESS, data: '0x' },
525
+ ]),
526
+ };
527
+ mockArtifactManager.createWriter.returns(mockWriter);
528
+ const artifact = {
529
+ ...baseDeployedArtifact,
530
+ config: configWithHookAndRouter,
531
+ };
532
+ const updateTxs = await writer.update(artifact);
533
+ expect(mockHookWriter.create.callCount).to.equal(1);
534
+ expect(updateTxs.length).to.equal(2);
535
+ });
536
+ });
537
+ describe('update() - Validation', () => {
538
+ it('should reject changing token type', async () => {
539
+ // Current artifact is collateral
540
+ const currentArtifact = {
541
+ ...baseDeployedArtifact,
542
+ config: {
543
+ ...actualConfig,
544
+ type: TokenType.collateral,
545
+ },
546
+ };
547
+ readStub.restore();
548
+ readStub = Sinon.stub(writer, 'read').resolves(currentArtifact);
549
+ // Try to change to synthetic
550
+ const syntheticConfig = {
551
+ type: TokenType.synthetic,
552
+ owner: OWNER_ADDRESS,
553
+ mailbox: MAILBOX_ADDRESS,
554
+ name: 'Synthetic Token',
555
+ symbol: 'SYN',
556
+ decimals: 18,
557
+ remoteRouters: {},
558
+ destinationGas: {},
559
+ };
560
+ const artifact = {
561
+ ...baseDeployedArtifact,
562
+ config: syntheticConfig,
563
+ };
564
+ await expect(writer.update(artifact)).to.be.rejectedWith(/Cannot change warp token type/);
565
+ });
566
+ });
567
+ describe('update() - Complex Scenarios', () => {
568
+ it('should handle ISM + router updates in single call', async () => {
569
+ const newIsmConfig = {
570
+ type: 'messageIdMultisigIsm',
571
+ validators: ['0xVALIDATOR'],
572
+ threshold: 1,
573
+ };
574
+ const configWithIsmAndRouter = {
575
+ ...actualConfig,
576
+ interchainSecurityModule: {
577
+ artifactState: ArtifactState.NEW,
578
+ config: newIsmConfig,
579
+ },
580
+ remoteRouters: {
581
+ ...actualConfig.remoteRouters,
582
+ [REMOTE_DOMAIN_ID_2]: { address: '0xNEWROUTER' },
583
+ },
584
+ destinationGas: {
585
+ ...actualConfig.destinationGas,
586
+ [REMOTE_DOMAIN_ID_2]: '300000',
587
+ },
588
+ };
589
+ // Mock ISM creation
590
+ const deployedIsm = {
591
+ artifactState: ArtifactState.DEPLOYED,
592
+ config: newIsmConfig,
593
+ deployed: { address: ISM_ADDRESS },
594
+ };
595
+ mockIsmWriter.create.resolves([deployedIsm, []]);
596
+ const mockWriter = {
597
+ read: Sinon.stub(),
598
+ create: Sinon.stub(),
599
+ update: Sinon.stub().resolves([
600
+ {
601
+ annotation: 'Set ISM',
602
+ to: TOKEN_ADDRESS,
603
+ data: '0x',
604
+ },
605
+ {
606
+ annotation: 'Enroll router',
607
+ to: TOKEN_ADDRESS,
608
+ data: '0x',
609
+ },
610
+ ]),
611
+ };
612
+ mockArtifactManager.createWriter.returns(mockWriter);
613
+ const artifact = {
614
+ ...baseDeployedArtifact,
615
+ config: configWithIsmAndRouter,
616
+ };
617
+ const updateTxs = await writer.update(artifact);
618
+ expect(mockIsmWriter.create.callCount).to.equal(1);
619
+ expect(updateTxs.length).to.be.greaterThan(1);
620
+ });
621
+ it('should handle ownership + ISM + router updates', async () => {
622
+ const newOwner = '0x9999999999999999999999999999999999999999';
623
+ const newIsmConfig = {
624
+ type: 'messageIdMultisigIsm',
625
+ validators: ['0xVALIDATOR'],
626
+ threshold: 1,
627
+ };
628
+ const complexConfig = {
629
+ ...actualConfig,
630
+ owner: newOwner,
631
+ interchainSecurityModule: {
632
+ artifactState: ArtifactState.NEW,
633
+ config: newIsmConfig,
634
+ },
635
+ remoteRouters: {},
636
+ destinationGas: {},
637
+ };
638
+ // Mock ISM creation
639
+ const deployedIsm = {
640
+ artifactState: ArtifactState.DEPLOYED,
641
+ config: newIsmConfig,
642
+ deployed: { address: ISM_ADDRESS },
643
+ };
644
+ mockIsmWriter.create.resolves([deployedIsm, []]);
645
+ const mockWriter = {
646
+ read: Sinon.stub(),
647
+ create: Sinon.stub(),
648
+ update: Sinon.stub().resolves([
649
+ {
650
+ annotation: 'Transfer ownership',
651
+ to: TOKEN_ADDRESS,
652
+ data: '0x',
653
+ },
654
+ {
655
+ annotation: 'Set ISM',
656
+ to: TOKEN_ADDRESS,
657
+ data: '0x',
658
+ },
659
+ {
660
+ annotation: 'Unenroll router',
661
+ to: TOKEN_ADDRESS,
662
+ data: '0x',
663
+ },
664
+ ]),
665
+ };
666
+ mockArtifactManager.createWriter.returns(mockWriter);
667
+ const artifact = {
668
+ ...baseDeployedArtifact,
669
+ config: complexConfig,
670
+ };
671
+ const updateTxs = await writer.update(artifact);
672
+ expect(mockIsmWriter.create.callCount).to.equal(1);
673
+ expect(updateTxs.length).to.equal(3);
674
+ });
675
+ });
676
+ describe('create()', () => {
677
+ it('should create warp token without ISM', async () => {
678
+ const mockWriter = {
679
+ read: Sinon.stub(),
680
+ create: Sinon.stub().resolves([
681
+ {
682
+ artifactState: ArtifactState.DEPLOYED,
683
+ config: actualConfig,
684
+ deployed: { address: TOKEN_ADDRESS },
685
+ },
686
+ [],
687
+ ]),
688
+ update: Sinon.stub(),
689
+ };
690
+ mockArtifactManager.createWriter.returns(mockWriter);
691
+ const artifact = {
692
+ artifactState: ArtifactState.NEW,
693
+ config: actualConfig,
694
+ };
695
+ const [deployed, receipts] = await writer.create(artifact);
696
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
697
+ expect(deployed.deployed.address).to.equal(TOKEN_ADDRESS);
698
+ expect(receipts).to.be.an('array');
699
+ expect(mockWriter.create.callCount).to.equal(1);
700
+ });
701
+ it('should create warp token with new ISM', async () => {
702
+ const newIsmConfig = {
703
+ type: 'messageIdMultisigIsm',
704
+ validators: ['0xVALIDATOR'],
705
+ threshold: 1,
706
+ };
707
+ const configWithIsm = {
708
+ ...actualConfig,
709
+ interchainSecurityModule: {
710
+ artifactState: ArtifactState.NEW,
711
+ config: newIsmConfig,
712
+ },
713
+ };
714
+ // Mock ISM creation
715
+ const deployedIsm = {
716
+ artifactState: ArtifactState.DEPLOYED,
717
+ config: newIsmConfig,
718
+ deployed: { address: ISM_ADDRESS },
719
+ };
720
+ mockIsmWriter.create.resolves([deployedIsm, []]);
721
+ const mockWriter = {
722
+ read: Sinon.stub(),
723
+ create: Sinon.stub().resolves([
724
+ {
725
+ artifactState: ArtifactState.DEPLOYED,
726
+ config: configWithIsm,
727
+ deployed: { address: TOKEN_ADDRESS },
728
+ },
729
+ [],
730
+ ]),
731
+ update: Sinon.stub(),
732
+ };
733
+ mockArtifactManager.createWriter.returns(mockWriter);
734
+ const artifact = {
735
+ artifactState: ArtifactState.NEW,
736
+ config: configWithIsm,
737
+ };
738
+ const [deployed, receipts] = await writer.create(artifact);
739
+ expect(mockIsmWriter.create.callCount).to.equal(1);
740
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
741
+ expect(deployed.deployed.address).to.equal(TOKEN_ADDRESS);
742
+ expect(receipts).to.be.an('array');
743
+ });
744
+ it('should create warp token with existing ISM', async () => {
745
+ const existingIsmConfig = {
746
+ type: 'messageIdMultisigIsm',
747
+ validators: ['0xVALIDATOR'],
748
+ threshold: 1,
749
+ };
750
+ const configWithExistingIsm = {
751
+ ...actualConfig,
752
+ interchainSecurityModule: {
753
+ artifactState: ArtifactState.DEPLOYED,
754
+ config: existingIsmConfig,
755
+ deployed: { address: ISM_ADDRESS },
756
+ },
757
+ };
758
+ const mockWriter = {
759
+ read: Sinon.stub(),
760
+ create: Sinon.stub().resolves([
761
+ {
762
+ artifactState: ArtifactState.DEPLOYED,
763
+ config: configWithExistingIsm,
764
+ deployed: { address: TOKEN_ADDRESS },
765
+ },
766
+ [],
767
+ ]),
768
+ update: Sinon.stub(),
769
+ };
770
+ mockArtifactManager.createWriter.returns(mockWriter);
771
+ const artifact = {
772
+ artifactState: ArtifactState.NEW,
773
+ config: configWithExistingIsm,
774
+ };
775
+ const [deployed, receipts] = await writer.create(artifact);
776
+ // Should not create new ISM
777
+ expect(mockIsmWriter.create.called).to.be.false;
778
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
779
+ expect(deployed.deployed.address).to.equal(TOKEN_ADDRESS);
780
+ expect(receipts).to.be.an('array');
781
+ });
782
+ });
783
+ describe('create() - Hook', () => {
784
+ const merkleTreeHookConfig = {
785
+ type: 'merkleTreeHook',
786
+ };
787
+ it('should deploy hook before warp token when hook is new', async () => {
788
+ const configWithHook = {
789
+ ...actualConfig,
790
+ hook: {
791
+ artifactState: ArtifactState.NEW,
792
+ config: merkleTreeHookConfig,
793
+ },
794
+ };
795
+ const hookReceipt = { transactionHash: '0xHOOKTX' };
796
+ const deployedHook = {
797
+ artifactState: ArtifactState.DEPLOYED,
798
+ config: merkleTreeHookConfig,
799
+ deployed: { address: HOOK_ADDRESS },
800
+ };
801
+ mockHookWriter.create.resolves([deployedHook, [hookReceipt]]);
802
+ const mockWriter = {
803
+ read: Sinon.stub(),
804
+ create: Sinon.stub().resolves([
805
+ {
806
+ artifactState: ArtifactState.DEPLOYED,
807
+ config: configWithHook,
808
+ deployed: { address: TOKEN_ADDRESS },
809
+ },
810
+ [],
811
+ ]),
812
+ update: Sinon.stub(),
813
+ };
814
+ mockArtifactManager.createWriter.returns(mockWriter);
815
+ const artifact = {
816
+ artifactState: ArtifactState.NEW,
817
+ config: configWithHook,
818
+ };
819
+ const [deployed, receipts] = await writer.create(artifact);
820
+ expect(mockHookWriter.create.callCount).to.equal(1);
821
+ expect(receipts).to.include(hookReceipt);
822
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
823
+ expect(deployed.deployed.address).to.equal(TOKEN_ADDRESS);
824
+ });
825
+ it('should reuse hook address when hook is already deployed', async () => {
826
+ const configWithDeployedHook = {
827
+ ...actualConfig,
828
+ hook: {
829
+ artifactState: ArtifactState.DEPLOYED,
830
+ config: merkleTreeHookConfig,
831
+ deployed: { address: HOOK_ADDRESS },
832
+ },
833
+ };
834
+ const mockWriter = {
835
+ read: Sinon.stub(),
836
+ create: Sinon.stub().resolves([
837
+ {
838
+ artifactState: ArtifactState.DEPLOYED,
839
+ config: configWithDeployedHook,
840
+ deployed: { address: TOKEN_ADDRESS },
841
+ },
842
+ [],
843
+ ]),
844
+ update: Sinon.stub(),
845
+ };
846
+ mockArtifactManager.createWriter.returns(mockWriter);
847
+ const artifact = {
848
+ artifactState: ArtifactState.NEW,
849
+ config: configWithDeployedHook,
850
+ };
851
+ const [deployed] = await writer.create(artifact);
852
+ expect(mockHookWriter.create.called).to.be.false;
853
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
854
+ });
855
+ it('should skip hook deployment when protocol does not support hooks', async () => {
856
+ mockArtifactManager.supportsHookUpdates.returns(false);
857
+ const configWithHook = {
858
+ ...actualConfig,
859
+ hook: {
860
+ artifactState: ArtifactState.NEW,
861
+ config: merkleTreeHookConfig,
862
+ },
863
+ };
864
+ const mockWriter = {
865
+ read: Sinon.stub(),
866
+ create: Sinon.stub().resolves([
867
+ {
868
+ artifactState: ArtifactState.DEPLOYED,
869
+ config: configWithHook,
870
+ deployed: { address: TOKEN_ADDRESS },
871
+ },
872
+ [],
873
+ ]),
874
+ update: Sinon.stub(),
875
+ };
876
+ mockArtifactManager.createWriter.returns(mockWriter);
877
+ const artifact = {
878
+ artifactState: ArtifactState.NEW,
879
+ config: configWithHook,
880
+ };
881
+ const [deployed] = await writer.create(artifact);
882
+ expect(mockHookWriter.create.called).to.be.false;
883
+ expect(deployed.artifactState).to.equal(ArtifactState.DEPLOYED);
884
+ });
885
+ });
886
+ describe('update() - Idempotency', () => {
887
+ it('should return empty array when no changes needed', async () => {
888
+ const mockWriter = {
889
+ read: Sinon.stub(),
890
+ create: Sinon.stub(),
891
+ update: Sinon.stub().resolves([]),
892
+ };
893
+ mockArtifactManager.createWriter.returns(mockWriter);
894
+ const artifact = {
895
+ ...baseDeployedArtifact,
896
+ config: actualConfig,
897
+ };
898
+ const updateTxs = await writer.update(artifact);
899
+ expect(updateTxs).to.be.an('array').that.is.empty;
900
+ });
901
+ });
902
+ });
903
+ //# sourceMappingURL=warp-writer.test.js.map