From df2507304d09c7cd54cbba634f549a9745bd55e1 Mon Sep 17 00:00:00 2001 From: Tiance <40375298+diwufeiwen@users.noreply.github.com> Date: Fri, 17 Mar 2023 13:34:00 +0800 Subject: [PATCH] Merge pull request #5832 from filecoin-project/chore/transport-code Chore/transport code --- app/submodule/eth/eth_api.go | 68 +++++++++++++++++------------ app/submodule/eth/eth_test.go | 3 +- cmd/mpool.go | 17 +++++--- cmd/utils.go | 13 ++++++ pkg/messagepool/config.go | 17 +++++--- pkg/messagepool/messagepool.go | 18 ++++---- venus-devtool/api-gen/example.go | 4 ++ venus-devtool/go.mod | 2 +- venus-devtool/go.sum | 4 +- venus-shared/api/chain/v0/method.md | 4 +- venus-shared/api/chain/v1/method.md | 4 +- venus-shared/types/mpool_config.go | 2 +- venus-shared/types/percent.go | 37 ++++++++++++++++ venus-shared/types/percent_test.go | 34 +++++++++++++++ 14 files changed, 170 insertions(+), 57 deletions(-) create mode 100644 venus-shared/types/percent.go create mode 100644 venus-shared/types/percent_test.go diff --git a/app/submodule/eth/eth_api.go b/app/submodule/eth/eth_api.go index c742f48f92..fc00bd72e9 100644 --- a/app/submodule/eth/eth_api.go +++ b/app/submodule/eth/eth_api.go @@ -42,6 +42,8 @@ import ( var log = logging.Logger("eth_api") +var ErrNullRound = errors.New("requested epoch was a null round") + func newEthAPI(em *EthSubModule) (*ethAPI, error) { a := ðAPI{ em: em, @@ -184,7 +186,7 @@ func (a *ethAPI) EthGetBlockByHash(ctx context.Context, blkHash types.EthHash, f return newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, a.em.chainModule.MessageStore, a.chain) } -func (a *ethAPI) parseBlkParam(ctx context.Context, blkParam string) (tipset *types.TipSet, err error) { +func (a *ethAPI) parseBlkParam(ctx context.Context, blkParam string, strict bool) (tipset *types.TipSet, err error) { if blkParam == "earliest" { return nil, fmt.Errorf("block param \"earliest\" is not supported") } @@ -208,16 +210,22 @@ func (a *ethAPI) parseBlkParam(ctx context.Context, blkParam string) (tipset *ty if err != nil { return nil, fmt.Errorf("cannot parse block number: %v", err) } - ts, err := a.em.chainModule.ChainReader.GetTipSetByHeight(ctx, nil, abi.ChainEpoch(num), false) + if abi.ChainEpoch(num) > head.Height()-1 { + return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") + } + ts, err := a.chain.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(num), head.Key()) if err != nil { return nil, fmt.Errorf("cannot get tipset at height: %v", num) } + if strict && ts.Height() != abi.ChainEpoch(num) { + return nil, ErrNullRound + } return ts, nil } } func (a *ethAPI) EthGetBlockByNumber(ctx context.Context, blkParam string, fullTxInfo bool) (types.EthBlock, error) { - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, true) if err != nil { return types.EthBlock{}, err } @@ -322,7 +330,7 @@ func (a *ethAPI) EthGetTransactionCount(ctx context.Context, sender types.EthAdd if err != nil { return types.EthUint64(0), err } - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return types.EthUint64(0), fmt.Errorf("cannot parse block param: %s", blkParam) } @@ -411,7 +419,7 @@ func (a *ethAPI) EthGetCode(ctx context.Context, ethAddr types.EthAddress, blkPa return nil, fmt.Errorf("cannot get Filecoin address: %w", err) } - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return nil, fmt.Errorf("cannot parse block param: %s", blkParam) } @@ -490,7 +498,7 @@ func (a *ethAPI) EthGetCode(ctx context.Context, ethAddr types.EthAddress, blkPa } func (a *ethAPI) EthGetStorageAt(ctx context.Context, ethAddr types.EthAddress, position types.EthBytes, blkParam string) (types.EthBytes, error) { - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return nil, fmt.Errorf("cannot parse block param: %s", blkParam) } @@ -586,7 +594,7 @@ func (a *ethAPI) EthGetBalance(ctx context.Context, address types.EthAddress, bl return types.EthBigInt{}, err } - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return types.EthBigInt{}, fmt.Errorf("cannot parse block param: %s", blkParam) } @@ -633,16 +641,12 @@ func (a *ethAPI) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (types. } } - ts, err := a.parseBlkParam(ctx, params.NewestBlkNum) + ts, err := a.parseBlkParam(ctx, params.NewestBlkNum, false) if err != nil { return types.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err) } - // Deal with the case that the chain is shorter than the number of requested blocks. oldestBlkHeight := uint64(1) - if abi.ChainEpoch(params.BlkCount) <= ts.Height() { - oldestBlkHeight = uint64(ts.Height()) - uint64(params.BlkCount) + 1 - } // NOTE: baseFeePerGas should include the next block after the newest of the returned range, // because the next base fee can be inferred from the messages in the newest block. @@ -652,29 +656,31 @@ func (a *ethAPI) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (types. gasUsedRatioArray := []float64{} rewardsArray := make([][]types.EthBigInt, 0) - for ts.Height() >= abi.ChainEpoch(oldestBlkHeight) { - // Unfortunately we need to rebuild the full message view so we can - // totalize gas used in the tipset. - msgs, err := a.em.chainModule.MessageStore.MessagesForTipset(ts) + blocksIncluded := 0 + for blocksIncluded < int(params.BlkCount) && ts.Height() > 0 { + compOutput, err := a.chain.StateCompute(ctx, ts.Height(), nil, ts.Key()) if err != nil { - return types.EthFeeHistory{}, fmt.Errorf("error loading messages for tipset: %v: %w", ts, err) + return types.EthFeeHistory{}, fmt.Errorf("cannot lookup the status of for tipset: %v: %w", ts, err) } txGasRewards := gasRewardSorter{} - for txIdx, msg := range msgs { - msgLookup, err := a.chain.StateSearchMsg(ctx, types.EmptyTSK, msg.Cid(), constants.LookbackNoLimit, false) - if err != nil || msgLookup == nil { - return types.EthFeeHistory{}, nil + for _, msg := range compOutput.Trace { + if msg.Msg.From == builtintypes.SystemActorAddr { + continue } - tx, err := newEthTxFromMessageLookup(ctx, msgLookup, txIdx, a.em.chainModule.MessageStore, a.chain) + smsgCid, err := getSignedMessage(ctx, a.em.chainModule.MessageStore, msg.MsgCid) if err != nil { - return types.EthFeeHistory{}, nil + return types.EthFeeHistory{}, fmt.Errorf("failed to get signed msg %s: %w", msg.MsgCid, err) + } + tx, err := newEthTxFromSignedMessage(ctx, smsgCid, a.chain) + if err != nil { + return types.EthFeeHistory{}, err } txGasRewards = append(txGasRewards, gasRewardTuple{ reward: tx.Reward(ts.Blocks()[0].ParentBaseFee), - gas: uint64(msgLookup.Receipt.GasUsed), + gas: uint64(msg.MsgRct.GasUsed), }) } @@ -684,6 +690,8 @@ func (a *ethAPI) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (types. baseFeeArray = append(baseFeeArray, types.EthBigInt(ts.Blocks()[0].ParentBaseFee)) gasUsedRatioArray = append(gasUsedRatioArray, float64(totalGasUsed)/float64(constants.BlockGasLimit)) rewardsArray = append(rewardsArray, rewards) + oldestBlkHeight = uint64(ts.Height()) + blocksIncluded++ parentTSKey := ts.Parents() ts, err = a.chain.ChainGetTipSet(ctx, parentTSKey) @@ -1033,7 +1041,7 @@ func (a *ethAPI) EthCall(ctx context.Context, tx types.EthCall, blkParam string) if err != nil { return nil, fmt.Errorf("failed to convert ethcall to filecoin message: %w", err) } - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return nil, fmt.Errorf("cannot parse block param: %s", blkParam) } @@ -1091,12 +1099,16 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return types.EthBlock{}, fmt.Errorf("failed to compute state: %w", err) } - for txIdx, msg := range compOutput.Trace { + txIdx := 0 + for _, msg := range compOutput.Trace { // skip system messages like reward application and cron if msg.Msg.From == builtintypes.SystemActorAddr { continue } + ti := types.EthUint64(txIdx) + txIdx++ + gasUsed += msg.MsgRct.GasUsed smsgCid, err := getSignedMessage(ctx, ms, msg.MsgCid) if err != nil { @@ -1107,8 +1119,6 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return types.EthBlock{}, fmt.Errorf("failed to convert msg to ethTx: %w", err) } - ti := types.EthUint64(txIdx) - tx.ChainID = types.EthUint64(types2.Eip155ChainID) tx.BlockHash = &blkHash tx.BlockNumber = &bn @@ -1643,7 +1653,7 @@ func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRew rewards := make([]types.EthBigInt, len(rewardPercentiles)) for i := range rewards { - rewards[i] = types.EthBigIntZero + rewards[i] = types.EthBigInt(types.NewInt(messagepool.MinGasPremium)) } if len(txGasRewards) == 0 { diff --git a/app/submodule/eth/eth_test.go b/app/submodule/eth/eth_test.go index cc9f78ea83..7b978f0d2b 100644 --- a/app/submodule/eth/eth_test.go +++ b/app/submodule/eth/eth_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/venus/pkg/messagepool" "github.com/filecoin-project/venus/venus-shared/types" ) @@ -133,7 +134,7 @@ func TestRewardPercentiles(t *testing.T) { { percentiles: []float64{25, 50, 75}, txGasRewards: []gasRewardTuple{}, - answer: []int64{0, 0, 0}, + answer: []int64{messagepool.MinGasPremium, messagepool.MinGasPremium, messagepool.MinGasPremium}, }, { percentiles: []float64{25, 50, 75, 100}, diff --git a/cmd/mpool.go b/cmd/mpool.go index f9ebb1a003..5cc0c796d8 100644 --- a/cmd/mpool.go +++ b/cmd/mpool.go @@ -226,6 +226,8 @@ var mpoolReplaceCmd = &cmds.Command{ cmds.StringOption("max-fee", "Spend up to X FIL for this message (applicable for auto mode)"), }, Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + ctx := requestContext(req) + feecap, premium, gasLimit, err := parseGasOptions(req) if err != nil { return err @@ -243,7 +245,7 @@ var mpoolReplaceCmd = &cmds.Command{ return err } - msg, err := env.(*node.Env).ChainAPI.ChainGetMessage(req.Context, mcid) + msg, err := env.(*node.Env).ChainAPI.ChainGetMessage(ctx, mcid) if err != nil { return fmt.Errorf("could not find referenced message: %w", err) } @@ -267,12 +269,12 @@ var mpoolReplaceCmd = &cmds.Command{ return errors.New("command syntax error") } - ts, err := env.(*node.Env).ChainAPI.ChainHead(req.Context) + ts, err := env.(*node.Env).ChainAPI.ChainHead(ctx) if err != nil { return fmt.Errorf("getting chain head: %w", err) } - pending, err := env.(*node.Env).MessagePoolAPI.MpoolPending(req.Context, ts.Key()) + pending, err := env.(*node.Env).MessagePoolAPI.MpoolPending(ctx, ts.Key()) if err != nil { return err } @@ -292,7 +294,12 @@ var mpoolReplaceCmd = &cmds.Command{ msg := found.Message if auto { - minRBF := messagepool.ComputeMinRBF(msg.GasPremium) + cfg, err := getEnv(env).MessagePoolAPI.MpoolGetConfig(ctx) + if err != nil { + return fmt.Errorf("failed to lookup the message pool config: %w", err) + } + + defaultRBF := messagepool.ComputeRBF(msg.GasPremium, cfg.ReplaceByFeeRatio) var mss *types.MessageSendSpec if len(maxFee) > 0 { @@ -313,7 +320,7 @@ var mpoolReplaceCmd = &cmds.Command{ return fmt.Errorf("failed to estimate gas values: %w", err) } - msg.GasPremium = big.Max(retm.GasPremium, minRBF) + msg.GasPremium = big.Max(retm.GasPremium, defaultRBF) msg.GasFeeCap = big.Max(retm.GasFeeCap, msg.GasPremium) mff := func() (abi.TokenAmount, error) { diff --git a/cmd/utils.go b/cmd/utils.go index 8104298d6a..4ee1994070 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -228,3 +228,16 @@ func isController(mi types.MinerInfo, addr address.Address) bool { func getEnv(env cmds.Environment) *node.Env { return env.(*node.Env) } + +func requestContext(req *cmds.Request) context.Context { + ctx, cancel := context.WithCancel(req.Context) + + sig := make(chan os.Signal, 2) + go func() { + <-sig + cancel() + }() + signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) + + return ctx +} diff --git a/pkg/messagepool/config.go b/pkg/messagepool/config.go index a54e313404..69b9adb5ab 100644 --- a/pkg/messagepool/config.go +++ b/pkg/messagepool/config.go @@ -10,10 +10,15 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/venus/pkg/repo" + "github.com/filecoin-project/venus/venus-shared/types" +) + +var ( + ReplaceByFeePercentageMinimum types.Percent = 110 + ReplaceByFeePercentageDefault types.Percent = 125 ) var ( - ReplaceByFeeRatioDefault = 1.25 MemPoolSizeLimitHiDefault = 30000 MemPoolSizeLimitLoDefault = 20000 PruneCooldownDefault = time.Minute @@ -26,7 +31,7 @@ type MpoolConfig struct { PriorityAddrs []address.Address SizeLimitHigh int SizeLimitLow int - ReplaceByFeeRatio float64 + ReplaceByFeeRatio types.Percent PruneCooldown time.Duration GasLimitOverestimation float64 } @@ -71,9 +76,9 @@ func (mp *MessagePool) GetConfig() *MpoolConfig { } func validateConfg(cfg *MpoolConfig) error { - if cfg.ReplaceByFeeRatio < ReplaceByFeeRatioDefault { - return fmt.Errorf("'ReplaceByFeeRatio' is less than required %f < %f", - cfg.ReplaceByFeeRatio, ReplaceByFeeRatioDefault) + if cfg.ReplaceByFeeRatio < ReplaceByFeePercentageMinimum { + return fmt.Errorf("'ReplaceByFeeRatio' is less than required %s < %s", + cfg.ReplaceByFeeRatio, ReplaceByFeePercentageMinimum) } if cfg.GasLimitOverestimation < 1 { return fmt.Errorf("'GasLimitOverestimation' cannot be less than 1") @@ -102,7 +107,7 @@ func DefaultConfig() *MpoolConfig { return &MpoolConfig{ SizeLimitHigh: MemPoolSizeLimitHiDefault, SizeLimitLow: MemPoolSizeLimitLoDefault, - ReplaceByFeeRatio: ReplaceByFeeRatioDefault, + ReplaceByFeeRatio: ReplaceByFeePercentageDefault, PruneCooldown: PruneCooldownDefault, GasLimitOverestimation: GasLimitOverestimation, } diff --git a/pkg/messagepool/messagepool.go b/pkg/messagepool/messagepool.go index bf8500490e..6ae7594c73 100644 --- a/pkg/messagepool/messagepool.go +++ b/pkg/messagepool/messagepool.go @@ -45,12 +45,8 @@ var log = logging.Logger("messagepool") var futureDebug = false -var ( - rbfNumBig = big.NewInt(int64((ReplaceByFeeRatioDefault - 1) * RbfDenom)) - rbfDenomBig = big.NewInt(RbfDenom) -) - -const RbfDenom = 256 +var rbfNumBig = types.NewInt(uint64(ReplaceByFeePercentageMinimum)) +var rbfDenomBig = types.NewInt(100) var RepublishInterval time.Duration @@ -208,8 +204,14 @@ func newMsgSet(nonce uint64) *msgSet { } func ComputeMinRBF(curPrem abi.TokenAmount) abi.TokenAmount { - minPrice := big.Add(curPrem, big.Div(big.Mul(curPrem, rbfNumBig), rbfDenomBig)) - return big.Add(minPrice, big.NewInt(1)) + minPrice := types.BigDiv(types.BigMul(curPrem, rbfNumBig), rbfDenomBig) + return types.BigAdd(minPrice, types.NewInt(1)) +} + +func ComputeRBF(curPrem abi.TokenAmount, replaceByFeeRatio types.Percent) abi.TokenAmount { + rbfNumBig := types.NewInt(uint64(replaceByFeeRatio)) + minPrice := types.BigDiv(types.BigMul(curPrem, rbfNumBig), rbfDenomBig) + return types.BigAdd(minPrice, types.NewInt(1)) } func CapGasFee(mff DefaultMaxFeeFunc, msg *types.Message, sendSepc *types.MessageSendSpec) { diff --git a/venus-devtool/api-gen/example.go b/venus-devtool/api-gen/example.go index 15485829e7..9c119987be 100644 --- a/venus-devtool/api-gen/example.go +++ b/venus-devtool/api-gen/example.go @@ -291,6 +291,10 @@ func init() { FromBlock: pstring("2301220"), Address: []types.EthAddress{ethaddr}, }) + + percent := types.Percent(123) + addExample(percent) + addExample(&percent) } func ExampleValue(method string, t, parent reflect.Type) interface{} { diff --git a/venus-devtool/go.mod b/venus-devtool/go.mod index 4022739098..a5274e00cb 100644 --- a/venus-devtool/go.mod +++ b/venus-devtool/go.mod @@ -9,7 +9,7 @@ require ( github.com/filecoin-project/go-fil-markets v1.25.2 github.com/filecoin-project/go-jsonrpc v0.2.1 github.com/filecoin-project/go-state-types v0.10.0 - github.com/filecoin-project/lotus v1.20.1 + github.com/filecoin-project/lotus v1.20.3 github.com/filecoin-project/venus v0.0.0-00010101000000-000000000000 github.com/google/uuid v1.3.0 github.com/ipfs/go-cid v0.3.2 diff --git a/venus-devtool/go.sum b/venus-devtool/go.sum index 6e02cf6a73..2602cd599f 100644 --- a/venus-devtool/go.sum +++ b/venus-devtool/go.sum @@ -338,8 +338,8 @@ github.com/filecoin-project/go-statemachine v1.0.2 h1:421SSWBk8GIoCoWYYTE/d+qCWc github.com/filecoin-project/go-statestore v0.2.0 h1:cRRO0aPLrxKQCZ2UOQbzFGn4WDNdofHZoGPjfNaAo5Q= github.com/filecoin-project/go-statestore v0.2.0/go.mod h1:8sjBYbS35HwPzct7iT4lIXjLlYyPor80aU7t7a/Kspo= github.com/filecoin-project/index-provider v0.9.1 h1:Jnh9dviIHvQxZ2baNoYu3n8z6F9O62ksnVlyREgPyyM= -github.com/filecoin-project/lotus v1.20.1 h1:thlefi6NROiJo58dRBf+RweNmTlxCFdEIysLjx83tMw= -github.com/filecoin-project/lotus v1.20.1/go.mod h1:dprpVaiQezI8Jl4tWcPNIYGGAAo31feZlGAOk8D7bJU= +github.com/filecoin-project/lotus v1.20.3 h1:7Szh7jCc8pOa1++tdZgaUGMJYSHIMkUpypAO4at9I2U= +github.com/filecoin-project/lotus v1.20.3/go.mod h1:eNjjbZvjLgH7OEaD7kAkk5i8OrZ7owq349yfQ1wrVTo= github.com/filecoin-project/pubsub v1.0.0 h1:ZTmT27U07e54qV1mMiQo4HDr0buo8I1LDHBYLXlsNXM= github.com/filecoin-project/pubsub v1.0.0/go.mod h1:GkpB33CcUtUNrLPhJgfdy4FDx4OMNR9k+46DHx/Lqrg= github.com/filecoin-project/specs-actors v0.9.13/go.mod h1:TS1AW/7LbG+615j4NsjMK1qlpAwaFsG9w0V2tg2gSao= diff --git a/venus-shared/api/chain/v0/method.md b/venus-shared/api/chain/v0/method.md index c95bff29eb..936b9fdc1d 100644 --- a/venus-shared/api/chain/v0/method.md +++ b/venus-shared/api/chain/v0/method.md @@ -2592,7 +2592,7 @@ Response: ], "SizeLimitHigh": 123, "SizeLimitLow": 123, - "ReplaceByFeeRatio": 12.3, + "ReplaceByFeeRatio": 1.23, "PruneCooldown": 60000000000, "GasLimitOverestimation": 12.3 } @@ -2973,7 +2973,7 @@ Inputs: ], "SizeLimitHigh": 123, "SizeLimitLow": 123, - "ReplaceByFeeRatio": 12.3, + "ReplaceByFeeRatio": 1.23, "PruneCooldown": 60000000000, "GasLimitOverestimation": 12.3 } diff --git a/venus-shared/api/chain/v1/method.md b/venus-shared/api/chain/v1/method.md index 9b2ed40944..d5ef4f61a0 100644 --- a/venus-shared/api/chain/v1/method.md +++ b/venus-shared/api/chain/v1/method.md @@ -3540,7 +3540,7 @@ Response: ], "SizeLimitHigh": 123, "SizeLimitLow": 123, - "ReplaceByFeeRatio": 12.3, + "ReplaceByFeeRatio": 1.23, "PruneCooldown": 60000000000, "GasLimitOverestimation": 12.3 } @@ -3921,7 +3921,7 @@ Inputs: ], "SizeLimitHigh": 123, "SizeLimitLow": 123, - "ReplaceByFeeRatio": 12.3, + "ReplaceByFeeRatio": 1.23, "PruneCooldown": 60000000000, "GasLimitOverestimation": 12.3 } diff --git a/venus-shared/types/mpool_config.go b/venus-shared/types/mpool_config.go index b48112b847..656866253b 100644 --- a/venus-shared/types/mpool_config.go +++ b/venus-shared/types/mpool_config.go @@ -10,7 +10,7 @@ type MpoolConfig struct { PriorityAddrs []address.Address SizeLimitHigh int SizeLimitLow int - ReplaceByFeeRatio float64 + ReplaceByFeeRatio Percent PruneCooldown time.Duration GasLimitOverestimation float64 } diff --git a/venus-shared/types/percent.go b/venus-shared/types/percent.go new file mode 100644 index 0000000000..62f652bc92 --- /dev/null +++ b/venus-shared/types/percent.go @@ -0,0 +1,37 @@ +package types + +import ( + "fmt" + "math" + "strconv" +) + +// Percent stores a signed percentage as an int64. When converted to a string (or json), it's stored +// as a decimal with two places (e.g., 100% -> 1.00). +type Percent int64 + +func (p Percent) String() string { + abs := p + sign := "" + if abs < 0 { + abs = -abs + sign = "-" + } + return fmt.Sprintf(`%s%d.%d`, sign, abs/100, abs%100) +} + +func (p Percent) MarshalJSON() ([]byte, error) { + return []byte(p.String()), nil +} + +func (p *Percent) UnmarshalJSON(b []byte) error { + flt, err := strconv.ParseFloat(string(b)+"e2", 64) + if err != nil { + return fmt.Errorf("unable to parse ratio %s: %w", string(b), err) + } + if math.Trunc(flt) != flt { + return fmt.Errorf("ratio may only have two decimals: %s", string(b)) + } + *p = Percent(flt) + return nil +} diff --git a/venus-shared/types/percent_test.go b/venus-shared/types/percent_test.go new file mode 100644 index 0000000000..7364c24479 --- /dev/null +++ b/venus-shared/types/percent_test.go @@ -0,0 +1,34 @@ +package types + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPercent(t *testing.T) { + for _, tc := range []struct { + p Percent + s string + }{ + {100, "1.0"}, + {111, "1.11"}, + {12, "0.12"}, + {-12, "-0.12"}, + {1012, "10.12"}, + {-1012, "-10.12"}, + {0, "0.0"}, + } { + tc := tc + t.Run(fmt.Sprintf("%d <> %s", tc.p, tc.s), func(t *testing.T) { + m, err := tc.p.MarshalJSON() + require.NoError(t, err) + require.Equal(t, tc.s, string(m)) + var p Percent + require.NoError(t, p.UnmarshalJSON([]byte(tc.s))) + require.Equal(t, tc.p, p) + }) + } + +}