@layerzerolabs/lz-v2-stellar-sdk 0.2.50 → 0.2.52

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.
@@ -0,0 +1,59 @@
1
+ import { keccak_256 } from '@noble/hashes/sha3';
2
+ import * as secp from '@noble/secp256k1';
3
+
4
+ /**
5
+ * A secp256k1 key pair with private key and derived Ethereum-style address.
6
+ * Used for DVN multisig signing.
7
+ */
8
+ export class Secp256k1KeyPair {
9
+ private privateKey: Uint8Array;
10
+ public readonly ethAddress: Buffer;
11
+
12
+ constructor(privateKey: Uint8Array | Buffer | string) {
13
+ if (typeof privateKey === 'string') {
14
+ // Remove 0x prefix if present
15
+ const hex = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey;
16
+ this.privateKey = Buffer.from(hex, 'hex');
17
+ } else {
18
+ this.privateKey = new Uint8Array(privateKey);
19
+ }
20
+ this.ethAddress = this.deriveEthAddress();
21
+ }
22
+
23
+ /**
24
+ * Generate a random key pair.
25
+ */
26
+ static generate(): Secp256k1KeyPair {
27
+ const privateKey = secp.utils.randomPrivateKey();
28
+ return new Secp256k1KeyPair(privateKey);
29
+ }
30
+
31
+ /**
32
+ * Derive Ethereum-style address from the public key.
33
+ * Address = last 20 bytes of keccak256(uncompressed_pubkey[1:65])
34
+ */
35
+ private deriveEthAddress(): Buffer {
36
+ const publicKey = secp.getPublicKey(this.privateKey, false); // uncompressed
37
+ const pubkeyWithoutPrefix = publicKey.slice(1); // remove 0x04 prefix
38
+ const hash = keccak_256(pubkeyWithoutPrefix);
39
+ return Buffer.from(hash.slice(12)); // last 20 bytes
40
+ }
41
+
42
+ /**
43
+ * Sign a 32-byte digest and return a 65-byte signature (r || s || v).
44
+ */
45
+ async sign(digest: Uint8Array): Promise<Buffer> {
46
+ const [signature, recoveryId] = await secp.sign(digest, this.privateKey, {
47
+ canonical: true,
48
+ recovered: true,
49
+ der: false,
50
+ });
51
+
52
+ const v = 27 + recoveryId;
53
+ const result = Buffer.alloc(65);
54
+ result.set(signature, 0); // r (32 bytes) + s (32 bytes)
55
+ result[64] = v;
56
+
57
+ return result;
58
+ }
59
+ }
@@ -1,6 +1,6 @@
1
1
  import { Asset, Keypair, Networks } from '@stellar/stellar-sdk';
2
2
 
3
- import { Secp256k1KeyPair } from '../utils';
3
+ import { Secp256k1KeyPair } from '../secp256k1';
4
4
 
5
5
  const CORE_URL = 'http://localhost:8086';
6
6
  export const FRIENDBOT_URL = `${CORE_URL}/friendbot`;
@@ -17,6 +17,10 @@ export const ZRO_DISTRIBUTOR = Keypair.fromSecret(
17
17
  export const EXECUTOR_ADMIN = Keypair.fromSecret(
18
18
  'SACWJCNRT2AYRPBWW7IBRNI765EMZSWPXXAAHYN57UFQNOXMGET7HM5K',
19
19
  );
20
+ // Separate deployer for Chain B to enable parallel contract deployment in globalSetup
21
+ export const CHAIN_B_DEPLOYER = Keypair.fromSecret(
22
+ 'SDLIZSTG7W4C3FZYY52WIKF7FTWAXCWC5Z4OVVF3TDA3MBOR37LMIANJ',
23
+ );
20
24
 
21
25
  // DVN secp256k1 signer for multisig (deterministic key for testing)
22
26
  // Private key is keccak256("dvn_test_signer") truncated to 32 bytes
@@ -149,6 +149,7 @@ export async function deployContract<T extends { options: { contractId: string }
149
149
  deployer: Keypair,
150
150
  options: {
151
151
  salt?: Buffer;
152
+ wasmHash?: string;
152
153
  rpcUrl?: string;
153
154
  networkPassphrase?: string;
154
155
  allowHttp?: boolean;
@@ -165,14 +166,19 @@ export async function deployContract<T extends { options: { contractId: string }
165
166
  allowHttp: allowHttp,
166
167
  });
167
168
 
168
- // Step 1: Read WASM file
169
- console.log('📖 Reading WASM file from:', wasmFilePath);
170
- const wasmBuffer = readFileSync(wasmFilePath);
171
-
172
- // Step 2: Upload WASM and get hash
173
- console.log('📤 Uploading WASM...');
174
- const wasmHash = await uploadWasm(wasmBuffer, deployer, server);
175
- console.log('✅ WASM uploaded, hash:', wasmHash);
169
+ let wasmHash = options.wasmHash;
170
+ if (wasmHash) {
171
+ console.log('📦 Using pre-uploaded WASM hash:', wasmHash);
172
+ } else {
173
+ // Step 1: Read WASM file
174
+ console.log('📖 Reading WASM file from:', wasmFilePath);
175
+ const wasmBuffer = readFileSync(wasmFilePath);
176
+
177
+ // Step 2: Upload WASM and get hash
178
+ console.log('📤 Uploading WASM...');
179
+ wasmHash = await uploadWasm(wasmBuffer, deployer, server);
180
+ console.log('✅ WASM uploaded, hash:', wasmHash);
181
+ }
176
182
 
177
183
  // Step 3: Deploy the contract
178
184
  console.log('🚀 Deploying contract...');
@@ -1,3 +1,5 @@
1
+ import { Keypair, rpc } from '@stellar/stellar-sdk';
2
+ import { readFileSync } from 'fs';
1
3
  import path from 'path';
2
4
  import type { GlobalSetupContext } from 'vitest/node';
3
5
 
@@ -13,7 +15,9 @@ import { Client as PriceFeedClient } from '../../src/generated/price_feed';
13
15
  import { Client as SMLClient } from '../../src/generated/sml';
14
16
  import { Client as TreasuryClient } from '../../src/generated/treasury';
15
17
  import { Client as Uln302Client } from '../../src/generated/uln302';
18
+ import { createClient } from '../utils';
16
19
  import {
20
+ CHAIN_B_DEPLOYER,
17
21
  DEFAULT_DEPLOYER,
18
22
  DVN_SIGNER,
19
23
  DVN_VID,
@@ -21,9 +25,10 @@ import {
21
25
  EID_B,
22
26
  EXECUTOR_ADMIN,
23
27
  NATIVE_TOKEN_ADDRESS,
28
+ RPC_URL,
24
29
  ZRO_TOKEN_ADDRESS,
25
30
  } from './constants';
26
- import { deployContract } from './deploy';
31
+ import { deployContract, uploadWasm } from './deploy';
27
32
  import { startStellarLocalnet, stopStellarLocalnet } from './localnet';
28
33
 
29
34
  /**
@@ -75,21 +80,59 @@ declare module 'vitest' {
75
80
  }
76
81
  }
77
82
 
83
+ interface WasmHashes {
84
+ endpoint: string;
85
+ treasury: string;
86
+ uln302: string;
87
+ sml: string;
88
+ priceFeed: string;
89
+ executorFeeLib: string;
90
+ dvnFeeLib: string;
91
+ dvn: string;
92
+ executorHelper: string;
93
+ executor: string;
94
+ }
95
+
78
96
  /**
79
- * Deploy all protocol contracts for a single chain
97
+ * Upload all protocol WASM files once and return their hashes.
80
98
  */
81
- async function deployChainContracts(eid: number, chainLabel: string): Promise<ChainSetup> {
82
- const repoRoot = await getFullyQualifiedRepoRootPath();
83
- const wasmDir = path.join(
84
- repoRoot,
85
- 'contracts',
86
- 'protocol',
87
- 'stellar',
88
- 'target',
89
- 'wasm32v1-none',
90
- 'release',
91
- );
99
+ async function uploadAllWasms(wasmDir: string): Promise<WasmHashes> {
100
+ const server = new rpc.Server(RPC_URL, { allowHttp: true });
101
+
102
+ const wasmFiles = {
103
+ endpoint: 'endpoint_v2.wasm',
104
+ treasury: 'treasury.wasm',
105
+ uln302: 'uln302.wasm',
106
+ sml: 'simple_message_lib.wasm',
107
+ priceFeed: 'price_feed.wasm',
108
+ executorFeeLib: 'executor_fee_lib.wasm',
109
+ dvnFeeLib: 'dvn_fee_lib.wasm',
110
+ dvn: 'dvn.wasm',
111
+ executorHelper: 'executor_helper.wasm',
112
+ executor: 'executor.wasm',
113
+ };
92
114
 
115
+ const hashes: Record<string, string> = {};
116
+ for (const [name, file] of Object.entries(wasmFiles)) {
117
+ const wasmBuffer = readFileSync(path.join(wasmDir, file));
118
+ console.log(`📤 Uploading ${name} WASM (${(wasmBuffer.length / 1024).toFixed(1)} KB)...`);
119
+ hashes[name] = await uploadWasm(wasmBuffer, DEFAULT_DEPLOYER, server);
120
+ }
121
+
122
+ return hashes as unknown as WasmHashes;
123
+ }
124
+
125
+ /**
126
+ * Deploy all protocol contracts for a single chain using pre-uploaded WASM hashes.
127
+ * The deployer pays gas; contract ownership is always DEFAULT_DEPLOYER.
128
+ */
129
+ async function deployChainContracts(
130
+ eid: number,
131
+ chainLabel: string,
132
+ deployer: Keypair,
133
+ wasmDir: string,
134
+ wasmHashes: WasmHashes,
135
+ ): Promise<ChainAddresses> {
93
136
  const addresses: ChainAddresses = {
94
137
  eid,
95
138
  endpointV2: '',
@@ -106,7 +149,7 @@ async function deployChainContracts(eid: number, chainLabel: string): Promise<Ch
106
149
 
107
150
  // 1. Deploy Endpoint
108
151
  console.log(`🚀 [${chainLabel}] Deploying Endpoint (EID: ${eid})...`);
109
- const endpointClient = await deployContract<EndpointClient>(
152
+ const deployedEndpoint = await deployContract<EndpointClient>(
110
153
  EndpointClient,
111
154
  path.join(wasmDir, 'endpoint_v2.wasm'),
112
155
  {
@@ -114,25 +157,27 @@ async function deployChainContracts(eid: number, chainLabel: string): Promise<Ch
114
157
  owner: DEFAULT_DEPLOYER.publicKey(),
115
158
  native_token: NATIVE_TOKEN_ADDRESS,
116
159
  },
117
- DEFAULT_DEPLOYER,
160
+ deployer,
161
+ { wasmHash: wasmHashes.endpoint },
118
162
  );
119
- addresses.endpointV2 = endpointClient.options.contractId;
163
+ addresses.endpointV2 = deployedEndpoint.options.contractId;
120
164
  console.log(`✅ [${chainLabel}] Endpoint deployed:`, addresses.endpointV2);
121
165
 
122
166
  // 2. Deploy Treasury
123
167
  console.log(`🚀 [${chainLabel}] Deploying Treasury...`);
124
- const treasuryClient = await deployContract<TreasuryClient>(
168
+ const deployedTreasury = await deployContract<TreasuryClient>(
125
169
  TreasuryClient,
126
170
  path.join(wasmDir, 'treasury.wasm'),
127
171
  { owner: DEFAULT_DEPLOYER.publicKey() },
128
- DEFAULT_DEPLOYER,
172
+ deployer,
173
+ { wasmHash: wasmHashes.treasury },
129
174
  );
130
- addresses.treasury = treasuryClient.options.contractId;
175
+ addresses.treasury = deployedTreasury.options.contractId;
131
176
  console.log(`✅ [${chainLabel}] Treasury deployed:`, addresses.treasury);
132
177
 
133
178
  // 3. Deploy ULN302
134
179
  console.log(`🚀 [${chainLabel}] Deploying ULN302...`);
135
- const uln302Client = await deployContract<Uln302Client>(
180
+ const deployedUln302 = await deployContract<Uln302Client>(
136
181
  Uln302Client,
137
182
  path.join(wasmDir, 'uln302.wasm'),
138
183
  {
@@ -140,14 +185,15 @@ async function deployChainContracts(eid: number, chainLabel: string): Promise<Ch
140
185
  endpoint: addresses.endpointV2,
141
186
  treasury: addresses.treasury,
142
187
  },
143
- DEFAULT_DEPLOYER,
188
+ deployer,
189
+ { wasmHash: wasmHashes.uln302 },
144
190
  );
145
- addresses.uln302 = uln302Client.options.contractId;
191
+ addresses.uln302 = deployedUln302.options.contractId;
146
192
  console.log(`✅ [${chainLabel}] ULN302 deployed:`, addresses.uln302);
147
193
 
148
194
  // 4. Deploy SML (SimpleMessageLib)
149
195
  console.log(`🚀 [${chainLabel}] Deploying SimpleMessageLib...`);
150
- const smlClient = await deployContract<SMLClient>(
196
+ const deployedSml = await deployContract<SMLClient>(
151
197
  SMLClient,
152
198
  path.join(wasmDir, 'simple_message_lib.wasm'),
153
199
  {
@@ -155,50 +201,54 @@ async function deployChainContracts(eid: number, chainLabel: string): Promise<Ch
155
201
  endpoint: addresses.endpointV2,
156
202
  fee_recipient: DEFAULT_DEPLOYER.publicKey(),
157
203
  },
158
- DEFAULT_DEPLOYER,
204
+ deployer,
205
+ { wasmHash: wasmHashes.sml },
159
206
  );
160
- addresses.sml = smlClient.options.contractId;
207
+ addresses.sml = deployedSml.options.contractId;
161
208
  console.log(`✅ [${chainLabel}] SimpleMessageLib deployed:`, addresses.sml);
162
209
 
163
210
  // 5. Deploy Price Feed
164
211
  console.log(`🚀 [${chainLabel}] Deploying Price Feed...`);
165
- const priceFeedClient = await deployContract<PriceFeedClient>(
212
+ const deployedPriceFeed = await deployContract<PriceFeedClient>(
166
213
  PriceFeedClient,
167
214
  path.join(wasmDir, 'price_feed.wasm'),
168
215
  {
169
216
  owner: DEFAULT_DEPLOYER.publicKey(),
170
217
  price_updater: DEFAULT_DEPLOYER.publicKey(),
171
218
  },
172
- DEFAULT_DEPLOYER,
219
+ deployer,
220
+ { wasmHash: wasmHashes.priceFeed },
173
221
  );
174
- addresses.priceFeed = priceFeedClient.options.contractId;
222
+ addresses.priceFeed = deployedPriceFeed.options.contractId;
175
223
  console.log(`✅ [${chainLabel}] Price Feed deployed:`, addresses.priceFeed);
176
224
 
177
225
  // 6. Deploy Executor Fee Lib
178
226
  console.log(`🚀 [${chainLabel}] Deploying Executor Fee Lib...`);
179
- const executorFeeLibClient = await deployContract<ExecutorFeeLibClient>(
227
+ const deployedExecutorFeeLib = await deployContract<ExecutorFeeLibClient>(
180
228
  ExecutorFeeLibClient,
181
229
  path.join(wasmDir, 'executor_fee_lib.wasm'),
182
230
  { owner: DEFAULT_DEPLOYER.publicKey() },
183
- DEFAULT_DEPLOYER,
231
+ deployer,
232
+ { wasmHash: wasmHashes.executorFeeLib },
184
233
  );
185
- addresses.executorFeeLib = executorFeeLibClient.options.contractId;
234
+ addresses.executorFeeLib = deployedExecutorFeeLib.options.contractId;
186
235
  console.log(`✅ [${chainLabel}] Executor Fee Lib deployed:`, addresses.executorFeeLib);
187
236
 
188
237
  // 7. Deploy DVN Fee Lib
189
238
  console.log(`🚀 [${chainLabel}] Deploying DVN Fee Lib...`);
190
- const dvnFeeLibClient = await deployContract<DvnFeeLibClient>(
239
+ const deployedDvnFeeLib = await deployContract<DvnFeeLibClient>(
191
240
  DvnFeeLibClient,
192
241
  path.join(wasmDir, 'dvn_fee_lib.wasm'),
193
242
  { owner: DEFAULT_DEPLOYER.publicKey() },
194
- DEFAULT_DEPLOYER,
243
+ deployer,
244
+ { wasmHash: wasmHashes.dvnFeeLib },
195
245
  );
196
- addresses.dvnFeeLib = dvnFeeLibClient.options.contractId;
246
+ addresses.dvnFeeLib = deployedDvnFeeLib.options.contractId;
197
247
  console.log(`✅ [${chainLabel}] DVN Fee Lib deployed:`, addresses.dvnFeeLib);
198
248
 
199
249
  // 8. Deploy DVN (same signer for both chains)
200
250
  console.log(`🚀 [${chainLabel}] Deploying DVN...`);
201
- const dvnClient = await deployContract<DvnClient>(
251
+ const deployedDvn = await deployContract<DvnClient>(
202
252
  DvnClient,
203
253
  path.join(wasmDir, 'dvn.wasm'),
204
254
  {
@@ -212,25 +262,27 @@ async function deployChainContracts(eid: number, chainLabel: string): Promise<Ch
212
262
  worker_fee_lib: addresses.dvnFeeLib,
213
263
  deposit_address: DEFAULT_DEPLOYER.publicKey(),
214
264
  },
215
- DEFAULT_DEPLOYER,
265
+ deployer,
266
+ { wasmHash: wasmHashes.dvn },
216
267
  );
217
- addresses.dvn = dvnClient.options.contractId;
268
+ addresses.dvn = deployedDvn.options.contractId;
218
269
  console.log(`✅ [${chainLabel}] DVN deployed:`, addresses.dvn);
219
270
 
220
271
  // 9. Deploy Executor Helper
221
272
  console.log(`🚀 [${chainLabel}] Deploying Executor Helper...`);
222
- const executorHelperClient = await deployContract<ExecutorHelperClient>(
273
+ const deployedExecutorHelper = await deployContract<ExecutorHelperClient>(
223
274
  ExecutorHelperClient,
224
275
  path.join(wasmDir, 'executor_helper.wasm'),
225
276
  undefined,
226
- DEFAULT_DEPLOYER,
277
+ deployer,
278
+ { wasmHash: wasmHashes.executorHelper },
227
279
  );
228
- addresses.executorHelper = executorHelperClient.options.contractId;
280
+ addresses.executorHelper = deployedExecutorHelper.options.contractId;
229
281
  console.log(`✅ [${chainLabel}] Executor Helper deployed:`, addresses.executorHelper);
230
282
 
231
283
  // 10. Deploy Executor (supports both ULN302 and SML)
232
284
  console.log(`🚀 [${chainLabel}] Deploying Executor...`);
233
- const executorClient = await deployContract<ExecutorClient>(
285
+ const deployedExecutor = await deployContract<ExecutorClient>(
234
286
  ExecutorClient,
235
287
  path.join(wasmDir, 'executor.wasm'),
236
288
  {
@@ -243,13 +295,26 @@ async function deployChainContracts(eid: number, chainLabel: string): Promise<Ch
243
295
  worker_fee_lib: addresses.executorFeeLib,
244
296
  deposit_address: DEFAULT_DEPLOYER.publicKey(),
245
297
  },
246
- DEFAULT_DEPLOYER,
298
+ deployer,
299
+ { wasmHash: wasmHashes.executor },
247
300
  );
248
- addresses.executor = executorClient.options.contractId;
301
+ addresses.executor = deployedExecutor.options.contractId;
249
302
  console.log(`✅ [${chainLabel}] Executor deployed:`, addresses.executor);
250
303
 
251
- // 11. Register Executor Helper with Executor
304
+ return addresses;
305
+ }
306
+
307
+ /**
308
+ * Register executor helper and create owner-signed clients for a chain.
309
+ * Must be called sequentially (uses DEFAULT_DEPLOYER for signing).
310
+ */
311
+ async function initChainClients(
312
+ addresses: ChainAddresses,
313
+ chainLabel: string,
314
+ ): Promise<ChainSetup> {
315
+ // Register Executor Helper with Executor (needs owner-signed client)
252
316
  console.log(`🚀 [${chainLabel}] Registering Executor Helper with Executor...`);
317
+ const executorClient = createClient(ExecutorClient, addresses.executor);
253
318
  await (
254
319
  await executorClient.set_executor_helper({
255
320
  admin: DEFAULT_DEPLOYER.publicKey(),
@@ -260,16 +325,16 @@ async function deployChainContracts(eid: number, chainLabel: string): Promise<Ch
260
325
  console.log(`✅ [${chainLabel}] Executor Helper registered`);
261
326
 
262
327
  const clients: ChainClients = {
263
- endpointClient,
264
- uln302Client,
265
- smlClient,
266
- treasuryClient,
328
+ endpointClient: createClient(EndpointClient, addresses.endpointV2),
329
+ uln302Client: createClient(Uln302Client, addresses.uln302),
330
+ smlClient: createClient(SMLClient, addresses.sml),
331
+ treasuryClient: createClient(TreasuryClient, addresses.treasury),
267
332
  executorClient,
268
- executorHelperClient,
269
- executorFeeLibClient,
270
- priceFeedClient,
271
- dvnFeeLibClient,
272
- dvnClient,
333
+ executorHelperClient: createClient(ExecutorHelperClient, addresses.executorHelper),
334
+ executorFeeLibClient: createClient(ExecutorFeeLibClient, addresses.executorFeeLib),
335
+ priceFeedClient: createClient(PriceFeedClient, addresses.priceFeed),
336
+ dvnFeeLibClient: createClient(DvnFeeLibClient, addresses.dvnFeeLib),
337
+ dvnClient: createClient(DvnClient, addresses.dvn),
273
338
  };
274
339
 
275
340
  return { addresses, clients };
@@ -477,17 +542,36 @@ export default async function globalSetup({
477
542
 
478
543
  await startStellarLocalnet();
479
544
 
545
+ const repoRoot = await getFullyQualifiedRepoRootPath();
546
+ const wasmDir = path.join(
547
+ repoRoot,
548
+ 'contracts',
549
+ 'protocol',
550
+ 'stellar',
551
+ 'target',
552
+ 'wasm32v1-none',
553
+ 'release',
554
+ );
555
+
556
+ console.log('\n========================================');
557
+ console.log('📤 GLOBAL SETUP: Uploading WASM (once)');
558
+ console.log('========================================\n');
559
+
560
+ const wasmHashes = await uploadAllWasms(wasmDir);
561
+
480
562
  console.log('\n========================================');
481
- console.log('📦 GLOBAL SETUP: Deploying Protocol Contracts (Two Chains)');
563
+ console.log('📦 GLOBAL SETUP: Deploying Protocol Contracts (Two Chains in Parallel)');
482
564
  console.log('========================================\n');
483
565
 
484
- // Deploy Chain A
485
- console.log('\n--- CHAIN A (EID: ' + EID_A + ') ---');
486
- const chainA = await deployChainContracts(EID_A, 'Chain A');
566
+ // Deploy both chains in parallel (each uses its own deployer key)
567
+ const [addressesA, addressesB] = await Promise.all([
568
+ deployChainContracts(EID_A, 'Chain A', DEFAULT_DEPLOYER, wasmDir, wasmHashes),
569
+ deployChainContracts(EID_B, 'Chain B', CHAIN_B_DEPLOYER, wasmDir, wasmHashes),
570
+ ]);
487
571
 
488
- // Deploy Chain B
489
- console.log('\n--- CHAIN B (EID: ' + EID_B + ') ---');
490
- const chainB = await deployChainContracts(EID_B, 'Chain B');
572
+ // Register executor helpers and create clients sequentially (uses DEFAULT_DEPLOYER)
573
+ const chainA = await initChainClients(addressesA, 'Chain A');
574
+ const chainB = await initChainClients(addressesB, 'Chain B');
491
575
 
492
576
  console.log('\n========================================');
493
577
  console.log('🔗 GLOBAL SETUP: Wiring Protocol Contracts (Cross-Chain)');
@@ -2,6 +2,7 @@ import axios from 'axios';
2
2
  import { $, sleep } from 'zx';
3
3
 
4
4
  import {
5
+ CHAIN_B_DEPLOYER,
5
6
  DEFAULT_DEPLOYER,
6
7
  EXECUTOR_ADMIN,
7
8
  FRIENDBOT_URL,
@@ -74,33 +75,27 @@ async function waitForRpcHealth(startTime: number): Promise<void> {
74
75
  }
75
76
 
76
77
  async function waitForFriendbotAndFundAccounts(startTime: number): Promise<void> {
77
- const accountsToFund = [
78
- { keypair: DEFAULT_DEPLOYER, name: 'DEFAULT_DEPLOYER' },
79
- { keypair: ZRO_DISTRIBUTOR, name: 'ZRO_DISTRIBUTOR' },
80
- { keypair: EXECUTOR_ADMIN, name: 'EXECUTOR_ADMIN' },
81
- ];
82
-
83
- for (const { keypair, name } of accountsToFund) {
84
- let funded = false;
85
- while (Date.now() - startTime < STARTUP_TIMEOUT_MS) {
86
- try {
87
- await fundAccount(keypair.publicKey());
88
- console.log(`✅ Account ${name} (${keypair.publicKey()}) funded`);
89
- funded = true;
90
- break;
91
- } catch (_error) {
92
- const elapsed = Math.round((Date.now() - startTime) / 1000);
93
- console.log(`⏳ [${elapsed}s] Waiting for friendbot to fund ${name}...`);
94
- await sleep(RETRY_INTERVAL_MS);
95
- }
96
- }
97
- if (!funded) {
98
- throw new Error(
99
- `Failed to fund account ${name} within ${STARTUP_TIMEOUT_MS / 1000} seconds`,
100
- );
78
+ while (Date.now() - startTime < STARTUP_TIMEOUT_MS) {
79
+ try {
80
+ // Fund DEFAULT_DEPLOYER first (doubles as friendbot readiness check)
81
+ await fundAccount(DEFAULT_DEPLOYER.publicKey());
82
+ console.log('✅ Friendbot ready, DEFAULT_DEPLOYER funded');
83
+
84
+ // Fund remaining accounts in parallel
85
+ await Promise.all([
86
+ fundAccount(ZRO_DISTRIBUTOR.publicKey()),
87
+ fundAccount(EXECUTOR_ADMIN.publicKey()),
88
+ fundAccount(CHAIN_B_DEPLOYER.publicKey()),
89
+ ]);
90
+ console.log('✅ All accounts funded');
91
+ return;
92
+ } catch {
93
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
94
+ console.log(`⏳ [${elapsed}s] Waiting for friendbot...`);
95
+ await sleep(RETRY_INTERVAL_MS);
101
96
  }
102
97
  }
103
- console.log('✅ Stellar localnet started');
98
+ throw new Error(`Friendbot not ready within ${STARTUP_TIMEOUT_MS / 1000} seconds`);
104
99
  }
105
100
 
106
101
  export async function fundAccount(publicKey: string): Promise<void> {
package/test/utils.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { keccak_256 } from '@noble/hashes/sha3';
2
- import * as secp from '@noble/secp256k1';
3
2
  import {
4
3
  Account,
5
4
  Address,
@@ -153,66 +152,7 @@ export async function getTokenAuthorized(
153
152
  return false;
154
153
  }
155
154
 
156
- // ============================================================================
157
- // Secp256k1 Key Pair for DVN Multisig
158
- // ============================================================================
159
-
160
- /**
161
- * A secp256k1 key pair with private key and derived Ethereum-style address.
162
- * Used for DVN multisig signing.
163
- */
164
- export class Secp256k1KeyPair {
165
- private privateKey: Uint8Array;
166
- public readonly ethAddress: Buffer;
167
-
168
- constructor(privateKey: Uint8Array | Buffer | string) {
169
- if (typeof privateKey === 'string') {
170
- // Remove 0x prefix if present
171
- const hex = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey;
172
- this.privateKey = Buffer.from(hex, 'hex');
173
- } else {
174
- this.privateKey = new Uint8Array(privateKey);
175
- }
176
- this.ethAddress = this.deriveEthAddress();
177
- }
178
-
179
- /**
180
- * Generate a random key pair.
181
- */
182
- static generate(): Secp256k1KeyPair {
183
- const privateKey = secp.utils.randomPrivateKey();
184
- return new Secp256k1KeyPair(privateKey);
185
- }
186
-
187
- /**
188
- * Derive Ethereum-style address from the public key.
189
- * Address = last 20 bytes of keccak256(uncompressed_pubkey[1:65])
190
- */
191
- private deriveEthAddress(): Buffer {
192
- const publicKey = secp.getPublicKey(this.privateKey, false); // uncompressed
193
- const pubkeyWithoutPrefix = publicKey.slice(1); // remove 0x04 prefix
194
- const hash = keccak_256(pubkeyWithoutPrefix);
195
- return Buffer.from(hash.slice(12)); // last 20 bytes
196
- }
197
-
198
- /**
199
- * Sign a 32-byte digest and return a 65-byte signature (r || s || v).
200
- */
201
- async sign(digest: Uint8Array): Promise<Buffer> {
202
- const [signature, recoveryId] = await secp.sign(digest, this.privateKey, {
203
- canonical: true,
204
- recovered: true,
205
- der: false,
206
- });
207
-
208
- const v = 27 + recoveryId;
209
- const result = Buffer.alloc(65);
210
- result.set(signature, 0); // r (32 bytes) + s (32 bytes)
211
- result[64] = v;
212
-
213
- return result;
214
- }
215
- }
155
+ import { Secp256k1KeyPair } from './secp256k1';
216
156
 
217
157
  // ============================================================================
218
158
  // DVN Abstract Account Auth Signing