From da92cfc54175e0fd071625adacfbbc8a7056ddaf Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 2 Aug 2024 07:30:47 +0800 Subject: [PATCH 01/15] Fix error while applying budget for project --- internal/api/app_bundle/app_bundle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/app_bundle/app_bundle.go b/internal/api/app_bundle/app_bundle.go index 567d7fcd..a4a15878 100644 --- a/internal/api/app_bundle/app_bundle.go +++ b/internal/api/app_bundle/app_bundle.go @@ -528,7 +528,7 @@ func CreateAppBundle(ctx *gin.Context) { if newAppBundleReq.Entity == "project" && !isCityHallProject { for assetName, usedAmount := range assetAmountUsedInThisRequest { - err = model.GuildBudgetModel.WithdrawSingleAsset(tx, newAppBundleReq.EntityId, assetName, usedAmount) + err = model.ProjectBudgetModel.WithdrawSingleAsset(tx, newAppBundleReq.EntityId, assetName, usedAmount) if err != nil { log.Error().Msgf("withdraw budget asset %s error: %+v", assetName, err) return err From 907f9d88b27399b3c469567b079f6c4a9f360f33 Mon Sep 17 00:00:00 2001 From: Soul_Cai <5113047+CooCoode@users.noreply.github.com> Date: Sun, 16 Mar 2025 15:37:41 +0800 Subject: [PATCH 02/15] fix: reback code --- 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..01104486 100644 --- a/internal_inject/data_srv/data_srv.dto.go +++ b/internal_inject/data_srv/data_srv.dto.go @@ -16,7 +16,7 @@ from applications where applications.type = 'NEW_REWARD' and applications.asset_name = 'SCR' and applications.state in ('completed') - and applications.sub_type IN (NULL ,'', 'MintRewards') + and applications.sub_type IN (NULL ,'') GROUP by season_id, target_user_wallet, seasons.name, season_idx` const MetaforoTotalCreditRatio = "0.05" From 739c43501b5e5724b532fcfaf58e10a586494552 Mon Sep 17 00:00:00 2001 From: Soul_Cai <5113047+CooCoode@users.noreply.github.com> Date: Sun, 16 Mar 2025 16:07:47 +0800 Subject: [PATCH 03/15] Revert "fix: reback code" This reverts commit 907f9d88b27399b3c469567b079f6c4a9f360f33. --- 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 01104486..8436826b 100644 --- a/internal_inject/data_srv/data_srv.dto.go +++ b/internal_inject/data_srv/data_srv.dto.go @@ -16,7 +16,7 @@ from applications where applications.type = 'NEW_REWARD' and applications.asset_name = 'SCR' and applications.state in ('completed') - and applications.sub_type IN (NULL ,'') + and applications.sub_type IN (NULL ,'', 'MintRewards') GROUP by season_id, target_user_wallet, seasons.name, season_idx` const MetaforoTotalCreditRatio = "0.05" From 5e5997657d1bc5912ee8b454cfee60cb336c9667 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Mon, 7 Jul 2025 23:17:07 +0800 Subject: [PATCH 04/15] Add missing proposal state name --- internal/model/proposal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/model/proposal.go b/internal/model/proposal.go index 7514127a..3872d20f 100644 --- a/internal/model/proposal.go +++ b/internal/model/proposal.go @@ -83,6 +83,7 @@ var ProposalStateName = []string{ "executed", "execution_failed", "vetoed", + "deleted_from_metaforo", } // MetaforoUser saves user mapping between OS and metaforo From 648aa13e4a7d9dee1892d716279c1b1f6566ae46 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Mon, 7 Jul 2025 23:42:48 +0800 Subject: [PATCH 05/15] Add missing proposal state --- internal/model/proposal.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/model/proposal.go b/internal/model/proposal.go index 3872d20f..4b6179ff 100644 --- a/internal/model/proposal.go +++ b/internal/model/proposal.go @@ -68,6 +68,7 @@ var ProposalStateIdNameMapping = map[string]ProposalState{ "execution_failed": ProposalStateExecutionFailed, "vetoed": ProposalStateVetoed, "deleted_from_metaforo": ProposalStateDeletedFromMetaforo, + "metaforo_error": ProposalStateUncategorizedMetaforoError, } var ProposalStateName = []string{ @@ -84,6 +85,7 @@ var ProposalStateName = []string{ "execution_failed", "vetoed", "deleted_from_metaforo", + "metaforo_error", } // MetaforoUser saves user mapping between OS and metaforo From 11806433efa3f063e9f2544747667ec785e3efb6 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Wed, 13 Aug 2025 22:38:28 +0800 Subject: [PATCH 06/15] feat(asset_records): add my transfers endpoint and filtering - Add new `/my` endpoint to list transfers for authenticated user - Extend ListTransfers service method with myWallet parameter - Update query logic to filter by either from/to user or myWallet - Rename detail endpoint path from `/:id` to `/show/:id` --- internal/model/user_asset_transfer_log.go | 16 +++--- .../asset_records/asset_records.controller.go | 54 ++++++++++++++++++- .../asset_records/asset_records.service.go | 4 +- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/internal/model/user_asset_transfer_log.go b/internal/model/user_asset_transfer_log.go index 74984d1e..ec0c3cdb 100644 --- a/internal/model/user_asset_transfer_log.go +++ b/internal/model/user_asset_transfer_log.go @@ -83,18 +83,22 @@ func (*userAssetTransferLogModel) UpdateResult(db *gorm.DB, id uint, result stri } // ListPaginated returns paginated transfer records with optional filters -func (*userAssetTransferLogModel) ListPaginated(db *gorm.DB, page, size int, fromUser, toUser string) ([]*UserAssetTransferLog, int64, error) { +func (*userAssetTransferLogModel) ListPaginated(db *gorm.DB, page, size int, fromUser, toUser, myWallet 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 myWallet != "" { + query = query.Where("from_user = ? OR to_user = ?", common.FormatUserWallet(myWallet), common.FormatUserWallet(myWallet)) + } else { + if fromUser != "" { + query = query.Where("from_user = ?", common.FormatUserWallet(fromUser)) + } - if toUser != "" { - query = query.Where("to_user = ?", common.FormatUserWallet(toUser)) + if toUser != "" { + query = query.Where("to_user = ?", common.FormatUserWallet(toUser)) + } } // Count total records diff --git a/internal_inject/asset_records/asset_records.controller.go b/internal_inject/asset_records/asset_records.controller.go index f70de88a..1e4a1a4e 100644 --- a/internal_inject/asset_records/asset_records.controller.go +++ b/internal_inject/asset_records/asset_records.controller.go @@ -50,9 +50,10 @@ func Register(fatherGroup *gin.RouterGroup) { // No auth endpoints assetRecordsGroup.GET("/", assetRecords.List) - assetRecordsGroup.GET("/:id", assetRecords.Detail) + assetRecordsGroup.GET("/show/:id", assetRecords.Detail) // Auth required endpoints + assetRecordsAuthGroup.GET("/my", assetRecords.MyList) assetRecordsAuthGroup.POST("/", assetRecords.Create) } @@ -105,6 +106,55 @@ func (c *AssetRecordsController) Create(ctx *gin.Context) { })) } +func (c *AssetRecordsController) MyList(ctx *gin.Context) { + user := api.ForContextOnlyUser(ctx) + + 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, "", "", user.Wallet) + 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, + })) +} + // List returns paginated asset transfer records with optional query filters func (c *AssetRecordsController) List(ctx *gin.Context) { var queryParams TransferListQueryParams @@ -122,7 +172,7 @@ func (c *AssetRecordsController) List(ctx *gin.Context) { queryParams.Size = DefaultPageSize } - transferLogs, 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) diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index f6fa84e4..2ef3e062 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -124,8 +124,8 @@ func (s *AssetRecordsService) CreateTransfer(fromUser, toUser, assetName string, } // 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) +func (s *AssetRecordsService) ListTransfers(page, size int, fromUser, toUser, myWallet string) ([]*model.UserAssetTransferLog, int64, error) { + return model.UserAssetTransferLogModel.ListPaginated(s.db, page, size, fromUser, toUser, myWallet) } // GetTransferByID returns a single transfer record by ID From 1ca020bc3bd9feb2fce641aaff7f8d0fdae01614 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Wed, 13 Aug 2025 22:45:42 +0800 Subject: [PATCH 07/15] Add /list to list all user asset transfer logs to avoid confusion --- internal_inject/asset_records/asset_records.controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal_inject/asset_records/asset_records.controller.go b/internal_inject/asset_records/asset_records.controller.go index 1e4a1a4e..a746dd7d 100644 --- a/internal_inject/asset_records/asset_records.controller.go +++ b/internal_inject/asset_records/asset_records.controller.go @@ -49,7 +49,7 @@ func Register(fatherGroup *gin.RouterGroup) { } // No auth endpoints - assetRecordsGroup.GET("/", assetRecords.List) + assetRecordsGroup.GET("/list", assetRecords.List) assetRecordsGroup.GET("/show/:id", assetRecords.Detail) // Auth required endpoints From 1fbf2b45e3a91293ce4e08373961919478c399a2 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Thu, 14 Aug 2025 11:48:42 +0800 Subject: [PATCH 08/15] feat(asset_records): add SEE token claim functionality from indexer - Add new error constants for claim operations - Implement indexer client method to get user SCR balance - Add claim endpoint and service logic to create asset records --- internal/sdk/indexer_client.go | 39 ++++++++++ .../asset_records/asset_records.controller.go | 38 +++++++++- .../asset_records/asset_records.service.go | 71 +++++++++++++++++++ internal_inject/asset_records/consts.go | 21 +++--- 4 files changed, 159 insertions(+), 10 deletions(-) diff --git a/internal/sdk/indexer_client.go b/internal/sdk/indexer_client.go index 0590d11d..b608570f 100644 --- a/internal/sdk/indexer_client.go +++ b/internal/sdk/indexer_client.go @@ -8,7 +8,9 @@ import ( "github.com/rs/zerolog/log" "github.com/samber/lo" + "github.com/shopspring/decimal" "github.com/theseed-labs/os-backend/internal" + "github.com/theseed-labs/os-backend/internal/common" ) type SeedHolderRecord struct { @@ -22,6 +24,11 @@ type SeasonSBTRecord struct { Values []string `json:"values"` } +type Erc20SnapshotRecord struct { + Wallet string `json:"wallet"` + Value decimal.Decimal `json:"amount"` +} + type ComputeNodeSbt struct { Node int `json:"node"` Sbt int `json:"sbt"` @@ -175,3 +182,35 @@ func (c *IndexerClient) GetComputeNodeSbt() (*ComputeNodeSbt, error) { return respData, nil } + +func (c *IndexerClient) GetUserCurrentScrAmount(userWallet string) (decimal.Decimal, error) { + endpoint := fmt.Sprintf("%s/snapshot/%s/%s", c.ApiBase, internal.ScrContractType, internal.ScrContractAddr) + log.Debug().Msgf("Try to get SCR amount, endpoint is %s", endpoint) + + resp, err := http.Get(endpoint) + if err != nil { + return decimal.Zero, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return decimal.Zero, fmt.Errorf("got error response from indexer endpoint: status code: %d, resp: %+v", resp.StatusCode, resp) + } + + var respData []*Erc20SnapshotRecord + err = json.NewDecoder(resp.Body).Decode(&respData) + if err != nil { + return decimal.Zero, err + } + + formattedWallet := common.FormatUserWallet(userWallet) + scrAmount := decimal.Zero + for _, record := range respData { + if record.Wallet == formattedWallet { + scrAmount = record.Value + break + } + } + return scrAmount, nil +} diff --git a/internal_inject/asset_records/asset_records.controller.go b/internal_inject/asset_records/asset_records.controller.go index a746dd7d..3e9cc6a7 100644 --- a/internal_inject/asset_records/asset_records.controller.go +++ b/internal_inject/asset_records/asset_records.controller.go @@ -54,7 +54,8 @@ func Register(fatherGroup *gin.RouterGroup) { // Auth required endpoints assetRecordsAuthGroup.GET("/my", assetRecords.MyList) - assetRecordsAuthGroup.POST("/", assetRecords.Create) + assetRecordsAuthGroup.POST("/new", assetRecords.Create) + assetRecordsAuthGroup.POST("/claim_see", assetRecords.ClaimSee) } // Create creates a new asset transfer @@ -239,3 +240,38 @@ func (c *AssetRecordsController) Detail(ctx *gin.Context) { ctx.JSON(http.StatusOK, api.Success(response)) } + +// ClaimSee allows users to claim their asset records from indexer +func (c *AssetRecordsController) ClaimSee(ctx *gin.Context) { + user := api.ForContextOnlyUser(ctx) + + // Check if user already has asset records + existingRecords, err := c.Service.GetUserAssetRecords(user.Wallet, DefaultTransferAssetName) + if err != nil { + log.Error().Msgf("Error checking user asset records: %+v", err) + sdk.LogServerErrorToSentry(ctx, err) + ctx.JSON(http.StatusInternalServerError, api.ServerError(err)) + return + } + + // If user already has records, return already claimed message + if len(existingRecords) > 0 { + ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("assets has already claimed"))) + return + } + + // Get user asset info from indexer and create records + assetRecords, err := c.Service.ClaimUserSeeAssets(user.Wallet) + + if err != nil { + log.Error().Msgf("Error claiming user assets: %+v", err) + sdk.LogServerErrorToSentry(ctx, err) + ctx.JSON(http.StatusInternalServerError, api.ServerError(err)) + return + } + + ctx.JSON(http.StatusCreated, api.Success(map[string]any{ + "message": "Assets claimed successfully", + "records": len(assetRecords), + })) +} diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index 2ef3e062..d5d532fc 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -7,7 +7,10 @@ import ( "github.com/rs/zerolog/log" "github.com/shopspring/decimal" + "github.com/theseed-labs/os-backend/internal" + "github.com/theseed-labs/os-backend/internal/common" "github.com/theseed-labs/os-backend/internal/model" + "github.com/theseed-labs/os-backend/internal/sdk" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -132,3 +135,71 @@ func (s *AssetRecordsService) ListTransfers(page, size int, fromUser, toUser, my func (s *AssetRecordsService) GetTransferByID(id uint) (*model.UserAssetTransferLog, error) { return model.UserAssetTransferLogModel.GetByID(s.db, id) } + +// GetUserAssetRecords checks if user already has asset records for specified asset +func (s *AssetRecordsService) GetUserAssetRecords(userWallet string, assetName string) ([]*model.UserAssetRecord, error) { + formattedWallet := common.FormatUserWallet(userWallet) + + var records []*model.UserAssetRecord + err := s.db.Where("user_wallet = ? AND asset_name = ?", formattedWallet, assetName).Find(&records).Error + if err != nil { + return nil, err + } + + return records, nil +} + +// ClaimUserSeeAssets gets user asset info from indexer and creates asset records +func (s *AssetRecordsService) ClaimUserSeeAssets(userWallet string) ([]*model.UserAssetRecord, error) { + formattedWallet := common.FormatUserWallet(userWallet) + + // TODO: Replace this with actual indexer API call to get user SEE balance + // For now, we'll use a placeholder implementation + userScrBalance, err := s.getUserScrBalanceFromIndexer(formattedWallet) + if err != nil { + return nil, fmt.Errorf("failed to get user balance from indexer: %w", err) + } + + // Create asset record for SEE token + var createdRecords []*model.UserAssetRecord + + if userScrBalance.GreaterThan(decimal.Zero) { + err = model.UserAssetRecordModel.CreateOrUpdate( + s.db, + formattedWallet, + internal.AssetNameSEE, + decimal.Zero, // processing_amount + userScrBalance, // dealt_amount + ) + if err != nil { + return nil, fmt.Errorf("failed to create asset record: %w", err) + } + + // Fetch the created record + records, err := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps( + s.db, + formattedWallet, + internal.AssetNameSEE, + ) + if err != nil { + return nil, err + } + + if len(records) > 0 { + createdRecords = append(createdRecords, records[0]) + } + } + + return createdRecords, nil +} + +// getUserScrBalanceFromIndexer gets user SCR token balance from indexer +func (s *AssetRecordsService) getUserScrBalanceFromIndexer(userWallet string) (decimal.Decimal, error) { + indexerClient := sdk.GetIndexerClient() + scrAmount, err := indexerClient.GetUserCurrentScrAmount(userWallet) + if err != nil { + return decimal.Zero, err + } + + return scrAmount, nil +} diff --git a/internal_inject/asset_records/consts.go b/internal_inject/asset_records/consts.go index ee4efd7a..04e77927 100644 --- a/internal_inject/asset_records/consts.go +++ b/internal_inject/asset_records/consts.go @@ -9,11 +9,14 @@ const ( 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" + 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" + ErrAlreadyClaimed = "you have already claimed your assets" + ErrClaimingAssets = "error claiming user assets" + ErrGettingIndexerBalance = "error getting balance from indexer" ) // Status values @@ -28,9 +31,9 @@ const ( // Pagination defaults const ( - DefaultPageSize = 20 - MaxPageSize = 100 - DefaultPageNumber = 1 + DefaultPageSize = 20 + MaxPageSize = 100 + DefaultPageNumber = 1 ) // Field names @@ -50,4 +53,4 @@ const ( // Default values const ( DefaultTransferAssetName = "SEE" -) \ No newline at end of file +) From 442f3189c956d2f5f29617ce0101a2fccbec8279 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 15 Aug 2025 00:51:39 +0800 Subject: [PATCH 09/15] Update claim logic - SEE should be saved to user asset after last deployment - Only claimed SEE can be used to transfer - User can only claim it before 2026-01-11 --- internal/const.go | 3 + .../asset_records/asset_records.controller.go | 35 +++------ .../asset_records/asset_records.service.go | 75 +++++++++++-------- 3 files changed, 58 insertions(+), 55 deletions(-) diff --git a/internal/const.go b/internal/const.go index b451bfb4..e01f625e 100644 --- a/internal/const.go +++ b/internal/const.go @@ -176,3 +176,6 @@ const MetaforoPollNotExistsPrompt = "Poll not exists" const ( ERRCODE_GetMetaforoDataError = -1001 ) + +// Claim function is deployed at 2025.8.11, and the end date is 6 months later, which is 2026.1.11 00:00:00 UTC+8 +var ClaimSeeAssetEndDate = time.Date(2026, 1, 10, 16, 0, 0, 0, time.UTC) diff --git a/internal_inject/asset_records/asset_records.controller.go b/internal_inject/asset_records/asset_records.controller.go index 3e9cc6a7..1208d2d9 100644 --- a/internal_inject/asset_records/asset_records.controller.go +++ b/internal_inject/asset_records/asset_records.controller.go @@ -5,11 +5,13 @@ import ( "fmt" "net/http" "strconv" + "time" "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" "github.com/theseed-labs/os-backend/internal/api" "github.com/theseed-labs/os-backend/internal/common" "github.com/theseed-labs/os-backend/internal/config" @@ -243,35 +245,20 @@ func (c *AssetRecordsController) Detail(ctx *gin.Context) { // ClaimSee allows users to claim their asset records from indexer func (c *AssetRecordsController) ClaimSee(ctx *gin.Context) { - user := api.ForContextOnlyUser(ctx) - - // Check if user already has asset records - existingRecords, err := c.Service.GetUserAssetRecords(user.Wallet, DefaultTransferAssetName) - if err != nil { - log.Error().Msgf("Error checking user asset records: %+v", err) - sdk.LogServerErrorToSentry(ctx, err) - ctx.JSON(http.StatusInternalServerError, api.ServerError(err)) - return - } - - // If user already has records, return already claimed message - if len(existingRecords) > 0 { - ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("assets has already claimed"))) + if time.Now().After(internal.ClaimSeeAssetEndDate) { + ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("claim see asset end"))) return } - // Get user asset info from indexer and create records - assetRecords, err := c.Service.ClaimUserSeeAssets(user.Wallet) - + user := api.ForContextOnlyUser(ctx) + err := c.Service.ClaimUserSeeAssets(user.Wallet) if err != nil { - log.Error().Msgf("Error claiming user assets: %+v", err) + log.Error().Msgf("Error claiming user see assets: %+v", err) sdk.LogServerErrorToSentry(ctx, err) - ctx.JSON(http.StatusInternalServerError, api.ServerError(err)) + ctx.JSON(http.StatusInternalServerError, api.BadRequest(errors.New("asset record error, please contract admin"))) + return + } else { + ctx.JSON(http.StatusOK, api.Success("see claimed successfully")) return } - - ctx.JSON(http.StatusCreated, api.Success(map[string]any{ - "message": "Assets claimed successfully", - "records": len(assetRecords), - })) } diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index d5d532fc..205fe45a 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -150,47 +150,60 @@ func (s *AssetRecordsService) GetUserAssetRecords(userWallet string, assetName s } // ClaimUserSeeAssets gets user asset info from indexer and creates asset records -func (s *AssetRecordsService) ClaimUserSeeAssets(userWallet string) ([]*model.UserAssetRecord, error) { +func (s *AssetRecordsService) ClaimUserSeeAssets(userWallet string) error { formattedWallet := common.FormatUserWallet(userWallet) - // TODO: Replace this with actual indexer API call to get user SEE balance - // For now, we'll use a placeholder implementation - userScrBalance, err := s.getUserScrBalanceFromIndexer(formattedWallet) - if err != nil { - return nil, fmt.Errorf("failed to get user balance from indexer: %w", err) - } + return s.db.Transaction(func(tx *gorm.DB) error { + // Use pessimistic locking to prevent concurrent access issues and check record count + var existingRecords []model.UserAssetRecord + err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}). + Where("user_wallet = ? AND asset_name = ?", formattedWallet, internal.AssetNameSEE). + Find(&existingRecords).Error - // Create asset record for SEE token - var createdRecords []*model.UserAssetRecord - - if userScrBalance.GreaterThan(decimal.Zero) { - err = model.UserAssetRecordModel.CreateOrUpdate( - s.db, - formattedWallet, - internal.AssetNameSEE, - decimal.Zero, // processing_amount - userScrBalance, // dealt_amount - ) if err != nil { - return nil, fmt.Errorf("failed to create asset record: %w", err) + return err } - // Fetch the created record - records, err := model.UserAssetRecordModel.FindWithUserWalletAndAssetProps( - s.db, - formattedWallet, - internal.AssetNameSEE, - ) - if err != nil { - return nil, err + if len(existingRecords) == 0 { + return errors.New("user has no see asset records") } - if len(records) > 0 { - createdRecords = append(createdRecords, records[0]) + if len(existingRecords) > 1 { + return errors.New("user has multiple see asset records") } - } - return createdRecords, nil + existingRecord := existingRecords[0] + + // Check if there's any processing amount to claim + if existingRecord.ProcessingAmount.Cmp(decimal.Zero) <= 0 { + return errors.New("user has no see amount to claim") + } + + // Perform atomic update using the locked record's processing amount + // This ensures we use the exact amount that was locked, preventing race conditions + result := tx.Model(&model.UserAssetRecord{}). + Where("id = ? AND processing_amount >= ?", existingRecord.ID, existingRecord.ProcessingAmount). + Updates(map[string]interface{}{ + "processing_amount": gorm.Expr("processing_amount::decimal - ?", existingRecord.ProcessingAmount.String()), + "dealt_amount": gorm.Expr("dealt_amount::decimal + ?", existingRecord.ProcessingAmount.String()), + }) + + if result.Error != nil { + return result.Error + } + + // Check if the update actually affected any rows + if result.RowsAffected == 0 { + return errors.New("failed to claim assets: record may have been modified by another transaction") + } + + return nil + }) +} + +func (s *AssetRecordsService) CreateUserAssetRecord(userWallet string, assetName string, amount decimal.Decimal, dealtAmount decimal.Decimal) error { + formattedWallet := common.FormatUserWallet(userWallet) + return model.UserAssetRecordModel.CreateOrUpdate(s.db, formattedWallet, assetName, amount, dealtAmount) } // getUserScrBalanceFromIndexer gets user SCR token balance from indexer From 6a8dead37d49b0920ee2eaa328612cea4c5ab4e4 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 15 Aug 2025 10:42:33 +0800 Subject: [PATCH 10/15] Update claim SEE logic 1. claim will be stopped at 2026.01.11 2. claim adds the processing amount to dealt amount, the processing amount will be populated in advanced 3. user can only claim once 4. user without see asset will not be able to invoke claim API --- cmd/apiserver/main.go | 2 +- internal/model/user_asset_record.go | 6 + .../asset_records/asset_records.controller.go | 20 +- .../asset_records/asset_records.service.go | 26 ++- internal_inject/user/user.service.go | 192 +++++++++--------- 5 files changed, 134 insertions(+), 112 deletions(-) diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index ba230709..2e232b2c 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -170,7 +170,7 @@ func main() { log.Logger = log.With().Caller().Logger() // setup task manager and start runner - task_manager.InitTaskManager(db, 5, cfg) + task_manager.InitTaskManager(db, 500000, cfg) task_manager.GetTaskManager().StartRunner() storage.SetConfig(cfg) diff --git a/internal/model/user_asset_record.go b/internal/model/user_asset_record.go index 88b3bd9c..e30fc58d 100644 --- a/internal/model/user_asset_record.go +++ b/internal/model/user_asset_record.go @@ -24,6 +24,9 @@ type UserAssetRecord struct { UpdatedAt time.Time `json:"-"` CreateTs int64 `json:"create_ts" gorm:"index"` UpdateTs int64 `json:"update_ts" gorm:"index"` + + // Claimed field is used for SEE claim requirements. + Claimed bool `json:"claimed" gorm:"default:false"` } type userAssetRecordModel struct{} @@ -70,6 +73,9 @@ func (*userAssetRecordModel) CreateOrUpdate(db *gorm.DB, userWallet string, asse AssetName: strings.ToUpper(assetName), DealtAmount: dealtAmount, ProcessingAmount: processingAmount, + + // Claimed field is added for see asset, for new created asset records, mark it as claimed + Claimed: true, }).Error } else { assetRecords[0].DealtAmount = assetRecords[0].DealtAmount.Add(dealtAmount) diff --git a/internal_inject/asset_records/asset_records.controller.go b/internal_inject/asset_records/asset_records.controller.go index 1208d2d9..a6ce3227 100644 --- a/internal_inject/asset_records/asset_records.controller.go +++ b/internal_inject/asset_records/asset_records.controller.go @@ -246,19 +246,25 @@ func (c *AssetRecordsController) Detail(ctx *gin.Context) { // ClaimSee allows users to claim their asset records from indexer func (c *AssetRecordsController) ClaimSee(ctx *gin.Context) { if time.Now().After(internal.ClaimSeeAssetEndDate) { - ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("claim see asset end"))) + err := errors.New("claim see asset end") + sdk.LogUserSideError(ctx, err) + ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) return } user := api.ForContextOnlyUser(ctx) - err := c.Service.ClaimUserSeeAssets(user.Wallet) - if err != nil { - log.Error().Msgf("Error claiming user see assets: %+v", err) + statusCode, err := c.Service.ClaimUserSeeAssets(user.Wallet) + switch statusCode { + case http.StatusOK: + ctx.JSON(http.StatusOK, api.Success("see claimed successfully")) + return + case http.StatusBadRequest: + sdk.LogUserSideError(ctx, err) + ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + return + default: sdk.LogServerErrorToSentry(ctx, err) ctx.JSON(http.StatusInternalServerError, api.BadRequest(errors.New("asset record error, please contract admin"))) return - } else { - ctx.JSON(http.StatusOK, api.Success("see claimed successfully")) - return } } diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index 205fe45a..10eabf3e 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -3,6 +3,7 @@ package asset_records_inject import ( "errors" "fmt" + "net/http" "strings" "github.com/rs/zerolog/log" @@ -150,32 +151,42 @@ func (s *AssetRecordsService) GetUserAssetRecords(userWallet string, assetName s } // ClaimUserSeeAssets gets user asset info from indexer and creates asset records -func (s *AssetRecordsService) ClaimUserSeeAssets(userWallet string) error { +// It will firstly check whether user has SEE asset to claim, and return error if no records found +// Then it will convert the processing amount of SEE into dealt amount, and the things is done +// There will be no processing amount for SEE in near future, so checking processing amount to determine claim state is available +func (s *AssetRecordsService) ClaimUserSeeAssets(userWallet string) (int, error) { formattedWallet := common.FormatUserWallet(userWallet) + statusCode := http.StatusOK - return s.db.Transaction(func(tx *gorm.DB) error { + err := s.db.Transaction(func(tx *gorm.DB) error { // Use pessimistic locking to prevent concurrent access issues and check record count var existingRecords []model.UserAssetRecord err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}). - Where("user_wallet = ? AND asset_name = ?", formattedWallet, internal.AssetNameSEE). + Where("user_wallet = ? AND asset_name = ? AND claimed = false", formattedWallet, internal.AssetNameSEE). Find(&existingRecords).Error if err != nil { + statusCode = http.StatusInternalServerError return err } if len(existingRecords) == 0 { - return errors.New("user has no see asset records") + statusCode = http.StatusBadRequest + return errors.New("user has no see asset records to claim") } if len(existingRecords) > 1 { + statusCode = http.StatusInternalServerError return errors.New("user has multiple see asset records") } existingRecord := existingRecords[0] + log.Error().Msgf("TTT: processing see: %+v", existingRecord.ProcessingAmount.Cmp(decimal.Zero)) + // Check if there's any processing amount to claim - if existingRecord.ProcessingAmount.Cmp(decimal.Zero) <= 0 { + if existingRecord.ProcessingAmount.Cmp(decimal.Zero) == 0 { + statusCode = http.StatusBadRequest return errors.New("user has no see amount to claim") } @@ -186,19 +197,24 @@ func (s *AssetRecordsService) ClaimUserSeeAssets(userWallet string) error { Updates(map[string]interface{}{ "processing_amount": gorm.Expr("processing_amount::decimal - ?", existingRecord.ProcessingAmount.String()), "dealt_amount": gorm.Expr("dealt_amount::decimal + ?", existingRecord.ProcessingAmount.String()), + "claimed": true, }) if result.Error != nil { + statusCode = http.StatusInternalServerError return result.Error } // Check if the update actually affected any rows if result.RowsAffected == 0 { + statusCode = http.StatusInternalServerError return errors.New("failed to claim assets: record may have been modified by another transaction") } return nil }) + + return statusCode, err } func (s *AssetRecordsService) CreateUserAssetRecord(userWallet string, assetName string, amount decimal.Decimal, dealtAmount decimal.Decimal) error { diff --git a/internal_inject/user/user.service.go b/internal_inject/user/user.service.go index 54f8fe1b..e2b6e755 100644 --- a/internal_inject/user/user.service.go +++ b/internal_inject/user/user.service.go @@ -1,15 +1,11 @@ package user_inject import ( - "context" "errors" "fmt" "net/http" - "strings" "time" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "github.com/spruceid/siwe-go" @@ -20,9 +16,6 @@ import ( "github.com/theseed-labs/os-backend/internal/model" "github.com/theseed-labs/os-backend/internal/sdk" "gorm.io/gorm" - - eth_common "github.com/ethereum/go-ethereum/common" - unipass_sigverify "github.com/unipassid/unipass-sigverify-go" ) type UserService struct { @@ -58,6 +51,7 @@ func (u *UserService) Login(ctx *gin.Context, req *LoginReq) (int, *api.Reply) { var token string var tokenExp int64 var user *model.User + var err error var httpCode int var reply *api.Reply @@ -65,98 +59,98 @@ func (u *UserService) Login(ctx *gin.Context, req *LoginReq) (int, *api.Reply) { _ = u.Db.Transaction(func(tx *gorm.DB) error { - // verify sign - // --> query nonce - userNonce, err := model.UserNonceModel.RecentNonce(tx, common.FormatUserWallet(req.Wallet), u.Cfg.Auth.NonceLifespan) - if err != nil { - sdk.LogServerErrorToSentry(ctx, err) - // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("nonce not found"))) - httpCode = http.StatusInternalServerError - reply = api.ServerError(errors.New("nonce not found")) - - return errors.New("nonce not found") - } - if userNonce == nil { - // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("please refresh nonce firstly"))) - httpCode = http.StatusBadRequest - reply = api.BadRequest(errors.New("please refresh nonce firstly")) - - return errors.New("please refresh nonce firstly") - } - if strings.EqualFold(req.WalletType, "EOA") { - // --> verify signature - message, err := siwe.ParseMessage(req.Message) - if err != nil { - // ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) - httpCode = http.StatusBadRequest - reply = api.BadRequest(err) - - return err - } - timestamp := time.Now() - publicKey, err := message.Verify(req.Signature, &req.Domain, &userNonce.Nonce, ×tamp) - if err != nil { - // ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) - httpCode = http.StatusBadRequest - reply = api.BadRequest(err) - - return err - } - if !strings.EqualFold(req.Wallet, crypto.PubkeyToAddress(*publicKey).Hex()) { - // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("signature not match"))) - httpCode = http.StatusBadRequest - reply = api.BadRequest(errors.New("signature not match")) - - return errors.New("signature not match") - } - } else if strings.EqualFold(req.WalletType, "AA") { - client, err := ethclient.Dial(u.Cfg.Auth.PolygonRPC) - if err != nil { - sdk.LogServerErrorToSentry(ctx, err) - // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to connect to polygon rpc"))) - httpCode = http.StatusInternalServerError - reply = api.ServerError(errors.New("failed to connect to polygon rpc detail:" + err.Error())) - - return errors.New("failed to connect to polygon rpc detail:" + err.Error()) - } - - // get AA's bytecode - bytecode, err := client.CodeAt(context.Background(), eth_common.HexToAddress(req.Wallet), nil) - if err != nil { - sdk.LogServerErrorToSentry(ctx, err) - // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to get bytecode"))) - httpCode = http.StatusInternalServerError - reply = api.ServerError(errors.New("failed to get bytecode detail:" + err.Error())) - - return errors.New("failed to get bytecode detail:" + err.Error()) - } - // if bytecode is not empty, means the wallet has deployed - // 2023/12/04: only verify signature when AA is deployed! - if len(bytecode) > 0 { - account := eth_common.HexToAddress(req.Wallet) - sig := eth_common.FromHex(req.Signature) - msg := []byte(req.Message) - - ok, err := unipass_sigverify.VerifyMessageSignature(context.Background(), account, msg, sig, req.IsEIP191Prefix, client) - if err != nil { - sdk.LogServerErrorToSentry(ctx, err) - // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to verify signature"))) - httpCode = http.StatusInternalServerError - reply = api.ServerError(errors.New("failed to verify signature")) - - return errors.New("failed to verify signature") - } - if !ok { - // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("signature not match"))) - httpCode = http.StatusBadRequest - reply = api.BadRequest(errors.New("signature not match")) - - return errors.New("signature not match") - } - } else { - userVerified = false - } - } + // // verify sign + // // --> query nonce + // userNonce, err := model.UserNonceModel.RecentNonce(tx, common.FormatUserWallet(req.Wallet), u.Cfg.Auth.NonceLifespan) + // if err != nil { + // sdk.LogServerErrorToSentry(ctx, err) + // // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("nonce not found"))) + // httpCode = http.StatusInternalServerError + // reply = api.ServerError(errors.New("nonce not found")) + + // return errors.New("nonce not found") + // } + // if userNonce == nil { + // // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("please refresh nonce firstly"))) + // httpCode = http.StatusBadRequest + // reply = api.BadRequest(errors.New("please refresh nonce firstly")) + + // return errors.New("please refresh nonce firstly") + // } + // if strings.EqualFold(req.WalletType, "EOA") { + // // --> verify signature + // message, err := siwe.ParseMessage(req.Message) + // if err != nil { + // // ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + // httpCode = http.StatusBadRequest + // reply = api.BadRequest(err) + + // return err + // } + // timestamp := time.Now() + // publicKey, err := message.Verify(req.Signature, &req.Domain, &userNonce.Nonce, ×tamp) + // if err != nil { + // // ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + // httpCode = http.StatusBadRequest + // reply = api.BadRequest(err) + + // return err + // } + // if !strings.EqualFold(req.Wallet, crypto.PubkeyToAddress(*publicKey).Hex()) { + // // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("signature not match"))) + // httpCode = http.StatusBadRequest + // reply = api.BadRequest(errors.New("signature not match")) + + // return errors.New("signature not match") + // } + // } else if strings.EqualFold(req.WalletType, "AA") { + // client, err := ethclient.Dial(u.Cfg.Auth.PolygonRPC) + // if err != nil { + // sdk.LogServerErrorToSentry(ctx, err) + // // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to connect to polygon rpc"))) + // httpCode = http.StatusInternalServerError + // reply = api.ServerError(errors.New("failed to connect to polygon rpc detail:" + err.Error())) + + // return errors.New("failed to connect to polygon rpc detail:" + err.Error()) + // } + + // // get AA's bytecode + // bytecode, err := client.CodeAt(context.Background(), eth_common.HexToAddress(req.Wallet), nil) + // if err != nil { + // sdk.LogServerErrorToSentry(ctx, err) + // // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to get bytecode"))) + // httpCode = http.StatusInternalServerError + // reply = api.ServerError(errors.New("failed to get bytecode detail:" + err.Error())) + + // return errors.New("failed to get bytecode detail:" + err.Error()) + // } + // // if bytecode is not empty, means the wallet has deployed + // // 2023/12/04: only verify signature when AA is deployed! + // if len(bytecode) > 0 { + // account := eth_common.HexToAddress(req.Wallet) + // sig := eth_common.FromHex(req.Signature) + // msg := []byte(req.Message) + + // ok, err := unipass_sigverify.VerifyMessageSignature(context.Background(), account, msg, sig, req.IsEIP191Prefix, client) + // if err != nil { + // sdk.LogServerErrorToSentry(ctx, err) + // // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to verify signature"))) + // httpCode = http.StatusInternalServerError + // reply = api.ServerError(errors.New("failed to verify signature")) + + // return errors.New("failed to verify signature") + // } + // if !ok { + // // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("signature not match"))) + // httpCode = http.StatusBadRequest + // reply = api.BadRequest(errors.New("signature not match")) + + // return errors.New("signature not match") + // } + // } else { + // userVerified = false + // } + // } // query user user, err = model.UserModel.Detail(tx, common.FormatUserWallet(req.Wallet)) From 4c80cee0e2d6341c6e09bfdb410760f9000af48f Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 15 Aug 2025 10:43:33 +0800 Subject: [PATCH 11/15] Clean unused code --- .../asset_records/asset_records.service.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/internal_inject/asset_records/asset_records.service.go b/internal_inject/asset_records/asset_records.service.go index 10eabf3e..106a1793 100644 --- a/internal_inject/asset_records/asset_records.service.go +++ b/internal_inject/asset_records/asset_records.service.go @@ -11,7 +11,6 @@ import ( "github.com/theseed-labs/os-backend/internal" "github.com/theseed-labs/os-backend/internal/common" "github.com/theseed-labs/os-backend/internal/model" - "github.com/theseed-labs/os-backend/internal/sdk" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -216,19 +215,3 @@ func (s *AssetRecordsService) ClaimUserSeeAssets(userWallet string) (int, error) return statusCode, err } - -func (s *AssetRecordsService) CreateUserAssetRecord(userWallet string, assetName string, amount decimal.Decimal, dealtAmount decimal.Decimal) error { - formattedWallet := common.FormatUserWallet(userWallet) - return model.UserAssetRecordModel.CreateOrUpdate(s.db, formattedWallet, assetName, amount, dealtAmount) -} - -// getUserScrBalanceFromIndexer gets user SCR token balance from indexer -func (s *AssetRecordsService) getUserScrBalanceFromIndexer(userWallet string) (decimal.Decimal, error) { - indexerClient := sdk.GetIndexerClient() - scrAmount, err := indexerClient.GetUserCurrentScrAmount(userWallet) - if err != nil { - return decimal.Zero, err - } - - return scrAmount, nil -} From 0f1c2092acc33a45738497aba75dd1d07f40dd7c Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 15 Aug 2025 22:39:18 +0800 Subject: [PATCH 12/15] revert error commit --- internal_inject/user/user.service.go | 192 ++++++++++++++------------- 1 file changed, 99 insertions(+), 93 deletions(-) diff --git a/internal_inject/user/user.service.go b/internal_inject/user/user.service.go index e2b6e755..54f8fe1b 100644 --- a/internal_inject/user/user.service.go +++ b/internal_inject/user/user.service.go @@ -1,11 +1,15 @@ package user_inject import ( + "context" "errors" "fmt" "net/http" + "strings" "time" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "github.com/spruceid/siwe-go" @@ -16,6 +20,9 @@ import ( "github.com/theseed-labs/os-backend/internal/model" "github.com/theseed-labs/os-backend/internal/sdk" "gorm.io/gorm" + + eth_common "github.com/ethereum/go-ethereum/common" + unipass_sigverify "github.com/unipassid/unipass-sigverify-go" ) type UserService struct { @@ -51,7 +58,6 @@ func (u *UserService) Login(ctx *gin.Context, req *LoginReq) (int, *api.Reply) { var token string var tokenExp int64 var user *model.User - var err error var httpCode int var reply *api.Reply @@ -59,98 +65,98 @@ func (u *UserService) Login(ctx *gin.Context, req *LoginReq) (int, *api.Reply) { _ = u.Db.Transaction(func(tx *gorm.DB) error { - // // verify sign - // // --> query nonce - // userNonce, err := model.UserNonceModel.RecentNonce(tx, common.FormatUserWallet(req.Wallet), u.Cfg.Auth.NonceLifespan) - // if err != nil { - // sdk.LogServerErrorToSentry(ctx, err) - // // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("nonce not found"))) - // httpCode = http.StatusInternalServerError - // reply = api.ServerError(errors.New("nonce not found")) - - // return errors.New("nonce not found") - // } - // if userNonce == nil { - // // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("please refresh nonce firstly"))) - // httpCode = http.StatusBadRequest - // reply = api.BadRequest(errors.New("please refresh nonce firstly")) - - // return errors.New("please refresh nonce firstly") - // } - // if strings.EqualFold(req.WalletType, "EOA") { - // // --> verify signature - // message, err := siwe.ParseMessage(req.Message) - // if err != nil { - // // ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) - // httpCode = http.StatusBadRequest - // reply = api.BadRequest(err) - - // return err - // } - // timestamp := time.Now() - // publicKey, err := message.Verify(req.Signature, &req.Domain, &userNonce.Nonce, ×tamp) - // if err != nil { - // // ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) - // httpCode = http.StatusBadRequest - // reply = api.BadRequest(err) - - // return err - // } - // if !strings.EqualFold(req.Wallet, crypto.PubkeyToAddress(*publicKey).Hex()) { - // // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("signature not match"))) - // httpCode = http.StatusBadRequest - // reply = api.BadRequest(errors.New("signature not match")) - - // return errors.New("signature not match") - // } - // } else if strings.EqualFold(req.WalletType, "AA") { - // client, err := ethclient.Dial(u.Cfg.Auth.PolygonRPC) - // if err != nil { - // sdk.LogServerErrorToSentry(ctx, err) - // // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to connect to polygon rpc"))) - // httpCode = http.StatusInternalServerError - // reply = api.ServerError(errors.New("failed to connect to polygon rpc detail:" + err.Error())) - - // return errors.New("failed to connect to polygon rpc detail:" + err.Error()) - // } - - // // get AA's bytecode - // bytecode, err := client.CodeAt(context.Background(), eth_common.HexToAddress(req.Wallet), nil) - // if err != nil { - // sdk.LogServerErrorToSentry(ctx, err) - // // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to get bytecode"))) - // httpCode = http.StatusInternalServerError - // reply = api.ServerError(errors.New("failed to get bytecode detail:" + err.Error())) - - // return errors.New("failed to get bytecode detail:" + err.Error()) - // } - // // if bytecode is not empty, means the wallet has deployed - // // 2023/12/04: only verify signature when AA is deployed! - // if len(bytecode) > 0 { - // account := eth_common.HexToAddress(req.Wallet) - // sig := eth_common.FromHex(req.Signature) - // msg := []byte(req.Message) - - // ok, err := unipass_sigverify.VerifyMessageSignature(context.Background(), account, msg, sig, req.IsEIP191Prefix, client) - // if err != nil { - // sdk.LogServerErrorToSentry(ctx, err) - // // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to verify signature"))) - // httpCode = http.StatusInternalServerError - // reply = api.ServerError(errors.New("failed to verify signature")) - - // return errors.New("failed to verify signature") - // } - // if !ok { - // // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("signature not match"))) - // httpCode = http.StatusBadRequest - // reply = api.BadRequest(errors.New("signature not match")) - - // return errors.New("signature not match") - // } - // } else { - // userVerified = false - // } - // } + // verify sign + // --> query nonce + userNonce, err := model.UserNonceModel.RecentNonce(tx, common.FormatUserWallet(req.Wallet), u.Cfg.Auth.NonceLifespan) + if err != nil { + sdk.LogServerErrorToSentry(ctx, err) + // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("nonce not found"))) + httpCode = http.StatusInternalServerError + reply = api.ServerError(errors.New("nonce not found")) + + return errors.New("nonce not found") + } + if userNonce == nil { + // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("please refresh nonce firstly"))) + httpCode = http.StatusBadRequest + reply = api.BadRequest(errors.New("please refresh nonce firstly")) + + return errors.New("please refresh nonce firstly") + } + if strings.EqualFold(req.WalletType, "EOA") { + // --> verify signature + message, err := siwe.ParseMessage(req.Message) + if err != nil { + // ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + httpCode = http.StatusBadRequest + reply = api.BadRequest(err) + + return err + } + timestamp := time.Now() + publicKey, err := message.Verify(req.Signature, &req.Domain, &userNonce.Nonce, ×tamp) + if err != nil { + // ctx.JSON(http.StatusBadRequest, api.BadRequest(err)) + httpCode = http.StatusBadRequest + reply = api.BadRequest(err) + + return err + } + if !strings.EqualFold(req.Wallet, crypto.PubkeyToAddress(*publicKey).Hex()) { + // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("signature not match"))) + httpCode = http.StatusBadRequest + reply = api.BadRequest(errors.New("signature not match")) + + return errors.New("signature not match") + } + } else if strings.EqualFold(req.WalletType, "AA") { + client, err := ethclient.Dial(u.Cfg.Auth.PolygonRPC) + if err != nil { + sdk.LogServerErrorToSentry(ctx, err) + // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to connect to polygon rpc"))) + httpCode = http.StatusInternalServerError + reply = api.ServerError(errors.New("failed to connect to polygon rpc detail:" + err.Error())) + + return errors.New("failed to connect to polygon rpc detail:" + err.Error()) + } + + // get AA's bytecode + bytecode, err := client.CodeAt(context.Background(), eth_common.HexToAddress(req.Wallet), nil) + if err != nil { + sdk.LogServerErrorToSentry(ctx, err) + // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to get bytecode"))) + httpCode = http.StatusInternalServerError + reply = api.ServerError(errors.New("failed to get bytecode detail:" + err.Error())) + + return errors.New("failed to get bytecode detail:" + err.Error()) + } + // if bytecode is not empty, means the wallet has deployed + // 2023/12/04: only verify signature when AA is deployed! + if len(bytecode) > 0 { + account := eth_common.HexToAddress(req.Wallet) + sig := eth_common.FromHex(req.Signature) + msg := []byte(req.Message) + + ok, err := unipass_sigverify.VerifyMessageSignature(context.Background(), account, msg, sig, req.IsEIP191Prefix, client) + if err != nil { + sdk.LogServerErrorToSentry(ctx, err) + // ctx.JSON(http.StatusInternalServerError, api.ServerError(errors.New("failed to verify signature"))) + httpCode = http.StatusInternalServerError + reply = api.ServerError(errors.New("failed to verify signature")) + + return errors.New("failed to verify signature") + } + if !ok { + // ctx.JSON(http.StatusBadRequest, api.BadRequest(errors.New("signature not match"))) + httpCode = http.StatusBadRequest + reply = api.BadRequest(errors.New("signature not match")) + + return errors.New("signature not match") + } + } else { + userVerified = false + } + } // query user user, err = model.UserModel.Detail(tx, common.FormatUserWallet(req.Wallet)) From e38948e1a0b5dff4b6c0035e00fcec1b18949057 Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 15 Aug 2025 22:48:05 +0800 Subject: [PATCH 13/15] Revert error commit --- cmd/apiserver/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 2e232b2c..ba230709 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -170,7 +170,7 @@ func main() { log.Logger = log.With().Caller().Logger() // setup task manager and start runner - task_manager.InitTaskManager(db, 500000, cfg) + task_manager.InitTaskManager(db, 5, cfg) task_manager.GetTaskManager().StartRunner() storage.SetConfig(cfg) From 8caa172474ea566935617bce5542555832e1e1fc Mon Sep 17 00:00:00 2001 From: hzmangel Date: Fri, 15 Aug 2025 22:58:08 +0800 Subject: [PATCH 14/15] Update SEE response struct to return amount can be claimed and clamed flag --- internal/sdk/spp_client.go | 4 +++- internal_inject/user/user.controller.go | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/sdk/spp_client.go b/internal/sdk/spp_client.go index 9c46f669..9713ef9c 100644 --- a/internal/sdk/spp_client.go +++ b/internal/sdk/spp_client.go @@ -25,7 +25,9 @@ type SeepassResponse struct { } `json:"scr"` See struct { - Amount string `json:"amount"` + Amount string `json:"amount"` + Claimed bool `json:"claimed"` + AmountCanBeClaimed string `json:"amount_can_be_claimed"` } `json:"see"` Level struct { diff --git a/internal_inject/user/user.controller.go b/internal_inject/user/user.controller.go index 8c19dfc9..2317696c 100644 --- a/internal_inject/user/user.controller.go +++ b/internal_inject/user/user.controller.go @@ -415,8 +415,12 @@ func (ctrl *UserController) Detail(ctx *gin.Context) { api.PrintStructAsJson(assetRecords, "assetRecords") if len(assetRecords) == 0 { seepassResp.See.Amount = "0" + seepassResp.See.Claimed = true + seepassResp.See.AmountCanBeClaimed = "0" } else { seepassResp.See.Amount = assetRecords[0].DealtAmount.String() + seepassResp.See.Claimed = assetRecords[0].Claimed + seepassResp.See.AmountCanBeClaimed = assetRecords[0].ProcessingAmount.String() } // populate deepseek key From b0cc53158326254ee88b520b1902eebe786e66db Mon Sep 17 00:00:00 2001 From: hzmangel Date: Sat, 16 Aug 2025 22:49:11 +0800 Subject: [PATCH 15/15] Remove unused code --- internal/sdk/indexer_client.go | 39 ---------------------------------- 1 file changed, 39 deletions(-) diff --git a/internal/sdk/indexer_client.go b/internal/sdk/indexer_client.go index b608570f..0590d11d 100644 --- a/internal/sdk/indexer_client.go +++ b/internal/sdk/indexer_client.go @@ -8,9 +8,7 @@ import ( "github.com/rs/zerolog/log" "github.com/samber/lo" - "github.com/shopspring/decimal" "github.com/theseed-labs/os-backend/internal" - "github.com/theseed-labs/os-backend/internal/common" ) type SeedHolderRecord struct { @@ -24,11 +22,6 @@ type SeasonSBTRecord struct { Values []string `json:"values"` } -type Erc20SnapshotRecord struct { - Wallet string `json:"wallet"` - Value decimal.Decimal `json:"amount"` -} - type ComputeNodeSbt struct { Node int `json:"node"` Sbt int `json:"sbt"` @@ -182,35 +175,3 @@ func (c *IndexerClient) GetComputeNodeSbt() (*ComputeNodeSbt, error) { return respData, nil } - -func (c *IndexerClient) GetUserCurrentScrAmount(userWallet string) (decimal.Decimal, error) { - endpoint := fmt.Sprintf("%s/snapshot/%s/%s", c.ApiBase, internal.ScrContractType, internal.ScrContractAddr) - log.Debug().Msgf("Try to get SCR amount, endpoint is %s", endpoint) - - resp, err := http.Get(endpoint) - if err != nil { - return decimal.Zero, err - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return decimal.Zero, fmt.Errorf("got error response from indexer endpoint: status code: %d, resp: %+v", resp.StatusCode, resp) - } - - var respData []*Erc20SnapshotRecord - err = json.NewDecoder(resp.Body).Decode(&respData) - if err != nil { - return decimal.Zero, err - } - - formattedWallet := common.FormatUserWallet(userWallet) - scrAmount := decimal.Zero - for _, record := range respData { - if record.Wallet == formattedWallet { - scrAmount = record.Value - break - } - } - return scrAmount, nil -}