Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/bitgo/test/v2/unit/keychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ describe('V2 Keychains', function () {
n.asset !== UnderlyingAsset.DOGEOS &&
n.asset !== UnderlyingAsset.MEGAETH &&
n.asset !== UnderlyingAsset.ARC &&
n.asset !== UnderlyingAsset.ZKSYNCERA &&
coinFamilyValues.includes(n.name)
);

Expand Down
227 changes: 227 additions & 0 deletions modules/sdk-coin-evm/test/integration/zksyncera.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// modules/sdk-coin-evm/test/integration/zksyncera.integration.ts
import should from 'should';
import { TransactionType } from '@bitgo/sdk-core';
import { coins, Networks, CoinFamily } from '@bitgo/statics';
import { decodeTransferData, KeyPair } from '@bitgo/abstract-eth';
import { TransactionBuilder } from '../../src/lib';

describe('ZkSync Era Transaction Builder Integration', function () {
describe('Mainnet (zksyncera)', function () {
let txBuilder: TransactionBuilder;
let key: string;
let contractAddress: string;

const testPrivateKey =
'xprv9s21ZrQH143K3vYxF8BfcG8g82bkkrf962jYfdc2SdsbXzLRcnaWAD3jWMsQaTz9ZoqD7gvYeR3fRPZy3Fr2UFXrome67sTdb66wAFmcz6G';

beforeEach(function () {
contractAddress = '0x8f977e912ef500548a0c3be6ddde9899f1199b81';
txBuilder = new TransactionBuilder(coins.get('zksyncera'));
const keyPair = new KeyPair({ prv: testPrivateKey });
key = keyPair.getKeys().prv as string;
txBuilder.fee({
fee: '1000000000',
gasLimit: '12100000',
});
txBuilder.counter(2);
txBuilder.type(TransactionType.Send);
txBuilder.contract(contractAddress);
});

it('should have correct network configuration', function () {
const coin = coins.get('zksyncera');
coin.family.should.equal(CoinFamily.ZKSYNCERA);
coin.network.should.deepEqual(Networks.main.zkSyncEra);
(coin.network as any).chainId.should.equal(324);
});

it('should build a send funds transaction with correct chainId', async function () {
const recipient = '0x19645032c7f1533395d44a629462e751084d3e4c';
const amount = '1000000000';
const expireTime = 1590066728;
const sequenceId = 5;

txBuilder
.transfer()
.amount(amount)
.to(recipient)
.expirationTime(expireTime)
.contractSequenceId(sequenceId)
.key(key);

txBuilder.sign({ key: testPrivateKey });
const tx = await txBuilder.build();

// Verify chainId is 324 (zkSync Era mainnet)
const txJson = tx.toJson();
// ChainId may be in hex or decimal format depending on implementation
const chainId = typeof txJson.chainId === 'string' ? parseInt(txJson.chainId, 16) : txJson.chainId;
chainId.should.equal(324);

// Verify transaction structure
should.equal(tx.signature.length, 2);
should.equal(tx.inputs.length, 1);
should.equal(tx.inputs[0].address, contractAddress);
should.equal(tx.inputs[0].value, amount);

should.equal(tx.outputs.length, 1);
should.equal(tx.outputs[0].address, recipient);
should.equal(tx.outputs[0].value, amount);

// Verify decoded transfer data
const data = txJson.data;
const {
to,
amount: parsedAmount,
expireTime: parsedExpireTime,
sequenceId: parsedSequenceId,
} = decodeTransferData(data);
should.equal(to, recipient);
should.equal(parsedAmount, amount);
should.equal(parsedExpireTime, expireTime);
should.equal(parsedSequenceId, sequenceId);
});

it('should build a send funds transaction with amount 0', async function () {
txBuilder
.transfer()
.amount('0')
.to('0x19645032c7f1533395d44a629462e751084d3e4c')
.expirationTime(1590066728)
.contractSequenceId(5)
.key(key);
txBuilder.sign({ key: testPrivateKey });
const tx = await txBuilder.build();

should.exist(tx.toBroadcastFormat());
tx.inputs[0].value.should.equal('0');
});

it('should use non-packed encoding for txdata (USES_NON_PACKED_ENCODING_FOR_TXDATA feature)', async function () {
const recipient = '0x19645032c7f1533395d44a629462e751084d3e4c';
const amount = '1000000000';

txBuilder.transfer().amount(amount).to(recipient).expirationTime(1590066728).contractSequenceId(5).key(key);

txBuilder.sign({ key: testPrivateKey });
const tx = await txBuilder.build();

// The transaction should be buildable and have valid data
const txJson = tx.toJson();
should.exist(txJson.data);
txJson.data.should.startWith('0x');
});

it('should be identified as ETH_ROLLUP_CHAIN', function () {
const coin = coins.get('zksyncera');
coin.features.should.containEql('eth-rollup-chain');
});
});

describe('Testnet (tzksyncera)', function () {
let txBuilder: TransactionBuilder;
let key: string;
let contractAddress: string;

const testPrivateKey =
'xprv9s21ZrQH143K3vYxF8BfcG8g82bkkrf962jYfdc2SdsbXzLRcnaWAD3jWMsQaTz9ZoqD7gvYeR3fRPZy3Fr2UFXrome67sTdb66wAFmcz6G';

beforeEach(function () {
contractAddress = '0x8f977e912ef500548a0c3be6ddde9899f1199b81';
txBuilder = new TransactionBuilder(coins.get('tzksyncera'));
const keyPair = new KeyPair({ prv: testPrivateKey });
key = keyPair.getKeys().prv as string;
txBuilder.fee({
fee: '1000000000',
gasLimit: '12100000',
});
txBuilder.counter(2);
txBuilder.type(TransactionType.Send);
txBuilder.contract(contractAddress);
});

it('should have correct testnet network configuration', function () {
const coin = coins.get('tzksyncera');
coin.family.should.equal(CoinFamily.ZKSYNCERA);
coin.network.should.deepEqual(Networks.test.zkSyncEra);
(coin.network as any).chainId.should.equal(300);
});

it('should build a send funds transaction with testnet chainId', async function () {
const recipient = '0x19645032c7f1533395d44a629462e751084d3e4c';
const amount = '1000000000';
const expireTime = 1590066728;
const sequenceId = 5;

txBuilder
.transfer()
.amount(amount)
.to(recipient)
.expirationTime(expireTime)
.contractSequenceId(sequenceId)
.key(key);

txBuilder.sign({ key: testPrivateKey });
const tx = await txBuilder.build();

// Verify chainId is 300 (zkSync Era Sepolia testnet)
const txJson = tx.toJson();
const chainId = typeof txJson.chainId === 'string' ? parseInt(txJson.chainId, 16) : txJson.chainId;
chainId.should.equal(300);

// Verify transaction structure
should.equal(tx.signature.length, 2);
should.equal(tx.inputs.length, 1);
should.equal(tx.outputs.length, 1);
});

it('should have testnet contract addresses configured', function () {
const network = Networks.test.zkSyncEra;
should.exist(network.forwarderFactoryAddress);
should.exist(network.forwarderImplementationAddress);
should.exist(network.walletFactoryAddress);
should.exist(network.walletImplementationAddress);

network.forwarderFactoryAddress.should.equal('0xdd498702f44c4da08eb9e08d3f015eefe5cb71fc');
network.walletFactoryAddress.should.equal('0x4550e1e7616d3364877fc6c9324938dab678621a');
});
});

describe('Transaction serialization and deserialization', function () {
it('should serialize and deserialize a transaction correctly', async function () {
const txBuilder = new TransactionBuilder(coins.get('tzksyncera'));
const testPrivateKey =
'xprv9s21ZrQH143K3vYxF8BfcG8g82bkkrf962jYfdc2SdsbXzLRcnaWAD3jWMsQaTz9ZoqD7gvYeR3fRPZy3Fr2UFXrome67sTdb66wAFmcz6G';
const keyPair = new KeyPair({ prv: testPrivateKey });
const key = keyPair.getKeys().prv as string;
const contractAddress = '0x8f977e912ef500548a0c3be6ddde9899f1199b81';

txBuilder.fee({
fee: '1000000000',
gasLimit: '12100000',
});
txBuilder.counter(2);
txBuilder.type(TransactionType.Send);
txBuilder.contract(contractAddress);
txBuilder
.transfer()
.amount('1000000000')
.to('0x19645032c7f1533395d44a629462e751084d3e4c')
.expirationTime(1590066728)
.contractSequenceId(5)
.key(key);
txBuilder.sign({ key: testPrivateKey });

const tx = await txBuilder.build();
const serialized = tx.toBroadcastFormat();

// Rebuild from serialized
const txBuilder2 = new TransactionBuilder(coins.get('tzksyncera'));
txBuilder2.from(serialized);
const tx2 = await txBuilder2.build();

// Should produce the same serialized output
tx2.toBroadcastFormat().should.equal(serialized);
});
});
});
6 changes: 6 additions & 0 deletions modules/sdk-core/src/bitgo/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ const mainnetBase: EnvironmentTemplate = {
xdc: {
baseUrl: 'https://api.etherscan.io/v2',
},
zksyncera: {
baseUrl: 'https://api.etherscan.io/v2',
},
},
icpNodeUrl: 'https://ic0.app',
worldExplorerBaseUrl: 'https://worldscan.org/',
Expand Down Expand Up @@ -475,6 +478,9 @@ const testnetBase: EnvironmentTemplate = {
xdc: {
baseUrl: 'https://api.etherscan.io/v2',
},
zksyncera: {
baseUrl: 'https://api.etherscan.io/v2',
},
},
stxNodeUrl: 'https://api.testnet.hiro.so',
vetNodeUrl: 'https://sync-testnet.vechain.org',
Expand Down
18 changes: 18 additions & 0 deletions modules/sdk-core/test/unit/bitgo/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import should from 'should';
import { Environments } from '../../../src';

describe('zkSync Era Environment Configuration', function () {
it('should have evm config for mainnet zksyncera', function () {
const mainnetEnv = Environments.prod;
should.exist(mainnetEnv.evm);
should.exist(mainnetEnv.evm?.zksyncera);
mainnetEnv.evm?.zksyncera.baseUrl.should.equal('https://api.etherscan.io/v2');
});

it('should have evm config for testnet zksyncera', function () {
const testnetEnv = Environments.test;
should.exist(testnetEnv.evm);
should.exist(testnetEnv.evm?.zksyncera);
testnetEnv.evm?.zksyncera.baseUrl.should.equal('https://api.etherscan.io/v2');
});
});
21 changes: 21 additions & 0 deletions modules/statics/src/allCoinsAndTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ import {
XTZ_FEATURES,
ZETA_FEATURES,
ZKETH_FEATURES,
ZKSYNCERA_FEATURES,
} from './coinFeatures';
import { botTokens } from './coins/botTokens';
import { adaTokens } from './coins/adaTokens';
Expand Down Expand Up @@ -1375,6 +1376,26 @@ export const allCoinsAndTokens = [
BaseUnit.ETH,
ZKETH_FEATURES
),
account(
'73c6f066-107a-4dcb-84e6-5a5f9dab2a1e',
'zksyncera',
'zkSync Era',
Networks.main.zkSyncEra,
18,
UnderlyingAsset.ZKSYNCERA,
BaseUnit.ETH,
ZKSYNCERA_FEATURES
),
account(
'fc901cec-26fa-4afb-830a-6793425d7064',
'tzksyncera',
'Testnet zkSync Era',
Networks.test.zkSyncEra,
18,
UnderlyingAsset.ZKSYNCERA,
BaseUnit.ETH,
ZKSYNCERA_FEATURES
),
account(
'ac3c225e-55a9-4236-b907-a4cccc30a2fd',
'bera',
Expand Down
2 changes: 2 additions & 0 deletions modules/statics/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export enum CoinFamily {
ZEC = 'zec',
ZETA = 'zeta',
ZKETH = 'zketh',
ZKSYNCERA = 'zksyncera', // ZkSync Era
LINEAETH = 'lineaeth',
IP = 'ip', // Story Chain
SOMI = 'somi', // Somnia Chain
Expand Down Expand Up @@ -641,6 +642,7 @@ export enum UnderlyingAsset {
ZETA = 'zeta',
ZKETH = 'zketh',

ZKSYNCERA = 'zksyncera', // ZkSync Era
// ERC 20 tokens
'$Evmosia.com' = '$evmosia.com',
'0xREVIEW' = '0xreview',
Expand Down
17 changes: 17 additions & 0 deletions modules/statics/src/coinFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,23 @@ export const ZKETH_FEATURES = [
CoinFeature.ETH_ROLLUP_CHAIN,
CoinFeature.EIP1559,
];

export const ZKSYNCERA_FEATURES = [
...EVM_FEATURES,
CoinFeature.SHARED_EVM_SIGNING,
CoinFeature.SHARED_EVM_SDK,
CoinFeature.EVM_COMPATIBLE_IMS,
CoinFeature.EVM_COMPATIBLE_UI,
CoinFeature.EVM_COMPATIBLE_WP,
CoinFeature.SUPPORTS_ERC20,
CoinFeature.EVM_NON_BITGO_RECOVERY,
CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY,
CoinFeature.MULTISIG_COLD,
CoinFeature.MULTISIG,
CoinFeature.USES_NON_PACKED_ENCODING_FOR_TXDATA,
CoinFeature.ETH_ROLLUP_CHAIN,
];

export const BERA_FEATURES = [
...ETH_FEATURES,
CoinFeature.TSS,
Expand Down
16 changes: 16 additions & 0 deletions modules/statics/src/coins/ofcCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,22 @@ export const ofcCoins = [
),
ofc('aa7e956f-2d59-4bf6-aba6-2d51bd298150', 'ofcip', 'Story', 18, UnderlyingAsset.IP, CoinKind.CRYPTO),
tofc('773b02f6-32ea-493a-bca5-13d93cb0afff', 'ofctip', 'Story Testnet', 18, UnderlyingAsset.IP, CoinKind.CRYPTO),
ofc(
'8b50bd47-54d4-456d-a141-09f8e90df850',
'ofczksyncera',
'ZKSyncEra',
18,
UnderlyingAsset.ZKSYNCERA,
CoinKind.CRYPTO
),
tofc(
'fef4f726-0b9c-42c6-a06a-f76a33020326',
'ofctzksyncera',
'ZKSyncEra Testnet',
18,
UnderlyingAsset.ZKSYNCERA,
CoinKind.CRYPTO
),
ofc('c5015165-6ae4-4925-bd3f-4b767feba2f9', 'ofcplume', 'Plume', 18, UnderlyingAsset.PLUME, CoinKind.CRYPTO),
tofc(
'7b81e4fb-0ca7-4626-8f0f-0ab36239a35f',
Expand Down
Loading