Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f4183e5
Merge pull request #135 from Taoist-Labs/preview
hzmangel Jun 26, 2024
8979894
Merge pull request #138 from Taoist-Labs/preview
hzmangel Jul 19, 2024
97286be
Merge pull request #140 from Taoist-Labs/preview
hzmangel Jul 24, 2024
da92cfc
Fix error while applying budget for project
hzmangel Aug 1, 2024
c76f479
Merge pull request #144 from Taoist-Labs/preview
hzmangel Aug 11, 2024
27bc953
Merge pull request #148 from Taoist-Labs/preview
hzmangel Oct 8, 2024
68ac679
Merge pull request #156 from Taoist-Labs/preview
hzmangel Nov 12, 2024
70da465
Merge pull request #158 from Taoist-Labs/fix_sip_rollback_check
hzmangel Nov 14, 2024
ef813e2
Merge pull request #162 from Taoist-Labs/preview
hzmangel Nov 20, 2024
d0edb3f
Merge pull request #166 from Taoist-Labs/preview
hzmangel Nov 20, 2024
73ff24a
Merge pull request #171 from Taoist-Labs/preview
CooCoode Dec 11, 2024
adbc677
Merge pull request #173 from Taoist-Labs/preview
CooCoode Dec 19, 2024
a4ba4a3
Merge pull request #181 from Taoist-Labs/preview
CooCoode Jan 25, 2025
d9b7b71
Merge pull request #183 from Taoist-Labs/preview
CooCoode Feb 7, 2025
e6e92bd
Merge pull request #185 from Taoist-Labs/preview
CooCoode Feb 8, 2025
6b13b5d
Merge pull request #187 from Taoist-Labs/preview
CooCoode Feb 8, 2025
3a05d5a
Merge pull request #189 from Taoist-Labs/preview
CooCoode Feb 8, 2025
b250084
Merge pull request #191 from Taoist-Labs/preview
CooCoode Feb 9, 2025
a9e0eca
Merge pull request #193 from Taoist-Labs/preview
CooCoode Feb 11, 2025
dca4721
Merge pull request #195 from Taoist-Labs/preview
CooCoode Feb 12, 2025
1e52ad2
Merge pull request #197 from Taoist-Labs/preview
CooCoode Feb 13, 2025
f07f8d1
Merge pull request #199 from Taoist-Labs/preview
CooCoode Feb 19, 2025
644ac7b
Merge pull request #201 from Taoist-Labs/preview
CooCoode Mar 13, 2025
d88f53a
Merge pull request #203 from Taoist-Labs/preview
CooCoode Mar 16, 2025
907f9d8
fix: reback code
CooCoode Mar 16, 2025
739c435
Revert "fix: reback code"
CooCoode Mar 16, 2025
da2b2f3
Merge pull request #206 from Taoist-Labs/preview
CooCoode Apr 11, 2025
e911dbe
Merge pull request #208 from Taoist-Labs/preview
CooCoode Jun 18, 2025
5e59976
Add missing proposal state name
hzmangel Jul 7, 2025
648aa13
Add missing proposal state
hzmangel Jul 7, 2025
a4b3296
Merge pull request #210 from Taoist-Labs/preview
hzmangel Aug 10, 2025
1654c76
Merge branch 'main' into dev
hzmangel Aug 13, 2025
1180643
feat(asset_records): add my transfers endpoint and filtering
hzmangel Aug 13, 2025
1ca020b
Add /list to list all user asset transfer logs to avoid confusion
hzmangel Aug 13, 2025
1fbf2b4
feat(asset_records): add SEE token claim functionality from indexer
hzmangel Aug 14, 2025
442f318
Update claim logic
hzmangel Aug 14, 2025
6a8dead
Update claim SEE logic
hzmangel Aug 15, 2025
4c80cee
Clean unused code
hzmangel Aug 15, 2025
0f1c209
revert error commit
hzmangel Aug 15, 2025
e38948e
Revert error commit
hzmangel Aug 15, 2025
8caa172
Update SEE response struct to return amount can be claimed and clamed…
hzmangel Aug 15, 2025
b0cc531
Remove unused code
hzmangel Aug 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions internal/model/proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ var ProposalStateIdNameMapping = map[string]ProposalState{
"execution_failed": ProposalStateExecutionFailed,
"vetoed": ProposalStateVetoed,
"deleted_from_metaforo": ProposalStateDeletedFromMetaforo,
"metaforo_error": ProposalStateUncategorizedMetaforoError,
}

var ProposalStateName = []string{
Expand All @@ -83,6 +84,8 @@ var ProposalStateName = []string{
"executed",
"execution_failed",
"vetoed",
"deleted_from_metaforo",
"metaforo_error",
}

// MetaforoUser saves user mapping between OS and metaforo
Expand Down
6 changes: 6 additions & 0 deletions internal/model/user_asset_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 10 additions & 6 deletions internal/model/user_asset_transfer_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion internal/sdk/spp_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
87 changes: 83 additions & 4 deletions internal_inject/asset_records/asset_records.controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -49,11 +51,13 @@ func Register(fatherGroup *gin.RouterGroup) {
}

// No auth endpoints
assetRecordsGroup.GET("/", assetRecords.List)
assetRecordsGroup.GET("/:id", assetRecords.Detail)
assetRecordsGroup.GET("/list", assetRecords.List)
assetRecordsGroup.GET("/show/:id", assetRecords.Detail)

// Auth required endpoints
assetRecordsAuthGroup.POST("/", assetRecords.Create)
assetRecordsAuthGroup.GET("/my", assetRecords.MyList)
assetRecordsAuthGroup.POST("/new", assetRecords.Create)
assetRecordsAuthGroup.POST("/claim_see", assetRecords.ClaimSee)
}

// Create creates a new asset transfer
Expand Down Expand Up @@ -105,6 +109,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
Expand All @@ -122,7 +175,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)
Expand Down Expand Up @@ -189,3 +242,29 @@ 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) {
if time.Now().After(internal.ClaimSeeAssetEndDate) {
err := errors.New("claim see asset end")
sdk.LogUserSideError(ctx, err)
ctx.JSON(http.StatusBadRequest, api.BadRequest(err))
return
}

user := api.ForContextOnlyUser(ctx)
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
}
}
87 changes: 85 additions & 2 deletions internal_inject/asset_records/asset_records.service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package asset_records_inject
import (
"errors"
"fmt"
"net/http"
"strings"

"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"
"gorm.io/gorm"
"gorm.io/gorm/clause"
Expand Down Expand Up @@ -124,11 +127,91 @@ 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
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
// 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

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 = ? AND claimed = false", formattedWallet, internal.AssetNameSEE).
Find(&existingRecords).Error

if err != nil {
statusCode = http.StatusInternalServerError
return err
}

if len(existingRecords) == 0 {
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 {
statusCode = http.StatusBadRequest
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()),
"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
}
21 changes: 12 additions & 9 deletions internal_inject/asset_records/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,9 +31,9 @@ const (

// Pagination defaults
const (
DefaultPageSize = 20
MaxPageSize = 100
DefaultPageNumber = 1
DefaultPageSize = 20
MaxPageSize = 100
DefaultPageNumber = 1
)

// Field names
Expand All @@ -50,4 +53,4 @@ const (
// Default values
const (
DefaultTransferAssetName = "SEE"
)
)
4 changes: 4 additions & 0 deletions internal_inject/user/user.controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down