|
|
|
package services
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math/big"
|
|
|
|
|
|
|
|
common2 "github.com/harmony-one/harmony/internal/common"
|
|
|
|
|
|
|
|
"github.com/coinbase/rosetta-sdk-go/types"
|
|
|
|
|
|
|
|
"github.com/harmony-one/harmony/rosetta/common"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// maxNumOfConstructionOps ..
|
|
|
|
maxNumOfConstructionOps = 2
|
|
|
|
|
|
|
|
// transferOperationCount ..
|
|
|
|
transferOperationCount = 2
|
|
|
|
)
|
|
|
|
|
|
|
|
// OperationComponents are components from a set of operations to construct a valid transaction
|
|
|
|
type OperationComponents struct {
|
|
|
|
Type string `json:"type"`
|
|
|
|
From *types.AccountIdentifier `json:"from"`
|
|
|
|
To *types.AccountIdentifier `json:"to"`
|
|
|
|
Amount *big.Int `json:"amount"`
|
|
|
|
StakingMessage interface{} `json:"staking_message,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsStaking ..
|
|
|
|
func (s *OperationComponents) IsStaking() bool {
|
|
|
|
return s.StakingMessage != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetOperationComponents ensures the provided operations creates a valid transaction and returns
|
|
|
|
// the OperationComponents of the resulting transaction.
|
|
|
|
//
|
|
|
|
// Providing a gas expenditure operation is INVALID.
|
|
|
|
// All staking & cross-shard operations require metadata matching the operation type to be a valid.
|
|
|
|
// All other operations do not require metadata.
|
|
|
|
func GetOperationComponents(
|
|
|
|
operations []*types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if operations == nil {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "nil operations",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if len(operations) > maxNumOfConstructionOps || len(operations) == 0 {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": fmt.Sprintf("invalid number of operations, must <= %v & > 0", maxNumOfConstructionOps),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(operations) == transferOperationCount {
|
|
|
|
return getTransferOperationComponents(operations)
|
|
|
|
}
|
|
|
|
switch operations[0].Type {
|
|
|
|
case common.NativeCrossShardTransferOperation:
|
|
|
|
return getCrossShardOperationComponents(operations[0])
|
|
|
|
case common.ContractCreationOperation:
|
|
|
|
return getContractCreationOperationComponents(operations[0])
|
|
|
|
case common.CreateValidatorOperation:
|
|
|
|
return getCreateValidatorOperationComponents(operations[0])
|
|
|
|
case common.EditValidatorOperation:
|
|
|
|
return getEditValidatorOperationComponents(operations[0])
|
|
|
|
case common.DelegateOperation:
|
|
|
|
return getDelegateOperationComponents(operations[0])
|
|
|
|
case common.UndelegateOperation:
|
|
|
|
return getUndelegateOperationComponents(operations[0])
|
|
|
|
case common.CollectRewardsOperation:
|
|
|
|
return getCollectRewardsOperationComponents(operations[0])
|
|
|
|
default:
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": fmt.Sprintf("%v is unsupported or invalid operation type", operations[0].Type),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// getTransferOperationComponents ..
|
|
|
|
func getTransferOperationComponents(
|
|
|
|
operations []*types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if len(operations) != transferOperationCount {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "require exactly 2 operations",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
op0, op1 := operations[0], operations[1]
|
|
|
|
if op0.Type != common.NativeTransferOperation || op1.Type != common.NativeTransferOperation {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "invalid operation type(s) for same shard transfer",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
val0, err := types.AmountValue(op0.Amount)
|
|
|
|
if err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
val1, err := types.AmountValue(op1.Amount)
|
|
|
|
if err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if new(big.Int).Add(val0, val1).Cmp(big.NewInt(0)) != 0 {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "amount taken from sender is not exactly paid out to receiver for same shard transfer",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if types.Hash(op0.Amount.Currency) != common.NativeCurrencyHash ||
|
|
|
|
types.Hash(op1.Amount.Currency) != common.NativeCurrencyHash {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "invalid currency for provided amounts",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(op1.RelatedOperations) == 1 &&
|
|
|
|
op1.RelatedOperations[0].Index != op0.OperationIdentifier.Index {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "second operation is not related to the first operation for same shard transfer",
|
|
|
|
})
|
|
|
|
} else if len(op0.RelatedOperations) == 1 &&
|
|
|
|
op0.RelatedOperations[0].Index != op1.OperationIdentifier.Index {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "first operation is not related to the second operation for same shard transfer",
|
|
|
|
})
|
|
|
|
} else if len(op0.RelatedOperations) > 1 || len(op1.RelatedOperations) > 1 ||
|
|
|
|
len(op0.RelatedOperations)^len(op1.RelatedOperations) != 1 {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operations must only relate to one another in one direction for same shard transfers",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
components := &OperationComponents{
|
|
|
|
Type: op0.Type,
|
|
|
|
Amount: new(big.Int).Abs(val0),
|
|
|
|
}
|
|
|
|
if val0.Sign() != 1 {
|
|
|
|
components.From = op0.Account
|
|
|
|
components.To = op1.Account
|
|
|
|
} else {
|
|
|
|
components.From = op1.Account
|
|
|
|
components.To = op0.Account
|
|
|
|
}
|
|
|
|
if components.From == nil || components.To == nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "both operations must have account identifiers for same shard transfer",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return components, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getCrossShardOperationComponents ..
|
|
|
|
func getCrossShardOperationComponents(
|
|
|
|
operation *types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if operation == nil {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "nil operation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
metadata := common.CrossShardTransactionOperationMetadata{}
|
|
|
|
if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": errors.WithMessage(err, "invalid metadata").Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
amount, err := types.AmountValue(operation.Amount)
|
|
|
|
if err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if amount.Sign() == 1 {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "sender amount must not be positive for cross shard transfer",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if types.Hash(operation.Amount.Currency) != common.NativeCurrencyHash {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "invalid currency for provided amounts",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
components := &OperationComponents{
|
|
|
|
Type: operation.Type,
|
|
|
|
To: metadata.To,
|
|
|
|
From: metadata.From,
|
|
|
|
Amount: new(big.Int).Abs(amount),
|
|
|
|
}
|
|
|
|
if components.From == nil || components.To == nil || operation.Account == nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operation must have account sender/from & receiver/to identifiers for cross shard transfer",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if operation.Account.Address != components.From.Address {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operation account identifier does not match sender/from identifiers for cross shard transfer",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return components, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getContractCreationOperationComponents ..
|
|
|
|
func getContractCreationOperationComponents(
|
|
|
|
operation *types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if operation == nil {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "nil operation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
amount, err := types.AmountValue(operation.Amount)
|
|
|
|
if err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if amount.Sign() == 1 {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "sender amount must not be positive for contract creation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if types.Hash(operation.Amount.Currency) != common.NativeCurrencyHash {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "invalid currency for provided amounts",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
components := &OperationComponents{
|
|
|
|
Type: operation.Type,
|
|
|
|
From: operation.Account,
|
|
|
|
Amount: new(big.Int).Abs(amount),
|
|
|
|
}
|
|
|
|
if components.From == nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operation must have account sender/from identifier for contract creation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return components, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCreateValidatorOperationComponents(
|
|
|
|
operation *types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if operation == nil {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "nil operation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
metadata := common.CreateValidatorOperationMetadata{}
|
|
|
|
if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": errors.WithMessage(err, "invalid metadata").Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if metadata.ValidatorAddress == "" || !common2.IsBech32Address(metadata.ValidatorAddress) {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": "validator address must not be empty or wrong format",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if metadata.CommissionRate == nil || metadata.MaxCommissionRate == nil || metadata.MaxChangeRate == nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": "commission rate & max commission rate & max change rate must not be nil",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if metadata.MinSelfDelegation == nil || metadata.MaxTotalDelegation == nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": "min self delegation & max total delegation much not be nil",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if metadata.Amount == nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": "amount must not be nil",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if metadata.Name == "" || metadata.Website == "" || metadata.Identity == "" ||
|
|
|
|
metadata.SecurityContact == "" || metadata.Details == "" {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": "name & website & identity & security contract & details must no be empty",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// slot public key would be add into
|
|
|
|
// https://github.com/harmony-one/harmony/blob/3a8125666817149eaf9cea7870735e26cfe49c87/rosetta/services/tx_construction.go#L16
|
|
|
|
// see https://github.com/harmony-one/harmony/issues/3431
|
|
|
|
|
|
|
|
components := &OperationComponents{
|
|
|
|
Type: operation.Type,
|
|
|
|
From: operation.Account,
|
|
|
|
StakingMessage: metadata,
|
|
|
|
}
|
|
|
|
|
|
|
|
if components.From == nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operation must have account sender/from identifier for creating validator",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return components, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func getEditValidatorOperationComponents(
|
|
|
|
operation *types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if operation == nil {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "nil operation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
metadata := common.EditValidatorOperationMetadata{}
|
|
|
|
if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": errors.WithMessage(err, "invalid metadata").Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if metadata.ValidatorAddress == "" || !common2.IsBech32Address(metadata.ValidatorAddress) {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": "validator address must not be empty or wrong format",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if metadata.CommissionRate == nil || metadata.MinSelfDelegation == nil || metadata.MaxTotalDelegation == nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": "commission rate & max commission rate & max change rate must not be nil",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if metadata.Name == "" || metadata.Website == "" || metadata.Identity == "" ||
|
|
|
|
metadata.SecurityContact == "" || metadata.Details == "" {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": "name & website & identity & security contract & details must no be empty",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
components := &OperationComponents{
|
|
|
|
Type: operation.Type,
|
|
|
|
From: operation.Account,
|
|
|
|
StakingMessage: metadata,
|
|
|
|
}
|
|
|
|
|
|
|
|
if components.From == nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operation must have account sender/from identifier for editing validator",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return components, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func getDelegateOperationComponents(
|
|
|
|
operation *types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if operation == nil {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "nil operation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
metadata := common.DelegateOperationMetadata{}
|
|
|
|
if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": errors.WithMessage(err, "invalid metadata").Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// validator and delegator and amount already got checked inside UnmarshalFromInterface
|
|
|
|
components := &OperationComponents{
|
|
|
|
Type: operation.Type,
|
|
|
|
From: operation.Account,
|
|
|
|
StakingMessage: metadata,
|
|
|
|
}
|
|
|
|
|
|
|
|
if components.From == nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operation must have account sender/from identifier for delegating",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return components, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func getUndelegateOperationComponents(
|
|
|
|
operation *types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if operation == nil {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "nil operation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
metadata := common.UndelegateOperationMetadata{}
|
|
|
|
if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": errors.WithMessage(err, "invalid metadata").Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// validator and delegator and amount already got checked inside UnmarshalFromInterface
|
|
|
|
components := &OperationComponents{
|
|
|
|
Type: operation.Type,
|
|
|
|
From: operation.Account,
|
|
|
|
StakingMessage: metadata,
|
|
|
|
}
|
|
|
|
|
|
|
|
if components.From == nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operation must have account sender/from identifier for undelegating",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return components, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCollectRewardsOperationComponents(
|
|
|
|
operation *types.Operation,
|
|
|
|
) (*OperationComponents, *types.Error) {
|
|
|
|
if operation == nil {
|
|
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
|
|
"message": "nil operation",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
metadata := common.CollectRewardsMetadata{}
|
|
|
|
if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil {
|
|
|
|
return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{
|
|
|
|
"message": errors.WithMessage(err, "invalid metadata").Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
//delegator already got checked inside UnmarshalFromInterface
|
|
|
|
|
|
|
|
components := &OperationComponents{
|
|
|
|
Type: operation.Type,
|
|
|
|
From: operation.Account,
|
|
|
|
StakingMessage: metadata,
|
|
|
|
}
|
|
|
|
|
|
|
|
if components.From == nil {
|
|
|
|
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
|
|
|
|
"message": "operation must have account sender/from identifier for collecting rewards",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return components, nil
|
|
|
|
}
|