Keeper

The main logic of the House module definitions and methods.

Create Campaign

There are several steps for the campaign to be created:

  1. Check for existing campaign with the same identifier.

  2. Check authorization if the creator and promoter are not the same.

  3. Validate the ticket payload according to the defined reward type and category.

  4. Transfer the pool amount to the pool module account.

  5. Store the campaign data in the state.

func (k msgServer) CreateCampaign(goCtx context.Context, msg *types.MsgCreateCampaign) (*types.MsgCreateCampaignResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	// Check if the value already exists
	_, isFound := k.GetCampaign(ctx, msg.Uid)
	if isFound {
		return nil, sdkerrors.Wrapf(sdkerrtypes.ErrInvalidRequest, "campaign with the uid: %s already exists", msg.Uid)
	}

	var payload types.CreateCampaignPayload
	if err := k.ovmKeeper.VerifyTicketUnmarshal(goCtx, msg.Ticket, &payload); err != nil {
		return nil, sdkerrors.Wrapf(types.ErrInTicketVerification, "%s", err)
	}

	if msg.Creator != payload.Promoter {
		if err := utils.ValidateMsgAuthorization(k.authzKeeper, ctx, msg.Creator, payload.Promoter, msg,
			types.ErrAuthorizationNotFound, types.ErrAuthorizationNotAccepted); err != nil {
			return nil, err
		}
	}

	if err := payload.Validate(cast.ToUint64(ctx.BlockTime().Unix())); err != nil {
		return nil, err
	}

	totalRewardAmount := sdkmath.ZeroInt()
	if !payload.RewardAmount.MainAccountAmount.IsNil() {
		totalRewardAmount = totalRewardAmount.Add(payload.RewardAmount.MainAccountAmount)
	}
	if !payload.RewardAmount.SubaccountAmount.IsNil() {
		totalRewardAmount = totalRewardAmount.Add(payload.RewardAmount.SubaccountAmount)
	}

	if msg.TotalFunds.LT(totalRewardAmount) {
		return nil, sdkerrors.Wrapf(sdkerrtypes.ErrInvalidRequest, "defined reward amount %s is more than total funds %s", totalRewardAmount, msg.TotalFunds)
	}

	campaign := types.NewCampaign(
		msg.Creator, payload.Promoter, msg.Uid,
		payload.StartTs, payload.EndTs, payload.ClaimsPerCategory,
		payload.RewardType,
		payload.Category,
		payload.RewardAmountType,
		payload.RewardAmount,
		payload.IsActive,
		payload.Meta,
		types.NewPool(msg.TotalFunds),
	)

	rewardFactory, err := campaign.GetRewardsFactory()
	if err != nil {
		return nil, err
	}

	err = rewardFactory.ValidateCampaign(campaign, cast.ToUint64(ctx.BlockTime().Unix()))
	if err != nil {
		return nil, err
	}

	// transfer the pool amount to the reward pool module account
	if err := k.modFunder.Fund(
		types.RewardPoolFunder{}, ctx,
		sdk.MustAccAddressFromBech32(payload.Promoter),
		msg.TotalFunds,
	); err != nil {
		return nil, sdkerrors.Wrapf(types.ErrInFundingCampaignPool, "%s", err)
	}

	k.SetCampaign(ctx, campaign)

	msg.EmitEvent(&ctx, msg.Uid)

	return &types.MsgCreateCampaignResponse{}, nil
}

Update Campaign

There are several steps for updating a campaign:

  1. Get current campaign configurations from the state.

  2. Validate the payload of the campaign update.

  3. Check the Authorization if the creator and the campaign promoter are not the same.

  4. Calculate the top-up amount of the campaign pool.

  5. Update the end timestamp of the campaign.

  6. Update the active or inactive status of the campaign.

func (k msgServer) UpdateCampaign(goCtx context.Context, msg *types.MsgUpdateCampaign) (*types.MsgUpdateCampaignResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	var payload types.UpdateCampaignPayload
	if err := k.ovmKeeper.VerifyTicketUnmarshal(goCtx, msg.Ticket, &payload); err != nil {
		return nil, sdkerrors.Wrapf(types.ErrInTicketVerification, "%s", err)
	}

	if err := payload.Validate(cast.ToUint64(ctx.BlockTime().Unix())); err != nil {
		return nil, err
	}

	// Check if the value exists
	campaign, isFound := k.GetCampaign(ctx, msg.Uid)
	if !isFound {
		return nil, sdkerrors.Wrapf(sdkerrtypes.ErrKeyNotFound, "campaign with the id: %s does not exist", msg.Uid)
	}

	if !campaign.IsActive {
		return nil, sdkerrors.Wrap(sdkerrtypes.ErrInvalidRequest, "inactive campaign can not be updated")
	}

	// Checks if the msg creator is the same as the current owner
	if msg.Creator != campaign.Promoter {
		if err := utils.ValidateMsgAuthorization(k.authzKeeper, ctx, msg.Creator, campaign.Promoter, msg,
			types.ErrAuthorizationNotFound, types.ErrAuthorizationNotAccepted); err != nil {
			return nil, err
		}
	}

	if !msg.TopupFunds.IsNil() && msg.TopupFunds.GT(sdkmath.ZeroInt()) {
		// transfer the pool amount to the reward pool module account
		if err := k.modFunder.Fund(
			types.RewardPoolFunder{}, ctx,
			sdk.MustAccAddressFromBech32(campaign.Promoter),
			msg.TopupFunds,
		); err != nil {
			return nil, sdkerrors.Wrapf(types.ErrInFundingCampaignPool, "%s", err)
		}

		campaign.Pool.Total = campaign.Pool.Total.Add(msg.TopupFunds)
	}

	campaign.EndTS = payload.EndTs
	campaign.IsActive = payload.IsActive

	k.SetCampaign(ctx, campaign)

	msg.EmitEvent(&ctx, msg.Uid)

	return &types.MsgUpdateCampaignResponse{}, nil
}

WithdrawFunds

There are several steps for a campaign funds withdrawal to be made:

  1. Get the campaign from the blockchain state.

  2. Validate authorization if the promoter and the creator are not the same.

  3. Calculate the available amount that can be withdrawn from the campaign pool.

  4. Transfer the withdrawable amount from the pool module account to the promoter.

  5. Update the campaign balance and set in the state of the blockchain.

func (k msgServer) WithdrawFunds(goCtx context.Context, msg *types.MsgWithdrawFunds) (*types.MsgWithdrawFundsResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	var payload types.WithdrawFundsPayload
	if err := k.ovmKeeper.VerifyTicketUnmarshal(goCtx, msg.Ticket, &payload); err != nil {
		return nil, sdkerrors.Wrapf(types.ErrInTicketVerification, "%s", err)
	}

	// Validate ticket payload
	if err := payload.Validate(); err != nil {
		return nil, err
	}

	// Check if the campaign exists
	valFound, isFound := k.GetCampaign(ctx, msg.Uid)
	if !isFound {
		return nil, sdkerrors.Wrap(sdkerrtypes.ErrKeyNotFound, "campaign not found")
	}

	if payload.Promoter != valFound.Promoter {
		return nil, sdkerrors.Wrap(sdkerrtypes.ErrKeyNotFound, "promoter should be the same as stored campaign promoter")
	}

	// Checks if the msg creator is the same as the current owner
	if msg.Creator != valFound.Promoter {
		if err := utils.ValidateMsgAuthorization(k.authzKeeper, ctx, msg.Creator, valFound.Promoter, msg,
			types.ErrAuthorizationNotFound, types.ErrAuthorizationNotAccepted); err != nil {
			return nil, err
		}
	}
	availableAmount := valFound.Pool.Total.Sub(valFound.Pool.Spent)
	// check if the pool amount is positive
	if availableAmount.IsNil() || !availableAmount.GT(sdkmath.ZeroInt()) {
		return nil, sdkerrors.Wrapf(types.ErrWithdrawFromCampaignPool, "pool amount should be positive")
	}

	// transfer the funds present in campaign to the promoter
	if err := k.modFunder.Refund(
		types.RewardPoolFunder{}, ctx,
		sdk.MustAccAddressFromBech32(payload.Promoter),
		availableAmount,
	); err != nil {
		return nil, sdkerrors.Wrapf(types.ErrWithdrawFromCampaignPool, "%s", err)
	}
	// set the pool amount to zero
	valFound.Pool.Total = sdkmath.ZeroInt()
	// deactivate the campaign
	valFound.IsActive = false

	// store the campaign
	k.SetCampaign(ctx, valFound)
	// emit withdraw event
	msg.EmitEvent(&ctx, msg.Uid)

	return &types.MsgWithdrawFundsResponse{}, nil
}

GrantReward

There are several steps for a reward to be granted:

  1. Check for existing rewards with the identifier.

  2. Retrieve the campaign information from the blockchain state.

  3. Check the campaign validity period according to the current block time.

  4. Calculate the reward amount according to the campaign reward type and category.

  5. Check the claims per category for the reward.

  6. Check the campaign pool balance to have enough funds.

  7. Distribute the reward to the corresponding receiver account's subaccount.

  8. Update the campaign's pool spent amount.

  9. Store the reward, reward by campaign and reward by category into the blockchain state.

func (k msgServer) GrantReward(goCtx context.Context, msg *types.MsgGrantReward) (*types.MsgGrantRewardResponse, error) {
	ctx := sdk.UnwrapSDKContext(goCtx)

	if _, isFound := k.GetReward(ctx, msg.Uid); isFound {
		return nil, sdkerrors.Wrapf(sdkerrtypes.ErrInvalidRequest, "reward grant with uid: %s exists", msg.Uid)
	}

	campaign, isFound := k.GetCampaign(ctx, msg.CampaignUid)
	if !isFound {
		return nil, sdkerrors.Wrapf(sdkerrtypes.ErrInvalidRequest, "campaign with the uid: %s not found", msg.CampaignUid)
	}

	if !campaign.IsActive {
		return nil, sdkerrors.Wrapf(sdkerrtypes.ErrInvalidRequest, "campaign with uid: %s not active", msg.CampaignUid)
	}

	if err := campaign.CheckTS(cast.ToUint64(ctx.BlockTime().Unix())); err != nil {
		return nil, err
	}

	rewardFactory, err := campaign.GetRewardsFactory()
	if err != nil {
		return nil, sdkerrors.Wrap(sdkerrtypes.ErrInvalidRequest, "failed to retrieve reward factory")
	}

	factData, err := rewardFactory.Calculate(goCtx, ctx,
		types.RewardFactoryKeepers{
			OVMKeeper:        k.ovmKeeper,
			BetKeeper:        k.betKeeper,
			SubAccountKeeper: k.subaccountKeeper,
			RewardKeeper:     k.Keeper,
			AccountKeeper:    k.accountKeeper,
		}, campaign, msg.Ticket, msg.Creator)
	if err != nil {
		return nil, sdkerrors.Wrapf(sdkerrtypes.ErrInvalidRequest, "distribution calculation failed %s", err)
	}

	rewards, err := k.GetRewardsByAddressAndCategory(ctx, factData.Receiver.MainAccountAddr, campaign.RewardCategory)
	if err != nil {
		return nil, sdkerrors.Wrap(sdkerrtypes.ErrInvalidRequest, "failed to retrieve rewards for user.")
	}
	if len(rewards) >= cast.ToInt(campaign.ClaimsPerCategory) {
		return nil, sdkerrors.Wrap(sdkerrtypes.ErrInvalidRequest, "maximum rewards claimed for the given category.")
	}

	if err := campaign.CheckPoolBalance(factData.Receiver.SubAccountAmount.Add(factData.Receiver.MainAccountAmount)); err != nil {
		return nil, types.ErrInsufficientPoolBalance
	}

	unlockTS, err := k.DistributeRewards(ctx, factData.Receiver)
	if err != nil {
		return nil, sdkerrors.Wrapf(types.ErrInDistributionOfRewards, "%s", err)
	}

	k.UpdateCampaignPool(ctx, campaign, factData.Receiver)
	k.SetReward(ctx, types.NewReward(
		msg.Uid, msg.Creator, factData.Receiver.MainAccountAddr,
		msg.CampaignUid, campaign.RewardAmount,
		factData.Common.SourceUID,
		factData.Common.Meta,
	))
	k.SetRewardByReceiver(ctx, types.NewRewardByCategory(msg.Uid, factData.Receiver.MainAccountAddr, campaign.RewardCategory))
	k.SetRewardByCampaign(ctx, types.NewRewardByCampaign(msg.Uid, campaign.UID))

	msg.EmitEvent(&ctx, msg.CampaignUid, msg.Uid, campaign.Promoter, *campaign.RewardAmount, factData.Receiver, unlockTS)

	return &types.MsgGrantRewardResponse{}, nil
}

Last updated