Skip to content

[CLAIM] OAuth3 Round 2 - Workflow Triggered #77

[CLAIM] OAuth3 Round 2 - Workflow Triggered

[CLAIM] OAuth3 Round 2 - Workflow Triggered #77

Workflow file for this run

# 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']
});