generated from TrueCloudLab/basic
[#3] policy: Introduce policy contract interface wrapper #24
6 changed files with 210 additions and 22 deletions
19
go.mod
19
go.mod
|
@ -2,13 +2,24 @@ module git.frostfs.info/TrueCloudLab/policy-engine
|
|||
|
||||
go 1.20
|
||||
|
||||
require github.com/stretchr/testify v1.8.4
|
||||
require (
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958
|
||||
dkirillov marked this conversation as resolved
Outdated
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/nspcc-dev/neo-go v0.103.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
)
|
||||
|
||||
require (
|
||||
dkirillov marked this conversation as resolved
Outdated
dkirillov
commented
Can we keep just one require section with indirect dependencies? Can we keep just one require section with indirect dependencies?
aarifullin
commented
Sure! Sure!
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5 // indirect
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
43
go.sum
43
go.sum
|
@ -1,18 +1,43 @@
|
|||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 h1:X9yPizADIhD3K/gdKVCthlAnf9aQ3UJJGnZgIwwixRQ=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958/go.mod h1:rQWdsG18NaiFvkJpMguJev913KD/yleHaniRBkUyt0o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
||||
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U=
|
||||
github.com/nspcc-dev/neo-go v0.103.0 h1:UVyWPhzZdfYFG35ORP3FRDLh8J/raRQ6m8SptDdlgfM=
|
||||
github.com/nspcc-dev/neo-go v0.103.0/go.mod h1:x+wmcYqpZYJwLp1l/pHZrqNp3RSWlkMymWGDij3/OPo=
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5 h1:09CpI5uwsxb1EeFPIKQRwwWlfCmDD/Dwwh01lPiQScM=
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5/go.mod h1:J/Mk6+nKeKSW4wygkZQFLQ6SkLOSGX5Ga0RuuuktEag=
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -669,7 +669,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
|||
requireChainRulesMatch(t, expected.Rules, nativeChain.Rules)
|
||||
|
||||
s := inmemory.NewInMemory()
|
||||
err = s.MorphRuleChainStorage().AddMorphRuleChain("name", engine.NamespaceTarget("ns"), nativeChain)
|
||||
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain("name", engine.NamespaceTarget("ns"), nativeChain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range []struct {
|
||||
|
@ -933,7 +933,7 @@ func TestComplexS3Conditions(t *testing.T) {
|
|||
requireChainRulesMatch(t, expected.Rules, s3Chain.Rules)
|
||||
|
||||
s := inmemory.NewInMemory()
|
||||
err = s.MorphRuleChainStorage().AddMorphRuleChain("name", engine.NamespaceTarget("ns"), s3Chain)
|
||||
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain("name", engine.NamespaceTarget("ns"), s3Chain)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range []struct {
|
||||
|
|
|
@ -3,6 +3,7 @@ package inmemory
|
|||
import (
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
type inmemoryMorphRuleChainStorage struct {
|
||||
|
@ -17,7 +18,7 @@ func NewInmemoryMorphRuleChainStorage() engine.MorphRuleChainStorage {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (err error) {
|
||||
func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (_ util.Uint256, _ uint32, err error) {
|
||||
switch target.Type {
|
||||
case engine.Namespace:
|
||||
_, err = s.nameToNamespaceChains.AddOverride(name, target, c)
|
||||
|
@ -29,15 +30,16 @@ func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, targe
|
|||
return
|
||||
}
|
||||
|
||||
func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) error {
|
||||
func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (_ util.Uint256, _ uint32, err error) {
|
||||
switch target.Type {
|
||||
case engine.Namespace:
|
||||
return s.nameToNamespaceChains.RemoveOverride(name, target, chainID)
|
||||
err = s.nameToNamespaceChains.RemoveOverride(name, target, chainID)
|
||||
case engine.Container:
|
||||
return s.nameToContainerChains.RemoveOverride(name, target, chainID)
|
||||
err = s.nameToContainerChains.RemoveOverride(name, target, chainID)
|
||||
default:
|
||||
return engine.ErrUnknownTarget
|
||||
err = engine.ErrUnknownTarget
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *inmemoryMorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package engine
|
|||
import (
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
type ChainRouter interface {
|
||||
|
@ -93,10 +94,13 @@ func ContainerTarget(container string) Target {
|
|||
// MorphRuleChainStorage is the interface to manage chains from the chain storage.
|
||||
// Basically, this implies that the storage manages rules stored in policy contract.
|
||||
type MorphRuleChainStorage interface {
|
||||
AddMorphRuleChain(name chain.Name, target Target, c *chain.Chain) error
|
||||
// AddMorphRuleChain adds a chain rule to the policy contract and returns transaction hash, VUB and error.
|
||||
AddMorphRuleChain(name chain.Name, target Target, c *chain.Chain) (util.Uint256, uint32, error)
|
||||
|
||||
RemoveMorphRuleChain(name chain.Name, target Target, chainID chain.ID) error
|
||||
// RemoveMorphRuleChain removes a chain rule to the policy contract and returns transaction hash, VUB and error.
|
||||
RemoveMorphRuleChain(name chain.Name, target Target, chainID chain.ID) (util.Uint256, uint32, error)
|
||||
|
||||
// ListMorphRuleChains just lists deserialized chains.
|
||||
ListMorphRuleChains(name chain.Name, target Target) ([]*chain.Chain, error)
|
||||
}
|
||||
|
||||
|
|
146
pkg/morph/policy/policy_contract_storage.go
Normal file
146
pkg/morph/policy/policy_contract_storage.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package policy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
|
||||
client "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/policy"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||
"github.com/mr-tron/base58"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyChainID = errors.New("chain id is not set")
|
||||
|
||||
ErrEngineTargetTypeUnsupported = errors.New("this target type is not supported yet")
|
||||
)
|
||||
|
||||
// ContractStorage is the interface to manage chain rules within the policy contract.
|
||||
type ContractStorage struct {
|
||||
contractInterface *client.Contract
|
||||
}
|
||||
|
||||
var _ engine.MorphRuleChainStorage = (*ContractStorage)(nil)
|
||||
|
||||
func NewContractStorage(actor client.Actor, contract util.Uint160) *ContractStorage {
|
||||
return &ContractStorage{
|
||||
contractInterface: client.New(actor, contract),
|
||||
}
|
||||
}
|
||||
|
||||
func NewContractStorageWithSimpleActor(rpcActor actor.RPCActor, acc *wallet.Account, contract util.Uint160) (*ContractStorage, error) {
|
||||
act, err := actor.NewSimple(rpcActor, acc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create simple actor: %w", err)
|
||||
}
|
||||
return NewContractStorage(act, contract), nil
|
||||
}
|
||||
|
||||
func transformNameIfContainer(target engine.Target) (name string) {
|
||||
aarifullin
commented
Need your help, please Despite this implementation works in @fyrchik
Need your help, please
Despite this implementation works in `frostfs-node` (I checked locally), I think it is still raw.
There are some problems like should I return `transactionHash` and `VUB` from these invocations and consider them for `awaitTx` usage outside?
fyrchik
commented
Returning hash and VUB is desireable, but I believe we could use some callbacks in actor to catch them. Returning hash and VUB is desireable, but I believe we could use some callbacks in actor to catch them.
|
||||
name = target.Name
|
||||
if target.Type == engine.Container {
|
||||
// Container name can be too long and, thus, cannot be
|
||||
// used as a key name for policy-contract storage.
|
||||
name = base58.FastBase58Encoding([]byte(target.Name))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *ContractStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (txHash util.Uint256, vub uint32, err error) {
|
||||
if c.ID == "" {
|
||||
err = ErrEmptyChainID
|
||||
return
|
||||
}
|
||||
|
||||
var kind policy.Kind
|
||||
kind, err = policyKind(target.Type)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fullName := prefixedChainName(name, c.ID)
|
||||
targetName := transformNameIfContainer(target)
|
||||
|
||||
txHash, vub, err = s.contractInterface.AddChain(big.NewInt(int64(kind)), targetName, fullName, c.Bytes())
|
||||
return
|
||||
}
|
||||
|
||||
func (s *ContractStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) (txHash util.Uint256, vub uint32, err error) {
|
||||
if chainID == "" {
|
||||
err = ErrEmptyChainID
|
||||
return
|
||||
}
|
||||
|
||||
var kind policy.Kind
|
||||
kind, err = policyKind(target.Type)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fullName := prefixedChainName(name, chainID)
|
||||
targetName := transformNameIfContainer(target)
|
||||
|
||||
txHash, vub, err = s.contractInterface.RemoveChain(big.NewInt(int64(kind)), targetName, fullName)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *ContractStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
|
||||
kind, err := policyKind(target.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetName := transformNameIfContainer(target)
|
||||
|
||||
items, err := s.contractInterface.ListChainsByPrefix(big.NewInt(int64(kind)), targetName, []byte(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chains []*chain.Chain
|
||||
for _, item := range items {
|
||||
serialized, err := bytesFromStackItem(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := new(chain.Chain)
|
||||
if err := c.DecodeBytes(serialized); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chains = append(chains, c)
|
||||
}
|
||||
|
||||
return chains, nil
|
||||
}
|
||||
|
||||
func bytesFromStackItem(param stackitem.Item) ([]byte, error) {
|
||||
switch param.Type() {
|
||||
case stackitem.BufferT, stackitem.ByteArrayT, stackitem.IntegerT:
|
||||
return param.TryBytes()
|
||||
case stackitem.AnyT:
|
||||
if param.Value() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
fyrchik
commented
This is invalid approach, This is invalid approach, `n.Bytes()` has different encoding
`TryBytes` is enough, irregardless of type (it will work for integer too).
aarifullin
commented
Alright. I have fixed. Alright. I have fixed.
This was copy-pasta from [here](https://git.frostfs.info/TrueCloudLab/frostfs-node/src/branch/master/pkg/morph/client/util.go#L49)
|
||||
return nil, fmt.Errorf("chain/client: %s is not a byte array type", param.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func prefixedChainName(name chain.Name, chainID chain.ID) []byte {
|
||||
return []byte(strings.ToLower(fmt.Sprintf("%s:%s", name, chainID)))
|
||||
}
|
||||
|
||||
func policyKind(typ engine.TargetType) (policy.Kind, error) {
|
||||
if typ == engine.Namespace {
|
||||
return policy.Namespace, nil
|
||||
} else if typ == engine.Container {
|
||||
return policy.Container, nil
|
||||
}
|
||||
return policy.Kind(0), ErrEngineTargetTypeUnsupported
|
||||
}
|
Loading…
Add table
Reference in a new issue
Can we user the latest version? We have different method signature now
(name is
[]byte
func AddChain(entity Kind, entityName string, name []byte, chain []byte) {
)
Ofc, thanks for this point