initial upload

This commit is contained in:
mmarcel 2024-04-11 14:35:17 +02:00
commit 9f67d3a6ee
19 changed files with 2917 additions and 0 deletions

201
cfg/config.go Normal file
View File

@ -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
}
}
}

49
cfg/variables.go Normal file
View File

@ -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}}

545
coin/btc.go Normal file
View File

@ -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]
}
}

12
coin/constants.go Normal file
View File

@ -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"
)

130
coin/functions.go Normal file
View File

@ -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
}

146
coin/rpc.go Normal file
View File

@ -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"`
}
)

52
coin/variables.go Normal file
View File

@ -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

129
dero/daemon.go Normal file
View File

@ -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 ""
}
}
}

192
dero/wallet.go Normal file
View File

@ -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
}

50
extras.go Normal file
View File

@ -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)
}
}
}

35
go.mod Normal file
View File

@ -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
)

42
main.go Normal file
View File

@ -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()
}

113
monero/rpc.go Normal file
View File

@ -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"`
}
)

193
monero/wallet.go Normal file
View File

@ -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)}
}

303
price.go Normal file
View File

@ -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
}

73
price_variables.go Normal file
View File

@ -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

275
server.go Normal file
View File

@ -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()
}

204
swap.go Normal file
View File

@ -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
}

173
swap_manager.go Normal file
View File

@ -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)
}
}
}