Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/interchaintest-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ name: ictest E2E

on:
pull_request:
branches:
- main
- master
branches-ignore:
- "mvp/**"

Comment on lines +5 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix invalid pull_request filters (branches + branches-ignore are mutually exclusive).

actionlint flags this; GitHub Actions ignores one set unpredictably. Also note: pull_request filters apply to base branch, not head. To exclude PRs from mvp/** heads, drop branches-ignore here and gate jobs with an if condition.

Apply this diff to the trigger:

   pull_request:
-    branches:
-      - main
-      - master
-    branches-ignore:
-      - "mvp/**"
+    branches:
+      - main
+      - master

Then add a job-level guard to skip PRs whose head starts with mvp/ (outside this hunk):

jobs:
  build-docker:
    if: ${{ github.event_name != 'pull_request' || !startsWith(github.head_ref, 'mvp/') }}
    ...
  e2e-tests:
    if: ${{ github.event_name != 'pull_request' || !startsWith(github.head_ref, 'mvp/') }}
    ...
🧰 Tools
🪛 actionlint (1.7.7)

8-8: both "branches" and "branches-ignore" filters cannot be used for the same event "pull_request". note: use '!' to negate patterns

(events)

🤖 Prompt for AI Agents
.github/workflows/interchaintest-e2e.yml lines 5-10: the pull_request trigger
currently uses both branches and branches-ignore which are mutually exclusive
and branches-ignore is inappropriate for filtering PR heads; remove the
branches-ignore entry and keep only the branches list under the pull_request
trigger, then add job-level guards on each job (e.g., build-docker and
e2e-tests) to skip PRs whose head ref starts with "mvp/" by using an if
condition that allows the job when the event is not pull_request or when
startsWith(github.head_ref, 'mvp/') is false (i.e., if: ${{ github.event_name !=
'pull_request' || !startsWith(github.head_ref, 'mvp/') }}).

push:
tags:
- "**"
branches:
- "main"
- "master"
- main
- master
branches-ignore:
- "mvp/**"
Comment on lines +15 to +18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix invalid push filters (branches + branches-ignore conflict).

Use negative patterns in branches instead of branches-ignore.

   push:
     tags:
       - "**"
     branches:
-      - main
-      - master
-    branches-ignore:
-      - "mvp/**"
+      - main
+      - master
+      - '!mvp/**'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- main
- master
branches-ignore:
- "mvp/**"
push:
tags:
- "**"
branches:
- main
- master
- '!mvp/**'
🧰 Tools
🪛 actionlint (1.7.7)

17-17: both "branches" and "branches-ignore" filters cannot be used for the same event "push". note: use '!' to negate patterns

(events)

🤖 Prompt for AI Agents
.github/workflows/interchaintest-e2e.yml lines 15-18: the workflow currently
mixes branches and branches-ignore which conflicts for push filters; remove the
branches-ignore entry and replace it by using negative patterns in the branches
list (e.g., keep the allowed branches like main and master and add the negative
pattern for the mvp/** paths) so the push trigger uses only a valid branches
array with exclusion patterns.


permissions:
contents: read
Expand Down
10 changes: 10 additions & 0 deletions app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"github.com/bitsongofficial/go-bitsong/x/fantoken"
fantokenkeeper "github.com/bitsongofficial/go-bitsong/x/fantoken/keeper"
fantokentypes "github.com/bitsongofficial/go-bitsong/x/fantoken/types"
nftkeeper "github.com/bitsongofficial/go-bitsong/x/nft/keeper"
nfttypes "github.com/bitsongofficial/go-bitsong/x/nft/types"
"github.com/bitsongofficial/go-bitsong/x/smart-account/authenticator"
smartaccountkeeper "github.com/bitsongofficial/go-bitsong/x/smart-account/keeper"
smartaccounttypes "github.com/bitsongofficial/go-bitsong/x/smart-account/types"
Expand Down Expand Up @@ -149,6 +151,7 @@ type AppKeepers struct {
ICQKeeper *icqkeeper.Keeper
EvidenceKeeper evidencekeeper.Keeper
FanTokenKeeper fantokenkeeper.Keeper
NftKeeper nftkeeper.Keeper
WasmKeeper wasmkeeper.Keeper
CadenceKeeper cadencekeeper.Keeper
IBCFeeKeeper ibcfeekeeper.Keeper
Expand Down Expand Up @@ -406,6 +409,13 @@ func NewAppKeepers(
BlockedAddrs(),
)

appKeepers.NftKeeper = nftkeeper.NewKeeper(
appCodec,
runtime.NewKVStoreService(appKeepers.keys[nfttypes.StoreKey]),
appKeepers.AccountKeeper,
bApp.Logger(),
)

// Stargate Queries
acceptedStargateQueries := wasmkeeper.AcceptedQueries{
// ibc
Expand Down
2 changes: 2 additions & 0 deletions app/keepers/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keepers

import (
storetypes "cosmossdk.io/store/types"
nfttypes "github.com/bitsongofficial/go-bitsong/x/nft/types"

"cosmossdk.io/x/feegrant"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
Expand Down Expand Up @@ -57,6 +58,7 @@ func (appKeepers *AppKeepers) GenerateKeys() {
wasmtypes.StoreKey,
icqtypes.StoreKey,
fantokentypes.StoreKey,
nfttypes.StoreKey,
cadencetypes.StoreKey,
smartaccounttypes.StoreKey,
protocolpooltypes.StoreKey,
Expand Down
38 changes: 38 additions & 0 deletions proto/bitsong/nft/v1beta1/nft.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
syntax = "proto3";
package bitsong.nft.v1beta1;

import "gogoproto/gogo.proto";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

buf: missing dependency for gogoproto; build will fail.

Add gogoproto to buf deps or vendor third_party/proto/gogoproto/gogo.proto.

Add to buf.yaml:

version: v2
deps:
  - buf.build/cosmos/gogo-proto

If you also rely on google/cosmos imports elsewhere, include:

  • buf.build/googleapis/googleapis
  • buf.build/cosmos/cosmos-proto
  • buf.build/cosmos/cosmos-sdk
🧰 Tools
🪛 Buf (1.55.1)

4-4: import "gogoproto/gogo.proto": file does not exist

(COMPILE)

🤖 Prompt for AI Agents
In proto/bitsong/nft/v1beta1/nft.proto around line 4, the file imports
gogoproto/gogo.proto but buf.yaml has no dependency for gogoproto which will
cause builds to fail; update buf.yaml to add the gogoproto dependency
(buf.build/cosmos/gogo-proto) or vendor third_party/proto/gogoproto/gogo.proto
into the repo, and if your project also uses google/cosmos imports add the
recommended buf deps (buf.build/googleapis/googleapis,
buf.build/cosmos/cosmos-proto, buf.build/cosmos/cosmos-sdk) so the proto import
resolves during buf build.


option go_package = "github.com/bitsongofficial/go-bitsong/x/nft/types";

message Collection {
option (gogoproto.goproto_getters) = false;

string symbol = 1;
string name = 2;
string description = 3;
string uri = 4;

string creator = 5;
string minter = 6;
uint64 num_tokens = 7;
// bool is_mutable
// update_autority (who can update name, description and uri if is_mutable = true)
}

message Nft {
option (gogoproto.goproto_getters) = false;

string collection = 1;
string token_id = 2;

string name = 3;
string description = 4;
string uri = 5;

string owner = 6;
// seller_fee_bps
// payment_address
// bool is_mutable
// update_autority (who can update name, description and uri if is_mutable = true)
}
136 changes: 136 additions & 0 deletions proto/bitsong/nft/v1beta1/query.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
syntax = "proto3";
package bitsong.nft.v1beta1;

import "gogoproto/gogo.proto";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

buf: missing dependency for gogoproto; build will fail.

Same as nft.proto: add gogoproto (and likely googleapis/cosmos) to buf deps or vendor the files.

Add to buf.yaml:

version: v2
deps:
  - buf.build/cosmos/gogo-proto
  - buf.build/googleapis/googleapis
  - buf.build/cosmos/cosmos-proto
  - buf.build/cosmos/cosmos-sdk
🧰 Tools
🪛 Buf (1.55.1)

4-4: import "gogoproto/gogo.proto": file does not exist

(COMPILE)

🤖 Prompt for AI Agents
In proto/bitsong/nft/v1beta1/query.proto around line 4, the file imports
gogoproto/gogo.proto but buf build is missing that dependency so the build will
fail; update the repository's buf.yaml to include the required buf deps
(examples: buf.build/cosmos/gogo-proto, buf.build/googleapis/googleapis and any
cosmos proto/sdk repos referenced) or vendor the gogoproto and googleapis files
into the proto tree, then re-run buf mod update and buf generate to ensure
imports resolve; also verify import paths in the .proto files match the added
deps.

import "google/api/annotations.proto";
import "cosmos/query/v1/query.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "bitsong/nft/v1beta1/nft.proto";
Comment on lines +4 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

buf build will fail: missing gogoproto (and likely cosmos/google) deps

Add deps to buf.yaml (or vendor).

Example buf.yaml:

version: v2
deps:
  - buf.build/cosmos/gogo-proto
  - buf.build/googleapis/googleapis
  - buf.build/cosmos/cosmos-proto
  - buf.build/cosmos/cosmos-sdk

Then:

  • buf mod update
  • buf generate
🧰 Tools
🪛 Buf (1.55.1)

4-4: import "gogoproto/gogo.proto": file does not exist

(COMPILE)

🤖 Prompt for AI Agents
In proto/bitsong/nft/v1beta1/query.proto around lines 4 to 8, the file imports
external proto packages (gogoproto, google api, cosmos query) but the
repository's buf.yaml lacks those deps causing buf build/generate to fail;
update buf.yaml to add the missing module dependencies (for example add entries
for gogo-proto, googleapis, and the relevant cosmos proto modules) or vendor
those protos, then run buf mod update and buf generate to fetch modules and
regenerate artifacts.


option go_package = "github.com/bitsongofficial/go-bitsong/x/nft/types";

service Query {
rpc Collection(QueryCollectionRequest) returns (QueryCollectionResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/collection/{collection}";
}

rpc OwnerOf(QueryOwnerOfRequest) returns (QueryOwnerOfResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/owner/{collection}/{token_id}";
}

rpc NumTokens(QueryNumTokensRequest) returns (QueryNumTokensResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/num_tokens/{collection}";
}

rpc NftInfo(QueryNftInfoRequest) returns (QueryNftInfoResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nft_info/{collection}/{token_id}";
}

rpc Nfts(QueryNftsRequest) returns (QueryNftsResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nfts/{collection}";
}

rpc AllNftsByOwner(QueryAllNftsByOwnerRequest) returns (QueryAllNftsByOwnerResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nfts_by_owner/{owner}";
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Missing planned RPC: AllNftInfo (nft + owner).

PR objectives include “allNftInfo”; it’s not defined here. Add an RPC and messages returning both Nft and Owner in one call.

Proto additions:

 service Query {
@@
   rpc NftInfo(QueryNftInfoRequest) returns (QueryNftInfoResponse) {
     option (cosmos.query.v1.module_query_safe) = true;
     option (google.api.http).get = "/bitsong/nft/v1beta1/nft_info/{collection}/{token_id}";
   }
+
+  rpc AllNftInfo(QueryAllNftInfoRequest) returns (QueryAllNftInfoResponse) {
+    option (cosmos.query.v1.module_query_safe) = true;
+    option (google.api.http).get = "/bitsong/nft/v1beta1/all_nft_info/{collection}/{token_id}";
+  }
@@
 }
+
+message QueryAllNftInfoRequest {
+  option (gogoproto.equal)           = false;
+  option (gogoproto.goproto_getters) = false;
+  string collection = 1;
+  string token_id   = 2;
+}
+
+message QueryAllNftInfoResponse {
+  option (gogoproto.equal)           = false;
+  option (gogoproto.goproto_getters) = false;
+  bitsong.nft.v1beta1.Nft nft = 1 [(gogoproto.nullable) = false];
+  string owner = 2;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
service Query {
rpc Collection(QueryCollectionRequest) returns (QueryCollectionResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/collection/{collection}";
}
rpc OwnerOf(QueryOwnerOfRequest) returns (QueryOwnerOfResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/owner/{collection}/{token_id}";
}
rpc NumTokens(QueryNumTokensRequest) returns (QueryNumTokensResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/num_tokens/{collection}";
}
rpc NftInfo(QueryNftInfoRequest) returns (QueryNftInfoResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nft_info/{collection}/{token_id}";
}
rpc Nfts(QueryNftsRequest) returns (QueryNftsResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nfts/{collection}";
}
rpc AllNftsByOwner(QueryAllNftsByOwnerRequest) returns (QueryAllNftsByOwnerResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nfts_by_owner/{owner}";
}
}
service Query {
rpc Collection(QueryCollectionRequest) returns (QueryCollectionResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/collection/{collection}";
}
rpc OwnerOf(QueryOwnerOfRequest) returns (QueryOwnerOfResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/owner/{collection}/{token_id}";
}
rpc NumTokens(QueryNumTokensRequest) returns (QueryNumTokensResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/num_tokens/{collection}";
}
rpc NftInfo(QueryNftInfoRequest) returns (QueryNftInfoResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nft_info/{collection}/{token_id}";
}
rpc AllNftInfo(QueryAllNftInfoRequest) returns (QueryAllNftInfoResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/all_nft_info/{collection}/{token_id}";
}
rpc Nfts(QueryNftsRequest) returns (QueryNftsResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nfts/{collection}";
}
rpc AllNftsByOwner(QueryAllNftsByOwnerRequest) returns (QueryAllNftsByOwnerResponse) {
option (cosmos.query.v1.module_query_safe) = true;
option (google.api.http).get = "/bitsong/nft/v1beta1/nfts_by_owner/{owner}";
}
}
message QueryAllNftInfoRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
string collection = 1;
string token_id = 2;
}
message QueryAllNftInfoResponse {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
bitsong.nft.v1beta1.Nft nft = 1 [(gogoproto.nullable) = false];
string owner = 2;
}
🤖 Prompt for AI Agents
In proto/bitsong/nft/v1beta1/query.proto around lines 12 to 42, the planned RPC
AllNftInfo is missing; add an RPC rpc AllNftInfo(QueryAllNftInfoRequest) returns
(QueryAllNftInfoResponse) with the same module_query_safe and google.api.http
GET pattern (e.g. "/bitsong/nft/v1beta1/all_nft_info/{collection}/{token_id}" or
appropriate path), and define the accompanying messages QueryAllNftInfoRequest
(collection, token_id) and QueryAllNftInfoResponse containing both the Nft info
message (e.g. Nft) and owner string (or reference to existing
QueryNftInfoResponse and Owner field) so a single call returns both NFT metadata
and owner together.


message QueryCollectionRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

string collection = 1;
}

message QueryCollectionResponse {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

bitsong.nft.v1beta1.Collection collection = 1;
}

message QueryOwnerOfRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

string collection = 1;
string token_id = 2;
}

message QueryOwnerOfResponse {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

string owner = 1;
}

message QueryNumTokensRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

string collection = 1;
}

message QueryNumTokensResponse {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

uint64 count = 1;
}

message QueryNftInfoRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

string collection = 1;
string token_id = 2;
}

message QueryNftInfoResponse {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

bitsong.nft.v1beta1.Nft nft = 1;
}

message QueryNftsRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

string collection = 1;

cosmos.base.query.v1beta1.PageRequest pagination = 2;
}

message QueryNftsResponse {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

repeated bitsong.nft.v1beta1.Nft nfts = 1 [
(gogoproto.nullable) = false
];

cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

message QueryAllNftsByOwnerRequest {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

string owner = 1;
}

message QueryAllNftsByOwnerResponse {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

repeated bitsong.nft.v1beta1.Nft nfts = 1 [
(gogoproto.nullable) = false
];
}
35 changes: 35 additions & 0 deletions third_party/proto/cosmos/query/v1/query.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
syntax = "proto3";

package cosmos.query.v1;

import "google/protobuf/descriptor.proto";

// TODO: once we fully migrate to protov2 the go_package needs to be updated.
// We need this right now because gogoproto codegen needs to import the extension.
option go_package = "github.com/cosmos/cosmos-sdk/types/query";

extend google.protobuf.MethodOptions {
// module_query_safe is set to true when the query is safe to be called from
// within the state machine, for example from another module's Keeper, via
// ADR-033 calls or from CosmWasm contracts.
// Concretely, it means that the query is:
// 1. deterministic: given a block height, returns the exact same response
// upon multiple calls; and doesn't introduce any state-machine-breaking
// changes across SDK patch version.
// 2. consumes gas correctly.
//
// If you are a module developer and want to add this annotation to one of
// your own queries, please make sure that the corresponding query:
// 1. is deterministic and won't introduce state-machine-breaking changes
// without a coordinated upgrade path,
// 2. has its gas tracked, to avoid the attack vector where no gas is
// accounted for on potentially high-computation queries.
//
// For queries that potentially consume a large amount of gas (for example
// those with pagination, if the pagination field is incorrectly set), we
// also recommend adding Protobuf comments to warn module developers
// consuming these queries.
//
// When set to true, the query can safely be called
bool module_query_safe = 11110001;
}
89 changes: 89 additions & 0 deletions x/nft/keeper/collection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package keeper

import (
"fmt"

"cosmossdk.io/math"
"github.com/bitsongofficial/go-bitsong/x/nft/types"
tmcrypto "github.com/cometbft/cometbft/crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
)

func (k Keeper) CreateCollection(ctx sdk.Context, creator sdk.AccAddress, coll types.Collection) (denom string, err error) {
denom, err = k.validateCollectionDenom(ctx, creator, coll.Symbol)
if err != nil {
return "", err
}

// TODO: charge fee

if err := k.setCollection(ctx, denom, coll); err != nil {
return "", err
}

return denom, nil
}

func (k Keeper) GetSupply(ctx sdk.Context, denom string) math.Int {
supply, err := k.Supply.Get(ctx, denom)
if err != nil {
return math.ZeroInt()
}

return supply
}

func (k Keeper) HasSupply(ctx sdk.Context, denom string) bool {
has, err := k.Supply.Has(ctx, denom)
return has && err == nil
}

func (k Keeper) setSupply(ctx sdk.Context, denom string, supply math.Int) error {
return k.Supply.Set(ctx, denom, supply)
}

func (k Keeper) incrementSupply(ctx sdk.Context, denom string) error {
supply := k.GetSupply(ctx, denom)
supply = supply.Add(math.NewInt(1))

return k.setSupply(ctx, denom, supply)
}

func (k Keeper) createCollectionDenom(creator sdk.AccAddress, symbol string) string {
// TODO: if necessary add a salt field

bz := []byte(fmt.Sprintf("%s/%s", creator.String(), symbol))
return fmt.Sprintf("nft%x", tmcrypto.AddressHash(bz))
}

func (k Keeper) validateCollectionDenom(ctx sdk.Context, creator sdk.AccAddress, symbol string) (string, error) {
denom := k.createCollectionDenom(creator, symbol)

if err := sdk.ValidateDenom(denom); err != nil {
return "", err
}

if k.HasCollection(ctx, denom) {
return "", types.ErrCollectionAlreadyExists
}

return denom, nil
}

func (k Keeper) setCollection(ctx sdk.Context, denom string, coll types.Collection) error {
return k.Collections.Set(ctx, denom, coll)
}

func (k Keeper) getCollection(ctx sdk.Context, denom string) (types.Collection, error) {
coll, err := k.Collections.Get(ctx, denom)
if err != nil {
return types.Collection{}, types.ErrCollectionNotFound
}

return coll, nil
}

func (k Keeper) HasCollection(ctx sdk.Context, denom string) bool {
has, err := k.Collections.Has(ctx, denom)
return has && err == nil
}
21 changes: 21 additions & 0 deletions x/nft/keeper/collection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package keeper

import (
"testing"

"github.com/cometbft/cometbft/crypto/tmhash"
sdk "github.com/cosmos/cosmos-sdk/types"
)

func TestKeeper_createCollectionDenom(t *testing.T) {
creator := sdk.AccAddress(tmhash.SumTruncated([]byte("creator")))
symbol := "MYNFT"

expectedDenom := "nft653AF6715F0C4EE2E24A54B191EBD0AD5DB33723"
k := Keeper{}

denom := k.createCollectionDenom(creator, symbol)
if denom != expectedDenom {
t.Errorf("expected %s, got %s", expectedDenom, denom)
}
}
Loading