forked from go/golangs_learn
				
			
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							374 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							374 lines
						
					
					
						
							11 KiB
						
					
					
				| package app | |
| 
 | |
| import ( | |
| 	"bufio" | |
| 	"bytes" | |
| 	"encoding/json" | |
| 	"io/ioutil" | |
| 	"net/http" | |
| 	"os" | |
| 	"time" | |
| 
 | |
| 	"github.com/grvlle/constellation_wallet/backend/models" | |
| ) | |
| 
 | |
| // Transaction contains all tx information | |
| type Transaction struct { | |
| 	Edge struct { | |
| 		ObservationEdge struct { | |
| 			Parents []struct { | |
| 				HashReference string `json:"hashReference"` | |
| 				HashType      string `json:"hashType"` | |
| 				BaseHash      string `json:"baseHash"` | |
| 			} `json:"parents"` | |
| 			Data struct { | |
| 				HashReference string `json:"hashReference"` | |
| 				HashType      string `json:"hashType"` | |
| 				BaseHash      string `json:"baseHash"` | |
| 			} `json:"data"` | |
| 		} `json:"observationEdge"` | |
| 		SignedObservationEdge struct { | |
| 			SignatureBatch struct { | |
| 				Hash       string `json:"hash"` | |
| 				Signatures []struct { | |
| 					Signature string `json:"signature"` | |
| 					ID        struct { | |
| 						Hex string `json:"hex"` | |
| 					} `json:"id"` | |
| 				} `json:"signatures"` | |
| 			} `json:"signatureBatch"` | |
| 		} `json:"signedObservationEdge"` | |
| 		Data struct { | |
| 			Amount    int64 `json:"amount"` | |
| 			LastTxRef struct { | |
| 				PrevHash string `json:"prevHash"` | |
| 				Ordinal  int    `json:"ordinal"` | |
| 			} `json:"lastTxRef"` | |
| 			Fee  int64 `json:"fee,omitempty"` | |
| 			Salt int64 `json:"salt"` | |
| 		} `json:"data"` | |
| 	} `json:"edge"` | |
| 	LastTxRef struct { | |
| 		PrevHash string `json:"prevHash"` | |
| 		Ordinal  int    `json:"ordinal"` | |
| 	} `json:"lastTxRef"` | |
| 	IsDummy bool `json:"isDummy"` | |
| 	IsTest  bool `json:"isTest"` | |
| } | |
| 
 | |
| /* Send a transaction */ | |
| 
 | |
| // TriggerTXFromFE will initate a new transaction triggered from the frontend. | |
| func (a *WalletApplication) TriggerTXFromFE(amount float64, fee float64, address string) bool { | |
| 	amountConverted := int64(amount * 1e8) | |
| 	feeConverted := int64(fee * 1e8) | |
| 
 | |
| 	a.PrepareTransaction(amountConverted, feeConverted, address) | |
| 	for !a.TransactionFinished { | |
| 		time.Sleep(1 * time.Second) | |
| 	} | |
| 	return a.TransactionFailed | |
| } | |
| 
 | |
| // PrepareTransaction is triggered from the frontend (Transaction.vue) and will initialize a new tx. | |
| // methods called are defined in buildchain.go | |
| func (a *WalletApplication) PrepareTransaction(amount int64, fee int64, address string) { | |
| 
 | |
| 	balance, err := a.GetTokenBalance() | |
| 	if err != nil { | |
| 		a.log.Errorln("Error when querying wallet balance. Reason: ", err) | |
| 		a.sendWarning("Unable to poll balance for wallet. Please try again later.") | |
| 		a.TransactionFailed = true | |
| 		return | |
| 	} | |
| 
 | |
| 	if amount+fee > int64(balance*1e8) { | |
| 		a.log.Warnf("Trying to send: %d", amount+fee) | |
| 		a.log.Warnf("Insufficient Balance: %d", int64(balance*1e8)) | |
| 		a.sendWarning("Insufficent Balance.") | |
| 		a.TransactionFailed = true | |
| 		return | |
| 	} | |
| 
 | |
| 	if a.TransactionFinished { | |
| 		a.TransactionFinished = false | |
| 
 | |
| 		// Asynchronously inform FE of TX state in wallet. | |
| 		go func() { | |
| 			for !a.TransactionFinished { | |
| 				a.RT.Events.Emit("tx_in_transit", a.TransactionFinished) | |
| 				time.Sleep(1 * time.Second) | |
| 			} | |
| 			a.RT.Events.Emit("tx_in_transit", a.TransactionFinished) | |
| 		}() | |
| 		ptx := a.loadTXFromFile(a.paths.PrevTXFile) | |
| 		ltx := a.loadTXFromFile(a.paths.LastTXFile) | |
| 
 | |
| 		ptxObj, ltxObj := a.convertToTXObject(ptx, ltx) | |
| 
 | |
| 		a.formTXChain(amount, fee, address, ptxObj, ltxObj) | |
| 	} | |
| } | |
| 
 | |
| func (a *WalletApplication) putTXOnNetwork(tx *Transaction) (bool, string) { | |
| 	a.log.Info("Attempting to communicate with mainnet on: " + a.Network.URL + a.Network.Handles.Transaction) | |
| 	/* TEMPORARILY COMMENTED OUT */ | |
| 	a.log.Warnln("TX Ordinal:", tx.Edge.Data.LastTxRef.Ordinal) | |
| 	bytesRepresentation, err := json.Marshal(tx) | |
| 	if err != nil { | |
| 		a.log.Errorln("Unable to parse JSON data for transaction", err) | |
| 		a.sendError("Unable to parse JSON data for transaction", err) | |
| 		return false, "" | |
| 	} | |
| 	resp, err := http.Post(a.Network.URL+a.Network.Handles.Transaction, "application/json", bytes.NewBuffer(bytesRepresentation)) | |
| 	if err != nil { | |
| 		a.log.Errorln("Failed to send HTTP request. Reason: ", err) | |
| 		a.sendError("Unable to send request to mainnet. Please check your internet connection. Reason: ", err) | |
| 		return false, "" | |
| 	} | |
| 	defer resp.Body.Close() | |
| 
 | |
| 	if resp.StatusCode == http.StatusOK { | |
| 		bodyBytes, err := ioutil.ReadAll(resp.Body) | |
| 		if err != nil { | |
| 			a.log.Errorln(string(bodyBytes)) | |
| 			a.log.Errorln("Failed to read the response body. Reason: ", err) | |
| 		} | |
| 
 | |
| 		bodyString := string(bodyBytes[1:65]) | |
| 		a.log.Infoln("The bytesize of the request body: ", len(bodyBytes)) | |
| 		if len(bodyBytes) == 66 { | |
| 			a.log.Info("Transaction Hash: ", bodyString) | |
| 			a.TxPending(bodyString) | |
| 			a.log.Infoln("Transaction has been successfully sent to the network.") | |
| 			a.sendSuccess("Transaction successfully sent!") | |
| 			return true, bodyString | |
| 		} | |
| 		a.log.Warn(bodyString) | |
| 		a.sendWarning("Unable to put transaction on the network. Reason: " + bodyString) | |
| 		return false, "" | |
| 	} | |
| 
 | |
| 	bodyBytes, err := ioutil.ReadAll(resp.Body) | |
| 	if err != nil { | |
| 		a.log.Errorln(err) | |
| 	} | |
| 	bodyString := string(bodyBytes) | |
| 	a.sendError("Unable to communicate with mainnet. Reason: "+bodyString, err) | |
| 	a.log.Errorln("Unable to put TX on the network. HTTP Code: " + string(resp.StatusCode) + " - " + bodyString) | |
| 
 | |
| 	return false, "" | |
| } | |
| 
 | |
| func (a *WalletApplication) sendTransaction(txFile string) *models.TXHistory { | |
| 
 | |
| 	txObject := a.loadTXFromFile(txFile) | |
| 
 | |
| 	tx := &Transaction{} | |
| 
 | |
| 	bytes := []byte(txObject) | |
| 	err := json.Unmarshal(bytes, &tx) | |
| 	if err != nil { | |
| 		a.sendError("Unable to parse the last transaction. Reason:", err) | |
| 		a.log.Errorf("Unable to parse contents of last_tx. Reason: %s", err) | |
| 		return nil | |
| 	} | |
| 
 | |
| 	// Put TX object on network | |
| 	TXSuccessfullyPutOnNetwork, hash := a.putTXOnNetwork(tx) | |
| 	if TXSuccessfullyPutOnNetwork { | |
| 		txData := &models.TXHistory{ | |
| 			Amount:   tx.Edge.Data.Amount, | |
| 			Receiver: tx.Edge.ObservationEdge.Parents[1].HashReference, | |
| 			Fee:      tx.Edge.Data.Fee, | |
| 			Hash:     hash, | |
| 			TS:       time.Now().Format("Jan _2 15:04:05"), | |
| 			Status:   "Pending", | |
| 			Failed:   false, | |
| 		} | |
| 		a.storeTX(txData) | |
| 		a.RT.Events.Emit("new_transaction", txData) // Pass the tx to the frontend as a new transaction. | |
| 		a.TransactionFinished = true | |
| 		a.TransactionFailed = false | |
| 		return txData | |
| 	} | |
| 	txData := &models.TXHistory{ | |
| 		Amount:   tx.Edge.Data.Amount, | |
| 		Receiver: tx.Edge.ObservationEdge.Parents[1].HashReference, | |
| 		Fee:      tx.Edge.Data.Fee, | |
| 		Hash:     hash, | |
| 		TS:       time.Now().Format("Jan _2 15:04:05"), | |
| 		Status:   "Error", | |
| 		Failed:   true, | |
| 	} | |
| 	a.log.Errorln("TX Failed, storing with failed state.") | |
| 	a.storeTX(txData) | |
| 	a.TransactionFinished = true | |
| 	a.TransactionFailed = true | |
| 	return txData | |
| } | |
| 
 | |
| func (a *WalletApplication) storeTX(txData *models.TXHistory) { | |
| 
 | |
| 	if txData == nil { | |
| 		return | |
| 	} | |
| 	if err := a.DB.Model(&a.wallet).Where("wallet_alias = ?", a.wallet.WalletAlias).Association("TXHistory").Append(txData).Error; err != nil { | |
| 		a.log.Errorln("Unable to update the DB record with the new TX. Reason: ", err) | |
| 		a.sendError("Unable to update the DB record with the new TX. Reason: ", err) | |
| 	} | |
| 	a.log.Infoln("Successfully stored tx in DB") | |
| } | |
| 
 | |
| // loadTXFromFile takes a file, scans it and returns it in an object | |
| func (a *WalletApplication) loadTXFromFile(txFile string) string { | |
| 	var txObjects string | |
| 
 | |
| 	fi, err := os.Stat(txFile) | |
| 	if err != nil { | |
| 		a.log.Errorln("Unable to stat last_tx. Reason: ", err) | |
| 		a.sendError("Unable to stat last_tx. Reason: ", err) | |
| 		return "" | |
| 	} | |
| 	// get the size | |
| 	size := fi.Size() | |
| 	if size <= 0 { | |
| 		a.log.Info("TX file is empty.") | |
| 		return "" | |
| 	} | |
| 
 | |
| 	file, err := os.Open(txFile) // acct | |
| 	if err != nil { | |
| 		a.log.Errorln("Unable to open TX file. Reason: ", err) | |
| 		a.sendError("Unable to read last tx. Aborting... Reason: ", err) | |
| 		return "" | |
| 	} | |
| 
 | |
| 	scanner := bufio.NewScanner(file) | |
| 	scanner.Split(bufio.ScanLines) | |
| 
 | |
| 	for scanner.Scan() { | |
| 		txObjects = scanner.Text() | |
| 	} | |
| 	defer file.Close() | |
| 	return txObjects | |
| } | |
| 
 | |
| /* Query TX */ | |
| 
 | |
| // TxProcessed will query the last transaction. If no answer is returned, it means it's processed and the | |
| // method will return true. | |
| func (a *WalletApplication) TxProcessed(TXHash string) bool { | |
| 	a.log.Info("Communicating with mainnet on: " + a.Network.URL + a.Network.Handles.Transaction + "/" + TXHash) | |
| 
 | |
| 	resp, err := http.Get(a.Network.URL + a.Network.Handles.Transaction + "/" + TXHash) | |
| 	if err != nil { | |
| 		a.log.Errorln("Failed to send HTTP request. Reason: ", err) | |
| 		if err := a.DB.Model(&a.wallet).Where("wallet_alias = ?", a.wallet.WalletAlias).Delete(&a.wallet).Error; err != nil { | |
| 			a.log.Errorln("Unable to delete wallet upon failed import. Reason: ", err) | |
| 			return false | |
| 		} | |
| 		a.log.Errorln("Unable to verify transaction status. Please check your internet connection.") | |
| 		return false | |
| 	} | |
| 	defer resp.Body.Close() | |
| 
 | |
| 	if resp.Body == nil { | |
| 		return false | |
| 	} | |
| 
 | |
| 	bodyBytes, err := ioutil.ReadAll(resp.Body) | |
| 	if err != nil { | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Declared an empty interface | |
| 	var result map[string]interface{} | |
| 
 | |
| 	// Unmarshal or Decode the JSON to the interface. | |
| 	err = json.Unmarshal(bodyBytes, &result) | |
| 	if err != nil { | |
| 		return false | |
| 	} | |
| 
 | |
| 	if result["cbBaseHash"] != nil { | |
| 		a.log.Infoln("CheckPoint Hash :", result["cbBaseHash"]) | |
| 		return true | |
| 	} | |
| 
 | |
| 	// null response means it's snapshotted | |
| 	return string(bodyBytes) == "null" | |
| 
 | |
| } | |
| 
 | |
| type txStatus struct { | |
| 	Complete string | |
| 	Pending  string | |
| 	Error    string | |
| } | |
| 
 | |
| // TxPending takes a TX Hash and updates the frontend with the current status (Pending/Error/Complete) | |
| func (a *WalletApplication) TxPending(TXHash string) { | |
| 
 | |
| 	status := &txStatus{ | |
| 		Complete: "Complete", | |
| 		Pending:  "Pending", | |
| 		Error:    "Error", | |
| 	} | |
| 
 | |
| 	consensus := 0 | |
| 
 | |
| 	select { | |
| 	case <-a.killSignal: | |
| 		return | |
| 	default: | |
| 		go func() bool { | |
| 			for retryCounter := 0; retryCounter < 30; retryCounter++ { | |
| 				processed := a.TxProcessed(TXHash) | |
| 				if !processed { | |
| 					a.log.Warnf("Transaction %v pending", TXHash) | |
| 					a.RT.Events.Emit("tx_pending", status.Pending) | |
| 					time.Sleep(time.Duration(retryCounter) * time.Second) // Increase polling interval | |
|  | |
| 					if retryCounter == 29 { | |
| 						// Register failed transaction | |
| 						a.sendWarning("Unable to get verification of processed transaction from the network. Please try again later.") | |
| 						a.log.Errorf("Unable to get status from the network on transaction: %s", TXHash) | |
| 						a.RT.Events.Emit("tx_pending", status.Error) | |
| 						if err := a.DB.Table("tx_histories").Where("hash = ?", TXHash).Updates(map[string]interface{}{"status": status.Error, "failed": true}).Error; err != nil { | |
| 							a.log.Errorln("Unable to query database object for the imported wallet. Reason: ", err) | |
| 							a.LoginError("Unable to query database object for the imported wallet.") | |
| 							return false | |
| 						} | |
| 						a.RT.Events.Emit("update_tx_history", []models.TXHistory{}) // Clear TX history | |
| 						a.initTXFromDB() | |
| 						return false | |
| 					} | |
| 
 | |
| 					consensus = 0 // Reset consensus | |
| 				} | |
| 				if processed && consensus != 3 { | |
| 					consensus++ | |
| 					a.log.Infof("TX status check has reached consensus %v/3", consensus) | |
| 					time.Sleep(1 * time.Second) | |
| 				} | |
| 				if processed && consensus == 3 { // Need five consecetive confirmations that TX has been processed. | |
| 					break | |
| 				} | |
| 
 | |
| 			} | |
| 			a.log.Infof("Transaction %v has been successfully processed", TXHash) | |
| 			a.sendSuccess("Transaction " + TXHash[:30] + "... has been successfully processed") | |
| 			if err := a.DB.Table("tx_histories").Where("hash = ?", TXHash).UpdateColumn("status", status.Complete).Error; err != nil { | |
| 				a.log.Errorln("Unable to query database object for the imported wallet. Reason: ", err) | |
| 				a.LoginError("Unable to query database object for the imported wallet.") | |
| 				return false | |
| 			} | |
| 			a.RT.Events.Emit("tx_pending", status.Complete) | |
| 			a.RT.Events.Emit("update_tx_history", []models.TXHistory{}) // Clear TX history | |
| 			a.initTXFromDB() | |
| 			return true | |
| 
 | |
| 		}() | |
| 	} | |
| }
 | |
| 
 |