From 13e9f5f51bf0bf35158f7f8a246d64545672489a Mon Sep 17 00:00:00 2001 From: hzmangel Date: Wed, 30 Jul 2025 23:03:41 +0800 Subject: [PATCH 01/29] Add see field in user profile response --- internal/api/user/user.go | 8 ++++++++ internal/sdk/spp_client.go | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/internal/api/user/user.go b/internal/api/user/user.go index edebc38f..5bae5a66 100644 --- a/internal/api/user/user.go +++ b/internal/api/user/user.go @@ -445,6 +445,14 @@ func Detail(ctx *gin.Context) { return } + // Get see data from db + assetRecords, err := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps(db, user.Wallet, "SEE") + if len(assetRecords) == 0 { + seepassResp.See.Amount = "0" + } else { + seepassResp.See.Amount = assetRecords[0].DealtAmount.String() + } + log.Warn().Msgf("query seepass data error, wallet: %s, error: %+v", user.Wallet, err) u, err := model.UserModel.Detail(db, user.Wallet) 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"` From d1607acbd5d53608e701540d5c008ae459320596 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Thu, 31 Jul 2025 01:51:27 +0800 Subject: [PATCH 02/29] feat(asset_records): add asset transfer functionality with validation - Implement asset transfer service with balance checks and transaction handling - Add controller endpoints for creating and querying transfer records - Include DTOs for request/response and error constants - Add model for tracking transfer logs and user balances --- cmd/apiserver/main.go | 3 + internal/model/user_asset_transfer_log.go | 144 +++++++++++++++ .../asset_records/asset_records.controller.go | 173 ++++++++++++++++++ .../asset_records/asset_records.dto.go | 34 ++++ .../asset_records/asset_records.service.go | 97 ++++++++++ internal_inject/asset_records/consts.go | 53 ++++++ 6 files changed, 504 insertions(+) create mode 100644 internal/model/user_asset_transfer_log.go create mode 100644 internal_inject/asset_records/asset_records.controller.go create mode 100644 internal_inject/asset_records/asset_records.dto.go create mode 100644 internal_inject/asset_records/asset_records.service.go create mode 100644 internal_inject/asset_records/consts.go 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/internal/model/user_asset_transfer_log.go b/internal/model/user_asset_transfer_log.go new file mode 100644 index 00000000..722e471f --- /dev/null +++ b/internal/model/user_asset_transfer_log.go @@ -0,0 +1,144 @@ +package model + +import ( + "time" + + "github.com/shopspring/decimal" + "github.com/theseed-labs/os-backend/internal" + "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 + + // Create or get from user + if err := db.Where(User{Wallet: fromUser}).Attrs(User{ + CreatedAt: time.Now().In(internal.ProjectTimezone), + UpdatedAt: time.Now().In(internal.ProjectTimezone), + CreateTs: GetCurrentUtcEpochSecond(), + UpdateTs: GetCurrentUtcEpochSecond(), + }).FirstOrCreate(&fromUserRecord).Error; err != nil { + return nil, err + } + + // Create or get to user + if err := db.Where(User{Wallet: toUser}).Attrs(User{ + CreatedAt: time.Now().In(internal.ProjectTimezone), + UpdatedAt: time.Now().In(internal.ProjectTimezone), + CreateTs: GetCurrentUtcEpochSecond(), + UpdateTs: GetCurrentUtcEpochSecond(), + }).FirstOrCreate(&toUserRecord).Error; err != nil { + return nil, err + } + + transferLog := &UserAssetTransferLog{ + FromUser: fromUser, + ToUser: toUser, + AssetName: assetName, + Amount: amount, + TransactionTs: GetCurrentUtcEpochSecond(), + Result: TransferResultPending, + Comment: comment, + CreateTs: GetCurrentUtcEpochSecond(), + UpdateTs: GetCurrentUtcEpochSecond(), + } + + 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, + "update_ts": GetCurrentUtcEpochSecond(), + "updated_at": time.Now().In(internal.ProjectTimezone), + }).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 +} \ No newline at end of file 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..407e76fa --- /dev/null +++ b/internal_inject/asset_records/asset_records.controller.go @@ -0,0 +1,173 @@ +package asset_records_inject + +import ( + "errors" + "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/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) { + var req CreateTransferRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + sdk.LogUserSideError(ctx, err) + ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + return + } + + transferLog, err := c.Service.CreateTransfer(req.FromUser, 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 + } + + transfers, 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(transfers)) + for i, transfer := range transfers { + response[i] = &TransferResponse{ + ID: transfer.ID, + FromUser: transfer.FromUser, + ToUser: transfer.ToUser, + AssetName: transfer.AssetName, + Amount: transfer.Amount, + TransactionTs: transfer.TransactionTs, + Result: transfer.Result, + Comment: transfer.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 + } + + transfer, 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: transfer.ID, + FromUser: transfer.FromUser, + ToUser: transfer.ToUser, + AssetName: transfer.AssetName, + Amount: transfer.Amount, + TransactionTs: transfer.TransactionTs, + Result: transfer.Result, + Comment: transfer.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..5cdeeafa --- /dev/null +++ b/internal_inject/asset_records/asset_records.dto.go @@ -0,0 +1,34 @@ +package asset_records_inject + +import ( + "github.com/shopspring/decimal" +) + +// CreateTransferRequest represents the request payload for creating a new asset transfer +type CreateTransferRequest struct { + FromUser string `json:"from_user" binding:"required"` + ToUser string `json:"to_user" 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..abab59d6 --- /dev/null +++ b/internal_inject/asset_records/asset_records.service.go @@ -0,0 +1,97 @@ +package asset_records_inject + +import ( + "errors" + "fmt" + + "github.com/shopspring/decimal" + "github.com/theseed-labs/os-backend/internal/model" + "gorm.io/gorm" +) + +// 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 +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 = DefaultAssetName + } + // 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) + } + + // Check if from user has sufficient balance + fromUserRecords, err := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps(s.db, fromUser, assetName) + if err != nil { + return nil, fmt.Errorf("%s: %w", ErrCheckingBalance, err) + } + + if len(fromUserRecords) == 0 { + return nil, errors.New(ErrNoAssetRecords) + } + + availableBalance := fromUserRecords[0].DealtAmount + if availableBalance.Cmp(amount) < 0 { + return nil, errors.New(ErrInsufficientBalance) + } + + var transferLog *model.UserAssetTransferLog + + // Execute all operations within a database transaction + err = s.db.Transaction(func(tx *gorm.DB) error { + // Create transfer log + var createErr error + transferLog, createErr = model.UserAssetTransferLogModel.Create(tx, fromUser, toUser, assetName, amount, comment) + if createErr != nil { + return fmt.Errorf("%s: %w", ErrCreatingTransfer, createErr) + } + + // Deduct amount from from user's dealt amount + if updateErr := model.UserAssetRecordModel.CreateOrUpdate(tx, fromUser, assetName, decimal.Zero, amount.Neg()); updateErr != nil { + return fmt.Errorf("%s: %w", ErrUpdatingFromUser, updateErr) + } + + // Add amount to to user's dealt amount + if updateErr := model.UserAssetRecordModel.CreateOrUpdate(tx, toUser, assetName, decimal.Zero, amount); updateErr != nil { + return fmt.Errorf("%s: %w", ErrUpdatingToUser, updateErr) + } + + // 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..1e8ea96b --- /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 ( + DefaultAssetName = "see" +) \ No newline at end of file From de3c430f68798c4e829a99e4cd0db1d75e7ba1ff Mon Sep 17 00:00:00 2001 From: hzmangel Date: Thu, 31 Jul 2025 01:52:47 +0800 Subject: [PATCH 03/29] feat(model): add UserAssetTransferLog to MigrateTables --- internal/model/utils.go | 1 + 1 file changed, 1 insertion(+) 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{}, From edb3f6f4900ff89bb12ff7650ab5fd007f1be947 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Thu, 31 Jul 2025 10:36:35 +0800 Subject: [PATCH 04/29] refactor(asset-transfer): simplify transfer logic and improve error handling - Remove FromUser from request DTO and use authenticated user's wallet - Simplify user lookup logic by removing unnecessary user creation - Add automatic transfer failure marking on balance update errors - Normalize asset names to uppercase - Remove redundant timestamp fields from transfer log --- internal/model/user_asset_transfer_log.go | 28 +++--------- .../asset_records/asset_records.controller.go | 44 ++++++++++--------- .../asset_records/asset_records.dto.go | 3 +- .../asset_records/asset_records.service.go | 18 +++++--- 4 files changed, 42 insertions(+), 51 deletions(-) diff --git a/internal/model/user_asset_transfer_log.go b/internal/model/user_asset_transfer_log.go index 722e471f..74984d1e 100644 --- a/internal/model/user_asset_transfer_log.go +++ b/internal/model/user_asset_transfer_log.go @@ -4,7 +4,6 @@ import ( "time" "github.com/shopspring/decimal" - "github.com/theseed-labs/os-backend/internal" "github.com/theseed-labs/os-backend/internal/common" "gorm.io/gorm" ) @@ -49,24 +48,13 @@ func (*userAssetTransferLogModel) Create(db *gorm.DB, fromUser string, toUser st // Ensure both users exist in the database var fromUserRecord, toUserRecord User - - // Create or get from user - if err := db.Where(User{Wallet: fromUser}).Attrs(User{ - CreatedAt: time.Now().In(internal.ProjectTimezone), - UpdatedAt: time.Now().In(internal.ProjectTimezone), - CreateTs: GetCurrentUtcEpochSecond(), - UpdateTs: GetCurrentUtcEpochSecond(), - }).FirstOrCreate(&fromUserRecord).Error; err != nil { + + // get from user and to user + if err := db.Where(User{Wallet: fromUser}).First(&fromUserRecord).Error; err != nil { return nil, err } - // Create or get to user - if err := db.Where(User{Wallet: toUser}).Attrs(User{ - CreatedAt: time.Now().In(internal.ProjectTimezone), - UpdatedAt: time.Now().In(internal.ProjectTimezone), - CreateTs: GetCurrentUtcEpochSecond(), - UpdateTs: GetCurrentUtcEpochSecond(), - }).FirstOrCreate(&toUserRecord).Error; err != nil { + if err := db.Where(User{Wallet: toUser}).FirstOrCreate(&toUserRecord).Error; err != nil { return nil, err } @@ -78,8 +66,6 @@ func (*userAssetTransferLogModel) Create(db *gorm.DB, fromUser string, toUser st TransactionTs: GetCurrentUtcEpochSecond(), Result: TransferResultPending, Comment: comment, - CreateTs: GetCurrentUtcEpochSecond(), - UpdateTs: GetCurrentUtcEpochSecond(), } if err := db.Create(transferLog).Error; err != nil { @@ -92,9 +78,7 @@ func (*userAssetTransferLogModel) Create(db *gorm.DB, fromUser string, toUser st // 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, - "update_ts": GetCurrentUtcEpochSecond(), - "updated_at": time.Now().In(internal.ProjectTimezone), + "result": result, }).Error } @@ -141,4 +125,4 @@ func (*userAssetTransferLogModel) GetByID(db *gorm.DB, id uint) (*UserAssetTrans return nil, err } return &log, nil -} \ No newline at end of file +} diff --git a/internal_inject/asset_records/asset_records.controller.go b/internal_inject/asset_records/asset_records.controller.go index 407e76fa..da94d8f2 100644 --- a/internal_inject/asset_records/asset_records.controller.go +++ b/internal_inject/asset_records/asset_records.controller.go @@ -56,6 +56,8 @@ func Register(fatherGroup *gin.RouterGroup) { // 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) @@ -63,7 +65,7 @@ func (c *AssetRecordsController) Create(ctx *gin.Context) { return } - transferLog, err := c.Service.CreateTransfer(req.FromUser, req.ToUser, req.AssetName, req.Amount, req.Comment) + transferLog, err := c.Service.CreateTransfer(user.Wallet, req.ToUser, req.AssetName, req.Amount, req.Comment) if err != nil { log.Error().Msgf("Error creating transfer: %+v", err) sdk.LogServerErrorToSentry(ctx, err) @@ -104,7 +106,7 @@ func (c *AssetRecordsController) List(ctx *gin.Context) { queryParams.Size = DefaultPageSize } - transfers, total, err := c.Service.ListTransfers(queryParams.Page, queryParams.Size, queryParams.FromUser, queryParams.ToUser) + 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) @@ -113,17 +115,17 @@ func (c *AssetRecordsController) List(ctx *gin.Context) { } // Convert to response format - response := make([]*TransferResponse, len(transfers)) - for i, transfer := range transfers { + response := make([]*TransferResponse, len(transferLogs)) + for i, transferLog := range transferLogs { response[i] = &TransferResponse{ - ID: transfer.ID, - FromUser: transfer.FromUser, - ToUser: transfer.ToUser, - AssetName: transfer.AssetName, - Amount: transfer.Amount, - TransactionTs: transfer.TransactionTs, - Result: transfer.Result, - Comment: transfer.Comment, + ID: transferLog.ID, + FromUser: transferLog.FromUser, + ToUser: transferLog.ToUser, + AssetName: transferLog.AssetName, + Amount: transferLog.Amount, + TransactionTs: transferLog.TransactionTs, + Result: transferLog.Result, + Comment: transferLog.Comment, } } @@ -145,7 +147,7 @@ func (c *AssetRecordsController) Detail(ctx *gin.Context) { return } - transfer, err := c.Service.GetTransferByID(uint(transferID)) + transferLog, err := c.Service.GetTransferByID(uint(transferID)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { sdk.LogUserSideError(ctx, err) @@ -159,14 +161,14 @@ func (c *AssetRecordsController) Detail(ctx *gin.Context) { } response := &TransferResponse{ - ID: transfer.ID, - FromUser: transfer.FromUser, - ToUser: transfer.ToUser, - AssetName: transfer.AssetName, - Amount: transfer.Amount, - TransactionTs: transfer.TransactionTs, - Result: transfer.Result, - Comment: transfer.Comment, + 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 index 5cdeeafa..99e7fd5d 100644 --- a/internal_inject/asset_records/asset_records.dto.go +++ b/internal_inject/asset_records/asset_records.dto.go @@ -6,8 +6,7 @@ import ( // CreateTransferRequest represents the request payload for creating a new asset transfer type CreateTransferRequest struct { - FromUser string `json:"from_user" binding:"required"` - ToUser string `json:"to_user" binding:"required"` + ToUser string `json:"to" binding:"required"` AssetName string `json:"asset_name"` Amount decimal.Decimal `json:"amount" binding:"required,gt=0"` Comment string `json:"comment"` diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index abab59d6..cd9a158d 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -3,7 +3,9 @@ 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" @@ -21,9 +23,11 @@ func NewAssetRecordsService(db *gorm.DB) *AssetRecordsService { // CreateTransfer creates a new asset transfer with all necessary validations func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, amount decimal.Decimal, comment string) (*model.UserAssetTransferLog, error) { + upperAssetName := strings.ToUpper(assetName) + log.Debug().Msgf("CreateTransfer: fromUser=%s, toUser=%s, assetName=%s, amount=%s, comment=%s", fromUser, toUser, upperAssetName, amount, comment) // Set default asset name to "see" if empty - if assetName == "" { - assetName = DefaultAssetName + if upperAssetName == "" { + upperAssetName = DefaultAssetName } // Validate that from and to users are different if fromUser == toUser { @@ -36,7 +40,7 @@ func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, } // Check if from user has sufficient balance - fromUserRecords, err := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps(s.db, fromUser, assetName) + fromUserRecords, err := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps(s.db, fromUser, upperAssetName) if err != nil { return nil, fmt.Errorf("%s: %w", ErrCheckingBalance, err) } @@ -56,18 +60,20 @@ func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, err = s.db.Transaction(func(tx *gorm.DB) error { // Create transfer log var createErr error - transferLog, createErr = model.UserAssetTransferLogModel.Create(tx, fromUser, toUser, assetName, amount, comment) + transferLog, createErr = model.UserAssetTransferLogModel.Create(tx, fromUser, toUser, upperAssetName, amount, comment) if createErr != nil { return fmt.Errorf("%s: %w", ErrCreatingTransfer, createErr) } // Deduct amount from from user's dealt amount - if updateErr := model.UserAssetRecordModel.CreateOrUpdate(tx, fromUser, assetName, decimal.Zero, amount.Neg()); updateErr != nil { + if updateErr := model.UserAssetRecordModel.CreateOrUpdate(tx, fromUser, upperAssetName, decimal.Zero, amount.Neg()); updateErr != nil { + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) return fmt.Errorf("%s: %w", ErrUpdatingFromUser, updateErr) } // Add amount to to user's dealt amount - if updateErr := model.UserAssetRecordModel.CreateOrUpdate(tx, toUser, assetName, decimal.Zero, amount); updateErr != nil { + if updateErr := model.UserAssetRecordModel.CreateOrUpdate(tx, toUser, upperAssetName, decimal.Zero, amount); updateErr != nil { + model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) return fmt.Errorf("%s: %w", ErrUpdatingToUser, updateErr) } From 051bad7bd88ec6b5e19653b7330e37292c8f1a9a Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 1 Aug 2025 12:59:13 +0800 Subject: [PATCH 05/29] Move see asset fetch to correct place, and add const for SEE to avoid magic string --- internal/api/user/user.go | 8 -------- internal/const.go | 6 ++++++ internal_inject/user/user.controller.go | 13 ++++++++++++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/internal/api/user/user.go b/internal/api/user/user.go index 5bae5a66..edebc38f 100644 --- a/internal/api/user/user.go +++ b/internal/api/user/user.go @@ -445,14 +445,6 @@ func Detail(ctx *gin.Context) { return } - // Get see data from db - assetRecords, err := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps(db, user.Wallet, "SEE") - if len(assetRecords) == 0 { - seepassResp.See.Amount = "0" - } else { - seepassResp.See.Amount = assetRecords[0].DealtAmount.String() - } - log.Warn().Msgf("query seepass data error, wallet: %s, error: %+v", user.Wallet, err) u, err := model.UserModel.Detail(db, user.Wallet) diff --git a/internal/const.go b/internal/const.go index 38934cd3..cae6b857 100644 --- a/internal/const.go +++ b/internal/const.go @@ -134,6 +134,12 @@ type DecimalsAndContractAddr struct { Addr string } +var ( + AssetNameSCR = "SCR" + AssetNameSEE = "SEE" + AssetNameUSDT = "USDT" +) + // AssertDecimalsAndContractAddr supported token for sending application to QuickAccounting var AssertDecimalsAndContractAddr = map[string]*DecimalsAndContractAddr{ "SCR": {Decimals: 18, Addr: ScrContractAddr}, 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() From ef4e4f0bebc26dfe96ef3f067f0f68517ae88ab3 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 1 Aug 2025 13:43:43 +0800 Subject: [PATCH 06/29] fix(user_asset_record): normalize asset names to uppercase for consistency Ensure asset names are consistently handled in uppercase across all operations to prevent case sensitivity issues in database queries and updates. --- internal/model/user_asset_record.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 } From 187f038e7d501bb4c345ca0c8395e00617e368b0 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 1 Aug 2025 13:46:28 +0800 Subject: [PATCH 07/29] refactor(asset_records): rename default asset name and improve transfer logic --- internal_inject/asset_records/asset_records.service.go | 10 ++++++---- internal_inject/asset_records/consts.go | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index cd9a158d..32b216c0 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -23,12 +23,14 @@ func NewAssetRecordsService(db *gorm.DB) *AssetRecordsService { // CreateTransfer creates a new asset transfer with all necessary validations func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, amount decimal.Decimal, comment string) (*model.UserAssetTransferLog, error) { - upperAssetName := strings.ToUpper(assetName) - log.Debug().Msgf("CreateTransfer: fromUser=%s, toUser=%s, assetName=%s, amount=%s, comment=%s", fromUser, toUser, upperAssetName, amount, comment) // Set default asset name to "see" if empty - if upperAssetName == "" { - upperAssetName = DefaultAssetName + 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) diff --git a/internal_inject/asset_records/consts.go b/internal_inject/asset_records/consts.go index 1e8ea96b..ee4efd7a 100644 --- a/internal_inject/asset_records/consts.go +++ b/internal_inject/asset_records/consts.go @@ -49,5 +49,5 @@ const ( // Default values const ( - DefaultAssetName = "see" + DefaultTransferAssetName = "SEE" ) \ No newline at end of file From 7eeedf1a55f4989c2dd098d90ddba3ed9c713291 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 1 Aug 2025 22:50:39 +0800 Subject: [PATCH 08/29] feat(asset_records): add wallet validation and formatting in transfer Validate target user wallet before transfer and format both source and target wallets using common utilities to ensure consistency and prevent invalid transfers. --- .../asset_records/asset_records.controller.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/internal_inject/asset_records/asset_records.controller.go b/internal_inject/asset_records/asset_records.controller.go index da94d8f2..f70de88a 100644 --- a/internal_inject/asset_records/asset_records.controller.go +++ b/internal_inject/asset_records/asset_records.controller.go @@ -2,6 +2,7 @@ package asset_records_inject import ( "errors" + "fmt" "net/http" "strconv" @@ -10,6 +11,7 @@ import ( "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" @@ -65,7 +67,21 @@ func (c *AssetRecordsController) Create(ctx *gin.Context) { return } - transferLog, err := c.Service.CreateTransfer(user.Wallet, req.ToUser, req.AssetName, req.Amount, req.Comment) + 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) From 8f0fcb16e893b525b12f5cd0219411152cfd8c8c Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 00:05:42 +0800 Subject: [PATCH 09/29] Add new const value for budget_p2 and asset name --- internal/const.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/const.go b/internal/const.go index cae6b857..a28bf23e 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" @@ -135,9 +136,11 @@ type DecimalsAndContractAddr struct { } var ( + AssetNameWANG = "WANG" AssetNameSCR = "SCR" AssetNameSEE = "SEE" AssetNameUSDT = "USDT" + AssetNameUSDC = "USDC" ) // AssertDecimalsAndContractAddr supported token for sending application to QuickAccounting From 76a2f19819a856e1bf7770dc2068663360f81d18 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 00:08:26 +0800 Subject: [PATCH 10/29] Update magic string into const variable --- internal_inject/projects/projects.service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(), From b073150a638582b35783db473aeb56afa15a6f85 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 00:11:28 +0800 Subject: [PATCH 11/29] Handle budget component p2 and change code to switch mode --- internal/api/proposal/helper.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/api/proposal/helper.go b/internal/api/proposal/helper.go index a4fb9456..2506ecb2 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, internal.ComponentNameBudgetP2: 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: 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 From 0e7728489d419da57880406fece57c2ca0ae56af Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 10:15:45 +0800 Subject: [PATCH 12/29] Add some debug log --- internal/api/proposal/helper.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/api/proposal/helper.go b/internal/api/proposal/helper.go index 2506ecb2..4f0546b2 100644 --- a/internal/api/proposal/helper.go +++ b/internal/api/proposal/helper.go @@ -1786,6 +1786,9 @@ func CreateProjectFromAutoTasks(db *gorm.DB, proposalId uint) (*model.Project, e return nil, err } + log.Error().Msgf("TTT: component data: %q", pComponentRecord.Data) + log.Error().Msgf("TTT: budget params: %+v", budgetParams) + projectBudgetRcds = budgetParams.prepareBudgetRecords(proposalId) case internal.ComponentNameBudget: var budgetParams budgetComponentData From 652119d68b37c481924fd59c15b8a0834bd8b619 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 10:20:25 +0800 Subject: [PATCH 13/29] Move budget_p2 processing to another branch --- internal/api/proposal/helper.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/api/proposal/helper.go b/internal/api/proposal/helper.go index 4f0546b2..2b1160b4 100644 --- a/internal/api/proposal/helper.go +++ b/internal/api/proposal/helper.go @@ -1778,7 +1778,7 @@ func CreateProjectFromAutoTasks(db *gorm.DB, proposalId uint) (*model.Project, e for _, pComponentRecord := range pComponents { if compName, found := getProposalComponentIdNameMapping(db)[pComponentRecord.ComponentID]; found { switch compName { - case internal.ComponentNameBudgetP1, internal.ComponentNameBudgetP2: + case internal.ComponentNameBudgetP1: var budgetParams budgetComponentDataP1 err = json.Unmarshal([]byte(pComponentRecord.Data), &budgetParams) if err != nil { @@ -1786,11 +1786,8 @@ func CreateProjectFromAutoTasks(db *gorm.DB, proposalId uint) (*model.Project, e return nil, err } - log.Error().Msgf("TTT: component data: %q", pComponentRecord.Data) - log.Error().Msgf("TTT: budget params: %+v", budgetParams) - projectBudgetRcds = budgetParams.prepareBudgetRecords(proposalId) - case internal.ComponentNameBudget: + case internal.ComponentNameBudget, internal.ComponentNameBudgetP2: var budgetParams budgetComponentData err = json.Unmarshal([]byte(pComponentRecord.Data), &budgetParams) if err != nil { @@ -1798,6 +1795,9 @@ func CreateProjectFromAutoTasks(db *gorm.DB, proposalId uint) (*model.Project, e return nil, err } + log.Error().Msgf("TTT: component data: %q", pComponentRecord.Data) + log.Error().Msgf("TTT: budget params: %+v", budgetParams) + projectBudgetRcds = budgetParams.prepareBudgetRecords(proposalId) case internal.ComponentNameDeliverables: var deliverableParams commonCreateProjectRelatedData From 9cd607ccfb5fb6cc424359ceffe92cbb8eeb4e21 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 10:27:01 +0800 Subject: [PATCH 14/29] Remove test log --- internal/api/proposal/helper.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/api/proposal/helper.go b/internal/api/proposal/helper.go index 2b1160b4..3839e204 100644 --- a/internal/api/proposal/helper.go +++ b/internal/api/proposal/helper.go @@ -1795,9 +1795,6 @@ func CreateProjectFromAutoTasks(db *gorm.DB, proposalId uint) (*model.Project, e return nil, err } - log.Error().Msgf("TTT: component data: %q", pComponentRecord.Data) - log.Error().Msgf("TTT: budget params: %+v", budgetParams) - projectBudgetRcds = budgetParams.prepareBudgetRecords(proposalId) case internal.ComponentNameDeliverables: var deliverableParams commonCreateProjectRelatedData From 4f9aa37182cae2fe6e919331819fbfb47aab1af8 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 17:24:51 +0800 Subject: [PATCH 15/29] feat(treasury): add SEE asset support and refactor asset constants - Add SeeUsedAmount field to TreasuryAssetsResponse struct - Introduce AssetPrefixUSD constant for USD asset prefix - Update asset name checks to use constants instead of hardcoded strings - Include SEE asset in used amount calculations --- internal/const.go | 11 ++++++----- internal/model/treasury_asset.go | 14 +++++++++----- internal/model/typedef.go | 1 + 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/internal/const.go b/internal/const.go index a28bf23e..fe87005b 100644 --- a/internal/const.go +++ b/internal/const.go @@ -136,11 +136,12 @@ type DecimalsAndContractAddr struct { } var ( - AssetNameWANG = "WANG" - AssetNameSCR = "SCR" - AssetNameSEE = "SEE" - AssetNameUSDT = "USDT" - AssetNameUSDC = "USDC" + AssetNameWANG = "WANG" + AssetNameSCR = "SCR" + AssetNameSEE = "SEE" + AssetPrefixUSD = "USD" + AssetNameUSDT = "USDT" + AssetNameUSDC = "USDC" ) // AssertDecimalsAndContractAddr supported token for sending application to QuickAccounting diff --git a/internal/model/treasury_asset.go b/internal/model/treasury_asset.go index 3a945113..bc9514d6 100644 --- a/internal/model/treasury_asset.go +++ b/internal/model/treasury_asset.go @@ -60,13 +60,13 @@ type TreasuryAuditLog struct { } func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsResponse, error) { - var creditTotal, creditUsed, tokenTotal, tokenUsed decimal.Decimal + var creditTotal, creditUsed, tokenTotal, tokenUsed, seeUsed decimal.Decimal // 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) @@ -84,15 +84,18 @@ func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsRe creditUsed = decimal.Zero tokenUsed = decimal.Zero + seeUsed = 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") { + } else if strings.EqualFold(application.AssetName, internal.AssetNameWANG) { // calculate processing and completed SCR creditUsed = creditUsed.Add(application.AssetAmount) + } else if strings.EqualFold(application.AssetName, internal.AssetNameSEE) { + seeUsed = seeUsed.Add(application.AssetAmount) } else { log.Warn().Msgf("non token or credit asset %s, ignore the application record: %+v", application.AssetName, application) } @@ -103,6 +106,7 @@ func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsRe CreditUsedAmount: creditUsed, TokenTotalAmount: tokenTotal, TokenUsedAmount: tokenUsed, + SeeUsedAmount: seeUsed, }, nil } diff --git a/internal/model/typedef.go b/internal/model/typedef.go index f7362988..d5bd122c 100644 --- a/internal/model/typedef.go +++ b/internal/model/typedef.go @@ -208,6 +208,7 @@ type TreasuryAssetsResponse struct { CreditUsedAmount decimal.Decimal `json:"credit_used_amount"` TokenTotalAmount decimal.Decimal `json:"token_total_amount"` TokenUsedAmount decimal.Decimal `json:"token_used_amount"` + SeeUsedAmount decimal.Decimal `json:"see_used_amount"` } // NewApplicationRequest is used to save new application request data passed from frontend From afbafee57e5544e383f9f5e9e7bcafe1330e352c Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 19:47:28 +0800 Subject: [PATCH 16/29] Update SEE related stats function --- internal/model/treasury_asset.go | 6 +----- internal/model/typedef.go | 5 ++++- .../treasury/treasury.controller.go | 19 ++++++++++++++++++ internal_inject/treasury/treasury.service.go | 20 +++++++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/internal/model/treasury_asset.go b/internal/model/treasury_asset.go index bc9514d6..0b4c21e2 100644 --- a/internal/model/treasury_asset.go +++ b/internal/model/treasury_asset.go @@ -60,7 +60,7 @@ type TreasuryAuditLog struct { } func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsResponse, error) { - var creditTotal, creditUsed, tokenTotal, tokenUsed, seeUsed decimal.Decimal + var creditTotal, creditUsed, tokenTotal, tokenUsed decimal.Decimal // Calculate total amount for _, detailedRcd := range r.DetailedRecords { @@ -84,7 +84,6 @@ func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsRe creditUsed = decimal.Zero tokenUsed = decimal.Zero - seeUsed = decimal.Zero for _, application := range applications { if strings.HasPrefix(application.AssetName, internal.AssetPrefixUSD) { // only calculate completed USD @@ -94,8 +93,6 @@ func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsRe } else if strings.EqualFold(application.AssetName, internal.AssetNameWANG) { // calculate processing and completed SCR creditUsed = creditUsed.Add(application.AssetAmount) - } else if strings.EqualFold(application.AssetName, internal.AssetNameSEE) { - seeUsed = seeUsed.Add(application.AssetAmount) } else { log.Warn().Msgf("non token or credit asset %s, ignore the application record: %+v", application.AssetName, application) } @@ -106,7 +103,6 @@ func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsRe CreditUsedAmount: creditUsed, TokenTotalAmount: tokenTotal, TokenUsedAmount: tokenUsed, - SeeUsedAmount: seeUsed, }, nil } diff --git a/internal/model/typedef.go b/internal/model/typedef.go index d5bd122c..9d50e75f 100644 --- a/internal/model/typedef.go +++ b/internal/model/typedef.go @@ -208,7 +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"` - SeeUsedAmount decimal.Decimal `json:"see_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_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..ddf06f1f 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,21 @@ 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 sum decimal.Decimal + query := db.Model(&model.Application{}).Where(&model.Application{ + AssetName: strings.ToUpper(assetName), + }) + + if seasonId != 0 { + query = query.Where("season_id = ?", seasonId) + } + + err := query.Select("SUM(asset_amount)"). + Scan(&sum).Error + if err != nil { + return decimal.Zero, err + } + return sum, nil +} From 9c9cbd6e40287a1cdb508102c37edfa3c5d5358b Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 19:57:31 +0800 Subject: [PATCH 17/29] Try to fix query of sum --- internal_inject/treasury/treasury.service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal_inject/treasury/treasury.service.go b/internal_inject/treasury/treasury.service.go index ddf06f1f..eaf1ebc0 100644 --- a/internal_inject/treasury/treasury.service.go +++ b/internal_inject/treasury/treasury.service.go @@ -77,7 +77,7 @@ func (s *TreasuryService) GetSumOfIssuedAsset(db *gorm.DB, assetName string, sea query = query.Where("season_id = ?", seasonId) } - err := query.Select("SUM(asset_amount)"). + err := query.Select("SUM(asset_amount::decimal)"). Scan(&sum).Error if err != nil { return decimal.Zero, err From 4a83181db22c761a9f522ed5d8ecec89f9083e68 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 20:04:44 +0800 Subject: [PATCH 18/29] Another fix for get total asset amount --- internal_inject/treasury/treasury.service.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/internal_inject/treasury/treasury.service.go b/internal_inject/treasury/treasury.service.go index eaf1ebc0..f601fde4 100644 --- a/internal_inject/treasury/treasury.service.go +++ b/internal_inject/treasury/treasury.service.go @@ -68,7 +68,7 @@ func (s *TreasuryService) UpdateAssets(ctx *gin.Context) (int, *api.Reply) { } func (s *TreasuryService) GetSumOfIssuedAsset(db *gorm.DB, assetName string, seasonId uint) (decimal.Decimal, error) { - var sum decimal.Decimal + var applications []model.Application query := db.Model(&model.Application{}).Where(&model.Application{ AssetName: strings.ToUpper(assetName), }) @@ -77,10 +77,17 @@ func (s *TreasuryService) GetSumOfIssuedAsset(db *gorm.DB, assetName string, sea query = query.Where("season_id = ?", seasonId) } - err := query.Select("SUM(asset_amount::decimal)"). - Scan(&sum).Error + // 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 } From 0f4b00d77823e08783e8e413abda3f567c788059 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 21:09:50 +0800 Subject: [PATCH 19/29] Fix score leaderboard error --- internal_inject/data_srv/data_srv.dto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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` From 16cf1a6079d8df6ecb4cc6df2de313504bbc3ce9 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 21:16:47 +0800 Subject: [PATCH 20/29] Fix scr qurey error --- internal_inject/applications/applications.controller.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal_inject/applications/applications.controller.go b/internal_inject/applications/applications.controller.go index 5302a404..86eb02e8 100644 --- a/internal_inject/applications/applications.controller.go +++ b/internal_inject/applications/applications.controller.go @@ -627,7 +627,7 @@ 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), internal.AssetNameWANG, int(currentSeason.ID)) if err != nil { log.Error().Msgf("get sum wait grant scr amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) @@ -643,7 +643,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), internal.AssetNameWANG, int(currentSeason.ID)) if err != nil { log.Error().Msgf("get sum granted scr amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) @@ -659,7 +659,7 @@ 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), internal.AssetNameWANG, int(currentSeason.ID)) if err != nil { log.Error().Msgf("get sum checking scr amount error: %+v", err) sdk.LogServerErrorToSentry(ctx, err) From 6bd0dfc90c42aff0f5188df7bf235c046ae6c47a Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 21:25:16 +0800 Subject: [PATCH 21/29] Fix asset query error --- .../applications/applications.controller.go | 19 +++++++++++++------ .../applications/applications.service.go | 7 +++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/internal_inject/applications/applications.controller.go b/internal_inject/applications/applications.controller.go index 86eb02e8..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), internal.AssetNameWANG, 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), internal.AssetNameWANG, 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), internal.AssetNameWANG, 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..11ccdeaf 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("(%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("asset_name in ?", assetQueryCond). Where("season_id = ?", seasonId). Scan(&value).Error if err != nil { From 7a34c0542a0aef9811c14567766c73c39c0e53f0 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 21:26:29 +0800 Subject: [PATCH 22/29] Fix query error --- internal_inject/applications/applications.service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal_inject/applications/applications.service.go b/internal_inject/applications/applications.service.go index 11ccdeaf..1f982e25 100644 --- a/internal_inject/applications/applications.service.go +++ b/internal_inject/applications/applications.service.go @@ -208,7 +208,7 @@ func (s *ApplicationsService) GetCronJobRecordFromStrId(taskId string) (model.Cr func (s *ApplicationsService) SumAssetAmount(ctx *gin.Context, stat string, assetNames []string, seasonId int) (float64, error) { var value float64 - assetQueryCond := fmt.Sprintf("(%s)", strings.Join(assetNames, ",")) + assetQueryCond := fmt.Sprintf("('%s')", strings.Join(assetNames, "','")) err := s.Db.Model(&model.Application{}). Select("sum(cast(asset_amount as decimal)) as total"). From ad160c94e9c8ca15653a6d17aae6a751e361858f Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 8 Aug 2025 21:30:52 +0800 Subject: [PATCH 23/29] Fix sum query --- internal_inject/applications/applications.service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal_inject/applications/applications.service.go b/internal_inject/applications/applications.service.go index 1f982e25..8e0c8c29 100644 --- a/internal_inject/applications/applications.service.go +++ b/internal_inject/applications/applications.service.go @@ -208,11 +208,11 @@ func (s *ApplicationsService) GetCronJobRecordFromStrId(taskId string) (model.Cr func (s *ApplicationsService) SumAssetAmount(ctx *gin.Context, stat string, assetNames []string, seasonId int) (float64, error) { var value float64 - assetQueryCond := fmt.Sprintf("('%s')", strings.Join(assetNames, "','")) + 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 in ?", assetQueryCond). + Where("state = ?", stat).Where(assetQueryCond). Where("season_id = ?", seasonId). Scan(&value).Error if err != nil { From 1db05867cf563a58cdcbf74f3e84b9d79617148e Mon Sep 17 00:00:00 2001 From: hzmangel Date: Sat, 9 Aug 2025 16:48:01 +0800 Subject: [PATCH 24/29] Fix auto xfer SCR task to send WANG --- internal/api/utils.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 { From a554c68a9c681a7c3dfe732ba1d6dc66ee803893 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Sat, 9 Aug 2025 17:38:16 +0800 Subject: [PATCH 25/29] Some text changes to remove SCR --- internal/const.go | 24 +++++++++++++++--------- internal/model/treasury_asset.go | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/internal/const.go b/internal/const.go index fe87005b..b451bfb4 100644 --- a/internal/const.go +++ b/internal/const.go @@ -136,23 +136,29 @@ type DecimalsAndContractAddr struct { } var ( - AssetNameWANG = "WANG" - AssetNameSCR = "SCR" - AssetNameSEE = "SEE" + AssetNameWANG = "WANG" + AssetNameSCR = "SCR" + AssetNameSEE = "SEE" + AssetNameUSDT = "USDT" + AssetNameUSDC = "USDC" + + // This is for DB query of USD* asssets AssetPrefixUSD = "USD" - AssetNameUSDT = "USDT" - AssetNameUSDC = "USDC" + + 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 0b4c21e2..cb65f6f6 100644 --- a/internal/model/treasury_asset.go +++ b/internal/model/treasury_asset.go @@ -91,7 +91,7 @@ func (r *TreasuryAsset) ToTreasuryAssetsResponse(db *gorm.DB) (*TreasuryAssetsRe tokenUsed = tokenUsed.Add(application.AssetAmount) } } else if strings.EqualFold(application.AssetName, internal.AssetNameWANG) { - // calculate processing and completed SCR + // 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) From 3dd0305b76be219f0164e0e35d3306ae95fcd5ec Mon Sep 17 00:00:00 2001 From: hzmangel Date: Sat, 9 Aug 2025 20:16:47 +0800 Subject: [PATCH 26/29] Add SELECT FOR UPDATE locking to prevent race conditions of transfering assets --- .../asset_records/asset_records.service.go | 75 +++++++++++++------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index 32b216c0..1ad23ef2 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -9,6 +9,7 @@ import ( "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 @@ -22,6 +23,7 @@ func NewAssetRecordsService(db *gorm.DB) *AssetRecordsService { } // 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 == "" { @@ -41,42 +43,69 @@ func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, return nil, errors.New(ErrInvalidAmount) } - // Check if from user has sufficient balance - fromUserRecords, err := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps(s.db, fromUser, upperAssetName) - if err != nil { - return nil, fmt.Errorf("%s: %w", ErrCheckingBalance, err) - } - - if len(fromUserRecords) == 0 { - return nil, errors.New(ErrNoAssetRecords) - } - - availableBalance := fromUserRecords[0].DealtAmount - if availableBalance.Cmp(amount) < 0 { - return nil, errors.New(ErrInsufficientBalance) - } - var transferLog *model.UserAssetTransferLog // Execute all operations within a database transaction - err = s.db.Transaction(func(tx *gorm.DB) error { - // Create transfer log + 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) } - // Deduct amount from from user's dealt amount - if updateErr := model.UserAssetRecordModel.CreateOrUpdate(tx, fromUser, upperAssetName, decimal.Zero, amount.Neg()); updateErr != nil { + // 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 - ?", amount.String())) + if result.Error != nil { model.UserAssetTransferLogModel.UpdateResult(tx, transferLog.ID, model.TransferResultFailed) - return fmt.Errorf("%s: %w", ErrUpdatingFromUser, updateErr) + return fmt.Errorf("%s: %w", ErrUpdatingFromUser, result.Error) } - // Add amount to to user's dealt amount - if updateErr := model.UserAssetRecordModel.CreateOrUpdate(tx, toUser, upperAssetName, decimal.Zero, amount); updateErr != nil { + // 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, updateErr) + 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 + ?", 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 From f5b06d2b0b521dfbe369b9ff23783c644f3cde90 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Sun, 10 Aug 2025 01:24:49 +0800 Subject: [PATCH 27/29] Fix minus with text error --- internal_inject/asset_records/asset_records.service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index 1ad23ef2..68491848 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -79,7 +79,7 @@ func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, // 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 - ?", amount.String())) + 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) From 28aed8aa322a2866f112e49b2525be81ec018dea Mon Sep 17 00:00:00 2001 From: hzmangel Date: Sun, 10 Aug 2025 01:25:08 +0800 Subject: [PATCH 28/29] Update deps --- go.mod | 10 +++++++++- go.sum | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) 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= From d13ff24f495973653c363c6fea1296b16b6333f2 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Sun, 10 Aug 2025 01:36:51 +0800 Subject: [PATCH 29/29] Fix asset add error --- internal_inject/asset_records/asset_records.service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index 68491848..f6fa84e4 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -101,7 +101,7 @@ func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, // Update existing record result := tx.Model(&model.UserAssetRecord{}). Where("user_wallet = ? AND asset_name = ?", toUser, upperAssetName). - Update("dealt_amount", gorm.Expr("dealt_amount + ?", amount.String())) + 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)