skillpp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPATIBILITY.md +58 -0
- package/LICENSE +21 -0
- package/README.md +307 -0
- package/README.zh-CN.md +307 -0
- package/SKILL.md +490 -0
- package/adapters/binance-ai.md +22 -0
- package/adapters/claude.md +21 -0
- package/adapters/gemini.md +26 -0
- package/adapters/gpt.md +28 -0
- package/adapters/kimi.md +26 -0
- package/adapters/mimo.md +22 -0
- package/adapters/openclaw.md +29 -0
- package/assets/skillpp-banner.png +0 -0
- package/package.json +59 -0
- package/pipelines.md +310 -0
- package/prompts/newbie-mode.md +48 -0
- package/prompts/router-prompt.md +32 -0
- package/prompts/universal-system-prompt.md +41 -0
- package/registry.md +209 -0
- package/rules.md +323 -0
- package/schemas/audit.schema.json +67 -0
- package/schemas/checkpoint.schema.json +86 -0
- package/schemas/handoff.schema.json +82 -0
- package/schemas/token.schema.json +36 -0
- package/scripts/compatibility-check.mjs +130 -0
- package/scripts/selftest.mjs +384 -0
- package/scripts/skillpp.mjs +448 -0
- package/scripts/validate-skillpp.mjs +140 -0
- package/skillpp.manifest.json +714 -0
- package/skills/audit-plus/SKILL.md +612 -0
- package/skills/binance/binance/CHANGELOG.md +112 -0
- package/skills/binance/binance/LICENSE.md +9 -0
- package/skills/binance/binance/SKILL.md +69 -0
- package/skills/binance/binance/references/algo.md +21 -0
- package/skills/binance/binance/references/alpha.md +9 -0
- package/skills/binance/binance/references/auth.md +32 -0
- package/skills/binance/binance/references/c2c.md +5 -0
- package/skills/binance/binance/references/convert.md +19 -0
- package/skills/binance/binance/references/copy-trading.md +6 -0
- package/skills/binance/binance/references/crypto-loan.md +27 -0
- package/skills/binance/binance/references/derivatives-options-streams.md +25 -0
- package/skills/binance/binance/references/derivatives-options.md +85 -0
- package/skills/binance/binance/references/derivatives-portfolio-margin-pro-streams.md +5 -0
- package/skills/binance/binance/references/derivatives-portfolio-margin-pro.md +34 -0
- package/skills/binance/binance/references/derivatives-portfolio-margin-streams.md +5 -0
- package/skills/binance/binance/references/derivatives-portfolio-margin.md +146 -0
- package/skills/binance/binance/references/dual-investment.md +15 -0
- package/skills/binance/binance/references/fiat.md +9 -0
- package/skills/binance/binance/references/futures-coin-streams.md +29 -0
- package/skills/binance/binance/references/futures-coin.md +109 -0
- package/skills/binance/binance/references/futures-usds-streams.md +35 -0
- package/skills/binance/binance/references/futures-usds.md +144 -0
- package/skills/binance/binance/references/gift-card.md +10 -0
- package/skills/binance/binance/references/margin-trading-streams.md +6 -0
- package/skills/binance/binance/references/margin-trading.md +101 -0
- package/skills/binance/binance/references/mining.md +17 -0
- package/skills/binance/binance/references/pay.md +5 -0
- package/skills/binance/binance/references/rebate.md +5 -0
- package/skills/binance/binance/references/simple-earn.md +56 -0
- package/skills/binance/binance/references/spot-streams.md +25 -0
- package/skills/binance/binance/references/spot.md +114 -0
- package/skills/binance/binance/references/staking.md +59 -0
- package/skills/binance/binance/references/sub-account.md +67 -0
- package/skills/binance/binance/references/vip-loan.md +27 -0
- package/skills/binance/binance/references/wallet.md +75 -0
- package/skills/binance/fiat/CHANGELOG.md +11 -0
- package/skills/binance/fiat/LICENSE.md +9 -0
- package/skills/binance/fiat/SKILL.md +169 -0
- package/skills/binance/fiat/references/authentication.md +126 -0
- package/skills/binance/fiat/references/sapi-endpoints.md +217 -0
- package/skills/binance/onchain-pay/.local.md.example +10 -0
- package/skills/binance/onchain-pay/CHANGELOG.md +20 -0
- package/skills/binance/onchain-pay/LICENSE.md +9 -0
- package/skills/binance/onchain-pay/SKILL.md +466 -0
- package/skills/binance/onchain-pay/references/authentication.md +92 -0
- package/skills/binance/onchain-pay/scripts/sign_and_call.sh +52 -0
- package/skills/binance/p2p/CHANGELOG.md +33 -0
- package/skills/binance/p2p/LICENSE.md +9 -0
- package/skills/binance/p2p/SKILL.md +1082 -0
- package/skills/binance/p2p/references/agent-sapi-api.md +795 -0
- package/skills/binance/p2p/references/authentication.md +100 -0
- package/skills/binance/payment/SKILL.md +824 -0
- package/skills/binance/payment/common.py +560 -0
- package/skills/binance/payment/payment_skill.py +86 -0
- package/skills/binance/payment/receive.py +109 -0
- package/skills/binance/payment/references/setup-guide.md +77 -0
- package/skills/binance/payment/requirements.txt +4 -0
- package/skills/binance/payment/send.py +952 -0
- package/skills/binance/payment/send_extension/__init__.py +43 -0
- package/skills/binance/payment/send_extension/base.py +48 -0
- package/skills/binance/payment/send_extension/c2c.py +193 -0
- package/skills/binance/payment/send_extension/pix.py +316 -0
- package/skills/binance/square-post/README.md +62 -0
- package/skills/binance/square-post/SKILL.md +171 -0
- package/skills/binance/square-post/scripts/lib.mjs +175 -0
- package/skills/binance/square-post/scripts/post-image.mjs +80 -0
- package/skills/binance/square-post/scripts/post-text.mjs +41 -0
- package/skills/binance/square-post/scripts/post-video.mjs +110 -0
- package/skills/binance/square-post/scripts/save-key.mjs +34 -0
- package/skills/binance-web3/binance-agentic-wallet/SKILL.md +150 -0
- package/skills/binance-web3/binance-agentic-wallet/references/authentication.md +136 -0
- package/skills/binance-web3/binance-agentic-wallet/references/limit-order.md +204 -0
- package/skills/binance-web3/binance-agentic-wallet/references/market-order.md +179 -0
- package/skills/binance-web3/binance-agentic-wallet/references/prediction.md +489 -0
- package/skills/binance-web3/binance-agentic-wallet/references/preflight.md +66 -0
- package/skills/binance-web3/binance-agentic-wallet/references/security.md +47 -0
- package/skills/binance-web3/binance-agentic-wallet/references/send.md +53 -0
- package/skills/binance-web3/binance-agentic-wallet/references/wallet-setting.md +86 -0
- package/skills/binance-web3/binance-agentic-wallet/references/wallet-view.md +312 -0
- package/skills/binance-web3/binance-agentic-wallet/references/x402-payment.md +259 -0
- package/skills/binance-web3/binance-tokenized-securities-info/SKILL.md +613 -0
- package/skills/binance-web3/crypto-market-rank/SKILL.md +91 -0
- package/skills/binance-web3/crypto-market-rank/references/cli.md +219 -0
- package/skills/binance-web3/crypto-market-rank/scripts/cli.mjs +149 -0
- package/skills/binance-web3/meme-rush/SKILL.md +72 -0
- package/skills/binance-web3/meme-rush/references/cli.md +158 -0
- package/skills/binance-web3/meme-rush/scripts/cli.mjs +101 -0
- package/skills/binance-web3/query-address-info/SKILL.md +61 -0
- package/skills/binance-web3/query-address-info/references/cli.md +56 -0
- package/skills/binance-web3/query-address-info/scripts/cli.mjs +132 -0
- package/skills/binance-web3/query-token-audit/SKILL.md +162 -0
- package/skills/binance-web3/query-token-info/SKILL.md +83 -0
- package/skills/binance-web3/query-token-info/references/cli.md +135 -0
- package/skills/binance-web3/query-token-info/scripts/cli.mjs +112 -0
- package/skills/binance-web3/trading-signal/SKILL.md +66 -0
- package/skills/binance-web3/trading-signal/references/cli.md +90 -0
- package/skills/binance-web3/trading-signal/scripts/cli.mjs +92 -0
- package/skills/four-meme/four-guard/API-Contract-TaxToken.md +277 -0
- package/skills/four-meme/four-guard/API-CreateToken.02-02-2026.md +285 -0
- package/skills/four-meme/four-guard/API-Documents.03-03-2026.md +789 -0
- package/skills/four-meme/four-guard/AgentIdentifier.abi +585 -0
- package/skills/four-meme/four-guard/README.md +21 -0
- package/skills/four-meme/four-guard/SKILL.md +31 -0
- package/skills/four-meme/four-guard/TaxToken.abi +969 -0
- package/skills/four-meme/four-guard/TokenIdentifierSample.js_ +81 -0
- package/skills/four-meme/four-guard/TokenIdentifierSample.sol +69 -0
- package/skills/four-meme/four-guard/TokenManager.lite.abi +836 -0
- package/skills/four-meme/four-guard/TokenManager2.lite.abi +2325 -0
- package/skills/four-meme/four-guard/TokenManagerHelper3.abi +999 -0
- package/skills/four-meme/four-guard/go.mod +36 -0
- package/skills/four-meme/four-guard/go.sum +127 -0
- package/skills/four-meme/four-guard/main.go +183 -0
- package/skills/four-meme/four-meme-ai/SKILL.md +31 -0
- package/skills/four-meme/four-meme-ai/references/agent-creator-and-wallets.md +87 -0
- package/skills/four-meme/four-meme-ai/references/api-create-token.md +55 -0
- package/skills/four-meme/four-meme-ai/references/contract-addresses.md +47 -0
- package/skills/four-meme/four-meme-ai/references/create-token-scripts.md +131 -0
- package/skills/four-meme/four-meme-ai/references/errors.md +29 -0
- package/skills/four-meme/four-meme-ai/references/event-listening.md +75 -0
- package/skills/four-meme/four-meme-ai/references/execute-trade.md +31 -0
- package/skills/four-meme/four-meme-ai/references/tax-token-query.md +38 -0
- package/skills/four-meme/four-meme-ai/references/token-query-api.md +44 -0
- package/skills/four-meme/four-meme-ai/references/token-tax-info.md +77 -0
- package/skills/four-meme/four-meme-ai/scripts/8004-balance.ts +52 -0
- package/skills/four-meme/four-meme-ai/scripts/8004-register.ts +108 -0
- package/skills/four-meme/four-meme-ai/scripts/create-token-api.ts +321 -0
- package/skills/four-meme/four-meme-ai/scripts/create-token-chain.ts +102 -0
- package/skills/four-meme/four-meme-ai/scripts/create-token-instant.ts +106 -0
- package/skills/four-meme/four-meme-ai/scripts/execute-buy.ts +198 -0
- package/skills/four-meme/four-meme-ai/scripts/execute-sell.ts +150 -0
- package/skills/four-meme/four-meme-ai/scripts/get-public-config.ts +25 -0
- package/skills/four-meme/four-meme-ai/scripts/get-recent-events.ts +76 -0
- package/skills/four-meme/four-meme-ai/scripts/get-tax-token-info.ts +69 -0
- package/skills/four-meme/four-meme-ai/scripts/get-token-info.ts +94 -0
- package/skills/four-meme/four-meme-ai/scripts/quote-buy.ts +85 -0
- package/skills/four-meme/four-meme-ai/scripts/quote-sell.ts +66 -0
- package/skills/four-meme/four-meme-ai/scripts/send-token.ts +98 -0
- package/skills/four-meme/four-meme-ai/scripts/token-get.ts +31 -0
- package/skills/four-meme/four-meme-ai/scripts/token-list.ts +134 -0
- package/skills/four-meme/four-meme-ai/scripts/token-rankings.ts +162 -0
- package/skills/four-meme/four-meme-ai/scripts/verify-events.ts +47 -0
- package/skills/four-meme/four-meme-integration/SKILL.md +374 -0
- package/skills/four-meme/four-meme-integration/references/agent-creator-and-wallets.md +87 -0
- package/skills/four-meme/four-meme-integration/references/api-create-token.md +55 -0
- package/skills/four-meme/four-meme-integration/references/contract-addresses.md +47 -0
- package/skills/four-meme/four-meme-integration/references/create-token-scripts.md +131 -0
- package/skills/four-meme/four-meme-integration/references/errors.md +29 -0
- package/skills/four-meme/four-meme-integration/references/event-listening.md +75 -0
- package/skills/four-meme/four-meme-integration/references/execute-trade.md +31 -0
- package/skills/four-meme/four-meme-integration/references/tax-token-query.md +38 -0
- package/skills/four-meme/four-meme-integration/references/token-query-api.md +44 -0
- package/skills/four-meme/four-meme-integration/references/token-tax-info.md +77 -0
- package/skills/four-meme/four-meme-integration/scripts/8004-balance.ts +52 -0
- package/skills/four-meme/four-meme-integration/scripts/8004-register.ts +108 -0
- package/skills/four-meme/four-meme-integration/scripts/create-token-api.ts +321 -0
- package/skills/four-meme/four-meme-integration/scripts/create-token-chain.ts +102 -0
- package/skills/four-meme/four-meme-integration/scripts/create-token-instant.ts +106 -0
- package/skills/four-meme/four-meme-integration/scripts/execute-buy.ts +198 -0
- package/skills/four-meme/four-meme-integration/scripts/execute-sell.ts +150 -0
- package/skills/four-meme/four-meme-integration/scripts/get-public-config.ts +25 -0
- package/skills/four-meme/four-meme-integration/scripts/get-recent-events.ts +76 -0
- package/skills/four-meme/four-meme-integration/scripts/get-tax-token-info.ts +69 -0
- package/skills/four-meme/four-meme-integration/scripts/get-token-info.ts +94 -0
- package/skills/four-meme/four-meme-integration/scripts/quote-buy.ts +85 -0
- package/skills/four-meme/four-meme-integration/scripts/quote-sell.ts +66 -0
- package/skills/four-meme/four-meme-integration/scripts/send-token.ts +98 -0
- package/skills/four-meme/four-meme-integration/scripts/token-get.ts +31 -0
- package/skills/four-meme/four-meme-integration/scripts/token-list.ts +134 -0
- package/skills/four-meme/four-meme-integration/scripts/token-rankings.ts +162 -0
- package/skills/four-meme/four-meme-integration/scripts/verify-events.ts +47 -0
- package/skills/skillpp/contract-profiler/SKILL.md +118 -0
- package/skills/skillpp/newbie-tutor/SKILL.md +85 -0
- package/skills/skillpp/opportunity-board/SKILL.md +87 -0
- package/skills/skillpp/risk-fusion/SKILL.md +146 -0
- package/skills/skillpp/scam-pattern-lab/SKILL.md +115 -0
- package/skills/skillpp/wallet-doctor/SKILL.md +119 -0
- package/skills/skillpp/watchtower/SKILL.md +72 -0
- package/tests/compatibility/v0.1.0.json +117 -0
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Payment Assistant - Send Actions
|
|
4
|
+
|
|
5
|
+
All send/pay action functions + QRCodeHandler.
|
|
6
|
+
Extracted from payment_skill.py — logic unchanged.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
import platform
|
|
12
|
+
import time
|
|
13
|
+
from typing import Dict, Any, Optional
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import qrcode
|
|
17
|
+
HAS_QRCODE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
HAS_QRCODE = False
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from PIL import Image
|
|
23
|
+
HAS_PIL = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
HAS_PIL = False
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from pyzbar.pyzbar import decode as pyzbar_decode
|
|
29
|
+
HAS_PYZBAR = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
HAS_PYZBAR = False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
import cv2
|
|
35
|
+
HAS_CV2 = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
HAS_CV2 = False
|
|
38
|
+
|
|
39
|
+
from common import (
|
|
40
|
+
OrderStatus, SKILLS_ERROR_CODES,
|
|
41
|
+
SKILL_DIR, CONFIG_FILE_PATH, STATE_FILE_PATH, QR_CODE_OUTPUT_PATH, INBOX_DIR, CLIPBOARD_IMAGE_PATH,
|
|
42
|
+
API_KEY_GUIDE_MESSAGE,
|
|
43
|
+
load_config, is_config_ready, show_config_guide, validate_config,
|
|
44
|
+
load_state, update_state, set_order_status, get_order_status, clear_state, get_status_hint,
|
|
45
|
+
PaymentAPI,
|
|
46
|
+
)
|
|
47
|
+
from send_extension import detect_extension, get_extension_by_type, get_all_endpoints
|
|
48
|
+
|
|
49
|
+
# API Endpoints - aggregated from all extensions
|
|
50
|
+
ENDPOINTS = get_all_endpoints()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ============================================================
|
|
54
|
+
# State helpers dict - passed to extension.purchase()
|
|
55
|
+
# ============================================================
|
|
56
|
+
def _get_state_helpers() -> Dict[str, Any]:
|
|
57
|
+
"""Build the state_helpers dict that extensions use to manage state."""
|
|
58
|
+
return {
|
|
59
|
+
'set_order_status': set_order_status,
|
|
60
|
+
'update_state': update_state,
|
|
61
|
+
'OrderStatus': OrderStatus,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ============================================================
|
|
66
|
+
# QR Code Handler
|
|
67
|
+
# ============================================================
|
|
68
|
+
class QRCodeHandler:
|
|
69
|
+
"""Handle QR code generation, decoding, and clipboard/inbox image operations."""
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def generate_qr_image(qr_string: str, output_path: str = QR_CODE_OUTPUT_PATH) -> Optional[str]:
|
|
73
|
+
"""Generate QR code image from string"""
|
|
74
|
+
if not HAS_QRCODE:
|
|
75
|
+
return None
|
|
76
|
+
try:
|
|
77
|
+
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=2)
|
|
78
|
+
qr.add_data(qr_string)
|
|
79
|
+
qr.make(fit=True)
|
|
80
|
+
img = qr.make_image(fill_color="black", back_color="white")
|
|
81
|
+
img.save(output_path)
|
|
82
|
+
return output_path
|
|
83
|
+
except:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def decode_qr_from_image(image_path: str) -> Optional[str]:
|
|
88
|
+
"""Decode QR code from image file. Tries pyzbar first, then opencv."""
|
|
89
|
+
# Try pyzbar first
|
|
90
|
+
if HAS_PIL and HAS_PYZBAR:
|
|
91
|
+
try:
|
|
92
|
+
img = Image.open(image_path)
|
|
93
|
+
decoded = pyzbar_decode(img)
|
|
94
|
+
if decoded:
|
|
95
|
+
return decoded[0].data.decode('utf-8')
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Fallback to OpenCV
|
|
100
|
+
if HAS_CV2:
|
|
101
|
+
try:
|
|
102
|
+
img = cv2.imread(image_path)
|
|
103
|
+
if img is not None:
|
|
104
|
+
detector = cv2.QRCodeDetector()
|
|
105
|
+
data, _, _ = detector.detectAndDecode(img)
|
|
106
|
+
if data:
|
|
107
|
+
return data
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def save_clipboard_image_macos(output_path: str) -> bool:
|
|
115
|
+
"""Save clipboard image to file on macOS using osascript"""
|
|
116
|
+
try:
|
|
117
|
+
script = f'''
|
|
118
|
+
set theFile to POSIX file "{output_path}"
|
|
119
|
+
try
|
|
120
|
+
set imgData to the clipboard as «class PNGf»
|
|
121
|
+
set fileRef to open for access theFile with write permission
|
|
122
|
+
write imgData to fileRef
|
|
123
|
+
close access fileRef
|
|
124
|
+
return "success"
|
|
125
|
+
on error
|
|
126
|
+
return "no_image"
|
|
127
|
+
end try
|
|
128
|
+
'''
|
|
129
|
+
result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True, timeout=5)
|
|
130
|
+
return 'success' in result.stdout
|
|
131
|
+
except:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def save_clipboard_image_linux(output_path: str) -> bool:
|
|
136
|
+
"""Save clipboard image to file on Linux using xclip"""
|
|
137
|
+
try:
|
|
138
|
+
result = subprocess.run(
|
|
139
|
+
['xclip', '-selection', 'clipboard', '-t', 'image/png', '-o'],
|
|
140
|
+
capture_output=True, timeout=5
|
|
141
|
+
)
|
|
142
|
+
if result.returncode == 0 and result.stdout:
|
|
143
|
+
with open(output_path, 'wb') as f:
|
|
144
|
+
f.write(result.stdout)
|
|
145
|
+
return True
|
|
146
|
+
except:
|
|
147
|
+
pass
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def save_clipboard_image_windows(output_path: str) -> bool:
|
|
152
|
+
"""Save clipboard image to file on Windows"""
|
|
153
|
+
try:
|
|
154
|
+
script = f'''
|
|
155
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
156
|
+
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
157
|
+
if ($img) {{
|
|
158
|
+
$img.Save("{output_path}")
|
|
159
|
+
Write-Output "success"
|
|
160
|
+
}} else {{
|
|
161
|
+
Write-Output "no_image"
|
|
162
|
+
}}
|
|
163
|
+
'''
|
|
164
|
+
result = subprocess.run(['powershell', '-Command', script], capture_output=True, text=True, timeout=5)
|
|
165
|
+
return 'success' in result.stdout
|
|
166
|
+
except:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def save_clipboard_image(output_path: str) -> bool:
|
|
171
|
+
"""Save clipboard image to file (cross-platform)"""
|
|
172
|
+
system = platform.system().lower()
|
|
173
|
+
if system == 'darwin':
|
|
174
|
+
return QRCodeHandler.save_clipboard_image_macos(output_path)
|
|
175
|
+
elif system == 'linux':
|
|
176
|
+
return QRCodeHandler.save_clipboard_image_linux(output_path)
|
|
177
|
+
elif system == 'windows':
|
|
178
|
+
return QRCodeHandler.save_clipboard_image_windows(output_path)
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def decode_qr_from_clipboard() -> tuple:
|
|
183
|
+
"""
|
|
184
|
+
Decode QR from clipboard image.
|
|
185
|
+
Returns: (success: bool, qr_data: str or None, message: str)
|
|
186
|
+
"""
|
|
187
|
+
os.makedirs(INBOX_DIR, exist_ok=True)
|
|
188
|
+
|
|
189
|
+
if not QRCodeHandler.save_clipboard_image(CLIPBOARD_IMAGE_PATH):
|
|
190
|
+
return False, None, "clipboard_no_image"
|
|
191
|
+
|
|
192
|
+
qr_data = QRCodeHandler.decode_qr_from_image(CLIPBOARD_IMAGE_PATH)
|
|
193
|
+
if qr_data:
|
|
194
|
+
return True, qr_data, "success"
|
|
195
|
+
else:
|
|
196
|
+
return False, None, "decode_failed"
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def parse_emvco_qr(qr_string: str) -> Dict[str, str]:
|
|
200
|
+
"""Parse EMVCo QR code format to extract merchant info"""
|
|
201
|
+
result = {}
|
|
202
|
+
try:
|
|
203
|
+
if '5918' in qr_string:
|
|
204
|
+
idx = qr_string.index('5918') + 4
|
|
205
|
+
result['merchant_name'] = qr_string[idx:idx+18].strip()
|
|
206
|
+
if '6012' in qr_string:
|
|
207
|
+
idx = qr_string.index('6012') + 4
|
|
208
|
+
result['merchant_city'] = qr_string[idx:idx+12].strip()
|
|
209
|
+
if '5802' in qr_string:
|
|
210
|
+
idx = qr_string.index('5802') + 4
|
|
211
|
+
result['country_code'] = qr_string[idx:idx+2]
|
|
212
|
+
except:
|
|
213
|
+
pass
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ============================================================
|
|
218
|
+
# Actions
|
|
219
|
+
# ============================================================
|
|
220
|
+
def action_config():
|
|
221
|
+
"""Show configuration status and guide user to complete setup"""
|
|
222
|
+
config = load_config()
|
|
223
|
+
is_valid, missing = validate_config(config)
|
|
224
|
+
file_exists = os.path.exists(CONFIG_FILE_PATH)
|
|
225
|
+
|
|
226
|
+
print()
|
|
227
|
+
print("════════════════════════════════════════════════════")
|
|
228
|
+
print("⚙️ Configuration Status")
|
|
229
|
+
print("════════════════════════════════════════════════════")
|
|
230
|
+
print()
|
|
231
|
+
print(f"📁 Config file: {CONFIG_FILE_PATH}")
|
|
232
|
+
print(f" Status: {'✅ Exists' if file_exists else '❌ Not found'}")
|
|
233
|
+
print()
|
|
234
|
+
|
|
235
|
+
base_url_ok = config.get('base_url') and len(config.get('base_url', '')) > 0
|
|
236
|
+
api_key_ok = config.get('api_key') and len(config.get('api_key', '')) > 0
|
|
237
|
+
api_secret_ok = config.get('api_secret') and len(config.get('api_secret', '')) > 0
|
|
238
|
+
|
|
239
|
+
print("📊 Current Settings:")
|
|
240
|
+
print(f" base_url: {'✅ ' + config.get('base_url', '') + ' (auto)' if base_url_ok else '❌ Not set'}")
|
|
241
|
+
print(f" api_key: {'✅ ****' + config.get('api_key', '')[-4:] if api_key_ok else '❌ Not set'}")
|
|
242
|
+
print(f" api_secret: {'✅ ****' + config.get('api_secret', '')[-4:] if api_secret_ok else '❌ Not set'}")
|
|
243
|
+
print()
|
|
244
|
+
|
|
245
|
+
if is_valid:
|
|
246
|
+
print("════════════════════════════════════════════════════")
|
|
247
|
+
print("✅ Ready")
|
|
248
|
+
print("════════════════════════════════════════════════════")
|
|
249
|
+
print(" All credentials are configured.")
|
|
250
|
+
else:
|
|
251
|
+
print("════════════════════════════════════════════════════")
|
|
252
|
+
print("⚠️ Setup Required")
|
|
253
|
+
print("════════════════════════════════════════════════════")
|
|
254
|
+
print(f" Missing: {', '.join(missing)}")
|
|
255
|
+
print()
|
|
256
|
+
print(f"📝 Please edit: {CONFIG_FILE_PATH}")
|
|
257
|
+
print()
|
|
258
|
+
print(" Required fields:")
|
|
259
|
+
if 'api_key' in missing:
|
|
260
|
+
print(" • api_key: Your API key")
|
|
261
|
+
if 'api_secret' in missing:
|
|
262
|
+
print(" • api_secret: Your API secret")
|
|
263
|
+
print()
|
|
264
|
+
print("📝 Configuration Template:")
|
|
265
|
+
print(' {')
|
|
266
|
+
print(' "configured": true,')
|
|
267
|
+
print(' "api_key": "YOUR_API_KEY",')
|
|
268
|
+
print(' "api_secret": "YOUR_API_SECRET"')
|
|
269
|
+
print(' }')
|
|
270
|
+
print()
|
|
271
|
+
print(f"🔑 {API_KEY_GUIDE_MESSAGE}")
|
|
272
|
+
print()
|
|
273
|
+
print(" Or use environment variables:")
|
|
274
|
+
print(" export PAYMENT_API_KEY='your_key'")
|
|
275
|
+
print(" export PAYMENT_API_SECRET='your_secret'")
|
|
276
|
+
|
|
277
|
+
print("════════════════════════════════════════════════════")
|
|
278
|
+
print()
|
|
279
|
+
|
|
280
|
+
print(json.dumps({
|
|
281
|
+
'config_exists': file_exists,
|
|
282
|
+
'is_valid': is_valid,
|
|
283
|
+
'missing_fields': missing,
|
|
284
|
+
'config_path': CONFIG_FILE_PATH
|
|
285
|
+
}))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def action_purchase(config: Dict[str, Any], raw_qr: str):
|
|
289
|
+
"""
|
|
290
|
+
Unified Purchase Flow - Step 1: Parse QR
|
|
291
|
+
|
|
292
|
+
Auto-detects QR type and delegates to the matching extension.
|
|
293
|
+
"""
|
|
294
|
+
if not raw_qr:
|
|
295
|
+
print()
|
|
296
|
+
print("❌ Missing QR code")
|
|
297
|
+
print("💡 Please provide QR code data with --raw_qr parameter")
|
|
298
|
+
print()
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
is_ready, reason, missing_fields = is_config_ready(config)
|
|
302
|
+
if not is_ready:
|
|
303
|
+
show_config_guide(config, reason, missing_fields)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
# Detect QR type via extension registry
|
|
307
|
+
ext = detect_extension(raw_qr)
|
|
308
|
+
api = PaymentAPI(config)
|
|
309
|
+
|
|
310
|
+
print()
|
|
311
|
+
print("════════════════════════════════════════════════════")
|
|
312
|
+
print(f"📦 Starting {ext.payment_type} Purchase Flow")
|
|
313
|
+
print("════════════════════════════════════════════════════")
|
|
314
|
+
print()
|
|
315
|
+
|
|
316
|
+
# Initialize state with payment type
|
|
317
|
+
update_state({
|
|
318
|
+
'raw_qr': raw_qr,
|
|
319
|
+
'payment_type': ext.payment_type,
|
|
320
|
+
'order_status': OrderStatus.INIT.value
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
# Delegate to extension
|
|
324
|
+
ext.purchase(api, raw_qr, _get_state_helpers())
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def action_set_amount(amount: float, currency: str = None):
|
|
328
|
+
"""Set payment amount (and optionally currency) for orders without preset amount"""
|
|
329
|
+
state = load_state()
|
|
330
|
+
|
|
331
|
+
if not state.get('checkout_id'):
|
|
332
|
+
print()
|
|
333
|
+
print("❌ No active order")
|
|
334
|
+
print("💡 Run '--action purchase --raw_qr <QR_DATA>' first")
|
|
335
|
+
print()
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Block amount change if PIX QR has a locked amount
|
|
339
|
+
if state.get('pix_amount_locked'):
|
|
340
|
+
preset = state.get('preset_amount') or state.get('suggested_amount')
|
|
341
|
+
cur = state.get('currency', 'BRL')
|
|
342
|
+
print()
|
|
343
|
+
print("════════════════════════════════════════════════════")
|
|
344
|
+
print("❌ Cannot change amount")
|
|
345
|
+
print("════════════════════════════════════════════════════")
|
|
346
|
+
print(f" This PIX QR code has a fixed amount: {preset} {cur}")
|
|
347
|
+
print(" The amount is embedded in the QR code and cannot be modified.")
|
|
348
|
+
print()
|
|
349
|
+
print("💡 Reply 'y' to confirm payment with the QR amount, 'n' to cancel")
|
|
350
|
+
print()
|
|
351
|
+
print(json.dumps({
|
|
352
|
+
'status': 'AMOUNT_LOCKED',
|
|
353
|
+
'message': f'PIX QR has fixed amount: {preset} {cur}. Cannot be modified.',
|
|
354
|
+
'locked_amount': str(preset),
|
|
355
|
+
'currency': cur
|
|
356
|
+
}))
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
final_currency = currency or state.get('currency', 'USDT')
|
|
360
|
+
|
|
361
|
+
set_order_status(OrderStatus.AMOUNT_SET,
|
|
362
|
+
suggested_amount=amount,
|
|
363
|
+
currency=final_currency,
|
|
364
|
+
needs_amount_input=False
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
print()
|
|
368
|
+
print(f"✅ Amount set: {amount} {final_currency}")
|
|
369
|
+
print()
|
|
370
|
+
print("💡 Reply 'y' to confirm, 'n' to cancel")
|
|
371
|
+
print(json.dumps({
|
|
372
|
+
'status': 'AMOUNT_SET',
|
|
373
|
+
'amount': amount,
|
|
374
|
+
'currency': final_currency,
|
|
375
|
+
'checkout_id': state.get('checkout_id'),
|
|
376
|
+
'payee': state.get('nickname')
|
|
377
|
+
}))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def action_pay_confirm(config: Dict[str, Any], amount: float = None, currency: str = None):
|
|
381
|
+
"""
|
|
382
|
+
Payment Flow - Step 2: Confirm Payment
|
|
383
|
+
|
|
384
|
+
Routes to the correct extension endpoint based on payment_type in state.
|
|
385
|
+
"""
|
|
386
|
+
is_ready, reason, missing_fields = is_config_ready(config)
|
|
387
|
+
if not is_ready:
|
|
388
|
+
show_config_guide(config, reason, missing_fields)
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
state = load_state()
|
|
392
|
+
|
|
393
|
+
# Safety check: Prevent duplicate payment
|
|
394
|
+
current_status = state.get('order_status')
|
|
395
|
+
if current_status in [OrderStatus.SUCCESS.value, OrderStatus.PAYMENT_CONFIRMED.value, OrderStatus.POLLING.value]:
|
|
396
|
+
print()
|
|
397
|
+
print("════════════════════════════════════════════════════")
|
|
398
|
+
print("⚠️ Payment Already In Progress or Complete")
|
|
399
|
+
print("════════════════════════════════════════════════════")
|
|
400
|
+
print(f" Current status: {current_status}")
|
|
401
|
+
if current_status == OrderStatus.SUCCESS.value:
|
|
402
|
+
print(" This order has already been paid successfully.")
|
|
403
|
+
else:
|
|
404
|
+
print(" Payment is in progress. Run --action poll to check result.")
|
|
405
|
+
print()
|
|
406
|
+
print("💡 Run: --action status to check current state")
|
|
407
|
+
print(" Run: --action reset to start a new payment")
|
|
408
|
+
print()
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
if not state.get('checkout_id'):
|
|
412
|
+
print()
|
|
413
|
+
print("❌ No active order")
|
|
414
|
+
print("💡 Run '--action purchase --raw_qr <QR_DATA>' first")
|
|
415
|
+
print()
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
# If PIX amount is locked, force use the QR amount regardless of user input
|
|
419
|
+
if state.get('pix_amount_locked'):
|
|
420
|
+
locked_amount = state.get('preset_amount') or state.get('suggested_amount')
|
|
421
|
+
if locked_amount is not None:
|
|
422
|
+
if amount is not None and float(amount) != float(locked_amount):
|
|
423
|
+
print(f"⚠️ PIX QR has fixed amount {locked_amount} {state.get('currency', 'BRL')}. Ignoring user amount {amount}.")
|
|
424
|
+
amount = float(locked_amount)
|
|
425
|
+
currency = state.get('currency', 'BRL')
|
|
426
|
+
|
|
427
|
+
if amount is None:
|
|
428
|
+
amount = state.get('suggested_amount')
|
|
429
|
+
|
|
430
|
+
if amount is None:
|
|
431
|
+
print()
|
|
432
|
+
print("❌ No amount specified")
|
|
433
|
+
print("💡 Use: --action set_amount --amount <amount> [--currency <currency>]")
|
|
434
|
+
print()
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
final_currency = currency or state.get('currency', 'USDT')
|
|
438
|
+
|
|
439
|
+
# Get the right extension for this payment type
|
|
440
|
+
payment_type = state.get('payment_type', 'C2C')
|
|
441
|
+
ext = get_extension_by_type(payment_type)
|
|
442
|
+
api = PaymentAPI(config)
|
|
443
|
+
|
|
444
|
+
payee = state.get('nickname', 'Unknown')
|
|
445
|
+
amount_str = str(int(amount)) if amount == int(amount) else str(amount)
|
|
446
|
+
|
|
447
|
+
print()
|
|
448
|
+
print("════════════════════════════════════════════════════")
|
|
449
|
+
print(f"💳 [Step 2] Confirming {payment_type} Payment")
|
|
450
|
+
print("════════════════════════════════════════════════════")
|
|
451
|
+
print(f" Checkout: {state.get('checkout_id')}")
|
|
452
|
+
print(f" Amount: {amount_str} {final_currency}")
|
|
453
|
+
print(f" Payee: {payee}")
|
|
454
|
+
print()
|
|
455
|
+
|
|
456
|
+
# Build params via extension and call API
|
|
457
|
+
print("🔄 Processing payment...")
|
|
458
|
+
confirm_params = ext.build_confirm_params(state, amount_str, final_currency)
|
|
459
|
+
confirm_result = api.confirm_payment(ext.get_confirm_endpoint(), confirm_params)
|
|
460
|
+
|
|
461
|
+
if not confirm_result['success']:
|
|
462
|
+
error_status = confirm_result.get('status', 'ERROR')
|
|
463
|
+
error_msg = confirm_result.get('message', 'Payment failed')
|
|
464
|
+
error_hint = confirm_result.get('hint', '')
|
|
465
|
+
error_code = confirm_result.get('code')
|
|
466
|
+
|
|
467
|
+
set_order_status(OrderStatus.FAILED, error_message=error_msg, error_code=error_code)
|
|
468
|
+
|
|
469
|
+
print()
|
|
470
|
+
print("════════════════════════════════════════════════════")
|
|
471
|
+
print(f"❌ Payment Failed")
|
|
472
|
+
print("════════════════════════════════════════════════════")
|
|
473
|
+
print(f" {error_msg}")
|
|
474
|
+
if error_hint:
|
|
475
|
+
print(f" 💡 {error_hint}")
|
|
476
|
+
print("════════════════════════════════════════════════════")
|
|
477
|
+
|
|
478
|
+
print(json.dumps({
|
|
479
|
+
'status': error_status,
|
|
480
|
+
'code': error_code,
|
|
481
|
+
'message': error_msg,
|
|
482
|
+
'hint': error_hint
|
|
483
|
+
}))
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
payment_info = confirm_result['payment_info']
|
|
487
|
+
|
|
488
|
+
set_order_status(OrderStatus.PAYMENT_CONFIRMED,
|
|
489
|
+
pay_order_id=payment_info.pay_order_id,
|
|
490
|
+
amount=amount,
|
|
491
|
+
currency=final_currency,
|
|
492
|
+
usd_amount=str(payment_info.usd_amount) if payment_info.usd_amount else None,
|
|
493
|
+
daily_used_before=str(payment_info.daily_used_before) if payment_info.daily_used_before is not None else None,
|
|
494
|
+
daily_used_after=str(payment_info.daily_used_after) if payment_info.daily_used_after is not None else None
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
print("✅ Payment confirmed, processing...")
|
|
498
|
+
print(f" Pay Order ID: {payment_info.pay_order_id}")
|
|
499
|
+
if payment_info.usd_amount:
|
|
500
|
+
print(f" USD Amount: {payment_info.usd_amount}")
|
|
501
|
+
daily_limit = state.get('daily_limit')
|
|
502
|
+
if payment_info.daily_used_before is not None and payment_info.daily_used_after is not None and daily_limit:
|
|
503
|
+
print(f" Daily Usage: {payment_info.daily_used_before} → {payment_info.daily_used_after} / {daily_limit} USD")
|
|
504
|
+
elif payment_info.daily_used_after is not None and daily_limit:
|
|
505
|
+
print(f" Daily Usage: {payment_info.daily_used_after} / {daily_limit} USD")
|
|
506
|
+
|
|
507
|
+
print(json.dumps({
|
|
508
|
+
'status': 'PROCESSING',
|
|
509
|
+
'pay_order_id': payment_info.pay_order_id,
|
|
510
|
+
'amount': amount,
|
|
511
|
+
'currency': final_currency,
|
|
512
|
+
'payee': payee,
|
|
513
|
+
'usd_amount': str(payment_info.usd_amount) if payment_info.usd_amount else None,
|
|
514
|
+
'daily_used_before': str(payment_info.daily_used_before) if payment_info.daily_used_before is not None else None,
|
|
515
|
+
'daily_used_after': str(payment_info.daily_used_after) if payment_info.daily_used_after is not None else None,
|
|
516
|
+
'daily_limit': daily_limit
|
|
517
|
+
}))
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def action_poll(config: Dict[str, Any]):
|
|
521
|
+
"""Payment Flow - Step 3: Poll payment status until final result"""
|
|
522
|
+
state = load_state()
|
|
523
|
+
pay_order_id = state.get('pay_order_id')
|
|
524
|
+
|
|
525
|
+
if not pay_order_id:
|
|
526
|
+
print()
|
|
527
|
+
print("❌ No active payment")
|
|
528
|
+
print()
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
# Get the right extension for this payment type
|
|
532
|
+
payment_type = state.get('payment_type', 'C2C')
|
|
533
|
+
ext = get_extension_by_type(payment_type)
|
|
534
|
+
api = PaymentAPI(config)
|
|
535
|
+
|
|
536
|
+
print()
|
|
537
|
+
print("🔍 Querying order status...")
|
|
538
|
+
|
|
539
|
+
poll_params = ext.build_poll_params(state)
|
|
540
|
+
status_result = api.query_payment_status(ext.get_poll_endpoint(), poll_params)
|
|
541
|
+
|
|
542
|
+
if not status_result['success']:
|
|
543
|
+
print(f"❌ Query failed: {status_result.get('message', '')}")
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
status_info = status_result['status_info']
|
|
547
|
+
status_icon = '✅' if status_info.status == 'SUCCESS' else ('❌' if status_info.status in ['FAILED', 'FAIL'] else '⏳')
|
|
548
|
+
status_text = 'Success' if status_info.status == 'SUCCESS' else ('Failed' if status_info.status in ['FAILED', 'FAIL'] else 'Processing')
|
|
549
|
+
|
|
550
|
+
print()
|
|
551
|
+
print("════════════════════════════════════════════════════")
|
|
552
|
+
print(f"{status_icon} Status: {status_text}")
|
|
553
|
+
print("════════════════════════════════════════════════════")
|
|
554
|
+
print(f" 📝 Pay Order: {pay_order_id}")
|
|
555
|
+
if state.get('amount'):
|
|
556
|
+
print(f" 💵 Amount Sent: {state['amount']} {state.get('currency', 'USDT')}")
|
|
557
|
+
if status_info.asset_cost_vos:
|
|
558
|
+
costs = [f"{vo['amount']} {vo['asset']}" for vo in status_info.asset_cost_vos]
|
|
559
|
+
print(f" 💳 Paid With: {' + '.join(costs)}")
|
|
560
|
+
# Show daily usage change on success
|
|
561
|
+
daily_used_before = state.get('daily_used_before')
|
|
562
|
+
daily_used_after = state.get('daily_used_after')
|
|
563
|
+
daily_limit = state.get('daily_limit')
|
|
564
|
+
if status_info.status == 'SUCCESS' and daily_used_before is not None and daily_used_after is not None and daily_limit:
|
|
565
|
+
print(f" 📊 Daily Usage: {daily_used_before} → {daily_used_after} / {daily_limit} USD")
|
|
566
|
+
print("════════════════════════════════════════════════════")
|
|
567
|
+
print(json.dumps({
|
|
568
|
+
'status': status_info.status,
|
|
569
|
+
'pay_order_id': pay_order_id,
|
|
570
|
+
'amount_sent': state.get('amount'),
|
|
571
|
+
'currency': state.get('currency', 'USDT'),
|
|
572
|
+
'paid_with': status_info.asset_cost_vos if status_info.asset_cost_vos else None,
|
|
573
|
+
'daily_used_before': daily_used_before,
|
|
574
|
+
'daily_used_after': daily_used_after,
|
|
575
|
+
'daily_limit': daily_limit
|
|
576
|
+
}))
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def action_status():
|
|
580
|
+
"""Show current order status and next steps"""
|
|
581
|
+
state = load_state()
|
|
582
|
+
status = get_order_status()
|
|
583
|
+
|
|
584
|
+
print()
|
|
585
|
+
print("════════════════════════════════════════════════════")
|
|
586
|
+
print("📊 Current Order Status")
|
|
587
|
+
print("════════════════════════════════════════════════════")
|
|
588
|
+
|
|
589
|
+
if not state or not status:
|
|
590
|
+
print(" No active order")
|
|
591
|
+
print()
|
|
592
|
+
print("💡 Start with: --action purchase --raw_qr <QR_DATA>")
|
|
593
|
+
print("════════════════════════════════════════════════════")
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
checkout_id = state.get('checkout_id')
|
|
597
|
+
pay_order_id = state.get('pay_order_id')
|
|
598
|
+
payment_type = state.get('payment_type', 'C2C')
|
|
599
|
+
|
|
600
|
+
print(f" Type: {payment_type}")
|
|
601
|
+
print(f" Status: {status.value}")
|
|
602
|
+
print(f" Checkout ID: {checkout_id or 'Not yet created'}")
|
|
603
|
+
if pay_order_id:
|
|
604
|
+
print(f" Pay Order: {pay_order_id}")
|
|
605
|
+
|
|
606
|
+
if state.get('nickname'):
|
|
607
|
+
print(f" Payee: {state.get('nickname')}")
|
|
608
|
+
if state.get('receiver_psp'):
|
|
609
|
+
print(f" Bank: {state.get('receiver_psp')}")
|
|
610
|
+
if state.get('receiver_document'):
|
|
611
|
+
print(f" Document: {state.get('receiver_document')}")
|
|
612
|
+
if state.get('currency'):
|
|
613
|
+
print(f" Currency: {state.get('currency')}")
|
|
614
|
+
if state.get('suggested_amount') or state.get('amount'):
|
|
615
|
+
amt = state.get('amount') or state.get('suggested_amount')
|
|
616
|
+
print(f" Amount: {amt} {state.get('currency', '')}")
|
|
617
|
+
if state.get('error_message'):
|
|
618
|
+
print(f" Error: {state.get('error_message')}")
|
|
619
|
+
if state.get('last_updated'):
|
|
620
|
+
print(f" Updated: {state.get('last_updated')}")
|
|
621
|
+
|
|
622
|
+
print()
|
|
623
|
+
print(f"💡 {get_status_hint(status, state)}")
|
|
624
|
+
print("════════════════════════════════════════════════════")
|
|
625
|
+
|
|
626
|
+
print(json.dumps({
|
|
627
|
+
'status': status.value,
|
|
628
|
+
'payment_type': payment_type,
|
|
629
|
+
'checkout_id': checkout_id,
|
|
630
|
+
'pay_order_id': pay_order_id,
|
|
631
|
+
'amount': state.get('amount') or state.get('suggested_amount'),
|
|
632
|
+
'currency': state.get('currency'),
|
|
633
|
+
'payee': state.get('nickname')
|
|
634
|
+
}))
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def action_reset():
|
|
638
|
+
"""Clear state and start fresh"""
|
|
639
|
+
clear_state()
|
|
640
|
+
print()
|
|
641
|
+
print("🗑️ State cleared")
|
|
642
|
+
print()
|
|
643
|
+
print("💡 Ready for new payment: --action purchase --raw_qr <QR_DATA>")
|
|
644
|
+
print()
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def action_resume(config: Dict[str, Any]):
|
|
648
|
+
"""Resume from current state - automatically continue the payment flow."""
|
|
649
|
+
is_ready, reason, missing_fields = is_config_ready(config)
|
|
650
|
+
if not is_ready:
|
|
651
|
+
show_config_guide(config, reason, missing_fields)
|
|
652
|
+
return
|
|
653
|
+
|
|
654
|
+
state = load_state()
|
|
655
|
+
status = get_order_status()
|
|
656
|
+
|
|
657
|
+
if not state or not status:
|
|
658
|
+
print()
|
|
659
|
+
print("📭 No active order to resume")
|
|
660
|
+
print("💡 Start with: --action purchase --raw_qr <QR_DATA>")
|
|
661
|
+
print()
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
print()
|
|
665
|
+
print(f"🔄 Resuming from status: {status.value}")
|
|
666
|
+
print()
|
|
667
|
+
|
|
668
|
+
if status == OrderStatus.INIT:
|
|
669
|
+
raw_qr = state.get('raw_qr')
|
|
670
|
+
if raw_qr:
|
|
671
|
+
action_purchase(config, raw_qr)
|
|
672
|
+
else:
|
|
673
|
+
print("❌ No QR code in state")
|
|
674
|
+
print("💡 Run: --action purchase --raw_qr <QR_DATA>")
|
|
675
|
+
|
|
676
|
+
elif status == OrderStatus.QR_PARSED:
|
|
677
|
+
if state.get('has_preset_amount') and state.get('preset_amount'):
|
|
678
|
+
amount = float(state.get('preset_amount'))
|
|
679
|
+
action_pay_confirm(config, amount)
|
|
680
|
+
else:
|
|
681
|
+
print("💡 Please set amount: --action set_amount --amount <AMOUNT>")
|
|
682
|
+
print(f" Currency: {state.get('currency', 'USDT')}")
|
|
683
|
+
|
|
684
|
+
elif status == OrderStatus.AWAITING_AMOUNT:
|
|
685
|
+
print("💡 Please set amount: --action set_amount --amount <AMOUNT>")
|
|
686
|
+
print(f" Currency: {state.get('currency', 'USDT')}")
|
|
687
|
+
|
|
688
|
+
elif status == OrderStatus.AMOUNT_SET:
|
|
689
|
+
amount = state.get('suggested_amount') or state.get('amount')
|
|
690
|
+
if amount:
|
|
691
|
+
action_pay_confirm(config, float(amount))
|
|
692
|
+
else:
|
|
693
|
+
print("❌ No amount set")
|
|
694
|
+
print("💡 Run: --action set_amount --amount <AMOUNT>")
|
|
695
|
+
|
|
696
|
+
elif status in [OrderStatus.PAYMENT_CONFIRMED, OrderStatus.POLLING]:
|
|
697
|
+
action_poll(config)
|
|
698
|
+
|
|
699
|
+
elif status == OrderStatus.SUCCESS:
|
|
700
|
+
print("✅ Payment already completed!")
|
|
701
|
+
if state.get('asset_costs'):
|
|
702
|
+
costs = [f"{c.get('amount')} {c.get('asset')}" for c in state['asset_costs']]
|
|
703
|
+
print(f" 💳 Paid With: {' + '.join(costs)}")
|
|
704
|
+
print()
|
|
705
|
+
print("💡 Run: --action reset for a new payment")
|
|
706
|
+
|
|
707
|
+
elif status == OrderStatus.FAILED:
|
|
708
|
+
print(f"❌ Order failed: {state.get('error_message', 'Unknown error')}")
|
|
709
|
+
print()
|
|
710
|
+
print("💡 Run: --action reset to start over")
|
|
711
|
+
|
|
712
|
+
else:
|
|
713
|
+
print(f"⚠️ Unknown status: {status.value}")
|
|
714
|
+
print("💡 Run: --action status to check details")
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def action_help():
|
|
718
|
+
"""Show help information"""
|
|
719
|
+
print()
|
|
720
|
+
print("════════════════════════════════════════════════════")
|
|
721
|
+
print("👋 Payment Assistant Skill (C2C + PIX)")
|
|
722
|
+
print("════════════════════════════════════════════════════")
|
|
723
|
+
print()
|
|
724
|
+
print("📋 Core Actions (3-step flow):")
|
|
725
|
+
print(" purchase - Step 1: Parse QR (requires --raw_qr)")
|
|
726
|
+
print(" Auto-detects C2C URL or PIX EMV QR")
|
|
727
|
+
print(" set_amount - Set amount (e.g., --amount 100 --currency BRL)")
|
|
728
|
+
print(" pay_confirm - Step 2: Confirm payment")
|
|
729
|
+
print(" poll - Step 3: Poll until final status")
|
|
730
|
+
print(" query - Check order status (API call)")
|
|
731
|
+
print()
|
|
732
|
+
print("📷 QR Decode Actions:")
|
|
733
|
+
print(" decode_qr - Decode QR from clipboard or image file")
|
|
734
|
+
print()
|
|
735
|
+
print("💰 Receive Actions:")
|
|
736
|
+
print(" receive - Generate receive QR code / payment link")
|
|
737
|
+
print()
|
|
738
|
+
print("🔄 Recovery Actions:")
|
|
739
|
+
print(" status - Show current state and next steps")
|
|
740
|
+
print(" resume - Auto-continue from any state")
|
|
741
|
+
print(" reset - Clear state for fresh start")
|
|
742
|
+
print()
|
|
743
|
+
print("⚙️ Config Actions:")
|
|
744
|
+
print(" config - Show configuration guide")
|
|
745
|
+
|
|
746
|
+
print()
|
|
747
|
+
print("💡 C2C Example Flow:")
|
|
748
|
+
print(" 1. --action decode_qr # Decode from clipboard/inbox")
|
|
749
|
+
print(" 2. --action purchase --raw_qr '<QR_DATA>'")
|
|
750
|
+
print(" 3. --action set_amount --amount 50 # If no preset amount")
|
|
751
|
+
print(" 4. --action pay_confirm")
|
|
752
|
+
print(" 5. --action poll")
|
|
753
|
+
print()
|
|
754
|
+
print("💡 PIX Example Flow:")
|
|
755
|
+
print(" 1. --action purchase --raw_qr '00020126...br.gov.bcb.pix...'")
|
|
756
|
+
print(" 2. --action set_amount --amount 100 --currency BRL # If no preset")
|
|
757
|
+
print(" 3. --action pay_confirm")
|
|
758
|
+
print(" 4. --action poll")
|
|
759
|
+
print()
|
|
760
|
+
print("💡 Receive Example:")
|
|
761
|
+
print(" --action receive --currency USDT --amount 50 --note 'For lunch'")
|
|
762
|
+
print()
|
|
763
|
+
print("🔄 Recovery (if interrupted at any point):")
|
|
764
|
+
print(" --action status # Check where you are")
|
|
765
|
+
print(" --action resume # Auto-continue")
|
|
766
|
+
print("════════════════════════════════════════════════════")
|
|
767
|
+
print()
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _get_file_info(file_path: str) -> Dict[str, Any]:
|
|
771
|
+
"""Get file metadata for debugging/transparency."""
|
|
772
|
+
try:
|
|
773
|
+
stat = os.stat(file_path)
|
|
774
|
+
return {
|
|
775
|
+
'path': file_path,
|
|
776
|
+
'filename': os.path.basename(file_path),
|
|
777
|
+
'size_bytes': stat.st_size,
|
|
778
|
+
'modified_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime)),
|
|
779
|
+
}
|
|
780
|
+
except Exception:
|
|
781
|
+
return {'path': file_path, 'filename': os.path.basename(file_path)}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def action_decode_qr(image_path: str = None, base64_data: str = None, use_clipboard: bool = False):
|
|
785
|
+
"""
|
|
786
|
+
Decode QR code from image.
|
|
787
|
+
|
|
788
|
+
Three MUTUALLY EXCLUSIVE input modes (no fallback between them):
|
|
789
|
+
- --image <path> : Decode from file path
|
|
790
|
+
- --base64 <data> : Decode from base64 encoded image
|
|
791
|
+
- --clipboard : Explicitly read from system clipboard
|
|
792
|
+
|
|
793
|
+
If no input specified, returns an error asking for explicit input.
|
|
794
|
+
This ensures 100% clarity on which image is being decoded.
|
|
795
|
+
|
|
796
|
+
Returns JSON with qr_data and source info for transparency.
|
|
797
|
+
"""
|
|
798
|
+
qr_handler = QRCodeHandler()
|
|
799
|
+
|
|
800
|
+
has_decoder = (HAS_PIL and HAS_PYZBAR) or HAS_CV2
|
|
801
|
+
if not has_decoder:
|
|
802
|
+
print(json.dumps({
|
|
803
|
+
'success': False,
|
|
804
|
+
'error': 'missing_dependencies',
|
|
805
|
+
'message': "No QR decoder available. Install: pip install opencv-python pyzbar"
|
|
806
|
+
}))
|
|
807
|
+
return
|
|
808
|
+
|
|
809
|
+
# Count how many input modes are specified
|
|
810
|
+
input_modes = sum([bool(image_path), bool(base64_data), use_clipboard])
|
|
811
|
+
|
|
812
|
+
if input_modes > 1:
|
|
813
|
+
print(json.dumps({
|
|
814
|
+
'success': False,
|
|
815
|
+
'error': 'multiple_inputs',
|
|
816
|
+
'message': "Only one input mode allowed. Use --image OR --base64 OR --clipboard, not multiple.",
|
|
817
|
+
'hint': 'Choose one input source to avoid ambiguity.'
|
|
818
|
+
}))
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
if input_modes == 0:
|
|
822
|
+
print(json.dumps({
|
|
823
|
+
'success': False,
|
|
824
|
+
'error': 'no_input',
|
|
825
|
+
'message': "No image input specified. You must provide one of: --image, --base64, or --clipboard",
|
|
826
|
+
'usage': {
|
|
827
|
+
'--image <path>': 'Path to image file (from message attachment)',
|
|
828
|
+
'--base64 <data>': 'Base64 encoded image data',
|
|
829
|
+
'--clipboard': 'Read from system clipboard (user must have just copied an image)'
|
|
830
|
+
},
|
|
831
|
+
'hint': 'AI should use --image with the attachment path from the user message, or use Vision to read QR directly and pass --raw_qr to purchase action.'
|
|
832
|
+
}))
|
|
833
|
+
return
|
|
834
|
+
|
|
835
|
+
# ============================================================
|
|
836
|
+
# Mode 1: Image file path
|
|
837
|
+
# ============================================================
|
|
838
|
+
if image_path:
|
|
839
|
+
if not os.path.exists(image_path):
|
|
840
|
+
print(json.dumps({
|
|
841
|
+
'success': False,
|
|
842
|
+
'error': 'file_not_found',
|
|
843
|
+
'message': f"File not found: {image_path}",
|
|
844
|
+
'source_type': 'image_path',
|
|
845
|
+
'provided_path': image_path
|
|
846
|
+
}))
|
|
847
|
+
return
|
|
848
|
+
|
|
849
|
+
file_info = _get_file_info(image_path)
|
|
850
|
+
qr_data = qr_handler.decode_qr_from_image(image_path)
|
|
851
|
+
|
|
852
|
+
if qr_data:
|
|
853
|
+
print(json.dumps({
|
|
854
|
+
'success': True,
|
|
855
|
+
'qr_data': qr_data,
|
|
856
|
+
'source_type': 'image_path',
|
|
857
|
+
'source_info': file_info,
|
|
858
|
+
'message': f"QR decoded from: {file_info['filename']}"
|
|
859
|
+
}))
|
|
860
|
+
else:
|
|
861
|
+
print(json.dumps({
|
|
862
|
+
'success': False,
|
|
863
|
+
'error': 'decode_failed',
|
|
864
|
+
'message': f"No QR code found in image: {file_info['filename']}",
|
|
865
|
+
'source_type': 'image_path',
|
|
866
|
+
'source_info': file_info,
|
|
867
|
+
'hint': 'Image exists but no QR code detected. Verify this is the correct image.'
|
|
868
|
+
}))
|
|
869
|
+
return
|
|
870
|
+
|
|
871
|
+
# ============================================================
|
|
872
|
+
# Mode 2: Base64 encoded image
|
|
873
|
+
# ============================================================
|
|
874
|
+
if base64_data:
|
|
875
|
+
import base64
|
|
876
|
+
import tempfile
|
|
877
|
+
|
|
878
|
+
try:
|
|
879
|
+
# Remove data URI prefix if present (e.g., "data:image/png;base64,")
|
|
880
|
+
if ',' in base64_data:
|
|
881
|
+
base64_data = base64_data.split(',', 1)[1]
|
|
882
|
+
|
|
883
|
+
image_bytes = base64.b64decode(base64_data)
|
|
884
|
+
|
|
885
|
+
# Save to temp file for decoding
|
|
886
|
+
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
|
887
|
+
tmp.write(image_bytes)
|
|
888
|
+
tmp_path = tmp.name
|
|
889
|
+
|
|
890
|
+
qr_data = qr_handler.decode_qr_from_image(tmp_path)
|
|
891
|
+
|
|
892
|
+
# Clean up temp file
|
|
893
|
+
try:
|
|
894
|
+
os.unlink(tmp_path)
|
|
895
|
+
except:
|
|
896
|
+
pass
|
|
897
|
+
|
|
898
|
+
if qr_data:
|
|
899
|
+
print(json.dumps({
|
|
900
|
+
'success': True,
|
|
901
|
+
'qr_data': qr_data,
|
|
902
|
+
'source_type': 'base64',
|
|
903
|
+
'source_info': {
|
|
904
|
+
'data_length': len(base64_data),
|
|
905
|
+
'decoded_size': len(image_bytes)
|
|
906
|
+
},
|
|
907
|
+
'message': 'QR decoded from base64 image data'
|
|
908
|
+
}))
|
|
909
|
+
else:
|
|
910
|
+
print(json.dumps({
|
|
911
|
+
'success': False,
|
|
912
|
+
'error': 'decode_failed',
|
|
913
|
+
'message': 'No QR code found in base64 image',
|
|
914
|
+
'source_type': 'base64',
|
|
915
|
+
'hint': 'Image decoded successfully but no QR code detected.'
|
|
916
|
+
}))
|
|
917
|
+
except Exception as e:
|
|
918
|
+
print(json.dumps({
|
|
919
|
+
'success': False,
|
|
920
|
+
'error': 'base64_decode_failed',
|
|
921
|
+
'message': f'Failed to decode base64 image: {str(e)}',
|
|
922
|
+
'source_type': 'base64',
|
|
923
|
+
'hint': 'Ensure the base64 data is valid image data.'
|
|
924
|
+
}))
|
|
925
|
+
return
|
|
926
|
+
|
|
927
|
+
# ============================================================
|
|
928
|
+
# Mode 3: System clipboard (explicit)
|
|
929
|
+
# ============================================================
|
|
930
|
+
if use_clipboard:
|
|
931
|
+
success, data, msg = qr_handler.decode_qr_from_clipboard()
|
|
932
|
+
|
|
933
|
+
if success:
|
|
934
|
+
print(json.dumps({
|
|
935
|
+
'success': True,
|
|
936
|
+
'qr_data': data,
|
|
937
|
+
'source_type': 'clipboard',
|
|
938
|
+
'source_info': {
|
|
939
|
+
'method': 'system_clipboard',
|
|
940
|
+
'note': 'Image was read from current system clipboard'
|
|
941
|
+
},
|
|
942
|
+
'message': 'QR decoded from clipboard'
|
|
943
|
+
}))
|
|
944
|
+
else:
|
|
945
|
+
print(json.dumps({
|
|
946
|
+
'success': False,
|
|
947
|
+
'error': 'clipboard_failed',
|
|
948
|
+
'message': msg or 'Failed to read QR from clipboard',
|
|
949
|
+
'source_type': 'clipboard',
|
|
950
|
+
'hint': 'Ensure an image is copied to clipboard. On macOS: Cmd+Ctrl+Shift+4 to screenshot to clipboard.'
|
|
951
|
+
}))
|
|
952
|
+
return
|