diff --git a/.github/actions/walletkit-build-and-maestro/action.yml b/.github/actions/walletkit-build-and-maestro/action.yml index 303377100..fee88c718 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@3a145abaa0dcf9f609fabe17f6e6722e0db49535 + uses: WalletConnect/actions/maestro/pay-tests@06e4e04ca111ab27cdcb35b73d70325607510f83 - name: Install Maestro uses: WalletConnect/actions/maestro/setup@3a145abaa0dcf9f609fabe17f6e6722e0db49535 @@ -562,6 +572,26 @@ 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. Must be a Node step — + # Maestro's runScript sandbox can't sign transactions. Non-fatal: a reset hiccup + # shouldn't fail an otherwise-green run. + - name: Reset USDT Permit2 allowance (Polygon) + if: always() && contains(inputs.maestro-tags, 'pay') + shell: bash + working-directory: ${{ steps.paths.outputs.wallet_root }} + env: + WALLET_PRIVATE_KEY: ${{ inputs.wallet-private-key }} + POLYGON_RPC_URL: ${{ inputs.polygon-rpc-url }} + run: | + # walletAddress is derived from the private key (no separate var needed). + # Polygon (eip155:137) min priority fee defaults to 25 gwei in the script. + node ./scripts/revoke-permit2-approval.js \ + --chainId eip155:137 \ + --privateKey "$WALLET_PRIVATE_KEY" \ + --rpcUrl "$POLYGON_RPC_URL" || echo "::warning::Permit2 allowance reset failed; next run may skip the approve step." + - 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/wallets/rn_cli_wallet/AGENTS.md b/wallets/rn_cli_wallet/AGENTS.md index 1e56ad7f8..b2a91db43 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`) runs `scripts/revoke-permit2-approval.js` (yarn `permit2:revoke`) to reset the USDT-on-Polygon Permit2 allowance back to 0, so `pay_usdt_polygon` always re-exercises the `approve` step. This is a Node step, not a Maestro `runScript` — Maestro's script sandbox cannot sign transactions. `.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 index 72d05cb7a..63beb994c 100644 --- a/wallets/rn_cli_wallet/scripts/revoke-permit2-approval.js +++ b/wallets/rn_cli_wallet/scripts/revoke-permit2-approval.js @@ -23,13 +23,14 @@ const MIN_PRIORITY_FEE_GWEI_BY_CHAIN = { 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 ] + yarn permit2:revoke --chainId --privateKey <0x...> (--projectId | --rpcUrl ) [--walletAddress <0x...>] [--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 + yarn permit2:revoke --chainId eip155:137 --privateKey 0xYourPrivateKey --projectId yourProjectId + yarn permit2:revoke --chainId eip155:42161 --privateKey 0xYourPrivateKey --rpcUrl https://arb1.arbitrum.io/rpc Defaults: + If --walletAddress is omitted, it is derived from --privateKey. If provided, it is verified to match the key. 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). @@ -223,7 +224,9 @@ async function main() { } const chainId = normalizeChainId(args.chainId); - const walletAddress = normalizeAddress('walletAddress', args.walletAddress); + const walletAddressArg = args.walletAddress + ? normalizeAddress('walletAddress', args.walletAddress) + : null; const privateKey = normalizePrivateKey(args.privateKey); const tokenAddress = normalizeAddress( 'tokenAddress', @@ -251,11 +254,12 @@ async function main() { const signer = new ethers.Wallet(privateKey, provider); const signerAddress = ethers.utils.getAddress(signer.address); - if (signerAddress !== walletAddress) { + if (walletAddressArg && signerAddress !== walletAddressArg) { throw new Error( - `walletAddress (${walletAddress}) does not match private key (${signerAddress}).`, + `walletAddress (${walletAddressArg}) does not match private key (${signerAddress}).`, ); } + const walletAddress = signerAddress; const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer); 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)} + /> + ); })}