github.com/lino-network/lino@v0.6.11/x/post/manager/manager.go (about)

     1  package manager
     2  
     3  import (
     4  	"fmt"
     5  
     6  	codec "github.com/cosmos/cosmos-sdk/codec"
     7  	sdk "github.com/cosmos/cosmos-sdk/types"
     8  
     9  	linotypes "github.com/lino-network/lino/types"
    10  	"github.com/lino-network/lino/utils"
    11  	acc "github.com/lino-network/lino/x/account"
    12  	dev "github.com/lino-network/lino/x/developer"
    13  	global "github.com/lino-network/lino/x/global"
    14  	"github.com/lino-network/lino/x/post/model"
    15  	"github.com/lino-network/lino/x/post/types"
    16  	price "github.com/lino-network/lino/x/price"
    17  	rep "github.com/lino-network/lino/x/reputation"
    18  	vote "github.com/lino-network/lino/x/vote"
    19  )
    20  
    21  const (
    22  	exportVersion = 2
    23  	importVersion = 2
    24  )
    25  
    26  type PostManager struct {
    27  	postStorage model.PostStorage
    28  
    29  	// deps
    30  	am    acc.AccountKeeper
    31  	gm    global.GlobalKeeper
    32  	dev   dev.DeveloperKeeper
    33  	rep   rep.ReputationKeeper
    34  	vote  vote.VoteKeeper
    35  	price price.PriceKeeper
    36  }
    37  
    38  // NewPostManager - create a new post manager
    39  func NewPostManager(key sdk.StoreKey, am acc.AccountKeeper, gm global.GlobalKeeper, dev dev.DeveloperKeeper, rep rep.ReputationKeeper, price price.PriceKeeper, vote vote.VoteKeeper) PostManager {
    40  	return PostManager{
    41  		postStorage: model.NewPostStorage(key),
    42  		am:          am,
    43  		gm:          gm,
    44  		dev:         dev,
    45  		rep:         rep,
    46  		vote:        vote,
    47  		price:       price,
    48  	}
    49  }
    50  
    51  // DoesPostExist - check if post exist
    52  // 1. permlink kv exists.
    53  // 2. post is not marked as deleted.
    54  func (pm PostManager) DoesPostExist(ctx sdk.Context, permlink linotypes.Permlink) bool {
    55  	if !pm.postStorage.HasPost(ctx, permlink) {
    56  		return false
    57  	}
    58  	post, _ := pm.postStorage.GetPost(ctx, permlink)
    59  	return !post.IsDeleted
    60  }
    61  
    62  // GetPost - return post.
    63  // return err if post is deleted.
    64  func (pm PostManager) GetPost(ctx sdk.Context, permlink linotypes.Permlink) (model.Post, sdk.Error) {
    65  	post, err := pm.postStorage.GetPost(ctx, permlink)
    66  	if err != nil {
    67  		return model.Post{}, err
    68  	}
    69  	if post.IsDeleted {
    70  		return model.Post{}, types.ErrPostDeleted(permlink)
    71  	}
    72  	return *post, nil
    73  }
    74  
    75  // CreatePost validate and handles CreatePostMsg
    76  // stateful validation;
    77  // 1. both author and post id exists.
    78  // 2. if createdBy is not author, then it must be an app.
    79  // 3. post's permlink does not exists.
    80  func (pm PostManager) CreatePost(ctx sdk.Context, author linotypes.AccountKey, postID string, createdBy linotypes.AccountKey, content string, title string) sdk.Error {
    81  	if !pm.am.DoesAccountExist(ctx, author) {
    82  		return types.ErrAccountNotFound(author)
    83  	}
    84  	if !pm.am.DoesAccountExist(ctx, createdBy) {
    85  		return types.ErrAccountNotFound(createdBy)
    86  	}
    87  	permlink := linotypes.GetPermlink(author, postID)
    88  	if pm.postStorage.HasPost(ctx, permlink) {
    89  		return types.ErrPostAlreadyExist(permlink)
    90  	}
    91  	if author != createdBy {
    92  		// if created by app, then createdBy must either be the app or an affiliated account of app.
    93  		dev := createdBy
    94  		var err error
    95  		createdBy, err = pm.dev.GetAffiliatingApp(ctx, createdBy)
    96  		if err != nil {
    97  			return types.ErrDeveloperNotFound(dev)
    98  		}
    99  	}
   100  
   101  	createdAt := ctx.BlockHeader().Time.Unix()
   102  	postInfo := &model.Post{
   103  		PostID:    postID,
   104  		Title:     title,
   105  		Content:   content,
   106  		Author:    author,
   107  		CreatedBy: createdBy,
   108  		CreatedAt: createdAt,
   109  		UpdatedAt: createdAt,
   110  	}
   111  	pm.postStorage.SetPost(ctx, postInfo)
   112  	return nil
   113  }
   114  
   115  // UpdatePost - update post title, content and links.
   116  // stateful validation:
   117  // 1. author exist.
   118  // 2. post exist.
   119  func (pm PostManager) UpdatePost(ctx sdk.Context, author linotypes.AccountKey, postID, title, content string) sdk.Error {
   120  	permlink := linotypes.GetPermlink(author, postID)
   121  	postInfo, err := pm.postStorage.GetPost(ctx, permlink)
   122  	if err != nil {
   123  		// post not exists
   124  		return err
   125  	}
   126  	postInfo.Title = title
   127  	postInfo.Content = content
   128  	postInfo.UpdatedAt = ctx.BlockHeader().Time.Unix()
   129  	pm.postStorage.SetPost(ctx, postInfo)
   130  	return nil
   131  }
   132  
   133  // DeletePost - delete post by author or content censorship
   134  // stateful validation:
   135  // 1. permlink exists.
   136  // 2. post not deleted.
   137  // Delete does not delete the post in kv store, as that will make `permlink` not permanent.
   138  // It is marked as deleted, then on deleted posts,
   139  // 1. manager.DoesPostExist will return false.
   140  // 2. manager.GetPost will return ErrPermlinkDeleted.
   141  // 3. manager.CreatePost will return ErrPostAlreadyExist.
   142  func (pm PostManager) DeletePost(ctx sdk.Context, permlink linotypes.Permlink) sdk.Error {
   143  	post, err := pm.postStorage.GetPost(ctx, permlink)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	if post.IsDeleted {
   148  		return types.ErrPostDeleted(permlink)
   149  	}
   150  	post.IsDeleted = true
   151  	post.Title = ""
   152  	post.Content = ""
   153  	pm.postStorage.SetPost(ctx, post)
   154  	return nil
   155  }
   156  
   157  // LinoDonate handles donation using lino.
   158  // stateful validation:
   159  // 1. post exits
   160  // 2. from/to account exists.
   161  // 3. no self donation.
   162  // 4. if app is not empty, then developer must exist.
   163  // 5. amount positive > 0.
   164  // 6. 9.9% of amount > 0 coin.
   165  func (pm PostManager) LinoDonate(ctx sdk.Context, from linotypes.AccountKey, amount linotypes.Coin, author linotypes.AccountKey, postID string, app linotypes.AccountKey) sdk.Error {
   166  	if err := pm.validateLinoDonation(ctx, from, amount, author, postID, app); err != nil {
   167  		return err
   168  	}
   169  	// donation.
   170  	rate := sdk.MustNewDecFromStr(linotypes.ConsumptionFrictionRate)
   171  	frictionCoin := linotypes.DecToCoin(amount.ToDec().Mul(rate))
   172  	if frictionCoin.IsZero() {
   173  		return types.ErrDonateAmountTooLittle()
   174  	}
   175  	// friction goes to the friction pool for voters.
   176  	err := pm.am.MoveToPool(ctx,
   177  		linotypes.VoteFrictionPool, linotypes.NewAccOrAddrFromAcc(from), frictionCoin)
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	// rest goes to the author.
   183  	err = pm.am.MoveCoin(ctx, linotypes.NewAccOrAddrFromAcc(from),
   184  		linotypes.NewAccOrAddrFromAcc(author), amount.Minus(frictionCoin))
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	mdamount, err := pm.price.CoinToMiniDollar(ctx, amount)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	return pm.afterDonation(ctx, author, postID, from, mdamount, frictionCoin, app)
   194  }
   195  
   196  // IDADonate - handle IDA donation.
   197  func (pm PostManager) IDADonate(ctx sdk.Context, from linotypes.AccountKey, n linotypes.MiniIDA, author linotypes.AccountKey, postID string, app, signer linotypes.AccountKey) sdk.Error {
   198  	if err := pm.validateIDADonate(ctx, from, n, author, postID, app); err != nil {
   199  		return err
   200  	}
   201  	signerApp, err := pm.dev.GetAffiliatingApp(ctx, signer)
   202  	if err != nil || signerApp != app {
   203  		return types.ErrInvalidSigner()
   204  	}
   205  	idaPrice, err := pm.dev.GetMiniIDAPrice(ctx, app)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	// amount = tax + dollarTransfer
   211  	// tax: burned to lino
   212  	// dollarTransfer: moved from sender to receipient.
   213  	rate := sdk.MustNewDecFromStr(linotypes.ConsumptionFrictionRate)
   214  	dollarAmount := linotypes.MiniIDAToMiniDollar(n, idaPrice) // unit conversion
   215  	tax := linotypes.NewMiniDollarFromInt(dollarAmount.ToDec().Mul(rate).TruncateInt())
   216  
   217  	// burn and check taxable coins.
   218  	// tax will be subtracted from @p from's IDA account, and converted to coins and
   219  	// saved in the account.
   220  	taxcoins, err := pm.dev.BurnIDA(ctx, app, from, tax)
   221  	if err != nil {
   222  		return err
   223  	}
   224  	if !taxcoins.IsPositive() {
   225  		return types.ErrDonateAmountTooLittle()
   226  	}
   227  
   228  	// friction goes to the friction pool for voters.
   229  	err = pm.am.MoveToPool(ctx,
   230  		linotypes.VoteFrictionPool, linotypes.NewAccOrAddrFromAcc(from), taxcoins)
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	// rest goes to the author
   236  	if err := pm.dev.MoveIDA(ctx, app, from, author, dollarAmount.Minus(tax)); err != nil {
   237  		return err
   238  	}
   239  
   240  	return pm.afterDonation(ctx, author, postID, from, dollarAmount, taxcoins, app)
   241  }
   242  
   243  func (pm PostManager) afterDonation(ctx sdk.Context, author linotypes.AccountKey, postID string, from linotypes.AccountKey, damount linotypes.MiniDollar, friction linotypes.Coin, app linotypes.AccountKey) sdk.Error {
   244  	// impact is the evaluated consumption.
   245  	impact, err := pm.rep.DonateAt(ctx, from, linotypes.GetPermlink(author, postID), damount)
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	// update consumptionm window
   251  	consumptionWindow := pm.postStorage.GetConsumptionWindow(ctx)
   252  	pm.postStorage.SetConsumptionWindow(ctx, consumptionWindow.Plus(impact))
   253  
   254  	// record friction stats.
   255  	err = pm.vote.RecordFriction(ctx, friction)
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	// add content bonus return event.
   261  	rewardEvent := types.RewardEvent{
   262  		PostAuthor: author,
   263  		PostID:     postID,
   264  		Consumer:   from,
   265  		Evaluate:   impact,
   266  		FromApp:    app,
   267  	}
   268  	eventTime := ctx.BlockHeader().Time.Unix() + linotypes.ConsumptionFreezingPeriodSec
   269  	if err := pm.gm.RegisterEventAtTime(ctx, eventTime, rewardEvent); err != nil {
   270  		return err
   271  	}
   272  	return nil
   273  }
   274  
   275  // donation stateful basic validation:
   276  // 1. post exits
   277  // 2. from/to account exists.
   278  // 3. no self donation.
   279  func (pm PostManager) validateDonationBasic(ctx sdk.Context, from linotypes.AccountKey, author linotypes.AccountKey, postID string) sdk.Error {
   280  	if from == author {
   281  		return types.ErrCannotDonateToSelf(from)
   282  	}
   283  	if !pm.am.DoesAccountExist(ctx, from) {
   284  		return types.ErrAccountNotFound(from)
   285  	}
   286  	if !pm.am.DoesAccountExist(ctx, author) {
   287  		return types.ErrAccountNotFound(author)
   288  	}
   289  	permlink := linotypes.GetPermlink(author, postID)
   290  	if !pm.DoesPostExist(ctx, permlink) {
   291  		return types.ErrPostNotFound(permlink)
   292  	}
   293  	return nil
   294  }
   295  
   296  // lino donation stateful.
   297  // 1. basic validation
   298  // 2. lino amount > 0.
   299  // 3. if app is not empty, then developer must exist.
   300  func (pm PostManager) validateLinoDonation(ctx sdk.Context, from linotypes.AccountKey, amount linotypes.Coin, author linotypes.AccountKey, postID string, app linotypes.AccountKey) sdk.Error {
   301  	err := pm.validateDonationBasic(ctx, from, author, postID)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	if app != "" && !pm.dev.DoesDeveloperExist(ctx, app) {
   306  		return types.ErrDeveloperNotFound(app)
   307  	}
   308  	if !amount.IsPositive() {
   309  		return types.ErrInvalidDonationAmount(amount)
   310  	}
   311  	return nil
   312  }
   313  
   314  // IDA donation stateful.
   315  // 1. basic validation
   316  // 2. lino amount > 0.
   317  // 3. app cannot be empty and the developer must exist.
   318  func (pm PostManager) validateIDADonate(ctx sdk.Context, from linotypes.AccountKey, n linotypes.MiniIDA, author linotypes.AccountKey, postID string, app linotypes.AccountKey) sdk.Error {
   319  	err := pm.validateDonationBasic(ctx, from, author, postID)
   320  	if err != nil {
   321  		return err
   322  	}
   323  	if app == "" || !pm.dev.DoesDeveloperExist(ctx, app) {
   324  		return types.ErrDeveloperNotFound(app)
   325  	}
   326  	if !n.IsPositive() {
   327  		return types.ErrNonPositiveIDAAmount(n)
   328  	}
   329  	return nil
   330  }
   331  
   332  // ExecRewardEvent - execute reward events.
   333  func (pm PostManager) ExecRewardEvent(ctx sdk.Context, event types.RewardEvent) sdk.Error {
   334  	// check if post is deleted, Note that if post is deleted, it's ok to just
   335  	// skip this event. It does not return an error because errors will panic in events.
   336  	permlink := linotypes.GetPermlink(event.PostAuthor, event.PostID)
   337  	if !pm.DoesPostExist(ctx, permlink) {
   338  		return nil
   339  	}
   340  
   341  	// if developer exist, add to developer consumption
   342  	if pm.dev.DoesDeveloperExist(ctx, event.FromApp) {
   343  		// ignore report consumption err.
   344  		_ = pm.dev.ReportConsumption(ctx, event.FromApp, event.Evaluate)
   345  	}
   346  
   347  	return pm.allocContentBonus(ctx, event.Evaluate, event.PostAuthor)
   348  }
   349  
   350  func (pm PostManager) allocContentBonus(ctx sdk.Context, impact linotypes.MiniDollar, author linotypes.AccountKey) sdk.Error {
   351  	if impact.IsZero() {
   352  		return nil
   353  	}
   354  
   355  	// get consumption window and update the window
   356  	consumptionWindow := pm.postStorage.GetConsumptionWindow(ctx)
   357  	pm.postStorage.SetConsumptionWindow(ctx, consumptionWindow.Minus(impact))
   358  
   359  	// XXX(yumin): the ratio is zero when the window is zero, because the consumption window
   360  	// as the sum of past donation impacts, shall be large than zero as the @p impact is nonzero.
   361  	// consumptionRatio = (this consumption * penalty score) / (total consumption in 7 days window)
   362  	consumptionRatio := sdk.ZeroDec()
   363  	if !consumptionWindow.ToDec().IsZero() {
   364  		consumptionRatio = impact.ToDec().Quo(consumptionWindow.ToDec())
   365  	}
   366  	rewardPool, err := pm.am.GetPool(ctx, linotypes.InflationConsumptionPool)
   367  	if err != nil {
   368  		return err
   369  	}
   370  	// reward = (consumption reward pool) * (consumptionRatio)
   371  	reward := linotypes.DecToCoin(rewardPool.ToDec().Mul(consumptionRatio))
   372  	return pm.am.MoveFromPool(ctx,
   373  		linotypes.InflationConsumptionPool, linotypes.NewAccOrAddrFromAcc(author), reward)
   374  }
   375  
   376  func (pm PostManager) GetComsumptionWindow(ctx sdk.Context) linotypes.MiniDollar {
   377  	return pm.postStorage.GetConsumptionWindow(ctx)
   378  }
   379  
   380  // Export - to file.
   381  func (pm PostManager) ExportToFile(ctx sdk.Context, cdc *codec.Codec, filepath string) error {
   382  	state := &model.PostTablesIR{
   383  		Version: exportVersion,
   384  	}
   385  	storeList := pm.postStorage.PartialStoreMap(ctx)
   386  
   387  	// export posts
   388  	posts := make([]model.PostIR, 0)
   389  	postSubStore := storeList[string(model.PostSubStore)]
   390  	postSubStore.Iterate(func(key []byte, val interface{}) bool {
   391  		post := val.(*model.Post)
   392  		posts = append(posts, model.PostIR(*post))
   393  		return false
   394  	})
   395  	state.Posts = posts
   396  
   397  	// consumption window
   398  	state.ConsumptionWindow = pm.postStorage.GetConsumptionWindow(ctx)
   399  
   400  	return utils.Save(filepath, cdc, state)
   401  }
   402  
   403  // Import - from file
   404  func (pm PostManager) ImportFromFile(ctx sdk.Context, cdc *codec.Codec, filepath string) error {
   405  	rst, err := utils.Load(filepath, cdc, func() interface{} { return &model.PostTablesIR{} })
   406  	if err != nil {
   407  		return err
   408  	}
   409  	table := rst.(*model.PostTablesIR)
   410  
   411  	if table.Version != importVersion {
   412  		return fmt.Errorf("unsupported import version: %d", table.Version)
   413  	}
   414  
   415  	for _, v := range table.Posts {
   416  		pm.postStorage.SetPost(ctx, &model.Post{
   417  			PostID:    v.PostID,
   418  			Title:     v.Title,
   419  			Content:   v.Content,
   420  			Author:    v.Author,
   421  			CreatedBy: v.Author,
   422  			CreatedAt: v.CreatedAt,
   423  			UpdatedAt: v.UpdatedAt,
   424  			IsDeleted: v.IsDeleted,
   425  		})
   426  	}
   427  
   428  	pm.postStorage.SetConsumptionWindow(ctx, table.ConsumptionWindow)
   429  	return nil
   430  }