From c8f673157d325da20085482b43c8985d77fed140 Mon Sep 17 00:00:00 2001
From: Ethan Frey <ethanfrey@users.noreply.github.com>
Date: Thu, 23 Sep 2021 10:07:05 +0200
Subject: [PATCH 1/6] Start cw721-metadata-uri example

---
 Cargo.lock                                    | 16 ++++++
 contracts/cw721-base/src/msg.rs               |  5 +-
 contracts/cw721-metadata-uri/.cargo/config    |  5 ++
 contracts/cw721-metadata-uri/Cargo.toml       | 39 ++++++++++++++
 contracts/cw721-metadata-uri/NOTICE           | 14 +++++
 .../cw721-metadata-uri/examples/schema.rs     | 38 ++++++++++++++
 contracts/cw721-metadata-uri/src/lib.rs       | 52 +++++++++++++++++++
 7 files changed, 167 insertions(+), 2 deletions(-)
 create mode 100644 contracts/cw721-metadata-uri/.cargo/config
 create mode 100644 contracts/cw721-metadata-uri/Cargo.toml
 create mode 100644 contracts/cw721-metadata-uri/NOTICE
 create mode 100644 contracts/cw721-metadata-uri/examples/schema.rs
 create mode 100644 contracts/cw721-metadata-uri/src/lib.rs

diff --git a/Cargo.lock b/Cargo.lock
index 285ea43c0..191cfb3a8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -656,6 +656,22 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw721-metadata-uri"
+version = "0.9.0"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cw-storage-plus 0.9.0",
+ "cw0 0.9.0",
+ "cw2",
+ "cw721",
+ "cw721-base",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "der"
 version = "0.4.1"
diff --git a/contracts/cw721-base/src/msg.rs b/contracts/cw721-base/src/msg.rs
index a2c5d0aab..750fff6c1 100644
--- a/contracts/cw721-base/src/msg.rs
+++ b/contracts/cw721-base/src/msg.rs
@@ -1,8 +1,9 @@
-use cosmwasm_std::Binary;
-use cw721::Expiration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
+use cosmwasm_std::Binary;
+use cw721::Expiration;
+
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 pub struct InstantiateMsg {
     /// Name of the NFT contract
diff --git a/contracts/cw721-metadata-uri/.cargo/config b/contracts/cw721-metadata-uri/.cargo/config
new file mode 100644
index 000000000..7d1a066c8
--- /dev/null
+++ b/contracts/cw721-metadata-uri/.cargo/config
@@ -0,0 +1,5 @@
+[alias]
+wasm = "build --release --target wasm32-unknown-unknown"
+wasm-debug = "build --target wasm32-unknown-unknown"
+unit-test = "test --lib"
+schema = "run --example schema"
diff --git a/contracts/cw721-metadata-uri/Cargo.toml b/contracts/cw721-metadata-uri/Cargo.toml
new file mode 100644
index 000000000..31fa179c3
--- /dev/null
+++ b/contracts/cw721-metadata-uri/Cargo.toml
@@ -0,0 +1,39 @@
+[package]
+name = "cw721-metadata-uri"
+version = "0.9.0"
+authors = ["Ethan Frey <ethanfrey@users.noreply.github.com>"]
+edition = "2018"
+description = "Example extending CW721 NFT with ERC20 like metadata_uri"
+license = "Apache-2.0"
+repository = "https://github.com/CosmWasm/cw-plus"
+homepage = "https://cosmwasm.com"
+documentation = "https://docs.cosmwasm.com"
+
+exclude = [
+  # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
+  "artifacts/*",
+]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+# for more explicit tests, cargo test --features=backtraces
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all instantiate/execute/query exports
+library = []
+
+[dependencies]
+cw0 = { path = "../../packages/cw0", version = "0.9.0" }
+cw2 = { path = "../../packages/cw2", version = "0.9.0" }
+cw721 = { path = "../../packages/cw721", version = "0.9.0" }
+cw721-base = { path = "../cw721-base", version = "0.9.0", features = ["library"] }
+cw-storage-plus = { path = "../../packages/storage-plus", version = "0.9.0" }
+cosmwasm-std = { version = "0.16.0" }
+schemars = "0.8.1"
+serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+thiserror = { version = "1.0.23" }
+
+[dev-dependencies]
+cosmwasm-schema = { version = "0.16.0" }
diff --git a/contracts/cw721-metadata-uri/NOTICE b/contracts/cw721-metadata-uri/NOTICE
new file mode 100644
index 000000000..c03203270
--- /dev/null
+++ b/contracts/cw721-metadata-uri/NOTICE
@@ -0,0 +1,14 @@
+Cw721_base
+Copyright (C) 2020 Confio OÜ
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/contracts/cw721-metadata-uri/examples/schema.rs b/contracts/cw721-metadata-uri/examples/schema.rs
new file mode 100644
index 000000000..f9c424023
--- /dev/null
+++ b/contracts/cw721-metadata-uri/examples/schema.rs
@@ -0,0 +1,38 @@
+use std::env::current_dir;
+use std::fs::create_dir_all;
+
+use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for};
+
+use cw721::{
+    AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, NftInfoResponse,
+    NumTokensResponse, OwnerOfResponse, TokensResponse,
+};
+use cw721_base::entry::Extension;
+use cw721_base::msg::{ExecuteMsg, InstantiateMsg, MinterResponse, QueryMsg};
+
+fn main() {
+    let mut out_dir = current_dir().unwrap();
+    out_dir.push("schema");
+    create_dir_all(&out_dir).unwrap();
+    remove_schemas(&out_dir).unwrap();
+
+    export_schema(&schema_for!(InstantiateMsg), &out_dir);
+    export_schema_with_title(&schema_for!(ExecuteMsg<Extension>), &out_dir, "ExecuteMsg");
+    export_schema(&schema_for!(QueryMsg), &out_dir);
+    export_schema_with_title(
+        &schema_for!(AllNftInfoResponse<Extension>),
+        &out_dir,
+        "AllNftInfoResponse",
+    );
+    export_schema(&schema_for!(ApprovedForAllResponse), &out_dir);
+    export_schema(&schema_for!(ContractInfoResponse), &out_dir);
+    export_schema(&schema_for!(MinterResponse), &out_dir);
+    export_schema_with_title(
+        &schema_for!(NftInfoResponse<Extension>),
+        &out_dir,
+        "NftInfoResponse",
+    );
+    export_schema(&schema_for!(NumTokensResponse), &out_dir);
+    export_schema(&schema_for!(OwnerOfResponse), &out_dir);
+    export_schema(&schema_for!(TokensResponse), &out_dir);
+}
diff --git a/contracts/cw721-metadata-uri/src/lib.rs b/contracts/cw721-metadata-uri/src/lib.rs
new file mode 100644
index 000000000..aec41f9da
--- /dev/null
+++ b/contracts/cw721-metadata-uri/src/lib.rs
@@ -0,0 +1,52 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+pub use cw721_base::{
+    ContractError, ExecuteMsg, InstantiateMsg, MintMsg, MinterResponse, QueryMsg,
+};
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct Extension {
+    pub metadata_uri: String,
+}
+
+#[cfg(not(feature = "library"))]
+pub mod entry {
+    use super::*;
+
+    pub use cw721_base::Cw721Contract;
+
+    use cosmwasm_std::entry_point;
+    use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
+
+    // This is a simple type to let us handle empty extensions
+
+    // This makes a conscious choice on the various generics used by the contract
+    #[entry_point]
+    pub fn instantiate(
+        deps: DepsMut,
+        env: Env,
+        info: MessageInfo,
+        msg: InstantiateMsg,
+    ) -> StdResult<Response> {
+        let tract = Cw721Contract::<Extension, Empty>::default();
+        tract.instantiate(deps, env, info, msg)
+    }
+
+    #[entry_point]
+    pub fn execute(
+        deps: DepsMut,
+        env: Env,
+        info: MessageInfo,
+        msg: ExecuteMsg<Extension>,
+    ) -> Result<Response, ContractError> {
+        let tract = Cw721Contract::<Extension, Empty>::default();
+        tract.execute(deps, env, info, msg)
+    }
+
+    #[entry_point]
+    pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+        let tract = Cw721Contract::<Extension, Empty>::default();
+        tract.query(deps, env, msg)
+    }
+}

From 07b904f3b3bc9b0e09243dada0d66a43c916ff1e Mon Sep 17 00:00:00 2001
From: Ethan Frey <ethanfrey@users.noreply.github.com>
Date: Thu, 23 Sep 2021 10:19:21 +0200
Subject: [PATCH 2/6] Cleanup and generate schema

---
 contracts/cw721-base/src/lib.rs               |  20 +-
 .../cw721-metadata-uri/examples/schema.rs     |   5 +-
 .../schema/all_nft_info_response.json         | 172 ++++++++++
 .../schema/approved_for_all_response.json     |  97 ++++++
 .../schema/contract_info_response.json        |  17 +
 .../schema/execute_msg.json                   | 306 ++++++++++++++++++
 .../schema/instantiate_msg.json               |  24 ++
 .../schema/minter_response.json               |  14 +
 .../schema/nft_info_response.json             |  48 +++
 .../schema/num_tokens_response.json           |  15 +
 .../schema/owner_of_response.json             | 103 ++++++
 .../cw721-metadata-uri/schema/query_msg.json  | 227 +++++++++++++
 .../schema/tokens_response.json               |  17 +
 contracts/cw721-metadata-uri/src/lib.rs       |  23 +-
 14 files changed, 1062 insertions(+), 26 deletions(-)
 create mode 100644 contracts/cw721-metadata-uri/schema/all_nft_info_response.json
 create mode 100644 contracts/cw721-metadata-uri/schema/approved_for_all_response.json
 create mode 100644 contracts/cw721-metadata-uri/schema/contract_info_response.json
 create mode 100644 contracts/cw721-metadata-uri/schema/execute_msg.json
 create mode 100644 contracts/cw721-metadata-uri/schema/instantiate_msg.json
 create mode 100644 contracts/cw721-metadata-uri/schema/minter_response.json
 create mode 100644 contracts/cw721-metadata-uri/schema/nft_info_response.json
 create mode 100644 contracts/cw721-metadata-uri/schema/num_tokens_response.json
 create mode 100644 contracts/cw721-metadata-uri/schema/owner_of_response.json
 create mode 100644 contracts/cw721-metadata-uri/schema/query_msg.json
 create mode 100644 contracts/cw721-metadata-uri/schema/tokens_response.json

diff --git a/contracts/cw721-base/src/lib.rs b/contracts/cw721-base/src/lib.rs
index e5a6e35ce..9d9a4ab8e 100644
--- a/contracts/cw721-base/src/lib.rs
+++ b/contracts/cw721-base/src/lib.rs
@@ -9,10 +9,10 @@ pub use crate::error::ContractError;
 pub use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg, MinterResponse, QueryMsg};
 pub use crate::state::Cw721Contract;
 
+#[cfg(not(feature = "library"))]
 pub mod entry {
     use super::*;
 
-    #[cfg(not(feature = "library"))]
     use cosmwasm_std::entry_point;
     use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
 
@@ -20,31 +20,31 @@ pub mod entry {
     pub type Extension = Option<Empty>;
 
     // This makes a conscious choice on the various generics used by the contract
-    #[cfg_attr(not(feature = "library"), entry_point)]
+    #[entry_point]
     pub fn instantiate(
         deps: DepsMut,
         env: Env,
         info: MessageInfo,
-        msg: msg::InstantiateMsg,
+        msg: InstantiateMsg,
     ) -> StdResult<Response> {
-        let tract = state::Cw721Contract::<Extension, Empty>::default();
+        let tract = Cw721Contract::<Extension, Empty>::default();
         tract.instantiate(deps, env, info, msg)
     }
 
-    #[cfg_attr(not(feature = "library"), entry_point)]
+    #[entry_point]
     pub fn execute(
         deps: DepsMut,
         env: Env,
         info: MessageInfo,
-        msg: msg::ExecuteMsg<Extension>,
+        msg: ExecuteMsg<Extension>,
     ) -> Result<Response, ContractError> {
-        let tract = state::Cw721Contract::<Extension, Empty>::default();
+        let tract = Cw721Contract::<Extension, Empty>::default();
         tract.execute(deps, env, info, msg)
     }
 
-    #[cfg_attr(not(feature = "library"), entry_point)]
-    pub fn query(deps: Deps, env: Env, msg: msg::QueryMsg) -> StdResult<Binary> {
-        let tract = state::Cw721Contract::<Extension, Empty>::default();
+    #[entry_point]
+    pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+        let tract = Cw721Contract::<Extension, Empty>::default();
         tract.query(deps, env, msg)
     }
 }
diff --git a/contracts/cw721-metadata-uri/examples/schema.rs b/contracts/cw721-metadata-uri/examples/schema.rs
index f9c424023..305267994 100644
--- a/contracts/cw721-metadata-uri/examples/schema.rs
+++ b/contracts/cw721-metadata-uri/examples/schema.rs
@@ -7,8 +7,7 @@ use cw721::{
     AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, NftInfoResponse,
     NumTokensResponse, OwnerOfResponse, TokensResponse,
 };
-use cw721_base::entry::Extension;
-use cw721_base::msg::{ExecuteMsg, InstantiateMsg, MinterResponse, QueryMsg};
+use cw721_metadata_uri::{ExecuteMsg, Extension, InstantiateMsg, MinterResponse, QueryMsg};
 
 fn main() {
     let mut out_dir = current_dir().unwrap();
@@ -17,7 +16,7 @@ fn main() {
     remove_schemas(&out_dir).unwrap();
 
     export_schema(&schema_for!(InstantiateMsg), &out_dir);
-    export_schema_with_title(&schema_for!(ExecuteMsg<Extension>), &out_dir, "ExecuteMsg");
+    export_schema_with_title(&schema_for!(ExecuteMsg), &out_dir, "ExecuteMsg");
     export_schema(&schema_for!(QueryMsg), &out_dir);
     export_schema_with_title(
         &schema_for!(AllNftInfoResponse<Extension>),
diff --git a/contracts/cw721-metadata-uri/schema/all_nft_info_response.json b/contracts/cw721-metadata-uri/schema/all_nft_info_response.json
new file mode 100644
index 000000000..3fab93565
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/all_nft_info_response.json
@@ -0,0 +1,172 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "AllNftInfoResponse",
+  "type": "object",
+  "required": [
+    "access",
+    "info"
+  ],
+  "properties": {
+    "access": {
+      "description": "Who can transfer the token",
+      "allOf": [
+        {
+          "$ref": "#/definitions/OwnerOfResponse"
+        }
+      ]
+    },
+    "info": {
+      "description": "Data on the token itself,",
+      "allOf": [
+        {
+          "$ref": "#/definitions/NftInfoResponse_for_Extension"
+        }
+      ]
+    }
+  },
+  "definitions": {
+    "Approval": {
+      "type": "object",
+      "required": [
+        "expires",
+        "spender"
+      ],
+      "properties": {
+        "expires": {
+          "description": "When the Approval expires (maybe Expiration::never)",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Expiration"
+            }
+          ]
+        },
+        "spender": {
+          "description": "Account that can transfer/send the token",
+          "type": "string"
+        }
+      }
+    },
+    "Expiration": {
+      "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+      "anyOf": [
+        {
+          "description": "AtHeight will expire when `env.block.height` >= height",
+          "type": "object",
+          "required": [
+            "at_height"
+          ],
+          "properties": {
+            "at_height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "AtTime will expire when `env.block.time` >= time",
+          "type": "object",
+          "required": [
+            "at_time"
+          ],
+          "properties": {
+            "at_time": {
+              "$ref": "#/definitions/Timestamp"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Never will never expire. Used to express the empty variant",
+          "type": "object",
+          "required": [
+            "never"
+          ],
+          "properties": {
+            "never": {
+              "type": "object"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Extension": {
+      "type": "object",
+      "required": [
+        "metadata_uri"
+      ],
+      "properties": {
+        "metadata_uri": {
+          "type": "string"
+        }
+      }
+    },
+    "NftInfoResponse_for_Extension": {
+      "type": "object",
+      "required": [
+        "description",
+        "extension",
+        "name"
+      ],
+      "properties": {
+        "description": {
+          "description": "Describes the asset to which this NFT represents",
+          "type": "string"
+        },
+        "extension": {
+          "description": "You can add any custom metadata here when you extend cw721-base",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Extension"
+            }
+          ]
+        },
+        "image": {
+          "description": "\"A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive. TODO: Use https://docs.rs/url_serde for type-safety",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "name": {
+          "description": "Identifies the asset to which this NFT represents",
+          "type": "string"
+        }
+      }
+    },
+    "OwnerOfResponse": {
+      "type": "object",
+      "required": [
+        "approvals",
+        "owner"
+      ],
+      "properties": {
+        "approvals": {
+          "description": "If set this address is approved to transfer/send the token as well",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Approval"
+          }
+        },
+        "owner": {
+          "description": "Owner of the token",
+          "type": "string"
+        }
+      }
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/approved_for_all_response.json b/contracts/cw721-metadata-uri/schema/approved_for_all_response.json
new file mode 100644
index 000000000..f405e3581
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/approved_for_all_response.json
@@ -0,0 +1,97 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ApprovedForAllResponse",
+  "type": "object",
+  "required": [
+    "operators"
+  ],
+  "properties": {
+    "operators": {
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Approval"
+      }
+    }
+  },
+  "definitions": {
+    "Approval": {
+      "type": "object",
+      "required": [
+        "expires",
+        "spender"
+      ],
+      "properties": {
+        "expires": {
+          "description": "When the Approval expires (maybe Expiration::never)",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Expiration"
+            }
+          ]
+        },
+        "spender": {
+          "description": "Account that can transfer/send the token",
+          "type": "string"
+        }
+      }
+    },
+    "Expiration": {
+      "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+      "anyOf": [
+        {
+          "description": "AtHeight will expire when `env.block.height` >= height",
+          "type": "object",
+          "required": [
+            "at_height"
+          ],
+          "properties": {
+            "at_height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "AtTime will expire when `env.block.time` >= time",
+          "type": "object",
+          "required": [
+            "at_time"
+          ],
+          "properties": {
+            "at_time": {
+              "$ref": "#/definitions/Timestamp"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Never will never expire. Used to express the empty variant",
+          "type": "object",
+          "required": [
+            "never"
+          ],
+          "properties": {
+            "never": {
+              "type": "object"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/contract_info_response.json b/contracts/cw721-metadata-uri/schema/contract_info_response.json
new file mode 100644
index 000000000..a16712589
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/contract_info_response.json
@@ -0,0 +1,17 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ContractInfoResponse",
+  "type": "object",
+  "required": [
+    "name",
+    "symbol"
+  ],
+  "properties": {
+    "name": {
+      "type": "string"
+    },
+    "symbol": {
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/execute_msg.json b/contracts/cw721-metadata-uri/schema/execute_msg.json
new file mode 100644
index 000000000..68630375f
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/execute_msg.json
@@ -0,0 +1,306 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ExecuteMsg",
+  "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.",
+  "anyOf": [
+    {
+      "description": "Transfer is a base message to move a token to another account without triggering actions",
+      "type": "object",
+      "required": [
+        "transfer_nft"
+      ],
+      "properties": {
+        "transfer_nft": {
+          "type": "object",
+          "required": [
+            "recipient",
+            "token_id"
+          ],
+          "properties": {
+            "recipient": {
+              "type": "string"
+            },
+            "token_id": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.",
+      "type": "object",
+      "required": [
+        "send_nft"
+      ],
+      "properties": {
+        "send_nft": {
+          "type": "object",
+          "required": [
+            "contract",
+            "msg",
+            "token_id"
+          ],
+          "properties": {
+            "contract": {
+              "type": "string"
+            },
+            "msg": {
+              "$ref": "#/definitions/Binary"
+            },
+            "token_id": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit",
+      "type": "object",
+      "required": [
+        "approve"
+      ],
+      "properties": {
+        "approve": {
+          "type": "object",
+          "required": [
+            "spender",
+            "token_id"
+          ],
+          "properties": {
+            "expires": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/Expiration"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "spender": {
+              "type": "string"
+            },
+            "token_id": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "Remove previously granted Approval",
+      "type": "object",
+      "required": [
+        "revoke"
+      ],
+      "properties": {
+        "revoke": {
+          "type": "object",
+          "required": [
+            "spender",
+            "token_id"
+          ],
+          "properties": {
+            "spender": {
+              "type": "string"
+            },
+            "token_id": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit",
+      "type": "object",
+      "required": [
+        "approve_all"
+      ],
+      "properties": {
+        "approve_all": {
+          "type": "object",
+          "required": [
+            "operator"
+          ],
+          "properties": {
+            "expires": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/Expiration"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "operator": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "Remove previously granted ApproveAll permission",
+      "type": "object",
+      "required": [
+        "revoke_all"
+      ],
+      "properties": {
+        "revoke_all": {
+          "type": "object",
+          "required": [
+            "operator"
+          ],
+          "properties": {
+            "operator": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "Mint a new NFT, can only be called by the contract minter",
+      "type": "object",
+      "required": [
+        "mint"
+      ],
+      "properties": {
+        "mint": {
+          "$ref": "#/definitions/MintMsg_for_Extension"
+        }
+      },
+      "additionalProperties": false
+    }
+  ],
+  "definitions": {
+    "Binary": {
+      "description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>",
+      "type": "string"
+    },
+    "Expiration": {
+      "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+      "anyOf": [
+        {
+          "description": "AtHeight will expire when `env.block.height` >= height",
+          "type": "object",
+          "required": [
+            "at_height"
+          ],
+          "properties": {
+            "at_height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "AtTime will expire when `env.block.time` >= time",
+          "type": "object",
+          "required": [
+            "at_time"
+          ],
+          "properties": {
+            "at_time": {
+              "$ref": "#/definitions/Timestamp"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Never will never expire. Used to express the empty variant",
+          "type": "object",
+          "required": [
+            "never"
+          ],
+          "properties": {
+            "never": {
+              "type": "object"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Extension": {
+      "type": "object",
+      "required": [
+        "metadata_uri"
+      ],
+      "properties": {
+        "metadata_uri": {
+          "type": "string"
+        }
+      }
+    },
+    "MintMsg_for_Extension": {
+      "type": "object",
+      "required": [
+        "extension",
+        "name",
+        "owner",
+        "token_id"
+      ],
+      "properties": {
+        "description": {
+          "description": "Describes the asset to which this NFT represents (may be empty)",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "extension": {
+          "description": "Any custom extension used by this contract",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Extension"
+            }
+          ]
+        },
+        "image": {
+          "description": "A URI pointing to an image representing the asset",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "name": {
+          "description": "Identifies the asset to which this NFT represents",
+          "type": "string"
+        },
+        "owner": {
+          "description": "The owner of the newly minter NFT",
+          "type": "string"
+        },
+        "token_id": {
+          "description": "Unique ID of the NFT",
+          "type": "string"
+        }
+      }
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/instantiate_msg.json b/contracts/cw721-metadata-uri/schema/instantiate_msg.json
new file mode 100644
index 000000000..b024c82c1
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/instantiate_msg.json
@@ -0,0 +1,24 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "InstantiateMsg",
+  "type": "object",
+  "required": [
+    "minter",
+    "name",
+    "symbol"
+  ],
+  "properties": {
+    "minter": {
+      "description": "The minter is the only one who can create new NFTs. This is designed for a base NFT that is controlled by an external program or contract. You will likely replace this with custom logic in custom NFTs",
+      "type": "string"
+    },
+    "name": {
+      "description": "Name of the NFT contract",
+      "type": "string"
+    },
+    "symbol": {
+      "description": "Symbol of the NFT contract",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/minter_response.json b/contracts/cw721-metadata-uri/schema/minter_response.json
new file mode 100644
index 000000000..a20e0d767
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/minter_response.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "MinterResponse",
+  "description": "Shows who can mint these tokens",
+  "type": "object",
+  "required": [
+    "minter"
+  ],
+  "properties": {
+    "minter": {
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/nft_info_response.json b/contracts/cw721-metadata-uri/schema/nft_info_response.json
new file mode 100644
index 000000000..bc8d6da58
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/nft_info_response.json
@@ -0,0 +1,48 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "NftInfoResponse",
+  "type": "object",
+  "required": [
+    "description",
+    "extension",
+    "name"
+  ],
+  "properties": {
+    "description": {
+      "description": "Describes the asset to which this NFT represents",
+      "type": "string"
+    },
+    "extension": {
+      "description": "You can add any custom metadata here when you extend cw721-base",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Extension"
+        }
+      ]
+    },
+    "image": {
+      "description": "\"A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive. TODO: Use https://docs.rs/url_serde for type-safety",
+      "type": [
+        "string",
+        "null"
+      ]
+    },
+    "name": {
+      "description": "Identifies the asset to which this NFT represents",
+      "type": "string"
+    }
+  },
+  "definitions": {
+    "Extension": {
+      "type": "object",
+      "required": [
+        "metadata_uri"
+      ],
+      "properties": {
+        "metadata_uri": {
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/num_tokens_response.json b/contracts/cw721-metadata-uri/schema/num_tokens_response.json
new file mode 100644
index 000000000..4647c23aa
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/num_tokens_response.json
@@ -0,0 +1,15 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "NumTokensResponse",
+  "type": "object",
+  "required": [
+    "count"
+  ],
+  "properties": {
+    "count": {
+      "type": "integer",
+      "format": "uint64",
+      "minimum": 0.0
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/owner_of_response.json b/contracts/cw721-metadata-uri/schema/owner_of_response.json
new file mode 100644
index 000000000..2bd34b01b
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/owner_of_response.json
@@ -0,0 +1,103 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "OwnerOfResponse",
+  "type": "object",
+  "required": [
+    "approvals",
+    "owner"
+  ],
+  "properties": {
+    "approvals": {
+      "description": "If set this address is approved to transfer/send the token as well",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Approval"
+      }
+    },
+    "owner": {
+      "description": "Owner of the token",
+      "type": "string"
+    }
+  },
+  "definitions": {
+    "Approval": {
+      "type": "object",
+      "required": [
+        "expires",
+        "spender"
+      ],
+      "properties": {
+        "expires": {
+          "description": "When the Approval expires (maybe Expiration::never)",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Expiration"
+            }
+          ]
+        },
+        "spender": {
+          "description": "Account that can transfer/send the token",
+          "type": "string"
+        }
+      }
+    },
+    "Expiration": {
+      "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+      "anyOf": [
+        {
+          "description": "AtHeight will expire when `env.block.height` >= height",
+          "type": "object",
+          "required": [
+            "at_height"
+          ],
+          "properties": {
+            "at_height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "AtTime will expire when `env.block.time` >= time",
+          "type": "object",
+          "required": [
+            "at_time"
+          ],
+          "properties": {
+            "at_time": {
+              "$ref": "#/definitions/Timestamp"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Never will never expire. Used to express the empty variant",
+          "type": "object",
+          "required": [
+            "never"
+          ],
+          "properties": {
+            "never": {
+              "type": "object"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/schema/query_msg.json b/contracts/cw721-metadata-uri/schema/query_msg.json
new file mode 100644
index 000000000..471924bf0
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/query_msg.json
@@ -0,0 +1,227 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "QueryMsg",
+  "anyOf": [
+    {
+      "description": "Return the owner of the given token, error if token does not exist Return type: OwnerOfResponse",
+      "type": "object",
+      "required": [
+        "owner_of"
+      ],
+      "properties": {
+        "owner_of": {
+          "type": "object",
+          "required": [
+            "token_id"
+          ],
+          "properties": {
+            "include_expired": {
+              "description": "unset or false will filter out expired approvals, you must set to true to see them",
+              "type": [
+                "boolean",
+                "null"
+              ]
+            },
+            "token_id": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "List all operators that can access all of the owner's tokens Return type: `ApprovedForAllResponse`",
+      "type": "object",
+      "required": [
+        "approved_for_all"
+      ],
+      "properties": {
+        "approved_for_all": {
+          "type": "object",
+          "required": [
+            "owner"
+          ],
+          "properties": {
+            "include_expired": {
+              "description": "unset or false will filter out expired items, you must set to true to see them",
+              "type": [
+                "boolean",
+                "null"
+              ]
+            },
+            "limit": {
+              "type": [
+                "integer",
+                "null"
+              ],
+              "format": "uint32",
+              "minimum": 0.0
+            },
+            "owner": {
+              "type": "string"
+            },
+            "start_after": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "Total number of tokens issued",
+      "type": "object",
+      "required": [
+        "num_tokens"
+      ],
+      "properties": {
+        "num_tokens": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "With MetaData Extension. Returns top-level metadata about the contract: `ContractInfoResponse`",
+      "type": "object",
+      "required": [
+        "contract_info"
+      ],
+      "properties": {
+        "contract_info": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract: `NftInfoResponse`",
+      "type": "object",
+      "required": [
+        "nft_info"
+      ],
+      "properties": {
+        "nft_info": {
+          "type": "object",
+          "required": [
+            "token_id"
+          ],
+          "properties": {
+            "token_id": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients: `AllNftInfo`",
+      "type": "object",
+      "required": [
+        "all_nft_info"
+      ],
+      "properties": {
+        "all_nft_info": {
+          "type": "object",
+          "required": [
+            "token_id"
+          ],
+          "properties": {
+            "include_expired": {
+              "description": "unset or false will filter out expired approvals, you must set to true to see them",
+              "type": [
+                "boolean",
+                "null"
+              ]
+            },
+            "token_id": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset. Return type: TokensResponse.",
+      "type": "object",
+      "required": [
+        "tokens"
+      ],
+      "properties": {
+        "tokens": {
+          "type": "object",
+          "required": [
+            "owner"
+          ],
+          "properties": {
+            "limit": {
+              "type": [
+                "integer",
+                "null"
+              ],
+              "format": "uint32",
+              "minimum": 0.0
+            },
+            "owner": {
+              "type": "string"
+            },
+            "start_after": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract. Return type: TokensResponse.",
+      "type": "object",
+      "required": [
+        "all_tokens"
+      ],
+      "properties": {
+        "all_tokens": {
+          "type": "object",
+          "properties": {
+            "limit": {
+              "type": [
+                "integer",
+                "null"
+              ],
+              "format": "uint32",
+              "minimum": 0.0
+            },
+            "start_after": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "minter"
+      ],
+      "properties": {
+        "minter": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    }
+  ]
+}
diff --git a/contracts/cw721-metadata-uri/schema/tokens_response.json b/contracts/cw721-metadata-uri/schema/tokens_response.json
new file mode 100644
index 000000000..b8e3d75b5
--- /dev/null
+++ b/contracts/cw721-metadata-uri/schema/tokens_response.json
@@ -0,0 +1,17 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "TokensResponse",
+  "type": "object",
+  "required": [
+    "tokens"
+  ],
+  "properties": {
+    "tokens": {
+      "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.",
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    }
+  }
+}
diff --git a/contracts/cw721-metadata-uri/src/lib.rs b/contracts/cw721-metadata-uri/src/lib.rs
index aec41f9da..9111d4a20 100644
--- a/contracts/cw721-metadata-uri/src/lib.rs
+++ b/contracts/cw721-metadata-uri/src/lib.rs
@@ -1,23 +1,23 @@
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
-pub use cw721_base::{
-    ContractError, ExecuteMsg, InstantiateMsg, MintMsg, MinterResponse, QueryMsg,
-};
+use cosmwasm_std::Empty;
+pub use cw721_base::{ContractError, InstantiateMsg, MintMsg, MinterResponse, QueryMsg};
+pub type ExecuteMsg = cw721_base::ExecuteMsg<Extension>;
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 pub struct Extension {
     pub metadata_uri: String,
 }
 
+pub type Cw721MetadataContract<'a> = cw721_base::Cw721Contract<'a, Extension, Empty>;
+
 #[cfg(not(feature = "library"))]
 pub mod entry {
     use super::*;
 
-    pub use cw721_base::Cw721Contract;
-
     use cosmwasm_std::entry_point;
-    use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
+    use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
 
     // This is a simple type to let us handle empty extensions
 
@@ -29,8 +29,7 @@ pub mod entry {
         info: MessageInfo,
         msg: InstantiateMsg,
     ) -> StdResult<Response> {
-        let tract = Cw721Contract::<Extension, Empty>::default();
-        tract.instantiate(deps, env, info, msg)
+        Cw721MetadataContract::default().instantiate(deps, env, info, msg)
     }
 
     #[entry_point]
@@ -38,15 +37,13 @@ pub mod entry {
         deps: DepsMut,
         env: Env,
         info: MessageInfo,
-        msg: ExecuteMsg<Extension>,
+        msg: ExecuteMsg,
     ) -> Result<Response, ContractError> {
-        let tract = Cw721Contract::<Extension, Empty>::default();
-        tract.execute(deps, env, info, msg)
+        Cw721MetadataContract::default().execute(deps, env, info, msg)
     }
 
     #[entry_point]
     pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
-        let tract = Cw721Contract::<Extension, Empty>::default();
-        tract.query(deps, env, msg)
+        Cw721MetadataContract::default().query(deps, env, msg)
     }
 }

From 1e0d338e74ed39b63918bf158ad21a2422da5157 Mon Sep 17 00:00:00 2001
From: Ethan Frey <ethanfrey@users.noreply.github.com>
Date: Thu, 23 Sep 2021 10:24:22 +0200
Subject: [PATCH 3/6] Add cw721-metadata-uri to CI

---
 .circleci/config.yml | 38 ++++++++++++++++++++++++++++++++++++++
 Cargo.toml           |  8 ++++++++
 2 files changed, 46 insertions(+)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 54e1c520f..9b631ef55 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -17,6 +17,7 @@ workflows:
       - contract_cw20_staking
       - contract_cw20_merkle_airdrop
       - contract_cw721_base
+      - contract_cw721_metadata_uri
       - contract_cw1155_base
       - package_controllers
       - package_cw0
@@ -548,6 +549,43 @@ jobs:
             - target
           key: cargocache-cw721-base-rust:1.53.0-{{ checksum "~/project/Cargo.lock" }}
 
+
+  contract_cw721_metadata_uri:
+    docker:
+      - image: rust:1.53.0
+    working_directory: ~/project/contracts/cw721-metadata-uri
+    steps:
+      - checkout:
+          path: ~/project
+      - run:
+          name: Version information
+          command: rustc --version; cargo --version; rustup --version
+      - restore_cache:
+          keys:
+            - cargocache-cw721-metadata-uri-rust:1.53.0-{{ checksum "~/project/Cargo.lock" }}
+      - run:
+          name: Unit Tests
+          environment:
+            RUST_BACKTRACE: 1
+          command: cargo unit-test --locked
+      - run:
+          name: Build and run schema generator
+          command: cargo schema --locked
+      - run:
+          name: Ensure checked-in schemas are up-to-date
+          command: |
+            CHANGES_IN_REPO=$(git status --porcelain)
+            if [[ -n "$CHANGES_IN_REPO" ]]; then
+              echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:"
+              git status && git --no-pager diff
+              exit 1
+            fi
+      - save_cache:
+          paths:
+            - /usr/local/cargo/registry
+            - target
+          key: cargocache-cw721-metadata-uri-rust:1.53.0-{{ checksum "~/project/Cargo.lock" }}
+
   contract_cw1155_base:
     docker:
       - image: rust:1.53.0
diff --git a/Cargo.toml b/Cargo.toml
index 7990e6df8..5928d6e51 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,6 +45,10 @@ incremental = false
 codegen-units = 1
 incremental = false
 
+[profile.release.package.cw20-merkle-airdrop]
+codegen-units = 1
+incremental = false
+
 [profile.release.package.cw20-staking]
 codegen-units = 1
 incremental = false
@@ -53,6 +57,10 @@ incremental = false
 codegen-units = 1
 incremental = false
 
+[profile.release.package.cw721-metadata-uri]
+codegen-units = 1
+incremental = false
+
 [profile.release.package.cw1155-base]
 codegen-units = 1
 incremental = false

From 62a193e5b6c758f8ad1ed3a1142b54989e4414c4 Mon Sep 17 00:00:00 2001
From: Ethan Frey <ethanfrey@users.noreply.github.com>
Date: Thu, 23 Sep 2021 10:41:01 +0200
Subject: [PATCH 4/6] Fix linting issue

---
 contracts/cw721-base/examples/schema.rs    | 3 +--
 contracts/cw721-base/src/contract_tests.rs | 5 +++--
 contracts/cw721-base/src/lib.rs            | 8 +++++---
 3 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/contracts/cw721-base/examples/schema.rs b/contracts/cw721-base/examples/schema.rs
index f9c424023..a5ca2036e 100644
--- a/contracts/cw721-base/examples/schema.rs
+++ b/contracts/cw721-base/examples/schema.rs
@@ -7,8 +7,7 @@ use cw721::{
     AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, NftInfoResponse,
     NumTokensResponse, OwnerOfResponse, TokensResponse,
 };
-use cw721_base::entry::Extension;
-use cw721_base::msg::{ExecuteMsg, InstantiateMsg, MinterResponse, QueryMsg};
+use cw721_base::{Extension, ExecuteMsg, InstantiateMsg, MinterResponse, QueryMsg};
 
 fn main() {
     let mut out_dir = current_dir().unwrap();
diff --git a/contracts/cw721-base/src/contract_tests.rs b/contracts/cw721-base/src/contract_tests.rs
index 83630a2ff..5416b561a 100644
--- a/contracts/cw721-base/src/contract_tests.rs
+++ b/contracts/cw721-base/src/contract_tests.rs
@@ -7,8 +7,9 @@ use cw721::{
     NftInfoResponse, OwnerOfResponse,
 };
 
-use crate::entry::Extension;
-use crate::{ContractError, Cw721Contract, ExecuteMsg, InstantiateMsg, MintMsg, QueryMsg};
+use crate::{
+    ContractError, Cw721Contract, ExecuteMsg, Extension, InstantiateMsg, MintMsg, QueryMsg,
+};
 
 const MINTER: &str = "merlin";
 const CONTRACT_NAME: &str = "Magic Power";
diff --git a/contracts/cw721-base/src/lib.rs b/contracts/cw721-base/src/lib.rs
index 9d9a4ab8e..cc92699e1 100644
--- a/contracts/cw721-base/src/lib.rs
+++ b/contracts/cw721-base/src/lib.rs
@@ -8,16 +8,18 @@ pub mod state;
 pub use crate::error::ContractError;
 pub use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg, MinterResponse, QueryMsg};
 pub use crate::state::Cw721Contract;
+use cosmwasm_std::Empty;
+
+// This is a simple type to let us handle empty extensions
+pub type Extension = Option<Empty>;
 
 #[cfg(not(feature = "library"))]
 pub mod entry {
     use super::*;
 
     use cosmwasm_std::entry_point;
-    use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
+    use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
 
-    // This is a simple type to let us handle empty extensions
-    pub type Extension = Option<Empty>;
 
     // This makes a conscious choice on the various generics used by the contract
     #[entry_point]

From aead0d9ebe446031349e884d6c98e32b4f1cf7a8 Mon Sep 17 00:00:00 2001
From: Ethan Frey <ethanfrey@users.noreply.github.com>
Date: Thu, 23 Sep 2021 10:52:29 +0200
Subject: [PATCH 5/6] Add test showing Metadata Extension working

---
 contracts/cw721-base/examples/schema.rs |  2 +-
 contracts/cw721-base/src/lib.rs         |  1 -
 contracts/cw721-metadata-uri/src/lib.rs | 50 ++++++++++++++++++++++++-
 3 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/contracts/cw721-base/examples/schema.rs b/contracts/cw721-base/examples/schema.rs
index a5ca2036e..1b7b115d0 100644
--- a/contracts/cw721-base/examples/schema.rs
+++ b/contracts/cw721-base/examples/schema.rs
@@ -7,7 +7,7 @@ use cw721::{
     AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, NftInfoResponse,
     NumTokensResponse, OwnerOfResponse, TokensResponse,
 };
-use cw721_base::{Extension, ExecuteMsg, InstantiateMsg, MinterResponse, QueryMsg};
+use cw721_base::{ExecuteMsg, Extension, InstantiateMsg, MinterResponse, QueryMsg};
 
 fn main() {
     let mut out_dir = current_dir().unwrap();
diff --git a/contracts/cw721-base/src/lib.rs b/contracts/cw721-base/src/lib.rs
index cc92699e1..4d635f277 100644
--- a/contracts/cw721-base/src/lib.rs
+++ b/contracts/cw721-base/src/lib.rs
@@ -20,7 +20,6 @@ pub mod entry {
     use cosmwasm_std::entry_point;
     use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
 
-
     // This makes a conscious choice on the various generics used by the contract
     #[entry_point]
     pub fn instantiate(
diff --git a/contracts/cw721-metadata-uri/src/lib.rs b/contracts/cw721-metadata-uri/src/lib.rs
index 9111d4a20..d987700c1 100644
--- a/contracts/cw721-metadata-uri/src/lib.rs
+++ b/contracts/cw721-metadata-uri/src/lib.rs
@@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize};
 
 use cosmwasm_std::Empty;
 pub use cw721_base::{ContractError, InstantiateMsg, MintMsg, MinterResponse, QueryMsg};
-pub type ExecuteMsg = cw721_base::ExecuteMsg<Extension>;
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 pub struct Extension {
@@ -11,6 +10,7 @@ pub struct Extension {
 }
 
 pub type Cw721MetadataContract<'a> = cw721_base::Cw721Contract<'a, Extension, Empty>;
+pub type ExecuteMsg = cw721_base::ExecuteMsg<Extension>;
 
 #[cfg(not(feature = "library"))]
 pub mod entry {
@@ -47,3 +47,51 @@ pub mod entry {
         Cw721MetadataContract::default().query(deps, env, msg)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
+    use cw721::Cw721Query;
+
+    const CREATOR: &str = "creator";
+
+    #[test]
+    fn use_metadata_extension() {
+        let mut deps = mock_dependencies(&[]);
+        let contract = Cw721MetadataContract::default();
+
+        let info = mock_info(CREATOR, &[]);
+        let init_msg = InstantiateMsg {
+            name: "SpaceShips".to_string(),
+            symbol: "SPACE".to_string(),
+            minter: CREATOR.to_string(),
+        };
+        contract
+            .instantiate(deps.as_mut(), mock_env(), info.clone(), init_msg)
+            .unwrap();
+
+        let token_id = "Enterprise";
+        let mint_msg = MintMsg {
+            token_id: token_id.to_string(),
+            owner: "john".to_string(),
+            name: "Starship USS Enterprise".to_string(),
+            description: Some("Spaceship with Warp Drive".into()),
+            image: None,
+            extension: Extension {
+                metadata_uri: "http://starships.example.com/Starship/Enterprise.json".into(),
+            },
+        };
+        let exec_msg = ExecuteMsg::Mint(mint_msg.clone());
+        contract
+            .execute(deps.as_mut(), mock_env(), info, exec_msg)
+            .unwrap();
+
+        let res = contract.nft_info(deps.as_ref(), token_id.into()).unwrap();
+        assert_eq!(res.name, mint_msg.name);
+        assert_eq!(res.description, mint_msg.description.unwrap());
+        assert_eq!(res.image, mint_msg.image);
+        assert_eq!(res.extension, mint_msg.extension);
+    }
+}

From 749dfc7623e61beb209853c3fb0ed459019e0e2a Mon Sep 17 00:00:00 2001
From: Ethan Frey <ethanfrey@users.noreply.github.com>
Date: Thu, 23 Sep 2021 12:41:08 +0200
Subject: [PATCH 6/6] Add README to explain purpose/usage

---
 contracts/cw721-metadata-uri/NOTICE    |  2 +-
 contracts/cw721-metadata-uri/README.md | 37 ++++++++++++++++++++++++++
 2 files changed, 38 insertions(+), 1 deletion(-)
 create mode 100644 contracts/cw721-metadata-uri/README.md

diff --git a/contracts/cw721-metadata-uri/NOTICE b/contracts/cw721-metadata-uri/NOTICE
index c03203270..f0a440e12 100644
--- a/contracts/cw721-metadata-uri/NOTICE
+++ b/contracts/cw721-metadata-uri/NOTICE
@@ -1,4 +1,4 @@
-Cw721_base
+Cw721_metadata_uri
 Copyright (C) 2020 Confio OÜ
 
 Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/contracts/cw721-metadata-uri/README.md b/contracts/cw721-metadata-uri/README.md
new file mode 100644
index 000000000..9d6cb3e6d
--- /dev/null
+++ b/contracts/cw721-metadata-uri/README.md
@@ -0,0 +1,37 @@
+# CW721 Metadata URI
+
+In Ethereum, the ERC721 standard includes a metadata_uri field to store all metadata offchain.
+With CW721-Base in CosmWasm, we allow you to store any data on chain you wish, using a generic `extension: T`.
+
+In order to provide better "out of the box" compatibility for people migrating from the Ethereum ecosystem,
+and to demonstrate how to use the extension ability, we have created this simple contract. There is no business
+logic here, but looking at `lib.rs` will show you how do define custom data that is included when minting and
+available in all queries.
+
+In particular, here we define:
+
+```rust
+pub struct Extension {
+    pub metadata_uri: String,
+}
+```
+
+This means when you query `NftInfo{name: "Enterprise"}`, you will get something like:
+
+```json
+{
+  "name": "Enterprise",
+  "description": "USS Starship Enterprise",
+  "image": null,
+  "extension": {
+    "metadata_uri": "http://starships.example.com/Starships/Enterprise.json"
+  }
+}
+```
+
+Please look at the test code for an example usage in Rust.
+
+## Notice
+
+Feel free to use this contract out of the box, or as inspiration for further customization of cw721-base.
+We will not be adding new features or business logic here.