Skip to content

Commit d30598c

Browse files
feat: statics configuration for zksyncera
Ticket: WIN-8193
1 parent 54e2595 commit d30598c

File tree

13 files changed

+442
-0
lines changed

13 files changed

+442
-0
lines changed

modules/bitgo/test/v2/unit/keychains.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ describe('V2 Keychains', function () {
108108
n.asset !== UnderlyingAsset.DOGEOS &&
109109
n.asset !== UnderlyingAsset.MEGAETH &&
110110
n.asset !== UnderlyingAsset.ARC &&
111+
n.asset !== UnderlyingAsset.ZKSYNCERA &&
111112
coinFamilyValues.includes(n.name)
112113
);
113114

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// modules/sdk-coin-evm/test/integration/zksyncera.integration.ts
2+
3+
import { TransactionType } from '@bitgo/sdk-core';
4+
import { coins, Networks, CoinFamily } from '@bitgo/statics';
5+
import should from 'should';
6+
import { decodeTransferData, KeyPair } from '@bitgo/abstract-eth';
7+
import { TransactionBuilder } from '../../src/lib';
8+
9+
describe('ZkSync Era Transaction Builder Integration', function () {
10+
describe('Mainnet (zksyncera)', function () {
11+
let txBuilder: TransactionBuilder;
12+
let key: string;
13+
let contractAddress: string;
14+
15+
const testPrivateKey =
16+
'xprv9s21ZrQH143K3vYxF8BfcG8g82bkkrf962jYfdc2SdsbXzLRcnaWAD3jWMsQaTz9ZoqD7gvYeR3fRPZy3Fr2UFXrome67sTdb66wAFmcz6G';
17+
18+
beforeEach(function () {
19+
contractAddress = '0x8f977e912ef500548a0c3be6ddde9899f1199b81';
20+
txBuilder = new TransactionBuilder(coins.get('zksyncera'));
21+
const keyPair = new KeyPair({ prv: testPrivateKey });
22+
key = keyPair.getKeys().prv as string;
23+
txBuilder.fee({
24+
fee: '1000000000',
25+
gasLimit: '12100000',
26+
});
27+
txBuilder.counter(2);
28+
txBuilder.type(TransactionType.Send);
29+
txBuilder.contract(contractAddress);
30+
});
31+
32+
it('should have correct network configuration', function () {
33+
const coin = coins.get('zksyncera');
34+
coin.family.should.equal(CoinFamily.ZKSYNCERA);
35+
coin.network.should.deepEqual(Networks.main.zkSyncEra);
36+
(coin.network as any).chainId.should.equal(324);
37+
});
38+
39+
it('should build a send funds transaction with correct chainId', async function () {
40+
const recipient = '0x19645032c7f1533395d44a629462e751084d3e4c';
41+
const amount = '1000000000';
42+
const expireTime = 1590066728;
43+
const sequenceId = 5;
44+
45+
txBuilder
46+
.transfer()
47+
.amount(amount)
48+
.to(recipient)
49+
.expirationTime(expireTime)
50+
.contractSequenceId(sequenceId)
51+
.key(key);
52+
53+
txBuilder.sign({ key: testPrivateKey });
54+
const tx = await txBuilder.build();
55+
56+
// Verify chainId is 324 (zkSync Era mainnet)
57+
const txJson = tx.toJson();
58+
// ChainId may be in hex or decimal format depending on implementation
59+
const chainId = typeof txJson.chainId === 'string' ? parseInt(txJson.chainId, 16) : txJson.chainId;
60+
chainId.should.equal(324);
61+
62+
// Verify transaction structure
63+
should.equal(tx.signature.length, 2);
64+
should.equal(tx.inputs.length, 1);
65+
should.equal(tx.inputs[0].address, contractAddress);
66+
should.equal(tx.inputs[0].value, amount);
67+
68+
should.equal(tx.outputs.length, 1);
69+
should.equal(tx.outputs[0].address, recipient);
70+
should.equal(tx.outputs[0].value, amount);
71+
72+
// Verify decoded transfer data
73+
const data = txJson.data;
74+
const {
75+
to,
76+
amount: parsedAmount,
77+
expireTime: parsedExpireTime,
78+
sequenceId: parsedSequenceId,
79+
} = decodeTransferData(data);
80+
should.equal(to, recipient);
81+
should.equal(parsedAmount, amount);
82+
should.equal(parsedExpireTime, expireTime);
83+
should.equal(parsedSequenceId, sequenceId);
84+
});
85+
86+
it('should build a send funds transaction with amount 0', async function () {
87+
txBuilder
88+
.transfer()
89+
.amount('0')
90+
.to('0x19645032c7f1533395d44a629462e751084d3e4c')
91+
.expirationTime(1590066728)
92+
.contractSequenceId(5)
93+
.key(key);
94+
txBuilder.sign({ key: testPrivateKey });
95+
const tx = await txBuilder.build();
96+
97+
should.exist(tx.toBroadcastFormat());
98+
tx.inputs[0].value.should.equal('0');
99+
});
100+
101+
it('should use non-packed encoding for txdata (USES_NON_PACKED_ENCODING_FOR_TXDATA feature)', async function () {
102+
const recipient = '0x19645032c7f1533395d44a629462e751084d3e4c';
103+
const amount = '1000000000';
104+
105+
txBuilder.transfer().amount(amount).to(recipient).expirationTime(1590066728).contractSequenceId(5).key(key);
106+
107+
txBuilder.sign({ key: testPrivateKey });
108+
const tx = await txBuilder.build();
109+
110+
// The transaction should be buildable and have valid data
111+
const txJson = tx.toJson();
112+
should.exist(txJson.data);
113+
txJson.data.should.startWith('0x');
114+
});
115+
116+
it('should be identified as ETH_ROLLUP_CHAIN', function () {
117+
const coin = coins.get('zksyncera');
118+
coin.features.should.containEql('eth-rollup-chain');
119+
});
120+
});
121+
122+
describe('Testnet (tzksyncera)', function () {
123+
let txBuilder: TransactionBuilder;
124+
let key: string;
125+
let contractAddress: string;
126+
127+
const testPrivateKey =
128+
'xprv9s21ZrQH143K3vYxF8BfcG8g82bkkrf962jYfdc2SdsbXzLRcnaWAD3jWMsQaTz9ZoqD7gvYeR3fRPZy3Fr2UFXrome67sTdb66wAFmcz6G';
129+
130+
beforeEach(function () {
131+
contractAddress = '0x8f977e912ef500548a0c3be6ddde9899f1199b81';
132+
txBuilder = new TransactionBuilder(coins.get('tzksyncera'));
133+
const keyPair = new KeyPair({ prv: testPrivateKey });
134+
key = keyPair.getKeys().prv as string;
135+
txBuilder.fee({
136+
fee: '1000000000',
137+
gasLimit: '12100000',
138+
});
139+
txBuilder.counter(2);
140+
txBuilder.type(TransactionType.Send);
141+
txBuilder.contract(contractAddress);
142+
});
143+
144+
it('should have correct testnet network configuration', function () {
145+
const coin = coins.get('tzksyncera');
146+
coin.family.should.equal(CoinFamily.ZKSYNCERA);
147+
coin.network.should.deepEqual(Networks.test.zkSyncEra);
148+
(coin.network as any).chainId.should.equal(300);
149+
});
150+
151+
it('should build a send funds transaction with testnet chainId', async function () {
152+
const recipient = '0x19645032c7f1533395d44a629462e751084d3e4c';
153+
const amount = '1000000000';
154+
const expireTime = 1590066728;
155+
const sequenceId = 5;
156+
157+
txBuilder
158+
.transfer()
159+
.amount(amount)
160+
.to(recipient)
161+
.expirationTime(expireTime)
162+
.contractSequenceId(sequenceId)
163+
.key(key);
164+
165+
txBuilder.sign({ key: testPrivateKey });
166+
const tx = await txBuilder.build();
167+
168+
// Verify chainId is 300 (zkSync Era Sepolia testnet)
169+
const txJson = tx.toJson();
170+
const chainId = typeof txJson.chainId === 'string' ? parseInt(txJson.chainId, 16) : txJson.chainId;
171+
chainId.should.equal(300);
172+
173+
// Verify transaction structure
174+
should.equal(tx.signature.length, 2);
175+
should.equal(tx.inputs.length, 1);
176+
should.equal(tx.outputs.length, 1);
177+
});
178+
179+
it('should have testnet contract addresses configured', function () {
180+
const network = Networks.test.zkSyncEra;
181+
should.exist(network.forwarderFactoryAddress);
182+
should.exist(network.forwarderImplementationAddress);
183+
should.exist(network.walletFactoryAddress);
184+
should.exist(network.walletImplementationAddress);
185+
186+
network.forwarderFactoryAddress.should.equal('0xdd498702f44c4da08eb9e08d3f015eefe5cb71fc');
187+
network.walletFactoryAddress.should.equal('0x4550e1e7616d3364877fc6c9324938dab678621a');
188+
});
189+
});
190+
191+
describe('Transaction serialization and deserialization', function () {
192+
it('should serialize and deserialize a transaction correctly', async function () {
193+
const txBuilder = new TransactionBuilder(coins.get('tzksyncera'));
194+
const testPrivateKey =
195+
'xprv9s21ZrQH143K3vYxF8BfcG8g82bkkrf962jYfdc2SdsbXzLRcnaWAD3jWMsQaTz9ZoqD7gvYeR3fRPZy3Fr2UFXrome67sTdb66wAFmcz6G';
196+
const keyPair = new KeyPair({ prv: testPrivateKey });
197+
const key = keyPair.getKeys().prv as string;
198+
const contractAddress = '0x8f977e912ef500548a0c3be6ddde9899f1199b81';
199+
200+
txBuilder.fee({
201+
fee: '1000000000',
202+
gasLimit: '12100000',
203+
});
204+
txBuilder.counter(2);
205+
txBuilder.type(TransactionType.Send);
206+
txBuilder.contract(contractAddress);
207+
txBuilder
208+
.transfer()
209+
.amount('1000000000')
210+
.to('0x19645032c7f1533395d44a629462e751084d3e4c')
211+
.expirationTime(1590066728)
212+
.contractSequenceId(5)
213+
.key(key);
214+
txBuilder.sign({ key: testPrivateKey });
215+
216+
const tx = await txBuilder.build();
217+
const serialized = tx.toBroadcastFormat();
218+
219+
// Rebuild from serialized
220+
const txBuilder2 = new TransactionBuilder(coins.get('tzksyncera'));
221+
txBuilder2.from(serialized);
222+
const tx2 = await txBuilder2.build();
223+
224+
// Should produce the same serialized output
225+
tx2.toBroadcastFormat().should.equal(serialized);
226+
});
227+
});
228+
});

modules/sdk-core/src/bitgo/environments.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,9 @@ const mainnetBase: EnvironmentTemplate = {
306306
xdc: {
307307
baseUrl: 'https://api.etherscan.io/v2',
308308
},
309+
zksyncera: {
310+
baseUrl: 'https://api.etherscan.io/v2',
311+
},
309312
},
310313
icpNodeUrl: 'https://ic0.app',
311314
worldExplorerBaseUrl: 'https://worldscan.org/',
@@ -475,6 +478,9 @@ const testnetBase: EnvironmentTemplate = {
475478
xdc: {
476479
baseUrl: 'https://api.etherscan.io/v2',
477480
},
481+
zksyncera: {
482+
baseUrl: 'https://api.etherscan.io/v2',
483+
},
478484
},
479485
stxNodeUrl: 'https://api.testnet.hiro.so',
480486
vetNodeUrl: 'https://sync-testnet.vechain.org',
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Environments } from '../../../src';
2+
import should from 'should';
3+
4+
describe('zkSync Era Environment Configuration', function () {
5+
it('should have evm config for mainnet zksyncera', function () {
6+
const mainnetEnv = Environments.prod;
7+
should.exist(mainnetEnv.evm);
8+
should.exist(mainnetEnv.evm?.zksyncera);
9+
mainnetEnv.evm?.zksyncera.baseUrl.should.equal('https://api.etherscan.io/v2');
10+
});
11+
12+
it('should have evm config for testnet zksyncera', function () {
13+
const testnetEnv = Environments.test;
14+
should.exist(testnetEnv.evm);
15+
should.exist(testnetEnv.evm?.zksyncera);
16+
testnetEnv.evm?.zksyncera.baseUrl.should.equal('https://api.etherscan.io/v2');
17+
});
18+
});

modules/statics/src/allCoinsAndTokens.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ import {
138138
XTZ_FEATURES,
139139
ZETA_FEATURES,
140140
ZKETH_FEATURES,
141+
ZKSYNCERA_FEATURES,
141142
} from './coinFeatures';
142143
import { botTokens } from './coins/botTokens';
143144
import { adaTokens } from './coins/adaTokens';
@@ -1375,6 +1376,26 @@ export const allCoinsAndTokens = [
13751376
BaseUnit.ETH,
13761377
ZKETH_FEATURES
13771378
),
1379+
account(
1380+
'73c6f066-107a-4dcb-84e6-5a5f9dab2a1e',
1381+
'zksyncera',
1382+
'zkSync Era',
1383+
Networks.main.zkSyncEra,
1384+
18,
1385+
UnderlyingAsset.ZKSYNCERA,
1386+
BaseUnit.ETH,
1387+
ZKSYNCERA_FEATURES
1388+
),
1389+
account(
1390+
'fc901cec-26fa-4afb-830a-6793425d7064',
1391+
'tzksyncera',
1392+
'Testnet zkSync Era',
1393+
Networks.test.zkSyncEra,
1394+
18,
1395+
UnderlyingAsset.ZKSYNCERA,
1396+
BaseUnit.ETH,
1397+
ZKSYNCERA_FEATURES
1398+
),
13781399
account(
13791400
'ac3c225e-55a9-4236-b907-a4cccc30a2fd',
13801401
'bera',

modules/statics/src/base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export enum CoinFamily {
120120
ZEC = 'zec',
121121
ZETA = 'zeta',
122122
ZKETH = 'zketh',
123+
ZKSYNCERA = 'zksyncera', // ZkSync Era
123124
LINEAETH = 'lineaeth',
124125
IP = 'ip', // Story Chain
125126
SOMI = 'somi', // Somnia Chain
@@ -641,6 +642,7 @@ export enum UnderlyingAsset {
641642
ZETA = 'zeta',
642643
ZKETH = 'zketh',
643644

645+
ZKSYNCERA = 'zksyncera', // ZkSync Era
644646
// ERC 20 tokens
645647
'$Evmosia.com' = '$evmosia.com',
646648
'0xREVIEW' = '0xreview',

modules/statics/src/coinFeatures.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,23 @@ export const ZKETH_FEATURES = [
534534
CoinFeature.ETH_ROLLUP_CHAIN,
535535
CoinFeature.EIP1559,
536536
];
537+
538+
export const ZKSYNCERA_FEATURES = [
539+
...EVM_FEATURES,
540+
CoinFeature.SHARED_EVM_SIGNING,
541+
CoinFeature.SHARED_EVM_SDK,
542+
CoinFeature.EVM_COMPATIBLE_IMS,
543+
CoinFeature.EVM_COMPATIBLE_UI,
544+
CoinFeature.EVM_COMPATIBLE_WP,
545+
CoinFeature.SUPPORTS_ERC20,
546+
CoinFeature.EVM_NON_BITGO_RECOVERY,
547+
CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY,
548+
CoinFeature.MULTISIG_COLD,
549+
CoinFeature.MULTISIG,
550+
CoinFeature.USES_NON_PACKED_ENCODING_FOR_TXDATA,
551+
CoinFeature.ETH_ROLLUP_CHAIN,
552+
];
553+
537554
export const BERA_FEATURES = [
538555
...ETH_FEATURES,
539556
CoinFeature.TSS,

modules/statics/src/coins/ofcCoins.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,22 @@ export const ofcCoins = [
333333
),
334334
ofc('aa7e956f-2d59-4bf6-aba6-2d51bd298150', 'ofcip', 'Story', 18, UnderlyingAsset.IP, CoinKind.CRYPTO),
335335
tofc('773b02f6-32ea-493a-bca5-13d93cb0afff', 'ofctip', 'Story Testnet', 18, UnderlyingAsset.IP, CoinKind.CRYPTO),
336+
ofc(
337+
'8b50bd47-54d4-456d-a141-09f8e90df850',
338+
'ofczksyncera',
339+
'ZKSyncEra',
340+
18,
341+
UnderlyingAsset.ZKSYNCERA,
342+
CoinKind.CRYPTO
343+
),
344+
tofc(
345+
'fef4f726-0b9c-42c6-a06a-f76a33020326',
346+
'ofctzksyncera',
347+
'ZKSyncEra Testnet',
348+
18,
349+
UnderlyingAsset.ZKSYNCERA,
350+
CoinKind.CRYPTO
351+
),
336352
ofc('c5015165-6ae4-4925-bd3f-4b767feba2f9', 'ofcplume', 'Plume', 18, UnderlyingAsset.PLUME, CoinKind.CRYPTO),
337353
tofc(
338354
'7b81e4fb-0ca7-4626-8f0f-0ab36239a35f',

0 commit comments

Comments
 (0)