diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..9fc6f89 --- /dev/null +++ b/client/client.go @@ -0,0 +1,5 @@ +package client + +import "github.com/stellar/go/clients/horizonclient" + +var SignetClient = horizonclient.DefaultTestNetClient diff --git a/data/context.go b/data/context.go index 77eaf09..0b160eb 100644 --- a/data/context.go +++ b/data/context.go @@ -34,6 +34,7 @@ type RewardFund struct { ModelBase Asset string `json:"asset"` FundWallet string `json:"fundWallet"` + FundSecret string `json:"fundSecret"` SellingWallet string `json:"sellingWallet"` IssuerWallet string `json:"issuerWallet"` Memo string `json:"memo"` @@ -57,6 +58,7 @@ type Contribution struct { ModelBase Wallet string `json:"wallet"` Amount float64 `gorm:"type:decimal(19,7)" json:"amount"` + Submitted bool `gorm:"type:boolean" json:"submitted"` TransactionID string `json:"transactionID"` RewardFundID uint `json:"rewardFundID"` } diff --git a/endpoints/analytics.go b/endpoints/analytics.go index d2153ea..140a4e0 100644 --- a/endpoints/analytics.go +++ b/endpoints/analytics.go @@ -38,8 +38,7 @@ func NearlyCompleteFunds(w http.ResponseWriter, r *http.Request) { Db.Table("contributions"). Select("rf.id", "asset", "min_contribution", "amount_available", "memo", "fund_wallet", "sum(amount) as raised"). Joins("inner join reward_funds rf on rf.id = contributions.reward_fund_id"). - Group("asset, rf.id, min_contribution, amount_available, memo, fund_wallet"). - Having("sum(amount) between (rf.amount_available * ?) and rf.amount_available", req.Threshold/100). + Where("rf.amount_available < ?", req.Threshold). Scan(&resp.Funds) err = json.NewEncoder(w).Encode(resp) if err != nil { diff --git a/endpoints/contribute.go b/endpoints/contribute.go index 56fb811..b10e3c4 100644 --- a/endpoints/contribute.go +++ b/endpoints/contribute.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" + "github.com/imosed/signet/client" . "github.com/imosed/signet/data" "github.com/rs/zerolog/log" "github.com/stellar/go/clients/horizonclient" @@ -44,9 +45,9 @@ func Contribute(resp http.ResponseWriter, req *http.Request) { source := keypair.MustParseFull(cont.PrivateKey) sourceReq := horizonclient.AccountRequest{AccountID: source.Address()} var sourceAcct horizon.Account - sourceAcct, err = client.AccountDetail(sourceReq) + sourceAcct, err = client.SignetClient.AccountDetail(sourceReq) if err != nil { - log.Error().Err(err).Msg("Could not get account details from Horizon client") + log.Error().Err(err).Msg("Could not get account details from Horizon SignetClient") return } @@ -80,15 +81,13 @@ func Contribute(resp http.ResponseWriter, req *http.Request) { } var response horizon.Transaction - response, err = client.SubmitTransaction(tx) + response, err = client.SignetClient.SubmitTransaction(tx) if err != nil { log.Error().Err(err).Msg("Could not submit contribution transaction") return } - fmt.Println("Successful Transaction:") - fmt.Println("Ledger:", response.Ledger) - fmt.Println("Hash:", response.Hash) + log.Info().Msg(fmt.Sprintf("Successful Transaction: { Ledger: %d, Hash: %s }", response.Ledger, response.Hash)) var result SuccessResponse result.Success = response.Successful && err == nil diff --git a/endpoints/contributionstream.go b/endpoints/contributionstream.go index 14e4aa1..2fce118 100644 --- a/endpoints/contributionstream.go +++ b/endpoints/contributionstream.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/gorilla/websocket" + "github.com/imosed/signet/client" . "github.com/imosed/signet/data" "github.com/rs/zerolog/log" "github.com/spf13/viper" @@ -52,13 +53,16 @@ func InitializeContributionStreams() { Db.Table("reward_funds").Select("fund_wallet").Scan(&wallets) contributionUpdateHandler := func(op operations.Operation) { + if op.GetBase().GetTypeI() == 6 || op.GetBase().GetTypeI() == 12 { + return + } payment := op.(operations.Payment) var tx horizon.Transaction var amt float64 var fund RewardFund - tx, err = client.TransactionDetail(payment.GetTransactionHash()) + tx, err = client.SignetClient.TransactionDetail(payment.GetTransactionHash()) if err != nil { log.Error().Err(err).Msg("Could not get transaction from hash") return @@ -104,9 +108,9 @@ func InitializeContributionStreams() { opReq.SetOperationsEndpoint() ctx, cancellation := context.WithCancel(context.Background()) cancellations = append(cancellations, cancellation) - err = client.StreamOperations(ctx, opReq, contributionUpdateHandler) + err = client.SignetClient.StreamOperations(ctx, opReq, contributionUpdateHandler) if err != nil { - log.Error().Err(err).Msg("Failed to stream contributions from Horizon client") + log.Error().Err(err).Msg("Failed to stream contributions from Horizon SignetClient") } } } diff --git a/endpoints/createrewardfund.go b/endpoints/createrewardfund.go index b709393..a1293bc 100644 --- a/endpoints/createrewardfund.go +++ b/endpoints/createrewardfund.go @@ -2,16 +2,14 @@ package endpoints import ( "encoding/json" - "errors" "fmt" "net/http" - "strconv" "github.com/imosed/signet/auth" . "github.com/imosed/signet/data" + "github.com/imosed/signet/utils" "github.com/rs/zerolog/log" "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/protocols/horizon" ) type CreateRewardFundRequest struct { @@ -66,7 +64,7 @@ func CreateRewardFund(resp http.ResponseWriter, req *http.Request) { Order: horizonclient.OrderDesc, } - if err, ok := FindOffer(offerReq, &rewardFund); !ok { + if err, ok := utils.FindOffer(offerReq, &rewardFund); !ok { err = json.NewEncoder(resp).Encode(&SuccessResponse{Success: ok}) if err != nil { log.Error().Err(err).Msg("Could not deliver response after failing to find issuer offer") @@ -108,53 +106,3 @@ func CreateRewardFund(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(403) } } - -func FindOffer(offerReq horizonclient.OfferRequest, rewardFund *RewardFund) (error, bool) { - op, err := client.Offers(offerReq) - if err != nil { - return errors.New("could not get offers"), false - } - offers := op.Embedded.Records - var price float64 - var amt float64 - if len(offers) == 1 { - price, err = strconv.ParseFloat(op.Embedded.Records[0].Price, 64) - if err != nil { - return errors.New("could not parse single offer price to float"), false - } - amt, err = strconv.ParseFloat(op.Embedded.Records[0].Amount, 64) - if err != nil { - return errors.New("could not parse single offer amount to float"), false - } - rewardFund.Price = price - rewardFund.AmountAvailable = amt - return nil, true - } else if len(offers) > 1 { - var maxOffers float64 = 0 - var correctOffer horizon.Offer - for _, o := range op.Embedded.Records { - parsedAmt, err := strconv.ParseFloat(o.Amount, 64) - if err != nil { - return errors.New("could not parse amount from offer slice to float"), false - } - if parsedAmt > maxOffers { - correctOffer = o - maxOffers = parsedAmt - } - } - price, err = strconv.ParseFloat(correctOffer.Price, 64) - if err != nil { - return errors.New("could not parse correct offer price to float"), false - } - rewardFund.Price = price - - amt, err = strconv.ParseFloat(correctOffer.Amount, 64) - if err != nil { - return errors.New("could not parse correct offer amount to float"), false - } - rewardFund.AmountAvailable = amt - return nil, true - } else { - return nil, false // no offers shouldn't error - } -} diff --git a/endpoints/getbalance.go b/endpoints/getbalance.go index e32de36..448c2f9 100644 --- a/endpoints/getbalance.go +++ b/endpoints/getbalance.go @@ -5,6 +5,7 @@ import ( "net/http" "strconv" + "github.com/imosed/signet/client" "github.com/rs/zerolog/log" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/keypair" @@ -38,7 +39,7 @@ func GetBalance(w http.ResponseWriter, r *http.Request) { AccountID: kp.Address(), } var acct horizon.Account - acct, err = client.AccountDetail(acctReq) + acct, err = client.SignetClient.AccountDetail(acctReq) if err != nil { log.Error().Err(err).Msg("Could not get account data from public key") return diff --git a/endpoints/submitfund.go b/endpoints/submitfund.go index 847372e..3fed95d 100644 --- a/endpoints/submitfund.go +++ b/endpoints/submitfund.go @@ -5,16 +5,22 @@ import ( "fmt" "net/http" + "github.com/imosed/signet/client" . "github.com/imosed/signet/data" + "github.com/imosed/signet/utils" "github.com/rs/zerolog/log" "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/network" "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "gorm.io/gorm/clause" ) -var client = horizonclient.DefaultTestNetClient - type SubmitFundRequest struct { - FundID uint `json:"fundId"` + FundID uint `json:"fundID"` + Submit bool `json:"submit"` } func SubmitFund(w http.ResponseWriter, r *http.Request) { @@ -25,69 +31,108 @@ func SubmitFund(w http.ResponseWriter, r *http.Request) { } var fund RewardFund - Db.Find(&fund, req.FundID) + Db.Preload(clause.Associations).Find(&fund, req.FundID) - // source := keypair.MustParseFull(fund.FundWallet) - // sourceReq := horizonclient.AccountRequest{AccountID: source.Address()} - // var sourceAcct horizon.Account - // sourceAcct, err = client.AccountDetail(sourceReq) + var resp SuccessResponse + if !req.Submit { + json.NewEncoder(w).Encode(&SuccessResponse{Success: false}) + return + } + + source := keypair.MustParseFull(fund.FundSecret) + sourceReq := horizonclient.AccountRequest{AccountID: source.Address()} + var sourceAcct horizon.Account + sourceAcct, err = client.SignetClient.AccountDetail(sourceReq) offerReq := horizonclient.OfferRequest{ + Seller: fund.SellingWallet, Selling: fmt.Sprintf("%s:%s", fund.Asset, fund.IssuerWallet), - Seller: fund.IssuerWallet, - Cursor: "0", + Order: horizonclient.OrderDesc, } - var offers horizon.OffersPage - offers, err = client.Offers(offerReq) - for _, o := range offers.Embedded.Records { - if float64(o.PriceR.N)/float64(o.PriceR.D) == fund.Price { - fmt.Println(o.PriceR.N) - fmt.Println(o.Amount) + if err, ok := utils.FindOffer(offerReq, &fund); !ok { + err = json.NewEncoder(w).Encode(&SuccessResponse{Success: ok}) + if err != nil { + log.Error().Err(err).Msg("Could not deliver response after failing to find issuer offer in submission") } + return } - // var tx *txnbuild.Transaction - // tx, err = txnbuild.NewTransaction( - // txnbuild.TransactionParams{ - // SourceAccount: &sourceAcct, - // IncrementSequenceNum: true, - // Operations: []txnbuild.Operation{ - // &txnbuild.ManageBuyOffer{ - // Selling: txnbuild.NativeAsset{}, - // Buying: txnbuild.CreditAsset{ - // Code: fund.Asset, - // Issuer: fund.IssuerWallet, - // }, - // Amount: fmt.Sprintf("%f", SumContributions(fund.Contributions)), - // Price: xdr.Price{}, // TODO: get price - // OfferID: 0, - // SourceAccount: "", - // }, - // }, - // BaseFee: txnbuild.MinBaseFee, - // Memo: txnbuild.Memo(txnbuild.MemoText(strconv.Itoa(int(fund.Model.ID)))), - // Preconditions: txnbuild.Preconditions{ - // TimeBounds: txnbuild.NewInfiniteTimeout(), // TODO: change from infinite - // }, - // }) - // if err != nil { - // log.Error().Err(err).Msg("Could not submit reward fund") - // } - // - // tx, err = tx.Sign(network.TestNetworkPassphrase, source) - // if err != nil { - // log.Error().Err(err).Msg("Could not submit fund") - // } - // - // var response horizon.Transaction - // response, err = client.SubmitTransaction(tx) + var currentContributions = fund.Contributions + var submissionAmount = SumContributions(currentContributions) - var resp SuccessResponse - // resp.Success = response.Successful + tr := Db.Begin() + tr.Table("contributions"). + Where("reward_fund_id = ? and submitted is null or submitted = false", req.FundID). + Updates(Contribution{Submitted: true}) + + var tx *txnbuild.Transaction + tx, err = txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &sourceAcct, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.ChangeTrust{ + Line: txnbuild.CreditAsset{ + Code: fund.Asset, + Issuer: fund.IssuerWallet, + }.MustToChangeTrustAsset(), + SourceAccount: fund.FundWallet, + }, + &txnbuild.ManageBuyOffer{ + Selling: txnbuild.NativeAsset{}, + Buying: txnbuild.CreditAsset{ + Code: fund.Asset, + Issuer: fund.IssuerWallet, + }, + Amount: fmt.Sprintf("%f", submissionAmount), + Price: xdr.Price{N: 1, D: xdr.Int32(fund.Price)}, + OfferID: 0, + SourceAccount: fund.FundWallet, + }, + }, + BaseFee: txnbuild.MinBaseFee, + Memo: txnbuild.Memo(nil), + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), // TODO: change from infinite + }, + }) + if err != nil { + log.Error().Err(err).Msg("Could not build submission transaction") + tr.Rollback() + return + } + + tx, err = tx.Sign(network.TestNetworkPassphrase, source) + if err != nil { + log.Error().Err(err).Msg("Could not sign submission transaction") + tr.Rollback() + return + } + + var response horizon.Transaction + response, err = client.SignetClient.SubmitTransaction(tx) + if err != nil { + log.Error().Err(err).Msg("Could not submit transaction") + tr.Rollback() + return + } + + tr.Commit() + resp.Success = response.Successful err = json.NewEncoder(w).Encode(resp) if err != nil { log.Error().Err(err).Msg("Could not deliver response in SubmitFund call") } } + +func SumContributions(contributions []Contribution) float64 { + var total float64 = 0 + for _, contribution := range contributions { + if !contribution.Submitted { + total += contribution.Amount + } + } + return total +} diff --git a/main.go b/main.go index cf6e0b8..a7bbcd6 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,7 @@ func main() { router.HandleFunc("/EditQueue", endpoints.EditQueue) router.HandleFunc("/CreateRewardFund", endpoints.CreateRewardFund) router.HandleFunc("/CloseRewardFund", endpoints.CloseRewardFund) + router.HandleFunc("/SubmitRewardFund", endpoints.SubmitFund) // router.HandleFunc("/SubmitFund", endpoints.SubmitFund) router.HandleFunc("/GetBalance", endpoints.GetBalance) router.HandleFunc("/Contribute", endpoints.Contribute) diff --git a/utils/offers.go b/utils/offers.go new file mode 100644 index 0000000..be156e3 --- /dev/null +++ b/utils/offers.go @@ -0,0 +1,60 @@ +package utils + +import ( + "errors" + "strconv" + + "github.com/imosed/signet/client" + "github.com/imosed/signet/data" + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/protocols/horizon" +) + +func FindOffer(offerReq horizonclient.OfferRequest, rewardFund *data.RewardFund) (error, bool) { + op, err := client.SignetClient.Offers(offerReq) + if err != nil { + return errors.New("could not get offers"), false + } + offers := op.Embedded.Records + var price float64 + var amt float64 + if len(offers) == 1 { + price, err = strconv.ParseFloat(op.Embedded.Records[0].Price, 64) + if err != nil { + return errors.New("could not parse single offer price to float"), false + } + amt, err = strconv.ParseFloat(op.Embedded.Records[0].Amount, 64) + if err != nil { + return errors.New("could not parse single offer amount to float"), false + } + rewardFund.Price = price + rewardFund.AmountAvailable = amt + return nil, true + } else if len(offers) > 1 { + var maxOffers float64 = 0 + var correctOffer horizon.Offer + for _, o := range op.Embedded.Records { + parsedAmt, err := strconv.ParseFloat(o.Amount, 64) + if err != nil { + return errors.New("could not parse amount from offer slice to float"), false + } + if parsedAmt > maxOffers { + correctOffer = o + maxOffers = parsedAmt + } + } + price, err = strconv.ParseFloat(correctOffer.Price, 64) + if err != nil { + return errors.New("could not parse correct offer price to float"), false + } + amt, err = strconv.ParseFloat(correctOffer.Amount, 64) + if err != nil { + return errors.New("could not parse correct offer amount to float"), false + } + rewardFund.Price = price + rewardFund.AmountAvailable = amt + return nil, true + } else { + return nil, false // no offers shouldn't error + } +}