diff --git a/.github/actions/walletkit-build-and-maestro/action.yml b/.github/actions/walletkit-build-and-maestro/action.yml index 989d1a7ee..bbf1e8fb6 100644 --- a/.github/actions/walletkit-build-and-maestro/action.yml +++ b/.github/actions/walletkit-build-and-maestro/action.yml @@ -40,6 +40,15 @@ inputs: wallet-private-key: description: 'Test wallet private key (sets ENV_TEST_PRIVATE_KEY).' required: true + polygon-rpc-url: + description: | + Polygon RPC URL used by the Permit2 allowance-reset step. Defaults to the + public endpoint. USDT on Polygon is a plain ERC-20 (no EIP-3009/2612), so + WC Pay uses the Permit2 approve path — this lets the reset revoke that + allowance so each run re-exercises the approve step. Default avoids + polygon-rpc.com, which gates requests (HTTP 401 "tenant disabled") in CI. + required: false + default: 'https://polygon-bor-rpc.publicnode.com' merchant-api-key-single-nokyc: description: 'Partner API key for the single-option no-KYC merchant (WPAY_CUSTOMER_KEY_SINGLE_NOKYC).' required: true @@ -399,9 +408,10 @@ runs: sudo udevadm trigger --name-match=kvm # --- Common: Maestro setup + run --- - # Pinned to WalletConnect/actions master + # TEMP: pinned to the WalletConnect/actions PR #97 head (adds pay_usdt_polygon). + # Re-pin to the squash-merge commit on master once #97 lands. - name: Copy shared Pay test flows - uses: WalletConnect/actions/maestro/pay-tests@dd7c71c7292ed568c0ce6c4160ad3c04863db3a0 + uses: WalletConnect/actions/maestro/pay-tests@d07c1f8a83dde5c52b7c8649a6814f98ba716b6b - name: Install Maestro uses: WalletConnect/actions/maestro/setup@dd7c71c7292ed568c0ce6c4160ad3c04863db3a0 @@ -562,6 +572,21 @@ runs: # download+rename step if both attempts' artifacts are needed. overwrite: true + # Reset the USDT Permit2 allowance back to 0 so the next pay_usdt_polygon run + # re-exercises the approve step. Runs even when the suite failed (if: always()) + # so a mid-flow failure doesn't leave the allowance set. Uses the shared + # permit2-reset action (the private key goes via env, never the CLI). + # continue-on-error so a reset hiccup can't fail an otherwise-green run. + - name: Reset USDT Permit2 allowance (Polygon) + if: always() && contains(inputs.maestro-tags, 'pay') + continue-on-error: true + uses: WalletConnect/actions/maestro/permit2-reset@d07c1f8a83dde5c52b7c8649a6814f98ba716b6b + with: + chain-id: eip155:137 + rpc-url: ${{ inputs.polygon-rpc-url }} + token-address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F' + private-key: ${{ inputs.wallet-private-key }} + - name: Log Maestro outcome if: always() shell: bash diff --git a/.github/workflows/e2e-balance-check.yml b/.github/workflows/e2e-balance-check.yml index 25530446e..990e24a76 100644 --- a/.github/workflows/e2e-balance-check.yml +++ b/.github/workflows/e2e-balance-check.yml @@ -15,6 +15,9 @@ env: BASE_RPC: 'https://mainnet.base.org' OP_USDC: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85' OP_RPC: 'https://mainnet.optimism.io' + POLYGON_USDT: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F' + # polygon-rpc.com gates requests (HTTP 401 "tenant disabled") in CI; use publicnode. + POLYGON_RPC: 'https://polygon-bor-rpc.publicnode.com' jobs: check-balance: @@ -115,3 +118,95 @@ jobs: env.SLACK_FAUCETBOT_WEBHOOK_URL == '' run: | echo "::warning::SLACK_FAUCETBOT_WEBHOOK_URL not configured, skipping Optimism USDC alert" + + # USDT on Polygon funds the pay_usdt_polygon Permit2 flow (the actual payment). + - name: Check USDT balance on Polygon + id: poly_usdt_balance + run: | + BALANCE=$(cast call --rpc-url "$POLYGON_RPC" "$POLYGON_USDT" \ + "balanceOf(address)(uint256)" \ + "${{ vars.TEST_WALLET_ADDRESS }}" | sed 's/\[.*\]//' | tr -d '[:space:]') + echo "balance=$BALANCE" >> $GITHUB_OUTPUT + BALANCE_HUMAN=$(echo "scale=2; $BALANCE / 1000000" | bc) + echo "USDT0 on Polygon: $BALANCE_HUMAN ($BALANCE raw)" + + - name: Prepare USDT on Polygon alert + id: poly_usdt_alert + run: | + BALANCE="${{ steps.poly_usdt_balance.outputs.balance }}" + THRESHOLD="${{ vars.USDT_POLYGON_THRESHOLD_UNITS || '1000000' }}" + if [ "$BALANCE" -lt "$THRESHOLD" ]; then + BALANCE_HUMAN=$(echo "scale=2; $BALANCE / 1000000" | bc) + THRESHOLD_HUMAN=$(echo "scale=2; $THRESHOLD / 1000000" | bc) + ALERT_TEXT="[request] Can i have ${THRESHOLD_HUMAN} USDT0 on Polygon in wallet: \ + ${{ vars.TEST_WALLET_ADDRESS }}" + echo "should_alert=true" >> "$GITHUB_OUTPUT" + echo "text=$ALERT_TEXT" >> "$GITHUB_OUTPUT" + echo "::warning::$ALERT_TEXT" + fi + + - name: Send Slack alert (USDT on Polygon) + if: | + steps.poly_usdt_alert.outputs.should_alert == 'true' && + env.SLACK_FAUCETBOT_WEBHOOK_URL != '' + uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0 + with: + webhook: ${{ env.SLACK_FAUCETBOT_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": "${{ steps.poly_usdt_alert.outputs.text }}" + } + + - name: Skip Slack alert (USDT on Polygon - no webhook) + if: | + steps.poly_usdt_alert.outputs.should_alert == 'true' && + env.SLACK_FAUCETBOT_WEBHOOK_URL == '' + run: | + echo "::warning::SLACK_FAUCETBOT_WEBHOOK_URL not configured, skipping Polygon USDT alert" + + # Native POL gas on Polygon: needed for both the payment txs (approve + pay) + # and the post-test Permit2 allowance-reset tx. + - name: Check POL gas balance on Polygon + id: poly_pol_balance + run: | + BALANCE=$(cast balance --rpc-url "$POLYGON_RPC" "${{ vars.TEST_WALLET_ADDRESS }}" | tr -d '[:space:]') + echo "balance=$BALANCE" >> $GITHUB_OUTPUT + BALANCE_HUMAN=$(echo "scale=6; $BALANCE / 1000000000000000000" | bc) + echo "POL on Polygon: $BALANCE_HUMAN ($BALANCE wei)" + + - name: Prepare Polygon POL gas alert + id: poly_pol_alert + run: | + BALANCE="${{ steps.poly_pol_balance.outputs.balance }}" + THRESHOLD="${{ vars.POLYGON_POL_THRESHOLD_WEI || '1000000000000000000' }}" + # Use bc for the comparison: wei values overflow shell arithmetic. + if [ "$(echo "$BALANCE < $THRESHOLD" | bc)" -eq 1 ]; then + BALANCE_HUMAN=$(echo "scale=6; $BALANCE / 1000000000000000000" | bc) + THRESHOLD_HUMAN=$(echo "scale=6; $THRESHOLD / 1000000000000000000" | bc) + ALERT_TEXT="[request] Can i have ${THRESHOLD_HUMAN} POL (gas) on Polygon in wallet: \ + ${{ vars.TEST_WALLET_ADDRESS }} (currently ${BALANCE_HUMAN})" + echo "should_alert=true" >> "$GITHUB_OUTPUT" + echo "text=$ALERT_TEXT" >> "$GITHUB_OUTPUT" + echo "::warning::$ALERT_TEXT" + fi + + - name: Send Slack alert (Polygon POL gas) + if: | + steps.poly_pol_alert.outputs.should_alert == 'true' && + env.SLACK_FAUCETBOT_WEBHOOK_URL != '' + uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0 + with: + webhook: ${{ env.SLACK_FAUCETBOT_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": "${{ steps.poly_pol_alert.outputs.text }}" + } + + - name: Skip Slack alert (Polygon POL gas - no webhook) + if: | + steps.poly_pol_alert.outputs.should_alert == 'true' && + env.SLACK_FAUCETBOT_WEBHOOK_URL == '' + run: | + echo "::warning::SLACK_FAUCETBOT_WEBHOOK_URL not configured, skipping Polygon POL gas alert" diff --git a/.gitignore b/.gitignore index 408ec8c34..99859f983 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,11 @@ fastlane/report.xml .maestro/pay_*.yaml .maestro/flows/pay_*.yaml .maestro/scripts/ + +# Maestro local run artifacts +*.mp4 +usdt-permit2-approve-setup.png + +# Downloaded by scripts/setup-maestro-pay-tests.sh from WalletConnect/actions +# (canonical source: maestro/permit2-reset). Used locally via `yarn permit2:revoke`. +wallets/rn_cli_wallet/scripts/revoke-permit2-approval.js diff --git a/scripts/setup-maestro-pay-tests.sh b/scripts/setup-maestro-pay-tests.sh index 71996436d..1a4e5e2ff 100755 --- a/scripts/setup-maestro-pay-tests.sh +++ b/scripts/setup-maestro-pay-tests.sh @@ -51,7 +51,22 @@ cp "$SRC_DIR"/flows/pay_*.yaml "$TARGET_DIR/flows/" mkdir -p "$TARGET_DIR/scripts" cp "$SRC_DIR"/scripts/*.js "$TARGET_DIR/scripts/" +# Also fetch the canonical Permit2 reset helper so it can be run locally via +# `yarn permit2:revoke` for manual testing. The committed source of truth is the +# actions repo (maestro/permit2-reset); this copy is gitignored in the wallet. +WALLET_SCRIPTS_DIR="$ROOT_DIR/wallets/rn_cli_wallet/scripts" +REVOKE_SRC="$(find "$TMP_DIR" -type f -path '*/maestro/permit2-reset/revoke-permit2-approval.js' | head -1)" +if [ -n "$REVOKE_SRC" ]; then + mkdir -p "$WALLET_SCRIPTS_DIR" + cp "$REVOKE_SRC" "$WALLET_SCRIPTS_DIR/revoke-permit2-approval.js" +fi + echo "Pay test flows copied to $TARGET_DIR/" echo " $(count_matches "$TARGET_DIR" 'pay_*.yaml') root flows" echo " $(count_matches "$TARGET_DIR/flows" 'pay_*.yaml') sub-flows" echo " $(count_matches "$TARGET_DIR/scripts" '*.js') scripts" +if [ -n "$REVOKE_SRC" ]; then + echo " permit2 reset helper -> wallets/rn_cli_wallet/scripts/ (run: yarn permit2:revoke)" +else + echo " (permit2-reset helper not found in $REPO@$REF; skipped)" +fi diff --git a/wallets/rn_cli_wallet/AGENTS.md b/wallets/rn_cli_wallet/AGENTS.md index 1e56ad7f8..25bc4d283 100644 --- a/wallets/rn_cli_wallet/AGENTS.md +++ b/wallets/rn_cli_wallet/AGENTS.md @@ -186,6 +186,7 @@ The app uses standardized `testID` props for Maestro E2E testing. These IDs are - `.maestro/pay_single_option_nokyc.yaml`: Single payment option, no KYC — goes straight to review screen - `.maestro/pay_multiple_options_nokyc.yaml`: Multiple payment options, no KYC — option selection then review - `.maestro/pay_multiple_options_kyc.yaml`: Multiple payment options with KYC — option selection, webview KYC flow, then review +- `.maestro/pay_usdt_polygon.yaml`: USDT on Polygon — a plain ERC-20 (no EIP-3009/2612), so WC Pay uses the Permit2 path: the wallet sends an `approve` (allowance) tx then the payment tx. Best-effort observes the setup step via the `pay-loading-setup-note` testID (soft screenshot), then asserts the success screen. The allowance is reset to 0 after the run (see below) so each run re-exercises `approve`. (Note: USDT on Arbitrum is EIP-3009 / signature-based, so it never needs an on-chain approve — Polygon is used precisely because it does.) - `.maestro/flows/pay_open_and_paste_url.yaml`: Shared sub-flow — opens wallet, pastes payment URL, waits for merchant info - `.maestro/flows/pay_confirm_and_verify.yaml`: Shared sub-flow — taps Pay, verifies success screen - `.maestro/scripts/create-payment.js`: Creates a payment via the WalletConnect Pay API (called via `runScript`) @@ -204,6 +205,9 @@ When set, the wallet auto-loads this private key on startup (if no stored wallet ### CI Workflow `.github/workflows/ci_e2e_walletkit.yaml` runs Maestro tests on both iOS (simulator) and Android (emulator). Triggers on PRs/pushes to main when `wallets/rn_cli_wallet/` or `.maestro/` files change. +### Permit2 allowance reset (USDT) +After the suite runs, the composite action (`.github/actions/walletkit-build-and-maestro`) calls the shared `WalletConnect/actions/maestro/permit2-reset` action to reset the USDT-on-Polygon Permit2 allowance back to 0, so `pay_usdt_polygon` always re-exercises the `approve` step. It signs a transaction (so it's a Node step, not a Maestro `runScript`); the private key is passed via env, never the CLI. `.github/workflows/e2e-balance-check.yml` also monitors USDT + POL (gas) on Polygon and pings the faucet bot on Slack when low. + ## Development ### Prerequisites diff --git a/wallets/rn_cli_wallet/scripts/revoke-permit2-approval.js b/wallets/rn_cli_wallet/scripts/revoke-permit2-approval.js deleted file mode 100644 index 72d05cb7a..000000000 --- a/wallets/rn_cli_wallet/scripts/revoke-permit2-approval.js +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/env node - -const { ethers } = require('ethers'); - -const WALLETCONNECT_RPC_BASE_URL = 'https://rpc.walletconnect.org/v1/'; -const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; -const ERC20_ABI = ['function approve(address spender, uint256 amount)']; - -const USDT_BY_CHAIN = { - 'eip155:1': '0xdac17f958d2ee523a2206206994597c13d831ec7', - 'eip155:10': '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58', - 'eip155:137': '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', - 'eip155:8453': '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', - 'eip155:42161': '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', - 'eip155:43114': '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', - 'eip155:59144': '0xA219439258ca9da29E9Cc4cE5596924745e12B93', -}; - -const MIN_PRIORITY_FEE_GWEI_BY_CHAIN = { - 'eip155:137': '25', -}; - -function printUsage() { - const supportedChains = Object.keys(USDT_BY_CHAIN).join(', '); - console.log(`Usage: - yarn permit2:revoke --chainId --walletAddress <0x...> --privateKey <0x...> (--projectId | --rpcUrl ) [--tokenAddress <0x...>] [--minPriorityFeeGwei ] - -Example: - yarn permit2:revoke --chainId eip155:137 --walletAddress 0xYourAddress --privateKey 0xYourPrivateKey --projectId yourProjectId - yarn permit2:revoke --chainId eip155:42161 --walletAddress 0xYourAddress --privateKey 0xYourPrivateKey --rpcUrl https://arb1.arbitrum.io/rpc - -Defaults: - If --tokenAddress is omitted, the script uses the USDT address for the selected chain. - If --rpcUrl is provided it takes precedence over --projectId (use this for chains where the - WalletConnect Blockchain API gates methods like eth_blockNumber, e.g. Arbitrum). - For Polygon (eip155:137), min priority fee defaults to 25 gwei. - Supported USDT chains: ${supportedChains} -`); -} - -function parseArgs(argv) { - const args = {}; - - for (let index = 0; index < argv.length; index += 1) { - const current = argv[index]; - if (!current.startsWith('--')) { - throw new Error(`Unexpected argument "${current}"`); - } - - const eqIndex = current.indexOf('='); - if (eqIndex > -1) { - const key = current.slice(2, eqIndex); - const value = current.slice(eqIndex + 1); - args[key] = value; - continue; - } - - const key = current.slice(2); - const next = argv[index + 1]; - if (!next || next.startsWith('--')) { - args[key] = 'true'; - continue; - } - - args[key] = next; - index += 1; - } - - return args; -} - -function normalizeChainId(chainIdInput) { - if (!chainIdInput) { - throw new Error('Missing --chainId'); - } - - const value = String(chainIdInput).trim(); - if (/^\d+$/.test(value)) { - return `eip155:${value}`; - } - - if (/^eip155:\d+$/.test(value)) { - return value; - } - - throw new Error( - `Invalid chainId "${chainIdInput}". Use numeric (e.g. 137) or CAIP-2 (e.g. eip155:137).`, - ); -} - -function normalizePrivateKey(privateKeyInput) { - if (!privateKeyInput) { - throw new Error('Missing --privateKey'); - } - - const value = String(privateKeyInput).trim(); - const prefixed = value.startsWith('0x') ? value : `0x${value}`; - if (!/^0x[0-9a-fA-F]{64}$/.test(prefixed)) { - throw new Error('Invalid private key format. Expected 32-byte hex.'); - } - - return prefixed; -} - -function normalizeAddress(name, address) { - if (!address) { - throw new Error(`Missing --${name}`); - } - - try { - return ethers.utils.getAddress(String(address).trim()); - } catch { - throw new Error(`Invalid ${name}: ${address}`); - } -} - -function parseMinPriorityFeeWei(chainId, minPriorityFeeGweiArg) { - const raw = - minPriorityFeeGweiArg != null && String(minPriorityFeeGweiArg).trim() !== '' - ? String(minPriorityFeeGweiArg).trim() - : MIN_PRIORITY_FEE_GWEI_BY_CHAIN[chainId]; - - if (!raw) { - return ethers.constants.Zero; - } - - if (!/^\d+(\.\d+)?$/.test(raw)) { - throw new Error(`Invalid --minPriorityFeeGwei value: ${minPriorityFeeGweiArg}`); - } - - return ethers.utils.parseUnits(raw, 'gwei'); -} - -function extractMinTipFromError(error) { - const message = error instanceof Error ? error.message : String(error); - const match = message.match(/minimum needed (\d+)/i); - if (!match) { - return null; - } - - try { - return ethers.BigNumber.from(match[1]); - } catch { - return null; - } -} - -function getWalletConnectRpcUrl(chainId, projectId) { - if (!projectId) { - throw new Error('Missing --projectId (or pass --rpcUrl to use a custom RPC).'); - } - - return `${WALLETCONNECT_RPC_BASE_URL}?chainId=${encodeURIComponent(chainId)}&projectId=${encodeURIComponent(projectId)}`; -} - -function maxBigNumber(a, b) { - return a.gt(b) ? a : b; -} - -async function buildFeeOverrides({ - provider, - signerAddress, - txRequest, - chainId, - minPriorityFeeWei, -}) { - const gasLimit = await provider.estimateGas({ - ...txRequest, - from: signerAddress, - }); - const feeData = await provider.getFeeData(); - let baseFeePerGas = ethers.constants.Zero; - try { - const latestBlock = await provider.getBlock('latest'); - baseFeePerGas = latestBlock?.baseFeePerGas || ethers.constants.Zero; - } catch (error) { - console.log( - `Skipping baseFee read (${error instanceof Error ? error.message : String(error)}); falling back to feeData.`, - ); - } - - const hasEip1559FeeData = - !!feeData.maxFeePerGas || - !!feeData.maxPriorityFeePerGas || - baseFeePerGas.gt(0); - - if (!hasEip1559FeeData) { - const gasPrice = maxBigNumber( - feeData.gasPrice || ethers.constants.Zero, - minPriorityFeeWei, - ); - return { gasLimit, gasPrice }; - } - - const maxPriorityFeePerGas = maxBigNumber( - feeData.maxPriorityFeePerGas || ethers.constants.Zero, - minPriorityFeeWei, - ); - const minMaxFeePerGas = baseFeePerGas.gt(0) - ? baseFeePerGas.mul(2).add(maxPriorityFeePerGas) - : maxPriorityFeePerGas.mul(2); - const maxFeePerGas = maxBigNumber( - feeData.maxFeePerGas || ethers.constants.Zero, - minMaxFeePerGas, - ); - - console.log( - `fee config for ${chainId}: maxPriorityFeePerGas=${ethers.utils.formatUnits(maxPriorityFeePerGas, 'gwei')} gwei, maxFeePerGas=${ethers.utils.formatUnits(maxFeePerGas, 'gwei')} gwei`, - ); - - return { - gasLimit, - maxPriorityFeePerGas, - maxFeePerGas, - }; -} - -async function main() { - const args = parseArgs(process.argv.slice(2)); - if (args.help === 'true' || args.h === 'true') { - printUsage(); - return; - } - - const chainId = normalizeChainId(args.chainId); - const walletAddress = normalizeAddress('walletAddress', args.walletAddress); - const privateKey = normalizePrivateKey(args.privateKey); - const tokenAddress = normalizeAddress( - 'tokenAddress', - args.tokenAddress || USDT_BY_CHAIN[chainId], - ); - const minPriorityFeeWei = parseMinPriorityFeeWei( - chainId, - args.minPriorityFeeGwei, - ); - const projectId = String(args.projectId || '').trim(); - const rpcUrlOverride = String(args.rpcUrl || '').trim(); - - if (!USDT_BY_CHAIN[chainId] && !args.tokenAddress) { - throw new Error( - `No default USDT address configured for ${chainId}. Pass --tokenAddress explicitly.`, - ); - } - - const rpcUrl = rpcUrlOverride || getWalletConnectRpcUrl(chainId, projectId); - const chainIdNumber = Number(chainId.split(':')[1]); - const provider = new ethers.providers.StaticJsonRpcProvider(rpcUrl, { - chainId: chainIdNumber, - name: `eip155-${chainIdNumber}`, - }); - - const signer = new ethers.Wallet(privateKey, provider); - const signerAddress = ethers.utils.getAddress(signer.address); - if (signerAddress !== walletAddress) { - throw new Error( - `walletAddress (${walletAddress}) does not match private key (${signerAddress}).`, - ); - } - - const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer); - - console.log('Revoking Permit2 token approval...'); - console.log(`chainId: ${chainId}`); - console.log(`wallet: ${walletAddress}`); - console.log(`token: ${tokenAddress}`); - console.log(`spender: ${PERMIT2_ADDRESS}`); - - const txRequest = await token.populateTransaction.approve(PERMIT2_ADDRESS, 0); - const feeOverrides = await buildFeeOverrides({ - provider, - signerAddress, - txRequest, - chainId, - minPriorityFeeWei, - }); - - let tx; - try { - tx = await signer.sendTransaction({ - ...txRequest, - ...feeOverrides, - }); - } catch (error) { - const minTipFromRpc = extractMinTipFromError(error); - if (!minTipFromRpc) { - throw error; - } - - console.log( - `RPC rejected tip as too low. Retrying with min tip ${ethers.utils.formatUnits(minTipFromRpc, 'gwei')} gwei...`, - ); - const retryOverrides = await buildFeeOverrides({ - provider, - signerAddress, - txRequest, - chainId, - minPriorityFeeWei: maxBigNumber(minPriorityFeeWei, minTipFromRpc), - }); - tx = await signer.sendTransaction({ - ...txRequest, - ...retryOverrides, - }); - } - - console.log(`tx hash: ${tx.hash}`); - - const receipt = await tx.wait(); - if (receipt.status !== 1) { - throw new Error('Transaction reverted.'); - } - - console.log(`Revoke successful in block ${receipt.blockNumber}.`); -} - -main().catch(error => { - console.error( - `Failed to revoke Permit2 approval: ${error instanceof Error ? error.message : String(error)}`, - ); - printUsage(); - process.exit(1); -}); diff --git a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/LoadingView.tsx b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/LoadingView.tsx index 5817df293..7145838f0 100644 --- a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/LoadingView.tsx +++ b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/LoadingView.tsx @@ -89,6 +89,7 @@ export function LoadingView({ color="text-secondary" center style={styles.loadingNote} + testID="pay-loading-setup-note" > {note} diff --git a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/SelectOptionView.tsx b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/SelectOptionView.tsx index eb1634ae5..7d8f3f383 100644 --- a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/SelectOptionView.tsx +++ b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal/SelectOptionView.tsx @@ -78,28 +78,37 @@ export function SelectOptionView({ !!(option as PaymentOptionWithCollectData).collectData?.url && !collectDataCompletedIds.includes(option.id); + // Stable, network+token-keyed testID for deterministic selection + // (e.g. `pay-option-usdt-polygon`), additive to the order-dependent + // `pay-option-${index}`. Lets a test pick a specific asset+network + // when several options share a token symbol across networks. + const optionTestID = `pay-option-${`${option.amount.display.assetSymbol}-${option.amount.display.networkName}` + .toLowerCase() + .replace(/\s+/g, '-')}`; + return ( - - } - onIconRightPress={hasCollectData ? onInfoPress : undefined} - onPress={() => onOptionPress(option)} - /> + + + } + onIconRightPress={hasCollectData ? onInfoPress : undefined} + onPress={() => onOptionPress(option)} + /> + ); })}