重置 Git 索引,移除不需要的路径引用
This commit is contained in:
parent
e634f7eaee
commit
e80ba4d90a
|
@ -0,0 +1,11 @@
|
||||||
|
# 对所有文本文件统一使用 LF 作为行尾字符
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# 指定特定文件使用 CRLF
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# 二进制文件不进行行尾转换
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.gif binary
|
|
@ -0,0 +1,3 @@
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
/build/
|
|
@ -0,0 +1,32 @@
|
||||||
|
/data/
|
||||||
|
|
||||||
|
# Python:
|
||||||
|
*.ipynb
|
||||||
|
*/__pycache__
|
||||||
|
/.vagrant
|
||||||
|
/scrapy.iml
|
||||||
|
*.pyc
|
||||||
|
_trial_temp*
|
||||||
|
dropin.cache
|
||||||
|
docs/build
|
||||||
|
*egg-info
|
||||||
|
.tox
|
||||||
|
venv
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
.idea
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage.*
|
||||||
|
.cache/
|
||||||
|
.mypy_cache/
|
||||||
|
/tests/keys/localhost.crt
|
||||||
|
/tests/keys/localhost.key
|
||||||
|
key.txt
|
||||||
|
key.txt.pub
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
|
@ -0,0 +1,3 @@
|
||||||
|
# payment
|
||||||
|
|
||||||
|
支付系统
|
|
@ -0,0 +1 @@
|
||||||
|
from tronscan import Tronscan
|
|
@ -0,0 +1,411 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from custom_decorators import singleton
|
||||||
|
from utils.tronscan import convert_to_tronscan_timestamp
|
||||||
|
|
||||||
|
trc20token_info = {
|
||||||
|
"usdt": {"tokenId": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
||||||
|
"tokenAbbr": "USDT",
|
||||||
|
"tokenName": "Tether USD",
|
||||||
|
"tokenLogo": "https://static.tronscan.org/production/logo/usdtlogo.png",
|
||||||
|
"issuerAddr": "THPvaUhoh2Qn2y9THCZML3H815hhFhn5YC",
|
||||||
|
"vip": True}
|
||||||
|
}
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class Tronscan:
|
||||||
|
def __init__(self, api_key):
|
||||||
|
self.api_key = api_key
|
||||||
|
|
||||||
|
def accountv2(self, address):
|
||||||
|
"""
|
||||||
|
Get account detail information
|
||||||
|
:param address: Account address
|
||||||
|
:return: Returns the detail information of an account.
|
||||||
|
"""
|
||||||
|
response = requests.get(f"https://apilist.tronscanapi.com/api/accountv2?address={address}",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key})
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def transactions(self, start=0, limit=10, start_timestamp=None, end_timestamp=None,
|
||||||
|
from_address=None, to_address=None, tokens=None, block=None,
|
||||||
|
type_=None, method=None):
|
||||||
|
"""
|
||||||
|
Get a list of transactions.
|
||||||
|
:param start: Start number. Default 0
|
||||||
|
:param limit: Number of items per page. Default 10
|
||||||
|
:param start_timestamp: Start time
|
||||||
|
:param end_timestamp: End time
|
||||||
|
:param from_address: Sender's address.
|
||||||
|
:param to_address: Recipient's address.
|
||||||
|
:param tokens: Tokens involved
|
||||||
|
:param block: Block
|
||||||
|
:param type_: Transaction type
|
||||||
|
:param method: Method called in a smart contract signature. Only one value can be specified each time.
|
||||||
|
:return: Getx a list of transactions.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"sort": "-timestamp",
|
||||||
|
"count": "true",
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
if start_timestamp is not None:
|
||||||
|
params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp)
|
||||||
|
if end_timestamp is not None:
|
||||||
|
params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp)
|
||||||
|
if from_address is not None:
|
||||||
|
params["fromAddress"] = from_address
|
||||||
|
if to_address is not None:
|
||||||
|
params["toAddress"] = to_address
|
||||||
|
if tokens is not None:
|
||||||
|
params["tokens"] = tokens
|
||||||
|
if block is not None:
|
||||||
|
params["block"] = block
|
||||||
|
if type_ is not None:
|
||||||
|
params["type"] = type_
|
||||||
|
if method is not None:
|
||||||
|
params["method"] = method
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"https://apilist.tronscanapi.com/api/transaction",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def transaction_info(self, hash_):
|
||||||
|
"""
|
||||||
|
Get transaction detail information by transaction hash.
|
||||||
|
:param hash_: Transaction hash
|
||||||
|
:return: Get transaction information.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"hash": hash_,
|
||||||
|
}
|
||||||
|
response = requests.get(
|
||||||
|
"https://apilist.tronscanapi.com/api/transaction-info",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def token_trc20_transfers(self, start=None, limit=None, contract_address=None,
|
||||||
|
start_timestamp=None, end_timestamp=None, confirm=True,
|
||||||
|
related_address=None, from_address=None, to_address=None):
|
||||||
|
"""
|
||||||
|
Get the transfer list of TRC20 and TRC721 tokens.
|
||||||
|
:param start: Start number. Default 0
|
||||||
|
:param limit: Number of items per page. Default 10
|
||||||
|
:param contract_address: Contract address
|
||||||
|
:param start_timestamp: Start time
|
||||||
|
:param end_timestamp: End time
|
||||||
|
:param confirm: Whether to return confirmed transfers only. Default: True
|
||||||
|
:param related_address: Account address
|
||||||
|
:param from_address: Sender's address
|
||||||
|
:param to_address: Recipient's address
|
||||||
|
:return: Get the transfer list of TRC20 and TRC721 tokens.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"filterTokenValue": 1
|
||||||
|
}
|
||||||
|
if start is not None:
|
||||||
|
params["start"] = start
|
||||||
|
if limit is not None:
|
||||||
|
params["limit"] = limit
|
||||||
|
if contract_address is not None:
|
||||||
|
params["contract_address"] = contract_address
|
||||||
|
if start_timestamp is not None:
|
||||||
|
params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp)
|
||||||
|
if end_timestamp is not None:
|
||||||
|
params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp)
|
||||||
|
if confirm is not None:
|
||||||
|
params["confirm"] = str(confirm).lower()
|
||||||
|
if related_address is not None:
|
||||||
|
params["relatedAddress"] = related_address
|
||||||
|
if from_address is not None:
|
||||||
|
params["fromAddress"] = from_address
|
||||||
|
if to_address is not None:
|
||||||
|
params["toAddress"] = to_address
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"https://apilist.tronscanapi.com/api/token_trc20/transfers",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def transfer(self, sort="-timestamp", start=0, limit=10, count="true",
|
||||||
|
address=None, from_address=None, to_address=None, tokens=None, block=None):
|
||||||
|
"""
|
||||||
|
Get account's transfer list.
|
||||||
|
:param sort: Sort type
|
||||||
|
:param start: Start index, default is 0
|
||||||
|
:param limit: Number of transfers per page
|
||||||
|
:param count: Whether to return total transfer number.
|
||||||
|
:param address: Address, like contract address
|
||||||
|
:param from_address: Sender's address
|
||||||
|
:param to_address: Recipient's address
|
||||||
|
:param tokens: Specific tokens
|
||||||
|
:param block: Block number
|
||||||
|
:return: Get account's transfer list.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"sort": sort,
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
"count": count,
|
||||||
|
"filterTokenValue": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if address is not None:
|
||||||
|
params["address"] = address
|
||||||
|
if from_address is not None:
|
||||||
|
params["fromAddress"] = from_address
|
||||||
|
if to_address is not None:
|
||||||
|
params["toAddress"] = to_address
|
||||||
|
if tokens is not None:
|
||||||
|
params["tokens"] = tokens
|
||||||
|
if block is not None:
|
||||||
|
params["block"] = block
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"https://apilist.tronscanapi.com/api/transfer",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def internal_transactions(self, start=0, limit=10, address=None, contract=None, block=None):
|
||||||
|
"""
|
||||||
|
Get internal transaction list for a specific address or block.
|
||||||
|
:param start: Start index, default is 0
|
||||||
|
:param limit: Number of transfers per page
|
||||||
|
:param address: Specific address. At least one of address, block, or contract must be specified
|
||||||
|
:param contract: Sender's address
|
||||||
|
:param block: Block number
|
||||||
|
:return: Get the internal transaction list.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"start": start,
|
||||||
|
"limit": limit
|
||||||
|
}
|
||||||
|
|
||||||
|
if address is not None:
|
||||||
|
params["address"] = address
|
||||||
|
if contract is not None:
|
||||||
|
params["contract"] = contract
|
||||||
|
if block is not None:
|
||||||
|
params["block"] = block
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"https://apilist.tronscanapi.com/api/internal-transaction",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def token_trc20_transfers_with_status(self, start=0, limit=10, trc20Id=None, address=None,
|
||||||
|
direction=0, db_version=0, reverse="false"):
|
||||||
|
"""
|
||||||
|
Get account's transaction data.
|
||||||
|
:param start: Start index, default is 0
|
||||||
|
:param limit: Number of transfers per page
|
||||||
|
:param trc20Id: TRC20 token address
|
||||||
|
:param address: Account address
|
||||||
|
:param direction: 0 for all, 1 for transfer-out, 2 for transfer-in
|
||||||
|
:param db_version: Whether to include approval transfers. 1 for include, 0 for exclude
|
||||||
|
:param reverse: Sort by creation time. Valid values: "true" or "false"
|
||||||
|
:return: Get account's transaction data.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
"direction": direction,
|
||||||
|
"db_version": db_version,
|
||||||
|
"reverse": reverse
|
||||||
|
}
|
||||||
|
|
||||||
|
if trc20Id is not None:
|
||||||
|
params["trc20Id"] = trc20Id
|
||||||
|
if address is not None:
|
||||||
|
params["address"] = address
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"https://apilist.tronscanapi.com/api/token_trc20/transfers-with-status",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def search(self, term, type="token", start=0, limit=10):
|
||||||
|
"""
|
||||||
|
Search token/contract/account information
|
||||||
|
Note : The maximum value for limit is 50.
|
||||||
|
:param term: Search term
|
||||||
|
:param type: Search type, including "token", "address", "contract", "transaction" and "block"
|
||||||
|
:param start: Start number. Default: 0
|
||||||
|
:param limit: Number of items per page. Default: 10
|
||||||
|
:return: Returns account authorization change records.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"term": term,
|
||||||
|
"type": type,
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
}
|
||||||
|
response = requests.get("https://apilist.tronscanapi.com/api/search/v2",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def approve_change(self, contract_address, from_address, to_address, start=0, limit=20, show=3):
|
||||||
|
"""
|
||||||
|
Returns account authorization change records.
|
||||||
|
:param contract_address: Contract address
|
||||||
|
:param from_address: Originator address
|
||||||
|
:param to_address: Recipient address
|
||||||
|
:param start: Start number. Default: 0
|
||||||
|
:param limit: Number of items per page. Default: 20
|
||||||
|
:param show: Token type. 1: TRC20 2: TRC721 3: ALL(default) 4: TRC1155
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"contract_address": contract_address,
|
||||||
|
"from_address": from_address,
|
||||||
|
"to_address": to_address,
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
"show": show,
|
||||||
|
"type": "approve",
|
||||||
|
}
|
||||||
|
response = requests.get("https://apilist.tronscanapi.com/api/account/approve/change",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def transfer_trx(self, address, start_timestamp=None, end_timestamp=None, start=0, limit=20, direction=1,
|
||||||
|
db_version=0,
|
||||||
|
reverse=True, fee=False):
|
||||||
|
"""
|
||||||
|
Get the list of trx transfers related to a specific address
|
||||||
|
Note : The value sum of start and limit must be less than or equal to 10000.
|
||||||
|
:param address: Query address
|
||||||
|
:param start_timestamp: Start timestamp
|
||||||
|
:param end_timestamp: End timestamp
|
||||||
|
:param start: Start number. Default: 0
|
||||||
|
:param limit: Number of items per page. Default: 20
|
||||||
|
:param direction: Default: 1. 1 represents inbound transfers, 2 represents outbound transfers, and 0 represents both.
|
||||||
|
:param db_version: Default: 0, which indicates to filter transfers with invalid “to” or “from” addresses out.
|
||||||
|
:param reverse: Sort the data in a descending order. Default: true
|
||||||
|
:param fee: Whether to return data of TRX burning for resource consumption. Default: false
|
||||||
|
:return: Returns the list of TRX transfers for a specific address.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"address": address,
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
"direction": direction,
|
||||||
|
"db_version": db_version,
|
||||||
|
"reverse": reverse,
|
||||||
|
"fee": fee,
|
||||||
|
}
|
||||||
|
if start_timestamp is not None:
|
||||||
|
params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp)
|
||||||
|
if end_timestamp is not None:
|
||||||
|
params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp)
|
||||||
|
response = requests.get("https://apilist.tronscanapi.com/api/transfer/trx",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def transfer_token10(self, address, trc10Id, start_timestamp=None, end_timestamp=None, start=0, limit=20,
|
||||||
|
direction=1, db_version=0, reverse=True):
|
||||||
|
"""
|
||||||
|
Get the transfer list of a specific TRC10 token for a certain address
|
||||||
|
Note : The value sum of start and limit must be less than or equal to 10000.
|
||||||
|
:param address: Query address
|
||||||
|
:param trc10Id: TRC10 token ID
|
||||||
|
:param start_timestamp: Start timestamp
|
||||||
|
:param end_timestamp: End timestamp
|
||||||
|
:param start: Start number. Default: 0
|
||||||
|
:param limit: Number of items per page. Default: 20
|
||||||
|
:param direction: Default: 1. 1 represents inbound transfers, 2 represents outbound transfers, and 0 represents both.
|
||||||
|
:param db_version: Default: 0, which indicates to filter transfers with invalid “to” or “from” addresses out.
|
||||||
|
:param reverse: Sort the data in a descending order. Default: true
|
||||||
|
:return: Returns the transfer list of a TRC10 token for a specific account.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"address": address,
|
||||||
|
"trc10Id": trc10Id,
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
"direction": direction,
|
||||||
|
"db_version": db_version,
|
||||||
|
"reverse": reverse,
|
||||||
|
}
|
||||||
|
if start_timestamp is not None:
|
||||||
|
params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp)
|
||||||
|
if end_timestamp is not None:
|
||||||
|
params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp)
|
||||||
|
response = requests.get("https://apilist.tronscanapi.com/api/transfer/token10",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def transfer_trc20(self, address, trc20Id, start_timestamp=None, end_timestamp=None, start=0, limit=20,
|
||||||
|
direction=1, db_version=0, reverse=True):
|
||||||
|
"""
|
||||||
|
Get the transfer list of a specific TRC20 token for a certain address
|
||||||
|
Note : The value sum of start and limit must be less than or equal to 10000.
|
||||||
|
:param address: Query address
|
||||||
|
:param trc20Id: TRC20 token ID
|
||||||
|
:param start_timestamp: Start timestamp
|
||||||
|
:param end_timestamp: End timestamp
|
||||||
|
:param start: Start number. Default: 0
|
||||||
|
:param limit: Number of items per page. Default: 20
|
||||||
|
:param direction: Default: 1. 1 represents inbound transfers, 2 represents outbound transfers, and 0 represents both.
|
||||||
|
:param db_version: Default: 0, which indicates to filter transfers with invalid “to” or “from” addresses out.
|
||||||
|
:param reverse: Sort the data in a descending order. Default: true
|
||||||
|
:return: Returns the transfer list of a TRC20 token for a specific account.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"address": address,
|
||||||
|
"trc20Id": trc20Id,
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
"direction": direction,
|
||||||
|
"db_version": db_version,
|
||||||
|
"reverse": reverse,
|
||||||
|
}
|
||||||
|
if start_timestamp is not None:
|
||||||
|
params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp)
|
||||||
|
if end_timestamp is not None:
|
||||||
|
params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp)
|
||||||
|
response = requests.get("https://apilist.tronscanapi.com/api/transfer/trc20",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def account_wallet(self, address, asset_type=0):
|
||||||
|
"""
|
||||||
|
Get the information of tokens held and followed in the account's web wallet
|
||||||
|
:param address: Query address
|
||||||
|
:param asset_type: Asset types: 0 - All (default); 1 - Assets (TRX, TRC10, TRC20); 2 - Collectibles (TRC721 and TRC1155)
|
||||||
|
:return: Returns a list of tokens held and followed by an account.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"address": address,
|
||||||
|
"asset_type": asset_type,
|
||||||
|
}
|
||||||
|
response = requests.get("https://apilist.tronscanapi.com/api/account/wallet",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params=params)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
address = "TB592A5QwHvvcJoCmvALmzT3S9Pux91Gub"
|
||||||
|
tronscan = Tronscan(api_key='cc87d361-7cd6-4f69-a57b-f0a77a213355')
|
||||||
|
print(tronscan.transfer_trc20(address, trc20token_info["usdt"]["tokenId"]))
|
|
@ -0,0 +1,42 @@
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
|
||||||
|
from config import get_config
|
||||||
|
from services.order import OrderService
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
order_service = OrderService() # 获取单例实例
|
||||||
|
|
||||||
|
@app.route('/createOrder', methods=['POST'])
|
||||||
|
def create_order():
|
||||||
|
data = request.get_json()
|
||||||
|
phone = data.get('phone', None)
|
||||||
|
email = data.get('email', None)
|
||||||
|
address = data.get('address', None)
|
||||||
|
try:
|
||||||
|
payment_method = data['paymentMethod']
|
||||||
|
except KeyError:
|
||||||
|
return jsonify({
|
||||||
|
"message": "Unsupported payment method. Currently, only USDT payments are supported."
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
addresses = order_service.get_user_addresses(phone, email, address, payment_method)
|
||||||
|
|
||||||
|
if not addresses:
|
||||||
|
return jsonify({
|
||||||
|
"message": "No payment address associated with you was found. Please provide a payment address."
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
if len(addresses) == 1:
|
||||||
|
order_id = order_service.create_order(addresses[0])
|
||||||
|
return jsonify({"order_id": order_id}), 200
|
||||||
|
|
||||||
|
# 多个地址的情况
|
||||||
|
return jsonify({
|
||||||
|
"message": "请选择一个地址进行下单。",
|
||||||
|
"addresses": addresses
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True)
|
|
@ -0,0 +1 @@
|
||||||
|
from .utils import get_config
|
|
@ -0,0 +1,7 @@
|
||||||
|
config = {
|
||||||
|
'user': 'your_mysql_username',
|
||||||
|
'password': 'your_mysql_password',
|
||||||
|
'host': 'localhost',
|
||||||
|
'database': 'your_database_name',
|
||||||
|
'autocommit': False,
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
[APIKey]
|
||||||
|
tronscan=cc87d361-7cd6-4f69-a57b-f0a77a213355
|
||||||
|
|
||||||
|
[PaymentAddresses]
|
||||||
|
usdt=TB592A5QwHvvcJoCmvALmzT3S9Pux91Gub
|
||||||
|
|
||||||
|
[MYSQL]
|
||||||
|
user: 'your_mysql_username'
|
||||||
|
password: 'your_mysql_password'
|
||||||
|
host: 'localhost'
|
||||||
|
database: 'your_database_name'
|
||||||
|
autocommit: false
|
||||||
|
allow_multi_statements: True
|
|
@ -0,0 +1,145 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
"""
|
||||||
|
@Project :payment
|
||||||
|
@File :__init__.py.py
|
||||||
|
@IDE :PyCharm
|
||||||
|
@Author :rengengchen
|
||||||
|
@Time :2024/11/06 16:11
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
from argparse import Namespace
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
random_seed = 20240717
|
||||||
|
ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
def setup_seed(seed):
|
||||||
|
# import torch
|
||||||
|
# torch.manual_seed(seed)
|
||||||
|
# torch.cuda.manual_seed_all(seed)
|
||||||
|
# torch.backends.cudnn.deterministic = True
|
||||||
|
# import numpy as np
|
||||||
|
# np.random.seed(seed)
|
||||||
|
random.seed(seed)
|
||||||
|
|
||||||
|
|
||||||
|
setup_seed(random_seed)
|
||||||
|
|
||||||
|
|
||||||
|
class Setting:
|
||||||
|
def __init__(self,
|
||||||
|
config_parser: ConfigParser = None,
|
||||||
|
argument_parser: Namespace = None,
|
||||||
|
_visited=None,
|
||||||
|
_parent=None,
|
||||||
|
**kwargs):
|
||||||
|
self._parent = _parent
|
||||||
|
if config_parser:
|
||||||
|
for section in config_parser.sections():
|
||||||
|
section_config = Setting(_parent=self)
|
||||||
|
for option in config_parser.options(section):
|
||||||
|
section_config[option] = config_parser.get(section, option)
|
||||||
|
self.__dict__[section] = section_config
|
||||||
|
|
||||||
|
if argument_parser:
|
||||||
|
for k in vars(argument_parser):
|
||||||
|
self.__dict__[k] = getattr(argument_parser, k)
|
||||||
|
|
||||||
|
if _visited is None:
|
||||||
|
_visited = set()
|
||||||
|
self.update(_visited=_visited, **kwargs)
|
||||||
|
|
||||||
|
def update(self, _visited=None, **kwargs):
|
||||||
|
if _visited is None:
|
||||||
|
_visited = {}
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
cls_attr = getattr(self.__class__, k, None)
|
||||||
|
if callable(cls_attr):
|
||||||
|
raise KeyError(f"The key '{k}' conflicts with an existing class method. you can use {k}_ instead.")
|
||||||
|
if isinstance(v, dict):
|
||||||
|
obj_id = id(v)
|
||||||
|
if obj_id in _visited:
|
||||||
|
logger.warning(f"Circular reference detected in key: '{k}'.")
|
||||||
|
v = _visited[obj_id]
|
||||||
|
else:
|
||||||
|
v = Setting(_visited=_visited, _parent=self, **v)
|
||||||
|
_visited[obj_id] = v
|
||||||
|
self.__dict__[k] = v
|
||||||
|
|
||||||
|
def get(self, item, default=None):
|
||||||
|
if item not in self.__dict__ and self._parent is not None:
|
||||||
|
return self._parent.get(item, default)
|
||||||
|
return default
|
||||||
|
|
||||||
|
def get_int(self, item):
|
||||||
|
return int(self.get(item))
|
||||||
|
|
||||||
|
def get_float(self, item):
|
||||||
|
return float(self.get(item))
|
||||||
|
|
||||||
|
def get_bool(self, item):
|
||||||
|
return bool(self.get(item))
|
||||||
|
|
||||||
|
def set(self, item, value):
|
||||||
|
self.__dict__[item] = value
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return self.__dict__[item]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.__dict__[key] = value
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
return self.get(key)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
def _str_helper(config, indent=0, visited=None):
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
lines = []
|
||||||
|
indent_str = ' ' * indent
|
||||||
|
for key, value in config.__dict__.items():
|
||||||
|
if key.startswith('_'):
|
||||||
|
continue
|
||||||
|
if isinstance(value, Setting):
|
||||||
|
if id(value) in visited:
|
||||||
|
lines.append(f"{indent_str}{key}: <Circular Reference>")
|
||||||
|
else:
|
||||||
|
visited.add(id(value))
|
||||||
|
lines.append(f"{indent_str}{key}:")
|
||||||
|
lines.append(_str_helper(value, indent + 1, visited))
|
||||||
|
else:
|
||||||
|
lines.append(f"{indent_str}{key}: {value}")
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
return _str_helper(self)
|
||||||
|
|
||||||
|
|
||||||
|
def log_config(config):
|
||||||
|
# fmt = '%(asctime)s [%(name)s] %(levelname)s: %(message)s'
|
||||||
|
# datefmt = "%Y-%m-%d %H:%M:%S"
|
||||||
|
logger.remove()
|
||||||
|
logger.add(sys.stdout, level=config.log_level)
|
||||||
|
logger.add(sys.stderr, level="ERROR")
|
||||||
|
logger.add(os.path.join(ROOT_DIR, "logs", "{time}.log"), level="DEBUG", encoding='utf8', rotation="100 MB",
|
||||||
|
retention=3)
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(config_file=fr'{ROOT_DIR}/config/param.ini') -> Setting:
|
||||||
|
requests.adapters.DEFAULT_RETRIES = 3
|
||||||
|
|
||||||
|
configparser = ConfigParser()
|
||||||
|
configparser.read(config_file)
|
||||||
|
parser = argparse.ArgumentParser(description='payment system')
|
||||||
|
parser.add_argument("--seed", type=int, default=2024)
|
||||||
|
args = parser.parse_args()
|
||||||
|
config = Setting(configparser, args)
|
||||||
|
return config
|
|
@ -0,0 +1,17 @@
|
||||||
|
def singleton(cls):
|
||||||
|
"""
|
||||||
|
Decorator for making a class a singleton.
|
||||||
|
This ensures that only one instance of the class exists.
|
||||||
|
"""
|
||||||
|
instances = {} # Dictionary to store the instance of the singleton class
|
||||||
|
|
||||||
|
def get_instance(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
If an instance of the class does not exist, create one and store it.
|
||||||
|
If it exists, return the existing instance.
|
||||||
|
"""
|
||||||
|
if cls not in instances:
|
||||||
|
instances[cls] = cls(*args, **kwargs)
|
||||||
|
return instances[cls]
|
||||||
|
|
||||||
|
return get_instance
|
|
@ -0,0 +1,66 @@
|
||||||
|
from loguru import logger
|
||||||
|
from mysql.connector import connect, Error, OperationalError
|
||||||
|
from mysql.connector import errors as db_errors
|
||||||
|
|
||||||
|
from custom_decorators import singleton
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class Database:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.connection = None
|
||||||
|
self.config = config
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Establish a new database connection."""
|
||||||
|
try:
|
||||||
|
self.connection = connect(**self.config)
|
||||||
|
if self.connection.is_connected():
|
||||||
|
logger.info("Connected to MySQL database")
|
||||||
|
except Error as e:
|
||||||
|
logger.info(f"Error while connecting to MySQL: {e}")
|
||||||
|
self.connection = None
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
"""Get the database connection, with reconnection logic."""
|
||||||
|
if self.connection is None or not self.connection.is_connected():
|
||||||
|
logger.info("Reconnecting to the database...")
|
||||||
|
self.connect()
|
||||||
|
return self.connection
|
||||||
|
|
||||||
|
def close_connection(self):
|
||||||
|
if self.connection and self.connection.is_connected():
|
||||||
|
self.connection.close()
|
||||||
|
logger.info("MySQL connection is closed")
|
||||||
|
|
||||||
|
def execute_query(self, query, params=None):
|
||||||
|
"""Execute a query with optional parameters, supports transactions."""
|
||||||
|
cursor = None
|
||||||
|
try:
|
||||||
|
connection = self.get_connection()
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(query, params)
|
||||||
|
return cursor
|
||||||
|
except OperationalError as e:
|
||||||
|
logger.info(f"Operational error: {e}. Attempting to reconnect...")
|
||||||
|
self.connect()
|
||||||
|
cursor = self.get_connection().cursor()
|
||||||
|
cursor.execute(query, params)
|
||||||
|
return cursor
|
||||||
|
except db_errors.Error as e:
|
||||||
|
logger.info(f"Database error: {e}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
"""Commit the current transaction."""
|
||||||
|
if self.connection:
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
"""Rollback the current transaction."""
|
||||||
|
if self.connection:
|
||||||
|
self.connection.rollback()
|
|
@ -0,0 +1,93 @@
|
||||||
|
from utils.database import pack_params
|
||||||
|
|
||||||
|
|
||||||
|
class User:
|
||||||
|
def __init__(self, id_=None, name=None, phone=None, email=None, address=None, payment_method=None):
|
||||||
|
self.id = id_
|
||||||
|
self.name = name
|
||||||
|
self.phone = phone
|
||||||
|
self.email = email
|
||||||
|
self.address = address
|
||||||
|
self.payment_method = payment_method
|
||||||
|
|
||||||
|
def insert_sql(self, params_format="list"):
|
||||||
|
params_sql, params = pack_params(params_format=params_format, param_sql="{param}", join_str=",",
|
||||||
|
name=self.name, phone=self.phone, email=self.email, address=self.address,
|
||||||
|
payment_method=self.payment_method)
|
||||||
|
return f"INSERT INTO user ({params_sql}) VALUES ({','.join('%s' for _ in params)})", params
|
||||||
|
|
||||||
|
def select_sql(self, condition="AND", params_format="list"):
|
||||||
|
params_sql, params = pack_params(params_format=params_format, param_sql="{param}=%s", join_str=f" {condition} ",
|
||||||
|
name=self.name, phone=self.phone, email=self.email, address=self.address,
|
||||||
|
payment_method=self.payment_method)
|
||||||
|
return f"SELECT id, name, phone, email, address, payment_method FROM user WHERE {params_sql}", params
|
||||||
|
|
||||||
|
def exists_sql(self, condition="AND", params_format="list"):
|
||||||
|
params_sql, params = pack_params(params_format=params_format, param_sql="{param}=%s", join_str=f" {condition} ",
|
||||||
|
name=self.name, phone=self.phone, email=self.email, address=self.address,
|
||||||
|
payment_method=self.payment_method)
|
||||||
|
return f"SELECT id FROM user WHERE {params_sql} LIMIT 1", params
|
||||||
|
|
||||||
|
def params(self, format="dict"):
|
||||||
|
if format == "list":
|
||||||
|
params = []
|
||||||
|
elif format == "dict":
|
||||||
|
params = {}
|
||||||
|
else:
|
||||||
|
raise ValueError("format must be list or dict")
|
||||||
|
if self.uid:
|
||||||
|
if format == "list":
|
||||||
|
params.append(self.uid)
|
||||||
|
elif format == "dict":
|
||||||
|
params["uid"] = self.uid
|
||||||
|
if self.name:
|
||||||
|
if format == "list":
|
||||||
|
params.append(self.name)
|
||||||
|
elif format == "dict":
|
||||||
|
params["name"] = self.name
|
||||||
|
if self.phone:
|
||||||
|
if format == "list":
|
||||||
|
params.append(self.phone)
|
||||||
|
elif format == "dict":
|
||||||
|
params["phone"] = self.phone
|
||||||
|
if self.email:
|
||||||
|
if format == "list":
|
||||||
|
params.append(self.email)
|
||||||
|
elif format == "dict":
|
||||||
|
params["email"] = self.email
|
||||||
|
if self.address:
|
||||||
|
if format == "list":
|
||||||
|
params.append(self.address)
|
||||||
|
elif format == "dict":
|
||||||
|
params["address"] = self.address
|
||||||
|
if self.payment_method:
|
||||||
|
if format == "list":
|
||||||
|
params.append(self.payment_method)
|
||||||
|
elif format == "dict":
|
||||||
|
params["payment_method"] = self.payment_method
|
||||||
|
return params
|
||||||
|
|
||||||
|
def get_difference(self, other_user):
|
||||||
|
different_attrs = {}
|
||||||
|
if self.uid != other_user.uid:
|
||||||
|
different_attrs["uid"] = (self.uid, other_user.uid)
|
||||||
|
if self.name != other_user.name:
|
||||||
|
different_attrs["name"] = (self.name, other_user.name)
|
||||||
|
if self.phone != other_user.phone:
|
||||||
|
different_attrs["phone"] = (self.phone, other_user.phone)
|
||||||
|
if self.email != other_user.email:
|
||||||
|
different_attrs["email"] = (self.email, other_user.email)
|
||||||
|
if self.address != other_user.address:
|
||||||
|
different_attrs["address"] = (self.address, other_user.address)
|
||||||
|
if self.payment_method != other_user.payment_method:
|
||||||
|
different_attrs["payment_method"] = (self.payment_method, other_user.payment_method)
|
||||||
|
return different_attrs
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, User):
|
||||||
|
return ((self.name, self.phone, self.email, self.address, self.payment_method)
|
||||||
|
== (other.name, other.phone, other.email, other.address, other.payment_method))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.name, self.phone, self.email, self.address, self.payment_method))
|
|
@ -0,0 +1,41 @@
|
||||||
|
from ruamel_yaml.util import create_timestamp
|
||||||
|
|
||||||
|
from custom_decorators import singleton
|
||||||
|
from database import Database
|
||||||
|
from utils.datetime import current_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class OrderRepository:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.db = Database(config['MYSQL'])
|
||||||
|
|
||||||
|
def create(self, order_id, from_address, to_address):
|
||||||
|
cur_time = current_timestamp()
|
||||||
|
try:
|
||||||
|
self.db.execute_query(
|
||||||
|
"INSERT INTO orders (order_id, from_address, to_address, create_timestamp, update_timestamp) "
|
||||||
|
"VALUES (%s, %s, %s, %s, %s)",
|
||||||
|
[order_id, from_address, to_address, cur_time, cur_time]
|
||||||
|
)
|
||||||
|
self.db.commit()
|
||||||
|
except Exception:
|
||||||
|
self.db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def update_status(self, order_id, status):
|
||||||
|
try:
|
||||||
|
self.db.execute_query("UPDATE orders "
|
||||||
|
"SET status = %s, update_timestamp = %s "
|
||||||
|
"WHERE order_id = %s",
|
||||||
|
[status, current_timestamp(), order_id])
|
||||||
|
self.db.commit()
|
||||||
|
except Exception:
|
||||||
|
self.db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_order_info(self, order_id):
|
||||||
|
self.db.execute_query("SELECT quant, from_address, to_address, create_timestamp "
|
||||||
|
"FROM orders "
|
||||||
|
"WHERE order_id = %s",
|
||||||
|
[order_id])
|
|
@ -0,0 +1,74 @@
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from custom_decorators import singleton
|
||||||
|
from database import Database
|
||||||
|
from models import User
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class UserRepository:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.db = Database(config['MYSQL'])
|
||||||
|
|
||||||
|
def get_or_create(self, user):
|
||||||
|
users = []
|
||||||
|
cursor = self.db.execute_query(*user.select_sql(condition="OR"))
|
||||||
|
same_users = cursor.fetchall()
|
||||||
|
new_user = not len(same_users)
|
||||||
|
# 对用户已存在的属性判断是否有新属性
|
||||||
|
update_user = set()
|
||||||
|
update_sqls = []
|
||||||
|
update_params_list = []
|
||||||
|
delete_params = []
|
||||||
|
exist_conflicting_attr = False
|
||||||
|
for same_user in same_users:
|
||||||
|
exist_conflicting_attr = False
|
||||||
|
different_attrs = user.get_difference(same_user)
|
||||||
|
# 用于判断是否有新属性
|
||||||
|
update_sql_params = []
|
||||||
|
update_params = []
|
||||||
|
for k, v in different_attrs.items():
|
||||||
|
new_attr, exist_attr = v
|
||||||
|
if exist_attr is None:
|
||||||
|
setattr(same_user, k, new_attr)
|
||||||
|
update_sql_params.append(f"{k}=%s")
|
||||||
|
update_params.append(new_attr)
|
||||||
|
else:
|
||||||
|
# 出现冲突的属性,考虑新增一行记录
|
||||||
|
exist_conflicting_attr = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if same_user in update_user:
|
||||||
|
delete_params.append((same_user.id,))
|
||||||
|
else:
|
||||||
|
users.append(same_user)
|
||||||
|
exist_new_attr = bool(update_params)
|
||||||
|
if exist_new_attr:
|
||||||
|
update_user.add(same_user)
|
||||||
|
update_sqls.append(f'UPDATE user SET {",".join(update_sql_params)} WHERE id=%s;')
|
||||||
|
update_params.append(same_user.id)
|
||||||
|
update_params_list.append(update_params)
|
||||||
|
sql_flag = False
|
||||||
|
try:
|
||||||
|
if delete_params:
|
||||||
|
sql_flag = True
|
||||||
|
self.db.get_connection().cursor().executemany("DELETE FROM user WHERE id=%s", delete_params)
|
||||||
|
if update_user:
|
||||||
|
sql_flag = True
|
||||||
|
self.db.get_connection().cursor().execute("".join(update_sqls),
|
||||||
|
list(itertools.chain.from_iterable(update_params_list)),
|
||||||
|
multi=True)
|
||||||
|
if sql_flag:
|
||||||
|
self.db.commit()
|
||||||
|
except Exception:
|
||||||
|
self.db.rollback()
|
||||||
|
raise
|
||||||
|
if new_user or exist_conflicting_attr:
|
||||||
|
try:
|
||||||
|
self.db.execute_query(*user.insert_sql())
|
||||||
|
self.db.commit()
|
||||||
|
except Exception:
|
||||||
|
self.db.rollback()
|
||||||
|
raise
|
||||||
|
users.append(user)
|
||||||
|
return users
|
|
@ -0,0 +1,3 @@
|
||||||
|
loguru==0.7.2
|
||||||
|
mysql-connector-python==9.1.0
|
||||||
|
Requests==2.32.3
|
|
@ -0,0 +1,58 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from custom_decorators import singleton
|
||||||
|
from models import User
|
||||||
|
from repositories.order import OrderRepository
|
||||||
|
from repositories.user import UserRepository
|
||||||
|
from services.payment import PaymentService
|
||||||
|
from utils.datetime import current, current_timestamp, is_time_difference_greater_than
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class OrderService:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.payment_service = PaymentService()
|
||||||
|
self.order_repo = OrderRepository(config)
|
||||||
|
self.user_repo = UserRepository(config)
|
||||||
|
|
||||||
|
def get_user_addresses(self, phone=None, email=None, address=None, payment_method=None):
|
||||||
|
if address is None:
|
||||||
|
if phone or email:
|
||||||
|
users = self.user_repo.get_or_create(User(phone=phone, email=email))
|
||||||
|
addresses = set(user.address for user in users if address)
|
||||||
|
return list(addresses)
|
||||||
|
raise ValueError('A phone number, email, or address is required.')
|
||||||
|
return [address]
|
||||||
|
|
||||||
|
def create_order(self, address=None):
|
||||||
|
date_str = current().strftime('%Y%m%d%H%M%S')
|
||||||
|
unique_id = str(uuid.uuid4()).split('-')[0]
|
||||||
|
order_id = f"{date_str}-{unique_id}"
|
||||||
|
|
||||||
|
self.order_repo.create(order_id, address,
|
||||||
|
self.config['PaymentAddresses'])
|
||||||
|
return order_id
|
||||||
|
|
||||||
|
def finish_order(self, order_id):
|
||||||
|
# 判断支付时间是否超过订单存活时间
|
||||||
|
quant, from_address, to_address, create_timestamp = self.order_repo.get_order_info(order_id)
|
||||||
|
current = current_timestamp()
|
||||||
|
status = 0
|
||||||
|
if is_time_difference_greater_than(create_timestamp, current, minutes=15):
|
||||||
|
# 订单超时
|
||||||
|
status = 4
|
||||||
|
else:
|
||||||
|
correct_quant, confirmed = self.payment_service.check_payment(quant, from_address, to_address, create_timestamp, current)
|
||||||
|
if correct_quant and confirmed:
|
||||||
|
# 支付成功
|
||||||
|
status = 1
|
||||||
|
elif correct_quant < 0:
|
||||||
|
# 没有转账
|
||||||
|
status = 2
|
||||||
|
elif confirmed:
|
||||||
|
# 金额不对
|
||||||
|
status = 3
|
||||||
|
if status:
|
||||||
|
self.order_repo.update_status(order_id, status)
|
||||||
|
return status
|
|
@ -0,0 +1,23 @@
|
||||||
|
from api import Tronscan
|
||||||
|
from custom_decorators import singleton
|
||||||
|
from utils.datetime import current_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class PaymentService:
|
||||||
|
def __init__(self, api_key):
|
||||||
|
self.tronscan = Tronscan(api_key)
|
||||||
|
|
||||||
|
def check_payment(self, quant, from_address, to_address, order_create_timestamp, end_timestamp=None):
|
||||||
|
if end_timestamp is None:
|
||||||
|
end_timestamp = current_timestamp()
|
||||||
|
result = self.tronscan.token_trc20_transfers(limit=100,
|
||||||
|
from_address=from_address, to_address=to_address,
|
||||||
|
start_timestamp=order_create_timestamp, end_timestamp=end_timestamp)
|
||||||
|
if result['rangeTotal'] == 0:
|
||||||
|
return -1, 0
|
||||||
|
token_transfers = result['token_transfers']
|
||||||
|
token_transfer = token_transfers[-1]
|
||||||
|
confirmed = token_transfer['confirmed']
|
||||||
|
correct_quant = int(quant == (token_transfer['quant'] / 6))
|
||||||
|
return correct_quant, confirmed
|
|
@ -0,0 +1,6 @@
|
||||||
|
from custom_decorators import singleton
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class UserService:
|
||||||
|
pass
|
|
@ -0,0 +1,35 @@
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from api import Tronscan
|
||||||
|
|
||||||
|
|
||||||
|
class TestExternalAPICalls(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Setup code runs before every test method
|
||||||
|
self.tronscan = Tronscan(api_key='cc87d361-7cd6-4f69-a57b-f0a77a213355')
|
||||||
|
|
||||||
|
@unittest.skip("Skipping this test temporarily")
|
||||||
|
@patch('requests.get')
|
||||||
|
def test_real_api_call(self, mock_get):
|
||||||
|
# Mocking API response
|
||||||
|
mock_response = {
|
||||||
|
"data": [{"name": "ExampleToken", "symbol": "ETK"}]
|
||||||
|
}
|
||||||
|
mock_get.return_value.status_code = 200
|
||||||
|
mock_get.return_value.json.return_value = mock_response
|
||||||
|
|
||||||
|
# Call the method
|
||||||
|
response = self.tronscan.search(term="example", type="token")
|
||||||
|
|
||||||
|
# Assert the response
|
||||||
|
self.assertEqual(response, mock_response)
|
||||||
|
mock_get.assert_called_once_with(
|
||||||
|
"https://apilist.tronscanapi.com/api/search/v2",
|
||||||
|
headers={'TRON-PRO-API-KEY': self.api_key},
|
||||||
|
params={"term": "example", "type": "token", "start": 0, "limit": 10}
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
|
@ -0,0 +1,16 @@
|
||||||
|
def pack_params(params_format="list", param_sql="{param}=%s", join_str=" AND ", **kwargs):
|
||||||
|
if params_format == "list":
|
||||||
|
params = []
|
||||||
|
elif params_format == "dict":
|
||||||
|
params = {}
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown params format")
|
||||||
|
param_sqls = []
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
if v is not None:
|
||||||
|
if params_format == "list":
|
||||||
|
params.append(v)
|
||||||
|
elif params_format == "dict":
|
||||||
|
params[k] = v
|
||||||
|
param_sqls.append(param_sql.format(param=k))
|
||||||
|
return join_str.join(param_sqls), params
|
|
@ -0,0 +1,35 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def current():
|
||||||
|
return datetime.datetime.now()
|
||||||
|
|
||||||
|
def current_timestamp():
|
||||||
|
datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
|
def is_time_difference_greater_than(timestamp1, timestamp2, hours=0, minutes=0, seconds=0):
|
||||||
|
"""
|
||||||
|
判断两个时间戳的时间差是否大于指定的小时、分钟和秒数
|
||||||
|
|
||||||
|
参数:
|
||||||
|
timestamp1 (int): 第一个时间戳
|
||||||
|
timestamp2 (int): 第二个时间戳
|
||||||
|
hours (int): 要比较的小时数,默认是0小时
|
||||||
|
minutes (int): 要比较的分钟数,默认是0分钟
|
||||||
|
seconds (int): 要比较的秒数,默认是0秒
|
||||||
|
|
||||||
|
返回:
|
||||||
|
bool: 如果时间差大于指定的小时、分钟和秒数返回True,否则返回False
|
||||||
|
"""
|
||||||
|
# 将时间戳转换为 datetime 对象
|
||||||
|
time1 = datetime.fromtimestamp(timestamp1)
|
||||||
|
time2 = datetime.fromtimestamp(timestamp2)
|
||||||
|
|
||||||
|
# 计算时间差
|
||||||
|
time_difference = abs(time2 - time1)
|
||||||
|
|
||||||
|
# 计算指定的时间差值
|
||||||
|
threshold = datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
||||||
|
|
||||||
|
# 判断时间差是否大于指定的时间
|
||||||
|
return time_difference > threshold
|
|
@ -0,0 +1,2 @@
|
||||||
|
def convert_to_tronscan_timestamp(timestamp):
|
||||||
|
return int(timestamp * 1000)
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Python:
|
||||||
|
*.ipynb
|
||||||
|
*/__pycache__
|
||||||
|
/.vagrant
|
||||||
|
/scrapy.iml
|
||||||
|
*.pyc
|
||||||
|
_trial_temp*
|
||||||
|
dropin.cache
|
||||||
|
docs/build
|
||||||
|
*egg-info
|
||||||
|
.tox
|
||||||
|
venv
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
.idea
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage.*
|
||||||
|
.cache/
|
||||||
|
.mypy_cache/
|
||||||
|
/tests/keys/localhost.crt
|
||||||
|
/tests/keys/localhost.key
|
||||||
|
key.txt
|
||||||
|
key.txt.pub
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
|
@ -0,0 +1,131 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>创建订单</title>
|
||||||
|
<style>
|
||||||
|
/* 简单的样式 */
|
||||||
|
.hidden { display: none; }
|
||||||
|
.error { color: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>创建订单</h1>
|
||||||
|
<form id="order-form">
|
||||||
|
<label for="phone">手机号:</label>
|
||||||
|
<input type="text" id="phone" name="phone"><br><br>
|
||||||
|
|
||||||
|
<label for="email">邮箱:</label>
|
||||||
|
<input type="email" id="email" name="email"><br><br>
|
||||||
|
|
||||||
|
<label for="address">地址:</label>
|
||||||
|
<input type="text" id="address" name="address"><br><br>
|
||||||
|
|
||||||
|
<button type="submit">提交订单</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="message" class="hidden"></div>
|
||||||
|
<div id="addresses" class="hidden">
|
||||||
|
<h2>请选择一个地址:</h2>
|
||||||
|
<ul id="address-list"></ul>
|
||||||
|
</div>
|
||||||
|
<div id="error" class="error hidden"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function createOrder(phone = "", email = "", address = "") {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/create_order', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ phone, email, address })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
if (data.order_id) {
|
||||||
|
handleOrderSuccess(data.order_id);
|
||||||
|
} else if (data.addresses && Array.isArray(data.addresses)) {
|
||||||
|
handleMultipleAddresses(data.addresses);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.message === "地址不能为空,请输入地址。") {
|
||||||
|
promptUserForAddress();
|
||||||
|
} else {
|
||||||
|
handleError(data.message || '发生未知错误。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('网络或服务器错误:', error);
|
||||||
|
handleError('网络或服务器错误,请稍后再试。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOrderSuccess(orderId) {
|
||||||
|
document.getElementById('message').innerText = `订单创建成功!您的订单号是:${orderId}`;
|
||||||
|
document.getElementById('message').classList.remove('hidden');
|
||||||
|
document.getElementById('addresses').classList.add('hidden');
|
||||||
|
document.getElementById('error').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMultipleAddresses(addresses) {
|
||||||
|
const addressList = document.getElementById('address-list');
|
||||||
|
addressList.innerHTML = ''; // 清空现有列表
|
||||||
|
|
||||||
|
addresses.forEach((addr, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.innerText = `选择地址 ${index + 1}`;
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
// 选择地址后重新提交订单
|
||||||
|
createOrder(null, null, addr);
|
||||||
|
});
|
||||||
|
li.innerText = addr + ' ';
|
||||||
|
li.appendChild(button);
|
||||||
|
addressList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('addresses').classList.remove('hidden');
|
||||||
|
document.getElementById('message').classList.add('hidden');
|
||||||
|
document.getElementById('error').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptUserForAddress() {
|
||||||
|
const address = prompt('地址不能为空,请输入您的地址:');
|
||||||
|
if (address && address.trim() !== "") {
|
||||||
|
createOrder(null, null, address.trim());
|
||||||
|
} else {
|
||||||
|
handleError('地址输入为空,无法创建订单。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(message) {
|
||||||
|
const errorDiv = document.getElementById('error');
|
||||||
|
errorDiv.innerText = `错误:${message}`;
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
document.getElementById('message').classList.add('hidden');
|
||||||
|
document.getElementById('addresses').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定表单提交事件
|
||||||
|
document.getElementById('order-form').addEventListener('submit', function(event) {
|
||||||
|
event.preventDefault(); // 阻止表单默认提交行为
|
||||||
|
|
||||||
|
// 获取表单输入值
|
||||||
|
const phone = document.getElementById('phone').value.trim();
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
const address = document.getElementById('address').value.trim();
|
||||||
|
|
||||||
|
// 清除之前的消息
|
||||||
|
document.getElementById('message').classList.add('hidden');
|
||||||
|
document.getElementById('addresses').classList.add('hidden');
|
||||||
|
document.getElementById('error').classList.add('hidden');
|
||||||
|
|
||||||
|
// 调用createOrder函数
|
||||||
|
createOrder(phone, email, address);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
console.log('Happy developing ✨')
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "payment_headend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
Loading…
Reference in New Issue