diff --git a/docs/api.md b/docs/api.md index 2a9aa0884..e2bf4da92 100644 --- a/docs/api.md +++ b/docs/api.md @@ -69,6 +69,10 @@ - [Msg](#coreum.asset.ft.v1.Msg) +- [coreum/asset/nft/v1/authz.proto](#coreum/asset/nft/v1/authz.proto) + - [NFTIdentifier](#coreum.asset.nft.v1.NFTIdentifier) + - [SendAuthorization](#coreum.asset.nft.v1.SendAuthorization) + - [coreum/asset/nft/v1/event.proto](#coreum/asset/nft/v1/event.proto) - [EventAddedToClassWhitelist](#coreum.asset.nft.v1.EventAddedToClassWhitelist) - [EventAddedToWhitelist](#coreum.asset.nft.v1.EventAddedToWhitelist) @@ -1224,6 +1228,53 @@ Msg defines the Msg service. + +
+ +## coreum/asset/nft/v1/authz.proto + + + + + +### NFTIdentifier + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `class_id` | [string](#string) | | class_id defines the unique identifier of the nft classification, similar to the contract address of ERC721 | +| `id` | [string](#string) | | id defines the unique identification of nft | + + + + + + + + +### SendAuthorization +SendAuthorization allows the grantee to send specific NFTs from the granter's account. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `nfts` | [NFTIdentifier](#coreum.asset.nft.v1.NFTIdentifier) | repeated | | + + + + + + + + + + + + + + + diff --git a/integration-tests/modules/assetnft_test.go b/integration-tests/modules/assetnft_test.go index d054a9c8f..0f364edfa 100644 --- a/integration-tests/modules/assetnft_test.go +++ b/integration-tests/modules/assetnft_test.go @@ -1875,3 +1875,143 @@ func TestAssetNFTClassWhitelist(t *testing.T) { ) requireT.NoError(err) } + +// TestAssetNFTSendAuthorization tests that assetnft SendAuthorization works as expected. +func TestAssetNFTSendAuthorization(t *testing.T) { + t.Parallel() + + ctx, chain := integrationtests.NewCoreumTestingContext(t) + + requireT := require.New(t) + granter := chain.GenAccount() + grantee := chain.GenAccount() + recipient := chain.GenAccount() + nftClient := nft.NewQueryClient(chain.ClientContext) + authzClient := authztypes.NewQueryClient(chain.ClientContext) + + chain.FundAccountWithOptions(ctx, t, granter, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &assetnfttypes.MsgIssueClass{}, + &assetnfttypes.MsgMint{}, + &authztypes.MsgGrant{}, + }, + Amount: chain.QueryAssetNFTParams(ctx, t).MintFee.Amount, + }) + + chain.FundAccountWithOptions(ctx, t, grantee, integration.BalancesOptions{ + Amount: sdk.NewInt(1), + }) + + // issue new NFT class + issueMsg := &assetnfttypes.MsgIssueClass{ + Issuer: granter.String(), + Symbol: "NFTClassSymbol", + Features: []assetnfttypes.ClassFeature{}, + } + + // mint new token in that class + classID := assetnfttypes.BuildClassID(issueMsg.Symbol, granter) + nftID := "id-1" + mintMsg1 := &assetnfttypes.MsgMint{ + Sender: granter.String(), + ID: nftID, + ClassID: classID, + } + + msgList := []sdk.Msg{issueMsg, mintMsg1} + _, err := client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(granter), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgList...)), + msgList..., + ) + requireT.NoError(err) + + // try to send before grant + sendMsg := &nft.MsgSend{ + ClassId: classID, + Id: nftID, + Sender: granter.String(), + Receiver: recipient.String(), + } + execMsg := authztypes.NewMsgExec(grantee, []sdk.Msg{sendMsg}) + + chain.FundAccountWithOptions(ctx, t, grantee, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &execMsg, + &execMsg, + }, + }) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(grantee), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(&execMsg)), + &execMsg, + ) + requireT.Error(err) + requireT.ErrorIs(err, authztypes.ErrNoAuthorizationFound) + + // grant authorization to send nft + grantMsg, err := authztypes.NewMsgGrant( + granter, + grantee, + assetnfttypes.NewSendAuthorization([]assetnfttypes.NFTIdentifier{ + {ClassId: classID, Id: nftID}, + {ClassId: classID, Id: "not-minted-yet"}, + }), + lo.ToPtr(time.Now().Add(time.Minute)), + ) + requireT.NoError(err) + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(granter), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(grantMsg)), + grantMsg, + ) + requireT.NoError(err) + + // assert granted + gransRes, err := authzClient.Grants(ctx, &authztypes.QueryGrantsRequest{ + Granter: granter.String(), + Grantee: grantee.String(), + }) + requireT.NoError(err) + requireT.Equal(1, len(gransRes.Grants)) + updatedGrant := assetnfttypes.SendAuthorization{} + chain.ClientContext.Codec().MustUnmarshal(gransRes.Grants[0].Authorization.Value, &updatedGrant) + requireT.ElementsMatch([]assetnfttypes.NFTIdentifier{ + {ClassId: classID, Id: nftID}, + {ClassId: classID, Id: "not-minted-yet"}, + }, updatedGrant.Nfts) + + // try to send after grant + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(grantee), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(&execMsg)), + &execMsg, + ) + requireT.NoError(err) + + // assert transfer of ownership + ownerResp, err := nftClient.Owner(ctx, &nft.QueryOwnerRequest{ + ClassId: classID, + Id: nftID, + }) + requireT.NoError(err) + requireT.EqualValues(ownerResp.Owner, recipient.String()) + + // assert granted + gransRes, err = authzClient.Grants(ctx, &authztypes.QueryGrantsRequest{ + Granter: granter.String(), + Grantee: grantee.String(), + }) + requireT.NoError(err) + requireT.Equal(1, len(gransRes.Grants)) + updatedGrant = assetnfttypes.SendAuthorization{} + chain.ClientContext.Codec().MustUnmarshal(gransRes.Grants[0].Authorization.Value, &updatedGrant) + requireT.ElementsMatch([]assetnfttypes.NFTIdentifier{ + {ClassId: classID, Id: "not-minted-yet"}, + }, updatedGrant.Nfts) +} diff --git a/proto/coreum/asset/nft/v1/authz.proto b/proto/coreum/asset/nft/v1/authz.proto new file mode 100644 index 000000000..a3aac4c86 --- /dev/null +++ b/proto/coreum/asset/nft/v1/authz.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; +package coreum.asset.nft.v1; + +import "amino/amino.proto"; +import "gogoproto/gogo.proto"; +import "cosmos_proto/cosmos.proto"; + +option go_package = "github.com/CoreumFoundation/coreum/v3/x/asset/nft/types"; + +// SendAuthorization allows the grantee to send specific NFTs from the granter's account. +message SendAuthorization { + option (cosmos_proto.implements_interface) = "cosmos.authz.v1beta1.Authorization"; + option (amino.name) = "cosmos-sdk/nft/SendAuthorization"; + repeated NFTIdentifier nfts = 1 [ + (gogoproto.nullable) = false, + (amino.dont_omitempty) = true + ]; +} + +message NFTIdentifier { + // class_id defines the unique identifier of the nft classification, similar to the contract address of ERC721 + string class_id = 1; + // id defines the unique identification of nft + string id = 2; +} diff --git a/x/asset/nft/types/authz.pb.go b/x/asset/nft/types/authz.pb.go new file mode 100644 index 000000000..0608493c8 --- /dev/null +++ b/x/asset/nft/types/authz.pb.go @@ -0,0 +1,563 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: coreum/asset/nft/v1/authz.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + _ "github.com/cosmos/cosmos-sdk/types/tx/amino" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// SendAuthorization allows the grantee to send specific NFTs from the granter's account. +type SendAuthorization struct { + Nfts []NFTIdentifier `protobuf:"bytes,1,rep,name=nfts,proto3" json:"nfts"` +} + +func (m *SendAuthorization) Reset() { *m = SendAuthorization{} } +func (m *SendAuthorization) String() string { return proto.CompactTextString(m) } +func (*SendAuthorization) ProtoMessage() {} +func (*SendAuthorization) Descriptor() ([]byte, []int) { + return fileDescriptor_58d9031136e2b4ca, []int{0} +} +func (m *SendAuthorization) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *SendAuthorization) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_SendAuthorization.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *SendAuthorization) XXX_Merge(src proto.Message) { + xxx_messageInfo_SendAuthorization.Merge(m, src) +} +func (m *SendAuthorization) XXX_Size() int { + return m.Size() +} +func (m *SendAuthorization) XXX_DiscardUnknown() { + xxx_messageInfo_SendAuthorization.DiscardUnknown(m) +} + +var xxx_messageInfo_SendAuthorization proto.InternalMessageInfo + +func (m *SendAuthorization) GetNfts() []NFTIdentifier { + if m != nil { + return m.Nfts + } + return nil +} + +type NFTIdentifier struct { + // class_id defines the unique identifier of the nft classification, similar to the contract address of ERC721 + ClassId string `protobuf:"bytes,1,opt,name=class_id,json=classId,proto3" json:"class_id,omitempty"` + // id defines the unique identification of nft + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` +} + +func (m *NFTIdentifier) Reset() { *m = NFTIdentifier{} } +func (m *NFTIdentifier) String() string { return proto.CompactTextString(m) } +func (*NFTIdentifier) ProtoMessage() {} +func (*NFTIdentifier) Descriptor() ([]byte, []int) { + return fileDescriptor_58d9031136e2b4ca, []int{1} +} +func (m *NFTIdentifier) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *NFTIdentifier) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_NFTIdentifier.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *NFTIdentifier) XXX_Merge(src proto.Message) { + xxx_messageInfo_NFTIdentifier.Merge(m, src) +} +func (m *NFTIdentifier) XXX_Size() int { + return m.Size() +} +func (m *NFTIdentifier) XXX_DiscardUnknown() { + xxx_messageInfo_NFTIdentifier.DiscardUnknown(m) +} + +var xxx_messageInfo_NFTIdentifier proto.InternalMessageInfo + +func (m *NFTIdentifier) GetClassId() string { + if m != nil { + return m.ClassId + } + return "" +} + +func (m *NFTIdentifier) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func init() { + proto.RegisterType((*SendAuthorization)(nil), "coreum.asset.nft.v1.SendAuthorization") + proto.RegisterType((*NFTIdentifier)(nil), "coreum.asset.nft.v1.NFTIdentifier") +} + +func init() { proto.RegisterFile("coreum/asset/nft/v1/authz.proto", fileDescriptor_58d9031136e2b4ca) } + +var fileDescriptor_58d9031136e2b4ca = []byte{ + // 329 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4f, 0xce, 0x2f, 0x4a, + 0x2d, 0xcd, 0xd5, 0x4f, 0x2c, 0x2e, 0x4e, 0x2d, 0xd1, 0xcf, 0x4b, 0x2b, 0xd1, 0x2f, 0x33, 0xd4, + 0x4f, 0x2c, 0x2d, 0xc9, 0xa8, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x86, 0x28, 0xd0, + 0x03, 0x2b, 0xd0, 0xcb, 0x4b, 0x2b, 0xd1, 0x2b, 0x33, 0x94, 0x12, 0x4c, 0xcc, 0xcd, 0xcc, 0xcb, + 0xd7, 0x07, 0x93, 0x10, 0x75, 0x52, 0x22, 0xe9, 0xf9, 0xe9, 0xf9, 0x60, 0xa6, 0x3e, 0x88, 0x05, + 0x15, 0x95, 0x4c, 0xce, 0x2f, 0xce, 0xcd, 0x2f, 0x8e, 0x87, 0x48, 0x40, 0x38, 0x10, 0x29, 0xa5, + 0xc5, 0x8c, 0x5c, 0x82, 0xc1, 0xa9, 0x79, 0x29, 0x8e, 0xa5, 0x25, 0x19, 0xf9, 0x45, 0x99, 0x55, + 0x89, 0x25, 0x99, 0xf9, 0x79, 0x42, 0x8e, 0x5c, 0x2c, 0x79, 0x69, 0x25, 0xc5, 0x12, 0x8c, 0x0a, + 0xcc, 0x1a, 0xdc, 0x46, 0x4a, 0x7a, 0x58, 0x6c, 0xd7, 0xf3, 0x73, 0x0b, 0xf1, 0x4c, 0x49, 0xcd, + 0x2b, 0xc9, 0x4c, 0xcb, 0x4c, 0x2d, 0x72, 0xe2, 0x3c, 0x71, 0x4f, 0x9e, 0x61, 0xc5, 0xf3, 0x0d, + 0x5a, 0x8c, 0x41, 0x60, 0xad, 0x56, 0xde, 0xa7, 0xb6, 0xe8, 0x2a, 0x41, 0xad, 0x82, 0xf8, 0xa4, + 0xcc, 0x30, 0x29, 0xb5, 0x24, 0xd1, 0x50, 0x0f, 0xc5, 0xaa, 0xae, 0xe7, 0x1b, 0xb4, 0x14, 0x20, + 0xca, 0x74, 0x8b, 0x53, 0xb2, 0xc1, 0x7e, 0xc7, 0x70, 0x8f, 0x92, 0x15, 0x17, 0x2f, 0x8a, 0x75, + 0x42, 0x92, 0x5c, 0x1c, 0xc9, 0x39, 0x89, 0xc5, 0xc5, 0xf1, 0x99, 0x29, 0x12, 0x8c, 0x0a, 0x8c, + 0x1a, 0x9c, 0x41, 0xec, 0x60, 0xbe, 0x67, 0x8a, 0x10, 0x1f, 0x17, 0x53, 0x66, 0x8a, 0x04, 0x13, + 0x58, 0x90, 0x29, 0x33, 0xc5, 0x29, 0xf0, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, + 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0xe1, 0xc2, 0x63, 0x39, 0x86, 0x1b, 0x8f, 0xe5, 0x18, + 0xa2, 0xcc, 0xd3, 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0x9d, 0xc1, 0x3e, + 0x74, 0xcb, 0x2f, 0xcd, 0x4b, 0x01, 0x5b, 0xa9, 0x0f, 0x8d, 0x91, 0x32, 0x63, 0xfd, 0x0a, 0xa4, + 0x68, 0x29, 0xa9, 0x2c, 0x48, 0x2d, 0x4e, 0x62, 0x03, 0x87, 0x9d, 0x31, 0x20, 0x00, 0x00, 0xff, + 0xff, 0x35, 0xc4, 0xa2, 0x3c, 0xb7, 0x01, 0x00, 0x00, +} + +func (m *SendAuthorization) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *SendAuthorization) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *SendAuthorization) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Nfts) > 0 { + for iNdEx := len(m.Nfts) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Nfts[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintAuthz(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *NFTIdentifier) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *NFTIdentifier) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *NFTIdentifier) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Id) > 0 { + i -= len(m.Id) + copy(dAtA[i:], m.Id) + i = encodeVarintAuthz(dAtA, i, uint64(len(m.Id))) + i-- + dAtA[i] = 0x12 + } + if len(m.ClassId) > 0 { + i -= len(m.ClassId) + copy(dAtA[i:], m.ClassId) + i = encodeVarintAuthz(dAtA, i, uint64(len(m.ClassId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintAuthz(dAtA []byte, offset int, v uint64) int { + offset -= sovAuthz(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *SendAuthorization) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Nfts) > 0 { + for _, e := range m.Nfts { + l = e.Size() + n += 1 + l + sovAuthz(uint64(l)) + } + } + return n +} + +func (m *NFTIdentifier) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ClassId) + if l > 0 { + n += 1 + l + sovAuthz(uint64(l)) + } + l = len(m.Id) + if l > 0 { + n += 1 + l + sovAuthz(uint64(l)) + } + return n +} + +func sovAuthz(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozAuthz(x uint64) (n int) { + return sovAuthz(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *SendAuthorization) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: SendAuthorization: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: SendAuthorization: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Nfts", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthAuthz + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthAuthz + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Nfts = append(m.Nfts, NFTIdentifier{}) + if err := m.Nfts[len(m.Nfts)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipAuthz(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthAuthz + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *NFTIdentifier) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: NFTIdentifier: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: NFTIdentifier: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ClassId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthAuthz + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAuthz + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ClassId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthAuthz + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAuthz + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Id = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipAuthz(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthAuthz + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipAuthz(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAuthz + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAuthz + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAuthz + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthAuthz + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupAuthz + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthAuthz + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthAuthz = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowAuthz = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupAuthz = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/asset/nft/types/codec.go b/x/asset/nft/types/codec.go index 543ee8b3d..c68e2dfe4 100644 --- a/x/asset/nft/types/codec.go +++ b/x/asset/nft/types/codec.go @@ -4,6 +4,7 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/msgservice" + "github.com/cosmos/cosmos-sdk/x/authz" "github.com/cosmos/gogoproto/proto" ) @@ -19,5 +20,9 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { &MsgAddToWhitelist{}, &MsgRemoveFromWhitelist{}, ) + registry.RegisterImplementations( + (*authz.Authorization)(nil), + &SendAuthorization{}, + ) msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } diff --git a/x/asset/nft/types/send_authorization.go b/x/asset/nft/types/send_authorization.go new file mode 100644 index 000000000..1d8bd7cfc --- /dev/null +++ b/x/asset/nft/types/send_authorization.go @@ -0,0 +1,70 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" + "github.com/cosmos/cosmos-sdk/x/nft" +) + +var _ authz.Authorization = &SendAuthorization{} + +// NewSendAuthorization returns a new SendAuthorization object. +func NewSendAuthorization(nfts []NFTIdentifier) *SendAuthorization { + return &SendAuthorization{ + Nfts: nfts, + } +} + +// MsgTypeURL implements Authorization.MsgTypeURL. +func (a SendAuthorization) MsgTypeURL() string { + return sdk.MsgTypeURL(&nft.MsgSend{}) +} + +// Accept implements Authorization.Accept. +func (a SendAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authz.AcceptResponse, error) { + mSend, ok := msg.(*nft.MsgSend) + if !ok { + return authz.AcceptResponse{}, sdkerrors.ErrInvalidType.Wrap("type mismatch") + } + + exists := a.findAndRemoveNFT(mSend.ClassId, mSend.Id) + if !exists { + return authz.AcceptResponse{}, sdkerrors.ErrUnauthorized.Wrapf("requested NFT does not have transfer grant") + } + + return authz.AcceptResponse{ + Accept: true, + Delete: len(a.Nfts) == 0, + Updated: &a, + }, nil +} + +// ValidateBasic implements Authorization.ValidateBasic. +func (a SendAuthorization) ValidateBasic() error { + if len(a.Nfts) == 0 { + return ErrInvalidInput.Wrap("empty NFT list") + } + + for _, nft := range a.Nfts { + if err := ValidateTokenID(nft.Id); err != nil { + return ErrInvalidInput.Wrap(err.Error()) + } + + if _, _, err := DeconstructClassID(nft.ClassId); err != nil { + return ErrInvalidInput.Wrap(err.Error()) + } + } + + return nil +} + +func (a *SendAuthorization) findAndRemoveNFT(classID, nftID string) bool { + for index, nft := range a.Nfts { + if nft.ClassId == classID && nft.Id == nftID { + a.Nfts = append(a.Nfts[:index], a.Nfts[index+1:]...) + return true + } + } + return false +} diff --git a/x/asset/nft/types/send_authorization_test.go b/x/asset/nft/types/send_authorization_test.go new file mode 100644 index 000000000..6085b9b63 --- /dev/null +++ b/x/asset/nft/types/send_authorization_test.go @@ -0,0 +1,95 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFindAndRemoveNFT(t *testing.T) { + testCases := []struct { + name string + nfts []NFTIdentifier + classID string + nftID string + expectedNfts []NFTIdentifier + found bool + }{ + { + "nft not found", + []NFTIdentifier{ + {"class1", "nft1"}, + {"class2", "nft2"}, + {"class3", "nft3"}, + }, + "class", "nft", + []NFTIdentifier{ + {"class1", "nft1"}, + {"class2", "nft2"}, + {"class3", "nft3"}, + }, + false, + }, + { + "single element in list", + []NFTIdentifier{ + {"class1", "nft1"}, + }, + "class1", "nft1", + []NFTIdentifier{}, + true, + }, + { + "match start of the list", + []NFTIdentifier{ + {"class1", "nft1"}, + {"class2", "nft2"}, + {"class3", "nft3"}, + }, + "class1", "nft1", + []NFTIdentifier{ + {"class2", "nft2"}, + {"class3", "nft3"}, + }, + true, + }, + { + "match end of the list", + []NFTIdentifier{ + {"class1", "nft1"}, + {"class2", "nft2"}, + {"class3", "nft3"}, + }, + "class3", "nft3", + []NFTIdentifier{ + {"class1", "nft1"}, + {"class2", "nft2"}, + }, + true, + }, + { + "match middle of the list", + []NFTIdentifier{ + {"class1", "nft1"}, + {"class2", "nft2"}, + {"class3", "nft3"}, + }, + "class2", "nft2", + []NFTIdentifier{ + {"class1", "nft1"}, + {"class3", "nft3"}, + }, + true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + a := NewSendAuthorization(tc.nfts) + found := a.findAndRemoveNFT(tc.classID, tc.nftID) + assert.EqualValues(t, tc.found, found) + assert.EqualValues(t, tc.expectedNfts, a.Nfts) + }) + } +}