├── .gitignore ├── README.md ├── btc ├── client.go ├── client_test.go ├── util.go ├── wallet.go ├── wallet_test.go ├── web.go └── web_test.go ├── client └── http.go ├── cmd ├── cmd.go └── version.go ├── config ├── config-example.yml └── config.go ├── data └── .gitignore ├── db ├── database.go ├── keydb.go └── keydb_test.go ├── doc.go ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── engine ├── btc_worker.go ├── concurrent.go ├── eth_worker.go └── tron_worker.go ├── go.mod ├── go.sum ├── main.go ├── scheduler ├── queue.go └── simple.go ├── script └── api.md ├── server ├── err_code.go ├── handler.go ├── middleware.go ├── req.go ├── res.go ├── server.go └── validator.go ├── swag.exe ├── tron ├── client.go ├── common.go ├── event_res.go ├── event_server.go ├── sign.go ├── web.go └── web_test.go └── types └── wallet.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | *.exe 4 | !swag.exe 5 | 6 | ### IntelliJ IDEA ### 7 | .idea 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | ### VS Code ### 13 | .vscode/ 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wallet 2 | 3 | > 虚拟币钱包服务,转账/提现/充值/归集 4 | > 5 | > 6 | > 完全实现与业务服务隔离,使用http服务相互调用 7 | 8 | # sdk列表 9 | 10 | > go示例 [https://github.com/lmxdawn/wallet-v2-sdk-go](https://github.com/lmxdawn/wallet-v2-sdk-go) 11 | > 12 | > 客户是PHP后端,特定增加一个php示例 [https://github.com/lmxdawn/wallet-v2-sdk-php](https://github.com/lmxdawn/wallet-v2-sdk-php) 13 | 14 | # 目前有 Pro 版本,可以在 Issues 联系我 15 | 16 | > 有 Pro 版本,更高级的钱包服务,可以wx 联系 appeth,添加时备注GitHub 17 | 18 | # 计划支持 19 | - [x] 以太坊(ERC20) 20 | - [x] 波场(TRC20) 21 | - [x] 币安(BEP20) 22 | - [x] OKC(KIP20) 23 | - [x] 比特币 24 | 25 | 26 | # 接口文档 27 | 28 | `script/api.md` 29 | 30 | # 下载-打包 31 | 32 | ```shell 33 | # 拉取代码 34 | $ git clone https://github.com/lmxdawn/wallet.git 35 | $ cd wallet 36 | 37 | # 打包 (-tags "doc") 可选,加上可以运行swagger 38 | $ go build [-tags "doc"] 39 | 40 | # 直接运行示例配置 41 | $ wallet -c config/config-example.yml 42 | 43 | ``` 44 | 45 | # 重新生成配置 46 | 47 | ```shell 48 | # 生成配置文件 49 | $ vim config.yml 50 | $ wallet -c config.yml 51 | 52 | ``` 53 | 54 | # 配置文件参数解释 55 | 56 | | 参数名 | 描述 | 57 | | ---- | ---- | 58 | | coin_name | 币种名称 | 59 | | contract | 合约地址(为空表示主币) | 60 | | contract_type | 合约类型(波场需要区分是TRC20还是TRC10) | 61 | | protocol | 协议名称 | 62 | | network | 网络名称(暂时BTC协议有用{MainNet:主网,TestNet:测试网,TestNet3:测试网3,SimNet:测试网}) | 63 | | rpc | rpc配置 | 64 | | user | rpc用户名(没有则为空) | 65 | | pass | rpc密码(没有则为空) | 66 | | file | db文件路径配置 | 67 | | wallet_prefix | 钱包的存储前缀 | 68 | | hash_prefix | 交易哈希的存储前缀 | 69 | | block_init | 初始块(默认读取最新块) | 70 | | block_after_time | 获取最新块的等待时间 | 71 | | receipt_count | 交易凭证worker数量 | 72 | | receipt_after_time | 获取交易信息的等待时间 | 73 | | collection_after_time | 归集等待时间 | 74 | | collection_count | 归集发送worker数量 | 75 | | collection_max | 最大的归集数量(满足多少才归集,为0表示不自动归集) | 76 | | collection_address | 归集地址 | 77 | | confirms | 确认数量 | 78 | | recharge_notify_url | 充值通知回调地址 | 79 | | withdraw_notify_url | 提现通知回调地址 | 80 | | withdraw_private_key | 提现的私钥地址 | 81 | 82 | > 启动后访问: `http://localhost:10009/swagger/index.html` 83 | 84 | 85 | # Swagger 86 | 87 | > 把 swag cmd 包下载 `go get -u github.com/swaggo/swag/cmd/swag` 88 | 89 | > 这时会在 bin 目录下生成一个 `swag.exe` ,把这个执行文件放到 `$GOPATH/bin` 下面 90 | 91 | > 执行 `swag init` 注意,一定要和main.go处于同一级目录 92 | 93 | > 启动时加上 `-tags "doc"` 才会启动swagger。 这里主要为了正式环境去掉 swagger,这样整体编译的包小一些 94 | 95 | > 启动后访问: `http://ip:prot/swagger/index.html` 96 | 97 | # 第三方库依赖 98 | 99 | > log 日志 `github.com/rs/zerolog` 100 | 101 | > 命令行工具 `github.com/urfave/cli` 102 | 103 | > 配置文件 `github.com/jinzhu/configor` 104 | 105 | # 环境依赖 106 | 107 | > go 1.16+ 108 | 109 | > Redis 3 110 | 111 | > MySQL 5.7 112 | 113 | # 其它 114 | 115 | > `script/Generate MyPOJOs.groovy` 生成数据库Model 116 | 117 | # 合约相关 118 | 119 | > `solcjs.cmd --version` 查看版本 120 | > 121 | > `solcjs.cmd --abi erc20.sol` 122 | > 123 | > `abigen --abi=erc20_sol_IERC20.abi --pkg=eth --out=erc20.go` 124 | 125 | # 准备 126 | 127 | 要实现这些功能首先得摸清楚我们需要完成些什么东西 128 | 129 | 1. 获取最新区块 130 | 2. 获取区块内部的交易记录 131 | 3. 通过交易哈希获取交易的完成状态 132 | 4. 获取某个地址的余额 133 | 5. 创建一个地址 134 | 6. 签名并发送luo交易 135 | 7. 定义接口如下 136 | 137 | ```go 138 | type Worker interface { 139 | getNowBlockNum() (uint64, error) 140 | getTransaction(uint64) ([]types.Transaction, uint64, error) 141 | getTransactionReceipt(*types.Transaction) error 142 | getBalance(address string) (*big.Int, error) 143 | createWallet() (*types.Wallet, error) 144 | sendTransaction(string, string, *big.Int) (string, error) 145 | } 146 | ``` 147 | 148 | # 实现 149 | 150 | > 创建一个地址后把地址和私钥保存下来 151 | 152 | ## 进 153 | 154 | 通过一个无限循环的服务不停的去获取最新块的交易数据,并且把交易数据都一一验证是否完成 ,这里判断数据的接收地址(to)是否属于本服务创建的钱包地址,如果是本服务的创建过的地址则判断为充值成功,**(这时逻辑服务里面需要做交易哈希做幂等)** 155 | 156 | ## 出 157 | 158 | 用户发起一笔提出操作,用户发起提出时通过服务配置的私钥来打包并签名luo交易。(私钥转到用户输入的提出地址),这里把提交的luo交易的哈希记录到服务 通过一个无限循环的服务不停的去获取最新块的交易数据,并且把交易数据都一一验证是否完成 159 | ,这里判断交易数据的哈希是否存在于服务,如果存在则处理**(这时逻辑服务里面需要做交易哈希做幂等)** 160 | 161 | ## 归集 162 | 163 | 通过定期循环服务创建的地址去转账到服务配置的归集地址里面,这里需要注意归集数量的限制,当满足固定的数量时才去归集(减少gas费) 164 | 165 | # 一个简单的示例 166 | 167 | github地址: [golang 实现加密货币的充值/提现/归集服务](https://github.com/lmxdawn/wallet) 168 | 169 | # 特别说明 170 | 171 | > 创建钱包的方式可以用 create2 创建合约,这样可以实现不用批量管理私钥,防止私钥丢失或者被盗。 172 | 173 | -------------------------------------------------------------------------------- /btc/client.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/btcjson" 5 | "github.com/btcsuite/btcd/chaincfg/chainhash" 6 | "github.com/btcsuite/btcd/rpcclient" 7 | "github.com/btcsuite/btcd/wire" 8 | "github.com/btcsuite/btcutil" 9 | ) 10 | 11 | type Client struct { 12 | rpc *rpcclient.Client 13 | } 14 | 15 | func NewClient(host, user, pass string) (*Client, error) { 16 | client, err := rpcclient.New(&rpcclient.ConnConfig{ 17 | HTTPPostMode: true, 18 | DisableTLS: true, 19 | Host: host, 20 | User: user, 21 | Pass: pass, 22 | }, nil) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &Client{ 29 | rpc: client, 30 | }, nil 31 | } 32 | 33 | // GetBlockCount 获取区块数量,调用返回本地最优链中的区块数量。 34 | func (c *Client) GetBlockCount() (int64, error) { 35 | return c.rpc.GetBlockCount() 36 | } 37 | 38 | // GetBlockHash 获取指定高度区块的哈希。 39 | func (c *Client) GetBlockHash(blockNumber int64) (*chainhash.Hash, error) { 40 | return c.rpc.GetBlockHash(blockNumber) 41 | } 42 | 43 | // ImportAddress 导入地址 44 | func (c *Client) ImportAddress(address string) error { 45 | return c.rpc.ImportAddress(address) 46 | } 47 | 48 | // ListUnspent 获取未交易的UTX 49 | func (c *Client) ListUnspent(address btcutil.Address) (listUnSpent []btcjson.ListUnspentResult, err error) { 50 | adds := [1]btcutil.Address{address} 51 | listUnSpent, err = c.rpc.ListUnspentMinMaxAddresses(1, 999999, adds[:]) 52 | if err != nil { 53 | return 54 | } 55 | return 56 | } 57 | 58 | // SendRawTransaction 发送裸交易 59 | func (c *Client) SendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { 60 | 61 | return c.rpc.SendRawTransaction(tx, false) 62 | 63 | } 64 | 65 | // ListSinceBlock 查询指定区块后发生的钱包交易 66 | func (c *Client) ListSinceBlock(blockHash *chainhash.Hash) (*btcjson.ListSinceBlockResult, error) { 67 | return c.rpc.ListSinceBlock(blockHash) 68 | } 69 | 70 | // ListTransactions 查询最近发生的钱包交易 71 | func (c *Client) ListTransactions(account string) ([]btcjson.ListTransactionsResult, error) { 72 | return c.rpc.ListTransactions(account) 73 | } 74 | -------------------------------------------------------------------------------- /btc/client_test.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "fmt" 5 | "github.com/btcsuite/btcutil" 6 | "testing" 7 | ) 8 | 9 | func TestClient_ListUnspent(t *testing.T) { 10 | 11 | var ( 12 | //host = "47.243.189.44:30000" 13 | //user = "BTCuser" 14 | //pass = "BTCpassword" 15 | //network = "MainNet" 16 | 17 | host = "127.0.0.1:18332" 18 | user = "admin" 19 | pass = "123456" 20 | network = "TestNet3" 21 | defaultNet = getNetwork(network) 22 | ) 23 | //address := "2N8Ah8xiqrfb37aktkkwYmMqpDGQJMSDAWY" 24 | address := "2NGU5d9Y3uKLJXZa6GgYm2bf4wNK33GcAwJ" 25 | client, err := NewClient(host, user, pass) 26 | if err != nil { 27 | t.Error("创建client失败:", err) 28 | } 29 | fromAddress, err := btcutil.DecodeAddress(address, defaultNet) 30 | if err != nil { 31 | t.Error("地址错误:", err) 32 | } 33 | 34 | list, err := client.ListUnspent(fromAddress) 35 | if err != nil { 36 | t.Error("获取未交易的UTXO失败:", err) 37 | } 38 | 39 | fmt.Println(list) 40 | 41 | } 42 | 43 | func TestClient_GetBlockHash(t *testing.T) { 44 | 45 | var ( 46 | host = "127.0.0.1:18332" 47 | user = "admin" 48 | pass = "123456" 49 | ) 50 | client, err := NewClient(host, user, pass) 51 | if err != nil { 52 | t.Error("创建client失败:", err) 53 | } 54 | 55 | blockNumber := int64(2000000) 56 | hash, err := client.GetBlockHash(blockNumber) 57 | if err != nil { 58 | t.Error("获取区块哈希失败:", err) 59 | } 60 | 61 | fmt.Println(hash) 62 | 63 | } 64 | 65 | func TestClient_ListSinceBlock(t *testing.T) { 66 | 67 | var ( 68 | //host = "47.243.189.44:30000" 69 | //user = "BTCuser" 70 | //pass = "BTCpassword" 71 | 72 | host = "127.0.0.1:18332" 73 | user = "admin" 74 | pass = "123456" 75 | ) 76 | client, err := NewClient(host, user, pass) 77 | if err != nil { 78 | t.Error("创建client失败:", err) 79 | } 80 | 81 | blockNumber := int64(600000) 82 | hash, err := client.GetBlockHash(blockNumber) 83 | if err != nil { 84 | t.Error("获取区块哈希失败:", err) 85 | } 86 | 87 | fmt.Println(hash) 88 | 89 | list, err := client.ListSinceBlock(hash) 90 | if err != nil { 91 | t.Error("获取交易列表失败:", err) 92 | } 93 | 94 | fmt.Println(list) 95 | 96 | } 97 | 98 | func TestClient_ListTransactions(t *testing.T) { 99 | 100 | var ( 101 | //host = "47.243.189.44:30000" 102 | //user = "BTCuser" 103 | //pass = "BTCpassword" 104 | host = "127.0.0.1:18332" 105 | user = "admin" 106 | pass = "123456" 107 | ) 108 | client, err := NewClient(host, user, pass) 109 | if err != nil { 110 | t.Error("创建client失败:", err) 111 | } 112 | 113 | address := "2N8Ah8xiqrfb37aktkkwYmMqpDGQJMSDAWY" 114 | list, err := client.ListTransactions(address) 115 | if err != nil { 116 | t.Error("获取最近发生的钱包交易失败:", err) 117 | } 118 | 119 | fmt.Println(list) 120 | 121 | } 122 | 123 | func TestClient_ImportAddress(t *testing.T) { 124 | 125 | var ( 126 | host = "127.0.0.1:18332" 127 | user = "admin" 128 | pass = "123456" 129 | ) 130 | client, err := NewClient(host, user, pass) 131 | if err != nil { 132 | t.Error("创建client失败:", err) 133 | } 134 | 135 | //address := "2N8Ah8xiqrfb37aktkkwYmMqpDGQJMSDAWY" 136 | address := "2NGU5d9Y3uKLJXZa6GgYm2bf4wNK33GcAwJ" 137 | err = client.ImportAddress(address) 138 | if err != nil { 139 | t.Error("导入地址失败:", err) 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /btc/util.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/chaincfg" 5 | "strings" 6 | ) 7 | 8 | func getNetwork(network string) *chaincfg.Params { 9 | var defaultNet *chaincfg.Params 10 | 11 | //指定网络 {MainNet:主网,TestNet:测试网,TestNet3:测试网3,SimNet:测试网} 12 | switch strings.ToLower(network) { 13 | case "mainnet": 14 | defaultNet = &chaincfg.MainNetParams 15 | case "testnet": 16 | defaultNet = &chaincfg.RegressionNetParams 17 | case "testnet3": 18 | defaultNet = &chaincfg.TestNet3Params 19 | case "simnet": 20 | defaultNet = &chaincfg.SimNetParams 21 | } 22 | return defaultNet 23 | } 24 | -------------------------------------------------------------------------------- /btc/wallet.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "github.com/btcsuite/btcd/btcec" 5 | "github.com/btcsuite/btcd/chaincfg" 6 | "github.com/btcsuite/btcd/txscript" 7 | "github.com/btcsuite/btcutil" 8 | ) 9 | 10 | type Wallet struct { 11 | LegacyAddress string // (P2PKH)格式,普通非隔离验证地址(由1开头)| 普及度:较高 | 矿工费:较低 12 | NestedSegWitAddress string // (P2SH)格式,隔离验证(兼容)地址(由3开头)| 普及度:较高 | 矿工费:较低 13 | NativeSegWitAddress string // (Bech32)格式,隔离验证(原生)地址(由bc1开头)| 普及度:较低 | 矿工费:最低 14 | PublicKey string 15 | PrivateKey string 16 | } 17 | 18 | // CreateWallet 创建钱包 19 | func CreateWallet(defaultNet *chaincfg.Params) (*Wallet, error) { 20 | 21 | //1.生成私钥,参数:Secp256k1 22 | privateKey, err := btcec.NewPrivateKey(btcec.S256()) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | //2.转成wif格式 28 | privateKeyWif, err := btcutil.NewWIF(privateKey, defaultNet, true) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return getWalletByPrivateKey(defaultNet, privateKeyWif) 34 | 35 | } 36 | 37 | // GetWalletByPrivateKey 获取钱包通过私钥,返回地址和公钥 38 | func GetWalletByPrivateKey(defaultNet *chaincfg.Params, privateKeyStr string) (*Wallet, error) { 39 | 40 | // 转成wif格式 41 | privateKeyWif, err := btcutil.DecodeWIF(privateKeyStr) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return getWalletByPrivateKey(defaultNet, privateKeyWif) 47 | } 48 | 49 | // getWalletByPrivateKey 根据网络和 私钥的 wif 获取地址 50 | func getWalletByPrivateKey(defaultNet *chaincfg.Params, wif *btcutil.WIF) (*Wallet, error) { 51 | 52 | // 获取publicKey 53 | publicKeySerial := wif.PrivKey.PubKey().SerializeCompressed() 54 | 55 | publicKey, err := btcutil.NewAddressPubKey(publicKeySerial, defaultNet) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | pkHash := btcutil.Hash160(publicKeySerial) 61 | nativeSegWitAddressHash, err := btcutil.NewAddressWitnessPubKeyHash(pkHash, defaultNet) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | nestedSegWitAddressWitnessProg, err := txscript.PayToAddrScript(nativeSegWitAddressHash) 67 | if err != nil { 68 | return nil, err 69 | } 70 | nestedSegWitAddressHash, err := btcutil.NewAddressScriptHash(nestedSegWitAddressWitnessProg, defaultNet) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return &Wallet{ 76 | LegacyAddress: publicKey.EncodeAddress(), 77 | NestedSegWitAddress: nestedSegWitAddressHash.EncodeAddress(), 78 | NativeSegWitAddress: nativeSegWitAddressHash.EncodeAddress(), 79 | PublicKey: publicKey.String(), 80 | PrivateKey: wif.String(), 81 | }, nil 82 | } 83 | -------------------------------------------------------------------------------- /btc/wallet_test.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestWeb_CreateWallet(t *testing.T) { 9 | network := "TestNet3" 10 | defaultNet := getNetwork(network) 11 | w, err := CreateWallet(defaultNet) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | fmt.Println("1开头地址:", w.LegacyAddress) 17 | fmt.Println("3开头地址:", w.NestedSegWitAddress) 18 | fmt.Println("bc1开头地址:", w.NativeSegWitAddress) 19 | fmt.Println("公钥:", w.PublicKey) 20 | fmt.Println("私钥:", w.PrivateKey) 21 | } 22 | 23 | /** 24 | === RUN TestWeb_CreateWallet 25 | 112333 26 | ***************************** 27 | 1开头地址: mxqPCMkp2zBYktDEEDGRogQN9baSFUMhBp 28 | 3开头地址: 2N8Ah8xiqrfb37aktkkwYmMqpDGQJMSDAWY 29 | bc1开头地址: tb1qhhmxngfsg3psgj8crq3c6r2mtlfzjma28zd0v5 30 | 公钥: 02b5b41245e18d7edb1fc01fa60804b718603de5bcc090aec2e11f49b55a152625 31 | 私钥: cNZiKqhwB5gTccW3MfVFVJ5tnLf5RfjLwcCjXprUsrbZnUtcGvZk 32 | 33 | === RUN TestWeb_CreateWallet 34 | ***************************** 35 | 1开头地址: mjtDhZ2c5X8bkAgMMyYYmvSkGz9mZSsWi1 36 | 3开头地址: 2NGU5d9Y3uKLJXZa6GgYm2bf4wNK33GcAwJ 37 | bc1开头地址: tb1q9lnpjyyff96w6rhf06jeuft00df62uedgwrfk4 38 | 公钥: 03d299f3d4096d07aab3b99d33253c5d20cbf83f3a1da1d36585adc12205ee9640 39 | 私钥: cTihhVCb3QMk7ZAzgAmnp3nuiaift1soqgvm3FWe6EGmfUnJvEDs 40 | */ 41 | 42 | func TestWeb_GetWalletByPrivateKey(t *testing.T) { 43 | 44 | //network := "TestNet3" 45 | network := "MainNet" 46 | defaultNet := getNetwork(network) 47 | privateKey := "cNZiKqhwB5gTccW3MfVFVJ5tnLf5RfjLwcCjXprUsrbZnUtcGvZk" 48 | w, err := GetWalletByPrivateKey(defaultNet, privateKey) 49 | if err != nil { 50 | t.Error(err) 51 | } 52 | 53 | fmt.Println("1开头地址:", w.LegacyAddress) 54 | fmt.Println("3开头地址:", w.NestedSegWitAddress) 55 | fmt.Println("bc1开头地址:", w.NativeSegWitAddress) 56 | fmt.Println("公钥:", w.PublicKey) 57 | fmt.Println("私钥:", w.PrivateKey) 58 | 59 | } 60 | -------------------------------------------------------------------------------- /btc/web.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "fmt" 7 | "github.com/btcsuite/btcd/chaincfg" 8 | "github.com/btcsuite/btcd/chaincfg/chainhash" 9 | "github.com/btcsuite/btcd/txscript" 10 | "github.com/btcsuite/btcd/wire" 11 | "github.com/btcsuite/btcutil" 12 | "github.com/shopspring/decimal" 13 | ) 14 | 15 | type Web struct { 16 | client *Client 17 | defaultNet *chaincfg.Params 18 | } 19 | 20 | func NewWeb(host, user, pass string, network string) (*Web, error) { 21 | 22 | client, err := NewClient(host, user, pass) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | defaultNet := getNetwork(network) 29 | 30 | return &Web{ 31 | client: client, 32 | defaultNet: defaultNet, 33 | }, nil 34 | 35 | } 36 | 37 | // CreateWallet 创建地址 38 | func (w *Web) CreateWallet() (*Wallet, error) { 39 | return CreateWallet(w.defaultNet) 40 | } 41 | 42 | // GetBlockCount 获取最高区块数量 43 | func (w *Web) GetBlockCount() (int64, error) { 44 | return w.client.GetBlockCount() 45 | } 46 | 47 | // GetWalletByPrivateKey 根据私钥获取地址 48 | func (w *Web) GetWalletByPrivateKey(privateKeyStr string) (string, error) { 49 | btcW, err := GetWalletByPrivateKey(w.defaultNet, privateKeyStr) 50 | if err != nil { 51 | return "", err 52 | } 53 | return btcW.NestedSegWitAddress, nil 54 | } 55 | 56 | // GetBalance 获取余额 57 | func (w *Web) GetBalance(address string) (int64, error) { 58 | fromAddress, err := btcutil.DecodeAddress(address, w.defaultNet) 59 | if err != nil { 60 | return 0, err 61 | } 62 | 63 | balance := decimal.NewFromInt(0) 64 | listUnSpent, err := w.client.ListUnspent(fromAddress) 65 | if err != nil { 66 | fmt.Println("报错") 67 | return 0, err 68 | } 69 | powDecimal := decimal.NewFromInt(1e8) 70 | for _, result := range listUnSpent { 71 | if result.Amount == 0 { 72 | continue 73 | } 74 | resultAmount := decimal.NewFromFloat(result.Amount) 75 | balance.Add(resultAmount.Mul(powDecimal)) 76 | } 77 | return balance.IntPart(), nil 78 | } 79 | 80 | // Transfer 转账 81 | func (w *Web) Transfer(from, to, privateKey string, amount, fee int64) (string, error) { 82 | 83 | fromAddress, err := btcutil.DecodeAddress(from, w.defaultNet) 84 | if err != nil { 85 | return "", err 86 | } 87 | toAddress, err := btcutil.DecodeAddress(to, w.defaultNet) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | // 记录累加金额 93 | outAmount := decimal.NewFromInt(0) 94 | listUnSpent, err := w.client.ListUnspent(fromAddress) 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | amountDecimal := decimal.NewFromInt(amount) 100 | feeDecimal := decimal.NewFromInt(fee) 101 | powDecimal := decimal.NewFromInt(1e8) 102 | 103 | // 构造输出 104 | var outputs []*wire.TxOut 105 | // 构造输入 106 | var inputs []*wire.TxIn 107 | var pkScripts [][]byte // txin 签名用script 108 | for _, result := range listUnSpent { 109 | if result.Amount == 0 { 110 | fmt.Println("sssssss") 111 | continue 112 | } 113 | if outAmount.Cmp(amountDecimal) >= 0 { 114 | fmt.Println("sssssss") 115 | // 已经累加到需要转账的金额 116 | break 117 | } 118 | 119 | resultAmount := decimal.NewFromFloat(result.Amount) 120 | 121 | outAmount.Add(resultAmount.Mul(powDecimal)) 122 | fmt.Println("余额:", outAmount) 123 | 124 | // 构造输入 125 | hash, _ := chainhash.NewHashFromStr(result.TxID) // tx hash 126 | outPoint := wire.NewOutPoint(hash, result.Vout) // 第几个输出 127 | txIn := wire.NewTxIn(outPoint, nil, nil) 128 | inputs = append(inputs, txIn) 129 | 130 | //设置签名用script 131 | txInPkScript, err := hex.DecodeString(result.ScriptPubKey) 132 | if err != nil { 133 | return "", err 134 | } 135 | pkScripts = append(pkScripts, txInPkScript) 136 | } 137 | 138 | fmt.Println(outAmount, amountDecimal) 139 | 140 | // 余额不足 141 | if outAmount.Cmp(amountDecimal) == -1 { 142 | return "", errors.New("余额不足") 143 | } 144 | 145 | // 输出给转账者自己 146 | leftToMe := outAmount.Sub(amountDecimal).Sub(feeDecimal) // 累加值-转账值-交易费就是剩下再给我的 147 | pkScript, err := txscript.PayToAddrScript(fromAddress) 148 | if err != nil { 149 | return "", err 150 | } 151 | outputs = append(outputs, wire.NewTxOut(leftToMe.IntPart(), pkScript)) 152 | 153 | // 输出给接收者 154 | pkScript, err = txscript.PayToAddrScript(toAddress) 155 | if err != nil { 156 | return "", err 157 | } 158 | outputs = append(outputs, wire.NewTxOut(amount, pkScript)) 159 | 160 | tx := &wire.MsgTx{ 161 | Version: wire.TxVersion, 162 | TxIn: inputs, 163 | TxOut: outputs, 164 | LockTime: 0, 165 | } 166 | 167 | // 签名 168 | err = sign(tx, privateKey, pkScripts) 169 | if err != nil { 170 | return "", err 171 | } 172 | 173 | // 广播裸交易 174 | hash, err := w.client.SendRawTransaction(tx) 175 | if err != nil { 176 | return "", err 177 | } 178 | 179 | return hash.String(), nil 180 | } 181 | 182 | // 签名 183 | func sign(tx *wire.MsgTx, privKeyStr string, prevPkScripts [][]byte) error { 184 | inputs := tx.TxIn 185 | wif, err := btcutil.DecodeWIF(privKeyStr) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | privateKey := wif.PrivKey 191 | 192 | for i := range inputs { 193 | pkScript := prevPkScripts[i] 194 | var script []byte 195 | script, err = txscript.SignatureScript(tx, i, pkScript, txscript.SigHashAll, 196 | privateKey, false) 197 | inputs[i].SignatureScript = script 198 | } 199 | return nil 200 | } 201 | -------------------------------------------------------------------------------- /btc/web_test.go: -------------------------------------------------------------------------------- 1 | package btc 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestWeb_Transfer(t *testing.T) { 9 | 10 | host := "127.0.0.1:18332" 11 | user := "admin" 12 | pass := "123456" 13 | network := "TestNet3" 14 | web, err := NewWeb(host, user, pass, network) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | 19 | address := "2N8Ah8xiqrfb37aktkkwYmMqpDGQJMSDAWY" 20 | to := "2MwjzD8z9NT72MvbLh7K6Wovk5VgNtSW8p6" 21 | privateKey := "cNZiKqhwB5gTccW3MfVFVJ5tnLf5RfjLwcCjXprUsrbZnUtcGvZk" 22 | hash, err := web.Transfer(address, to, privateKey, 1000, 0.001*1e8) 23 | if err != nil { 24 | t.Error("转账失败:", err) 25 | } 26 | 27 | fmt.Println("哈希值:", hash) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /client/http.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | ) 12 | 13 | // Response ... 14 | type Response struct { 15 | Code int `json:"code"` // 错误code码 16 | Message string `json:"message"` // 错误信息 17 | } 18 | 19 | func (br Response) Success() bool { 20 | return br.Code == 0 21 | } 22 | 23 | type HttpClient struct { 24 | protocol string 25 | coinName string 26 | rechargeNotifyUrl string 27 | withdrawNotifyUrl string 28 | client *http.Client 29 | } 30 | 31 | // NewHttpClient 创建 32 | func NewHttpClient(protocol, coinName, rechargeNotifyUrl, withdrawNotifyUrl string) *HttpClient { 33 | return &HttpClient{ 34 | protocol, 35 | coinName, 36 | rechargeNotifyUrl, 37 | withdrawNotifyUrl, 38 | &http.Client{ 39 | Timeout: time.Millisecond * time.Duration(10*1000), 40 | }, 41 | } 42 | } 43 | 44 | // RechargeSuccess 充值成功通知 45 | func (h *HttpClient) RechargeSuccess(hash string, status uint, address string, value int64) error { 46 | 47 | data := make(map[string]interface{}) 48 | data["protocol"] = h.protocol 49 | data["coinName"] = h.coinName 50 | data["hash"] = hash 51 | data["status"] = status 52 | data["address"] = address 53 | data["value"] = value 54 | 55 | var res Response 56 | err := h.post(h.withdrawNotifyUrl, data, &res) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if !res.Success() { 62 | return errors.New(res.Message) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // WithdrawSuccess 提现成功通知 69 | func (h *HttpClient) WithdrawSuccess(hash string, status uint, orderId string, address string, value int64) error { 70 | 71 | data := make(map[string]interface{}) 72 | data["protocol"] = h.protocol 73 | data["coinName"] = h.coinName 74 | data["hash"] = hash 75 | data["status"] = status 76 | data["orderId"] = orderId 77 | data["address"] = address 78 | data["value"] = value 79 | 80 | var res Response 81 | err := h.post(h.withdrawNotifyUrl, data, &res) 82 | 83 | if err != nil { 84 | return err 85 | } 86 | 87 | if !res.Success() { 88 | return errors.New(res.Message) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // get 请求 95 | func (h *HttpClient) get(urlStr string, params url.Values, res interface{}) error { 96 | 97 | Url, err := url.Parse(urlStr) 98 | if err != nil { 99 | return err 100 | } 101 | //如果参数中有中文参数,这个方法会进行URLEncode 102 | Url.RawQuery = params.Encode() 103 | urlPath := Url.String() 104 | 105 | req, err := http.NewRequest(http.MethodGet, urlPath, nil) 106 | if err != nil { 107 | // handle error 108 | return err 109 | } 110 | resp, err := h.client.Do(req) 111 | if err != nil { 112 | // handle error 113 | return err 114 | } 115 | defer resp.Body.Close() 116 | 117 | body, err := ioutil.ReadAll(resp.Body) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | err = json.Unmarshal(body, res) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | return nil 128 | } 129 | 130 | // post 请求 131 | func (h *HttpClient) post(urlStr string, data map[string]interface{}, res interface{}) error { 132 | bytesData, err := json.Marshal(data) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | req, err := http.NewRequest(http.MethodPost, urlStr, bytes.NewReader(bytesData)) 138 | if err != nil { 139 | // handle error 140 | return err 141 | } 142 | req.Header.Set("Content-Type", "application/json") 143 | resp, err := h.client.Do(req) 144 | if err != nil { 145 | // handle error 146 | return err 147 | } 148 | defer resp.Body.Close() 149 | 150 | body, err := ioutil.ReadAll(resp.Body) 151 | if err != nil { 152 | // handle error 153 | return err 154 | } 155 | 156 | err = json.Unmarshal(body, res) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lmxdawn/wallet/server" 6 | "github.com/urfave/cli" 7 | "os" 8 | ) 9 | 10 | func Run(isSwag bool) { 11 | app := cli.NewApp() 12 | app.Name = "wallet" 13 | app.Usage = "wallet -c config/config.yml" 14 | printVersion := false 15 | app.Flags = []cli.Flag{ 16 | cli.StringFlag{ 17 | Name: "conf, c", 18 | Value: "config/config-example.yml", 19 | Usage: "config/config.{local|dev|test|pre|prod}.yml", 20 | }, 21 | cli.BoolFlag{ 22 | Name: "version, v", 23 | Required: false, 24 | Usage: "-v", 25 | Destination: &printVersion, 26 | }, 27 | } 28 | 29 | app.Action = func(c *cli.Context) error { 30 | 31 | if printVersion { 32 | fmt.Printf("{%#v}", GetVersion()) 33 | return nil 34 | } 35 | 36 | conf := c.String("conf") 37 | server.Start(isSwag, conf) 38 | 39 | return nil 40 | } 41 | 42 | err := app.Run(os.Args) 43 | if err != nil { 44 | panic(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | gitTag = "" 10 | gitCommit = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD) 11 | gitTreeState = "not a git tree" // state of git tree, either "clean" or "dirty" 12 | buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 13 | ) 14 | 15 | // Info contains versioning information. 16 | type Info struct { 17 | GitTag string `json:"gitTag"` 18 | GitCommit string `json:"gitCommit"` 19 | GitTreeState string `json:"gitTreeState"` 20 | BuildDate string `json:"buildDate"` 21 | GoVersion string `json:"goVersion"` 22 | Compiler string `json:"compiler"` 23 | Platform string `json:"platform"` 24 | } 25 | 26 | // String returns info as a human-friendly version string. 27 | func (info *Info) String() string { 28 | return info.GitTag 29 | } 30 | 31 | // GetVersion ... 32 | func GetVersion() Info { 33 | return Info{ 34 | GitTag: gitTag, 35 | GitCommit: gitCommit, 36 | GitTreeState: gitTreeState, 37 | BuildDate: buildDate, 38 | GoVersion: runtime.Version(), 39 | Compiler: runtime.Compiler, 40 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/config-example.yml: -------------------------------------------------------------------------------- 1 | app: 2 | port: 10001 3 | engines: 4 | # # 以太坊主币 5 | # - coin_name: ETH 6 | # contract: 7 | # contract_type: 8 | # protocol: eth 9 | # rpc: https://ropsten.infura.io/v3/adee4ead47844d238802431fcb7683c6 10 | # file: data/eth 11 | # wallet_prefix: wallet- 12 | # hash_prefix: hash- 13 | # block_init: 0 14 | # block_after_time: 1 15 | # receipt_count: 20 16 | # receipt_after_time: 1 17 | # collection_after_time: 2 18 | # collection_count: 10 19 | # collection_max: 10 20 | # collection_address: 0x785380b6012648d0DEeE1F66ac18657Db2240789 21 | # confirms: 1 22 | # recharge_notify_url: http://localhost:10001/api/withdraw 23 | # withdraw_notify_url: http://localhost:10001/api/withdraw 24 | # withdraw_private_key: 61774dacba914e5675eef6c616df85c61d7c8917f56ee77f547a140f8f982d31 25 | # # 以太坊erc20代币 26 | # - coin_name: TEST 27 | # contract: 0xeDc4019BCBdA0f7acCBB4833ffF54d618d9b7f82 28 | # contract_type: erc20 29 | # protocol: eth 30 | # rpc: https://ropsten.infura.io/v3/adee4ead47844d238802431fcb7683c6 31 | # file: data/usdt 32 | # wallet_prefix: wallet- 33 | # hash_prefix: hash- 34 | # block_init: 0 35 | # block_after_time: 1 36 | # receipt_count: 1 37 | # receipt_after_time: 1 38 | # collection_after_time: 1 39 | # collection_count: 10 40 | # collection_max: 15 41 | # collection_address: 0x785380b6012648d0DEeE1F66ac18657Db2240789 42 | # confirms: 1 43 | # recharge_notify_url: https://mainnet.infura.io 44 | # withdraw_notify_url: https://mainnet.infura.io 45 | # withdraw_private_key: 61774dacba914e5675eef6c616df85c61d7c8917f56ee77f547a140f8f982d31 46 | # # 币安链主币 47 | # - coin_name: BNB 48 | # contract: 49 | # contract_type: 50 | # protocol: eth 51 | # rpc: https://data-seed-prebsc-1-s1.binance.org:8545 52 | # file: data/bnb 53 | # wallet_prefix: wallet- 54 | # hash_prefix: hash- 55 | # block_init: 0 56 | # block_after_time: 1 57 | # receipt_count: 20 58 | # receipt_after_time: 1 59 | # collection_after_time: 2 60 | # collection_count: 10 61 | # collection_max: 10 62 | # collection_address: 0x785380b6012648d0DEeE1F66ac18657Db2240789 63 | # confirms: 1 64 | # recharge_notify_url: http://localhost:10001/api/withdraw 65 | # withdraw_notify_url: http://localhost:10001/api/withdraw 66 | # withdraw_private_key: 61774dacba914e5675eef6c616df85c61d7c8917f56ee77f547a140f8f982d31 67 | # # 币安链erc20代币 68 | # - coin_name: BNBTEST 69 | # contract: 0x5A6D132443E62acd1074184599F3d034d8B76FD9 70 | # contract_type: erc20 71 | # protocol: eth 72 | # rpc: https://data-seed-prebsc-1-s1.binance.org:8545 73 | # file: data/bnbusdt 74 | # wallet_prefix: wallet- 75 | # hash_prefix: hash- 76 | # block_init: 0 77 | # block_after_time: 1 78 | # receipt_count: 1 79 | # receipt_after_time: 1 80 | # collection_after_time: 1 81 | # collection_count: 10 82 | # collection_max: 15 83 | # collection_address: 0x785380b6012648d0DEeE1F66ac18657Db2240789 84 | # confirms: 1 85 | # recharge_notify_url: https://mainnet.infura.io 86 | # withdraw_notify_url: https://mainnet.infura.io 87 | # withdraw_private_key: 61774dacba914e5675eef6c616df85c61d7c8917f56ee77f547a140f8f982d31 88 | # # 波场主币 89 | # - coin_name: TRX 90 | # contract: 91 | # contract_type: 92 | # protocol: trx 93 | # rpc: grpc.shasta.trongrid.io:50051 94 | # file: data/trx 95 | # wallet_prefix: wallet- 96 | # hash_prefix: hash- 97 | # block_init: 0 98 | # block_after_time: 3 99 | # receipt_count: 20 100 | # receipt_after_time: 1 101 | # collection_after_time: 2 102 | # collection_count: 10 103 | # collection_max: 10 104 | # collection_address: TYBpNWMNHPyk1hBbY1SRr2RQebF39iFk7z 105 | # confirms: 6 106 | # recharge_notify_url: http://localhost:10001/api/withdraw 107 | # withdraw_notify_url: http://localhost:10001/api/withdraw 108 | # withdraw_private_key: a0e0ac9b3a1ee1a27411642b7ff60030887c6b78c89d498aba49adedceb4233b 109 | # # 波场trc10 110 | # - coin_name: BTFSTOKEN(BSN) 111 | # contract: 1000252 112 | # contract_type: trc10 113 | # protocol: trx 114 | # rpc: grpc.shasta.trongrid.io:50051 115 | # file: data/trx10 116 | # wallet_prefix: wallet- 117 | # hash_prefix: hash- 118 | # block_init: 0 119 | # block_after_time: 3 120 | # receipt_count: 20 121 | # receipt_after_time: 1 122 | # collection_after_time: 2 123 | # collection_count: 10 124 | # collection_max: 10 125 | # collection_address: TYBpNWMNHPyk1hBbY1SRr2RQebF39iFk7z 126 | # confirms: 6 127 | # recharge_notify_url: http://localhost:10001/api/withdraw 128 | # withdraw_notify_url: http://localhost:10001/api/withdraw 129 | # withdraw_private_key: a0e0ac9b3a1ee1a27411642b7ff60030887c6b78c89d498aba49adedceb4233b 130 | # # 波场trc20 131 | # - coin_name: PiAToken (PiA) 132 | # contract: TQKucgWL1cbAtW8vxKbwRgThw3FPmrok2t 133 | # contract_type: trc20 134 | # protocol: trx 135 | # rpc: grpc.shasta.trongrid.io:50051 136 | # file: data/trx20 137 | # wallet_prefix: wallet- 138 | # hash_prefix: hash- 139 | # block_init: 0 140 | # block_after_time: 3 141 | # receipt_count: 20 142 | # receipt_after_time: 1 143 | # collection_after_time: 2 144 | # collection_count: 10 145 | # collection_max: 10 146 | # collection_address: TYBpNWMNHPyk1hBbY1SRr2RQebF39iFk7z 147 | # confirms: 6 148 | # recharge_notify_url: http://localhost:10001/api/withdraw 149 | # withdraw_notify_url: http://localhost:10001/api/withdraw 150 | # withdraw_private_key: a0e0ac9b3a1ee1a27411642b7ff60030887c6b78c89d498aba49adedceb4233b 151 | 152 | - coin_name: BTC 153 | contract: 154 | contract_type: 155 | protocol: btc 156 | network: MainNet 157 | rpc: 127.0.0.1:18332 158 | user: admin 159 | pass: 123456 160 | file: data/trx 161 | wallet_prefix: wallet- 162 | hash_prefix: hash- 163 | block_init: 0 164 | block_after_time: 3 165 | receipt_count: 20 166 | receipt_after_time: 1 167 | collection_after_time: 2 168 | collection_count: 10 169 | collection_max: 10 170 | collection_address: TYBpNWMNHPyk1hBbY1SRr2RQebF39iFk7z 171 | confirms: 6 172 | recharge_notify_url: http://localhost:10001/api/withdraw 173 | withdraw_notify_url: http://localhost:10001/api/withdraw 174 | withdraw_private_key: a0e0ac9b3a1ee1a27411642b7ff60030887c6b78c89d498aba49adedceb4233b -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/jinzhu/configor" 5 | ) 6 | 7 | type AppConfig struct { 8 | Port uint `yaml:"port"` 9 | } 10 | 11 | type EngineConfig struct { 12 | CoinName string `yaml:"coin_name"` // 币种名称 13 | Contract string `yaml:"contract"` // 合约地址(为空表示主币) 14 | ContractType string `yaml:"contract_type"` // 合约类型(波场需要区分是TRC20还是TRC10) 15 | Protocol string `yaml:"protocol"` // 协议名称 16 | Network string `yaml:"network"` // 网络名称(暂时BTC协议有用{MainNet:主网,TestNet:测试网,TestNet3:测试网3,SimNet:测试网}) 17 | Rpc string `yaml:"rpc"` // rpc配置 18 | User string `yaml:"user"` // rpc用户名(没有则为空) 19 | Pass string `yaml:"pass"` // rpc密码(没有则为空) 20 | File string `yaml:"file"` // db文件路径配置 21 | WalletPrefix string `yaml:"wallet_prefix"` // 钱包的存储前缀 22 | HashPrefix string `yaml:"hash_prefix"` // 交易哈希的存储前缀 23 | BlockInit uint64 `yaml:"block_init"` // 初始块(默认读取最新块) 24 | BlockAfterTime uint64 `yaml:"block_after_time"` // 获取最新块的等待时间 25 | ReceiptCount uint64 `yaml:"receipt_count"` // 交易凭证worker数量 26 | ReceiptAfterTime uint64 `yaml:"receipt_after_time"` // 获取交易信息的等待时间 27 | CollectionAfterTime uint64 `yaml:"collection_after_time"` // 归集等待时间 28 | CollectionCount uint64 `yaml:"collection_count"` // 归集发送worker数量 29 | CollectionMax string `yaml:"collection_max"` // 最大的归集数量(满足多少才归集,为0表示不自动归集) 30 | CollectionAddress string `yaml:"collection_address"` // 归集地址 31 | Confirms uint64 `yaml:"confirms"` // 确认数量 32 | RechargeNotifyUrl string `yaml:"recharge_notify_url"` // 充值通知回调地址 33 | WithdrawNotifyUrl string `yaml:"withdraw_notify_url"` // 提现通知回调地址 34 | WithdrawPrivateKey string `yaml:"withdraw_private_key"` // 提现的私钥地址 35 | } 36 | 37 | type Config struct { 38 | App AppConfig 39 | Engines []EngineConfig 40 | } 41 | 42 | func NewConfig(confPath string) (Config, error) { 43 | var config Config 44 | if confPath != "" { 45 | err := configor.Load(&config, confPath) 46 | if err != nil { 47 | return config, err 48 | } 49 | } else { 50 | err := configor.Load(&config, "config/config-example.yml") 51 | if err != nil { 52 | return config, err 53 | } 54 | } 55 | return config, nil 56 | } 57 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /db/database.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "io" 4 | 5 | type WalletItem struct { 6 | Address string // 地址 7 | PrivateKey string // 私钥 8 | } 9 | 10 | type Reader interface { 11 | // Has retrieves if a key is present in the key-value data store. 12 | Has(key string) (bool, error) 13 | 14 | // Get retrieves the given key if it's present in the key-value data store. 15 | Get(key string) (string, error) 16 | 17 | // ListWallet retrieves the given key if it's present in the key-value data store. 18 | ListWallet(prefix string) ([]WalletItem, error) 19 | } 20 | 21 | type Writer interface { 22 | // Put inserts the given value into the key-value data store. 23 | Put(key string, value string) error 24 | 25 | // Delete removes the key from the key-value data store. 26 | Delete(key string) error 27 | } 28 | 29 | type Database interface { 30 | Reader 31 | Writer 32 | io.Closer 33 | } 34 | -------------------------------------------------------------------------------- /db/keydb.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/syndtr/goleveldb/leveldb" 5 | "github.com/syndtr/goleveldb/leveldb/util" 6 | ) 7 | 8 | type KeyDB struct { 9 | db *leveldb.DB 10 | } 11 | 12 | func NewKeyDB(file string) (*KeyDB, error) { 13 | db, err := leveldb.OpenFile(file, nil) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return &KeyDB{ 19 | db: db, 20 | }, nil 21 | } 22 | 23 | func (l *KeyDB) Has(key string) (bool, error) { 24 | has, err := l.db.Has([]byte(key), nil) 25 | if err != nil { 26 | return false, err 27 | } 28 | return has, nil 29 | } 30 | 31 | func (l *KeyDB) Get(key string) (string, error) { 32 | data, err := l.db.Get([]byte(key), nil) 33 | if err != nil { 34 | return "", err 35 | } 36 | return string(data), nil 37 | } 38 | 39 | func (l *KeyDB) ListWallet(prefix string) ([]WalletItem, error) { 40 | iter := l.db.NewIterator(util.BytesPrefix([]byte(prefix)), nil) 41 | var list []WalletItem 42 | for iter.Next() { 43 | list = append(list, WalletItem{ 44 | Address: string(iter.Key()), 45 | PrivateKey: string(iter.Value()), 46 | }) 47 | } 48 | return list, nil 49 | } 50 | 51 | func (l *KeyDB) Put(key string, value string) error { 52 | err := l.db.Put([]byte(key), []byte(value), nil) 53 | if err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | 59 | func (l *KeyDB) Delete(key string) error { 60 | err := l.db.Delete([]byte(key), nil) 61 | if err != nil { 62 | return err 63 | } 64 | return nil 65 | } 66 | 67 | func (l *KeyDB) Close() error { 68 | return l.db.Close() 69 | } 70 | -------------------------------------------------------------------------------- /db/keydb_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestList(t *testing.T) { 9 | 10 | db, err := NewKeyDB("data/test") 11 | if err != nil { 12 | 13 | } 14 | 15 | db.Put("fff", "fff") 16 | db.Put("wallet-aaa", "aaa") 17 | db.Put("111", "111") 18 | 19 | list, _ := db.ListWallet("wallet-") 20 | 21 | fmt.Println(list) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // +build doc 2 | 3 | package main 4 | 5 | import ( 6 | _ "github.com/lmxdawn/wallet/docs" 7 | ) 8 | 9 | func init() { 10 | isSwag = true 11 | } 12 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | package docs 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/swaggo/swag" 12 | ) 13 | 14 | var doc = `{ 15 | "schemes": {{ marshal .Schemes }}, 16 | "swagger": "2.0", 17 | "info": { 18 | "description": "{{escape .Description}}", 19 | "title": "{{.Title}}", 20 | "termsOfService": "http://swagger.io/terms/", 21 | "contact": { 22 | "name": "API Support", 23 | "url": "http://www.swagger.io/support", 24 | "email": "support@swagger.io" 25 | }, 26 | "license": { 27 | "name": "Apache 2.0", 28 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 29 | }, 30 | "version": "{{.Version}}" 31 | }, 32 | "host": "{{.Host}}", 33 | "basePath": "{{.BasePath}}", 34 | "paths": { 35 | "/api/collection": { 36 | "post": { 37 | "security": [ 38 | { 39 | "ApiKeyAuth": [] 40 | } 41 | ], 42 | "produces": [ 43 | "application/json" 44 | ], 45 | "tags": [ 46 | "归集某个地址" 47 | ], 48 | "summary": "归集", 49 | "parameters": [ 50 | { 51 | "description": "参数", 52 | "name": "login", 53 | "in": "body", 54 | "required": true, 55 | "schema": { 56 | "$ref": "#/definitions/server.CollectionReq" 57 | } 58 | } 59 | ], 60 | "responses": { 61 | "200": { 62 | "description": "OK", 63 | "schema": { 64 | "allOf": [ 65 | { 66 | "$ref": "#/definitions/server.Response" 67 | }, 68 | { 69 | "type": "object", 70 | "properties": { 71 | "data": { 72 | "$ref": "#/definitions/server.CollectionRes" 73 | } 74 | } 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | } 81 | }, 82 | "/api/createWallet": { 83 | "post": { 84 | "security": [ 85 | { 86 | "ApiKeyAuth": [] 87 | } 88 | ], 89 | "produces": [ 90 | "application/json" 91 | ], 92 | "tags": [ 93 | "钱包" 94 | ], 95 | "summary": "创建钱包地址", 96 | "parameters": [ 97 | { 98 | "description": "参数", 99 | "name": "login", 100 | "in": "body", 101 | "required": true, 102 | "schema": { 103 | "$ref": "#/definitions/server.CreateWalletReq" 104 | } 105 | } 106 | ], 107 | "responses": { 108 | "200": { 109 | "description": "OK", 110 | "schema": { 111 | "allOf": [ 112 | { 113 | "$ref": "#/definitions/server.Response" 114 | }, 115 | { 116 | "type": "object", 117 | "properties": { 118 | "data": { 119 | "$ref": "#/definitions/server.CreateWalletRes" 120 | } 121 | } 122 | } 123 | ] 124 | } 125 | } 126 | } 127 | } 128 | }, 129 | "/api/delWallet": { 130 | "post": { 131 | "security": [ 132 | { 133 | "ApiKeyAuth": [] 134 | } 135 | ], 136 | "produces": [ 137 | "application/json" 138 | ], 139 | "tags": [ 140 | "钱包" 141 | ], 142 | "summary": "删除钱包地址", 143 | "parameters": [ 144 | { 145 | "description": "参数", 146 | "name": "login", 147 | "in": "body", 148 | "required": true, 149 | "schema": { 150 | "$ref": "#/definitions/server.DelWalletReq" 151 | } 152 | } 153 | ], 154 | "responses": { 155 | "200": { 156 | "description": "OK", 157 | "schema": { 158 | "$ref": "#/definitions/server.Response" 159 | } 160 | } 161 | } 162 | } 163 | }, 164 | "/api/getTransactionReceipt": { 165 | "get": { 166 | "security": [ 167 | { 168 | "ApiKeyAuth": [] 169 | } 170 | ], 171 | "produces": [ 172 | "application/json" 173 | ], 174 | "tags": [ 175 | "钱包" 176 | ], 177 | "summary": "获取交易结果", 178 | "parameters": [ 179 | { 180 | "description": "参数", 181 | "name": "login", 182 | "in": "body", 183 | "required": true, 184 | "schema": { 185 | "$ref": "#/definitions/server.TransactionReceiptReq" 186 | } 187 | } 188 | ], 189 | "responses": { 190 | "200": { 191 | "description": "OK", 192 | "schema": { 193 | "allOf": [ 194 | { 195 | "$ref": "#/definitions/server.Response" 196 | }, 197 | { 198 | "type": "object", 199 | "properties": { 200 | "data": { 201 | "$ref": "#/definitions/server.TransactionReceiptRes" 202 | } 203 | } 204 | } 205 | ] 206 | } 207 | } 208 | } 209 | } 210 | }, 211 | "/api/withdraw": { 212 | "post": { 213 | "security": [ 214 | { 215 | "ApiKeyAuth": [] 216 | } 217 | ], 218 | "produces": [ 219 | "application/json" 220 | ], 221 | "tags": [ 222 | "钱包" 223 | ], 224 | "summary": "提现", 225 | "parameters": [ 226 | { 227 | "description": "参数", 228 | "name": "login", 229 | "in": "body", 230 | "required": true, 231 | "schema": { 232 | "$ref": "#/definitions/server.WithdrawReq" 233 | } 234 | } 235 | ], 236 | "responses": { 237 | "200": { 238 | "description": "OK", 239 | "schema": { 240 | "allOf": [ 241 | { 242 | "$ref": "#/definitions/server.Response" 243 | }, 244 | { 245 | "type": "object", 246 | "properties": { 247 | "data": { 248 | "$ref": "#/definitions/server.WithdrawRes" 249 | } 250 | } 251 | } 252 | ] 253 | } 254 | } 255 | } 256 | } 257 | } 258 | }, 259 | "definitions": { 260 | "server.CollectionReq": { 261 | "type": "object", 262 | "required": [ 263 | "address", 264 | "coinName", 265 | "max", 266 | "protocol" 267 | ], 268 | "properties": { 269 | "address": { 270 | "description": "地址", 271 | "type": "string" 272 | }, 273 | "coinName": { 274 | "description": "币种名称", 275 | "type": "string" 276 | }, 277 | "max": { 278 | "description": "最大归集数量(满足当前值才会归集)", 279 | "type": "string" 280 | }, 281 | "protocol": { 282 | "description": "协议", 283 | "type": "string" 284 | } 285 | } 286 | }, 287 | "server.CollectionRes": { 288 | "type": "object", 289 | "properties": { 290 | "balance": { 291 | "description": "实际归集的数量", 292 | "type": "string" 293 | } 294 | } 295 | }, 296 | "server.CreateWalletReq": { 297 | "type": "object", 298 | "required": [ 299 | "coinName", 300 | "protocol" 301 | ], 302 | "properties": { 303 | "coinName": { 304 | "description": "币种名称", 305 | "type": "string" 306 | }, 307 | "protocol": { 308 | "description": "协议", 309 | "type": "string" 310 | } 311 | } 312 | }, 313 | "server.CreateWalletRes": { 314 | "type": "object", 315 | "properties": { 316 | "address": { 317 | "description": "生成的钱包地址", 318 | "type": "string" 319 | } 320 | } 321 | }, 322 | "server.DelWalletReq": { 323 | "type": "object", 324 | "required": [ 325 | "address", 326 | "coinName", 327 | "protocol" 328 | ], 329 | "properties": { 330 | "address": { 331 | "description": "地址", 332 | "type": "string" 333 | }, 334 | "coinName": { 335 | "description": "币种名称", 336 | "type": "string" 337 | }, 338 | "protocol": { 339 | "description": "协议", 340 | "type": "string" 341 | } 342 | } 343 | }, 344 | "server.Response": { 345 | "type": "object", 346 | "properties": { 347 | "code": { 348 | "description": "错误code码", 349 | "type": "integer" 350 | }, 351 | "data": { 352 | "description": "成功时返回的对象" 353 | }, 354 | "message": { 355 | "description": "错误信息", 356 | "type": "string" 357 | } 358 | } 359 | }, 360 | "server.TransactionReceiptReq": { 361 | "type": "object", 362 | "required": [ 363 | "coinName", 364 | "hash", 365 | "protocol" 366 | ], 367 | "properties": { 368 | "coinName": { 369 | "description": "币种名称", 370 | "type": "string" 371 | }, 372 | "hash": { 373 | "description": "交易哈希", 374 | "type": "string" 375 | }, 376 | "protocol": { 377 | "description": "协议", 378 | "type": "string" 379 | } 380 | } 381 | }, 382 | "server.TransactionReceiptRes": { 383 | "type": "object", 384 | "properties": { 385 | "status": { 386 | "description": "交易状态(0:未成功,1:已成功)", 387 | "type": "integer" 388 | } 389 | } 390 | }, 391 | "server.WithdrawReq": { 392 | "type": "object", 393 | "required": [ 394 | "address", 395 | "coinName", 396 | "orderId", 397 | "protocol", 398 | "value" 399 | ], 400 | "properties": { 401 | "address": { 402 | "description": "提现地址", 403 | "type": "string" 404 | }, 405 | "coinName": { 406 | "description": "币种名称", 407 | "type": "string" 408 | }, 409 | "orderId": { 410 | "description": "订单号", 411 | "type": "string" 412 | }, 413 | "protocol": { 414 | "description": "协议", 415 | "type": "string" 416 | }, 417 | "value": { 418 | "description": "金额", 419 | "type": "integer" 420 | } 421 | } 422 | }, 423 | "server.WithdrawRes": { 424 | "type": "object", 425 | "properties": { 426 | "hash": { 427 | "description": "生成的交易hash", 428 | "type": "string" 429 | } 430 | } 431 | } 432 | }, 433 | "securityDefinitions": { 434 | "ApiKeyAuth": { 435 | "type": "apiKey", 436 | "name": "x-token", 437 | "in": "header" 438 | } 439 | } 440 | }` 441 | 442 | type swaggerInfo struct { 443 | Version string 444 | Host string 445 | BasePath string 446 | Schemes []string 447 | Title string 448 | Description string 449 | } 450 | 451 | // SwaggerInfo holds exported Swagger Info so clients can modify it 452 | var SwaggerInfo = swaggerInfo{ 453 | Version: "1.0", 454 | Host: "", 455 | BasePath: "", 456 | Schemes: []string{}, 457 | Title: "Swagger Example API", 458 | Description: "This is a sample server Petstore server.", 459 | } 460 | 461 | type s struct{} 462 | 463 | func (s *s) ReadDoc() string { 464 | sInfo := SwaggerInfo 465 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) 466 | 467 | t, err := template.New("swagger_info").Funcs(template.FuncMap{ 468 | "marshal": func(v interface{}) string { 469 | a, _ := json.Marshal(v) 470 | return string(a) 471 | }, 472 | "escape": func(v interface{}) string { 473 | // escape tabs 474 | str := strings.Replace(v.(string), "\t", "\\t", -1) 475 | // replace " with \", and if that results in \\", replace that with \\\" 476 | str = strings.Replace(str, "\"", "\\\"", -1) 477 | return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) 478 | }, 479 | }).Parse(doc) 480 | if err != nil { 481 | return doc 482 | } 483 | 484 | var tpl bytes.Buffer 485 | if err := t.Execute(&tpl, sInfo); err != nil { 486 | return doc 487 | } 488 | 489 | return tpl.String() 490 | } 491 | 492 | func init() { 493 | swag.Register("swagger", &s{}) 494 | } 495 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample server Petstore server.", 5 | "title": "Swagger Example API", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "name": "API Support", 9 | "url": "http://www.swagger.io/support", 10 | "email": "support@swagger.io" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | }, 16 | "version": "1.0" 17 | }, 18 | "paths": { 19 | "/api/collection": { 20 | "post": { 21 | "security": [ 22 | { 23 | "ApiKeyAuth": [] 24 | } 25 | ], 26 | "produces": [ 27 | "application/json" 28 | ], 29 | "tags": [ 30 | "归集某个地址" 31 | ], 32 | "summary": "归集", 33 | "parameters": [ 34 | { 35 | "description": "参数", 36 | "name": "login", 37 | "in": "body", 38 | "required": true, 39 | "schema": { 40 | "$ref": "#/definitions/server.CollectionReq" 41 | } 42 | } 43 | ], 44 | "responses": { 45 | "200": { 46 | "description": "OK", 47 | "schema": { 48 | "allOf": [ 49 | { 50 | "$ref": "#/definitions/server.Response" 51 | }, 52 | { 53 | "type": "object", 54 | "properties": { 55 | "data": { 56 | "$ref": "#/definitions/server.CollectionRes" 57 | } 58 | } 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | "/api/createWallet": { 67 | "post": { 68 | "security": [ 69 | { 70 | "ApiKeyAuth": [] 71 | } 72 | ], 73 | "produces": [ 74 | "application/json" 75 | ], 76 | "tags": [ 77 | "钱包" 78 | ], 79 | "summary": "创建钱包地址", 80 | "parameters": [ 81 | { 82 | "description": "参数", 83 | "name": "login", 84 | "in": "body", 85 | "required": true, 86 | "schema": { 87 | "$ref": "#/definitions/server.CreateWalletReq" 88 | } 89 | } 90 | ], 91 | "responses": { 92 | "200": { 93 | "description": "OK", 94 | "schema": { 95 | "allOf": [ 96 | { 97 | "$ref": "#/definitions/server.Response" 98 | }, 99 | { 100 | "type": "object", 101 | "properties": { 102 | "data": { 103 | "$ref": "#/definitions/server.CreateWalletRes" 104 | } 105 | } 106 | } 107 | ] 108 | } 109 | } 110 | } 111 | } 112 | }, 113 | "/api/delWallet": { 114 | "post": { 115 | "security": [ 116 | { 117 | "ApiKeyAuth": [] 118 | } 119 | ], 120 | "produces": [ 121 | "application/json" 122 | ], 123 | "tags": [ 124 | "钱包" 125 | ], 126 | "summary": "删除钱包地址", 127 | "parameters": [ 128 | { 129 | "description": "参数", 130 | "name": "login", 131 | "in": "body", 132 | "required": true, 133 | "schema": { 134 | "$ref": "#/definitions/server.DelWalletReq" 135 | } 136 | } 137 | ], 138 | "responses": { 139 | "200": { 140 | "description": "OK", 141 | "schema": { 142 | "$ref": "#/definitions/server.Response" 143 | } 144 | } 145 | } 146 | } 147 | }, 148 | "/api/getTransactionReceipt": { 149 | "get": { 150 | "security": [ 151 | { 152 | "ApiKeyAuth": [] 153 | } 154 | ], 155 | "produces": [ 156 | "application/json" 157 | ], 158 | "tags": [ 159 | "钱包" 160 | ], 161 | "summary": "获取交易结果", 162 | "parameters": [ 163 | { 164 | "description": "参数", 165 | "name": "login", 166 | "in": "body", 167 | "required": true, 168 | "schema": { 169 | "$ref": "#/definitions/server.TransactionReceiptReq" 170 | } 171 | } 172 | ], 173 | "responses": { 174 | "200": { 175 | "description": "OK", 176 | "schema": { 177 | "allOf": [ 178 | { 179 | "$ref": "#/definitions/server.Response" 180 | }, 181 | { 182 | "type": "object", 183 | "properties": { 184 | "data": { 185 | "$ref": "#/definitions/server.TransactionReceiptRes" 186 | } 187 | } 188 | } 189 | ] 190 | } 191 | } 192 | } 193 | } 194 | }, 195 | "/api/withdraw": { 196 | "post": { 197 | "security": [ 198 | { 199 | "ApiKeyAuth": [] 200 | } 201 | ], 202 | "produces": [ 203 | "application/json" 204 | ], 205 | "tags": [ 206 | "钱包" 207 | ], 208 | "summary": "提现", 209 | "parameters": [ 210 | { 211 | "description": "参数", 212 | "name": "login", 213 | "in": "body", 214 | "required": true, 215 | "schema": { 216 | "$ref": "#/definitions/server.WithdrawReq" 217 | } 218 | } 219 | ], 220 | "responses": { 221 | "200": { 222 | "description": "OK", 223 | "schema": { 224 | "allOf": [ 225 | { 226 | "$ref": "#/definitions/server.Response" 227 | }, 228 | { 229 | "type": "object", 230 | "properties": { 231 | "data": { 232 | "$ref": "#/definitions/server.WithdrawRes" 233 | } 234 | } 235 | } 236 | ] 237 | } 238 | } 239 | } 240 | } 241 | } 242 | }, 243 | "definitions": { 244 | "server.CollectionReq": { 245 | "type": "object", 246 | "required": [ 247 | "address", 248 | "coinName", 249 | "max", 250 | "protocol" 251 | ], 252 | "properties": { 253 | "address": { 254 | "description": "地址", 255 | "type": "string" 256 | }, 257 | "coinName": { 258 | "description": "币种名称", 259 | "type": "string" 260 | }, 261 | "max": { 262 | "description": "最大归集数量(满足当前值才会归集)", 263 | "type": "string" 264 | }, 265 | "protocol": { 266 | "description": "协议", 267 | "type": "string" 268 | } 269 | } 270 | }, 271 | "server.CollectionRes": { 272 | "type": "object", 273 | "properties": { 274 | "balance": { 275 | "description": "实际归集的数量", 276 | "type": "string" 277 | } 278 | } 279 | }, 280 | "server.CreateWalletReq": { 281 | "type": "object", 282 | "required": [ 283 | "coinName", 284 | "protocol" 285 | ], 286 | "properties": { 287 | "coinName": { 288 | "description": "币种名称", 289 | "type": "string" 290 | }, 291 | "protocol": { 292 | "description": "协议", 293 | "type": "string" 294 | } 295 | } 296 | }, 297 | "server.CreateWalletRes": { 298 | "type": "object", 299 | "properties": { 300 | "address": { 301 | "description": "生成的钱包地址", 302 | "type": "string" 303 | } 304 | } 305 | }, 306 | "server.DelWalletReq": { 307 | "type": "object", 308 | "required": [ 309 | "address", 310 | "coinName", 311 | "protocol" 312 | ], 313 | "properties": { 314 | "address": { 315 | "description": "地址", 316 | "type": "string" 317 | }, 318 | "coinName": { 319 | "description": "币种名称", 320 | "type": "string" 321 | }, 322 | "protocol": { 323 | "description": "协议", 324 | "type": "string" 325 | } 326 | } 327 | }, 328 | "server.Response": { 329 | "type": "object", 330 | "properties": { 331 | "code": { 332 | "description": "错误code码", 333 | "type": "integer" 334 | }, 335 | "data": { 336 | "description": "成功时返回的对象" 337 | }, 338 | "message": { 339 | "description": "错误信息", 340 | "type": "string" 341 | } 342 | } 343 | }, 344 | "server.TransactionReceiptReq": { 345 | "type": "object", 346 | "required": [ 347 | "coinName", 348 | "hash", 349 | "protocol" 350 | ], 351 | "properties": { 352 | "coinName": { 353 | "description": "币种名称", 354 | "type": "string" 355 | }, 356 | "hash": { 357 | "description": "交易哈希", 358 | "type": "string" 359 | }, 360 | "protocol": { 361 | "description": "协议", 362 | "type": "string" 363 | } 364 | } 365 | }, 366 | "server.TransactionReceiptRes": { 367 | "type": "object", 368 | "properties": { 369 | "status": { 370 | "description": "交易状态(0:未成功,1:已成功)", 371 | "type": "integer" 372 | } 373 | } 374 | }, 375 | "server.WithdrawReq": { 376 | "type": "object", 377 | "required": [ 378 | "address", 379 | "coinName", 380 | "orderId", 381 | "protocol", 382 | "value" 383 | ], 384 | "properties": { 385 | "address": { 386 | "description": "提现地址", 387 | "type": "string" 388 | }, 389 | "coinName": { 390 | "description": "币种名称", 391 | "type": "string" 392 | }, 393 | "orderId": { 394 | "description": "订单号", 395 | "type": "string" 396 | }, 397 | "protocol": { 398 | "description": "协议", 399 | "type": "string" 400 | }, 401 | "value": { 402 | "description": "金额", 403 | "type": "integer" 404 | } 405 | } 406 | }, 407 | "server.WithdrawRes": { 408 | "type": "object", 409 | "properties": { 410 | "hash": { 411 | "description": "生成的交易hash", 412 | "type": "string" 413 | } 414 | } 415 | } 416 | }, 417 | "securityDefinitions": { 418 | "ApiKeyAuth": { 419 | "type": "apiKey", 420 | "name": "x-token", 421 | "in": "header" 422 | } 423 | } 424 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | server.CollectionReq: 3 | properties: 4 | address: 5 | description: 地址 6 | type: string 7 | coinName: 8 | description: 币种名称 9 | type: string 10 | max: 11 | description: 最大归集数量(满足当前值才会归集) 12 | type: string 13 | protocol: 14 | description: 协议 15 | type: string 16 | required: 17 | - address 18 | - coinName 19 | - max 20 | - protocol 21 | type: object 22 | server.CollectionRes: 23 | properties: 24 | balance: 25 | description: 实际归集的数量 26 | type: string 27 | type: object 28 | server.CreateWalletReq: 29 | properties: 30 | coinName: 31 | description: 币种名称 32 | type: string 33 | protocol: 34 | description: 协议 35 | type: string 36 | required: 37 | - coinName 38 | - protocol 39 | type: object 40 | server.CreateWalletRes: 41 | properties: 42 | address: 43 | description: 生成的钱包地址 44 | type: string 45 | type: object 46 | server.DelWalletReq: 47 | properties: 48 | address: 49 | description: 地址 50 | type: string 51 | coinName: 52 | description: 币种名称 53 | type: string 54 | protocol: 55 | description: 协议 56 | type: string 57 | required: 58 | - address 59 | - coinName 60 | - protocol 61 | type: object 62 | server.Response: 63 | properties: 64 | code: 65 | description: 错误code码 66 | type: integer 67 | data: 68 | description: 成功时返回的对象 69 | message: 70 | description: 错误信息 71 | type: string 72 | type: object 73 | server.TransactionReceiptReq: 74 | properties: 75 | coinName: 76 | description: 币种名称 77 | type: string 78 | hash: 79 | description: 交易哈希 80 | type: string 81 | protocol: 82 | description: 协议 83 | type: string 84 | required: 85 | - coinName 86 | - hash 87 | - protocol 88 | type: object 89 | server.TransactionReceiptRes: 90 | properties: 91 | status: 92 | description: 交易状态(0:未成功,1:已成功) 93 | type: integer 94 | type: object 95 | server.WithdrawReq: 96 | properties: 97 | address: 98 | description: 提现地址 99 | type: string 100 | coinName: 101 | description: 币种名称 102 | type: string 103 | orderId: 104 | description: 订单号 105 | type: string 106 | protocol: 107 | description: 协议 108 | type: string 109 | value: 110 | description: 金额 111 | type: integer 112 | required: 113 | - address 114 | - coinName 115 | - orderId 116 | - protocol 117 | - value 118 | type: object 119 | server.WithdrawRes: 120 | properties: 121 | hash: 122 | description: 生成的交易hash 123 | type: string 124 | type: object 125 | info: 126 | contact: 127 | email: support@swagger.io 128 | name: API Support 129 | url: http://www.swagger.io/support 130 | description: This is a sample server Petstore server. 131 | license: 132 | name: Apache 2.0 133 | url: http://www.apache.org/licenses/LICENSE-2.0.html 134 | termsOfService: http://swagger.io/terms/ 135 | title: Swagger Example API 136 | version: "1.0" 137 | paths: 138 | /api/collection: 139 | post: 140 | parameters: 141 | - description: 参数 142 | in: body 143 | name: login 144 | required: true 145 | schema: 146 | $ref: '#/definitions/server.CollectionReq' 147 | produces: 148 | - application/json 149 | responses: 150 | "200": 151 | description: OK 152 | schema: 153 | allOf: 154 | - $ref: '#/definitions/server.Response' 155 | - properties: 156 | data: 157 | $ref: '#/definitions/server.CollectionRes' 158 | type: object 159 | security: 160 | - ApiKeyAuth: [] 161 | summary: 归集 162 | tags: 163 | - 归集某个地址 164 | /api/createWallet: 165 | post: 166 | parameters: 167 | - description: 参数 168 | in: body 169 | name: login 170 | required: true 171 | schema: 172 | $ref: '#/definitions/server.CreateWalletReq' 173 | produces: 174 | - application/json 175 | responses: 176 | "200": 177 | description: OK 178 | schema: 179 | allOf: 180 | - $ref: '#/definitions/server.Response' 181 | - properties: 182 | data: 183 | $ref: '#/definitions/server.CreateWalletRes' 184 | type: object 185 | security: 186 | - ApiKeyAuth: [] 187 | summary: 创建钱包地址 188 | tags: 189 | - 钱包 190 | /api/delWallet: 191 | post: 192 | parameters: 193 | - description: 参数 194 | in: body 195 | name: login 196 | required: true 197 | schema: 198 | $ref: '#/definitions/server.DelWalletReq' 199 | produces: 200 | - application/json 201 | responses: 202 | "200": 203 | description: OK 204 | schema: 205 | $ref: '#/definitions/server.Response' 206 | security: 207 | - ApiKeyAuth: [] 208 | summary: 删除钱包地址 209 | tags: 210 | - 钱包 211 | /api/getTransactionReceipt: 212 | get: 213 | parameters: 214 | - description: 参数 215 | in: body 216 | name: login 217 | required: true 218 | schema: 219 | $ref: '#/definitions/server.TransactionReceiptReq' 220 | produces: 221 | - application/json 222 | responses: 223 | "200": 224 | description: OK 225 | schema: 226 | allOf: 227 | - $ref: '#/definitions/server.Response' 228 | - properties: 229 | data: 230 | $ref: '#/definitions/server.TransactionReceiptRes' 231 | type: object 232 | security: 233 | - ApiKeyAuth: [] 234 | summary: 获取交易结果 235 | tags: 236 | - 钱包 237 | /api/withdraw: 238 | post: 239 | parameters: 240 | - description: 参数 241 | in: body 242 | name: login 243 | required: true 244 | schema: 245 | $ref: '#/definitions/server.WithdrawReq' 246 | produces: 247 | - application/json 248 | responses: 249 | "200": 250 | description: OK 251 | schema: 252 | allOf: 253 | - $ref: '#/definitions/server.Response' 254 | - properties: 255 | data: 256 | $ref: '#/definitions/server.WithdrawRes' 257 | type: object 258 | security: 259 | - ApiKeyAuth: [] 260 | summary: 提现 261 | tags: 262 | - 钱包 263 | securityDefinitions: 264 | ApiKeyAuth: 265 | in: header 266 | name: x-token 267 | type: apiKey 268 | swagger: "2.0" 269 | -------------------------------------------------------------------------------- /engine/btc_worker.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/lmxdawn/wallet/btc" 5 | "github.com/lmxdawn/wallet/types" 6 | "math/big" 7 | ) 8 | 9 | type BtcWorker struct { 10 | confirms uint64 // 需要的确认数 11 | http *btc.Web 12 | network string // 网络{MainNet:主网,TestNet:测试网,TestNet3:测试网3,SimNet:测试网} 13 | } 14 | 15 | func NewBtcWorker(confirms uint64, host, user, pass, network string) (*BtcWorker, error) { 16 | http, err := btc.NewWeb(host, user, pass, network) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return &BtcWorker{ 22 | confirms: confirms, 23 | http: http, 24 | }, nil 25 | } 26 | 27 | // GetNowBlockNum 获取最新块 28 | func (e *BtcWorker) GetNowBlockNum() (uint64, error) { 29 | blockNumber, err := e.http.GetBlockCount() 30 | if err != nil { 31 | return 0, err 32 | } 33 | return uint64(blockNumber), nil 34 | } 35 | 36 | // GetTransactionReceipt 获取交易的票据 37 | func (e *BtcWorker) GetTransactionReceipt(transaction *types.Transaction) error { 38 | 39 | // TODO 待实现 40 | 41 | return nil 42 | 43 | } 44 | 45 | // GetTransaction 获取交易信息 46 | func (e *BtcWorker) GetTransaction(num uint64) ([]types.Transaction, uint64, error) { 47 | nowBlockNumber, err := e.GetNowBlockNum() 48 | if err != nil { 49 | return nil, num, err 50 | } 51 | toBlock := num + 100 52 | // 传入的num为0,表示最新块 53 | if num == 0 { 54 | toBlock = nowBlockNumber 55 | } else if toBlock > nowBlockNumber { 56 | toBlock = nowBlockNumber 57 | } 58 | 59 | //numInt := int64(num) 60 | //toBlockInt := int64(toBlock) 61 | 62 | // TODO 待实现 63 | 64 | var transactions []types.Transaction 65 | 66 | return transactions, toBlock, nil 67 | } 68 | 69 | // GetBalance 获取余额 70 | func (e *BtcWorker) GetBalance(address string) (*big.Int, error) { 71 | balance, err := e.http.GetBalance(address) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return big.NewInt(balance), nil 76 | } 77 | 78 | // CreateWallet 创建钱包 79 | func (e *BtcWorker) CreateWallet() (*types.Wallet, error) { 80 | 81 | btcW, err := e.http.CreateWallet() 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return &types.Wallet{ 87 | Address: btcW.NestedSegWitAddress, // 使用3开头的地址 88 | PublicKey: btcW.PublicKey, 89 | PrivateKey: btcW.PrivateKey, 90 | }, err 91 | } 92 | 93 | // Transfer 转账 94 | func (e *BtcWorker) Transfer(privateKeyStr string, toAddress string, value *big.Int, nonce uint64) (string, string, uint64, error) { 95 | 96 | from, err := e.GetAddressByPrivateKey(privateKeyStr) 97 | if err != nil { 98 | return "", "", 0, err 99 | } 100 | hash, err := e.http.Transfer(privateKeyStr, from, toAddress, value.Int64(), 0.001*1e8) 101 | if err != nil { 102 | return "", "", 0, err 103 | } 104 | 105 | return from, hash, nonce, nil 106 | } 107 | 108 | // GetAddressByPrivateKey 根据私钥获取地址 109 | func (e *BtcWorker) GetAddressByPrivateKey(privateKeyStr string) (string, error) { 110 | return e.http.GetWalletByPrivateKey(privateKeyStr) 111 | } 112 | -------------------------------------------------------------------------------- /engine/concurrent.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/lmxdawn/wallet/client" 5 | "github.com/lmxdawn/wallet/config" 6 | "github.com/lmxdawn/wallet/db" 7 | "github.com/lmxdawn/wallet/scheduler" 8 | "github.com/lmxdawn/wallet/types" 9 | "github.com/rs/zerolog/log" 10 | "math/big" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | type Worker interface { 16 | GetNowBlockNum() (uint64, error) 17 | GetTransaction(num uint64) ([]types.Transaction, uint64, error) 18 | GetTransactionReceipt(*types.Transaction) error 19 | GetBalance(address string) (*big.Int, error) 20 | CreateWallet() (*types.Wallet, error) 21 | Transfer(privateKeyStr string, toAddress string, value *big.Int, nonce uint64) (string, string, uint64, error) 22 | } 23 | 24 | type Scheduler interface { 25 | BlockWorkerChan() chan uint64 26 | BlockWorkerReady(chan uint64) 27 | BlockSubmit(uint64) 28 | BlockRun() 29 | ReceiptWorkerChan() chan types.Transaction 30 | ReceiptWorkerReady(chan types.Transaction) 31 | ReceiptSubmit(types.Transaction) 32 | ReceiptRun() 33 | CollectionSendWorkerChan() chan db.WalletItem 34 | CollectionSendWorkerReady(c chan db.WalletItem) 35 | CollectionSendSubmit(c db.WalletItem) 36 | CollectionSendRun() 37 | } 38 | 39 | type ConCurrentEngine struct { 40 | scheduler Scheduler 41 | Worker Worker 42 | config config.EngineConfig 43 | Protocol string 44 | CoinName string 45 | db db.Database 46 | http *client.HttpClient 47 | } 48 | 49 | // Run 启动 50 | func (c *ConCurrentEngine) Run() { 51 | // 关闭连接 52 | defer c.db.Close() 53 | 54 | go c.blockLoop() 55 | go c.collectionLoop() 56 | 57 | select {} 58 | } 59 | 60 | // blockLoop 区块循环监听 61 | func (c *ConCurrentEngine) blockLoop() { 62 | // 读取当前区块 63 | blockNumber := c.config.BlockInit 64 | blockNumberStr, err := c.db.Get("block_number") 65 | if err == nil && blockNumberStr != "" { 66 | blockNumber, _ = strconv.ParseUint(blockNumberStr, 10, 64) 67 | } 68 | 69 | // 区块信息 70 | c.scheduler.BlockRun() 71 | // 交易信息 72 | c.scheduler.ReceiptRun() 73 | // 归集信息 74 | c.scheduler.CollectionSendRun() 75 | 76 | // 批量创建区块worker 77 | blockWorkerOut := make(chan types.Transaction) 78 | c.createBlockWorker(blockWorkerOut) 79 | 80 | // 批量创建交易worker 81 | for i := uint64(0); i < c.config.ReceiptCount; i++ { 82 | c.createReceiptWorker() 83 | } 84 | 85 | c.scheduler.BlockSubmit(blockNumber) 86 | 87 | go func() { 88 | for { 89 | transaction := <-blockWorkerOut 90 | //log.Info().Msgf("交易:%v", transaction) 91 | c.scheduler.ReceiptSubmit(transaction) 92 | } 93 | }() 94 | } 95 | 96 | // collectionLoop 归集循环监听 97 | func (c *ConCurrentEngine) collectionLoop() { 98 | n := new(big.Int) 99 | collectionMax, ok := n.SetString(c.config.CollectionMax, 10) 100 | if !ok { 101 | panic("setString: error") 102 | } 103 | // 配置大于0才去自动归集 104 | if collectionMax.Cmp(big.NewInt(0)) > 0 { 105 | // 启动归集 106 | collectionWorkerOut := make(chan db.WalletItem) 107 | c.createCollectionWorker(collectionWorkerOut) 108 | 109 | // 启动归集发送worker 110 | for i := uint64(0); i < c.config.CollectionCount; i++ { 111 | c.createCollectionSendWorker(collectionMax) 112 | } 113 | 114 | go func() { 115 | for { 116 | collectionSend := <-collectionWorkerOut 117 | c.scheduler.CollectionSendSubmit(collectionSend) 118 | } 119 | }() 120 | } 121 | } 122 | 123 | // createBlockWorker 创建获取区块信息的工作 124 | func (c *ConCurrentEngine) createBlockWorker(out chan types.Transaction) { 125 | in := c.scheduler.BlockWorkerChan() 126 | go func() { 127 | for { 128 | c.scheduler.BlockWorkerReady(in) 129 | num := <-in 130 | log.Info().Msgf("%v,监听区块:%d", c.config.CoinName, num) 131 | transactions, blockNum, err := c.Worker.GetTransaction(num) 132 | if err != nil || blockNum == num { 133 | log.Info().Msgf("等待%d秒,当前已是最新区块", c.config.BlockAfterTime) 134 | <-time.After(time.Duration(c.config.BlockAfterTime) * time.Second) 135 | c.scheduler.BlockSubmit(num) 136 | continue 137 | } 138 | err = c.db.Put("block_number", strconv.FormatUint(blockNum, 10)) 139 | if err != nil { 140 | c.scheduler.BlockSubmit(num) 141 | } else { 142 | c.scheduler.BlockSubmit(blockNum) 143 | } 144 | for _, transaction := range transactions { 145 | out <- transaction 146 | } 147 | } 148 | }() 149 | } 150 | 151 | // createReceiptWorker 创建获取区块信息的工作 152 | func (c *ConCurrentEngine) createReceiptWorker() { 153 | in := c.scheduler.ReceiptWorkerChan() 154 | go func() { 155 | for { 156 | c.scheduler.ReceiptWorkerReady(in) 157 | transaction := <-in 158 | err := c.Worker.GetTransactionReceipt(&transaction) 159 | if err != nil { 160 | log.Info().Msgf("等待%d秒,收据信息无效, err: %v", c.config.ReceiptAfterTime, err) 161 | <-time.After(time.Duration(c.config.ReceiptAfterTime) * time.Second) 162 | c.scheduler.ReceiptSubmit(transaction) 163 | continue 164 | } 165 | if transaction.Status != 1 { 166 | log.Error().Msgf("交易失败:%v", transaction.Hash) 167 | continue 168 | } 169 | //log.Info().Msgf("交易完成:%v", transaction.Hash) 170 | 171 | // 判断是否存在 172 | if ok, err := c.db.Has(c.config.HashPrefix + transaction.Hash); err == nil && ok { 173 | log.Info().Msgf("监听到提现事件,提现地址:%v,提现哈希:%v", transaction.To, transaction.Hash) 174 | orderId, err := c.db.Get(c.config.HashPrefix + transaction.Hash) 175 | if err != nil { 176 | log.Error().Msgf("未查询到订单:%v, %v", transaction.Hash, err) 177 | // 重新提交 178 | c.scheduler.ReceiptSubmit(transaction) 179 | continue 180 | } 181 | err = c.http.WithdrawSuccess(transaction.Hash, transaction.Status, orderId, transaction.To, transaction.Value.Int64()) 182 | if err != nil { 183 | log.Error().Msgf("提现回调通知失败:%v, %v", transaction.Hash, err) 184 | // 重新提交 185 | c.scheduler.ReceiptSubmit(transaction) 186 | continue 187 | } 188 | _ = c.db.Delete(transaction.Hash) 189 | } else if ok, err := c.db.Has(c.config.WalletPrefix + transaction.To); err == nil && ok { 190 | log.Info().Msgf("监听到充值事件,充值地址:%v,充值哈希:%v", transaction.To, transaction.Hash) 191 | err = c.http.RechargeSuccess(transaction.Hash, transaction.Status, transaction.To, transaction.Value.Int64()) 192 | if err != nil { 193 | log.Error().Msgf("充值回调通知失败:%v, %v", transaction.Hash, err) 194 | // 重新提交 195 | c.scheduler.ReceiptSubmit(transaction) 196 | continue 197 | } 198 | } 199 | } 200 | }() 201 | } 202 | 203 | // createCollectionWorker 创建归集Worker 204 | func (c *ConCurrentEngine) createCollectionWorker(out chan db.WalletItem) { 205 | go func() { 206 | for { 207 | <-time.After(time.Duration(c.config.CollectionAfterTime) * time.Second) 208 | list, err := c.db.ListWallet(c.config.WalletPrefix) 209 | if err != nil { 210 | continue 211 | } 212 | for _, item := range list { 213 | out <- item 214 | } 215 | } 216 | }() 217 | } 218 | 219 | // collectionSendWorker 创建归集发送交易的worker 220 | func (c *ConCurrentEngine) createCollectionSendWorker(max *big.Int) { 221 | in := c.scheduler.CollectionSendWorkerChan() 222 | go func() { 223 | for { 224 | c.scheduler.CollectionSendWorkerReady(in) 225 | collectionSend := <-in 226 | _, err := c.collection(collectionSend.Address, collectionSend.PrivateKey, max) 227 | if err != nil { 228 | // 归集失败,重新加入归集队列 229 | c.scheduler.CollectionSendSubmit(collectionSend) 230 | continue 231 | } 232 | } 233 | }() 234 | } 235 | 236 | // 归集 237 | func (c ConCurrentEngine) collection(address, privateKey string, max *big.Int) (*big.Int, error) { 238 | balance, err := c.Worker.GetBalance(address) 239 | if err != nil { 240 | return nil, err 241 | } 242 | if balance.Cmp(max) < 0 { 243 | return big.NewInt(0), nil 244 | } 245 | 246 | // 开始归集 247 | _, _, _, err = c.Worker.Transfer(privateKey, c.config.CollectionAddress, balance, 0) 248 | if err != nil { 249 | return nil, err 250 | } 251 | return balance, nil 252 | } 253 | 254 | // Collection 归集某个地址 255 | func (c *ConCurrentEngine) Collection(address string, max *big.Int) (*big.Int, error) { 256 | 257 | // 查询地址是否存在 258 | privateKey, err := c.db.Get(c.config.WalletPrefix + address) 259 | if err != nil { 260 | return nil, err 261 | } 262 | 263 | balance, err := c.collection(address, privateKey, max) 264 | if err != nil { 265 | return nil, err 266 | } 267 | 268 | return balance, nil 269 | } 270 | 271 | // CreateWallet 创建钱包 272 | func (c *ConCurrentEngine) CreateWallet() (string, error) { 273 | wallet, err := c.Worker.CreateWallet() 274 | if err != nil { 275 | return "", err 276 | } 277 | _ = c.db.Put(c.config.WalletPrefix+wallet.Address, wallet.PrivateKey) 278 | log.Info().Msgf("创建钱包成功,地址:%v,私钥:%v", wallet.Address, wallet.PrivateKey) 279 | return wallet.Address, nil 280 | } 281 | 282 | // DeleteWallet 删除钱包 283 | func (c *ConCurrentEngine) DeleteWallet(address string) error { 284 | err := c.db.Delete(c.config.WalletPrefix + address) 285 | if err != nil { 286 | return err 287 | } 288 | return nil 289 | } 290 | 291 | // Withdraw 提现 292 | func (c *ConCurrentEngine) Withdraw(orderId string, toAddress string, value int64) (string, error) { 293 | 294 | _, hash, _, err := c.Worker.Transfer(c.config.WithdrawPrivateKey, toAddress, big.NewInt(value), 0) 295 | if err != nil { 296 | return "", err 297 | } 298 | _ = c.db.Put(c.config.HashPrefix+hash, orderId) 299 | return hash, nil 300 | } 301 | 302 | // GetTransactionReceipt 获取交易状态 303 | func (c *ConCurrentEngine) GetTransactionReceipt(hash string) (int, error) { 304 | 305 | t := &types.Transaction{ 306 | Hash: hash, 307 | Status: 0, 308 | } 309 | 310 | err := c.Worker.GetTransactionReceipt(t) 311 | if err != nil { 312 | return 0, err 313 | } 314 | 315 | return int(t.Status), nil 316 | } 317 | 318 | // NewEngine 创建ETH 319 | func NewEngine(config config.EngineConfig) (*ConCurrentEngine, error) { 320 | keyDB, err := db.NewKeyDB(config.File) 321 | if err != nil { 322 | return nil, err 323 | } 324 | 325 | var worker Worker 326 | switch config.Protocol { 327 | case "eth": 328 | worker, err = NewEthWorker(config.Confirms, config.Contract, config.Rpc) 329 | if err != nil { 330 | return nil, err 331 | } 332 | case "trx": 333 | worker, err = NewTronWorker(config.Confirms, config.Contract, config.ContractType, config.Rpc) 334 | if err != nil { 335 | return nil, err 336 | } 337 | case "btc": 338 | worker, err = NewBtcWorker(config.Confirms, config.Rpc, config.User, config.Pass, config.Rpc) 339 | if err != nil { 340 | return nil, err 341 | } 342 | } 343 | 344 | http := client.NewHttpClient(config.Protocol, config.CoinName, config.RechargeNotifyUrl, config.WithdrawNotifyUrl) 345 | 346 | return &ConCurrentEngine{ 347 | //scheduler: scheduler.NewSimpleScheduler(), // 简单的任务调度器 348 | scheduler: scheduler.NewQueueScheduler(), // 队列的任务调度器 349 | Worker: worker, 350 | config: config, 351 | Protocol: config.Protocol, 352 | CoinName: config.CoinName, 353 | db: keyDB, 354 | http: http, 355 | }, nil 356 | } 357 | -------------------------------------------------------------------------------- /engine/eth_worker.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "errors" 7 | "github.com/ethereum/go-ethereum" 8 | "github.com/ethereum/go-ethereum/accounts/abi" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/common/hexutil" 11 | ethTypes "github.com/ethereum/go-ethereum/core/types" 12 | "github.com/ethereum/go-ethereum/crypto" 13 | "github.com/ethereum/go-ethereum/ethclient" 14 | "github.com/lmxdawn/wallet/types" 15 | "math/big" 16 | "strings" 17 | ) 18 | 19 | type EthWorker struct { 20 | confirms uint64 // 需要的确认数 21 | http *ethclient.Client 22 | token string // 代币合约地址,为空表示主币 23 | tokenTransferEventHash common.Hash 24 | tokenAbi abi.ABI // 合约的abi 25 | } 26 | 27 | func NewEthWorker(confirms uint64, contract string, url string) (*EthWorker, error) { 28 | http, err := ethclient.Dial(url) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | tokenTransferEventHashSig := []byte("Transfer(address,address,uint256)") 34 | tokenTransferEventHash := crypto.Keccak256Hash(tokenTransferEventHashSig) 35 | tokenAbiStr := "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" 36 | tokenAbi, err := abi.JSON(strings.NewReader(tokenAbiStr)) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return &EthWorker{ 42 | confirms: confirms, 43 | token: contract, 44 | http: http, 45 | tokenTransferEventHash: tokenTransferEventHash, 46 | tokenAbi: tokenAbi, 47 | }, nil 48 | } 49 | 50 | // GetNowBlockNum 获取最新块 51 | func (e *EthWorker) GetNowBlockNum() (uint64, error) { 52 | 53 | blockNumber, err := e.http.BlockNumber(context.Background()) 54 | if err != nil { 55 | return 0, err 56 | } 57 | return blockNumber, nil 58 | } 59 | 60 | // GetTransactionReceipt 获取交易的票据 61 | func (e *EthWorker) GetTransactionReceipt(transaction *types.Transaction) error { 62 | 63 | hash := common.HexToHash(transaction.Hash) 64 | 65 | receipt, err := e.http.TransactionReceipt(context.Background(), hash) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // 获取最新区块 71 | latest, err := e.http.BlockNumber(context.Background()) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // 判断确认数 77 | confirms := latest - receipt.BlockNumber.Uint64() + 1 78 | if confirms < e.confirms { 79 | return errors.New("the number of confirmations is not satisfied") 80 | } 81 | 82 | status := receipt.Status 83 | transaction.Status = uint(status) 84 | 85 | return nil 86 | } 87 | 88 | // GetTransaction 获取交易信息 89 | func (e *EthWorker) GetTransaction(num uint64) ([]types.Transaction, uint64, error) { 90 | nowBlockNumber, err := e.GetNowBlockNum() 91 | if err != nil { 92 | return nil, num, err 93 | } 94 | toBlock := num + 100 95 | // 传入的num为0,表示最新块 96 | if num == 0 { 97 | toBlock = nowBlockNumber 98 | } else if toBlock > nowBlockNumber { 99 | toBlock = nowBlockNumber 100 | } 101 | if e.token == "" { 102 | return e.getBlockTransaction(num) 103 | } else { 104 | return e.getTokenTransaction(num, toBlock) 105 | } 106 | 107 | } 108 | 109 | // getBlockTransaction 获取主币的交易信息 110 | func (e *EthWorker) getBlockTransaction(num uint64) ([]types.Transaction, uint64, error) { 111 | 112 | block, err := e.http.BlockByNumber(context.Background(), big.NewInt(int64(num))) 113 | if err != nil { 114 | return nil, num, err 115 | } 116 | 117 | chainID, err := e.http.NetworkID(context.Background()) 118 | if err != nil { 119 | return nil, num, err 120 | } 121 | 122 | var transactions []types.Transaction 123 | for _, tx := range block.Transactions() { 124 | // 如果接收方地址为空,则是创建合约的交易,忽略过去 125 | if tx.To() == nil { 126 | continue 127 | } 128 | msg, err := tx.AsMessage(ethTypes.LatestSignerForChainID(chainID), tx.GasPrice()) 129 | if err != nil { 130 | continue 131 | } 132 | transactions = append(transactions, types.Transaction{ 133 | BlockNumber: big.NewInt(int64(num)), 134 | BlockHash: block.Hash().Hex(), 135 | Hash: tx.Hash().Hex(), 136 | From: msg.From().Hex(), 137 | To: tx.To().Hex(), 138 | Value: tx.Value(), 139 | }) 140 | } 141 | return transactions, num + 1, nil 142 | } 143 | 144 | // getTokenTransaction 获取代币的交易信息 145 | func (e *EthWorker) getTokenTransaction(num uint64, toBlock uint64) ([]types.Transaction, uint64, error) { 146 | contractAddress := common.HexToAddress(e.token) 147 | query := ethereum.FilterQuery{ 148 | FromBlock: big.NewInt(int64(num)), 149 | ToBlock: big.NewInt(int64(toBlock)), 150 | Addresses: []common.Address{ 151 | contractAddress, 152 | }, 153 | } 154 | logs, err := e.http.FilterLogs(context.Background(), query) 155 | if err != nil { 156 | return nil, num, err 157 | } 158 | 159 | var transactions []types.Transaction 160 | for _, vLog := range logs { 161 | switch vLog.Topics[0] { 162 | case e.tokenTransferEventHash: 163 | 164 | tokenTransfer := struct { 165 | From common.Address 166 | To common.Address 167 | Value *big.Int 168 | }{} 169 | 170 | err = e.tokenAbi.UnpackIntoInterface(&tokenTransfer, "Transfer", vLog.Data) 171 | if err != nil { 172 | continue 173 | } 174 | 175 | transactions = append(transactions, types.Transaction{ 176 | BlockNumber: big.NewInt(int64(num)), 177 | BlockHash: vLog.BlockHash.Hex(), 178 | Hash: vLog.TxHash.Hex(), 179 | From: strings.ToLower(common.HexToAddress(vLog.Topics[1].Hex()).Hex()), 180 | To: strings.ToLower(common.HexToAddress(vLog.Topics[2].Hex()).Hex()), 181 | Value: tokenTransfer.Value, 182 | }) 183 | } 184 | } 185 | 186 | return transactions, toBlock, nil 187 | } 188 | 189 | // GetBalance 获取余额 190 | func (e *EthWorker) GetBalance(address string) (*big.Int, error) { 191 | 192 | // 如果不是合约 193 | account := common.HexToAddress(address) 194 | if e.token == "" { 195 | balance, err := e.http.BalanceAt(context.Background(), account, nil) 196 | if err != nil { 197 | return nil, err 198 | } 199 | return balance, nil 200 | } else { 201 | res, err := e.callContract(e.token, "balanceOf", account) 202 | if err != nil { 203 | return nil, err 204 | } 205 | balance := big.NewInt(0) 206 | balance.SetBytes(res) 207 | return balance, nil 208 | } 209 | 210 | } 211 | 212 | // CreateWallet 创建钱包 213 | func (e *EthWorker) CreateWallet() (*types.Wallet, error) { 214 | privateKey, err := crypto.GenerateKey() 215 | if err != nil { 216 | return nil, err 217 | } 218 | 219 | privateKeyBytes := crypto.FromECDSA(privateKey) 220 | 221 | privateKeyString := hexutil.Encode(privateKeyBytes)[2:] 222 | 223 | publicKey := privateKey.Public() 224 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 225 | if !ok { 226 | return nil, errors.New("cannot assert type: publicKey is not of type *ecdsa.PublicKey") 227 | } 228 | 229 | publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) 230 | publicKeyString := hexutil.Encode(publicKeyBytes)[4:] 231 | 232 | address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() 233 | 234 | return &types.Wallet{ 235 | Address: address, 236 | PublicKey: publicKeyString, 237 | PrivateKey: privateKeyString, 238 | }, err 239 | } 240 | 241 | // GetAddressByPrivateKey 根据私钥获取地址 242 | func (e *EthWorker) GetAddressByPrivateKey(privateKeyStr string) (string, error) { 243 | 244 | privateKey, err := crypto.HexToECDSA(privateKeyStr) 245 | if err != nil { 246 | return "", err 247 | } 248 | publicKey := privateKey.Public() 249 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 250 | if !ok { 251 | return "", errors.New("cannot assert type: publicKey is not of type *ecdsa.PublicKey") 252 | } 253 | 254 | fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) 255 | return fromAddress.Hex(), nil 256 | } 257 | 258 | // callContract 查询智能合约 259 | func (e *EthWorker) callContract(contractAddress string, method string, params ...interface{}) ([]byte, error) { 260 | 261 | input, _ := e.tokenAbi.Pack(method, params...) 262 | 263 | to := common.HexToAddress(contractAddress) 264 | msg := ethereum.CallMsg{ 265 | To: &to, 266 | Data: input, 267 | } 268 | 269 | hex, err := e.http.CallContract(context.Background(), msg, nil) 270 | 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | return hex, nil 276 | } 277 | 278 | // Transfer 转账 279 | func (e *EthWorker) Transfer(privateKeyStr string, toAddress string, value *big.Int, nonce uint64) (string, string, uint64, error) { 280 | 281 | var data []byte 282 | var err error 283 | if e.token != "" { 284 | contractTransferHashSig := []byte("transfer(address,uint256)") 285 | contractTransferHash := crypto.Keccak256Hash(contractTransferHashSig) 286 | toAddressTmp := common.HexToAddress(toAddress) 287 | toAddressHex := &toAddressTmp 288 | data, err = makeEthERC20TransferData(contractTransferHash, toAddressHex, value) 289 | if err != nil { 290 | return "", "", 0, err 291 | } 292 | value = big.NewInt(0) 293 | } 294 | 295 | return e.sendTransaction(e.token, privateKeyStr, toAddress, value, nonce, data) 296 | } 297 | 298 | // sendTransaction 创建并发送交易 299 | func (e *EthWorker) sendTransaction(contractAddress string, privateKeyStr string, toAddress string, value *big.Int, nonce uint64, data []byte) (string, string, uint64, error) { 300 | 301 | privateKey, err := crypto.HexToECDSA(privateKeyStr) 302 | if err != nil { 303 | return "", "", 0, err 304 | } 305 | 306 | publicKey := privateKey.Public() 307 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 308 | if !ok { 309 | return "", "", 0, errors.New("cannot assert type: publicKey is not of type *ecdsa.PublicKey") 310 | } 311 | 312 | fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) 313 | if nonce <= 0 { 314 | nonce, err = e.http.PendingNonceAt(context.Background(), fromAddress) 315 | if err != nil { 316 | return "", "", 0, err 317 | } 318 | } 319 | 320 | var gasLimit uint64 321 | gasLimit = uint64(8000000) // in units 322 | gasPrice, err := e.http.SuggestGasPrice(context.Background()) 323 | if err != nil { 324 | return "", "", 0, err 325 | } 326 | var toAddressHex *common.Address 327 | if toAddress != "" { 328 | toAddressTmp := common.HexToAddress(toAddress) 329 | toAddressHex = &toAddressTmp 330 | } 331 | 332 | if contractAddress != "" { 333 | value = big.NewInt(0) 334 | contractAddressHex := common.HexToAddress(contractAddress) 335 | toAddressHex = &contractAddressHex 336 | } 337 | 338 | txData := ðTypes.LegacyTx{ 339 | Nonce: nonce, 340 | To: toAddressHex, 341 | Value: value, 342 | Gas: gasLimit, 343 | GasPrice: gasPrice.Add(gasPrice, big.NewInt(100000000)), 344 | Data: data, 345 | } 346 | 347 | tx := ethTypes.NewTx(txData) 348 | 349 | chainID, err := e.http.NetworkID(context.Background()) 350 | if err != nil { 351 | return "", "", 0, err 352 | } 353 | 354 | signTx, err := ethTypes.SignTx(tx, ethTypes.NewEIP155Signer(chainID), privateKey) 355 | if err != nil { 356 | return "", "", 0, err 357 | } 358 | 359 | err = e.http.SendTransaction(context.Background(), signTx) 360 | if err != nil { 361 | return "", "", 0, err 362 | } 363 | 364 | return fromAddress.Hex(), signTx.Hash().Hex(), nonce, nil 365 | } 366 | 367 | // TransactionMethod 获取某个交易执行的方法 368 | func (e *EthWorker) TransactionMethod(hash string) ([]byte, error) { 369 | 370 | tx, _, err := e.http.TransactionByHash(context.Background(), common.HexToHash(hash)) 371 | if err != nil { 372 | return nil, err 373 | } 374 | 375 | data := tx.Data() 376 | 377 | return data[0:4], nil 378 | } 379 | 380 | func makeEthERC20TransferData(contractTransferHash common.Hash, toAddress *common.Address, amount *big.Int) ([]byte, error) { 381 | var data []byte 382 | data = append(data, contractTransferHash[:4]...) 383 | paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32) 384 | data = append(data, paddedAddress...) 385 | paddedAmount := common.LeftPadBytes(amount.Bytes(), 32) 386 | data = append(data, paddedAmount...) 387 | return data, nil 388 | } 389 | -------------------------------------------------------------------------------- /engine/tron_worker.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "errors" 6 | "fmt" 7 | "github.com/ethereum/go-ethereum/common/hexutil" 8 | "github.com/ethereum/go-ethereum/crypto" 9 | "github.com/fbsobreira/gotron-sdk/pkg/common" 10 | "github.com/fbsobreira/gotron-sdk/pkg/proto/core" 11 | "github.com/golang/protobuf/proto" 12 | "github.com/lmxdawn/wallet/tron" 13 | "github.com/lmxdawn/wallet/types" 14 | "math/big" 15 | ) 16 | 17 | type TronWorker struct { 18 | confirms uint64 // 需要的确认数 19 | http *tron.TronWeb 20 | token string // 代币合约地址,为空表示主币 21 | tokenType string // 合约类型(trc10、trc20)区分 22 | } 23 | 24 | func NewTronWorker(confirms uint64, token string, tokenType string, url string) (*TronWorker, error) { 25 | http, err := tron.NewTronWeb(url, "", "", false) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &TronWorker{ 35 | confirms: confirms, 36 | http: http, 37 | token: token, 38 | tokenType: tokenType, 39 | }, nil 40 | } 41 | 42 | // GetNowBlockNum 获取最新块 43 | func (e *TronWorker) GetNowBlockNum() (uint64, error) { 44 | blockNumber, err := e.http.GetNowBlockNum() 45 | if err != nil { 46 | return 0, err 47 | } 48 | return uint64(blockNumber), nil 49 | } 50 | 51 | // GetTransactionReceipt 获取交易的票据 52 | func (e *TronWorker) GetTransactionReceipt(transaction *types.Transaction) error { 53 | receipt, err := e.http.GetTransactionInfoByID(transaction.Hash, false) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // 获取最新区块 59 | latest, err := e.http.GetNowBlockNum() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // 判断确认数 65 | confirms := latest - receipt.BlockNumber + 1 66 | if uint64(confirms) < e.confirms { 67 | return errors.New(fmt.Sprintf("哈希:%v,当前确认块:%d,需要确认块:%d", transaction.Hash, confirms, e.confirms)) 68 | } 69 | 70 | status := 0 71 | if receipt.Result == core.TransactionInfo_SUCESS { 72 | status = 1 73 | } 74 | transaction.Status = uint(status) 75 | 76 | return nil 77 | 78 | } 79 | 80 | // GetTransaction 获取交易信息 81 | func (e *TronWorker) GetTransaction(num uint64) ([]types.Transaction, uint64, error) { 82 | nowBlockNumber, err := e.GetNowBlockNum() 83 | if err != nil { 84 | return nil, num, err 85 | } 86 | toBlock := num + 100 87 | // 传入的num为0,表示最新块 88 | if num == 0 { 89 | toBlock = nowBlockNumber 90 | } else if toBlock > nowBlockNumber { 91 | toBlock = nowBlockNumber 92 | } 93 | 94 | numInt := int64(num) 95 | toBlockInt := int64(toBlock) 96 | 97 | blocks, err := e.http.GetTransaction(numInt, toBlockInt) 98 | if err != nil { 99 | return nil, num, err 100 | } 101 | 102 | var transactions []types.Transaction 103 | for _, block := range blocks.Block { 104 | for _, v := range block.Transactions { 105 | if v.Result == nil || !v.Result.Result { 106 | continue 107 | } 108 | 109 | rets := v.Transaction.Ret 110 | if len(rets) < 1 || rets[0].ContractRet != core.Transaction_Result_SUCCESS { 111 | continue 112 | } 113 | 114 | txid := common.Bytes2Hex(v.Txid) 115 | // https://tronscan.org/#/transaction/fede1aa9e5c5d7bd179fd62e23bdd11e3c1edd0ca51e41070e34a026d6a42569 116 | 117 | for _, v1 := range v.Transaction.RawData.Contract { 118 | from := "" 119 | to := "" 120 | amount := int64(0) 121 | if v1.Type == core.Transaction_Contract_TransferContract && e.token == "" { 122 | // trx 转账 123 | unObj := &core.TransferContract{} 124 | err := proto.Unmarshal(v1.Parameter.GetValue(), unObj) 125 | if err != nil { 126 | continue 127 | } 128 | from = common.EncodeCheck(unObj.GetOwnerAddress()) 129 | to = common.EncodeCheck(unObj.GetToAddress()) 130 | amount = unObj.GetAmount() 131 | //fmt.Println(form, to, unObj.GetAmount()) 132 | } else if v1.Type == core.Transaction_Contract_TriggerSmartContract && e.token != "" && e.tokenType == "trc20" { 133 | // 调用合约 134 | // trc20 转账 135 | //fmt.Println(v1.Parameter.GetValue()) 136 | // 调用智能合约 137 | unObj := &core.TriggerSmartContract{} 138 | err := proto.Unmarshal(v1.Parameter.GetValue(), unObj) 139 | if err != nil { 140 | continue 141 | } 142 | contract := common.EncodeCheck(unObj.GetContractAddress()) 143 | if contract != e.token { 144 | continue 145 | } 146 | data := unObj.GetData() 147 | // unObj.Data https://goethereumbook.org/en/transfer-tokens/ 参考eth 操作 148 | flag := false 149 | to, amount, flag = processData(data) 150 | // 只有调用了 transfer(address,uint256) 才是转账 151 | if !flag { 152 | continue 153 | } 154 | from = common.EncodeCheck(unObj.GetOwnerAddress()) 155 | //fmt.Println(contract, txid, from, to, amount) 156 | } else if v1.Type == core.Transaction_Contract_TransferAssetContract && e.token != "" && e.tokenType == "trc10" { 157 | // 通证转账合约 158 | // trc10 转账 159 | unObj := &core.TransferAssetContract{} 160 | err := proto.Unmarshal(v1.Parameter.GetValue(), unObj) 161 | if err != nil { 162 | continue 163 | } 164 | // contract := common.EncodeCheck(unObj.GetAssetName()) 165 | from = common.EncodeCheck(unObj.GetOwnerAddress()) 166 | to = common.EncodeCheck(unObj.GetToAddress()) 167 | } 168 | 169 | transactions = append(transactions, types.Transaction{ 170 | BlockNumber: big.NewInt(int64(num)), 171 | BlockHash: common.EncodeCheck(block.Blockid), 172 | Hash: txid, 173 | From: from, 174 | To: to, 175 | Value: big.NewInt(amount), 176 | }) 177 | } 178 | 179 | } 180 | } 181 | 182 | return transactions, toBlock, nil 183 | } 184 | 185 | // GetBalance 获取余额 186 | func (e *TronWorker) GetBalance(address string) (*big.Int, error) { 187 | if e.token != "" && e.tokenType == "TRC20" { 188 | // 获取代币合约余额 189 | jsonString := "[{\"address\":\"" + address + "\"}]" 190 | data, err := e.http.CallContract(e.token, "balanceOf", jsonString) 191 | if err != nil { 192 | return nil, err 193 | } 194 | balance, err := tron.ToNumber(data) 195 | if err != nil { 196 | return nil, err 197 | } 198 | return balance, nil 199 | } 200 | // 获取主币、trc10 201 | balance, err := e.http.GetBalance(address, e.token) 202 | if err != nil { 203 | return nil, err 204 | } 205 | return big.NewInt(balance), nil 206 | } 207 | 208 | // CreateWallet 创建钱包 209 | func (e *TronWorker) CreateWallet() (*types.Wallet, error) { 210 | privateKey, err := crypto.GenerateKey() 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | privateKeyBytes := crypto.FromECDSA(privateKey) 216 | 217 | privateKeyString := hexutil.Encode(privateKeyBytes)[2:] 218 | 219 | publicKey := privateKey.Public() 220 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 221 | if !ok { 222 | return nil, errors.New("cannot assert type: publicKey is not of type *ecdsa.PublicKey") 223 | } 224 | 225 | publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) 226 | publicKeyString := hexutil.Encode(publicKeyBytes)[4:] 227 | 228 | address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() 229 | tronAddress, _ := tron.EthAddress2TronAddress(address) 230 | 231 | return &types.Wallet{ 232 | Address: tronAddress, 233 | PublicKey: publicKeyString, 234 | PrivateKey: privateKeyString, 235 | }, err 236 | } 237 | 238 | // Transfer 转账 239 | func (e *TronWorker) Transfer(privateKeyStr string, toAddress string, value *big.Int, nonce uint64) (string, string, uint64, error) { 240 | 241 | from, err := e.GetAddressByPrivateKey(privateKeyStr) 242 | if err != nil { 243 | return "", "", 0, err 244 | } 245 | hash, err := e.http.Transfer(privateKeyStr, from, toAddress, e.token, value.Int64()) 246 | if err != nil { 247 | return "", "", 0, err 248 | } 249 | 250 | return from, hash, nonce, nil 251 | } 252 | 253 | // GetAddressByPrivateKey 根据私钥获取地址 254 | func (e *TronWorker) GetAddressByPrivateKey(privateKeyStr string) (string, error) { 255 | return tron.Private2TronAddress(privateKeyStr) 256 | } 257 | 258 | // processData 处理合约的调用参数 259 | func processData(data []byte) (to string, amount int64, flag bool) { 260 | 261 | if len(data) >= 68 { 262 | if common.Bytes2Hex(data[:4]) != "a9059cbb" { 263 | return 264 | } 265 | // 多1位41 266 | data[15] = 65 267 | to = common.EncodeCheck(data[15:36]) 268 | amount = new(big.Int).SetBytes(common.TrimLeftZeroes(data[36:68])).Int64() 269 | flag = false 270 | } 271 | return 272 | } 273 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lmxdawn/wallet 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/ethereum/go-ethereum v1.10.14 7 | github.com/fbsobreira/gotron-sdk v0.0.0-20211102183839-58a64f4da5f4 8 | github.com/gin-gonic/gin v1.7.7 9 | github.com/go-playground/universal-translator v0.18.0 10 | github.com/go-playground/validator/v10 v10.9.0 11 | github.com/golang/protobuf v1.4.3 12 | github.com/jinzhu/configor v1.2.1 13 | github.com/rs/zerolog v1.26.1 14 | github.com/shopspring/decimal v1.2.0 15 | github.com/swaggo/gin-swagger v1.3.3 16 | github.com/swaggo/swag v1.7.4 17 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 18 | github.com/urfave/cli v1.22.5 19 | google.golang.org/grpc v1.37.0 20 | ) 21 | 22 | require ( 23 | github.com/BurntSushi/toml v0.3.1 // indirect 24 | github.com/KyleBanks/depth v1.2.1 // indirect 25 | github.com/PuerkitoBio/purell v1.1.1 // indirect 26 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 27 | github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect 28 | github.com/aead/siphash v1.0.1 // indirect 29 | github.com/btcsuite/btcd v0.22.0-beta // indirect 30 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 31 | github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect 32 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect 33 | github.com/btcsuite/goleveldb v1.0.0 // indirect 34 | github.com/btcsuite/snappy-go v1.0.0 // indirect 35 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect 36 | github.com/btcsuite/winsvc v1.0.0 // indirect 37 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 38 | github.com/davecgh/go-spew v1.1.1 // indirect 39 | github.com/deckarep/golang-set v1.7.1 // indirect 40 | github.com/decred/dcrd/lru v1.0.0 // indirect 41 | github.com/gin-contrib/sse v0.1.0 // indirect 42 | github.com/go-ole/go-ole v1.2.1 // indirect 43 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 44 | github.com/go-openapi/jsonreference v0.19.5 // indirect 45 | github.com/go-openapi/spec v0.20.3 // indirect 46 | github.com/go-openapi/swag v0.19.14 // indirect 47 | github.com/go-playground/locales v0.14.0 // indirect 48 | github.com/go-stack/stack v1.8.0 // indirect 49 | github.com/golang/snappy v0.0.4 // indirect 50 | github.com/google/uuid v1.1.5 // indirect 51 | github.com/gorilla/websocket v1.4.2 // indirect 52 | github.com/jessevdk/go-flags v1.4.0 // indirect 53 | github.com/josharian/intern v1.0.0 // indirect 54 | github.com/jrick/logrotate v1.0.0 // indirect 55 | github.com/json-iterator/go v1.1.9 // indirect 56 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect 57 | github.com/leodido/go-urn v1.2.1 // indirect 58 | github.com/mailru/easyjson v0.7.6 // indirect 59 | github.com/mattn/go-isatty v0.0.12 // indirect 60 | github.com/mitchellh/go-homedir v1.1.0 // indirect 61 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 62 | github.com/modern-go/reflect2 v1.0.1 // indirect 63 | github.com/pborman/uuid v1.2.1 // indirect 64 | github.com/pkg/errors v0.9.1 // indirect 65 | github.com/rjeczalik/notify v0.9.2 // indirect 66 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 67 | github.com/shengdoushi/base58 v1.0.0 // indirect 68 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect 69 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 70 | github.com/tklauser/go-sysconf v0.3.5 // indirect 71 | github.com/tklauser/numcpus v0.2.2 // indirect 72 | github.com/tyler-smith/go-bip39 v1.0.2 // indirect 73 | github.com/ugorji/go/codec v1.1.7 // indirect 74 | go.uber.org/atomic v1.6.0 // indirect 75 | go.uber.org/multierr v1.5.0 // indirect 76 | go.uber.org/zap v1.15.0 // indirect 77 | golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e // indirect 78 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect 79 | golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c // indirect 80 | golang.org/x/text v0.3.6 // indirect 81 | golang.org/x/tools v0.1.7 // indirect 82 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect 83 | google.golang.org/protobuf v1.25.0 // indirect 84 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect 85 | gopkg.in/yaml.v2 v2.4.0 // indirect 86 | ) 87 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/lmxdawn/wallet/cmd" 5 | ) 6 | 7 | // 是否加载文档 8 | var isSwag bool 9 | 10 | // @title Swagger Example API 11 | // @version 1.0 12 | // @description This is a sample server Petstore server. 13 | // @termsOfService http://swagger.io/terms/ 14 | 15 | // @contact.name API Support 16 | // @contact.url http://www.swagger.io/support 17 | // @contact.email support@swagger.io 18 | 19 | // @license.name Apache 2.0 20 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 21 | 22 | // @securityDefinitions.apikey ApiKeyAuth 23 | // @in header 24 | // @name x-token 25 | func main() { 26 | 27 | cmd.Run(isSwag) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /scheduler/queue.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "github.com/lmxdawn/wallet/db" 5 | "github.com/lmxdawn/wallet/types" 6 | ) 7 | 8 | // QueueScheduler 队列的调度器 9 | type QueueScheduler struct { 10 | blockNum chan uint64 // 区块的通道 11 | blockNumWorker chan chan uint64 // 区块worker的通道 12 | receipt chan types.Transaction 13 | receiptWorker chan chan types.Transaction 14 | collectionSend chan db.WalletItem 15 | collectionSendWorker chan chan db.WalletItem 16 | } 17 | 18 | func NewQueueScheduler() *QueueScheduler { 19 | return &QueueScheduler{} 20 | } 21 | 22 | func (q *QueueScheduler) BlockWorkerChan() chan uint64 { 23 | return make(chan uint64) 24 | } 25 | 26 | func (q *QueueScheduler) BlockWorkerReady(w chan uint64) { 27 | q.blockNumWorker <- w 28 | } 29 | 30 | func (q *QueueScheduler) BlockSubmit(blockNum uint64) { 31 | q.blockNum <- blockNum 32 | } 33 | 34 | func (q *QueueScheduler) BlockRun() { 35 | q.blockNum = make(chan uint64) 36 | q.blockNumWorker = make(chan chan uint64) 37 | go func() { 38 | var nQ []uint64 39 | var nWQ []chan uint64 40 | for { 41 | var activateN uint64 42 | var activateNW chan uint64 43 | if len(nQ) > 0 && len(nWQ) > 0 { 44 | activateN = nQ[0] 45 | activateNW = nWQ[0] 46 | } 47 | select { 48 | case n := <-q.blockNum: 49 | nQ = append(nQ, n) 50 | case nw := <-q.blockNumWorker: 51 | nWQ = append(nWQ, nw) 52 | case activateNW <- activateN: 53 | nQ = nQ[1:] 54 | nWQ = nWQ[1:] 55 | } 56 | } 57 | 58 | }() 59 | } 60 | 61 | func (q *QueueScheduler) ReceiptWorkerChan() chan types.Transaction { 62 | return make(chan types.Transaction) 63 | } 64 | 65 | func (q *QueueScheduler) ReceiptWorkerReady(t chan types.Transaction) { 66 | q.receiptWorker <- t 67 | } 68 | 69 | func (q *QueueScheduler) ReceiptSubmit(transaction types.Transaction) { 70 | q.receipt <- transaction 71 | } 72 | 73 | func (q *QueueScheduler) ReceiptRun() { 74 | q.receipt = make(chan types.Transaction) 75 | q.receiptWorker = make(chan chan types.Transaction) 76 | go func() { 77 | var rQ []types.Transaction 78 | var rWQ []chan types.Transaction 79 | for { 80 | var activateR types.Transaction 81 | var activateRW chan types.Transaction 82 | if len(rQ) > 0 && len(rWQ) > 0 { 83 | activateR = rQ[0] 84 | activateRW = rWQ[0] 85 | } 86 | select { 87 | case r := <-q.receipt: 88 | rQ = append(rQ, r) 89 | case rw := <-q.receiptWorker: 90 | rWQ = append(rWQ, rw) 91 | case activateRW <- activateR: 92 | rQ = rQ[1:] 93 | rWQ = rWQ[1:] 94 | } 95 | } 96 | 97 | }() 98 | } 99 | 100 | func (q *QueueScheduler) CollectionSendWorkerChan() chan db.WalletItem { 101 | return make(chan db.WalletItem) 102 | } 103 | 104 | func (q *QueueScheduler) CollectionSendWorkerReady(c chan db.WalletItem) { 105 | q.collectionSendWorker <- c 106 | } 107 | 108 | func (q *QueueScheduler) CollectionSendSubmit(c db.WalletItem) { 109 | q.collectionSend <- c 110 | } 111 | 112 | func (q *QueueScheduler) CollectionSendRun() { 113 | q.collectionSend = make(chan db.WalletItem) 114 | q.collectionSendWorker = make(chan chan db.WalletItem) 115 | go func() { 116 | var cQ []db.WalletItem 117 | var cWQ []chan db.WalletItem 118 | for { 119 | var activateR db.WalletItem 120 | var activateRW chan db.WalletItem 121 | if len(cQ) > 0 && len(cWQ) > 0 { 122 | activateR = cQ[0] 123 | activateRW = cWQ[0] 124 | } 125 | select { 126 | case c := <-q.collectionSend: 127 | cQ = append(cQ, c) 128 | case cw := <-q.collectionSendWorker: 129 | cWQ = append(cWQ, cw) 130 | case activateRW <- activateR: 131 | cQ = cQ[1:] 132 | cWQ = cWQ[1:] 133 | } 134 | } 135 | 136 | }() 137 | } 138 | -------------------------------------------------------------------------------- /scheduler/simple.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "github.com/lmxdawn/wallet/db" 5 | "github.com/lmxdawn/wallet/types" 6 | ) 7 | 8 | // SimpleScheduler 简单的调度器 9 | type SimpleScheduler struct { 10 | blockNum chan uint64 // 区块的通道 11 | receipt chan types.Transaction // 交易的通道 12 | collectionSend chan db.WalletItem 13 | } 14 | 15 | func NewSimpleScheduler() *SimpleScheduler { 16 | return &SimpleScheduler{} 17 | } 18 | 19 | func (s *SimpleScheduler) BlockWorkerChan() chan uint64 { 20 | return s.blockNum 21 | } 22 | 23 | func (s *SimpleScheduler) BlockWorkerReady(chan uint64) { 24 | } 25 | 26 | func (s *SimpleScheduler) BlockSubmit(n uint64) { 27 | go func() { 28 | s.blockNum <- n 29 | }() 30 | } 31 | 32 | func (s *SimpleScheduler) BlockRun() { 33 | s.blockNum = make(chan uint64) 34 | } 35 | 36 | func (s *SimpleScheduler) ReceiptWorkerChan() chan types.Transaction { 37 | return s.receipt 38 | } 39 | 40 | func (s *SimpleScheduler) ReceiptWorkerReady(chan types.Transaction) { 41 | } 42 | 43 | func (s *SimpleScheduler) ReceiptSubmit(t types.Transaction) { 44 | go func() { 45 | s.receipt <- t 46 | }() 47 | } 48 | 49 | func (s *SimpleScheduler) ReceiptRun() { 50 | s.receipt = make(chan types.Transaction) 51 | } 52 | 53 | func (s *SimpleScheduler) CollectionSendWorkerChan() chan db.WalletItem { 54 | return s.collectionSend 55 | } 56 | 57 | func (s *SimpleScheduler) CollectionSendWorkerReady(chan db.WalletItem) { 58 | } 59 | 60 | func (s *SimpleScheduler) CollectionSendSubmit(c db.WalletItem) { 61 | go func() { 62 | s.collectionSend <- c 63 | }() 64 | } 65 | 66 | func (s *SimpleScheduler) CollectionSendRun() { 67 | s.collectionSend = make(chan db.WalletItem) 68 | } 69 | -------------------------------------------------------------------------------- /script/api.md: -------------------------------------------------------------------------------- 1 | # Swagger Example API 2 | [toc] 3 | ## 1 环境变量 4 | 5 | ### 默认环境1 6 | | 参数名 | 字段值 | 7 | | ------ | ------ | 8 | 9 | 10 | 11 | ## 2 Swagger Example API 12 | 13 | ##### 说明 14 | > 15 | 16 | 17 | 18 | ##### 联系方式 19 | - **联系人:**API Support 20 | - **邮箱:**support@swagger.io 21 | - **网址:**/http://www.swagger.io/support/ 22 | 23 | ##### 文档版本 24 | ``` 25 | 1.0 26 | ``` 27 | 28 | 29 | ## 3 归集 30 | 31 | > POST /api/collection 32 | ### 请求体(Request Body) 33 | | 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 | 34 | | ------ | ------ | ------ | ------ | ------ | 35 | | address|string||false|地址| 36 | | coinName|string||false|币种名称| 37 | | max|string||false|最大归集数量(满足当前值才会归集)| 38 | | protocol|string||false|协议| 39 | ### 响应体 40 | ● 200 响应数据格式:JSON 41 | | 参数名称 | 类型 | 默认值 | 不为空 | 描述 | 42 | | ------ | ------ | ------ | ------ | ------ | 43 | | data|object||false|| 44 | |⇥ balance|string||false|实际归集的数量| 45 | | server.Response|object||false|| 46 | |⇥ code|int32||false|错误code码| 47 | |⇥ data|object||false|成功时返回的对象| 48 | |⇥ message|string||false|错误信息| 49 | 50 | ##### 接口描述 51 | > 52 | 53 | 54 | 55 | 56 | ## 4 创建钱包地址 57 | 58 | > POST /api/createWallet 59 | ### 请求体(Request Body) 60 | | 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 | 61 | | ------ | ------ | ------ | ------ | ------ | 62 | | coinName|string||false|币种名称| 63 | | protocol|string||false|协议| 64 | ### 响应体 65 | ● 200 响应数据格式:JSON 66 | | 参数名称 | 类型 | 默认值 | 不为空 | 描述 | 67 | | ------ | ------ | ------ | ------ | ------ | 68 | | data|object||false|| 69 | |⇥ address|string||false|生成的钱包地址| 70 | | server.Response|object||false|| 71 | |⇥ code|int32||false|错误code码| 72 | |⇥ data|object||false|成功时返回的对象| 73 | |⇥ message|string||false|错误信息| 74 | 75 | ##### 接口描述 76 | > 77 | 78 | 79 | 80 | 81 | ## 5 删除钱包地址 82 | 83 | > POST /api/delWallet 84 | ### 请求体(Request Body) 85 | | 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 | 86 | | ------ | ------ | ------ | ------ | ------ | 87 | | address|string||false|地址| 88 | | coinName|string||false|币种名称| 89 | | protocol|string||false|协议| 90 | ### 响应体 91 | ● 200 响应数据格式:JSON 92 | | 参数名称 | 类型 | 默认值 | 不为空 | 描述 | 93 | | ------ | ------ | ------ | ------ | ------ | 94 | | code|int32||false|错误code码| 95 | | data|object||false|成功时返回的对象| 96 | | message|string||false|错误信息| 97 | 98 | ##### 接口描述 99 | > 100 | 101 | 102 | 103 | 104 | ## 6 获取交易结果 105 | 106 | > GET /api/getTransactionReceipt 107 | ### 请求体(Request Body) 108 | | 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 | 109 | | ------ | ------ | ------ | ------ | ------ | 110 | | coinName|string||false|币种名称| 111 | | hash|string||false|交易哈希| 112 | | protocol|string||false|协议| 113 | ### 响应体 114 | ● 200 响应数据格式:JSON 115 | | 参数名称 | 类型 | 默认值 | 不为空 | 描述 | 116 | | ------ | ------ | ------ | ------ | ------ | 117 | | data|object||false|| 118 | |⇥ status|int32||false|交易状态(0:未成功,1:已成功)| 119 | | server.Response|object||false|| 120 | |⇥ code|int32||false|错误code码| 121 | |⇥ data|object||false|成功时返回的对象| 122 | |⇥ message|string||false|错误信息| 123 | 124 | ##### 接口描述 125 | > 126 | 127 | 128 | 129 | 130 | ## 7 提现 131 | 132 | > POST /api/withdraw 133 | ### 请求体(Request Body) 134 | | 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 | 135 | | ------ | ------ | ------ | ------ | ------ | 136 | | address|string||false|提现地址| 137 | | coinName|string||false|币种名称| 138 | | orderId|string||false|订单号| 139 | | protocol|string||false|协议| 140 | | value|int32||false|金额| 141 | ### 响应体 142 | ● 200 响应数据格式:JSON 143 | | 参数名称 | 类型 | 默认值 | 不为空 | 描述 | 144 | | ------ | ------ | ------ | ------ | ------ | 145 | | data|object||false|| 146 | |⇥ hash|string||false|生成的交易hash| 147 | | server.Response|object||false|| 148 | |⇥ code|int32||false|错误code码| 149 | |⇥ data|object||false|成功时返回的对象| 150 | |⇥ message|string||false|错误信息| 151 | 152 | ##### 接口描述 153 | > 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /server/err_code.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "fmt" 4 | 5 | //nolint: golint 6 | var ( 7 | OK = &Errno{Code: 0, Message: "OK"} 8 | InternalServerError = &Errno{Code: 10001, Message: "Internal server error"} 9 | ErrToken = &Errno{Code: 10002, Message: "token错误"} 10 | ErrParam = &Errno{Code: 10003, Message: "参数有误"} 11 | ErrNotData = &Errno{Code: 10004, Message: "没有数据"} 12 | ErrNotChangeData = &Errno{Code: 10005, Message: "数据没有更改"} 13 | ErrNotRepeatData = &Errno{Code: 10006, Message: "数据已存在"} 14 | ErrEngine = &Errno{Code: 10007, Message: "Engine Not"} 15 | ErrCreateWallet = &Errno{Code: 10008, Message: "创建钱包失败"} 16 | ) 17 | 18 | // Errno ... 19 | type Errno struct { 20 | Code int 21 | Message string 22 | } 23 | 24 | func (err Errno) Error() string { 25 | return err.Message 26 | } 27 | 28 | // Err represents an error 29 | type Err struct { 30 | Code int 31 | Message string 32 | Err error 33 | } 34 | 35 | func (err *Err) Error() string { 36 | return fmt.Sprintf("Err - code: %d, message: %s, error: %s", err.Code, err.Message, err.Err) 37 | } 38 | 39 | // DecodeErr ... 40 | func DecodeErr(err error) (int, string) { 41 | if err == nil { 42 | return OK.Code, OK.Message 43 | } 44 | 45 | switch typed := err.(type) { 46 | case *Err: 47 | return typed.Code, typed.Message 48 | case *Errno: 49 | return typed.Code, typed.Message 50 | default: 51 | } 52 | 53 | return InternalServerError.Code, err.Error() 54 | } 55 | -------------------------------------------------------------------------------- /server/handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/gin-gonic/gin" 6 | "github.com/lmxdawn/wallet/engine" 7 | "math/big" 8 | ) 9 | 10 | // CreateWallet ... 11 | // @Tags 钱包 12 | // @Summary 创建钱包地址 13 | // @Produce json 14 | // @Security ApiKeyAuth 15 | // @Param login body CreateWalletReq true "参数" 16 | // @Success 200 {object} Response{data=server.CreateWalletRes} 17 | // @Router /api/createWallet [post] 18 | func CreateWallet(c *gin.Context) { 19 | 20 | var q CreateWalletReq 21 | 22 | if err := c.ShouldBindJSON(&q); err != nil { 23 | HandleValidatorError(c, err) 24 | return 25 | } 26 | 27 | v, ok := c.Get(q.Protocol + q.CoinName) 28 | if !ok { 29 | APIResponse(c, ErrEngine, nil) 30 | return 31 | } 32 | 33 | currentEngine := v.(*engine.ConCurrentEngine) 34 | 35 | // 创建钱包 36 | address, err := currentEngine.CreateWallet() 37 | if err != nil { 38 | APIResponse(c, ErrCreateWallet, nil) 39 | return 40 | } 41 | 42 | res := CreateWalletRes{Address: address} 43 | 44 | APIResponse(c, nil, res) 45 | } 46 | 47 | // DelWallet ... 48 | // @Tags 钱包 49 | // @Summary 删除钱包地址 50 | // @Produce json 51 | // @Security ApiKeyAuth 52 | // @Param login body DelWalletReq true "参数" 53 | // @Success 200 {object} Response 54 | // @Router /api/delWallet [post] 55 | func DelWallet(c *gin.Context) { 56 | 57 | var q DelWalletReq 58 | 59 | if err := c.ShouldBindJSON(&q); err != nil { 60 | HandleValidatorError(c, err) 61 | return 62 | } 63 | 64 | v, ok := c.Get(q.Protocol + q.CoinName) 65 | if !ok { 66 | APIResponse(c, ErrEngine, nil) 67 | return 68 | } 69 | 70 | currentEngine := v.(*engine.ConCurrentEngine) 71 | 72 | q.Address = common.HexToAddress(q.Address).Hex() 73 | 74 | err := currentEngine.DeleteWallet(q.Address) 75 | if err != nil { 76 | APIResponse(c, ErrCreateWallet, nil) 77 | return 78 | } 79 | 80 | APIResponse(c, nil, nil) 81 | } 82 | 83 | // Withdraw ... 84 | // @Tags 钱包 85 | // @Summary 提现 86 | // @Produce json 87 | // @Security ApiKeyAuth 88 | // @Param login body WithdrawReq true "参数" 89 | // @Success 200 {object} Response{data=server.WithdrawRes} 90 | // @Router /api/withdraw [post] 91 | func Withdraw(c *gin.Context) { 92 | 93 | var q WithdrawReq 94 | 95 | if err := c.ShouldBindJSON(&q); err != nil { 96 | HandleValidatorError(c, err) 97 | return 98 | } 99 | 100 | v, ok := c.Get(q.Protocol + q.CoinName) 101 | if !ok { 102 | APIResponse(c, ErrEngine, nil) 103 | return 104 | } 105 | 106 | currentEngine := v.(*engine.ConCurrentEngine) 107 | 108 | q.Address = common.HexToAddress(q.Address).Hex() 109 | 110 | hash, err := currentEngine.Withdraw(q.OrderId, q.Address, q.Value) 111 | if err != nil { 112 | APIResponse(c, err, nil) 113 | return 114 | } 115 | 116 | res := WithdrawRes{Hash: hash} 117 | 118 | APIResponse(c, nil, res) 119 | } 120 | 121 | // Collection ... 122 | // @Tags 归集某个地址 123 | // @Summary 归集 124 | // @Produce json 125 | // @Security ApiKeyAuth 126 | // @Param login body CollectionReq true "参数" 127 | // @Success 200 {object} Response{data=server.CollectionRes} 128 | // @Router /api/collection [post] 129 | func Collection(c *gin.Context) { 130 | 131 | var q CollectionReq 132 | 133 | if err := c.ShouldBindJSON(&q); err != nil { 134 | HandleValidatorError(c, err) 135 | return 136 | } 137 | 138 | v, ok := c.Get(q.Protocol + q.CoinName) 139 | if !ok { 140 | APIResponse(c, ErrEngine, nil) 141 | return 142 | } 143 | 144 | currentEngine := v.(*engine.ConCurrentEngine) 145 | 146 | n := new(big.Int) 147 | max, ok := n.SetString(q.Max, 10) 148 | if !ok { 149 | APIResponse(c, InternalServerError, nil) 150 | return 151 | } 152 | 153 | q.Address = common.HexToAddress(q.Address).Hex() 154 | 155 | balance, err := currentEngine.Collection(q.Address, max) 156 | if err != nil { 157 | APIResponse(c, err, nil) 158 | return 159 | } 160 | 161 | res := CollectionRes{Balance: balance.String()} 162 | 163 | APIResponse(c, nil, res) 164 | } 165 | 166 | // GetTransactionReceipt ... 167 | // @Tags 钱包 168 | // @Summary 获取交易结果 169 | // @Produce json 170 | // @Security ApiKeyAuth 171 | // @Param login body TransactionReceiptReq true "参数" 172 | // @Success 200 {object} Response{data=server.TransactionReceiptRes} 173 | // @Router /api/getTransactionReceipt [get] 174 | func GetTransactionReceipt(c *gin.Context) { 175 | 176 | var q TransactionReceiptReq 177 | 178 | if err := c.ShouldBindJSON(&q); err != nil { 179 | HandleValidatorError(c, err) 180 | return 181 | } 182 | 183 | v, ok := c.Get(q.Protocol + q.CoinName) 184 | if !ok { 185 | APIResponse(c, ErrEngine, nil) 186 | return 187 | } 188 | 189 | currentEngine := v.(*engine.ConCurrentEngine) 190 | 191 | status, err := currentEngine.GetTransactionReceipt(q.Hash) 192 | if err != nil { 193 | APIResponse(c, InternalServerError, nil) 194 | return 195 | } 196 | 197 | res := TransactionReceiptRes{ 198 | Status: status, 199 | } 200 | 201 | APIResponse(c, nil, res) 202 | } 203 | -------------------------------------------------------------------------------- /server/middleware.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/lmxdawn/wallet/engine" 6 | ) 7 | 8 | // AuthRequired 认证中间件 9 | func AuthRequired() gin.HandlerFunc { 10 | 11 | return func(c *gin.Context) { 12 | 13 | token := c.GetHeader("x-token") 14 | if token == "" { 15 | c.Abort() 16 | APIResponse(c, ErrToken, nil) 17 | } 18 | 19 | } 20 | 21 | } 22 | 23 | // SetEngine 设置db数据库 24 | func SetEngine(engines ...*engine.ConCurrentEngine) gin.HandlerFunc { 25 | 26 | return func(c *gin.Context) { 27 | for _, currentEngine := range engines { 28 | c.Set(currentEngine.Protocol+currentEngine.CoinName, currentEngine) 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /server/req.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type CreateWalletReq struct { 4 | Protocol string `json:"protocol" binding:"required"` // 协议 5 | CoinName string `json:"coinName" binding:"required"` // 币种名称 6 | } 7 | 8 | type DelWalletReq struct { 9 | Protocol string `json:"protocol" binding:"required"` // 协议 10 | CoinName string `json:"coinName" binding:"required"` // 币种名称 11 | Address string `json:"address" binding:"required"` // 地址 12 | } 13 | 14 | type WithdrawReq struct { 15 | Protocol string `json:"protocol" binding:"required"` // 协议 16 | CoinName string `json:"coinName" binding:"required"` // 币种名称 17 | OrderId string `json:"orderId" binding:"required"` // 订单号 18 | Address string `json:"address" binding:"required"` // 提现地址 19 | Value int64 `json:"value" binding:"required"` // 金额 20 | } 21 | 22 | type CollectionReq struct { 23 | Protocol string `json:"protocol" binding:"required"` // 协议 24 | CoinName string `json:"coinName" binding:"required"` // 币种名称 25 | Address string `json:"address" binding:"required"` // 地址 26 | Max string `json:"max" binding:"required"` // 最大归集数量(满足当前值才会归集) 27 | } 28 | 29 | type TransactionReceiptReq struct { 30 | Protocol string `json:"protocol" binding:"required"` // 协议 31 | CoinName string `json:"coinName" binding:"required"` // 币种名称 32 | Hash string `json:"hash" binding:"required"` // 交易哈希 33 | } 34 | -------------------------------------------------------------------------------- /server/res.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | // Response ... 9 | type Response struct { 10 | Code int `json:"code"` // 错误code码 11 | Message string `json:"message"` // 错误信息 12 | Data interface{} `json:"data"` // 成功时返回的对象 13 | } 14 | 15 | // APIResponse .... 16 | func APIResponse(Ctx *gin.Context, err error, data interface{}) { 17 | if err == nil { 18 | err = OK 19 | } 20 | codeNum, message := DecodeErr(err) 21 | Ctx.JSON(http.StatusOK, Response{ 22 | Code: codeNum, 23 | Message: message, 24 | Data: data, 25 | }) 26 | } 27 | 28 | // CreateWalletRes ... 29 | type CreateWalletRes struct { 30 | Address string `json:"address"` // 生成的钱包地址 31 | } 32 | 33 | // WithdrawRes ... 34 | type WithdrawRes struct { 35 | Hash string `json:"hash"` // 生成的交易hash 36 | } 37 | 38 | // CollectionRes ... 39 | type CollectionRes struct { 40 | Balance string `json:"balance"` // 实际归集的数量 41 | } 42 | 43 | // TransactionReceiptRes ... 44 | type TransactionReceiptRes struct { 45 | Status int `json:"status"` // 交易状态(0:未成功,1:已成功) 46 | } 47 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/lmxdawn/wallet/config" 7 | "github.com/lmxdawn/wallet/engine" 8 | "github.com/rs/zerolog/log" 9 | ginSwagger "github.com/swaggo/gin-swagger" 10 | "github.com/swaggo/gin-swagger/swaggerFiles" 11 | ) 12 | 13 | // Start 启动服务 14 | func Start(isSwag bool, configPath string) { 15 | 16 | conf, err := config.NewConfig(configPath) 17 | 18 | if err != nil || len(conf.Engines) == 0 { 19 | panic("Failed to load configuration") 20 | } 21 | 22 | var engines []*engine.ConCurrentEngine 23 | for _, engineConfig := range conf.Engines { 24 | eth, err := engine.NewEngine(engineConfig) 25 | if err != nil { 26 | panic(fmt.Sprintf("eth run err:%v", err)) 27 | } 28 | engines = append(engines, eth) 29 | } 30 | 31 | // 启动监听器 32 | for _, currentEngine := range engines { 33 | go currentEngine.Run() 34 | } 35 | 36 | if isSwag { 37 | gin.SetMode(gin.DebugMode) 38 | } else { 39 | gin.SetMode(gin.ReleaseMode) 40 | } 41 | server := gin.Default() 42 | 43 | // 中间件 44 | server.Use(gin.Logger()) 45 | server.Use(gin.Recovery()) 46 | server.Use(SetEngine(engines...)) 47 | 48 | auth := server.Group("/api", AuthRequired()) 49 | { 50 | auth.POST("/createWallet", CreateWallet) 51 | auth.POST("/delWallet", DelWallet) 52 | auth.POST("/withdraw", Withdraw) 53 | auth.POST("/collection", Collection) 54 | auth.GET("/getTransactionReceipt", GetTransactionReceipt) 55 | } 56 | 57 | if isSwag { 58 | swagHandler := ginSwagger.WrapHandler(swaggerFiles.Handler) 59 | server.GET("/swagger/*any", swagHandler) 60 | } 61 | 62 | err = server.Run(fmt.Sprintf(":%v", conf.App.Port)) 63 | if err != nil { 64 | panic("start error") 65 | } 66 | 67 | log.Info().Msgf("start success") 68 | 69 | } 70 | -------------------------------------------------------------------------------- /server/validator.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ut "github.com/go-playground/universal-translator" 6 | "github.com/go-playground/validator/v10" 7 | ) 8 | 9 | // 定义一个全局翻译器T 10 | var trans ut.Translator 11 | 12 | // HandleValidatorError 处理字段校验异常 13 | func HandleValidatorError(c *gin.Context, err error) { 14 | //如何返回错误信息 15 | errs, ok := err.(validator.ValidationErrors) 16 | if !ok { 17 | APIResponse(c, InternalServerError, nil) 18 | } 19 | APIResponse(c, ErrParam, firstErr(errs.Translate(trans))) 20 | return 21 | } 22 | 23 | // firstErr 返回第一个错误 24 | func firstErr(filedMap map[string]string) string { 25 | for _, err := range filedMap { 26 | return err 27 | } 28 | return "" 29 | } 30 | -------------------------------------------------------------------------------- /swag.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmxdawn/wallet/92dbf952b6ca6abb85f47adf9b86a9ece15097a5/swag.exe -------------------------------------------------------------------------------- /tron/client.go: -------------------------------------------------------------------------------- 1 | package tron 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/fbsobreira/gotron-sdk/pkg/client" 8 | "github.com/fbsobreira/gotron-sdk/pkg/proto/api" 9 | "github.com/fbsobreira/gotron-sdk/pkg/proto/core" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type Client struct { 17 | node string 18 | rpc *client.GrpcClient 19 | } 20 | 21 | func NewClient(node string, withTLS bool) (*Client, error) { 22 | opts := make([]grpc.DialOption, 0) 23 | if withTLS { 24 | opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(nil))) 25 | } else { 26 | opts = append(opts, grpc.WithInsecure()) 27 | } 28 | c := new(Client) 29 | c.node = node 30 | c.rpc = client.NewGrpcClient(node) 31 | err := c.rpc.Start(opts...) 32 | if err != nil { 33 | return nil, fmt.Errorf("grpc client start error: %v", err) 34 | } 35 | return c, nil 36 | } 37 | 38 | // SetTimeout 设置超时时间 39 | func (c *Client) SetTimeout(timeout time.Duration) error { 40 | if c == nil { 41 | return errors.New("client is nil ptr") 42 | } 43 | c.rpc = client.NewGrpcClientWithTimeout(c.node, timeout) 44 | err := c.rpc.Start() 45 | if err != nil { 46 | return fmt.Errorf("grpc start error: %v", err) 47 | } 48 | return nil 49 | } 50 | 51 | // keepConnect 52 | func (c *Client) keepConnect() error { 53 | _, err := c.rpc.GetNodeInfo() 54 | if err != nil { 55 | if strings.Contains(err.Error(), "no such host") { 56 | return c.rpc.Reconnect(c.node) 57 | } 58 | return fmt.Errorf("node connect error: %v", err) 59 | } 60 | return nil 61 | } 62 | 63 | // Transfer 转账 64 | func (c *Client) Transfer(from, to string, amount int64) (*api.TransactionExtention, error) { 65 | err := c.keepConnect() 66 | if err != nil { 67 | return nil, err 68 | } 69 | return c.rpc.Transfer(from, to, amount) 70 | } 71 | 72 | // TransferTrc10 trc10 转账 73 | func (c *Client) TransferTrc10(from, to, assetName string, amount int64) (*api.TransactionExtention, error) { 74 | err := c.keepConnect() 75 | if err != nil { 76 | return nil, err 77 | } 78 | return c.rpc.TransferAsset(from, to, assetName, amount) 79 | } 80 | 81 | // TriggerContract 执行智能合约的方法 82 | func (c *Client) TriggerContract(from, contractAddress, method, jsonString string, constant bool, feeLimit int64) (*api.TransactionExtention, error) { 83 | 84 | err := c.keepConnect() 85 | if err != nil { 86 | return nil, err 87 | } 88 | result := &api.TransactionExtention{} 89 | if constant { 90 | result, err = c.rpc.TriggerConstantContract(from, contractAddress, method, jsonString) 91 | if err != nil { 92 | return nil, err 93 | } 94 | } else { 95 | result, err = c.rpc.TriggerContract(from, contractAddress, method, jsonString, feeLimit, 0, "", 0) 96 | if err != nil { 97 | return nil, err 98 | } 99 | } 100 | return result, nil 101 | } 102 | 103 | // BroadcastTransaction 广播交易签名 104 | func (c *Client) BroadcastTransaction(transaction *core.Transaction) error { 105 | err := c.keepConnect() 106 | if err != nil { 107 | return err 108 | } 109 | result, err := c.rpc.Broadcast(transaction) 110 | if err != nil { 111 | return fmt.Errorf("broadcast transaction error: %v", err) 112 | } 113 | if result.Code != 0 { 114 | return fmt.Errorf("bad transaction: %v", string(result.GetMessage())) 115 | } 116 | if result.Result == true { 117 | return nil 118 | } 119 | d, _ := json.Marshal(result) 120 | return fmt.Errorf("tx send fail: %s", string(d)) 121 | } 122 | 123 | // GetTransactionInfoByID 查询交易是否成功 124 | func (c *Client) GetTransactionInfoByID(txid string, isRes bool) (*core.TransactionInfo, error) { 125 | err := c.keepConnect() 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | if !isRes { 131 | return c.rpc.GetTransactionInfoByID(txid) 132 | } 133 | 134 | count := 0 135 | for { 136 | if count >= 100 { 137 | return nil, errors.New("获取超时") 138 | } 139 | r, err := c.rpc.GetTransactionInfoByID(txid) 140 | if err != nil { 141 | count++ 142 | <-time.After(3 * time.Second) 143 | } else { 144 | //dd, _ := json.Marshal(r) 145 | return r, nil 146 | } 147 | } 148 | } 149 | 150 | // GetBlock 获取区块信息 151 | func (c *Client) GetBlock(start, end int64) (*api.BlockListExtention, error) { 152 | err := c.keepConnect() 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | blocks, err := c.rpc.GetBlockByLimitNext(start, end) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | return blocks, nil 163 | } 164 | 165 | // GetBlockByNum 获取区块信息 166 | func (c *Client) GetBlockByNum(num int64) (*api.BlockExtention, error) { 167 | err := c.keepConnect() 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | block, err := c.rpc.GetBlockByNum(num) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | return block, nil 178 | } 179 | 180 | // GetNowBlock 获取最新区块 181 | func (c *Client) GetNowBlock() (*api.BlockExtention, error) { 182 | err := c.keepConnect() 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | block, err := c.rpc.GetNowBlock() 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | return block, nil 193 | } 194 | 195 | // GetAccount 获取账户余额 196 | func (c *Client) GetAccount(address string) (*core.Account, error) { 197 | err := c.keepConnect() 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | return c.rpc.GetAccount(address) 203 | } 204 | -------------------------------------------------------------------------------- /tron/common.go: -------------------------------------------------------------------------------- 1 | package tron 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "github.com/ethereum/go-ethereum/crypto" 10 | "github.com/fbsobreira/gotron-sdk/pkg/address" 11 | "github.com/fbsobreira/gotron-sdk/pkg/common" 12 | "math/big" 13 | "strings" 14 | "unicode/utf8" 15 | ) 16 | 17 | func Private2TronAddress(privateKeyStr string) (string, error) { 18 | 19 | privateKey, err := crypto.HexToECDSA(privateKeyStr) 20 | if err != nil { 21 | return "", err 22 | } 23 | 24 | publicKey := privateKey.Public() 25 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 26 | if !ok { 27 | return "", errors.New("cannot assert type: publicKey is not of type *ecdsa.PublicKey") 28 | } 29 | 30 | fromAddress := address.PubkeyToAddress(*publicKeyECDSA) 31 | 32 | return fromAddress.String(), nil 33 | } 34 | 35 | func EthAddress2TronAddress(address string) (string, error) { 36 | ownerByte, err := common.Hex2Bytes(strings.Replace(address, "0x", "41", 1)) 37 | if err != nil { 38 | return "", err 39 | } 40 | owner := common.EncodeCheck(ownerByte) 41 | return owner, nil 42 | } 43 | 44 | func ToString(data string) (string, error) { 45 | if common.Has0xPrefix(data) { 46 | data = data[2:] 47 | } 48 | if len(data) > 128 { 49 | n, _ := ToNumber(data[64:128]) 50 | if n != nil { 51 | l := n.Uint64() 52 | if 2*int(l) <= len(data)-128 { 53 | b, err := hex.DecodeString(data[128 : 128+2*l]) 54 | if err == nil { 55 | return string(b), nil 56 | } 57 | } 58 | } 59 | } else if len(data) == 64 { 60 | // allow string properties as 32 bytes of UTF-8 data 61 | b, err := hex.DecodeString(data) 62 | if err == nil { 63 | i := bytes.Index(b, []byte{0}) 64 | if i > 0 { 65 | b = b[:i] 66 | } 67 | if utf8.Valid(b) { 68 | return string(b), nil 69 | } 70 | } 71 | } 72 | return "", fmt.Errorf("cannot parse %s", data) 73 | } 74 | 75 | func ToNumber(data string) (*big.Int, error) { 76 | if common.Has0xPrefix(data) { 77 | data = data[2:] 78 | } 79 | if len(data) == 64 { 80 | var n big.Int 81 | _, ok := n.SetString(data, 16) 82 | if ok { 83 | return &n, nil 84 | } 85 | } 86 | return nil, fmt.Errorf("cannot parse %s", data) 87 | } 88 | -------------------------------------------------------------------------------- /tron/event_res.go: -------------------------------------------------------------------------------- 1 | package tron 2 | 3 | // EventResult 事件 4 | type EventResult struct { 5 | Success bool `json:"success"` 6 | Data []struct { 7 | BlockNumber int `json:"block_number"` 8 | BlockTimestamp int64 `json:"block_timestamp"` 9 | CallerContractAddress string `json:"caller_contract_address"` 10 | ContractAddress string `json:"contract_address"` 11 | EventIndex int `json:"event_index"` 12 | EventName string `json:"event_name"` 13 | Result map[string]string `json:"result"` 14 | ResultType map[string]string `json:"result_type"` 15 | Event string `json:"event"` 16 | TransactionID string `json:"transaction_id"` 17 | } `json:"data"` 18 | Meta struct { 19 | At int64 `json:"at"` 20 | Fingerprint string `json:"fingerprint"` 21 | PageSize int `json:"page_size"` 22 | } `json:"meta"` 23 | } 24 | -------------------------------------------------------------------------------- /tron/event_server.go: -------------------------------------------------------------------------------- 1 | package tron 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | ) 12 | 13 | type EventServer struct { 14 | url string 15 | client *http.Client 16 | } 17 | 18 | func NewEventServer(url string) *EventServer { 19 | return &EventServer{ 20 | url: url, 21 | client: &http.Client{ 22 | Timeout: time.Millisecond * time.Duration(10*1000), 23 | }, 24 | } 25 | } 26 | 27 | func (e *EventServer) GetContractsEventsParams(contractAddress string, params url.Values, res interface{}) error { 28 | 29 | path := fmt.Sprintf("/v1/contracts/%s/events", contractAddress) 30 | err := e.get(e.url+path, params, res) 31 | if err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | 37 | // get 请求 38 | func (e *EventServer) get(urlStr string, params url.Values, res interface{}) error { 39 | 40 | Url, err := url.Parse(urlStr) 41 | if err != nil { 42 | return err 43 | } 44 | //如果参数中有中文参数,这个方法会进行URLEncode 45 | Url.RawQuery = params.Encode() 46 | urlPath := Url.String() 47 | req, err := http.NewRequest(http.MethodGet, urlPath, nil) 48 | if err != nil { 49 | // handle error 50 | return err 51 | } 52 | resp, err := e.client.Do(req) 53 | if err != nil { 54 | // handle error 55 | return err 56 | } 57 | defer resp.Body.Close() 58 | 59 | body, err := ioutil.ReadAll(resp.Body) 60 | if err != nil { 61 | return err 62 | } 63 | err = json.Unmarshal(body, res) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // post 请求 72 | func (e *EventServer) post(urlStr string, data map[string]interface{}, res interface{}) error { 73 | bytesData, err := json.Marshal(data) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | req, err := http.NewRequest(http.MethodPost, urlStr, bytes.NewReader(bytesData)) 79 | if err != nil { 80 | // handle error 81 | return err 82 | } 83 | req.Header.Set("Content-Type", "application/json") 84 | resp, err := e.client.Do(req) 85 | if err != nil { 86 | // handle error 87 | return err 88 | } 89 | defer resp.Body.Close() 90 | 91 | body, err := ioutil.ReadAll(resp.Body) 92 | if err != nil { 93 | // handle error 94 | return err 95 | } 96 | 97 | err = json.Unmarshal(body, res) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /tron/sign.go: -------------------------------------------------------------------------------- 1 | package tron 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "fmt" 8 | "github.com/ethereum/go-ethereum/crypto" 9 | "github.com/fbsobreira/gotron-sdk/pkg/proto/core" 10 | "github.com/golang/protobuf/proto" 11 | ) 12 | 13 | func SignTransaction(transaction *core.Transaction, privateKey string) (*core.Transaction, error) { 14 | privateBytes, err := hex.DecodeString(privateKey) 15 | if err != nil { 16 | return nil, fmt.Errorf("hex decode private key error: %v", err) 17 | } 18 | priv := crypto.ToECDSAUnsafe(privateBytes) 19 | defer zeroKey(priv) 20 | rawData, err := proto.Marshal(transaction.GetRawData()) 21 | if err != nil { 22 | return nil, fmt.Errorf("proto marshal tx raw data error: %v", err) 23 | } 24 | h256h := sha256.New() 25 | h256h.Write(rawData) 26 | hash := h256h.Sum(nil) 27 | signature, err := crypto.Sign(hash, priv) 28 | if err != nil { 29 | return nil, fmt.Errorf("sign error: %v", err) 30 | } 31 | transaction.Signature = append(transaction.Signature, signature) 32 | return transaction, nil 33 | } 34 | 35 | // zeroKey zeroes a private key in memory. 36 | func zeroKey(k *ecdsa.PrivateKey) { 37 | b := k.D.Bits() 38 | for i := range b { 39 | b[i] = 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tron/web.go: -------------------------------------------------------------------------------- 1 | package tron 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "github.com/fbsobreira/gotron-sdk/pkg/common" 8 | "github.com/fbsobreira/gotron-sdk/pkg/proto/api" 9 | "github.com/fbsobreira/gotron-sdk/pkg/proto/core" 10 | "net/url" 11 | ) 12 | 13 | type TronWeb struct { 14 | privateKey string 15 | formAddress string 16 | client *Client 17 | eventServer *EventServer 18 | } 19 | 20 | func NewTronWeb(node, eventServerUrl, privateKey string, withTLS bool) (*TronWeb, error) { 21 | var formAddress string 22 | var err error 23 | if privateKey != "" { 24 | formAddress, err = Private2TronAddress(privateKey) 25 | if err != nil { 26 | return nil, err 27 | } 28 | } 29 | var eventServer *EventServer 30 | if eventServerUrl != "" { 31 | eventServer = NewEventServer(eventServerUrl) 32 | } 33 | client, err := NewClient(node, withTLS) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return &TronWeb{ 38 | privateKey: privateKey, 39 | formAddress: formAddress, 40 | client: client, 41 | eventServer: eventServer, 42 | }, nil 43 | 44 | } 45 | 46 | func (t *TronWeb) CallContract(contractAddress, method, jsonString string) (string, error) { 47 | 48 | result, err := t.client.TriggerContract("", contractAddress, method, jsonString, true, 0) 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | data := common.BytesToHexString(result.GetConstantResult()[0]) 54 | 55 | return data, nil 56 | 57 | } 58 | 59 | func (t *TronWeb) SendContract(privateKey, contractAddress, method, jsonString string) (string, error) { 60 | 61 | fmt.Println(t.formAddress, contractAddress, method, jsonString) 62 | tx, err := t.client.TriggerContract(t.formAddress, contractAddress, method, jsonString, false, 10000000000) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | txid := common.BytesToHexString(tx.Txid) 68 | 69 | signTx, err := SignTransaction(tx.Transaction, privateKey) 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | err = t.client.BroadcastTransaction(signTx) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | // 查询交易 80 | r, err := t.client.GetTransactionInfoByID(txid, true) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | if r.Receipt.Result != 1 { 86 | return "", errors.New(string(r.ResMessage)) 87 | } 88 | 89 | return common.BytesToHexString(r.ContractResult[0]), nil 90 | 91 | } 92 | 93 | // Transfer 转账/TRX/TRX10 94 | func (t *TronWeb) Transfer(privateKey, from, to, assetName string, amount int64) (string, error) { 95 | var tx *api.TransactionExtention 96 | var err error 97 | if assetName != "" { 98 | tx, err = t.client.TransferTrc10(from, to, assetName, amount) 99 | 100 | } else { 101 | tx, err = t.client.Transfer(from, to, amount) 102 | } 103 | if err != nil { 104 | return "", err 105 | } 106 | 107 | txid := common.BytesToHexString(tx.Txid) 108 | 109 | signTx, err := SignTransaction(tx.Transaction, privateKey) 110 | if err != nil { 111 | return "", err 112 | } 113 | 114 | err = t.client.BroadcastTransaction(signTx) 115 | if err != nil { 116 | return "", err 117 | } 118 | 119 | return txid, nil 120 | 121 | } 122 | 123 | // GetTransactionInfoByID 查询交易信息 124 | func (t *TronWeb) GetTransactionInfoByID(txid string, isRes bool) (*core.TransactionInfo, error) { 125 | 126 | return t.client.GetTransactionInfoByID(txid, isRes) 127 | 128 | } 129 | 130 | // GetBlockByNum 根据区块号获取区块信息 131 | func (t *TronWeb) GetBlockByNum(num int64) (*api.BlockExtention, error) { 132 | 133 | return t.client.GetBlockByNum(num) 134 | 135 | } 136 | 137 | // GetNowBlockNum 获取最新区块 138 | func (t *TronWeb) GetNowBlockNum() (int64, error) { 139 | 140 | block, err := t.client.GetNowBlock() 141 | if err != nil { 142 | return 0, err 143 | } 144 | return int64(binary.BigEndian.Uint64(block.Blockid)), nil 145 | 146 | } 147 | 148 | // GetBalance 获取账户TRX余额 149 | func (t *TronWeb) GetBalance(address string, token string) (int64, error) { 150 | 151 | account, err := t.client.GetAccount(address) 152 | 153 | if err != nil { 154 | return 0, err 155 | } 156 | 157 | if token != "" { 158 | if account.AssetV2 == nil { 159 | return 0, nil 160 | } 161 | return account.AssetV2[token], nil 162 | } 163 | 164 | return account.Balance, nil 165 | } 166 | 167 | // GetEventResult 获取事件的历史 168 | func (t *TronWeb) GetEventResult(contractAddress string, blockNumber int64, res interface{}) error { 169 | 170 | params := url.Values{} 171 | if blockNumber > 0 { 172 | params.Set("block_number", fmt.Sprintf("%d", blockNumber)) 173 | } 174 | err := t.eventServer.GetContractsEventsParams(contractAddress, params, &res) 175 | if err != nil { 176 | return err 177 | } 178 | return nil 179 | } 180 | 181 | // GetEventResultParams 获取事件的历史 182 | func (t *TronWeb) GetEventResultParams(contractAddress string, params url.Values, res interface{}) error { 183 | 184 | err := t.eventServer.GetContractsEventsParams(contractAddress, params, &res) 185 | if err != nil { 186 | return err 187 | } 188 | return nil 189 | } 190 | 191 | func (t *TronWeb) GetTransaction(start, end int64) (*api.BlockListExtention, error) { 192 | 193 | return t.client.GetBlock(start, end) 194 | 195 | } 196 | -------------------------------------------------------------------------------- /tron/web_test.go: -------------------------------------------------------------------------------- 1 | package tron 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fbsobreira/gotron-sdk/pkg/abi" 6 | "github.com/fbsobreira/gotron-sdk/pkg/common" 7 | "github.com/shopspring/decimal" 8 | "math/big" 9 | "testing" 10 | ) 11 | 12 | func TestName(t *testing.T) { 13 | 14 | b := abi.Signature("Transfer(from,to,value)") 15 | fmt.Println([]byte("transfer(address,uint256)")) 16 | fmt.Println(common.Bytes2Hex(b)) 17 | 18 | } 19 | 20 | func TestName1(t *testing.T) { 21 | 22 | //s, _ := EthAddress2TronAddress("0x1a5a32bd07c33cd8d9f4bd158f235613480c7eef") 23 | //s, _ := EthAddress2TronAddress("0x740fbbcf714f0295207adc53f0128c0ff93c16cd") 24 | //s, _ := EthAddress2TronAddress("0xb9565E907eF7613338A3838a2Cd33D9E71bfFe9A") 25 | //fmt.Println(s) 26 | 27 | rewardAmount := decimal.NewFromBigInt(big.NewInt(100), 0) 28 | fmt.Println(rewardAmount) 29 | 30 | ii := big.NewInt(1000000000000000000) 31 | d := new(big.Int).Mul(big.NewInt(1000489), ii) 32 | a := big.NewInt(81197043129506) 33 | b := big.NewInt(1) 34 | s := new(big.Int).Div(d, a) 35 | c := new(big.Int).Div(s, b) 36 | fmt.Println(c) 37 | fmt.Println(new(big.Int).Div(d, a)) 38 | fmt.Println(1000489 / 123) 39 | } 40 | 41 | func TestSlice(t *testing.T) { 42 | 43 | var memberUpdateAddress []string 44 | 45 | memberUpdateAddress = append(memberUpdateAddress, "sss") 46 | 47 | fmt.Println(memberUpdateAddress) 48 | 49 | memberUpdate := make(map[string]interface{}) 50 | fmt.Println(memberUpdate) 51 | memberUpdate["ss"] = 1 52 | 53 | fmt.Println(memberUpdate) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /types/wallet.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "math/big" 4 | 5 | type Wallet struct { 6 | Address string 7 | PublicKey string 8 | PrivateKey string 9 | } 10 | 11 | type Transaction struct { 12 | BlockNumber *big.Int // 区块号 13 | BlockHash string // 区块哈希 14 | Hash string // 交易hash 15 | From string // 交易者 16 | To string // 接收者 17 | Value *big.Int // 交易数量 18 | Status uint // 状态(0:未完成,1:已完成) 19 | } 20 | --------------------------------------------------------------------------------