2020-10-22 14:19:07 +00:00
|
|
|
package netmap
|
|
|
|
|
|
|
|
import (
|
2022-05-06 08:06:25 +00:00
|
|
|
"errors"
|
2021-05-18 08:12:51 +00:00
|
|
|
"fmt"
|
|
|
|
|
2023-03-07 13:38:26 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
2022-01-31 11:58:55 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
|
2020-10-22 14:19:07 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
|
|
)
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
const (
|
2023-04-11 11:59:24 +00:00
|
|
|
MaxObjectSizeConfig = "MaxObjectSize"
|
|
|
|
BasicIncomeRateConfig = "BasicIncomeRate"
|
|
|
|
AuditFeeConfig = "AuditFee"
|
|
|
|
EpochDurationConfig = "EpochDuration"
|
|
|
|
ContainerFeeConfig = "ContainerFee"
|
|
|
|
ContainerAliasFeeConfig = "ContainerAliasFee"
|
|
|
|
IrCandidateFeeConfig = "InnerRingCandidateFee"
|
|
|
|
WithdrawFeeConfig = "WithdrawFee"
|
|
|
|
HomomorphicHashingDisabledKey = "HomomorphicHashingDisabled"
|
|
|
|
MaintenanceModeAllowedConfig = "MaintenanceModeAllowed"
|
2022-01-31 11:58:55 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// MaxObjectSize receives max object size configuration
|
|
|
|
// value through the Netmap contract call.
|
|
|
|
func (c *Client) MaxObjectSize() (uint64, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
objectSize, err := c.readUInt64Config(MaxObjectSizeConfig)
|
2022-01-31 11:58:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("(%T) could not get epoch number: %w", c, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return objectSize, nil
|
2020-10-22 14:19:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// BasicIncomeRate returns basic income rate configuration value from network
|
|
|
|
// config in netmap contract.
|
|
|
|
func (c *Client) BasicIncomeRate() (uint64, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
rate, err := c.readUInt64Config(BasicIncomeRateConfig)
|
2022-01-31 11:58:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("(%T) could not get basic income rate: %w", c, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rate, nil
|
2021-11-10 10:44:19 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// AuditFee returns audit fee configuration value from network
|
|
|
|
// config in netmap contract.
|
|
|
|
func (c *Client) AuditFee() (uint64, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
fee, err := c.readUInt64Config(AuditFeeConfig)
|
2022-01-31 11:58:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("(%T) could not get audit fee: %w", c, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fee, nil
|
2020-10-22 14:19:07 +00:00
|
|
|
}
|
|
|
|
|
2023-02-05 15:59:38 +00:00
|
|
|
// EpochDuration returns number of sidechain blocks per one FrostFS epoch.
|
2022-01-31 11:58:55 +00:00
|
|
|
func (c *Client) EpochDuration() (uint64, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
epochDuration, err := c.readUInt64Config(EpochDurationConfig)
|
2022-01-31 11:58:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("(%T) could not get epoch duration: %w", c, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return epochDuration, nil
|
2020-10-22 14:19:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// ContainerFee returns fee paid by container owner to each alphabet node
|
|
|
|
// for container registration.
|
|
|
|
func (c *Client) ContainerFee() (uint64, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
fee, err := c.readUInt64Config(ContainerFeeConfig)
|
2022-01-31 11:58:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("(%T) could not get container fee: %w", c, err)
|
|
|
|
}
|
2021-11-09 20:52:29 +00:00
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
return fee, nil
|
|
|
|
}
|
2021-11-09 20:52:29 +00:00
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// ContainerAliasFee returns additional fee paid by container owner to each
|
|
|
|
// alphabet node for container nice name registration.
|
|
|
|
func (c *Client) ContainerAliasFee() (uint64, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
fee, err := c.readUInt64Config(ContainerAliasFeeConfig)
|
2020-10-22 14:19:07 +00:00
|
|
|
if err != nil {
|
2022-01-31 11:58:55 +00:00
|
|
|
return 0, fmt.Errorf("(%T) could not get container alias fee: %w", c, err)
|
2020-10-22 14:19:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
return fee, nil
|
|
|
|
}
|
|
|
|
|
2022-04-29 17:47:14 +00:00
|
|
|
// HomomorphicHashDisabled returns global configuration value of homomorphic hashing
|
|
|
|
// settings.
|
2022-05-06 08:06:25 +00:00
|
|
|
//
|
|
|
|
// Returns (false, nil) if config key is not found in the contract.
|
2022-04-29 17:47:14 +00:00
|
|
|
func (c *Client) HomomorphicHashDisabled() (bool, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
return c.readBoolConfig(HomomorphicHashingDisabledKey)
|
2022-04-29 17:47:14 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// InnerRingCandidateFee returns global configuration value of fee paid by
|
|
|
|
// node to be in inner ring candidates list.
|
|
|
|
func (c *Client) InnerRingCandidateFee() (uint64, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
fee, err := c.readUInt64Config(IrCandidateFeeConfig)
|
2022-01-31 11:58:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("(%T) could not get inner ring candidate fee: %w", c, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fee, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithdrawFee returns global configuration value of fee paid by user to
|
2023-02-05 15:59:38 +00:00
|
|
|
// withdraw assets from FrostFS contract.
|
2022-01-31 11:58:55 +00:00
|
|
|
func (c *Client) WithdrawFee() (uint64, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
fee, err := c.readUInt64Config(WithdrawFeeConfig)
|
2022-01-31 11:58:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("(%T) could not get withdraw fee: %w", c, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fee, nil
|
|
|
|
}
|
|
|
|
|
2022-09-19 15:39:03 +00:00
|
|
|
// MaintenanceModeAllowed reads admission of "maintenance" state from the
|
2023-02-05 15:59:38 +00:00
|
|
|
// FrostFS network configuration stored in the Sidechain. The admission means
|
2022-09-19 15:39:03 +00:00
|
|
|
// that storage nodes are allowed to switch their state to "maintenance".
|
|
|
|
//
|
|
|
|
// By default, maintenance state is disallowed.
|
|
|
|
func (c *Client) MaintenanceModeAllowed() (bool, error) {
|
2023-04-11 11:59:24 +00:00
|
|
|
return c.readBoolConfig(MaintenanceModeAllowedConfig)
|
2022-09-19 15:39:03 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
func (c *Client) readUInt64Config(key string) (uint64, error) {
|
|
|
|
v, err := c.config([]byte(key), IntegerAssert)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
2020-10-22 14:19:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// IntegerAssert is guaranteed to return int64 if the error is nil.
|
|
|
|
return uint64(v.(int64)), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) readStringConfig(key string) (string, error) {
|
|
|
|
v, err := c.config([]byte(key), StringAssert)
|
2020-10-22 14:19:07 +00:00
|
|
|
if err != nil {
|
2022-01-31 11:58:55 +00:00
|
|
|
return "", err
|
2020-10-22 14:19:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// StringAssert is guaranteed to return string if the error is nil.
|
|
|
|
return v.(string), nil
|
2020-10-22 14:19:07 +00:00
|
|
|
}
|
|
|
|
|
2023-02-05 15:59:38 +00:00
|
|
|
// reads boolean value by the given key from the FrostFS network configuration
|
2022-09-19 15:03:54 +00:00
|
|
|
// stored in the Sidechain. Returns false if key is not presented.
|
2022-04-29 17:47:14 +00:00
|
|
|
func (c *Client) readBoolConfig(key string) (bool, error) {
|
|
|
|
v, err := c.config([]byte(key), BoolAssert)
|
|
|
|
if err != nil {
|
2022-09-19 15:03:54 +00:00
|
|
|
if errors.Is(err, ErrConfigNotFound) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return false, fmt.Errorf("read boolean configuration value %s from the Sidechain: %w", key, err)
|
2022-04-29 17:47:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// BoolAssert is guaranteed to return bool if the error is nil.
|
|
|
|
return v.(bool), nil
|
|
|
|
}
|
|
|
|
|
2021-11-10 10:44:19 +00:00
|
|
|
// SetConfigPrm groups parameters of SetConfig operation.
|
|
|
|
type SetConfigPrm struct {
|
|
|
|
id []byte
|
|
|
|
key []byte
|
2023-02-21 11:42:45 +00:00
|
|
|
value any
|
2021-11-10 10:44:19 +00:00
|
|
|
|
|
|
|
client.InvokePrmOptional
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetID sets ID of the config value.
|
|
|
|
func (s *SetConfigPrm) SetID(id []byte) {
|
|
|
|
s.id = id
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetKey sets key of the config value.
|
|
|
|
func (s *SetConfigPrm) SetKey(key []byte) {
|
|
|
|
s.key = key
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetValue sets value of the config value.
|
2023-02-21 11:42:45 +00:00
|
|
|
func (s *SetConfigPrm) SetValue(value any) {
|
2021-11-10 10:44:19 +00:00
|
|
|
s.value = value
|
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// SetConfig sets config field.
|
|
|
|
func (c *Client) SetConfig(p SetConfigPrm) error {
|
2021-11-09 20:52:29 +00:00
|
|
|
prm := client.InvokePrm{}
|
2022-01-29 13:06:36 +00:00
|
|
|
prm.SetMethod(setConfigMethod)
|
2022-01-31 11:58:55 +00:00
|
|
|
prm.SetArgs(p.id, p.key, p.value)
|
|
|
|
prm.InvokePrmOptional = p.InvokePrmOptional
|
2021-11-09 20:52:29 +00:00
|
|
|
|
|
|
|
return c.client.Invoke(prm)
|
2021-05-31 11:50:11 +00:00
|
|
|
}
|
|
|
|
|
2023-02-05 15:59:38 +00:00
|
|
|
// RawNetworkParameter is a FrostFS network parameter which is transmitted but
|
|
|
|
// not interpreted by the FrostFS API protocol.
|
2022-06-08 23:18:26 +00:00
|
|
|
type RawNetworkParameter struct {
|
|
|
|
// Name of the parameter.
|
|
|
|
Name string
|
|
|
|
|
|
|
|
// Raw parameter value.
|
|
|
|
Value []byte
|
|
|
|
}
|
|
|
|
|
2023-02-05 15:59:38 +00:00
|
|
|
// NetworkConfiguration represents FrostFS network configuration stored
|
|
|
|
// in the FrostFS Sidechain.
|
2022-06-08 23:18:26 +00:00
|
|
|
type NetworkConfiguration struct {
|
|
|
|
MaxObjectSize uint64
|
|
|
|
|
|
|
|
StoragePrice uint64
|
|
|
|
|
|
|
|
AuditFee uint64
|
|
|
|
|
|
|
|
EpochDuration uint64
|
|
|
|
|
|
|
|
ContainerFee uint64
|
|
|
|
|
|
|
|
ContainerAliasFee uint64
|
|
|
|
|
|
|
|
IRCandidateFee uint64
|
|
|
|
|
|
|
|
WithdrawalFee uint64
|
|
|
|
|
2022-09-19 15:31:56 +00:00
|
|
|
HomomorphicHashingDisabled bool
|
|
|
|
|
2022-09-19 15:39:03 +00:00
|
|
|
MaintenanceModeAllowed bool
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
Raw []RawNetworkParameter
|
|
|
|
}
|
|
|
|
|
2023-02-05 15:59:38 +00:00
|
|
|
// ReadNetworkConfiguration reads NetworkConfiguration from the FrostFS Sidechain.
|
2022-06-16 11:25:55 +00:00
|
|
|
func (c *Client) ReadNetworkConfiguration() (NetworkConfiguration, error) {
|
|
|
|
var res NetworkConfiguration
|
2022-01-31 11:58:55 +00:00
|
|
|
prm := client.TestInvokePrm{}
|
|
|
|
prm.SetMethod(configListMethod)
|
|
|
|
|
|
|
|
items, err := c.client.TestInvoke(prm)
|
|
|
|
if err != nil {
|
2022-06-16 11:25:55 +00:00
|
|
|
return res, fmt.Errorf("could not perform test invocation (%s): %w",
|
2022-01-31 11:58:55 +00:00
|
|
|
configListMethod, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if ln := len(items); ln != 1 {
|
2022-06-16 11:25:55 +00:00
|
|
|
return res, fmt.Errorf("unexpected stack item count (%s): %d", configListMethod, ln)
|
2022-01-31 11:58:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
arr, err := client.ArrayFromStackItem(items[0])
|
|
|
|
if err != nil {
|
2022-06-16 11:25:55 +00:00
|
|
|
return res, fmt.Errorf("record list (%s): %w", configListMethod, err)
|
2022-01-31 11:58:55 +00:00
|
|
|
}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
m := make(map[string]struct{}, len(arr))
|
|
|
|
res.Raw = make([]RawNetworkParameter, 0, len(arr))
|
2021-05-05 20:54:08 +00:00
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
err = iterateRecords(arr, func(name string, value []byte) error {
|
|
|
|
_, ok := m[name]
|
|
|
|
if ok {
|
|
|
|
return fmt.Errorf("duplicated config name %s", name)
|
|
|
|
}
|
2022-01-31 11:58:55 +00:00
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
m[name] = struct{}{}
|
|
|
|
|
|
|
|
switch name {
|
2022-01-31 11:58:55 +00:00
|
|
|
default:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.Raw = append(res.Raw, RawNetworkParameter{
|
|
|
|
Name: name,
|
|
|
|
Value: value,
|
|
|
|
})
|
2023-04-11 11:59:24 +00:00
|
|
|
case MaxObjectSizeConfig:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.MaxObjectSize = bytesToUint64(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case BasicIncomeRateConfig:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.StoragePrice = bytesToUint64(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case AuditFeeConfig:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.AuditFee = bytesToUint64(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case EpochDurationConfig:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.EpochDuration = bytesToUint64(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case ContainerFeeConfig:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.ContainerFee = bytesToUint64(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case ContainerAliasFeeConfig:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.ContainerAliasFee = bytesToUint64(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case IrCandidateFeeConfig:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.IRCandidateFee = bytesToUint64(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case WithdrawFeeConfig:
|
2022-06-08 23:18:26 +00:00
|
|
|
res.WithdrawalFee = bytesToUint64(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case HomomorphicHashingDisabledKey:
|
2022-09-19 15:31:56 +00:00
|
|
|
res.HomomorphicHashingDisabled = bytesToBool(value)
|
2023-04-11 11:59:24 +00:00
|
|
|
case MaintenanceModeAllowedConfig:
|
2022-09-19 15:39:03 +00:00
|
|
|
res.MaintenanceModeAllowed = bytesToBool(value)
|
2022-01-31 11:58:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2022-06-08 23:18:26 +00:00
|
|
|
|
2022-06-16 11:25:55 +00:00
|
|
|
return res, err
|
2022-01-31 11:58:55 +00:00
|
|
|
}
|
|
|
|
|
2022-04-12 13:59:33 +00:00
|
|
|
func bytesToUint64(val []byte) uint64 {
|
|
|
|
if len(val) == 0 {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return bigint.FromBytes(val).Uint64()
|
|
|
|
}
|
|
|
|
|
2022-09-19 15:31:56 +00:00
|
|
|
func bytesToBool(val []byte) bool {
|
|
|
|
for i := range val {
|
|
|
|
if val[i] != 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-05-06 08:06:25 +00:00
|
|
|
// ErrConfigNotFound is returned when the requested key was not found
|
|
|
|
// in the network config (returned value is `Null`).
|
|
|
|
var ErrConfigNotFound = errors.New("config value not found")
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// config performs the test invoke of get config value
|
2023-02-05 15:59:38 +00:00
|
|
|
// method of FrostFS Netmap contract.
|
2022-05-06 08:06:25 +00:00
|
|
|
//
|
|
|
|
// Returns ErrConfigNotFound if config key is not found in the contract.
|
2023-02-21 11:42:45 +00:00
|
|
|
func (c *Client) config(key []byte, assert func(stackitem.Item) (any, error)) (any, error) {
|
2022-01-31 11:58:55 +00:00
|
|
|
prm := client.TestInvokePrm{}
|
|
|
|
prm.SetMethod(configMethod)
|
|
|
|
prm.SetArgs(key)
|
|
|
|
|
|
|
|
items, err := c.client.TestInvoke(prm)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not perform test invocation (%s): %w",
|
|
|
|
configMethod, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if ln := len(items); ln != 1 {
|
|
|
|
return nil, fmt.Errorf("unexpected stack item count (%s): %d",
|
|
|
|
configMethod, ln)
|
|
|
|
}
|
|
|
|
|
2022-05-06 08:06:25 +00:00
|
|
|
if _, ok := items[0].(stackitem.Null); ok {
|
|
|
|
return nil, ErrConfigNotFound
|
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
return assert(items[0])
|
2021-05-05 20:54:08 +00:00
|
|
|
}
|
2021-09-20 16:09:32 +00:00
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// IntegerAssert converts stack item to int64.
|
2023-02-21 11:42:45 +00:00
|
|
|
func IntegerAssert(item stackitem.Item) (any, error) {
|
2022-01-31 11:58:55 +00:00
|
|
|
return client.IntFromStackItem(item)
|
2021-09-20 16:09:32 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// StringAssert converts stack item to string.
|
2023-02-21 11:42:45 +00:00
|
|
|
func StringAssert(item stackitem.Item) (any, error) {
|
2022-01-31 11:58:55 +00:00
|
|
|
return client.StringFromStackItem(item)
|
2021-09-20 16:09:32 +00:00
|
|
|
}
|
|
|
|
|
2022-04-29 17:47:14 +00:00
|
|
|
// BoolAssert converts stack item to bool.
|
2023-02-21 11:42:45 +00:00
|
|
|
func BoolAssert(item stackitem.Item) (any, error) {
|
2022-04-29 17:47:14 +00:00
|
|
|
return client.BoolFromStackItem(item)
|
|
|
|
}
|
|
|
|
|
2022-01-31 11:58:55 +00:00
|
|
|
// iterateRecords iterates over all config records and passes them to f.
|
2021-09-20 16:09:32 +00:00
|
|
|
//
|
|
|
|
// Returns f's errors directly.
|
2022-06-08 23:18:26 +00:00
|
|
|
func iterateRecords(arr []stackitem.Item, f func(key string, value []byte) error) error {
|
2022-01-31 11:58:55 +00:00
|
|
|
for i := range arr {
|
|
|
|
fields, err := client.ArrayFromStackItem(arr[i])
|
2021-09-20 16:09:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("record fields: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if ln := len(fields); ln != 2 {
|
|
|
|
return fmt.Errorf("unexpected record fields number: %d", ln)
|
|
|
|
}
|
|
|
|
|
|
|
|
k, err := client.BytesFromStackItem(fields[0])
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("record key: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
v, err := client.BytesFromStackItem(fields[1])
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("record value: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
if err := f(string(k), v); err != nil {
|
2021-09-20 16:09:32 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|