Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions .github/actions/walletkit-build-and-maestro/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
95 changes: 95 additions & 0 deletions .github/workflows/e2e-balance-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
4 changes: 4 additions & 0 deletions wallets/rn_cli_wallet/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -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
Expand Down
16 changes: 10 additions & 6 deletions wallets/rn_cli_wallet/scripts/revoke-permit2-approval.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <eip155:chainId|chainId> --walletAddress <0x...> --privateKey <0x...> (--projectId <projectId> | --rpcUrl <url>) [--tokenAddress <0x...>] [--minPriorityFeeGwei <number>]
yarn permit2:revoke --chainId <eip155:chainId|chainId> --privateKey <0x...> (--projectId <projectId> | --rpcUrl <url>) [--walletAddress <0x...>] [--tokenAddress <0x...>] [--minPriorityFeeGwei <number>]

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).
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function LoadingView({
color="text-secondary"
center
style={styles.loadingNote}
testID="pay-loading-setup-note"
>
{note}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<OptionItem
key={option.id}
option={option}
gasCostEstimate={
optionFeeEstimatesById[option.id]?.display ?? undefined
}
isEstimatingApprovalGas={
optionFeeEstimateStatusById[option.id] === 'loading'
}
testID={`pay-option-${index}`}
renderIconRight={
<Info
testID="pay-option-info-required"
height={20}
width={20}
fill={Theme['icon-invert']}
/>
}
onIconRightPress={hasCollectData ? onInfoPress : undefined}
onPress={() => onOptionPress(option)}
/>
<View key={option.id} testID={optionTestID}>
<OptionItem
option={option}
gasCostEstimate={
optionFeeEstimatesById[option.id]?.display ?? undefined
}
isEstimatingApprovalGas={
optionFeeEstimateStatusById[option.id] === 'loading'
}
testID={`pay-option-${index}`}
renderIconRight={
<Info
testID="pay-option-info-required"
height={20}
width={20}
fill={Theme['icon-invert']}
/>
}
onIconRightPress={hasCollectData ? onInfoPress : undefined}
onPress={() => onOptionPress(option)}
/>
</View>
);
})}
</ScrollView>
Expand Down
Loading