Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- package main
- import "C"
- import (
- "bytes"
- "context"
- "crypto/ecdsa"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "os"
- "strconv"
- "strings"
- "sync"
- "sync/atomic"
- "time"
- "unsafe"
- "github.com/ethereum/go-ethereum"
- "github.com/ethereum/go-ethereum/accounts/abi"
- "github.com/ethereum/go-ethereum/accounts/abi/bind"
- "github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/common/hexutil"
- "github.com/ethereum/go-ethereum/core/types"
- "github.com/ethereum/go-ethereum/crypto"
- "github.com/ethereum/go-ethereum/ethclient"
- )
- type Priority int
- const (
- DEFAULT Priority = iota
- HIGH
- )
- type TxInfo struct {
- submitTime time.Time
- timeout time.Duration
- priority Priority
- nonce int64
- hash string
- }
- type Client struct {
- publicKey *ecdsa.PublicKey
- privateKey *ecdsa.PrivateKey
- sender common.Address
- innerClient *ethclient.Client
- nonce uint64
- processingRetries int64
- contracts map[string]Contract // TODO: use self-contract in order to eliminate from expensive function finding
- muCancelOldTx sync.Mutex
- waitPrevFinishing chan struct{}
- }
- type Contract struct {
- boundContract *bind.BoundContract
- abi abi.ABI
- }
- var (
- globalClient *Client
- )
- //export Init
- func Init(nodeKey, privateKey *C.char) uintptr {
- innerClient, err := ethclient.Dial("https://polygon-mainnet.infura.io/v3/" + C.GoString(nodeKey))
- if err != nil {
- log.Fatalf("failed to create client: %v", err)
- }
- privateKeyECDSA, err := crypto.HexToECDSA(C.GoString(privateKey)[2:])
- if err != nil {
- log.Fatalf("failed to decode private key: %v", err)
- }
- publicKey := privateKeyECDSA.Public()
- publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
- if !ok {
- log.Fatalf("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
- }
- address := crypto.PubkeyToAddress(*publicKeyECDSA)
- initNonce, err := innerClient.NonceAt(context.Background(), address, nil)
- // initNonce, err := innerClient.PendingNonceAt(context.Background(), address)
- log.Printf("init Nonce: %d\n", initNonce)
- if err != nil {
- log.Fatalf("cannot get nonce: PendingNonceAt function failed: %v", err)
- }
- globalClient = &Client{ // клиент снимется с кучи? (не сразу же даже если так, а в следующую итерацию GC)
- publicKey: publicKeyECDSA,
- privateKey: privateKeyECDSA,
- sender: address,
- innerClient: innerClient,
- nonce: uint64(initNonce),
- contracts: make(map[string]Contract, 16), // 16 это примерная оценка
- waitPrevFinishing: make(chan struct{}, 1),
- }
- return uintptr(unsafe.Pointer(globalClient)) // no dangling reference?
- }
- //export GetWalletAddress
- func GetWalletAddress(clientPtr unsafe.Pointer) *C.char {
- client := (*Client)(clientPtr)
- if client == nil {
- log.Fatal("no client")
- }
- return C.CString(client.sender.String())
- }
- //export SignMessage
- func SignMessage(clientPtr unsafe.Pointer, message *C.char) *C.char {
- client := (*Client)(clientPtr)
- if client == nil {
- log.Fatal("no client")
- }
- data := []byte(C.GoString(message))
- hash := crypto.Keccak256Hash(data)
- signature, err := crypto.Sign(hash.Bytes(), client.privateKey)
- if err != nil {
- log.Fatal(err) // пока пусть, тк юзаем всего 1 раз при инициализации
- }
- return C.CString(hexutil.Encode(signature))
- }
- //export BindSmartContract
- func BindSmartContract(clientPtr unsafe.Pointer, contractAddress, pathToAbi *C.char) {
- lowerContractAddress := strings.ToLower(C.GoString(contractAddress))
- client := (*Client)(clientPtr)
- if client == nil {
- log.Fatal("no client")
- }
- file, err := os.Open(C.GoString(pathToAbi))
- if err != nil {
- log.Fatalf("failed to open ABI file: %v", err)
- }
- defer file.Close()
- content, err := io.ReadAll(file)
- if err != nil {
- log.Fatalf("failed to read ABI file: %v", err)
- }
- var contractInfo map[string]interface{}
- if json.Unmarshal(content, &contractInfo); err != nil {
- log.Fatalf("failed to unmarshal ABI JSON: %v", err)
- }
- contractABI, err := json.Marshal(contractInfo["abi"])
- if err != nil {
- log.Fatalf("failed to marshal ABI: %v", err)
- }
- parsedABI, err := abi.JSON(bytes.NewReader(contractABI))
- if err != nil {
- log.Fatalf("failed to parse ABI: %v", err)
- }
- address := common.HexToAddress(lowerContractAddress)
- contract := bind.NewBoundContract(address, parsedABI, client.innerClient, client.innerClient, client.innerClient)
- client.contracts[lowerContractAddress] = Contract{boundContract: contract, abi: parsedABI}
- log.Printf("bind contract %s", lowerContractAddress)
- }
- //export ContractCallConstFunc
- func ContractCallConstFunc(clientPtr unsafe.Pointer, contractAddress, cFunctionName, args *C.char) *C.char {
- lowerContractAddress := strings.ToLower(C.GoString(contractAddress))
- client := (*Client)(clientPtr)
- if client == nil {
- log.Fatal("no client")
- }
- contract, found := client.contracts[lowerContractAddress]
- if !found {
- log.Printf("No contract %s", lowerContractAddress)
- return C.CString("{\"error\":\"no contract\"}")
- }
- functionName := C.GoString(cFunctionName)
- functionAbi, found := contract.abi.Methods[functionName]
- if !found {
- log.Println("No function in abi")
- return C.CString("{\"error\":\"no function in abi\"}")
- }
- var callInput []interface{}
- inputStrToFunctionCallParams(functionAbi, C.GoString(args), &callInput)
- callOpts := &bind.CallOpts{
- Context: context.Background(),
- }
- var output []interface{}
- err := contract.boundContract.Call(callOpts, &output, functionName, callInput...) // в горутине запускать мб? а как горутины в cgo работают?
- if err != nil {
- return C.CString(fmt.Sprintf("{\"error\":\"err in call function: %v\"}", err))
- }
- resultOut, err := formOutput(functionAbi, &output)
- if err != nil {
- return C.CString(fmt.Sprintf("{\"error\":\"formOutput error: %v\"}", err))
- }
- return C.CString(resultOut)
- }
- //export ContractCallMutFunc
- func ContractCallMutFunc(clientPtr unsafe.Pointer, contractAddress, cFunctionName, args, submitExtends *C.char) *C.char {
- log.Printf("extends args: %s", C.GoString(submitExtends))
- log.Printf("args: %s", C.GoString(args))
- client := (*Client)(clientPtr)
- if client == nil {
- log.Fatal("no client")
- }
- lowerContractAddress := strings.ToLower(C.GoString(contractAddress))
- contract, found := client.contracts[lowerContractAddress]
- if !found {
- log.Printf("No contract %s", lowerContractAddress)
- return C.CString("{\"error\":\"no contract\", \"tx_hash\":\"0\"}")
- }
- functionName := C.GoString(cFunctionName)
- functionAbi, found := contract.abi.Methods[functionName]
- if !found {
- log.Println("No function in abi")
- return C.CString("{\"error\":\"no function in abi\", \"tx_hash\":\"0\"}")
- }
- // client.waitPrevFinishing <- struct{}{} // чтобы последовательно транзакции делали
- // defer func() {
- // <-client.waitPrevFinishing
- // }()
- if atomic.LoadInt64(&client.processingRetries) != 0 { // чтобы не скапливались транзакции
- return C.CString("{\"error\":\"any tx already being processed\", \"tx_hash\":\"0\"}")
- }
- var submitExtendsMap map[string]interface{}
- err := json.Unmarshal([]byte(C.GoString(submitExtends)), &submitExtendsMap)
- if err != nil {
- log.Printf("{\"error\":\"can't parse extends info: %v\", \"tx_hash\":\"0\"}", err)
- return C.CString(
- fmt.Sprintf("{\"error\":\"can't parse extends info: %v\", \"tx_hash\":\"0\"}", err),
- )
- }
- if _, exists := submitExtendsMap["priority"]; !exists {
- submitExtendsMap["priority"] = "0"
- }
- if _, exists := submitExtendsMap["timeout"]; !exists {
- submitExtendsMap["timeout"] = "300000" // 5 min
- }
- var txPriority Priority
- if submitExtendsMap["priority"].(string) == "0" {
- txPriority = DEFAULT
- } else {
- log.Println("High priority")
- txPriority = HIGH
- }
- timeout, err := strconv.Atoi(submitExtendsMap["timeout"].(string))
- if err != nil {
- log.Fatalf("can't convert timeout to int: %v", err)
- }
- newNonce := atomic.AddUint64(&client.nonce, 1) - 1 // innerClient.NonceAt(context.Background(), address, nil)
- auth, err := FormTransactor(client, newNonce, 3000000, txPriority)
- if err != nil {
- log.Fatalf("formTransactor() error: %v", err)
- }
- var callInput []interface{}
- inputStrToFunctionCallParams(functionAbi, C.GoString(args), &callInput)
- tx, err := contract.boundContract.Transact(auth, functionName, callInput...) // first try
- if err != nil {
- log.Printf("Failed to call contract function: %v", err)
- return C.CString(
- "{\"error\":\"failed to call contarct function\", \"tx_hash\":\"0\"}",
- )
- }
- firstTxHash := tx.Hash().Hex()
- log.Printf("TX Hash: %s ; Nonce: %d\n", tx.Hash().Hex(), newNonce)
- var receipt *types.Receipt
- retries := 0
- var submitTime time.Time
- retryMultiplicators := []float64{2.0, 2.5, 3.0, 3.0}
- for {
- submitTime = time.Now()
- ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Millisecond)
- defer cancel()
- receipt, err = bind.WaitMined(ctx, client.innerClient, tx)
- if err != nil { // timeout exceeded -> cancel
- retries++
- timeout = 12000 // 12000 ms = 12 s: Пока что это было выбрано по маленькому массиву данных
- if retries == 1 { // будет первый ретрай
- atomic.AddInt64(&client.processingRetries, 1)
- }
- log.Printf("timeout_metric -> exceeded: tx: %s ; timeout %d ms ; first tx: %s", tx.Hash().Hex(), timeout, firstTxHash)
- if retries > 3 { // отправляем финальную ждать
- if retries == 5 { // то есть экстра-ретрай 5 минут тоже не помог
- log.Fatalf("Extra retry was exceeded for first tx: %s", firstTxHash)
- }
- // не падаем, тк если ночь, то не хочется класть бота, вдруг через 5 минут заретраится
- log.Printf("Warning: call extra retry for first tx: %s", firstTxHash) // TODO вынести в метрики чтобы можно было увед получить
- timeout = 300000 // 5 min: TODO: параметром задать предельное время ожидания
- }
- cancelTx, err := CancelTx(client, tx.Hash().Hex(), newNonce, retryMultiplicators[retries-1])
- if err != nil {
- return C.CString(
- fmt.Sprintf(
- "{\"error\":\"don't know: %v\", \"tx_hash\":\"%s\"}",
- err, firstTxHash,
- ),
- )
- }
- tx = cancelTx
- } else { // succeed
- log.Printf("timeout_metric -> complete: tx: %s ; exec_time: %fs ; attempt = %d ; first tx %s\n", tx.Hash().Hex(), time.Since(submitTime).Seconds(), retries, firstTxHash)
- if retries == 0 {
- break
- }
- atomic.AddInt64(&client.processingRetries, -1) // отпустили ретрай когда успешно сработал
- return C.CString(
- fmt.Sprintf(
- "{\"error\":\"tx was cancelled with %s\", \"tx_hash\":\"%s\"}",
- tx.Hash().Hex(), firstTxHash,
- ),
- )
- }
- }
- if receipt.Status != 1 {
- log.Printf("{\n tx = %s failed\n exec_time = %fs\n}", tx.Hash().Hex(), time.Since(submitTime).Seconds())
- msg := ethereum.CallMsg{
- To: &receipt.ContractAddress,
- Data: tx.Data(),
- }
- errorMsg, err := client.innerClient.CallContract(context.Background(), msg, receipt.BlockNumber)
- if err != nil {
- return C.CString(
- fmt.Sprintf(
- "{\"error\":\"calling contract error: %v\", \"tx_hash\":\"%s\"}",
- err, tx.Hash().Hex(),
- ),
- )
- }
- if len(errorMsg) > 0 {
- return C.CString(
- fmt.Sprintf(
- "{\"error\":\"execution reverted error msg: %s\", \"tx_hash\":\"%s\"}",
- string(errorMsg), tx.Hash().Hex(),
- ),
- )
- }
- return C.CString(
- fmt.Sprintf(
- "{\"error\":\"unknown tx error\", \"tx_hash\":\"%s\"}",
- tx.Hash().Hex(),
- ),
- )
- }
- log.Printf(
- "golang tx log: {\n tx = %s ok;\n status = %d\n block = %d\n usedGas = %d\n exec_time = %fs\n}",
- tx.Hash().Hex(), receipt.Status, receipt.BlockNumber.Int64(), receipt.GasUsed, time.Since(submitTime).Seconds(),
- )
- return C.CString(
- fmt.Sprintf(
- "{\"error\":\"ok\", \"block\":\"%d\", \"used_gas\":\"%d\", \"tx_hash\":\"%s\"}",
- receipt.BlockNumber.Int64(), receipt.GasUsed, tx.Hash().Hex(),
- ),
- )
- }
- //export GetCoreUserIdFromJwt
- func GetCoreUserIdFromJwt(token *C.char) *C.char {
- id, err := getJwtTokenField(C.GoString(token), "MetaCoreId")
- if err != nil {
- log.Fatalf("GetUserIdFromJwt error: %v", err)
- }
- return C.CString(id)
- }
- func main() {}
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement