@layerzerolabs/test-utils-stellar 0.2.122
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.
- package/LICENSE +23 -0
- package/README.md +108 -0
- package/dist/4UKUSHEU.js +30 -0
- package/dist/4UKUSHEU.js.map +1 -0
- package/dist/7DZ4SVEO.js +46 -0
- package/dist/7DZ4SVEO.js.map +1 -0
- package/dist/CYINPPER.js +293 -0
- package/dist/CYINPPER.js.map +1 -0
- package/dist/DNRE4ENZ.js +56 -0
- package/dist/DNRE4ENZ.js.map +1 -0
- package/dist/GAGBEOCZ.js +25 -0
- package/dist/GAGBEOCZ.js.map +1 -0
- package/dist/PMDPSIA2.js +149 -0
- package/dist/PMDPSIA2.js.map +1 -0
- package/dist/TYAJMKGY.js +183 -0
- package/dist/TYAJMKGY.js.map +1 -0
- package/dist/VRABOZPX.js +364 -0
- package/dist/VRABOZPX.js.map +1 -0
- package/dist/VUOMXK5T.js +6 -0
- package/dist/VUOMXK5T.js.map +1 -0
- package/dist/ZWKXKZVT.js +394 -0
- package/dist/ZWKXKZVT.js.map +1 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -0
- package/dist/deploy.d.ts +43 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +4 -0
- package/dist/deploy.js.map +1 -0
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +5 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/localnet-global-setup.d.ts +7 -0
- package/dist/localnet-global-setup.d.ts.map +1 -0
- package/dist/localnet-global-setup.js +6 -0
- package/dist/localnet-global-setup.js.map +1 -0
- package/dist/localnet.d.ts +37 -0
- package/dist/localnet.d.ts.map +1 -0
- package/dist/localnet.js +5 -0
- package/dist/localnet.js.map +1 -0
- package/dist/protocol-global-setup.d.ts +58 -0
- package/dist/protocol-global-setup.d.ts.map +1 -0
- package/dist/protocol-global-setup.js +7 -0
- package/dist/protocol-global-setup.js.map +1 -0
- package/dist/scan.d.ts +59 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +4 -0
- package/dist/scan.js.map +1 -0
- package/dist/secp256k1.d.ts +23 -0
- package/dist/secp256k1.d.ts.map +1 -0
- package/dist/secp256k1.js +4 -0
- package/dist/secp256k1.js.map +1 -0
- package/dist/utils.d.ts +54 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +5 -0
- package/dist/utils.js.map +1 -0
- package/package.json +50 -0
- package/src/client.ts +37 -0
- package/src/deploy.ts +316 -0
- package/src/env.ts +104 -0
- package/src/index.ts +40 -0
- package/src/localnet-global-setup.ts +30 -0
- package/src/localnet.ts +406 -0
- package/src/protocol-global-setup.ts +747 -0
- package/src/scan.ts +255 -0
- package/src/secp256k1.ts +59 -0
- package/src/utils.ts +642 -0
package/src/localnet.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { BASE_FEE, Operation, rpc, TransactionBuilder } from '@stellar/stellar-sdk';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
import { deployNativeSac, deployZroToken } from './deploy.js';
|
|
5
|
+
import type { StellarTestEnv } from './env.js';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_STELLAR_LOCALNET_IMAGE =
|
|
8
|
+
'438003944538.dkr.ecr.us-east-1.amazonaws.com/layerzerolabs/stellar:2026-03-02';
|
|
9
|
+
const CONTAINER_PORT = 8000;
|
|
10
|
+
export const STELLAR_LOCALNET_OWNER_LABEL = 'com.layerzerolabs.test-utils-stellar=1';
|
|
11
|
+
const STELLAR_LOCALNET_OWNER_LABEL_KEY = STELLAR_LOCALNET_OWNER_LABEL.split('=')[0];
|
|
12
|
+
|
|
13
|
+
// Timeout configuration (in milliseconds)
|
|
14
|
+
const STARTUP_TIMEOUT_MS = 120_000; // 2 minutes
|
|
15
|
+
const REQUEST_TIMEOUT_MS = 5_000;
|
|
16
|
+
export const FUNDING_RETRY_INTERVAL_MS = 2_000;
|
|
17
|
+
|
|
18
|
+
/** Docker container names: alphanumeric start, then alnum / `_` / `.` / `-`. */
|
|
19
|
+
const DOCKER_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
20
|
+
const DOCKER_IMAGE_RE =
|
|
21
|
+
/^(?!-)[a-z0-9][a-z0-9._/-]*(?::[a-zA-Z0-9][a-zA-Z0-9_.-]*)?(?:@[a-zA-Z0-9][a-zA-Z0-9_+.-]*:[a-fA-F0-9]{32,})?$/;
|
|
22
|
+
|
|
23
|
+
export interface DockerCommandResult {
|
|
24
|
+
status: number;
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Injectable so container lifecycle policy can be tested without a Docker daemon. */
|
|
30
|
+
export type DockerCommandRunner = (args: readonly string[]) => DockerCommandResult;
|
|
31
|
+
|
|
32
|
+
export interface LocalnetLifecycleOptions {
|
|
33
|
+
env: StellarTestEnv;
|
|
34
|
+
commandRunner?: DockerCommandRunner;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type NamedContainerRemovalPolicy = 'absent' | 'remove' | 'reject';
|
|
38
|
+
|
|
39
|
+
export function assertDockerArgs(env: StellarTestEnv): void {
|
|
40
|
+
if (!DOCKER_NAME_RE.test(env.CONTAINER_NAME)) {
|
|
41
|
+
throw new Error(`Invalid docker container name: ${JSON.stringify(env.CONTAINER_NAME)}`);
|
|
42
|
+
}
|
|
43
|
+
if (!Number.isInteger(env.HOST_PORT) || env.HOST_PORT < 1 || env.HOST_PORT > 65535) {
|
|
44
|
+
throw new Error(`Invalid docker host port: ${env.HOST_PORT}`);
|
|
45
|
+
}
|
|
46
|
+
const dockerImage = env.DOCKER_IMAGE ?? DEFAULT_STELLAR_LOCALNET_IMAGE;
|
|
47
|
+
if (!DOCKER_IMAGE_RE.test(dockerImage)) {
|
|
48
|
+
throw new Error(`Invalid docker image: ${JSON.stringify(dockerImage)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildDockerRunArgs(env: StellarTestEnv): string[] {
|
|
53
|
+
assertDockerArgs(env);
|
|
54
|
+
return [
|
|
55
|
+
'run',
|
|
56
|
+
'-d',
|
|
57
|
+
'--name',
|
|
58
|
+
env.CONTAINER_NAME,
|
|
59
|
+
'--label',
|
|
60
|
+
STELLAR_LOCALNET_OWNER_LABEL,
|
|
61
|
+
'-p',
|
|
62
|
+
`127.0.0.1:${env.HOST_PORT}:${CONTAINER_PORT}`,
|
|
63
|
+
env.DOCKER_IMAGE ?? DEFAULT_STELLAR_LOCALNET_IMAGE,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildDockerContainerIdLookupArgs(containerName: string): string[] {
|
|
68
|
+
return ['container', 'ls', '-aq', '--filter', `name=^/${containerName}$`];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildDockerOwnershipLabelLookupArgs(containerId: string): string[] {
|
|
72
|
+
return [
|
|
73
|
+
'container',
|
|
74
|
+
'inspect',
|
|
75
|
+
'--format',
|
|
76
|
+
`{{ index .Config.Labels "${STELLAR_LOCALNET_OWNER_LABEL_KEY}" }}`,
|
|
77
|
+
containerId,
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildDockerRemoveArgs(containerId: string): string[] {
|
|
82
|
+
return ['rm', '-f', containerId];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getNamedContainerRemovalPolicy(
|
|
86
|
+
containerId: string | undefined,
|
|
87
|
+
ownerLabel: string | undefined,
|
|
88
|
+
): NamedContainerRemovalPolicy {
|
|
89
|
+
if (containerId == null) return 'absent';
|
|
90
|
+
return ownerLabel === '1' ? 'remove' : 'reject';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const runDockerCommand: DockerCommandRunner = (args) => {
|
|
94
|
+
const result = spawnSync('docker', args, { encoding: 'utf8' });
|
|
95
|
+
if (result.error != null) {
|
|
96
|
+
throw new Error(`Unable to run docker ${args[0]}: ${result.error.message}`, {
|
|
97
|
+
cause: result.error,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
status: result.status ?? 1,
|
|
102
|
+
stdout: result.stdout ?? '',
|
|
103
|
+
stderr: result.stderr ?? '',
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function runDockerOrThrow(
|
|
108
|
+
runner: DockerCommandRunner,
|
|
109
|
+
args: readonly string[],
|
|
110
|
+
action: string,
|
|
111
|
+
): DockerCommandResult {
|
|
112
|
+
const result = runner(args);
|
|
113
|
+
if (result.status !== 0) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Docker ${action} failed (exit ${result.status}): ${result.stderr.trim() || result.stdout.trim()}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function removeOwnedContainerIfPresent(
|
|
122
|
+
env: StellarTestEnv,
|
|
123
|
+
commandRunner: DockerCommandRunner,
|
|
124
|
+
): void {
|
|
125
|
+
const containerId = runDockerOrThrow(
|
|
126
|
+
commandRunner,
|
|
127
|
+
buildDockerContainerIdLookupArgs(env.CONTAINER_NAME),
|
|
128
|
+
`container lookup for ${env.CONTAINER_NAME}`,
|
|
129
|
+
).stdout.trim();
|
|
130
|
+
const normalizedContainerId = containerId === '' ? undefined : containerId;
|
|
131
|
+
|
|
132
|
+
if (normalizedContainerId == null) return;
|
|
133
|
+
|
|
134
|
+
const ownerLabel = runDockerOrThrow(
|
|
135
|
+
commandRunner,
|
|
136
|
+
buildDockerOwnershipLabelLookupArgs(normalizedContainerId),
|
|
137
|
+
`ownership lookup for ${env.CONTAINER_NAME}`,
|
|
138
|
+
).stdout.trim();
|
|
139
|
+
const policy = getNamedContainerRemovalPolicy(normalizedContainerId, ownerLabel);
|
|
140
|
+
if (policy === 'reject') {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Refusing to remove existing container ${env.CONTAINER_NAME}: it is not owned by ${STELLAR_LOCALNET_OWNER_LABEL}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
runDockerOrThrow(
|
|
146
|
+
commandRunner,
|
|
147
|
+
buildDockerRemoveArgs(normalizedContainerId),
|
|
148
|
+
`removal of ${env.CONTAINER_NAME}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function dockerRunLocalnet(env: StellarTestEnv, commandRunner: DockerCommandRunner): void {
|
|
153
|
+
runDockerOrThrow(commandRunner, buildDockerRunArgs(env), 'localnet start');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function startStellarLocalnet({
|
|
157
|
+
env,
|
|
158
|
+
commandRunner = runDockerCommand,
|
|
159
|
+
}: LocalnetLifecycleOptions): Promise<void> {
|
|
160
|
+
assertDockerArgs(env);
|
|
161
|
+
console.log('🚀 Starting Stellar localnet...');
|
|
162
|
+
|
|
163
|
+
removeOwnedContainerIfPresent(env, commandRunner);
|
|
164
|
+
dockerRunLocalnet(env, commandRunner);
|
|
165
|
+
|
|
166
|
+
// Container is up; if any startup step fails, tear it down before rethrowing.
|
|
167
|
+
// globalSetup only registers its teardown once startup fully succeeds, so
|
|
168
|
+
// without this a failed startup (e.g. a funding flake) would leak the container.
|
|
169
|
+
try {
|
|
170
|
+
console.log('⏳ Waiting for Stellar RPC to be healthy...');
|
|
171
|
+
await waitForRpcHealth(env);
|
|
172
|
+
console.log('✅ Stellar RPC is healthy');
|
|
173
|
+
|
|
174
|
+
await fundTestAccounts(env);
|
|
175
|
+
await deployNativeSac(env);
|
|
176
|
+
await deployZroToken(env);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
await stopStellarLocalnet({ env, commandRunner });
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Serialization queue for fundAccount — the junk wallet can only have one pending tx at a time.
|
|
184
|
+
// Chaining onto this promise ensures concurrent calls are sequenced correctly.
|
|
185
|
+
let fundAccountQueue: Promise<void> = Promise.resolve();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Fund a single account from the junk wallet (for ad-hoc test accounts).
|
|
189
|
+
* Calls are serialized to avoid sequence number conflicts on the junk wallet.
|
|
190
|
+
*/
|
|
191
|
+
export function fundAccount(env: StellarTestEnv, publicKey: string): Promise<void> {
|
|
192
|
+
const result = fundAccountQueue.then(() => fundAccountInternal(env, publicKey));
|
|
193
|
+
fundAccountQueue = result.catch(() => {}); // keep queue alive if this call fails
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function fundAccountInternal(env: StellarTestEnv, publicKey: string): Promise<void> {
|
|
198
|
+
const MAX_RETRIES = 5;
|
|
199
|
+
let lastError: unknown;
|
|
200
|
+
|
|
201
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
202
|
+
let transactionSubmitted = false;
|
|
203
|
+
let getDestinationAccount: ((accountId: string) => Promise<unknown>) | undefined;
|
|
204
|
+
try {
|
|
205
|
+
const server = new rpc.Server(env.RPC_URL, { allowHttp: true });
|
|
206
|
+
getDestinationAccount = (accountId) => server.getAccount(accountId);
|
|
207
|
+
// Re-fetch account on every attempt to get the latest sequence number
|
|
208
|
+
const junkAccount = await server.getAccount(env.JUNK_WALLET.publicKey());
|
|
209
|
+
|
|
210
|
+
const tx = new TransactionBuilder(junkAccount, {
|
|
211
|
+
fee: BASE_FEE,
|
|
212
|
+
networkPassphrase: env.NETWORK_PASSPHRASE,
|
|
213
|
+
})
|
|
214
|
+
.addOperation(
|
|
215
|
+
Operation.createAccount({
|
|
216
|
+
destination: publicKey,
|
|
217
|
+
startingBalance: '100',
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
.setTimeout(30)
|
|
221
|
+
.build();
|
|
222
|
+
|
|
223
|
+
tx.sign(env.JUNK_WALLET);
|
|
224
|
+
|
|
225
|
+
const sendResult = await server.sendTransaction(tx);
|
|
226
|
+
if (sendResult.status !== 'PENDING') {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Failed to fund account ${publicKey}: ${JSON.stringify(sendResult)}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
transactionSubmitted = true;
|
|
232
|
+
|
|
233
|
+
const txResult = await server.pollTransaction(sendResult.hash);
|
|
234
|
+
if (txResult.status !== 'SUCCESS') {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Account funding failed for ${publicKey}: ${JSON.stringify(txResult)}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return; // success
|
|
241
|
+
} catch (err) {
|
|
242
|
+
lastError = err;
|
|
243
|
+
if (
|
|
244
|
+
transactionSubmitted &&
|
|
245
|
+
getDestinationAccount != null &&
|
|
246
|
+
(await accountExists(getDestinationAccount, publicKey))
|
|
247
|
+
) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (attempt < MAX_RETRIES) {
|
|
251
|
+
await new Promise((resolve) => setTimeout(resolve, FUNDING_RETRY_INTERVAL_MS));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Failed to fund account ${publicKey} after ${MAX_RETRIES} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Returns true when a destination account was created despite an ambiguous transaction poll. */
|
|
262
|
+
export async function accountExists(
|
|
263
|
+
getAccount: (accountId: string) => Promise<unknown>,
|
|
264
|
+
accountId: string,
|
|
265
|
+
): Promise<boolean> {
|
|
266
|
+
try {
|
|
267
|
+
await getAccount(accountId);
|
|
268
|
+
return true;
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Back off between fundTestAccounts rebuilds so a lagging localnet can confirm
|
|
276
|
+
* the previous attempt before we burn another try.
|
|
277
|
+
*/
|
|
278
|
+
export async function pauseBeforeFundingRetry(
|
|
279
|
+
attempt: number,
|
|
280
|
+
maxAttempts: number,
|
|
281
|
+
sleep: (ms: number) => Promise<void> = (ms) =>
|
|
282
|
+
new Promise((resolve) => setTimeout(resolve, ms)),
|
|
283
|
+
intervalMs: number = FUNDING_RETRY_INTERVAL_MS,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
if (attempt < maxAttempts) {
|
|
286
|
+
await sleep(intervalMs);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function waitForRpcHealth(env: StellarTestEnv): Promise<void> {
|
|
291
|
+
const startTime = Date.now();
|
|
292
|
+
while (Date.now() - startTime < STARTUP_TIMEOUT_MS) {
|
|
293
|
+
try {
|
|
294
|
+
const response = await fetch(env.RPC_URL, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getHealth' }),
|
|
298
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
299
|
+
});
|
|
300
|
+
const data = (await response.json()) as { result?: { status?: string } };
|
|
301
|
+
if (data.result?.status === 'healthy') return;
|
|
302
|
+
} catch {
|
|
303
|
+
// Not ready yet
|
|
304
|
+
}
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, FUNDING_RETRY_INTERVAL_MS));
|
|
306
|
+
}
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Stellar RPC failed to become healthy within ${STARTUP_TIMEOUT_MS / 1000} seconds`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function fundTestAccounts(env: StellarTestEnv): Promise<void> {
|
|
313
|
+
console.log('💰 Funding test accounts from junk wallet...');
|
|
314
|
+
const server = new rpc.Server(env.RPC_URL, { allowHttp: true });
|
|
315
|
+
|
|
316
|
+
// A funded DEFAULT_DEPLOYER is the signal that funding already landed — lets us
|
|
317
|
+
// short-circuit when a prior attempt's tx confirmed after the poll gave up.
|
|
318
|
+
const alreadyFunded = async (): Promise<boolean> => {
|
|
319
|
+
try {
|
|
320
|
+
await server.getAccount(env.DEFAULT_DEPLOYER.publicKey());
|
|
321
|
+
return true;
|
|
322
|
+
} catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Running localnets concurrently makes the chain lag, so a funding tx can outrun
|
|
328
|
+
// the poll window or expire (NOT_FOUND). Use a longer validity + poll window, and
|
|
329
|
+
// rebuild-and-retry a bounded number of times before giving up.
|
|
330
|
+
const FUNDING_ATTEMPTS = 3;
|
|
331
|
+
for (let attempt = 1; attempt <= FUNDING_ATTEMPTS; attempt++) {
|
|
332
|
+
const junkAccount = await server.getAccount(env.JUNK_WALLET.publicKey());
|
|
333
|
+
const tx = new TransactionBuilder(junkAccount, {
|
|
334
|
+
fee: BASE_FEE,
|
|
335
|
+
networkPassphrase: env.NETWORK_PASSPHRASE,
|
|
336
|
+
})
|
|
337
|
+
.addOperation(
|
|
338
|
+
Operation.createAccount({
|
|
339
|
+
destination: env.DEFAULT_DEPLOYER.publicKey(),
|
|
340
|
+
startingBalance: '2000',
|
|
341
|
+
}),
|
|
342
|
+
)
|
|
343
|
+
.addOperation(
|
|
344
|
+
Operation.createAccount({
|
|
345
|
+
destination: env.ZRO_DISTRIBUTOR.publicKey(),
|
|
346
|
+
startingBalance: '2000',
|
|
347
|
+
}),
|
|
348
|
+
)
|
|
349
|
+
.addOperation(
|
|
350
|
+
Operation.createAccount({
|
|
351
|
+
destination: env.EXECUTOR_ADMIN.publicKey(),
|
|
352
|
+
startingBalance: '2000',
|
|
353
|
+
}),
|
|
354
|
+
)
|
|
355
|
+
.addOperation(
|
|
356
|
+
Operation.createAccount({
|
|
357
|
+
destination: env.CHAIN_B_DEPLOYER.publicKey(),
|
|
358
|
+
startingBalance: '2000',
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
.setTimeout(120)
|
|
362
|
+
.build();
|
|
363
|
+
|
|
364
|
+
tx.sign(env.JUNK_WALLET);
|
|
365
|
+
|
|
366
|
+
const sendResult = await server.sendTransaction(tx);
|
|
367
|
+
if (sendResult.status === 'PENDING') {
|
|
368
|
+
const txResult = await server.pollTransaction(sendResult.hash, { attempts: 60 });
|
|
369
|
+
if (txResult.status === 'SUCCESS') {
|
|
370
|
+
console.log('✅ All test accounts funded');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Send rejected or poll didn't confirm — but on a lagging chain the tx may
|
|
376
|
+
// have landed anyway. If the accounts now exist, funding is effectively done.
|
|
377
|
+
if (await alreadyFunded()) {
|
|
378
|
+
console.log('✅ All test accounts funded (confirmed after poll window)');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.warn(
|
|
383
|
+
`⚠️ Funding attempt ${attempt}/${FUNDING_ATTEMPTS} did not confirm; retrying…`,
|
|
384
|
+
);
|
|
385
|
+
// Match fundAccountInternal: give a lagging localnet time to confirm before
|
|
386
|
+
// rebuilding/retrying, otherwise we can burn all attempts while the first tx lands.
|
|
387
|
+
await pauseBeforeFundingRetry(attempt, FUNDING_ATTEMPTS);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
throw new Error(`Account funding failed to confirm after ${FUNDING_ATTEMPTS} attempts`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function stopStellarLocalnet({
|
|
394
|
+
env,
|
|
395
|
+
commandRunner = runDockerCommand,
|
|
396
|
+
}: LocalnetLifecycleOptions): Promise<void> {
|
|
397
|
+
// Guarded (like the startup docker rm -f) so a cleanup failure never throws —
|
|
398
|
+
// otherwise it would mask the original error when called from a startup catch.
|
|
399
|
+
try {
|
|
400
|
+
assertDockerArgs(env);
|
|
401
|
+
removeOwnedContainerIfPresent(env, commandRunner);
|
|
402
|
+
console.log('✅ Stellar localnet stopped');
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.warn(`⚠️ Failed to remove container ${env.CONTAINER_NAME}:`, err);
|
|
405
|
+
}
|
|
406
|
+
}
|