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 }