commit 9f67d3a6ee0feeb35c71dcae5500ada5cdd404b7 Author: mmarcel Date: Thu Apr 11 14:35:17 2024 +0200 initial upload diff --git a/cfg/config.go b/cfg/config.go new file mode 100644 index 0000000..c70a413 --- /dev/null +++ b/cfg/config.go @@ -0,0 +1,201 @@ +package cfg + +import ( + "dero-swap/coin" + "dero-swap/dero" + "dero-swap/monero" + "encoding/json" + "log" + "os" + "strings" + + "github.com/ybbus/jsonrpc/v3" +) + +// load config file +func LoadConfig() { + + fd, err := os.ReadFile("config.json") + if err != nil { + log.Printf("Error loading config file: %v\n", err) + return + } + err = json.Unmarshal(fd, &Settings) + if err != nil { + log.Printf("Error parsing config file: %v\n", err) + return + } + + dero.Dero_Daemon = jsonrpc.NewClient("http://" + Settings.Dero_daemon + "/json_rpc") + dero.Dero_Wallet = jsonrpc.NewClient("http://" + Settings.Dero_wallet + "/json_rpc") + monero.Monero_Wallet = jsonrpc.NewClient("http://" + Settings.Monero_wallet + "/json_rpc") + + coin.XTC_URL[coin.BTCDERO] = "http://" + Settings.BTC_daemon + coin.XTC_URL[coin.LTCDERO] = "http://" + Settings.LTC_daemon + coin.XTC_URL[coin.ARRRDERO] = "http://" + Settings.ARRR_daemon + + // check if pair is "supported" + for _, p := range Settings.Pairs { + supported := false + for i := range coin.Supported_pairs { + if p == coin.Supported_pairs[i] { + supported = true + break + } + } + if supported { + coin.Pairs[p] = true + } else { + log.Printf("%s is not a supported pair\n", p) + } + } + + log.Printf("Config successfully loaded\n") + + LoadFees() +} + +// load fees file +func LoadFees() { + + fd, err := os.ReadFile("fees.json") + if err != nil { + log.Printf("Error loading fees file: %v\n", err) + return + } + err = json.Unmarshal(fd, &SwapFees) + if err != nil { + log.Printf("Error parsing fees file: %v\n", err) + return + } + + log.Printf("%-14s: Buy: %.2f%% / Sell: %.2f%%\n", "Fees", SwapFees.Swap.Ask, SwapFees.Swap.Bid) +} + +// basic config check +func CheckConfig() bool { + + if Settings.Dero_daemon == "" || Settings.Dero_wallet == "" { + log.Println("Dero Daemon or Dero Wallet is not set") + return false + } + + for p := range coin.Pairs { + switch p { + case coin.BTCDERO, coin.DEROBTC: + if Settings.BTC_daemon == "" || Settings.BTC_dir == "" { + log.Printf("%s pair is set, but daemon or directory is not set\n", p) + return false + } else { + coin.BTC_dir = Settings.BTC_dir + } + case coin.LTCDERO, coin.DEROLTC: + if Settings.LTC_daemon == "" || Settings.LTC_dir == "" { + log.Printf("%s pair is set, but daemon or directory is not set\n", p) + return false + } else { + coin.LTC_dir = Settings.LTC_dir + } + case coin.ARRRDERO, coin.DEROARRR: + if Settings.ARRR_daemon == "" || Settings.ARRR_dir == "" { + log.Printf("%s pair is set, but daemon or directory is not set\n", p) + return false + } else { + coin.ARRR_dir = Settings.ARRR_dir + } + case coin.XMRDERO, coin.DEROXMR: + if Settings.Monero_wallet == "" { + log.Printf("%s pair is set, but wallet is not set\n", p) + return false + } + } + } + + if dero.GetHeight() == 0 || dero.CheckBlockHeight() == 0 { + log.Println("Dero daemon or wallet is not available") + return false + } + + return true +} + +// TODO: optimization needed +func LoadWallets() { + + for p := range coin.Pairs { + switch p { + case coin.ARRRDERO, coin.DEROARRR: + addr := coin.ARRR_GetAddress() + if !coin.XTCValidateAddress(p, addr) { + log.Printf("Disable pair \"%s\": wallet not available or other error\n", p) + delete(coin.Pairs, p) + } else { + if coin.ARRR_address == "" { + coin.ARRR_address = addr + coin.SimplePairs[p] = true + log.Printf("%-14s: %s\n", "ARRR Wallet", addr) + } + } + case coin.XMRDERO, coin.DEROXMR: + addr := monero.GetAddress() + if !monero.ValidateAddress(addr) { + log.Printf("Disable pair \"%s\": wallet not available or other error\n", p) + delete(coin.Pairs, p) + } else { + if coin.XMR_address == "" { + coin.XMR_address = addr + coin.SimplePairs[p] = true + log.Printf("%-14s: %s\n", "XMR Wallet", addr) + } + } + case coin.LTCDERO, coin.DEROLTC: + ok, err := coin.XTCLoadWallet(p) + if !ok && !strings.Contains(err, "is already loaded") { + ok, err := coin.XTCNewWallet(p) + if !ok { + log.Printf("Disable pair \"%s\": %s\n", p, err) + delete(coin.Pairs, p) + } else { + addr := coin.XTCGetAddress(p) + if coin.LTC_address == "" { + coin.LTC_address = addr + coin.SimplePairs[p] = true + log.Printf("%-14s: %s\n", "LTC Wallet", addr) + } + } + } else { + addr := coin.XTCGetAddress(p) + if coin.LTC_address == "" { + coin.LTC_address = addr + coin.SimplePairs[p] = true + log.Printf("%-14s: %s\n", "LTC Wallet", addr) + } + } + case coin.BTCDERO, coin.DEROBTC: + ok, err := coin.XTCLoadWallet(p) + if !ok && !strings.Contains(err, "is already loaded") { + ok, err := coin.XTCNewWallet(p) + if !ok { + log.Printf("Disable pair \"%s\": %s\n", p, err) + delete(coin.Pairs, p) + } else { + addr := coin.XTCGetAddress(p) + if coin.BTC_address == "" { + coin.BTC_address = addr + coin.SimplePairs[p] = true + log.Printf("%-14s: %s\n", "BTC Wallet", addr) + } + } + } else { + addr := coin.XTCGetAddress(p) + if coin.BTC_address == "" { + coin.BTC_address = addr + coin.SimplePairs[p] = true + log.Printf("%-14s: %s\n", "BTC Wallet", addr) + } + } + default: + continue + } + } +} diff --git a/cfg/variables.go b/cfg/variables.go new file mode 100644 index 0000000..f91cb46 --- /dev/null +++ b/cfg/variables.go @@ -0,0 +1,49 @@ +package cfg + +type Config struct { + ListenAddress string `json:"listen"` + BTC_daemon string `json:"btc_daemon"` + BTC_dir string `json:"btc_dir"` + LTC_daemon string `json:"ltc_daemon"` + LTC_dir string `json:"ltc_dir"` + ARRR_daemon string `json:"arrr_daemon"` + ARRR_dir string `json:"arrr_dir"` + Dero_daemon string `json:"dero_daemon"` + Dero_wallet string `json:"dero_wallet"` + Monero_daemon string `json:"monero_daemon"` + Monero_wallet string `json:"monero_wallet"` + Pairs []string `json:"pairs"` + //SwapFees float64 `json:"swap_fees"` +} + +type ( + Fees struct { + Swap Swap_Fees `json:"swap"` + Withdrawal Withdrawal_Fees `json:"withdrawal"` + } + /* + Swap_Fees struct { + DeroLTC float64 `json:"dero-ltc"` + DeroBTC float64 `json:"dero-btc"` + DeroARRR float64 `json:"dero-arrr"` + DeroXMR float64 `json:"dero-xmr"` + LTCDero float64 `json:"ltc-dero"` + BTCDero float64 `json:"btc-dero"` + ARRRDero float64 `json:"arrr-dero"` + XMRDero float64 `json:"xmr-dero"` + } + */ + Swap_Fees struct { + Bid float64 `json:"bid"` + Ask float64 `json:"ask"` + } + Withdrawal_Fees struct { + DeroLTC float64 `json:"dero-ltc"` + DeroBTC float64 `json:"dero-btc"` + DeroARRR float64 `json:"dero-arrr"` + DeroXMR float64 `json:"dero-xmr"` + } +) + +var Settings Config +var SwapFees = Fees{Withdrawal: Withdrawal_Fees{DeroLTC: 0.0002, DeroBTC: 0.00004, DeroARRR: 0.001, DeroXMR: 0.00006}} diff --git a/coin/btc.go b/coin/btc.go new file mode 100644 index 0000000..559db58 --- /dev/null +++ b/coin/btc.go @@ -0,0 +1,545 @@ +package coin + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "os" + + "net/http" +) + +func XTCGetCookie(pair string) bool { + + var data []byte + var err error + + switch pair { + case BTCDERO, DEROBTC: + if data, err = os.ReadFile(BTC_dir + "/.cookie"); err != nil { + log.Printf("Can't load BTC auth cookie: %v\n", err) + return false + } + case LTCDERO, DEROLTC: + if data, err = os.ReadFile(LTC_dir + "/.cookie"); err != nil { + log.Printf("Can't load LTC auth cookie: %v\n", err) + return false + } + case ARRRDERO, DEROARRR: + if data, err = os.ReadFile(ARRR_dir + "/.cookie"); err != nil { + log.Printf("Can't load ARRR auth cookie: %v\n", err) + return false + } + } + + XTC_auth = base64.StdEncoding.EncodeToString(data) + data = nil + + return true +} + +func SetHeaders(request *http.Request, auth string) { + + request.Header.Add("Content-Type", "text/plain") + request.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth)) +} + +func XTCBuildRequest(pair string, method string, options []interface{}) (*http.Request, error) { + + json_object := &RPC_Request{Jsonrpc: "1.0", Id: "swap", Method: method, Params: options} + json_bytes, err := json.Marshal(&json_object) + if err != nil { + log.Printf("Can't marshal %s request: %v\n", method, err) + return nil, err + } + + req, err := http.NewRequest("POST", XTC_GetURL(pair), bytes.NewBuffer(json_bytes)) + if err != nil { + log.Printf("Can't create %s request: %v\n", method, err) + return nil, err + } + + XTCGetCookie(pair) + SetHeaders(req, XTC_auth) + + return req, nil +} + +func XTCNewWallet(pair string) (result bool, message string) { + + switch pair { + case DEROARRR, ARRRDERO, DEROXMR, XMRDERO: + return false, "" + } + req, err := XTCBuildRequest(pair, "createwallet", []interface{}{"swap_wallet"}) + if err != nil { + log.Printf("Can't build createwallet request: %v\n", err) + return false, "" + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send createwallet request: %v\n", err) + return false, "" + } + defer resp.Body.Close() + + var json_resonse RPC_NewWallet_Response + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &json_resonse) + if err != nil { + log.Printf("Can't unmarshal createwallet response: %v\n", err) + return false, "" + } + + if json_resonse.Error != (RPC_Error{}) { + message = json_resonse.Error.Message + } else { + result = true + } + + return result, message +} + +func XTCLoadWallet(pair string) (result bool, message string) { + + req, err := XTCBuildRequest(pair, "loadwallet", []interface{}{"swap_wallet"}) + if err != nil { + log.Printf("Can't build loadwallet request: %v\n", err) + return false, "" + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send loadwallet request: %v\n", err) + return false, "" + } + defer resp.Body.Close() + + var json_resonse RPC_NewWallet_Response + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &json_resonse) + if err != nil { + log.Printf("Can't unmarshal createwallet response: %v\n", err) + return false, "" + } + + if json_resonse.Error != (RPC_Error{}) { + message = json_resonse.Error.Message + } else { + result = true + } + + return result, message +} + +func XTCNewAddress(pair string) string { + + req, err := XTCBuildRequest(pair, "getnewaddress", []interface{}{}) + if err != nil { + log.Printf("Can't build getnewaddress request: %v\n", err) + return "" + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send getnewaddress request: %v\n", err) + return "" + } + + defer resp.Body.Close() + + var json_resonse RPC_NewAddress_Response + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &json_resonse) + if err != nil { + log.Printf("Can't unmarshal getnewaddress response: %v\n", err) + return "" + } + log.Println("Successfully created new address") + + return json_resonse.Result +} + +func XTCGetAddress(pair string) string { + + switch pair { + case DEROARRR, ARRRDERO, DEROXMR, XMRDERO: + return "" + } + + _, _, address, _ := XTCListReceivedByAddress(pair, "", 0, 0, true) + + return address +} + +func XTCCheckBlockHeight(pair string) uint64 { + + req, err := XTCBuildRequest(pair, "getblockcount", []interface{}{}) + if err != nil { + log.Printf("Can't build getblockcount request: %v\n", err) + return 0 + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send getblockcount request: %v\n", err) + return 0 + } + + defer resp.Body.Close() + + var response RPC_GetBlockCount_Response + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &response) + if err != nil { + log.Printf("Can't unmarshal getblockcount response: %v\n", err) + return 0 + } else { + return response.Result + } +} + +func XTCReceivedByAddress(pair string, wallet string) (float64, error) { + + req, err := XTCBuildRequest(pair, "getreceivedbyaddress", []interface{}{wallet, 2}) + if err != nil { + log.Printf("Can't build getreceivedbyaddress request: %v\n", err) + return 0, err + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send getreceivedbyaddress request: %v\n", err) + return 0, err + } + + defer resp.Body.Close() + + var response RPC_ReceivedByAddress_Response + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &response) + if err != nil { + log.Printf("Can't unmarshal getreceivedbyaddress response: %v\n", err) + return 0, err + } else { + return response.Result, nil + } +} + +func XTCListReceivedByAddress(pair string, wallet string, amount float64, height uint64, get_address bool) (bool, bool, string, error) { + + var method string + var options []interface{} + + if !get_address { + if pair == DEROARRR { + method = "zs_listreceivedbyaddress" + options = append(options, wallet, 1, 3, height) + } else { + method = "listreceivedbyaddress" + options = append(options, 1, false, false, wallet) + } + } else { + method = "listreceivedbyaddress" + options = append(options, 0, true) + } + + req, err := XTCBuildRequest(pair, method, options) + if err != nil { + log.Printf("Can't build listreceivedbyaddress request: %v\n", err) + return false, false, "", err + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send listreceivedbyaddress request: %v\n", err) + return false, false, "", err + } + + defer resp.Body.Close() + + if pair != DEROARRR { + var response RPC_ListReceivedByAddress_Response + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &response) + if err != nil { + log.Printf("Can't unmarshal listreceivedbyaddress response: %v\n", err) + return false, false, "", err + } + + if !get_address { + if len(response.Result) > 0 { + for _, tx := range response.Result[0].Txids { + tx_data, err := XTCGetTransaction(pair, tx) + if err != nil { + log.Printf("Error checking TX: %v\n", err) + continue + } + if tx_data.Result.Amount == amount && tx_data.Result.Blockheight >= height { + if tx_data.Result.Confirmations > 1 { + return true, true, "", nil + } else { + return false, true, "", nil + } + } + } + } + } else { + if len(response.Result) > 0 { + return false, false, response.Result[0].Address, nil + } else { + return false, false, "", nil + } + } + } else { + var response RPC_ARRR_ListReceivedByAddress_Response + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &response) + if err != nil { + log.Printf("Can't unmarshal zs_listreceivedbyaddress response: %v\n", err) + return false, false, "", err + } + + if len(response.Result) > 0 { + for e := range response.Result { + if response.Result[e].BlockHeight >= height { + for _, tx := range response.Result[e].Received { + if tx.Value == amount { + if response.Result[e].Confirmations > 1 { + return true, true, "", nil + } else { + return false, true, "", nil + } + } + } + } + } + } + } + + return false, false, "", nil +} + +func XTCGetTransaction(pair string, txid string) (result RPC_GetTransaction_Response, err error) { + + req, err := XTCBuildRequest(pair, "gettransaction", []interface{}{txid, false}) + if err != nil { + log.Printf("Can't build gettransaction request: %v\n", err) + return result, err + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send gettransaction request: %v\n", err) + return result, err + } + + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &result) + if err != nil { + log.Printf("Can't unmarshal gettransaction response: %v\n", err) + return result, err + } + + return result, nil +} + +func XTCGetBalance(pair string) float64 { + + var method string + var options []interface{} + if pair == DEROARRR || pair == ARRRDERO { + method = "z_getbalance" + options = append(options, ARRR_GetAddress()) + } else { + method = "getbalance" + } + + req, err := XTCBuildRequest(pair, method, options) + if err != nil { + log.Printf("Can't build getbalance request: %v\n", err) + return 0 + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send getbalance request: %v\n", err) + return 0 + } + + defer resp.Body.Close() + + var result RPC_GetBalance_Result + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &result) + if err != nil { + log.Printf("Can't unmarshal getbalance response: %v\n", err) + return 0 + } + + return RoundFloat(result.Result-Locked.GetLockedBalance(pair), 8) +} + +func XTCSend(pair string, wallet string, amount float64) (bool, string) { + + var options []interface{} + + switch pair { + case DEROLTC: + options = append(options, wallet, amount) + case DEROBTC: + options = append(options, wallet, amount, "", "", false, true, nil, "unset", nil, 14) + } + + req, err := XTCBuildRequest(pair, "sendtoaddress", options) + if err != nil { + log.Printf("Can't build sendtoaddress request: %v\n", err) + return false, "" + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send sendtoaddress request: %v\n", err) + return false, "" + } + + defer resp.Body.Close() + + var result RPC_Send_Result + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &result) + if err != nil { + log.Printf("Can't unmarshal sendtoaddress response: %v\n", err) + return false, "" + } + + return true, result.Result +} + +func XTCValidateAddress(pair string, address string) bool { + + var method string + if pair == DEROARRR || pair == ARRRDERO { + method = "z_validateaddress" + } else { + method = "validateaddress" + } + + req, err := XTCBuildRequest(pair, method, []interface{}{address}) + if err != nil { + log.Printf("Can't build validateaddress request: %v\n", err) + return false + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send validateaddress request: %v\n", err) + return false + } + defer resp.Body.Close() + + var result RPC_Validate_Address_Result + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &result) + if err != nil { + log.Printf("Can't unmarshal validateaddress response: %v\n", err) + return false + } + + return result.Result.IsValid +} + +func XTC_GetURL(pair string) string { + + switch pair { + case BTCDERO, DEROBTC: + return XTC_URL[BTCDERO] + case LTCDERO, DEROLTC: + return XTC_URL[LTCDERO] + case ARRRDERO, DEROARRR: + return XTC_URL[ARRRDERO] + default: + return "" + } +} + +func ARRR_Send(wallet string, amount float64) (bool, string) { + + var params []RPC_ARRR_SendMany_Params + + params = append(params, RPC_ARRR_SendMany_Params{Address: wallet, Amount: amount}) + options := []interface{}{ARRR_GetAddress(), params} + + req, err := XTCBuildRequest(DEROARRR, "z_sendmany", options) + if err != nil { + log.Printf("Can't build z_sendmany request: %v\n", err) + return false, "" + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send z_sendmany request: %v\n", err) + return false, "" + } + + defer resp.Body.Close() + + var result RPC_Send_Result + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &result) + if err != nil { + log.Printf("Can't unmarshal z_sendmany response: %v\n", err) + return false, "" + } + + return true, result.Result +} + +func ARRR_GetAddress() string { + + req, err := XTCBuildRequest(DEROARRR, "z_listaddresses", nil) + if err != nil { + log.Printf("Can't build z_listaddresses request: %v\n", err) + return "" + } + + resp, err := XTC_Daemon.Do(req) + if err != nil { + log.Printf("Can't send z_listaddresses request: %v\n", err) + return "" + } + + defer resp.Body.Close() + + var result RPC_ARRR_ListAddresses + + body, _ := io.ReadAll(resp.Body) + err = json.Unmarshal(body, &result) + if err != nil { + log.Printf("Can't unmarshal z_listaddresses response: %v\n", err) + return "" + } + + if len(result.Result) == 0 { + return "" + } else { + return result.Result[0] + } +} diff --git a/coin/constants.go b/coin/constants.go new file mode 100644 index 0000000..cc6674b --- /dev/null +++ b/coin/constants.go @@ -0,0 +1,12 @@ +package coin + +const ( + ARRRDERO = "arrr-dero" + BTCDERO = "btc-dero" + LTCDERO = "ltc-dero" + XMRDERO = "xmr-dero" + DEROLTC = "dero-ltc" + DEROBTC = "dero-btc" + DEROARRR = "dero-arrr" + DEROXMR = "dero-xmr" +) diff --git a/coin/functions.go b/coin/functions.go new file mode 100644 index 0000000..38be596 --- /dev/null +++ b/coin/functions.go @@ -0,0 +1,130 @@ +package coin + +import ( + "encoding/json" + "log" + "math" + "os" +) + +func IsPairEnabled(coin string) bool { + for i := range Pairs { + if coin == i { + return true + } + } + return false +} + +func IsAmountFree(coin string, amount float64) bool { + + dir_entries, err := os.ReadDir("swaps/active") + if err != nil { + return false + } + + var swap_e Swap_Entry + + for _, e := range dir_entries { + file_data, err := os.ReadFile("swaps/active/" + e.Name()) + if err != nil { + return false + } + err = json.Unmarshal(file_data, &swap_e) + if err != nil || (swap_e.Price == amount && swap_e.Coin == coin) { + return false + } + } + + return true +} + +func (r *Swap) GetLockedBalance(coin string) float64 { + r.RLock() + defer r.RUnlock() + + switch coin { + case BTCDERO, LTCDERO, ARRRDERO: + return r.Dero_balance + case DEROLTC: + return r.LTC_balance + case DEROBTC: + return r.BTC_balance + case DEROARRR: + return r.ARRR_balance + default: + return 0 + } +} + +func (r *Swap) AddLockedBalance(coin string, amount float64) { + r.Lock() + defer r.Unlock() + + switch coin { + case BTCDERO, LTCDERO, ARRRDERO: + r.Dero_balance += amount + case DEROLTC: + r.LTC_balance += amount + case DEROBTC: + r.BTC_balance += amount + case DEROARRR: + r.ARRR_balance += amount + } +} + +func (r *Swap) RemoveLockedBalance(coin string, amount float64) { + r.Lock() + defer r.Unlock() + + switch coin { + case BTCDERO, LTCDERO, ARRRDERO: + r.Dero_balance -= amount + case DEROLTC: + r.LTC_balance -= amount + case DEROBTC: + r.BTC_balance -= amount + case DEROARRR: + r.ARRR_balance -= amount + } +} + +func (r *Swap) LoadLockedBalance() { + + dir_entries, err := os.ReadDir("swaps/active") + if err != nil { + ErrorCheckingOpenSwaps() + } + + var swap_e Swap_Entry + var amount float64 + + for _, e := range dir_entries { + file_data, err := os.ReadFile("swaps/active/" + e.Name()) + if err != nil { + ErrorCheckingOpenSwaps() + } + err = json.Unmarshal(file_data, &swap_e) + if err != nil { + ErrorCheckingOpenSwaps() + } + switch swap_e.Coin { + case LTCDERO, BTCDERO, ARRRDERO, XMRDERO: + amount += swap_e.Amount + r.AddLockedBalance(swap_e.Coin, swap_e.Amount) + default: + r.AddLockedBalance(swap_e.Coin, swap_e.Price) + } + } +} + +func ErrorCheckingOpenSwaps() { + log.Println("Can't check reserved amounts") + os.Exit(1) +} + +// round value to X decimal places +func RoundFloat(value float64, precision uint) float64 { + ratio := math.Pow(10, float64(precision)) + return math.Round(value*ratio) / ratio +} diff --git a/coin/rpc.go b/coin/rpc.go new file mode 100644 index 0000000..6fb3eb8 --- /dev/null +++ b/coin/rpc.go @@ -0,0 +1,146 @@ +package coin + +// RPC request / response +type ( + RPC_Request struct { + Jsonrpc string `json:"jsonrpc"` + Id string `json:"id"` + Method string `json:"method"` + Params []interface{} `json:"params"` + } + RPC_Response struct { + Error RPC_Error `json:"error"` + } + RPC_Send_Result struct { + Result string `json:"result"` + } + RPC_Error struct { + Code int `json:"code"` + Message string `json:"message"` + } +) + +// BTC/LTC related RPC structures +type ( + RPC_NewWallet_Response struct { + Result RPC_NewWallet `json:"result"` + Error RPC_Error `json:"error"` + } + RPC_NewWallet struct { + Name string `json:"name"` + Warning string `json:"warning"` + } + RPC_NewAddress_Response struct { + Result string `json:"result"` + Error RPC_Error `json:"error"` + } + RPC_GetBlockCount_Response struct { + Result uint64 `json:"result"` + Error RPC_Error `json:"error"` + } + RPC_ReceivedByAddress_Response struct { + Result float64 `json:"result"` + Error RPC_Error `json:"error"` + } + RPC_ListReceivedByAddress_Response struct { + Result []RPC_ListReceivedByAddress `json:"result"` + } + RPC_ListReceivedByAddress struct { + InvolvesWatchonly bool `json:"involvesWatchonly"` + Address string `json:"address"` + Amount float64 `json:"amount"` + Confirmations uint64 `json:"confirmations"` + Label string `json:"label"` + Txids []string `json:"txids"` + } + RPC_GetTransaction_Response struct { + Result RPC_GetTransaction `json:"result"` + } + RPC_GetTransaction struct { + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + Confirmations uint64 `json:"confirmations"` + Generated bool `json:"generated"` + Trusted bool `json:"trusted"` + Blockhash string `json:"blockhash"` + Blockheight uint64 `json:"blockheight"` + Blockindex uint64 `json:"blockindex"` + Blocktime uint64 `json:"blocktime"` + Txid string `json:"txid"` + Walletconflicts []string `json:"walletconflicts"` + Time uint64 `json:"time"` + TimeReceived uint64 `json:"timereceived"` + Comment string `json:"comment"` + Bip125Replaceable string `json:"bip125-replaceable"` + Detail []RPC_Details `json:"details"` + Hex string `json:"hex"` + } + RPC_GetBalance_Result struct { + Result float64 `json:"result"` + } + RPC_Details struct { + InvolvesWatchonly bool `json:"involvesWatchonly"` + Address string `json:"address"` + Category string `json:"category"` + Amount float64 `json:"amount"` + Label string `json:"label"` + Vout float64 `json:"vout"` + Fee float64 `json:"fee"` + Abandoned bool `json:"abandoned"` + } + RPC_Validate_Address struct { + IsValid bool `json:"isvalid"` + Address string `json:"address"` + ScriptPubKey string `json:"scriptPubKey"` + Isscript bool `json:"isscript"` + Iswitness bool `json:"iswitness"` + Witness_version float64 `json:"witness_version,omitempty"` + Witness_program string `json:"witness_program,omitempty"` + } + RPC_Validate_Address_Result struct { + Result RPC_Validate_Address `json:"result"` + } +) + +// ARRR related RPC structures +type ( + RPC_ARRR_ListReceivedByAddress_Response struct { + Result []RPC_ARRR_ListReceivedByAddress `json:"result"` + } + RPC_ARRR_ListReceivedByAddress_Details struct { + Type string `json:"type"` + Output uint64 `json:"output"` + Outgoing bool `json:"outgoing"` + Address string `json:"address"` + Value float64 `json:"value"` + ValueZat uint64 `json:"valueZat"` + Change bool `json:"change"` + Spendable bool `json:"spendable"` + Memo string `json:"memo"` + MemoStr string `json:"memoStr"` + } + RPC_ARRR_ListAddresses struct { + Result []string `json:"result"` + } + RPC_ARRR_ListReceivedByAddress struct { + Txid string `json:"txid"` + Coinbase bool `json:"coinbase"` + Category string `json:"category"` + BlockHeight uint64 `json:"blockHeight"` + Blockhash string `json:"blockhash"` + Blockindex uint64 `json:"blockindex"` + Blocktime uint64 `json:"blocktime"` + RawConfirmations uint64 `json:"rawconfirmations"` + Confirmations uint64 `json:"confirmations"` + Time uint64 `json:"time"` + ExpiryHeight uint64 `json:"expiryHeight"` + Size uint64 `json:"size"` + Fee uint64 `json:"fee"` + Received []RPC_ARRR_ListReceivedByAddress_Details `json:"received"` + } + RPC_ARRR_SendMany_Params struct { + Address string `json:"address"` + Amount float64 `json:"amount"` + Memo string `json:"memo,omitempty"` + } +) diff --git a/coin/variables.go b/coin/variables.go new file mode 100644 index 0000000..3b7871c --- /dev/null +++ b/coin/variables.go @@ -0,0 +1,52 @@ +package coin + +import ( + "net/http" + "sync" +) + +type ( + Swap_Request struct { + Pair string `json:"pair"` + Amount float64 `json:"amount"` + DeroAddr string `json:"dero_address"` + } + Swap_Response struct { + ID int64 `json:"id"` + Wallet string `json:"wallet,omitempty"` + Deposit float64 `json:"deposit,omitempty"` + Swap float64 `json:"swap,omitempty"` + Error string `json:"error,omitempty"` + Request Swap_Request `json:"request"` + } + Swap_Entry struct { + Coin string `json:"coin"` + Wallet string `json:"wallet"` + Destination string `json:"destination"` + Amount float64 `json:"amount"` + Price float64 `json:"price"` + Created int64 `json:"created"` + Block uint64 `json:"block"` + Balance float64 `json:"balance"` + Status uint64 `json:"status"` + } + Swap struct { + Dero_balance float64 + LTC_balance float64 + BTC_balance float64 + ARRR_balance float64 + XMR_balance float64 + sync.RWMutex + } +) + +var Locked Swap +var Supported_pairs = []string{BTCDERO, LTCDERO, ARRRDERO, XMRDERO, DEROBTC, DEROLTC, DEROARRR, DEROXMR} +var Pairs = make(map[string]bool) +var SimplePairs = make(map[string]bool) +var XTC_URL = make(map[string]string) +var XTC_Daemon = &http.Client{} +var XTC_auth string + +var BTC_address, LTC_address, ARRR_address, XMR_address string +var BTC_dir, LTC_dir, ARRR_dir string diff --git a/dero/daemon.go b/dero/daemon.go new file mode 100644 index 0000000..913049f --- /dev/null +++ b/dero/daemon.go @@ -0,0 +1,129 @@ +package dero + +import ( + "context" + "log" + "time" + + "github.com/deroproject/derohe/cryptography/crypto" + "github.com/deroproject/derohe/rpc" + "github.com/ybbus/jsonrpc/v3" +) + +var Dero_Daemon jsonrpc.RPCClient + +func IsDeroAddressRegistered(address string) bool { + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + if result, err := Dero_Daemon.Call(ctx, "DERO.IsRegistered", &rpc.GetEncryptedBalance_Params{Address: address, SCID: crypto.ZEROHASH}); err != nil { + cancel() + log.Printf("Error checking registration status: %v\n", err) + return false + } else { + cancel() + + var response rpc.GetEncryptedBalance_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error checking registration status: %v\n", err) + return false + } + + return response.Registration > 0 + } +} + +func CheckWalletBalance() (amount float64, err error) { + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + if result, err := Dero_Wallet.Call(ctx, "GetBalance"); err != nil { + cancel() + log.Printf("Error checking Dero wallet balance: %v\n", err) + return 0, err + } else { + cancel() + + var response rpc.GetBalance_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error checking Dero wallet balance: %v\n", err) + return 0, err + } + return float64(response.Balance) / 100000, nil + } +} + +func CheckBlockHeight() uint64 { + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + if result, err := Dero_Daemon.Call(ctx, "DERO.GetInfo"); err != nil { + cancel() + log.Printf("Error checking block height: %v\n", err) + return 0 + } else { + cancel() + + var response rpc.GetInfo_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error checking block height: %v\n", err) + return 0 + } + return uint64(response.Height) + } +} + +func DEROCheckTX(txid string) (valid bool, err error) { + + log.Println("Checking DERO transaction") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + if result, err := Dero_Daemon.Call(ctx, "DERO.GetTransaction", &rpc.GetTransaction_Params{Tx_Hashes: []string{txid}}); err != nil { + cancel() + log.Printf("Error getting transaction: %v\n", err) + return false, err + } else { + cancel() + + var response rpc.GetTransaction_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error getting transaction: %v\n", err) + return false, err + } + + if response.Txs[0].ValidBlock == "" || response.Txs == nil { + return false, nil + } else { + return true, nil + } + } +} + +func CheckAddress(name string) string { + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + if result, err := Dero_Daemon.Call(ctx, "DERO.NameToAddress", &rpc.NameToAddress_Params{Name: name, TopoHeight: -1}); err != nil { + cancel() + log.Printf("Error checking name: %v\n", err) + return "" + } else { + cancel() + + var response rpc.NameToAddress_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error checking name: %v\n", err) + return "" + } + if response.Status == "OK" { + return response.Address + } else { + return "" + } + } +} diff --git a/dero/wallet.go b/dero/wallet.go new file mode 100644 index 0000000..284b1a9 --- /dev/null +++ b/dero/wallet.go @@ -0,0 +1,192 @@ +package dero + +import ( + "context" + "log" + "time" + + "dero-swap/coin" + + "github.com/deroproject/derohe/cryptography/crypto" + "github.com/deroproject/derohe/rpc" + "github.com/ybbus/jsonrpc/v3" +) + +const atomicUnits float64 = 100000 +const TxFee float64 = 0.0004 + +var Dero_Wallet jsonrpc.RPCClient + +func AddTX(wallet string, amount float64) rpc.Transfer { + return rpc.Transfer{SCID: crypto.ZEROHASH, Destination: wallet, Amount: uint64(amount * atomicUnits)} +} + +func GetHeight() uint64 { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Dero_Wallet.Call(ctx, "getheight") + cancel() + + if err != nil { + return 0 + } + + var response rpc.GetHeight_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error getting DERO wallet height: %v\n", err) + return 0 + } + + return response.Height +} + +func GetBalance() float64 { + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Dero_Wallet.Call(ctx, "getbalance") + cancel() + + if err != nil { + return 0 + } + + var response rpc.GetBalance_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error getting DERO wallet balance: %v\n", err) + return 0 + } + + balance_fl := float64(response.Balance) / atomicUnits + balance_fl -= coin.Locked.GetLockedBalance(coin.BTCDERO) + + return coin.RoundFloat(balance_fl, 5) +} + +func Payout(tx []rpc.Transfer) { + + var tries uint + var reserve float64 + + for _, e := range tx { + reserve += float64(e.Amount) / atomicUnits + } + + for { + time.Sleep(time.Second * 18) + + tries++ + if tries > 3 { + log.Println("Too many failures, manual check necessary!") + break + } + + log.Printf("Sending DERO transaction try #%d\n", tries) + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + if result, err := Dero_Wallet.Call(ctx, "Transfer", &rpc.Transfer_Params{Ringsize: 2, Fees: 40, Transfers: tx}); err != nil { + cancel() + log.Printf("Error sending DERO transaction: %v\n", err) + continue + } else { + cancel() + + var response rpc.Transfer_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error sending DERO transaction: %v\n", err) + continue + } + if response.TXID == "" { + continue + } + + var init_block, current_block uint64 + var retry bool + + for init_block == 0 { + init_block = CheckBlockHeight() + time.Sleep(time.Second * 3) + } + current_block = init_block + + retry = true + for current_block-init_block <= 12 { + ok, err := DEROCheckTX(response.TXID) + if err != nil { + time.Sleep(time.Second * 3) + continue + } + if ok { + log.Printf("DERO transaction (TXID %s) successfully sent\n", response.TXID) + coin.Locked.RemoveLockedBalance(coin.BTCDERO, reserve) + retry = false + break + } + current_block = 0 + for current_block == 0 { + current_block = CheckBlockHeight() + time.Sleep(time.Second * 3) + } + time.Sleep(time.Second * 15) + } + if retry { + continue + } + break + } + } +} + +func CheckIncomingTransfers(dstPort uint64, block uint64) bool { + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Dero_Wallet.Call(ctx, "GetTransfers", &rpc.Get_Transfers_Params{In: true, Min_Height: block}) + cancel() + if err != nil { + log.Printf("Error sending DERO request: %v\n", err) + return false + } + + var response rpc.Get_Transfers_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error retrieving DERO response: %v\n", err) + return false + } + + for _, e := range response.Entries { + if e.DestinationPort == dstPort { + return true + } + } + + return false +} + +func MakeIntegratedAddress(sessionID int64) string { + + var payload = rpc.Arguments{rpc.Argument{Name: "D", DataType: "U", Value: uint64(sessionID)}} + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + result, err := Dero_Wallet.Call(ctx, "MakeIntegratedAddress", &rpc.Make_Integrated_Address_Params{Payload_RPC: payload}) + cancel() + if err != nil { + log.Printf("Error sending DERO request: %v\n", err) + return "" + } + + var response rpc.Make_Integrated_Address_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error retrieving DERO response: %v\n", err) + return "" + } + + return response.Integrated_Address +} diff --git a/extras.go b/extras.go new file mode 100644 index 0000000..6399954 --- /dev/null +++ b/extras.go @@ -0,0 +1,50 @@ +package main + +import ( + "sync" + "time" +) + +type backoff struct { + userlist map[string]int64 + sync.Mutex +} + +var Delay = backoff{userlist: make(map[string]int64)} + +func (u *backoff) AddUser(address string) { + + u.Lock() + defer u.Unlock() + + u.userlist[address] = time.Now().UnixMilli() +} + +func (u *backoff) DelUser(address string) { + delete(u.userlist, address) +} + +func (u *backoff) CheckUser(address string) bool { + + u.Lock() + defer u.Unlock() + + if _, ok := u.userlist[address]; ok { + return true + } else { + return false + } +} + +func (u *backoff) CheckBackoff() { + + u.Lock() + defer u.Unlock() + + for i, e := range u.userlist { + added := time.UnixMilli(e) + if time.Since(added) >= time.Minute*2 { + Delay.DelUser(i) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1bd7513 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module dero-swap + +go 1.21.5 + +require ( + github.com/deroproject/derohe v0.0.0-20240215152352-a5a0e6a68ada + github.com/lesismal/llib v1.1.12 + github.com/lesismal/nbio v1.5.3 + github.com/robfig/cron/v3 v3.0.1 + github.com/ybbus/jsonrpc/v3 v3.1.5 +) + +require ( + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/caarlos0/env/v6 v6.10.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deroproject/graviton v0.0.0-20220130070622-2c248a53b2e1 // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..997c10c --- /dev/null +++ b/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "log" + "os" + + "dero-swap/cfg" + "dero-swap/coin" + + "github.com/robfig/cron/v3" +) + +func Init() { + + // create all swap directories + os.MkdirAll("swaps/active", 0755) + os.MkdirAll("swaps/expired", 0755) + os.MkdirAll("swaps/done", 0755) +} + +func main() { + + Init() + cfg.LoadConfig() + if !cfg.CheckConfig() { + log.Println("Configuration error. Please check config file!") + os.Exit(1) + } + cfg.LoadWallets() + + coin.Locked.LoadLockedBalance() + + UpdateMarkets() + + c := cron.New() + c.AddFunc("@every 1m", UpdateMarkets) + c.AddFunc("@every 2m", Delay.CheckBackoff) + c.Start() + + go Swap_Controller() + StartServer() +} diff --git a/monero/rpc.go b/monero/rpc.go new file mode 100644 index 0000000..e02b710 --- /dev/null +++ b/monero/rpc.go @@ -0,0 +1,113 @@ +package monero + +// XMR related RPC structures +type ( + RPC_XMR_Height struct { + Height uint64 `json:"height"` + } + RPC_XMR_GetBalance_Params struct { + AccountIndex uint64 `json:"account_index"` + AddressIndices []uint64 `json:"address_indices,omitempty"` + AllAccounts bool `json:"all_accounts"` + Strict bool `json:"strict"` + } + RPC_XMR_GetBalance_Result struct { + Balance uint64 `json:"balance"` + UnlockedBalance uint64 `json:"unlocked_balance"` + TimeToUnlock uint64 `json:"time_to_unlock"` + BlocksToUnlock uint64 `json:"blocks_to_unlock"` + PerSubaddress []RPC_XMR_Subaddress_Info `json:"per_subaddress"` + } + RPC_XMR_GetAddress_Params struct { + AccountIndex uint64 `json:"account_index"` + AddressIndex []uint64 `json:"address_index,omitempty"` + } + RPC_XMR_GetAddress_Result struct { + Address string `json:"address"` + Addresses []RPC_XMR_Addresses `json:"addresses"` + } + RPC_XMR_Addresses struct { + Address string `json:"address"` + Label string `json:"label"` + AddressIndex uint64 `json:"address_index"` + Used bool `json:"used"` + } + RPC_XMR_Subaddress_Info struct { + AccountIndex uint64 `json:"account_index"` + AddressIndices []uint64 `json:"address_indices,omitempty"` + Address string `json:"address"` + Balance uint64 `json:"balance"` + UnlockedBalance uint64 `json:"unlocked_balance"` + Label string `json:"label"` + NumUnspentOutputs uint64 `json:"num_unspent_outputs"` + TimeToUnlock uint64 `json:"time_to_unlock"` + BlocksToUnlock uint64 `json:"blocks_to_unlock"` + } + RPC_XMR_IntegratedAddress_Result struct { + IntegratedAddress string `json:"integrated_address"` + PaymentID string `json:"payment_id"` + } + RPC_XMR_SplitIntegratedAddress_Params struct { + IntegratedAddress string `json:"integrated_address"` + } + RPC_XMR_SplitIntegratedAddress_Result struct { + StandardAddress string `json:"standard_address"` + PaymentID string `json:"payment_id"` + IsSubaddress bool `json:"is_subaddress"` + } + RPC_XMR_Validate_Address_Params struct { + Address string `json:"address"` + AnyNetType bool `json:"any_net_type"` + Allow_Openalias bool `json:"allow_openalias"` + } + RPC_XMR_Validate_Address_Result struct { + Valid bool `json:"valid"` + Integrated bool `json:"integrated"` + Subaddress bool `json:"subaddress"` + Nettype string `json:"nettype"` + OpenaliasAddress string `json:"openalias_address"` + } + RPC_XMR_Transfer_Params struct { + Address string `json:"address"` + Amount uint64 `json:"amount"` + } + RPC_XMR_Transfer struct { + Destinations []RPC_XMR_Transfer_Params `json:"destinations"` + AccountIndex uint64 `json:"account_index"` + SubaddrIndices []uint64 `json:"subaddr_indices"` + Ringsize uint64 `json:"ring_size"` + UnlockTime uint64 `json:"unlock_time"` + PaymentID string `json:"payment_id"` + GetTXKey bool `json:"get_tx_keys"` + Priority uint64 `json:"priority"` + DoNotRelay bool `json:"do_not_relay"` + GetTXHash bool `json:"get_tx_hash"` + GetTxMetadata bool `json:"get_tx_metadata"` + } + RPC_XMR_Transfer_Result struct { + Amount uint64 `json:"amount"` + Fee uint64 `json:"fee"` + MultiSig_TxSet string `json:"multisig_txset"` + TxBlob string `json:"tx_blob"` + TxHash string `json:"tx_hash"` + TxKey string `json:"tx_key"` + TxMetadata string `json:"tx_metadata"` + Unsigned_TxSet string `json:"unsigned_txset"` + } + RPC_XMR_Payments struct { + Address string `json:"address"` + Amount uint64 `json:"amount"` + BlockHeight uint64 `json:"block_height"` + PaymentID string `json:"payment_id"` + TxHash string `json:"tx_hash"` + UnlockTime uint64 `json:"unlock_time"` + Locked bool `json:"locked"` + } + RPC_XMR_GetPayments_Result struct { + Payments []RPC_XMR_Payments `json:"payments"` + } + RPC_XMR_BulkTX_Params struct { + Payment_IDs []string `json:"payment_ids"` + MinBlockHeight uint64 `json:"min_block_height"` + } +) diff --git a/monero/wallet.go b/monero/wallet.go new file mode 100644 index 0000000..bfff56f --- /dev/null +++ b/monero/wallet.go @@ -0,0 +1,193 @@ +package monero + +import ( + "context" + "log" + "time" + + "github.com/ybbus/jsonrpc/v3" +) + +const atomicUnits float64 = 1000000000000 + +var Monero_Wallet jsonrpc.RPCClient + +func GetHeight() uint64 { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Monero_Wallet.Call(ctx, "get_height") + cancel() + + if err != nil { + return 0 + } + + var response RPC_XMR_Height + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error checking XMR wallet height: %v\n", err) + return 0 + } + + return response.Height +} + +func XMRGetTX(payment string, block uint64) bool { + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Monero_Wallet.Call(ctx, "get_bulk_payments", RPC_XMR_BulkTX_Params{MinBlockHeight: block, Payment_IDs: []string{payment}}) + cancel() + + if err != nil { + return false + } + + var response RPC_XMR_GetPayments_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error getting XMR incoming payments: %v\n", err) + return false + } + + // placeholder + for _, p := range response.Payments { + if p.UnlockTime > 0 { + return false + } else { + return true + } + } + + return false +} + +func XMRSend(transfers []RPC_XMR_Transfer_Params) (ok bool, txid string) { + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Monero_Wallet.Call(ctx, "transfer", RPC_XMR_Transfer{Destinations: transfers, Priority: 0, Ringsize: 16}) + cancel() + + if err != nil { + return false, "" + } + + var response RPC_XMR_Transfer_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error sending XMR transaction: %v\n", err) + return false, "" + } + + if response.TxHash != "" { + return true, response.TxHash + } + return false, "" +} + +func GetBalance() float64 { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Monero_Wallet.Call(ctx, "get_balance", RPC_XMR_GetBalance_Params{AccountIndex: 0}) + cancel() + + if err != nil { + return 0 + } + + var response RPC_XMR_GetBalance_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error getting XMR wallet balance: %v\n", err) + return 0 + } + + return float64(response.UnlockedBalance) / atomicUnits +} + +func GetAddress() string { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Monero_Wallet.Call(ctx, "get_address", RPC_XMR_GetAddress_Params{AccountIndex: 0}) + cancel() + + if err != nil { + return "" + } + + var response RPC_XMR_GetAddress_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error getting XMR wallet address: %v\n", err) + return "" + } + + return response.Address +} + +func MakeIntegratedAddress() string { + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Monero_Wallet.Call(ctx, "make_integrated_address") + cancel() + + if err != nil { + return "" + } + + var response RPC_XMR_IntegratedAddress_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error generating XMR integrated address: %v\n", err) + return "" + } + + return response.IntegratedAddress +} + +func SplitIntegratedAddress(address string) string { + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Monero_Wallet.Call(ctx, "split_integrated_address", RPC_XMR_SplitIntegratedAddress_Params{IntegratedAddress: address}) + cancel() + + if err != nil { + return "" + } + + var response RPC_XMR_SplitIntegratedAddress_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error splitting XMR integrated address: %v\n", err) + return "" + } + + return response.PaymentID +} + +func ValidateAddress(address string) bool { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + result, err := Monero_Wallet.Call(ctx, "validate_address", RPC_XMR_Validate_Address_Params{Address: address}) + cancel() + + if err != nil { + return false + } + + var response RPC_XMR_Validate_Address_Result + + err = result.GetObject(&response) + if err != nil { + log.Printf("Error validating XMR address: %v\n", err) + return false + } + + return response.Valid +} + +func AddTX(wallet string, amount float64) RPC_XMR_Transfer_Params { + return RPC_XMR_Transfer_Params{Address: wallet, Amount: uint64(amount * atomicUnits)} +} diff --git a/price.go b/price.go new file mode 100644 index 0000000..3f74333 --- /dev/null +++ b/price.go @@ -0,0 +1,303 @@ +package main + +import ( + "bytes" + "dero-swap/cfg" + "dero-swap/coin" + "dero-swap/dero" + "dero-swap/monero" + "encoding/json" + "io" + "log" + "net/http" + "strconv" + "time" +) + +func GetMarket(pair string, provider int) (prices Swap_Price) { + + resp, err := http.Get(pair) + if err != nil { + log.Printf("Market: HTTP Get: %v\n", err) + return Swap_Price{} + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Market: Read Body: %v\n", err) + return Swap_Price{} + } + + var market Price_Provider + + if err := json.Unmarshal(body, &market); err != nil { + log.Printf("Market: Cannot unmarshal data: %v\n", err) + return Swap_Price{} + } + + switch provider { + case TO: + prices.Ask, err = strconv.ParseFloat(market.Ask, 64) + prices.Bid, err = strconv.ParseFloat(market.Bid, 64) + prices.Median, err = strconv.ParseFloat(market.Price, 64) + case XEGGEX: + prices.Ask, err = strconv.ParseFloat(market.BestAsk, 64) + prices.Bid, err = strconv.ParseFloat(market.BestBid, 64) + prices.Median, err = strconv.ParseFloat(market.LastPrice, 64) + } + if err != nil { + log.Printf("Market: Cannot convert string: %v\n", err) + return + } + + return +} + +// Get Pair values +// TODO: configurable fees; clean-up +func GetPrice(pair string) (bid float64, ask float64) { + + var base, base_usd Swap_Price + var atomicUnits uint = 8 + var simple bool + + switch pair { + case coin.BTCDERO, coin.DEROBTC: + simple = true + for i := range DERO_BTC { + if base = GetMarket(DERO_BTC[i], i); base.Ask > 0 && base.Bid > 0 { + break + } + } + case coin.LTCDERO, coin.DEROLTC: + for i := range LTC_USDT { + if base = GetMarket(LTC_USDT[i], i); base.Ask > 0 && base.Bid > 0 { + break + } + } + case coin.XMRDERO, coin.DEROXMR: + atomicUnits = 12 + for i := range XMR_USDT { + if base = GetMarket(XMR_USDT[i], i); base.Ask > 0 && base.Bid > 0 { + break + } + } + case coin.ARRRDERO, coin.DEROARRR: + base = GetMarket(ARRR_USDT, TO) + } + + if base.Ask == 0 || base.Bid == 0 { + return 0, 0 + } + + if simple { + bid = base.Bid - (base.Bid * cfg.SwapFees.Swap.Bid / 100) + bid = coin.RoundFloat(bid, atomicUnits) + ask = base.Ask + (base.Ask * cfg.SwapFees.Swap.Ask / 100) + ask = coin.RoundFloat(ask, atomicUnits) + return + } + + for i := range DERO_USDT { + if base_usd = GetMarket(DERO_USDT[i], i); base_usd.Ask > 0 && base_usd.Bid > 0 { + break + } + } + if base_usd.Ask == 0 || base_usd.Bid == 0 { + return 0, 0 + } + + bid = base_usd.Bid - (base_usd.Bid * cfg.SwapFees.Swap.Bid / 100) + ask = base_usd.Ask + (base_usd.Ask * cfg.SwapFees.Swap.Ask / 100) + + bid = coin.RoundFloat(bid/base.Bid, atomicUnits) + ask = coin.RoundFloat(ask/base.Ask, atomicUnits) + + return +} + +// TODO: simplify +func UpdateMarkets() { + + var btc, ltc, xmr, arrr float64 + var derobtc, deroltc, deroxmr, deroarrr float64 + + for p := range coin.SimplePairs { + switch p { + case coin.XMRDERO, coin.DEROXMR: + deroxmr, xmr = GetPrice(p) + case coin.ARRRDERO, coin.DEROARRR: + deroarrr, arrr = GetPrice(p) + case coin.LTCDERO, coin.DEROLTC: + deroltc, ltc = GetPrice(p) + case coin.BTCDERO, coin.DEROBTC: + derobtc, btc = GetPrice(p) + } + } + + // sometimes TradeOgre's BID/ASK values are swapped + if derobtc > 0 && btc > 0 && derobtc > btc { + swap := btc + btc = derobtc + derobtc = swap + } + if deroltc > 0 && ltc > 0 && deroltc > ltc { + swap := ltc + ltc = deroltc + deroltc = swap + } + if deroarrr > 0 && arrr > 0 && deroarrr > arrr { + swap := arrr + arrr = deroarrr + deroarrr = swap + } + if deroxmr > 0 && xmr > 0 && deroxmr > xmr { + swap := xmr + xmr = deroxmr + deroxmr = swap + } + + mk.Lock() + defer mk.Unlock() + + // TODO: simplify + if btc > 0 { + mk.Pairs.BTC = btc + mk.Update[coin.BTCDERO] = time.Now().UnixMilli() + IsPairAvailable[coin.BTCDERO] = true + } else { + t := time.UnixMilli(mk.Update[coin.BTCDERO]) + if time.Since(t) > time.Minute*2 { + IsPairAvailable[coin.BTCDERO] = false + log.Println("BTC->DERO disabled") + } + } + if ltc > 0 { + mk.Pairs.LTC = ltc + mk.Update[coin.LTCDERO] = time.Now().UnixMilli() + IsPairAvailable[coin.LTCDERO] = true + } else { + t := time.UnixMilli(mk.Update[coin.LTCDERO]) + if time.Since(t) > time.Minute*2 { + IsPairAvailable[coin.LTCDERO] = false + log.Println("LTC->DERO disabled") + } + } + if arrr > 0 { + mk.Pairs.ARRR = arrr + mk.Update[coin.ARRRDERO] = time.Now().UnixMilli() + IsPairAvailable[coin.ARRRDERO] = true + } else { + t := time.UnixMilli(mk.Update[coin.ARRRDERO]) + if time.Since(t) > time.Minute*2 { + IsPairAvailable[coin.ARRRDERO] = false + log.Println("ARRR->DERO disabled") + } + } + if xmr > 0 { + mk.Pairs.XMR = xmr + mk.Update[coin.XMRDERO] = time.Now().UnixMilli() + IsPairAvailable[coin.XMRDERO] = true + } else { + t := time.UnixMilli(mk.Update[coin.XMRDERO]) + if time.Since(t) > time.Minute*2 { + IsPairAvailable[coin.XMRDERO] = false + log.Println("XMR->DERO disabled") + } + } + if deroltc > 0 { + mk.Pairs.DEROLTC = deroltc + mk.Update[coin.DEROLTC] = time.Now().UnixMilli() + IsPairAvailable[coin.DEROLTC] = true + } else { + t := time.UnixMilli(mk.Update[coin.DEROLTC]) + if time.Since(t) > time.Minute*2 { + IsPairAvailable[coin.DEROLTC] = false + log.Println("DERO->LTC disabled") + } + } + if derobtc > 0 { + mk.Pairs.DEROBTC = derobtc + mk.Update[coin.DEROBTC] = time.Now().UnixMilli() + IsPairAvailable[coin.DEROBTC] = true + } else { + t := time.UnixMilli(mk.Update[coin.DEROBTC]) + if time.Since(t) > time.Minute*2 { + IsPairAvailable[coin.DEROBTC] = false + log.Println("DERO->BTC disabled") + } + } + if deroarrr > 0 { + mk.Pairs.DEROARRR = deroarrr + mk.Update[coin.DEROARRR] = time.Now().UnixMilli() + IsPairAvailable[coin.DEROARRR] = true + } else { + t := time.UnixMilli(mk.Update[coin.DEROARRR]) + if time.Since(t) > time.Minute*2 { + IsPairAvailable[coin.DEROARRR] = false + log.Println("DERO->ARRR disabled") + } + } + if deroxmr > 0 { + mk.Pairs.DEROXMR = deroxmr + mk.Update[coin.DEROXMR] = time.Now().UnixMilli() + IsPairAvailable[coin.DEROXMR] = true + } else { + t := time.UnixMilli(mk.Update[coin.DEROXMR]) + if time.Since(t) > time.Minute*2 { + IsPairAvailable[coin.DEROXMR] = false + log.Println("DERO->XMR disabled") + } + } + + var data bytes.Buffer + var out WS_Message + + out.Method = "market" + out.Result = mk.Pairs + encoder := json.NewEncoder(&data) + + err := encoder.Encode(out) + if err != nil { + log.Printf("Market: %v\n", err) + return + } + SendWSData(nil, data.Bytes()) + + data.Reset() + out.Method = "balance" + out.Result = UpdatePool() + err = encoder.Encode(out) + if err != nil { + log.Printf("Balance: %v\n", err) + return + } + SendWSData(nil, data.Bytes()) +} + +// TODO: reduce function calls +func UpdatePool() Swap_Balance { + + lock.Lock() + defer lock.Unlock() + + var balance Swap_Balance + + balance.Dero = dero.GetBalance() + + for p := range coin.SimplePairs { + switch p { + case coin.XMRDERO, coin.DEROXMR: + balance.XMR = monero.GetBalance() + case coin.ARRRDERO, coin.DEROARRR: + balance.ARRR = coin.XTCGetBalance(p) + case coin.LTCDERO, coin.DEROLTC: + balance.LTC = coin.XTCGetBalance(p) + case coin.BTCDERO, coin.DEROBTC: + balance.BTC = coin.XTCGetBalance(p) + } + } + + return balance +} diff --git a/price_variables.go b/price_variables.go new file mode 100644 index 0000000..5e28f69 --- /dev/null +++ b/price_variables.go @@ -0,0 +1,73 @@ +package main + +import "sync" + +type ( + Price_Provider struct { + // TradeOgre fields below + Success bool `json:"success"` + Init string `json:"initialprice"` + Price string `json:"price"` + High string `json:"high"` + Low string `json:"low"` + Volume string `json:"volume"` + Bid string `json:"bid"` + Ask string `json:"ask"` + // Xeggex fields below + BestAsk string `json:"bestAsk"` + BestBid string `json:"bestBid"` + LastPrice string `json:"lastPrice"` + } + Swap_Price struct { + Ask float64 + Bid float64 + Median float64 + } + Swap_Markets struct { + BTC float64 `json:"btcdero,omitempty"` + LTC float64 `json:"ltcdero,omitempty"` + ARRR float64 `json:"arrrdero,omitempty"` + XMR float64 `json:"xmrdero,omitempty"` + DEROLTC float64 `json:"deroltc,omitempty"` + DEROBTC float64 `json:"derobtc,omitempty"` + DEROARRR float64 `json:"deroarrr,omitempty"` + DEROXMR float64 `json:"deroxmr,omitempty"` + } + Swap_Balance struct { + Dero float64 `json:"dero"` + LTC float64 `json:"ltc,omitempty"` + BTC float64 `json:"btc,omitempty"` + ARRR float64 `json:"arrr,omitempty"` + XMR float64 `json:"xmr,omitempty"` + } +) + +const ( + ASK = iota + BID = iota + MEDIAN = iota +) +const ( + TO = iota + XEGGEX = iota +) +const SATOSHI float64 = 1e-08 + +type MarketData struct { + Pairs Swap_Markets + Update map[string]int64 + sync.RWMutex +} + +var mk = &MarketData{Update: make(map[string]int64)} +var IsPairAvailable = make(map[string]bool) +var lock sync.Mutex + +// Price URLs +var DERO_USDT = []string{"https://tradeogre.com/api/v1/ticker/DERO-USDT", "https://api.xeggex.com/api/v2/market/getbysymbol/DERO/USDT"} +var DERO_BTC = []string{"https://tradeogre.com/api/v1/ticker/DERO-BTC", "https://api.xeggex.com/api/v2/market/getbysymbol/DERO/BTC"} +var LTC_USDT = []string{"https://tradeogre.com/api/v1/ticker/LTC-USDT", "https://api.xeggex.com/api/v2/market/getbysymbol/LTC/USDT"} +var XMR_USDT = []string{"https://tradeogre.com/api/v1/ticker/XMR-USDT", "https://api.xeggex.com/api/v2/market/getbysymbol/XMR/USDT"} +var ARRR_USDT = "https://tradeogre.com/api/v1/ticker/ARRR-USDT" + +type ORDER int diff --git a/server.go b/server.go new file mode 100644 index 0000000..5253b2b --- /dev/null +++ b/server.go @@ -0,0 +1,275 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "sync" + + "github.com/lesismal/llib/std/crypto/tls" + + "github.com/deroproject/derohe/globals" + "github.com/lesismal/nbio/nbhttp" + "github.com/lesismal/nbio/nbhttp/websocket" + + "dero-swap/cfg" + "dero-swap/coin" + "dero-swap/dero" +) + +type WS_Message struct { + ID uint64 `json:"id"` + Method string `json:"method"` + Params any `json:"params,omitempty"` + Result any `json:"result"` +} + +var WSConnections sync.Map + +// swap other coins to Dero +func Dero_Swap(request coin.Swap_Request) (response coin.Swap_Response) { + + var err error + + // check if destination wallet is valid. Registered usernames can also be used. + if strings.HasPrefix(request.DeroAddr, "dero1") || strings.HasPrefix(request.DeroAddr, "deroi") { + _, err = globals.ParseValidateAddress(request.DeroAddr) + } else { + if addr := dero.CheckAddress(request.DeroAddr); addr != "" { + request.DeroAddr = addr + } else { + err = fmt.Errorf("invalid address") + } + } + + // basic checks + if request.Amount == 0 || err != nil { + response.Error = "invalid request" + return + } + + // prevent users from creating too many swap requests + if Delay.CheckUser(request.DeroAddr) { + response.Error = "2 minutes wait time triggered" + return + } + + // check if pair is enabled and available + pair := request.Pair + if !coin.IsPairEnabled(pair) || !IsPairAvailable[pair] { + response.Error = fmt.Sprintf("%s swap currently not possible", pair) + return + } + + // create swap + err = XTCSwap(pair, request.DeroAddr, coin.RoundFloat(request.Amount, 5), &response) + + if err != nil { + response.Error = err.Error() + log.Println(err) + } else { + Delay.AddUser(request.DeroAddr) + } + response.Request = request + + return response +} + +// swap Dero to other coins +func Reverse_Swap(request coin.Swap_Request) (response coin.Swap_Response) { + + var err error + + // prevent users from creating too many swap requests + if Delay.CheckUser(request.DeroAddr) { + response.Error = "2 minutes wait time triggered" + return + } + + // check if pair is enabled and available + pair := request.Pair + if !coin.IsPairEnabled(pair) || !IsPairAvailable[pair] { + response.Error = fmt.Sprintf("%s swap currently not possible", pair) + return + } + + response.Deposit = coin.RoundFloat(request.Amount, 5) + + // create swap + err = DeroXTCSwap(pair, request.DeroAddr, response.Deposit, &response) + + if err != nil { + response.Error = err.Error() + log.Println(err) + } else { + Delay.AddUser(request.DeroAddr) + } + response.Request = request + + return response +} + +func newUpgrader() *websocket.Upgrader { + u := websocket.NewUpgrader() + + u.CheckOrigin = (func(r *http.Request) bool { + return true + }) + + u.OnClose(func(c *websocket.Conn, err error) { + WSConnections.Delete(c) + }) + + u.OnMessage(func(c *websocket.Conn, messageType websocket.MessageType, data []byte) { + + var in, out WS_Message + var send bytes.Buffer + + read := bytes.NewReader(data) + decoder := json.NewDecoder(read) + encoder := json.NewEncoder(&send) + + err := decoder.Decode(&in) + if err != nil { + fmt.Println(err) + return + } + + out.ID = in.ID + + switch in.Method { + case "swap": + + var request coin.Swap_Request + var response coin.Swap_Response + + p := in.Params.(map[string]any) + + request.Pair = p["pair"].(string) + request.Amount = p["amount"].(float64) + request.DeroAddr = p["dero_address"].(string) + + switch request.Pair { + case coin.BTCDERO, coin.LTCDERO, coin.ARRRDERO, coin.XMRDERO: + response = Dero_Swap(request) + case coin.DEROBTC, coin.DEROLTC, coin.DEROARRR, coin.DEROXMR: + response = Reverse_Swap(request) + default: + } + + out.Method = "swap" + out.Result = response + encoder.Encode(out) + + SendWSData(c, send.Bytes()) + case "market": + + mk.RLock() + defer mk.RUnlock() + + var data bytes.Buffer + + out.Method = "market" + inner := mk.Pairs + out.Result = inner + + encoder := json.NewEncoder(&data) + encoder.Encode(out) + + SendWSData(c, data.Bytes()) + + case "balance": + + var data bytes.Buffer + + out.Method = "balance" + out.Result = UpdatePool() + + encoder := json.NewEncoder(&data) + encoder.Encode(out) + + SendWSData(c, data.Bytes()) + + default: + } + + }) + + return u +} + +func webSocketHandler(w http.ResponseWriter, r *http.Request) { + + upgrader := newUpgrader() + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + WSConnections.Store(conn, true) +} + +func SendWSData(c *websocket.Conn, data []byte) { + + if len(data) == 0 { + return + } + + if c != nil { + err := c.WriteMessage(websocket.TextMessage, data) + if err != nil { + log.Println(err) + } + } else { + WSConnections.Range(func(k any, v any) bool { + + conn := k.(*websocket.Conn) + + err := conn.WriteMessage(websocket.TextMessage, data) + if err != nil { + log.Println(err) + } + + return true + }) + } +} + +func StartServer() { + + // cert files + // Let's Encrypt: + // certFile = fullchain.pem + // keyFile = privkey.pem + + // comment the following block to disable TLS + cert, err := tls.LoadX509KeyPair("/etc/letsencrypt/live/mg25ot4aggt8dprv.myfritz.net/fullchain.pem", "/etc/letsencrypt/live/mg25ot4aggt8dprv.myfritz.net/privkey.pem") + if err != nil { + log.Println(err) + os.Exit(2) + } + + mux := http.NewServeMux() + mux.HandleFunc("/ws", webSocketHandler) + + srv := nbhttp.NewServer(nbhttp.Config{ + Network: "tcp", + Handler: mux, + // comment the following 2 lines and uncomment "Addrs" to start server without TLS + AddrsTLS: []string{cfg.Settings.ListenAddress}, + TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, + //Addrs: []string{cfg.Settings.ListenAddress}, + }) + + err = srv.Start() + if err != nil { + log.Println(err) + os.Exit(2) + } + srv.Wait() +} diff --git a/swap.go b/swap.go new file mode 100644 index 0000000..6f1e750 --- /dev/null +++ b/swap.go @@ -0,0 +1,204 @@ +package main + +import ( + "dero-swap/cfg" + "dero-swap/coin" + "dero-swap/dero" + "dero-swap/monero" + "encoding/json" + "fmt" + "log" + "os" + "time" +) + +func CreateSwap(pair string, wallet string, dest string, amount float64, price float64) int64 { + + var entry coin.Swap_Entry + var height uint64 + var payout float64 = amount + + // get current block height. Ignore transactions < height + switch pair { + case coin.BTCDERO, coin.LTCDERO, coin.ARRRDERO: + height = coin.XTCCheckBlockHeight(pair) + case coin.XMRDERO: + height = monero.GetHeight() + default: + height = dero.CheckBlockHeight() + } + + if height == 0 { + return 0 + } + + entry.Coin = pair + entry.Wallet = wallet + entry.Destination = dest + entry.Price = price + entry.Amount = amount + entry.Created = time.Now().UnixMilli() + entry.Block = height + entry.Status = 0 + + // create an integrated address for all Dero -> X swaps + if pair == coin.DEROLTC || pair == coin.DEROBTC || pair == coin.DEROARRR || pair == coin.DEROXMR { + if entry.Wallet = dero.MakeIntegratedAddress(entry.Created); entry.Wallet == "" { + return 0 + } + payout = entry.Price + } + + json_bytes, err := json.Marshal(&entry) + if err != nil { + return 0 + } + + err = os.WriteFile(fmt.Sprintf("swaps/active/%d", entry.Created), json_bytes, 0644) + if err != nil { + return 0 + } + + log.Printf("Swap request (%d) of %.8f (%s) successfully created\n", entry.Created, payout, entry.Coin) + + return entry.Created +} + +// X to Dero Swaps +func XTCSwap(pair string, dst_addr string, amount float64, resp *coin.Swap_Response) (err error) { + + // check if Dero address is registered on chain + if !dero.IsDeroAddressRegistered(dst_addr) { + return fmt.Errorf("dero address is not registered") + } + + // check balance and include locked swap balance + balance, err := dero.CheckWalletBalance() + if err != nil { + return fmt.Errorf("couldn't check swap balance") + } + if coin.Locked.GetLockedBalance(pair)+amount+dero.TxFee > balance { + return fmt.Errorf("insufficient swap balance") + } + + coin.Locked.AddLockedBalance(pair, amount) + + // create/get a deposit address + if pair != coin.XMRDERO { + resp.Wallet = coin.XTCGetAddress(pair) + } else { + resp.Wallet = monero.MakeIntegratedAddress() + } + if resp.Wallet == "" { + return fmt.Errorf("no swap deposit address available") + } + + var coin_value float64 + var atomicUnits uint = 8 + + switch pair { + case coin.BTCDERO: + coin_value = mk.Pairs.BTC + case coin.LTCDERO: + coin_value = mk.Pairs.LTC + case coin.ARRRDERO: + coin_value = mk.Pairs.ARRR + case coin.XMRDERO: + coin_value = mk.Pairs.XMR + atomicUnits = 12 + } + + deposit_value := coin_value * amount + + var loops int = 5 + var isAvailable bool + + // if there is a request with the same deposit amount, run in a loop and lower deposit value by 1 Sat + for i := 0; i < loops; i++ { + if coin.IsAmountFree(pair, deposit_value) { + isAvailable = true + break + } + deposit_value -= SATOSHI + } + if !isAvailable || deposit_value == 0 { + return fmt.Errorf("Pre-Check: Couldn't create swap") + } + + deposit_value = coin.RoundFloat(deposit_value, atomicUnits) + + resp.ID = CreateSwap(pair, resp.Wallet, dst_addr, amount, deposit_value) + if resp.ID == 0 { + return fmt.Errorf("couldn't create swap") + } + resp.Deposit = deposit_value + + return nil +} + +// Dero to X swaps +func DeroXTCSwap(pair string, dst_addr string, amount float64, resp *coin.Swap_Response) (err error) { + + var balance float64 + var atomicUnits uint = 8 + + // validate destination wallet and check for sufficient swap balance + if pair != coin.DEROXMR { + if !coin.XTCValidateAddress(pair, dst_addr) { + return fmt.Errorf("%s address is not valid", pair[5:]) + } + balance = coin.XTCGetBalance(pair) + } else { + if !monero.ValidateAddress(dst_addr) { + return fmt.Errorf("XMR address is not valid") + } + balance = monero.GetBalance() + atomicUnits = 12 + } + + var coin_value float64 + var fees float64 + + // determine fees and current price + switch pair { + case coin.DEROLTC: + coin_value = mk.Pairs.DEROLTC + fees = cfg.SwapFees.Withdrawal.DeroLTC + case coin.DEROARRR: + coin_value = mk.Pairs.DEROARRR + fees = cfg.SwapFees.Withdrawal.DeroARRR + case coin.DEROBTC: + coin_value = mk.Pairs.DEROBTC + fees = cfg.SwapFees.Withdrawal.DeroBTC + case coin.DEROXMR: + coin_value = mk.Pairs.XMR + fees = cfg.SwapFees.Withdrawal.DeroXMR + } + + payout_value := coin_value * amount + if payout_value-fees < 0 { + return fmt.Errorf("fees > payout value") + } + if payout_value == 0 { + return fmt.Errorf("couldn't create swap") + } + + // check for reserved balance + if coin.Locked.GetLockedBalance(pair)+payout_value+fees > balance { + return fmt.Errorf("insufficient swap balance") + } + + coin.Locked.AddLockedBalance(pair, payout_value) + + payout_value -= fees + payout_value = coin.RoundFloat(payout_value, atomicUnits) + resp.Swap = payout_value + + resp.ID = CreateSwap(pair, "", dst_addr, amount, payout_value) + if resp.ID == 0 { + return fmt.Errorf("couldn't create swap") + } + resp.Wallet = dero.MakeIntegratedAddress(resp.ID) + + return nil +} diff --git a/swap_manager.go b/swap_manager.go new file mode 100644 index 0000000..668058d --- /dev/null +++ b/swap_manager.go @@ -0,0 +1,173 @@ +package main + +import ( + "dero-swap/coin" + "dero-swap/dero" + "dero-swap/monero" + "encoding/json" + "fmt" + "io/fs" + "log" + "os" + "time" + + "github.com/deroproject/derohe/rpc" +) + +func Swap_Controller() { + + var file_data []byte + var swap_e coin.Swap_Entry + var expired, fails, sent, active uint + + var txs []rpc.Transfer + var xmr_txs []monero.RPC_XMR_Transfer_Params + + var dir_entries []fs.DirEntry + var err error + + for { + + time.Sleep(time.Minute) + dir_entries, err = os.ReadDir("swaps/active") + + expired = 0 + fails = 0 + sent = 0 + active = 0 + txs = nil + xmr_txs = nil + + for _, e := range dir_entries { + + active++ + file_data = nil + err = nil + + file_data, err = os.ReadFile("swaps/active/" + e.Name()) + if err != nil { + fails++ + continue + } + err = json.Unmarshal(file_data, &swap_e) + if err != nil { + fails++ + continue + } + creation_t := time.UnixMilli(swap_e.Created) + + // if there was no deposit, mark the request as expired + if swap_e.Status == 0 && time.Since(creation_t) > time.Hour { + os.WriteFile(fmt.Sprintf("swaps/expired/%d", swap_e.Created), file_data, 0644) + os.Remove("swaps/active/" + e.Name()) + switch swap_e.Coin { + case coin.LTCDERO, coin.BTCDERO, coin.ARRRDERO, coin.XMRDERO: + coin.Locked.RemoveLockedBalance(swap_e.Coin, swap_e.Amount) + default: + coin.Locked.RemoveLockedBalance(swap_e.Coin, swap_e.Price) + } + + expired++ + continue + } + + var found_deposit, visible bool + + // check for deposits + switch swap_e.Coin { + case coin.BTCDERO, coin.LTCDERO, coin.ARRRDERO: + found_deposit, visible, _, err = coin.XTCListReceivedByAddress(swap_e.Coin, swap_e.Wallet, swap_e.Price, swap_e.Block, false) + case coin.XMRDERO: + if payment_id := monero.SplitIntegratedAddress(swap_e.Wallet); payment_id != "" { + found_deposit = monero.XMRGetTX(payment_id, swap_e.Block) + visible = found_deposit + } else { + log.Println("Can't split intragrated XMR address") + } + default: + found_deposit = dero.CheckIncomingTransfers(uint64(swap_e.Created), swap_e.Block) + visible = found_deposit + } + + if err != nil { + log.Printf("Error checking incoming %s transactions\n", swap_e.Coin) + fails++ + continue + } + + // mark request as done + if swap_e.Status == 2 { + err = os.WriteFile(fmt.Sprintf("swaps/done/%d", swap_e.Created), file_data, 0644) + if err != nil { + log.Printf("Can't mark swap as done, swap %d, err %v\n", swap_e.Created, err) + } else { + os.Remove("swaps/active/" + e.Name()) + } + } + + // start payout if there are at least 2 confirmations + // requests won't be marked as expired, if there is already 1 confirmation + if visible { + if found_deposit && swap_e.Status <= 1 { + // create transaction + log.Printf("Found deposit for ID %d (%s): %.8f coins; adding to payout TX\n", swap_e.Created, swap_e.Coin, swap_e.Amount) + + switch swap_e.Coin { + case coin.DEROLTC, coin.DEROBTC: + log.Println("Starting LTC/BTC payout") + _, txid := coin.XTCSend(swap_e.Coin, swap_e.Destination, swap_e.Price) + log.Printf("LTC TXID: %s\n", txid) + coin.Locked.RemoveLockedBalance(swap_e.Coin, swap_e.Price) + case coin.DEROARRR: + log.Println("Starting ARRR payout") + ok, result := coin.ARRR_Send(swap_e.Destination, swap_e.Price) + log.Printf("ARRR status: %v, %s\n", ok, result) + coin.Locked.RemoveLockedBalance(swap_e.Coin, swap_e.Price) + case coin.DEROXMR: + xmr_txs = append(xmr_txs, monero.AddTX(swap_e.Destination, swap_e.Price)) + default: + txs = append(txs, dero.AddTX(swap_e.Destination, swap_e.Amount)) + } + + swap_e.Status = 2 + sent++ + active-- + } else { + // transaction was confirmed + swap_e.Status = 1 + } + + json_data, _ := json.Marshal(&swap_e) + os.WriteFile("swaps/active/"+e.Name(), json_data, 0644) + + if swap_e.Status == 2 { + err = os.WriteFile(fmt.Sprintf("swaps/done/%d", swap_e.Created), file_data, 0644) + if err != nil { + log.Printf("Can't mark swap as done, swap %d, err %v\n", swap_e.Created, err) + } else { + os.Remove("swaps/active/" + e.Name()) + } + } + } + } + + // Dero and Monero payout process + if len(txs) > 0 { + log.Println("Starting DERO payout process") + dero.Payout(txs) + } + // TODO: create function and TX verification + if len(xmr_txs) > 0 { + log.Println("Starting XMR payout process") + if ok, txid := monero.XMRSend(xmr_txs); ok { + log.Printf("XMR transaction (TXID %s) successfully sent\n", txid) + } else { + log.Println("Error sending XMR transaction") + } + } + + if sent+expired+fails > 0 { + log.Printf("Swap processing: %d sent, %d expired, %d errors\n", sent, expired, fails) + } + } +}