今年年初的时候,我写过如何用 Stripe 收款。但是 Stripe 的注册门槛确实要高一些,这一点就足够劝退很多人了。
退而求其次,用加密货币来收款也是可以的。区块链上所有的交易都是公开的,因此我们可以通过这一点来验证用户的付款情况。
想要验证用户付款,包括但不限于如下方式:
- 每笔交易都生成一对密钥,对应一个付款地址。密钥管理会非常麻烦
- 有些交易可以添加一个描述字段,用于区分。像极了给对公账户转账时的OCR/Reference
- 尾数标记,略微改变付款金额,比如同样需要付款1 USDT,可以一个用户需要付款1.000001另外一个需要付款1.000002
前几天了解到 HD 钱包(高清钱包 分层确定性钱包 Hierarchical Deterministic Wallet),发现这种技术非常有趣。它允许通过单个种子源生成一整套密钥对(即公钥和私钥),像树形结构一样。有一个根,就可以计算出树上任意一个节点的公私钥。
那这就刚刚好,每一笔交易我都给树上增加一个叶子节点,只要有根和路径,就可以计算出这个节点的公私钥。
这里我选择 Tron的TRX,主要原因还是转账手续费比较低😂
钱包路径
从树根走到某一个叶子,肯定要一个特定的路径的。对于 tron来说,一个典型的路径是这样的
m/44'/195'/0'/0/0
- m/44表示BIP44标准
- 195表示 TRON,每种加密货币都分配有一个唯一的编号
- 0'表示钱包的第一个账户,创建多个账户那就是1‘ 2’
- 0 链接层级 一般0表示接收 1表示找零
- 0 特定地址索引,改变这个数字就会生成新的地址
由于我们想每一笔交易都生成不同的地址,那么我们可以这样设想
- m/44'/195'/0'/0/0 这个路径用于保存所有的余额
- m/44'/195'/1'/0/(0-10000) 每一笔交易请求都通过1钱包的不同索引生成不同地址
- 每一笔交易确认后,都计算出这个地址的私钥,然后把钱转到0账户中
库
Python 中有很多库可以处理加密货币。比如 python-hdwallet 实现了一百多种货币的HD钱包;我们本次的目的非常简单,只使用 TRON,那么就用 tronpy 好了
生成助记词
这个助记词也就是所谓的根密钥,可以拿来生成下面任意节点的密钥。可不能泄漏出去,要不然所有的节点的密钥都可以被推断出来。
from tronpy.hdwallet import generate_mnemonic m = generate_mnemonic(12, "english") print(m)
连接区块链网络
TRON 有如下几个网络:
- mainnet 主网,里面的钱是具有经济价值的,意味着可以兑换法币
- shasta、nile 测试网,里面的钱没经济价值,完全测试用
由于不同国家和地区对于加密货币的法律政策限制不同,同时我并不想真正的去花钱,那么测试网就是再好不过的了。
from tronpy import Tron MNEMONIC = "slogan reduce clap umbrella liquid crumble outer exchange quiz promote owner buffalo" client = Tron(network="nile") addr = client.generate_address_from_mnemonic(MNEMONIC, "", "m/44'/195'/0'/0/0") print(addr["base58check_address"])
输出如下
TERbSCnpFp4FG6SLpUHpFRBRT69ZhHbLFp
你看我们这里使用了 account path,这个0'/0/0
就是我们最终集资的钱包
为每笔交易生成收款地址
我们使用 1'账户,然后需要在数据库中记录下索引,每笔交易都要生成不同的索引,自增就可以了,要记住这个索引,否则就没办法生成私钥了
addr = client.generate_address_from_mnemonic(MNEMONIC, "", "m/44'/195'/1'/0/1") print(addr["base58check_address"])
输出如下,这个地址就是用户要打钱的地方
TD2g6siz3NP4wcjP5iAueW9tottaAgP98a
模拟用户
模拟用户付钱,那么就要先弄到一个有数字货币的钱包地址。可是这又是测试网,没办法真正兑换法币的。不慌,有网站可以免费领用于测试的币,如: https://nileex.io/join/getJoinPage
同样生成一个地址,记住地址和私钥,然后页面提交就好了。
为了方便实用,我们可以封装一个简单的命令行小程序
付款
client.trx.transfer(from_, to, amount).build().inspect().sign(priv).broadcast() print(t.wait())
amount
是金额,1_000_000
是 1 TRX
priv
是私钥,如果通过助记词和路径生成key,可以使用如下代码
def mnemonic_to_private_key(): seed = seed_from_mnemonic(mnemonic, passphrase="") private_key = key_from_seed(seed, account_path="m/44'/195'/0'/0/0") return PrivateKey(private_key)
验证付款
简单的轮询即可
# 获取地址信息 client.get_account("TD2g6siz3NP4wcjP5iAueW9tottaAgP98a") 获取地址余额,返回值类型是 Decimal 避免出现 0.1 + 0.2 = 0.30000000000000004 的悲剧 client.get_account_balance("TD2g6siz3NP4wcjP5iAueW9tottaAgP98a")
验证成功后转钱到集中地址
和付款一样,把 To 改成自己的地址就好。如果你想也可以弄成交易所给的地址、或者本地钱包软件给的地址。
在转账失败时,应该把金额减少 1.1 TRX用于付矿工费
使用 Telegram Bot处理
理解了以上所有步骤之后,简单封装一下就可以了,下面是一组示例代码,使用 apscheduler 轮询获取付款状态。简单起见没有引入数据库,正常要用起来的话,还是要把一些关键的信息,如付款地址、路径等写入到数据库中的。
import logging import telebot from apscheduler.schedulers.background import BackgroundScheduler from telebot.types import Message from tronpy import Tron from tronpy.exceptions import TransactionError, ValidationError from tronpy.hdwallet import key_from_seed, seed_from_mnemonic from tronpy.keys import PrivateKey logging.basicConfig(level=logging.INFO) bot = telebot.TeleBot("12345") index = 0 # address: {user_id: 123, path: "m/44'/195'/1'/0/0" unpaid_map = {} mnemonic = "slogan reduce clap umbrella liquid crumble outer exchange quiz promote owner buffalo" class TronTrx: def __init__(self): self.client = Tron(network="nile") self.central = "m/44'/195'/0'/0/0" def mnemonic_to_private_key(self, path): seed = seed_from_mnemonic(mnemonic, passphrase="") private_key = key_from_seed(seed, account_path=path) return PrivateKey(private_key) def central_wallet(self): return self.client.generate_address_from_mnemonic(mnemonic, "", self.central)["base58check_address"] def generate_address(self, user_id): global index path = f"m/44'/195'/1'/0/{index}" logging.info("Generating address for user %s with path %s", user_id, path) addr = self.client.generate_address_from_mnemonic(mnemonic, "", path)["base58check_address"] unpaid_map[addr] = {"user_id": user_id, "path": path} index += 1 return addr def transfer(self, from_: str, to: str, amount: int): logging.info("Transfer %s TRX from %s to %s", amount, from_, to) from_path = unpaid_map[from_]["path"] try: ( self.client.trx.transfer(from_, to, amount) .build() .sign(self.mnemonic_to_private_key(from_path)) .broadcast() ) except (TransactionError, ValidationError): logging.warning("Balance not enough, try to transfer %s TRX", amount - 1_100_000) ( self.client.trx.transfer(from_, to, amount - 1_100_000) .build() .sign(self.mnemonic_to_private_key(from_path)) .broadcast() ) def verify_payment(self): logging.info("Checking %s unpaid payment", len(unpaid_map)) for addr, meta in unpaid_map.items(): try: balance = self.client.get_account_balance(addr) except: balance = 0 if balance: logging.info("addr %s has %s TRX", addr, balance) self.transfer(addr, self.central_wallet(), int(balance * 1_000_000)) bot.send_message(meta["user_id"], f"You have paid {balance} TRX") del unpaid_map.copy()[addr] trx = TronTrx() @bot.message_handler(commands=["pay"]) def send_address(message: Message): bot.send_message(message.chat.id, trx.generate_address(message.chat.id)) @bot.message_handler(commands=["start"]) def send_start(message: Message): bot.send_message(message.chat.id, "hello") if __name__ == "__main__": scheduler = BackgroundScheduler() scheduler.add_job(trx.verify_payment, "interval", seconds=10, max_instances=1) scheduler.start() bot.infinity_polling()