Skip to content

Commit 91c4dac

Browse files
committed
ci: add RTC auto-pay script
1 parent 542352c commit 91c4dac

1 file changed

Lines changed: 231 additions & 0 deletions

File tree

scripts/auto-pay.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/env python3
2+
"""
3+
RTC Auto-Pay — GitHub Actions script for automatic RTC payment on PR merge.
4+
5+
Scans PR comments for a payment directive from the repo owner, then calls
6+
the RustChain VPS transfer API and posts a confirmation comment.
7+
8+
Payment directive format (in a PR comment by repo owner):
9+
**Payment: 75 RTC**
10+
Payment: 75 RTC
11+
12+
Environment variables (set by the GitHub Action):
13+
GITHUB_TOKEN — GitHub token for API access
14+
PR_NUMBER — Pull request number
15+
REPO — Repository in "owner/repo" format
16+
PR_AUTHOR — GitHub username of the PR author
17+
RTC_VPS_HOST — RustChain VPS IP (e.g. 50.28.86.131)
18+
RTC_ADMIN_KEY — Admin key for /wallet/transfer
19+
REPO_OWNER — Repository owner username (e.g. Scottcjn)
20+
"""
21+
22+
import json
23+
import os
24+
import re
25+
import sys
26+
import time
27+
28+
import requests
29+
30+
# ---------------------------------------------------------------------------
31+
# Configuration
32+
# ---------------------------------------------------------------------------
33+
34+
GITHUB_API = "https://api.github.com"
35+
VPS_PORT = 8099
36+
FROM_WALLET = "founder_community"
37+
38+
# Payment directive pattern — matches both bold and plain variants:
39+
# **Payment: 75 RTC**
40+
# **Payment: 75.5 RTC**
41+
# Payment: 75 RTC
42+
PAYMENT_RE = re.compile(
43+
r"\*{0,2}Payment:\s*([\d]+(?:\.[\d]+)?)\s*RTC\*{0,2}",
44+
re.IGNORECASE,
45+
)
46+
47+
# Duplicate-detection: if this string appears in any comment, payment was
48+
# already processed for this PR.
49+
ALREADY_PAID_MARKER = "RTC-AutoPay-Confirmed"
50+
51+
# ---------------------------------------------------------------------------
52+
# Helpers
53+
# ---------------------------------------------------------------------------
54+
55+
56+
def env(name: str, required: bool = True) -> str:
57+
val = os.environ.get(name, "")
58+
if required and not val:
59+
print(f"::error::Missing required environment variable: {name}")
60+
sys.exit(1)
61+
return val
62+
63+
64+
def gh_headers() -> dict:
65+
return {
66+
"Authorization": f"token {env('GITHUB_TOKEN')}",
67+
"Accept": "application/vnd.github+json",
68+
"X-GitHub-Api-Version": "2022-11-28",
69+
}
70+
71+
72+
def fetch_pr_comments(repo: str, pr_number: str) -> list:
73+
"""Fetch all comments on a PR (issue comments endpoint)."""
74+
comments = []
75+
page = 1
76+
while True:
77+
url = f"{GITHUB_API}/repos/{repo}/issues/{pr_number}/comments"
78+
resp = requests.get(url, headers=gh_headers(), params={"per_page": 100, "page": page})
79+
resp.raise_for_status()
80+
batch = resp.json()
81+
if not batch:
82+
break
83+
comments.extend(batch)
84+
page += 1
85+
return comments
86+
87+
88+
def post_comment(repo: str, pr_number: str, body: str) -> None:
89+
"""Post a comment on a PR."""
90+
url = f"{GITHUB_API}/repos/{repo}/issues/{pr_number}/comments"
91+
resp = requests.post(url, headers=gh_headers(), json={"body": body})
92+
resp.raise_for_status()
93+
print(f"Posted confirmation comment on PR #{pr_number}")
94+
95+
96+
def transfer_rtc(vps_host: str, admin_key: str, to_wallet: str,
97+
amount: float, memo: str) -> dict:
98+
"""Call the RustChain VPS transfer endpoint."""
99+
url = f"http://{vps_host}:{VPS_PORT}/wallet/transfer"
100+
payload = {
101+
"from_miner": FROM_WALLET,
102+
"to_miner": to_wallet,
103+
"amount_rtc": amount,
104+
"memo": memo,
105+
}
106+
headers = {
107+
"Content-Type": "application/json",
108+
"X-Admin-Key": admin_key,
109+
}
110+
resp = requests.post(url, headers=headers, json=payload, timeout=30)
111+
resp.raise_for_status()
112+
return resp.json()
113+
114+
115+
# ---------------------------------------------------------------------------
116+
# Main
117+
# ---------------------------------------------------------------------------
118+
119+
120+
def main() -> None:
121+
repo = env("REPO")
122+
pr_number = env("PR_NUMBER")
123+
pr_author = env("PR_AUTHOR")
124+
vps_host = env("RTC_VPS_HOST")
125+
admin_key = env("RTC_ADMIN_KEY")
126+
repo_owner = env("REPO_OWNER")
127+
128+
print(f"Processing PR #{pr_number} in {repo} (author: {pr_author})")
129+
130+
# --- Fetch comments ---------------------------------------------------
131+
comments = fetch_pr_comments(repo, pr_number)
132+
print(f"Found {len(comments)} comment(s) on PR #{pr_number}")
133+
134+
# --- Check for duplicate run ------------------------------------------
135+
for c in comments:
136+
if ALREADY_PAID_MARKER in (c.get("body") or ""):
137+
print(f"Payment already processed (found {ALREADY_PAID_MARKER}). Skipping.")
138+
return
139+
140+
# --- Find payment directive from repo owner ---------------------------
141+
payment_amount = None
142+
payment_comment_id = None
143+
144+
for c in comments:
145+
author = (c.get("user") or {}).get("login", "")
146+
body = c.get("body") or ""
147+
148+
# Only accept directives from the repo owner
149+
if author.lower() != repo_owner.lower():
150+
continue
151+
152+
match = PAYMENT_RE.search(body)
153+
if match:
154+
payment_amount = float(match.group(1))
155+
payment_comment_id = c.get("id")
156+
print(f"Found payment directive: {payment_amount} RTC "
157+
f"(comment {payment_comment_id} by {author})")
158+
# Use the LAST matching directive from the owner in case of updates
159+
# (don't break — keep scanning)
160+
161+
if payment_amount is None:
162+
print("No payment directive found from repo owner. Nothing to do.")
163+
return
164+
165+
if payment_amount <= 0:
166+
print(f"::warning::Payment amount is {payment_amount} RTC — skipping.")
167+
return
168+
169+
if payment_amount > 10000:
170+
print(f"::error::Payment amount {payment_amount} RTC exceeds safety limit of 10,000 RTC. "
171+
"Process manually.")
172+
sys.exit(1)
173+
174+
# --- Determine recipient wallet ---------------------------------------
175+
# Wallet is the contributor's GitHub username
176+
to_wallet = pr_author
177+
memo = f"PR #{pr_number} in {repo} — auto-pay"
178+
179+
print(f"Initiating transfer: {payment_amount} RTC from {FROM_WALLET} to {to_wallet}")
180+
181+
# --- Call VPS transfer API --------------------------------------------
182+
try:
183+
result = transfer_rtc(vps_host, admin_key, to_wallet, payment_amount, memo)
184+
except requests.exceptions.ConnectionError as e:
185+
print(f"::error::Cannot reach VPS at {vps_host}:{VPS_PORT}{e}")
186+
sys.exit(1)
187+
except requests.exceptions.HTTPError as e:
188+
print(f"::error::VPS returned error: {e.response.status_code}{e.response.text}")
189+
sys.exit(1)
190+
except requests.exceptions.Timeout:
191+
print(f"::error::VPS request timed out after 30s")
192+
sys.exit(1)
193+
194+
ok = result.get("ok", False)
195+
pending_id = result.get("pending_id", result.get("tx_id", "n/a"))
196+
error = result.get("error", "")
197+
198+
if not ok:
199+
print(f"::error::Transfer failed: {error}")
200+
# Post failure notice so humans know
201+
fail_body = (
202+
f"**RTC Auto-Pay Failed**\n\n"
203+
f"Attempted to pay **{payment_amount} RTC** to `{to_wallet}` "
204+
f"but the transfer was rejected:\n\n"
205+
f"```\n{error}\n```\n\n"
206+
f"Please process this payment manually.\n\n"
207+
f"<!-- {ALREADY_PAID_MARKER}:FAILED -->"
208+
)
209+
post_comment(repo, pr_number, fail_body)
210+
sys.exit(1)
211+
212+
# --- Post confirmation comment ----------------------------------------
213+
confirm_body = (
214+
f"**RTC Payment Sent**\n\n"
215+
f"| Field | Value |\n"
216+
f"|-------|-------|\n"
217+
f"| Amount | **{payment_amount} RTC** |\n"
218+
f"| Recipient | `{to_wallet}` |\n"
219+
f"| From | `{FROM_WALLET}` |\n"
220+
f"| Memo | {memo} |\n"
221+
f"| pending_id | `{pending_id}` |\n\n"
222+
f"Transfer confirmed on RustChain.\n\n"
223+
f"<!-- {ALREADY_PAID_MARKER} pending_id={pending_id} -->"
224+
)
225+
post_comment(repo, pr_number, confirm_body)
226+
227+
print(f"Payment complete: {payment_amount} RTC to {to_wallet} (pending_id={pending_id})")
228+
229+
230+
if __name__ == "__main__":
231+
main()

0 commit comments

Comments
 (0)