diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 19b11a72..ba230709 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -19,6 +19,7 @@ import ( admin_inject "github.com/theseed-labs/os-backend/internal_inject/admin" appbundles_inject "github.com/theseed-labs/os-backend/internal_inject/app_bundles" applications_inject "github.com/theseed-labs/os-backend/internal_inject/applications" + assetrecords_inject "github.com/theseed-labs/os-backend/internal_inject/asset_records" cityhall_inject "github.com/theseed-labs/os-backend/internal_inject/city_hall" common_budget_sources_inject "github.com/theseed-labs/os-backend/internal_inject/common_budget_sources" datasrv_inject "github.com/theseed-labs/os-backend/internal_inject/data_srv" @@ -261,6 +262,8 @@ func setupRouter(cfg *config.Config, db *gorm.DB, enforcer *casbin.SyncedEnforce // publicity publicity_inject.Register(v1) + // asset records + assetrecords_inject.Register(v1) } // --> no auth required { diff --git a/go.mod b/go.mod index b3036cca..6394b530 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/casbin/casbin/v2 v2.77.2 github.com/casbin/gorm-adapter/v3 v3.20.0 github.com/ethereum/go-ethereum v1.13.8 + github.com/facebookgo/inject v0.0.0-20180706035515-f23751cae28b github.com/getsentry/sentry-go v0.25.0 github.com/gin-contrib/cors v1.4.0 github.com/gin-contrib/gzip v0.0.6 @@ -34,6 +35,7 @@ require ( github.com/samber/lo v1.38.1 github.com/shopspring/decimal v1.3.1 github.com/spruceid/siwe-go v0.2.1 + github.com/stretchr/testify v1.8.4 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.2 @@ -51,6 +53,7 @@ require ( gorm.io/datatypes v1.2.0 gorm.io/driver/mysql v1.5.2 gorm.io/driver/postgres v1.5.0 + gorm.io/driver/sqlite v1.4.3 gorm.io/gorm v1.25.5 ) @@ -83,6 +86,7 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/uniuri v1.2.0 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect @@ -92,8 +96,10 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect - github.com/facebookgo/inject v0.0.0-20180706035515-f23751cae28b // indirect + github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect + github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/structtag v0.0.0-20150214074306-217e25fb9691 // indirect + github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/forta-network/go-multicall v0.0.0-20230701154355-9467c4ddaa83 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -142,6 +148,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/microsoft/go-mssqldb v0.17.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -159,6 +166,7 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect diff --git a/go.sum b/go.sum index 564fe727..957bbab7 100644 --- a/go.sum +++ b/go.sum @@ -194,10 +194,16 @@ github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ethereum/go-ethereum v1.13.8 h1:1od+thJel3tM52ZUNQwvpYOeRHlbkVFZ5S8fhi0Lgsg= github.com/ethereum/go-ethereum v1.13.8/go.mod h1:sc48XYQxCzH3fG9BcrXCOOgQk2JfZzNAmIKnceogzsA= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/facebookgo/inject v0.0.0-20180706035515-f23751cae28b h1:V6c4/dSTNhSaNn4c5ulbakfv277qCvs7byFYv7P83iQ= github.com/facebookgo/inject v0.0.0-20180706035515-f23751cae28b/go.mod h1:oO8UHw+fDHjDsk4CTy/E96WDzFUYozAtBAaGNoVL0+c= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/structtag v0.0.0-20150214074306-217e25fb9691 h1:KnnwHN59Jxec0htA2pe/i0/WI9vxXLQifdhBrP3lqcQ= github.com/facebookgo/structtag v0.0.0-20150214074306-217e25fb9691/go.mod h1:sKLL1iua/0etWfo/nPCmyz+v2XDMXy+Ho53W7RAuZNY= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= @@ -452,6 +458,7 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= diff --git a/internal/api/proposal/helper.go b/internal/api/proposal/helper.go index a4fb9456..3839e204 100644 --- a/internal/api/proposal/helper.go +++ b/internal/api/proposal/helper.go @@ -1777,35 +1777,36 @@ func CreateProjectFromAutoTasks(db *gorm.DB, proposalId uint) (*model.Project, e for _, pComponentRecord := range pComponents { if compName, found := getProposalComponentIdNameMapping(db)[pComponentRecord.ComponentID]; found { - if compName == internal.ComponentNameBudgetP1 { + switch compName { + case internal.ComponentNameBudgetP1: var budgetParams budgetComponentDataP1 - err := json.Unmarshal([]byte(pComponentRecord.Data), &budgetParams) + err = json.Unmarshal([]byte(pComponentRecord.Data), &budgetParams) if err != nil { log.Error().Msgf("unmarshal project deliverables data error: %+v", err) return nil, err } projectBudgetRcds = budgetParams.prepareBudgetRecords(proposalId) - } else if compName == internal.ComponentNameBudget { + case internal.ComponentNameBudget, internal.ComponentNameBudgetP2: var budgetParams budgetComponentData - err := json.Unmarshal([]byte(pComponentRecord.Data), &budgetParams) + err = json.Unmarshal([]byte(pComponentRecord.Data), &budgetParams) if err != nil { log.Error().Msgf("unmarshal project deliverables data error: %+v", err) return nil, err } projectBudgetRcds = budgetParams.prepareBudgetRecords(proposalId) - } else if compName == internal.ComponentNameDeliverables { + case internal.ComponentNameDeliverables: var deliverableParams commonCreateProjectRelatedData - err := json.Unmarshal([]byte(pComponentRecord.Data), &deliverableParams) + err = json.Unmarshal([]byte(pComponentRecord.Data), &deliverableParams) if err != nil { log.Error().Msgf("unmarshal project deliverables data error: %+v", err) return nil, err } newProjectData.Deliverable = deliverableParams.Desc - } else if compName == internal.ComponentNameDeadline { + case internal.ComponentNameDeadline: var deadlineParams commonCreateProjectRelatedData - err := json.Unmarshal([]byte(pComponentRecord.Data), &deadlineParams) + err = json.Unmarshal([]byte(pComponentRecord.Data), &deadlineParams) if err != nil { log.Error().Msgf("unmarshal project deadline data error: %+v", err) return nil, err diff --git a/internal/api/utils.go b/internal/api/utils.go index d40827d0..acdbb56c 100644 --- a/internal/api/utils.go +++ b/internal/api/utils.go @@ -132,8 +132,9 @@ func RefreshSeepassDataCache(sppClient *sdk.SppClient, wallet string) (*sdk.Seep func CreateAutoTransferScrTask(db *gorm.DB, applications []*model.Application, proposalId uint) error { // Issue send SCR tasks + // 20250809 - Change the SCR into WANG, and only change the asset name in DB, but keep the variable name to minimal changes scrApplications := lo.Filter(applications, func(app *model.Application, _ int) bool { - return app.Type == model.ApplicationNewReward && app.AssetName == "SCR" + return app.Type == model.ApplicationNewReward && app.AssetName == internal.AssetNameWANG }) if len(scrApplications) > 0 { diff --git a/internal/const.go b/internal/const.go index 38934cd3..b451bfb4 100644 --- a/internal/const.go +++ b/internal/const.go @@ -111,6 +111,7 @@ const ( const ( ComponentNameBudgetP1 = "budget_p1" + ComponentNameBudgetP2 = "budget_p2" ComponentNameBudget = "budget" ComponentNameDeliverables = "deliverables" ComponentNameDeadline = "deadline" @@ -134,15 +135,30 @@ type DecimalsAndContractAddr struct { Addr string } +var ( + AssetNameWANG = "WANG" + AssetNameSCR = "SCR" + AssetNameSEE = "SEE" + AssetNameUSDT = "USDT" + AssetNameUSDC = "USDC" + + // This is for DB query of USD* asssets + AssetPrefixUSD = "USD" + + SNSInviteRewardsToken = AssetNameWANG +) + // AssertDecimalsAndContractAddr supported token for sending application to QuickAccounting var AssertDecimalsAndContractAddr = map[string]*DecimalsAndContractAddr{ - "SCR": {Decimals: 18, Addr: ScrContractAddr}, - "USDT": {Decimals: 6, Addr: USDTContractAddr}, + AssetNameSCR: {Decimals: 18, Addr: ScrContractAddr}, + AssetNameWANG: {Decimals: 18, Addr: ScrContractAddr}, + AssetNameSEE: {Decimals: 18, Addr: ""}, + AssetNameUSDT: {Decimals: 6, Addr: USDTContractAddr}, + AssetNameUSDC: {Decimals: 6, Addr: ""}, } const ( - SNSInviteRewardsToken = "SCR" - SNSInviteItem = "邀请 SNS" + SNSInviteItem = "邀请 SNS" ) const ( diff --git a/internal/model/treasury_asset.go b/internal/model/treasury_asset.go index 3a945113..cb65f6f6 100644 --- a/internal/model/treasury_asset.go +++ b/internal/model/treasury_asset.go @@ -64,9 +64,9 @@ func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsRe // Calculate total amount for _, detailedRcd := range r.DetailedRecords { - if strings.HasPrefix(detailedRcd.AssetName, "USD") { + if strings.HasPrefix(detailedRcd.AssetName, internal.AssetPrefixUSD) { tokenTotal = tokenTotal.Add(detailedRcd.TotalAmount) - } else if strings.EqualFold(detailedRcd.AssetName, "SCR") { + } else if strings.EqualFold(detailedRcd.AssetName, internal.AssetNameWANG) { creditTotal = creditTotal.Add(detailedRcd.TotalAmount) } else { log.Warn().Msgf("non token or credit asset %s, ignore the treasury record: %+v", detailedRcd.AssetName, detailedRcd) @@ -85,13 +85,13 @@ func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsRe creditUsed = decimal.Zero tokenUsed = decimal.Zero for _, application := range applications { - if strings.HasPrefix(application.AssetName, "USD") { + if strings.HasPrefix(application.AssetName, internal.AssetPrefixUSD) { // only calculate completed USD if application.State == ApplicationStateCompleted { tokenUsed = tokenUsed.Add(application.AssetAmount) } - } else if strings.EqualFold(application.AssetName, "SCR") { - // calculate processing and completed SCR + } else if strings.EqualFold(application.AssetName, internal.AssetNameWANG) { + // calculate processing and completed asset creditUsed = creditUsed.Add(application.AssetAmount) } else { log.Warn().Msgf("non token or credit asset %s, ignore the application record: %+v", application.AssetName, application) diff --git a/internal/model/typedef.go b/internal/model/typedef.go index f7362988..9d50e75f 100644 --- a/internal/model/typedef.go +++ b/internal/model/typedef.go @@ -208,6 +208,10 @@ type TreasuryAssetsResponse struct { CreditUsedAmount decimal.Decimal `json:"credit_used_amount"` TokenTotalAmount decimal.Decimal `json:"token_total_amount"` TokenUsedAmount decimal.Decimal `json:"token_used_amount"` + + // Total issued SEE and season used SEE. The variable name here is to keep with old naming rule + SeeTotalAmount decimal.Decimal `json:"see_total_amount"` + SeeUsedAmount decimal.Decimal `json:"see_used_amount"` } // NewApplicationRequest is used to save new application request data passed from frontend diff --git a/internal/model/user_asset_record.go b/internal/model/user_asset_record.go index 5d5d5ffa..88b3bd9c 100644 --- a/internal/model/user_asset_record.go +++ b/internal/model/user_asset_record.go @@ -2,6 +2,7 @@ package model import ( "fmt" + "strings" "time" "github.com/shopspring/decimal" @@ -49,7 +50,7 @@ func (*userAssetRecordModel) FindWithUserWalletAndAssetProps(db *gorm.DB, userWa } } - querySeg := db.Where(&UserAssetRecord{UserWallet: formattedUserWallet, AssetName: assetName}) + querySeg := db.Where(&UserAssetRecord{UserWallet: formattedUserWallet, AssetName: strings.ToUpper(assetName)}) return QueryRows[UserAssetRecord](querySeg, nil) } @@ -66,7 +67,7 @@ func (*userAssetRecordModel) CreateOrUpdate(db *gorm.DB, userWallet string, asse if len(assetRecords) == 0 { return db.Save(&UserAssetRecord{ UserWallet: common.FormatUserWallet(userWallet), - AssetName: assetName, + AssetName: strings.ToUpper(assetName), DealtAmount: dealtAmount, ProcessingAmount: processingAmount, }).Error @@ -79,7 +80,7 @@ func (*userAssetRecordModel) CreateOrUpdate(db *gorm.DB, userWallet string, asse // Rollback extracts processing and dealt amount from records func (*userAssetRecordModel) Rollback(db *gorm.DB, userWallet string, assetName string, processingAmount, dealtAmount decimal.Decimal) error { - assetRecords, err := UserAssetRecordModel.FindWithUserWalletAndAssetProps(db, common.FormatUserWallet(userWallet), assetName) + assetRecords, err := UserAssetRecordModel.FindWithUserWalletAndAssetProps(db, common.FormatUserWallet(userWallet), strings.ToUpper(assetName)) if err != nil { return err } @@ -95,7 +96,7 @@ func (*userAssetRecordModel) Rollback(db *gorm.DB, userWallet string, assetName } func (*userAssetRecordModel) CompleteAssetTransaction(db *gorm.DB, userWallet string, assetName string, amountToBeDealt decimal.Decimal) error { - assetRecords, err := UserAssetRecordModel.FindWithUserWalletAndAssetProps(db, common.FormatUserWallet(userWallet), assetName) + assetRecords, err := UserAssetRecordModel.FindWithUserWalletAndAssetProps(db, common.FormatUserWallet(userWallet), strings.ToUpper(assetName)) if err != nil { return err } diff --git a/internal/model/user_asset_transfer_log.go b/internal/model/user_asset_transfer_log.go new file mode 100644 index 00000000..74984d1e --- /dev/null +++ b/internal/model/user_asset_transfer_log.go @@ -0,0 +1,128 @@ +package model + +import ( + "time" + + "github.com/shopspring/decimal" + "github.com/theseed-labs/os-backend/internal/common" + "gorm.io/gorm" +) + +// UserAssetTransferLog records asset transfers between users +// This table logs all asset transfer transactions +// from_user and to_user should point to different records in user table +// asset_name: the name of the asset being transferred +// amount: the quantity of the asset being transferred +// transaction_ts: timestamp when the transaction was initiated +// result: the final status of the transaction (success, failed, pending) +type UserAssetTransferLog struct { + ID uint `json:"id" gorm:"primaryKey"` + FromUser string `json:"from_user" gorm:"type:varchar(256);index"` // user wallet address of sender + ToUser string `json:"to_user" gorm:"type:varchar(256);index"` // user wallet address of receiver + AssetName string `json:"asset_name" gorm:"type:varchar(64);index"` // asset name + Amount decimal.Decimal `json:"amount" sql:"type:decimal(20,8);"` // amount of asset transferred + TransactionTs int64 `json:"transaction_ts" gorm:"index"` // timestamp when transaction was initiated + Result string `json:"result" gorm:"type:varchar(20);index"` // success, failed, pending + Comment string `json:"comment" gorm:"type:varchar(500)"` // optional comment for the transfer + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + CreateTs int64 `json:"create_ts" gorm:"index"` + UpdateTs int64 `json:"update_ts" gorm:"index"` +} + +// TransferResult constants +const ( + TransferResultSuccess = "success" + TransferResultFailed = "failed" + TransferResultPending = "pending" +) + +type userAssetTransferLogModel struct{} + +var UserAssetTransferLogModel userAssetTransferLogModel + +// Create creates a new asset transfer log record +func (*userAssetTransferLogModel) Create(db *gorm.DB, fromUser string, toUser string, assetName string, amount decimal.Decimal, comment string) (*UserAssetTransferLog, error) { + fromUser = common.FormatUserWallet(fromUser) + toUser = common.FormatUserWallet(toUser) + + // Ensure both users exist in the database + var fromUserRecord, toUserRecord User + + // get from user and to user + if err := db.Where(User{Wallet: fromUser}).First(&fromUserRecord).Error; err != nil { + return nil, err + } + + if err := db.Where(User{Wallet: toUser}).FirstOrCreate(&toUserRecord).Error; err != nil { + return nil, err + } + + transferLog := &UserAssetTransferLog{ + FromUser: fromUser, + ToUser: toUser, + AssetName: assetName, + Amount: amount, + TransactionTs: GetCurrentUtcEpochSecond(), + Result: TransferResultPending, + Comment: comment, + } + + if err := db.Create(transferLog).Error; err != nil { + return nil, err + } + + return transferLog, nil +} + +// UpdateResult updates the result of a transfer log +func (*userAssetTransferLogModel) UpdateResult(db *gorm.DB, id uint, result string) error { + return db.Model(&UserAssetTransferLog{}).Where("id = ?", id).Updates(map[string]interface{}{ + "result": result, + }).Error +} + +// ListPaginated returns paginated transfer records with optional filters +func (*userAssetTransferLogModel) ListPaginated(db *gorm.DB, page, size int, fromUser, toUser string) ([]*UserAssetTransferLog, int64, error) { + var logs []*UserAssetTransferLog + var total int64 + + query := db.Model(&UserAssetTransferLog{}) + + if fromUser != "" { + query = query.Where("from_user = ?", common.FormatUserWallet(fromUser)) + } + + if toUser != "" { + query = query.Where("to_user = ?", common.FormatUserWallet(toUser)) + } + + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Apply pagination + if page > 0 && size > 0 { + offset := (page - 1) * size + query = query.Offset(offset).Limit(size) + } + + // Order by transaction timestamp desc + query = query.Order("transaction_ts DESC") + + if err := query.Find(&logs).Error; err != nil { + return nil, 0, err + } + + return logs, total, nil +} + +// GetByID returns a single transfer log by ID +func (*userAssetTransferLogModel) GetByID(db *gorm.DB, id uint) (*UserAssetTransferLog, error) { + var log UserAssetTransferLog + if err := db.Where("id = ?", id).First(&log).Error; err != nil { + return nil, err + } + return &log, nil +} diff --git a/internal/model/utils.go b/internal/model/utils.go index 8899c507..87ecd809 100644 --- a/internal/model/utils.go +++ b/internal/model/utils.go @@ -430,6 +430,7 @@ func MigrateTables(db *gorm.DB) error { &User{}, &UserNonce{}, &UserAssetRecord{}, + &UserAssetTransferLog{}, &Project{}, &ProjectBudget{}, &Guild{}, diff --git a/internal/sdk/spp_client.go b/internal/sdk/spp_client.go index 20efa118..9c46f669 100644 --- a/internal/sdk/spp_client.go +++ b/internal/sdk/spp_client.go @@ -24,6 +24,10 @@ type SeepassResponse struct { ContractAddr string `json:"contract_addr"` } `json:"scr"` + See struct { + Amount string `json:"amount"` + } `json:"see"` + Level struct { CurrentLv string `json:"current_lv"` NextLv string `json:"next_lv"` diff --git a/internal_inject/applications/applications.controller.go b/internal_inject/applications/applications.controller.go index 5302a404..19515d12 100644 --- a/internal_inject/applications/applications.controller.go +++ b/internal_inject/applications/applications.controller.go @@ -611,6 +611,9 @@ func (c *ApplicationsController) AssetStatistics(ctx *gin.Context) { var checkingUsd float64 var checkingScr float64 + usdAssets := []string{internal.AssetNameUSDC, internal.AssetNameUSDT} + scrAssets := []string{internal.AssetNameWANG} + currentSeason, err := model.GetCurrentSeason(c.Db) if err != nil { log.Error().Msgf("fetch current season error: %v", err) @@ -619,7 +622,8 @@ func (c *ApplicationsController) AssetStatistics(ctx *gin.Context) { return } - waitForGrantUsd, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateOpen), "USD", int(currentSeason.ID)) + waitForGrantUsd, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateOpen), usdAssets, int(currentSeason.ID)) + if err != nil { log.Error().Msgf("get sum wait grant usd amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) @@ -627,7 +631,8 @@ func (c *ApplicationsController) AssetStatistics(ctx *gin.Context) { return } - waitForGrantScr, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateOpen), "SCR", int(currentSeason.ID)) + waitForGrantScr, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateOpen), scrAssets, int(currentSeason.ID)) + if err != nil { log.Error().Msgf("get sum wait grant scr amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) @@ -635,7 +640,8 @@ func (c *ApplicationsController) AssetStatistics(ctx *gin.Context) { return } - grantedUsd, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateCompleted), "USD", int(currentSeason.ID)) + grantedUsd, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateCompleted), usdAssets, int(currentSeason.ID)) + if err != nil { log.Error().Msgf("get sum granted usd amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) @@ -643,7 +649,7 @@ func (c *ApplicationsController) AssetStatistics(ctx *gin.Context) { return } - grantedScr, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateCompleted), "SCR", int(currentSeason.ID)) + grantedScr, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateCompleted), scrAssets, int(currentSeason.ID)) if err != nil { log.Error().Msgf("get sum granted scr amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) @@ -651,7 +657,7 @@ func (c *ApplicationsController) AssetStatistics(ctx *gin.Context) { return } - checkingUsd, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateApproved), "USD", int(currentSeason.ID)) + checkingUsd, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateApproved), usdAssets, int(currentSeason.ID)) if err != nil { log.Error().Msgf("get sum checking usd amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) @@ -659,7 +665,8 @@ func (c *ApplicationsController) AssetStatistics(ctx *gin.Context) { return } - checkingScr, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateApproved), "SCR", int(currentSeason.ID)) + checkingScr, err = c.ApplicationsService.SumAssetAmount(ctx, string(model.ApplicationStateApproved), scrAssets, int(currentSeason.ID)) + if err != nil { log.Error().Msgf("get sum checking scr amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) diff --git a/internal_inject/applications/applications.service.go b/internal_inject/applications/applications.service.go index 0f7b005b..8e0c8c29 100644 --- a/internal_inject/applications/applications.service.go +++ b/internal_inject/applications/applications.service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -204,12 +205,14 @@ func (s *ApplicationsService) GetCronJobRecordFromStrId(taskId string) (model.Cr return task, nil } -func (s *ApplicationsService) SumAssetAmount(ctx *gin.Context, stat string, assetName string, seasonId int) (float64, error) { +func (s *ApplicationsService) SumAssetAmount(ctx *gin.Context, stat string, assetNames []string, seasonId int) (float64, error) { var value float64 + assetQueryCond := fmt.Sprintf("asset_name in ('%s')", strings.Join(assetNames, "','")) + err := s.Db.Model(&model.Application{}). Select("sum(cast(asset_amount as decimal)) as total"). - Where("state = ?", stat).Where("asset_name = ?", assetName). + Where("state = ?", stat).Where(assetQueryCond). Where("season_id = ?", seasonId). Scan(&value).Error if err != nil { diff --git a/internal_inject/asset_records/asset_records.controller.go b/internal_inject/asset_records/asset_records.controller.go new file mode 100644 index 00000000..f70de88a --- /dev/null +++ b/internal_inject/asset_records/asset_records.controller.go @@ -0,0 +1,191 @@ +package asset_records_inject + +import ( + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/facebookgo/inject" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + "github.com/theseed-labs/os-backend/global_object" + "github.com/theseed-labs/os-backend/internal/api" + "github.com/theseed-labs/os-backend/internal/common" + "github.com/theseed-labs/os-backend/internal/config" + "github.com/theseed-labs/os-backend/internal/middleware" + "github.com/theseed-labs/os-backend/internal/sdk" + "gorm.io/gorm" +) + +type AssetRecordsController struct { + // inject + Gin *gin.Engine `inject:""` + Db *gorm.DB `inject:""` + Cfg *config.Config `inject:""` + Service *AssetRecordsService `inject:""` +} + +func Register(fatherGroup *gin.RouterGroup) { + g := global_object.GetGlobalObject() + + var assetRecords AssetRecordsController + assetRecords.Service = NewAssetRecordsService(g.Db) + + err := inject.Populate(&assetRecords, g.Gin, g.Db, g.Cfg) + if err != nil { + panic(err) + } + + var assetRecordsGroup *gin.RouterGroup + var assetRecordsAuthGroup *gin.RouterGroup + + if fatherGroup != nil { + assetRecordsGroup = fatherGroup.Group(BaseRoutePath) + assetRecordsAuthGroup = fatherGroup.Group("/", middleware.AuthRequired).Group(BaseRoutePath) + } else { + assetRecordsGroup = assetRecords.Gin.Group(BaseRoutePath) + assetRecordsAuthGroup = assetRecords.Gin.Group("/", middleware.AuthRequired).Group(BaseRoutePath) + } + + // No auth endpoints + assetRecordsGroup.GET("/", assetRecords.List) + assetRecordsGroup.GET("/:id", assetRecords.Detail) + + // Auth required endpoints + assetRecordsAuthGroup.POST("/", assetRecords.Create) +} + +// Create creates a new asset transfer +func (c *AssetRecordsController) Create(ctx *gin.Context) { + user := api.ForContextOnlyUser(ctx) + + var req CreateTransferRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + sdk.LogUserSideError(ctx, err) + ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + return + } + + if !common.ValidateUserWallet(req.ToUser) { + err := fmt.Errorf("invalid target user wallet: %s", req.ToUser) + sdk.LogUserSideError(ctx, err) + ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + return + } + + transferLog, err := c.Service.CreateTransfer( + common.FormatUserWallet(user.Wallet), + common.FormatUserWallet(req.ToUser), + req.AssetName, + req.Amount, + req.Comment, + ) + + if err != nil { + log.Error().Msgf("Error creating transfer: %+v", err) + sdk.LogServerErrorToSentry(ctx, err) + + // Handle specific error cases + if err.Error() == ErrSameUserTransfer || + err.Error() == ErrInvalidAmount || + err.Error() == ErrNoAssetRecords || + err.Error() == ErrInsufficientBalance { + ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + return + } + + ctx.JSON(http.StatusInternalServerError, api.ServerError(err)) + return + } + + ctx.JSON(http.StatusCreated, api.Success(map[string]any{ + FieldTransferID: transferLog.ID, + FieldStatus: StatusSuccess, + })) +} + +// List returns paginated asset transfer records with optional query filters +func (c *AssetRecordsController) List(ctx *gin.Context) { + var queryParams TransferListQueryParams + if err := ctx.ShouldBindQuery(&queryParams); err != nil { + sdk.LogUserSideError(ctx, err) + ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + return + } + + // Validate pagination parameters + if queryParams.Page <= 0 { + queryParams.Page = DefaultPageNumber + } + if queryParams.Size <= 0 || queryParams.Size > MaxPageSize { + queryParams.Size = DefaultPageSize + } + + transferLogs, total, err := c.Service.ListTransfers(queryParams.Page, queryParams.Size, queryParams.FromUser, queryParams.ToUser) + if err != nil { + log.Error().Msgf("Error listing transfers: %+v", err) + sdk.LogServerErrorToSentry(ctx, err) + ctx.JSON(http.StatusInternalServerError, api.ServerError(err)) + return + } + + // Convert to response format + response := make([]*TransferResponse, len(transferLogs)) + for i, transferLog := range transferLogs { + response[i] = &TransferResponse{ + ID: transferLog.ID, + FromUser: transferLog.FromUser, + ToUser: transferLog.ToUser, + AssetName: transferLog.AssetName, + Amount: transferLog.Amount, + TransactionTs: transferLog.TransactionTs, + Result: transferLog.Result, + Comment: transferLog.Comment, + } + } + + ctx.JSON(http.StatusOK, api.Success(api.ListReplyData{ + Page: queryParams.Page, + Size: queryParams.Size, + Total: total, + Rows: response, + })) +} + +// Detail returns details of a single asset transfer record +func (c *AssetRecordsController) Detail(ctx *gin.Context) { + transferIDStr := ctx.Param("id") + transferID, err := strconv.ParseUint(transferIDStr, 10, 32) + if err != nil { + sdk.LogUserSideError(ctx, err) + ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + return + } + + transferLog, err := c.Service.GetTransferByID(uint(transferID)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + sdk.LogUserSideError(ctx, err) + ctx.JSON(http.StatusNotFound, api.BadRequest(errors.New(ErrTransactionNotFound))) + return + } + log.Error().Msgf("Error getting transfer detail: %+v", err) + sdk.LogServerErrorToSentry(ctx, err) + ctx.JSON(http.StatusInternalServerError, api.ServerError(err)) + return + } + + response := &TransferResponse{ + ID: transferLog.ID, + FromUser: transferLog.FromUser, + ToUser: transferLog.ToUser, + AssetName: transferLog.AssetName, + Amount: transferLog.Amount, + TransactionTs: transferLog.TransactionTs, + Result: transferLog.Result, + Comment: transferLog.Comment, + } + + ctx.JSON(http.StatusOK, api.Success(response)) +} diff --git a/internal_inject/asset_records/asset_records.dto.go b/internal_inject/asset_records/asset_records.dto.go new file mode 100644 index 00000000..99e7fd5d --- /dev/null +++ b/internal_inject/asset_records/asset_records.dto.go @@ -0,0 +1,33 @@ +package asset_records_inject + +import ( + "github.com/shopspring/decimal" +) + +// CreateTransferRequest represents the request payload for creating a new asset transfer +type CreateTransferRequest struct { + ToUser string `json:"to" binding:"required"` + AssetName string `json:"asset_name"` + Amount decimal.Decimal `json:"amount" binding:"required,gt=0"` + Comment string `json:"comment"` +} + +// TransferListQueryParams represents query parameters for listing transfers +type TransferListQueryParams struct { + FromUser string `form:"from_user"` + ToUser string `form:"to_user"` + Page int `form:"page,default=1"` + Size int `form:"size,default=20"` +} + +// TransferResponse represents the response format for asset transfer records +type TransferResponse struct { + ID uint `json:"id"` + FromUser string `json:"from_user"` + ToUser string `json:"to_user"` + AssetName string `json:"asset_name"` + Amount decimal.Decimal `json:"amount"` + TransactionTs int64 `json:"transaction_ts"` + Result string `json:"result"` + Comment string `json:"comment"` +} diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go new file mode 100644 index 00000000..f6fa84e4 --- /dev/null +++ b/internal_inject/asset_records/asset_records.service.go @@ -0,0 +1,134 @@ +package asset_records_inject + +import ( + "errors" + "fmt" + "strings" + + "github.com/rs/zerolog/log" + "github.com/shopspring/decimal" + "github.com/theseed-labs/os-backend/internal/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// AssetRecordsService handles business logic for asset transfers +type AssetRecordsService struct { + db *gorm.DB +} + +// NewAssetRecordsService creates a new instance of AssetRecordsService +func NewAssetRecordsService(db *gorm.DB) *AssetRecordsService { + return &AssetRecordsService{db: db} +} + +// CreateTransfer creates a new asset transfer with all necessary validations +// This implementation uses pessimistic locking within transaction to prevent race conditions +func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, amount decimal.Decimal, comment string) (*model.UserAssetTransferLog, error) { + // Set default asset name to "see" if empty + if assetName == "" { + assetName = DefaultTransferAssetName + } + upperAssetName := strings.ToUpper(assetName) + + log.Debug().Msgf("CreateTransfer: fromUser=%s, toUser=%s, assetName=%s, amount=%s, comment=%s", fromUser, toUser, upperAssetName, amount, comment) + + // Validate that from and to users are different + if fromUser == toUser { + return nil, errors.New(ErrSameUserTransfer) + } + + // Validate asset amount is positive + if amount.LessThanOrEqual(decimal.Zero) { + return nil, errors.New(ErrInvalidAmount) + } + + var transferLog *model.UserAssetTransferLog + + // Execute all operations within a database transaction + err := s.db.Transaction(func(tx *gorm.DB) error { + // Create transfer log first + var createErr error + transferLog, createErr = model.UserAssetTransferLogModel.Create(tx, fromUser, toUser, upperAssetName, amount, comment) + if createErr != nil { + return fmt.Errorf("%s: %w", ErrCreatingTransfer, createErr) + } + + // Lock and check from user's balance within transaction using SELECT FOR UPDATE + var fromUserRecord model.UserAssetRecord + err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}). + Where("user_wallet = ? AND asset_name = ?", fromUser, upperAssetName). + First(&fromUserRecord).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) + return errors.New(ErrNoAssetRecords) + } + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) + return fmt.Errorf("%s: %w", ErrCheckingBalance, err) + } + + // Check if balance is sufficient + if fromUserRecord.DealtAmount.Cmp(amount) < 0 { + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) + return errors.New(ErrInsufficientBalance) + } + + // Atomically update balances using direct SQL + // Deduct amount from from user + result := tx.Model(&model.UserAssetRecord{}). + Where("user_wallet = ? AND asset_name = ?", fromUser, upperAssetName). + Update("dealt_amount", gorm.Expr("dealt_amount::decimal - ?", amount.String())) + if result.Error != nil { + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) + return fmt.Errorf("%s: %w", ErrUpdatingFromUser, result.Error) + } + + // Add amount to to user (create record if not exists) + var toUserRecord model.UserAssetRecord + err = tx.Where("user_wallet = ? AND asset_name = ?", toUser, upperAssetName).First(&toUserRecord).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + // Create new record for to user + if createErr := model.UserAssetRecordModel.CreateOrUpdate(tx, toUser, upperAssetName, amount, decimal.Zero); createErr != nil { + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) + return fmt.Errorf("%s: %w", ErrUpdatingToUser, createErr) + } + } else if err != nil { + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) + return fmt.Errorf("%s: %w", ErrUpdatingToUser, err) + } else { + // Update existing record + result := tx.Model(&model.UserAssetRecord{}). + Where("user_wallet = ? AND asset_name = ?", toUser, upperAssetName). + Update("dealt_amount", gorm.Expr("dealt_amount::decimal + ?", amount.String())) + if result.Error != nil { + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) + return fmt.Errorf("%s: %w", ErrUpdatingToUser, result.Error) + } + } + + // Update transfer result to success + if updateErr := model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultSuccess); updateErr != nil { + return fmt.Errorf("%s: %w", ErrUpdatingResult, updateErr) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return transferLog, nil +} + +// ListTransfers returns paginated asset transfer records with optional filters +func (s *AssetRecordsService) ListTransfers(page, size int, fromUser, toUser string) ([]*model.UserAssetTransferLog, int64, error) { + return model.UserAssetTransferLogModel.ListPaginated(s.db, page, size, fromUser, toUser) +} + +// GetTransferByID returns a single transfer record by ID +func (s *AssetRecordsService) GetTransferByID(id uint) (*model.UserAssetTransferLog, error) { + return model.UserAssetTransferLogModel.GetByID(s.db, id) +} diff --git a/internal_inject/asset_records/consts.go b/internal_inject/asset_records/consts.go new file mode 100644 index 00000000..ee4efd7a --- /dev/null +++ b/internal_inject/asset_records/consts.go @@ -0,0 +1,53 @@ +package asset_records_inject + +// Error messages +const ( + ErrSameUserTransfer = "from_user and to_user cannot be the same" + ErrInvalidAmount = "amount must be greater than zero" + ErrNoAssetRecords = "from_user has no asset records for this asset" + ErrInsufficientBalance = "insufficient balance" + ErrTransactionNotFound = "transaction record not found" + ErrCheckingBalance = "error checking from user balance" + ErrCreatingTransfer = "error creating transfer log" + ErrUpdatingFromUser = "error updating from user processing amount" + ErrUpdatingToUser = "error updating to user processing amount" + ErrUpdatingResult = "error updating transfer result" + ErrCompletingFromUser = "error completing from user transaction" + ErrCompletingToUser = "error completing to user transaction" +) + +// Status values +const ( + StatusSuccess = "success" +) + +// Route constants +const ( + BaseRoutePath = "/asset_trade" +) + +// Pagination defaults +const ( + DefaultPageSize = 20 + MaxPageSize = 100 + DefaultPageNumber = 1 +) + +// Field names +const ( + FieldTransferID = "transfer_id" + FieldStatus = "status" + FieldID = "id" + FieldFromUser = "from_user" + FieldToUser = "to_user" + FieldAssetName = "asset_name" + FieldAmount = "amount" + FieldTimestamp = "transaction_ts" + FieldResult = "result" + FieldComment = "comment" +) + +// Default values +const ( + DefaultTransferAssetName = "SEE" +) \ No newline at end of file diff --git a/internal_inject/data_srv/data_srv.dto.go b/internal_inject/data_srv/data_srv.dto.go index 8436826b..456686f6 100644 --- a/internal_inject/data_srv/data_srv.dto.go +++ b/internal_inject/data_srv/data_srv.dto.go @@ -14,7 +14,7 @@ const dbQueryForSeasonTotalRewards = `select season_id, from applications join seasons on season_id = seasons.id where applications.type = 'NEW_REWARD' - and applications.asset_name = 'SCR' + and applications.asset_name = 'WANG' and applications.state in ('completed') and applications.sub_type IN (NULL ,'', 'MintRewards') GROUP by season_id, target_user_wallet, seasons.name, season_idx` diff --git a/internal_inject/projects/projects.service.go b/internal_inject/projects/projects.service.go index ca6d2c4c..12e025ad 100644 --- a/internal_inject/projects/projects.service.go +++ b/internal_inject/projects/projects.service.go @@ -97,7 +97,7 @@ func (s *ProjectsService) Create(ctx *gin.Context, req *CreateReq) (int, *api.Re if req.ScrBudget != decimal.Zero { budgets = append(budgets, &model.ProjectBudget{ ProjectID: proj.ID, - AssetName: "SCR", + AssetName: internal.AssetNameWANG, TotalAmount: req.ScrBudget, RemainAmount: req.ScrBudget, CreateTs: model.GetCurrentUtcEpochSecond(), @@ -108,7 +108,7 @@ func (s *ProjectsService) Create(ctx *gin.Context, req *CreateReq) (int, *api.Re if req.UsdcBudget != decimal.Zero { budgets = append(budgets, &model.ProjectBudget{ ProjectID: proj.ID, - AssetName: "USDC", + AssetName: internal.AssetNameUSDC, TotalAmount: req.UsdcBudget, RemainAmount: req.UsdcBudget, CreateTs: model.GetCurrentUtcEpochSecond(), diff --git a/internal_inject/treasury/treasury.controller.go b/internal_inject/treasury/treasury.controller.go index 624051db..18e77c62 100644 --- a/internal_inject/treasury/treasury.controller.go +++ b/internal_inject/treasury/treasury.controller.go @@ -7,6 +7,7 @@ import ( "github.com/facebookgo/inject" "github.com/gin-gonic/gin" "github.com/theseed-labs/os-backend/global_object" + "github.com/theseed-labs/os-backend/internal" "github.com/theseed-labs/os-backend/internal/api" "github.com/theseed-labs/os-backend/internal/config" "github.com/theseed-labs/os-backend/internal/middleware" @@ -71,6 +72,24 @@ func (c *TreasuryController) GetOrCreateCurrentAssetRecords(ctx *gin.Context) { return } + // Get SEE records + totalSeeAmount, err := c.TreasuryService.GetSumOfIssuedAsset(c.Db, internal.AssetNameSEE, 0) + if err != nil { + sdk.LogServerErrorToSentry(ctx, err) + ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("get sum of issued asset error detail:"+err.Error()))) + return + } + + seasonSeeAmount, err := c.TreasuryService.GetSumOfIssuedAsset(c.Db, internal.AssetNameSEE, currQuarterTreasuryRecord.SeasonId) + if err != nil { + sdk.LogServerErrorToSentry(ctx, err) + ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("get sum of issued asset error detail:"+err.Error()))) + return + } + + treasuryAssetResp.SeeTotalAmount = totalSeeAmount + treasuryAssetResp.SeeUsedAmount = seasonSeeAmount + ctx.JSON(http.StatusOK, api.Success(treasuryAssetResp)) } diff --git a/internal_inject/treasury/treasury.service.go b/internal_inject/treasury/treasury.service.go index 036231aa..f601fde4 100644 --- a/internal_inject/treasury/treasury.service.go +++ b/internal_inject/treasury/treasury.service.go @@ -3,8 +3,10 @@ package treasury_inject import ( "errors" "net/http" + "strings" "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" "github.com/theseed-labs/os-backend/internal" "github.com/theseed-labs/os-backend/internal/api" "github.com/theseed-labs/os-backend/internal/common" @@ -64,3 +66,28 @@ func (s *TreasuryService) UpdateAssets(ctx *gin.Context) (int, *api.Reply) { return http.StatusOK, api.Success(currQuarterTreasuryRecord) } + +func (s *TreasuryService) GetSumOfIssuedAsset(db *gorm.DB, assetName string, seasonId uint) (decimal.Decimal, error) { + var applications []model.Application + query := db.Model(&model.Application{}).Where(&model.Application{ + AssetName: strings.ToUpper(assetName), + }) + + if seasonId != 0 { + query = query.Where("season_id = ?", seasonId) + } + + // Load all records + err := query.Find(&applications).Error + if err != nil { + return decimal.Zero, err + } + + // Sum in golang + var sum decimal.Decimal + for _, app := range applications { + sum = sum.Add(app.AssetAmount) + } + + return sum, nil +} diff --git a/internal_inject/user/user.controller.go b/internal_inject/user/user.controller.go index f8e30390..8c19dfc9 100644 --- a/internal_inject/user/user.controller.go +++ b/internal_inject/user/user.controller.go @@ -404,11 +404,22 @@ func (ctrl *UserController) MetaforoActivities(ctx *gin.Context) { } func (ctrl *UserController) Detail(ctx *gin.Context) { - user, _ := api.ForContextUserAndDB(ctx) + user, db := api.ForContextUserAndDB(ctx) sppClient := sdk.GetSppClient() seepassResp, err := api.GetCachedSeepassData(sppClient, user.Wallet, false) if err == nil { + + // Get see data from db + assetRecords, _ := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps(db, user.Wallet, internal.AssetNameSEE) + api.PrintStructAsJson(assetRecords, "assetRecords") + if len(assetRecords) == 0 { + seepassResp.See.Amount = "0" + } else { + seepassResp.See.Amount = assetRecords[0].DealtAmount.String() + } + + // populate deepseek key _, ok := findString(seepassResp.Roles, "SEEDAO_MEMBER") if ok { dsChatClient := sdk.GetDsChatClient()