Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: calculator prototype #288

Merged
merged 4 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ gen:
go run ./internal/generator/main.go \
"./internal/engine/command/crowdfund/crowdfund.yml" \
"./internal/engine/command/voucher/voucher.yml" \
"./internal/engine/command/market/market.yml"
"./internal/engine/command/market/market.yml" \
"./internal/engine/command/calculator/calculator.yml"

find . -name "*.gen.go" -exec gofumpt -l -w {} +

Expand Down
73 changes: 73 additions & 0 deletions internal/engine/command/calculator/calculator.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 10 additions & 43 deletions internal/engine/command/calculator/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
)

type CalculatorCmd struct {
*calculatorSubCmds

clientMgr client.IManager
}

Expand All @@ -17,50 +19,15 @@ func NewCalculatorCmd(clientMgr client.IManager) *CalculatorCmd {
}

func (c *CalculatorCmd) GetCommand() *command.Command {
subCmdCalcReward := &command.Command{
Name: "reward",
Help: "Calculate the PAC coins you can earn based on your validator stake",
Args: []*command.Args{
{
Name: "stake",
Desc: "The amount of stake in your validator",
InputBox: command.InputBoxInteger,
Optional: false,
},
{
Name: "days",
Desc: "The number of days to calculate rewards for (range: 1-365)",
InputBox: command.InputBoxInteger,
Optional: false,
},
},
SubCommands: nil,
AppIDs: entity.AllAppIDs(),
Handler: c.calcRewardHandler,
TargetFlag: command.TargetMaskMainnet,
}
cmd := c.buildCalculatorCommand()
cmd.AppIDs = entity.AllAppIDs()
cmd.TargetFlag = command.TargetMaskMainnet

subCmdCalcFee := &command.Command{
Name: "fee",
Help: "Return the estimated transaction fee on the network",
SubCommands: nil,
AppIDs: entity.AllAppIDs(),
Handler: c.calcFeeHandler,
TargetFlag: command.TargetMaskMainnet,
}

cmdBlockchain := &command.Command{
Name: "calculate",
Help: "Perform calculations such as reward and fee estimations",
Args: nil,
AppIDs: entity.AllAppIDs(),
SubCommands: make([]*command.Command, 0),
Handler: nil,
TargetFlag: command.TargetMaskMainnet,
}
c.subCmdReward.AppIDs = entity.AllAppIDs()
c.subCmdReward.TargetFlag = command.TargetMaskMainnet

cmdBlockchain.AddSubCommand(subCmdCalcReward)
cmdBlockchain.AddSubCommand(subCmdCalcFee)
c.subCmdFee.AppIDs = entity.AllAppIDs()
c.subCmdFee.TargetFlag = command.TargetMaskMainnet

return cmdBlockchain
return cmd
}
29 changes: 29 additions & 0 deletions internal/engine/command/calculator/calculator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
name: calculator
emoji: 🧮
help: Perform calculations such as reward and fee estimations
sub_commands:
- name: reward
help: Calculate the PAC coins you can earn based on your validator stake
result_template: |
Approximately you earn {{.reward}} PAC reward, with {{.stake}} stake 🔒 on your validator in {{.days}} days ⏰ with {{.totalPower}} total power ⚡ of committee.

> Note📝: This number is just an estimation. It will vary depending on your stake amount and total network power.
args:
- name: stake
desc: The amount of stake in your validator
input_box: Integer
optional: false
- name: days
desc: "The number of days to calculate rewards for (range : 1-365)"
input_box: Integer
optional: false
- name: fee
help: Return the estimated transaction fee on the network
result_template: |
Sending {{.amount}} will cost {{.fee}} with current fee percentage.
args:
- name: amount
desc: The amount of PAC coins to calculate fee for
input_box: Integer
optional: false
34 changes: 34 additions & 0 deletions internal/engine/command/calculator/calculator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package calculator

import (
"testing"

"github.com/pagu-project/pagu/internal/testsuite"
"github.com/pagu-project/pagu/pkg/client"
"go.uber.org/mock/gomock"
)

type testData struct {
*testsuite.TestSuite

calculatorCmd *CalculatorCmd
mockClientMgr *client.MockIManager
}

func setup(t *testing.T) *testData {
t.Helper()

ts := testsuite.NewTestSuite(t)
ctrl := gomock.NewController(t)

mockClientMgr := client.NewMockIManager(ctrl)

calculatorCmd := NewCalculatorCmd(mockClientMgr)
calculatorCmd.buildCalculatorCommand()

return &testData{
TestSuite: ts,
calculatorCmd: calculatorCmd,
mockClientMgr: mockClientMgr,
}
}
11 changes: 4 additions & 7 deletions internal/engine/command/calculator/fee.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
package calculator

import (
"errors"

"github.com/pagu-project/pagu/internal/engine/command"
"github.com/pagu-project/pagu/internal/entity"
"github.com/pagu-project/pagu/pkg/amount"
)

func (c *CalculatorCmd) calcFeeHandler(
func (c *CalculatorCmd) feeHandler(
_ *entity.User,
cmd *command.Command,
args map[string]string,
) command.CommandResult {
amt, err := amount.FromString(args["amount"])
if err != nil {
return cmd.ErrorResult(errors.New("invalid amount param"))
return cmd.RenderFailedTemplate("Invalid amount param")
}

fee, err := c.clientMgr.GetFee(amt.ToNanoPAC())
if err != nil {
return cmd.ErrorResult(err)
return cmd.RenderErrorTemplate(err)
}

feeAmount := amount.Amount(fee)

return cmd.SuccessfulResultF("Sending %s will cost %s with current fee percentage."+
"\n> Note: Consider unbond and sortition transaction fee is 0 PAC always.", amt.String(), feeAmount.String())
return cmd.RenderResultTemplate("amount", amt, "fee", feeAmount)
}
56 changes: 56 additions & 0 deletions internal/engine/command/calculator/fee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package calculator

import (
"errors"
"fmt"
"testing"

"github.com/pagu-project/pagu/pkg/amount"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)

func TestFeeHandler(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

td := setup(t)
cmd := td.calculatorCmd.subCmdFee

t.Run("Invalid Amount Param", func(t *testing.T) {
args := map[string]string{
"amount": "invalid",
}

result := td.calculatorCmd.feeHandler(nil, cmd, args)
assert.False(t, result.Successful)
assert.Contains(t, result.Message, "Invalid amount param")
})

t.Run("Error from GetFee", func(t *testing.T) {
args := map[string]string{
"amount": "10",
}

amt, _ := amount.FromString("10")
td.mockClientMgr.EXPECT().GetFee(amt.ToNanoPAC()).Return(int64(0), errors.New("some error"))

result := td.calculatorCmd.feeHandler(nil, cmd, args)
assert.False(t, result.Successful)
assert.Contains(t, result.Message, "some error")
})

t.Run("Successful Fee Calculation", func(t *testing.T) {
args := map[string]string{
"amount": "10",
}

amt, _ := amount.FromString("10")
td.mockClientMgr.EXPECT().GetFee(amt.ToNanoPAC()).Return(int64(100), nil)

result := td.calculatorCmd.feeHandler(nil, cmd, args)
assert.True(t, result.Successful)
assert.Contains(t, result.Message,
fmt.Sprintf("Sending %v will cost %v with current fee percentage", amt, amount.Amount(int64(100))))
})
}
25 changes: 12 additions & 13 deletions internal/engine/command/calculator/reward.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,43 @@ import (
"github.com/pagu-project/pagu/pkg/utils"
)

func (c *CalculatorCmd) calcRewardHandler(
func (c *CalculatorCmd) rewardHandler(
_ *entity.User,
cmd *command.Command,
args map[string]string,
) command.CommandResult {
stake, err := amount.FromString(args["stake"])
if err != nil {
return cmd.ErrorResult(errors.New("invalid stake param"))
return cmd.RenderFailedTemplate("Invalid stake param")
}

minStake, _ := amount.NewAmount(1)
maxStake, _ := amount.NewAmount(1000)
if stake < minStake || stake > maxStake {
return cmd.ErrorResult(
fmt.Errorf("%v is invalid amount; minimum stake amount is 1 PAC and maximum is 1,000 PAC", stake))
return cmd.RenderErrorTemplate(
fmt.Errorf("%v is invalid amount, minimum stake amount is 1 PAC and maximum is 1,000 PAC", stake))
}

numOfDays, err := strconv.Atoi(args["days"])
if err != nil {
return cmd.ErrorResult(errors.New("invalid days param"))
return cmd.RenderErrorTemplate(errors.New("invalid days param"))
}

if numOfDays < 1 || numOfDays > 365 {
return cmd.ErrorResult(fmt.Errorf("%v is invalid time; minimum time value is 1 and maximum is 365", numOfDays))
return cmd.RenderErrorTemplate(
fmt.Errorf("%v is invalid time, minimum time value is 1 and maximum is 365", numOfDays))
}

blocks := numOfDays * 8640
info, err := c.clientMgr.GetBlockchainInfo()
if err != nil {
return cmd.ErrorResult(err)
return cmd.RenderErrorTemplate(err)
}

reward := (stake.ToNanoPAC() * int64(blocks)) / info.TotalPower

return cmd.SuccessfulResultF("Approximately you earn %v PAC reward, with %v stake 🔒 on your validator "+
"in %d days ⏰ with %s total power ⚡ of committee."+
"\n\n> Note📝: This number is just an estimation. "+
"It will vary depending on your stake amount and total network power.",
utils.FormatNumber(reward), stake, numOfDays,
utils.FormatNumber(int64(amount.Amount(info.TotalPower).ToPAC())))
return cmd.RenderResultTemplate("stake", stake,
"days", numOfDays,
"totalPower", utils.FormatNumber(int64(amount.Amount(info.TotalPower).ToPAC())),
"reward", utils.FormatNumber(reward))
}
Loading
Loading