[CLAIM] #93
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Faucet Claim Relayer | |
| # | |
| # Triggered when someone opens an issue with [CLAIM] in the title. | |
| # Parses proof data from issue body, submits tx, comments result. | |
| # | |
| # REQUIREMENTS: | |
| # - RELAYER_PRIVATE_KEY secret: funded wallet for gas | |
| # - Faucet contract must be deployed on Base Sepolia | |
| name: Process Faucet Claim | |
| on: | |
| issues: | |
| types: [opened] | |
| permissions: | |
| issues: write | |
| jobs: | |
| process-claim: | |
| if: contains(github.event.issue.title, '[CLAIM]') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Parse claim data | |
| id: parse | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const body = context.payload.issue.body || ''; | |
| // Extract JSON from code block | |
| const jsonMatch = body.match(/```json\s*([\s\S]*?)\s*```/); | |
| if (!jsonMatch) { | |
| core.setFailed('No JSON code block found in issue body'); | |
| return; | |
| } | |
| let claim; | |
| try { | |
| claim = JSON.parse(jsonMatch[1]); | |
| } catch (e) { | |
| core.setFailed(`Invalid JSON: ${e.message}`); | |
| return; | |
| } | |
| const required = ['proof', 'inputs', 'certificateHex', 'username', 'recipient']; | |
| for (const field of required) { | |
| if (!claim[field]) { | |
| core.setFailed(`Missing required field: ${field}`); | |
| return; | |
| } | |
| } | |
| if (!/^0x[a-fA-F0-9]{40}$/.test(claim.recipient)) { | |
| core.setFailed('Invalid recipient address format'); | |
| return; | |
| } | |
| if (!/^0x[a-fA-F0-9]+$/.test(claim.proof)) { | |
| core.setFailed('Invalid proof format (must be hex)'); | |
| return; | |
| } | |
| if (!Array.isArray(claim.inputs)) { | |
| core.setFailed('inputs must be an array'); | |
| return; | |
| } | |
| core.setOutput('proof', claim.proof); | |
| core.setOutput('inputs', JSON.stringify(claim.inputs)); | |
| core.setOutput('certificate', claim.certificateHex); | |
| core.setOutput('username', claim.username); | |
| core.setOutput('recipient', claim.recipient); | |
| core.setOutput('faucet', claim.faucet || '0x72cd70d28284dD215257f73e1C5aD8e28847215B'); | |
| console.log(`Claim: ${claim.username} -> ${claim.recipient} (${claim.inputs.length} inputs, ${claim.certificateHex.length} cert hex chars)`); | |
| - name: Install Foundry | |
| uses: foundry-rs/foundry-toolchain@v1 | |
| - name: Submit claim transaction | |
| id: submit | |
| env: | |
| RELAYER_KEY: ${{ secrets.RELAYER_PRIVATE_KEY }} | |
| run: | | |
| # Format inputs as Solidity array | |
| INPUTS='${{ steps.parse.outputs.inputs }}' | |
| INPUTS_FORMATTED=$(echo "$INPUTS" | jq -r 'join(",")') | |
| echo "Submitting claim to faucet..." | |
| echo "Username: ${{ steps.parse.outputs.username }}" | |
| # Decode a 4-byte error selector into a human-readable message | |
| decode_error() { | |
| local selector="$1" | |
| case "$selector" in | |
| 832f97cb) echo "WrongCommit: proof was generated from the wrong git ref. Run the workflow from the required tag (see VERSIONS.json)." ;; | |
| 09bde339) echo "InvalidProof: ZK proof failed on-chain verification." ;; | |
| ddf32b51) echo "CertificateMismatch: sha256(certificate) doesn't match the attested artifact hash." ;; | |
| 4dca8d92) echo "UsernameMismatch: username arg doesn't match what's in the certificate." ;; | |
| c0ee95bb) echo "RecipientMismatch: recipient address doesn't match what's in the certificate." ;; | |
| 39b1a59e) echo "AlreadyClaimedToday: 24h cooldown not met. Try again tomorrow." ;; | |
| c1336f85) echo "FaucetEmpty: no ETH left in faucet." ;; | |
| *) echo "" ;; | |
| esac | |
| } | |
| set +e | |
| OUTPUT=$(cast send \ | |
| ${{ steps.parse.outputs.faucet }} \ | |
| "claim(bytes,bytes32[],bytes,string,address)" \ | |
| "${{ steps.parse.outputs.proof }}" \ | |
| "[$INPUTS_FORMATTED]" \ | |
| "${{ steps.parse.outputs.certificate }}" \ | |
| "${{ steps.parse.outputs.username }}" \ | |
| "${{ steps.parse.outputs.recipient }}" \ | |
| --private-key "$RELAYER_KEY" \ | |
| --rpc-url https://sepolia.base.org \ | |
| --gas-limit 3500000 \ | |
| --json 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "Cast output:" | |
| echo "$OUTPUT" | |
| # Try custom error selector first, then string error, then raw output | |
| SELECTOR=$(echo "$OUTPUT" | grep -oP 'custom error 0x\K[a-f0-9]{8}' | head -1) | |
| REASON=$(decode_error "$SELECTOR") | |
| if [ -z "$REASON" ]; then | |
| SELECTOR=$(echo "$OUTPUT" | grep -oP '0x\K[a-f0-9]{8}' | head -1) | |
| REASON=$(decode_error "$SELECTOR") | |
| fi | |
| if [ -z "$REASON" ]; then | |
| REASON=$(echo "$OUTPUT" | grep -oP 'Error\("\K[^"]+' || echo "$OUTPUT" | head -1) | |
| fi | |
| echo "error_reason=$REASON" >> $GITHUB_OUTPUT | |
| echo "Transaction failed: $REASON" | |
| exit 1 | |
| fi | |
| TX_HASH=$(echo "$OUTPUT" | jq -r '.transactionHash') | |
| STATUS=$(echo "$OUTPUT" | jq -r '.status') | |
| echo "tx_hash=$TX_HASH" >> $GITHUB_OUTPUT | |
| if [ "$STATUS" = "0x0" ]; then | |
| # Transaction reverted on-chain — trace to get error selector | |
| REVERT_OUTPUT=$(cast run "$TX_HASH" --rpc-url https://sepolia.base.org 2>&1 || true) | |
| SELECTOR=$(echo "$REVERT_OUTPUT" | grep -oP 'custom error 0x\K[a-f0-9]{8}' | head -1) | |
| REASON=$(decode_error "$SELECTOR") | |
| if [ -z "$REASON" ]; then | |
| REASON="Transaction reverted on-chain (tx: $TX_HASH)" | |
| else | |
| REASON="$REASON (tx: $TX_HASH)" | |
| fi | |
| echo "error_reason=$REASON" >> $GITHUB_OUTPUT | |
| echo "Transaction reverted: $REASON" | |
| exit 1 | |
| fi | |
| echo "Transaction succeeded: $TX_HASH" | |
| - name: Comment success | |
| if: success() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const txHash = '${{ steps.submit.outputs.tx_hash }}'; | |
| const recipient = '${{ steps.parse.outputs.recipient }}'; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `## ✅ Claim Successful!\n\n` + | |
| `**Recipient:** \`${recipient}\`\n` + | |
| `**Transaction:** [${txHash}](https://sepolia.basescan.org/tx/${txHash})\n\n` + | |
| `Funds should arrive shortly.` | |
| }); | |
| await github.rest.issues.update({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'closed', | |
| labels: ['claimed'] | |
| }); | |
| - name: Comment failure | |
| if: failure() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const errorReason = '${{ steps.submit.outputs.error_reason }}' || 'Unknown'; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `## ❌ Claim Failed\n\n` + | |
| `**Reason:** ${errorReason}\n\n` + | |
| `[View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})` | |
| }); | |
| await github.rest.issues.addLabels({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: ['failed'] | |
| }); |