diff --git a/examples/README.md b/examples/README.md index b7e214e..ff19d06 100644 --- a/examples/README.md +++ b/examples/README.md @@ -103,6 +103,19 @@ npm run bridge evm2stellar -- --amount 1000000 --recipient G... > **Note:** Amounts are in EVM USDC subunits (6 decimals). `1000000` = 1 USDC. +### Additional Options + +| Flag | Default | Description | +| --- | --- | --- | +| `--timeout` | `600` | Maximum seconds to wait for attestation before timing out | +| `--dryRun` | `false` | If `true`, submits the burn transaction but skips attestation polling and the receive/mint step | + +**Example with timeout and dry-run:** + +```bash +npm run bridge stellar2evm -- --amount 10000000 --timeout 300 --dryRun true +``` + --- ## File Overview diff --git a/examples/main.ts b/examples/main.ts index b409b24..3695904 100644 --- a/examples/main.ts +++ b/examples/main.ts @@ -44,6 +44,10 @@ interface ParsedArgs { fastBurn: boolean; /** Stellar strkey of the final recipient (for evm2stellar) */ recipient: string; + /** Maximum seconds to wait for attestation (default: 600) */ + timeout: number; + /** If true, validate inputs without executing transactions */ + dryRun: boolean; } // --------------------------------------------------------------------------- @@ -87,11 +91,27 @@ const getMaxFee = (amount: bigint, minimumFeeBps: number): bigint => { // Attestation // --------------------------------------------------------------------------- -const fetchAttestation = async (txHash: string, domainId: number): Promise => { - console.log("Fetching attestation..."); +const fetchAttestation = async ( + txHash: string, + domainId: number, + timeoutSeconds: number = 600, +): Promise => { + console.log(`Fetching attestation (timeout: ${timeoutSeconds}s)...`); const url = `${IRIS_API_URL}/v2/messages/${domainId}?transactionHash=${txHash}`; + const startTime = Date.now(); + const maxDurationMs = timeoutSeconds * 1000; + let attempts = 0; while (true) { + attempts++; + const elapsed = Date.now() - startTime; + if (elapsed >= maxDurationMs) { + throw new Error( + `Attestation timeout: no attestation received after ${timeoutSeconds}s (${attempts} attempts). ` + + `The attestation may still be pending — try again later with a longer --timeout.`, + ); + } + try { const response = await fetch(url); @@ -109,12 +129,13 @@ const fetchAttestation = async (txHash: string, domainId: number): Promise setTimeout(r, 5000)); continue; } - console.log("Attestation retrieved successfully!"); + console.log(`Attestation retrieved successfully after ${attempts} attempt(s)!`); return data.messages[0]; } catch (error) { const msg = error instanceof Error ? error.message : String(error); @@ -151,7 +172,7 @@ const main = async (): Promise => { const commandName = process.argv.slice(2)[0] as CommandName; const rawArgs = minimist(process.argv.slice(3), { - string: ["amount", "fastBurn", "recipient"], + string: ["amount", "fastBurn", "recipient", "timeout", "dryRun"], }); const amountStr = rawArgs.amount; @@ -160,10 +181,24 @@ const main = async (): Promise => { process.exit(1); } + const amount = BigInt(amountStr); + if (amount === 0n) { + console.error("--amount must be greater than zero (contract will reject zero amounts)"); + process.exit(1); + } + + const timeoutSeconds = rawArgs.timeout ? Number.parseInt(rawArgs.timeout, 10) : 600; + if (Number.isNaN(timeoutSeconds) || timeoutSeconds <= 0) { + console.error("--timeout must be a positive integer (seconds)"); + process.exit(1); + } + const args: ParsedArgs = { - amount: BigInt(amountStr), + amount, fastBurn: rawArgs.fastBurn === "true", recipient: rawArgs.recipient ?? "", + timeout: timeoutSeconds, + dryRun: rawArgs.dryRun === "true", }; if (!Object.values(CommandName).includes(commandName)) { @@ -173,7 +208,7 @@ const main = async (): Promise => { console.log(`Direction: ${commandName}`); console.log( - `Args: amount=${args.amount.toString()}, fastBurn=${args.fastBurn}, recipient=${args.recipient || "(default)"}`, + `Args: amount=${args.amount.toString()}, fastBurn=${args.fastBurn}, recipient=${args.recipient || "(default)"}, timeout=${args.timeout}s, dryRun=${args.dryRun}`, ); if (commandName === CommandName.StellarToEvm) { @@ -189,7 +224,12 @@ const main = async (): Promise => { const depositTxHash = await depositForBurn(args.amount, maxFee, minFinalityThreshold); console.log(`DepositForBurn Tx: ${depositTxHash}`); - const attestation = await fetchAttestation(depositTxHash, STELLAR_DOMAIN_ID); + if (args.dryRun) { + console.log("[dry-run] Skipping attestation fetch and receive_message — burn tx submitted."); + return; + } + + const attestation = await fetchAttestation(depositTxHash, STELLAR_DOMAIN_ID, args.timeout); const receiveTxHash = await receiveMessageEvm(attestation.message as Hex, attestation.attestation as Hex); console.log(`ReceiveMessage Tx: ${receiveTxHash}`); @@ -242,7 +282,12 @@ const main = async (): Promise => { ); console.log(`DepositForBurnWithHook Tx: ${depositTxHash}`); - const attestation = await fetchAttestation(depositTxHash, EVM_DOMAIN); + if (args.dryRun) { + console.log("[dry-run] Skipping attestation fetch and mint_and_forward — burn tx submitted."); + return; + } + + const attestation = await fetchAttestation(depositTxHash, EVM_DOMAIN, args.timeout); const receiveTxHash = await mintAndForward( STELLAR_CCTP_FORWARDER_ADDRESS,