terra/nft_bridge: vendor cw721 and cw721-base (#696)

The most recent released versions of these libraries use a different
version of the cosmwasm than the rest of the wormhole projects, which
leads to a linker error. So we vendor these libraries and downgrade
their cosmwasm-std dependency to match the rest.

commit-id:a1a5c20b
This commit is contained in:
Csongor Kiss 2022-01-07 16:43:53 +01:00 committed by GitHub
parent 409b5ca5bf
commit 40837778a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 5554 additions and 2 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ venv
.env
bigtable-admin.json
bigtable-writer.json
.DS_Store

36
terra/Cargo.lock generated
View File

@ -263,6 +263,16 @@ dependencies = [
"syn",
]
[[package]]
name = "cosmwasm-schema"
version = "1.0.0-beta4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5de9749a4a933c34c59db83a77935a79cd705f93ae2b1cfbe40821f32751f75"
dependencies = [
"schemars",
"serde_json",
]
[[package]]
name = "cosmwasm-std"
version = "0.16.2"
@ -599,6 +609,32 @@ dependencies = [
"thiserror",
]
[[package]]
name = "cw721"
version = "0.10.1"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw0",
"schemars",
"serde",
]
[[package]]
name = "cw721-base"
version = "0.10.1"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-storage-plus",
"cw0",
"cw2",
"cw721",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "darling"
version = "0.13.0"

View File

@ -1,5 +1,5 @@
[workspace]
members = ["contracts/cw20-wrapped", "contracts/wormhole", "contracts/token-bridge", "contracts/pyth-bridge"]
members = ["contracts/cw20-wrapped", "contracts/wormhole", "contracts/token-bridge", "contracts/pyth-bridge", "packages/cw721", "contracts/cw721-base"]
[profile.release]
opt-level = 3
@ -13,4 +13,4 @@ incremental = false
overflow-checks = true
[patch.crates-io]
memmap2 = { git = "https://github.com/certusone/wormhole", package = "memmap2" }
memmap2 = { git = "https://github.com/certusone/wormhole", package = "memmap2" }

View File

@ -4,6 +4,7 @@ FROM cosmwasm/workspace-optimizer:0.12.1@sha256:1508cf7545f4b656ecafa34e29c1acf2
ADD Cargo.lock /code/
ADD Cargo.toml /code/
ADD contracts /code/contracts
ADD packages /code/packages
RUN optimize_workspace.sh
# Contract deployment stage

View File

@ -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"

View File

@ -0,0 +1,38 @@
[package]
name = "cw721-base"
version = "0.10.1"
authors = ["Ethan Frey <ethanfrey@users.noreply.github.com>"]
edition = "2018"
description = "Basic implementation cw721 NFTs"
license = "Apache-2.0"
repository = "https://github.com/CosmWasm/cw-nfts"
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 = { version = "0.8.0" }
cw2 = { version = "0.8.0" }
cw721 = { path = "../../packages/cw721", version = "0.10.1" }
cw-storage-plus = { version = "0.8.0" }
cosmwasm-std = { version = "0.16.0" }
schemars = "0.8.6"
serde = { version = "1.0.130", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.30" }
[dev-dependencies]
cosmwasm-schema = { version = "1.0.0-beta2" }

View File

@ -0,0 +1,14 @@
Cw721_base
Copyright (C) 2020-2021 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.

View File

@ -0,0 +1,71 @@
# Cw721 Basic
This is a basic implementation of a cw721 NFT contract. It implements
the [CW721 spec](../../packages/cw721/README.md) and is designed to
be deployed as is, or imported into other contracts to easily build
cw721-compatible NFTs with custom logic.
Implements:
- [x] CW721 Base
- [x] Metadata extension
- [ ] Enumerable extension (AllTokens done, but not Tokens - requires [#81](https://github.com/CosmWasm/cw-plus/issues/81))
## Implementation
The `ExecuteMsg` and `QueryMsg` implementations follow the [CW721 spec](../../packages/cw721/README.md) and are described there.
Beyond that, we make a few additions:
* `InstantiateMsg` takes name and symbol (for metadata), as well as a **Minter** address. This is a special address that has full
power to mint new NFTs (but not modify existing ones)
* `ExecuteMsg::Mint{token_id, owner, token_uri}` - creates a new token with given owner and (optional) metadata. It can only be called by
the Minter set in `instantiate`.
* `QueryMsg::Minter{}` - returns the minter address for this contract.
It requires all tokens to have defined metadata in the standard format (with no extensions). For generic NFTs this may
often be enough.
The *Minter* can either be an external actor (eg. web server, using PubKey) or another contract. If you just want to customize
the minting behavior but not other functionality, you could extend this contract (importing code and wiring it together)
or just create a custom contract as the owner and use that contract to Mint.
If provided, it is expected that the _token_uri_ points to a JSON file following the [ERC721 Metadata JSON Schema](https://eips.ethereum.org/EIPS/eip-721).
## Running this contract
You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed.
You can run unit tests on this via:
`cargo test`
Once you are happy with the content, you can compile it to wasm via:
```
RUSTFLAGS='-C link-arg=-s' cargo wasm
cp ../../target/wasm32-unknown-unknown/release/cw721_base.wasm .
ls -l cw721_base.wasm
sha256sum cw721_base.wasm
```
Or for a production-ready (optimized) build, run a build command in the
the repository root: https://github.com/CosmWasm/cw-plus#compiling.
## Importing this contract
You can also import much of the logic of this contract to build another
CW721-compliant contract, such as tradable names, crypto kitties,
or tokenized real estate.
Basically, you just need to write your handle function and import
`cw721_base::contract::handle_transfer`, etc and dispatch to them.
This allows you to use custom `ExecuteMsg` and `QueryMsg` with your additional
calls, but then use the underlying implementation for the standard cw721
messages you want to support. The same with `QueryMsg`. You will most
likely want to write a custom, domain-specific `instantiate`.
**TODO: add example when written**
For now, you can look at [`cw721-staking`](../cw721-staking/README.md)
for an example of how to "inherit" cw721 functionality and combine it with custom logic.
The process is similar for cw721.

View File

@ -0,0 +1,39 @@
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, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse,
NumTokensResponse, OperatorsResponse, OwnerOfResponse, TokensResponse,
};
use cw721_base::{ExecuteMsg, Extension, 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!(ApprovalResponse), &out_dir);
export_schema(&schema_for!(ApprovalsResponse), &out_dir);
export_schema(&schema_for!(OperatorsResponse), &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);
}

View File

@ -0,0 +1,393 @@
import axios from "axios";
import fs from "fs";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice, calculateFee, StdFee } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet, makeCosmoshubPath } from "@cosmjs/proto-signing";
import { Slip10RawIndex } from "@cosmjs/crypto";
import path from "path";
/*
* This is a set of helpers meant for use with @cosmjs/cli
* With these you can easily use the cw721 contract without worrying about forming messages and parsing queries.
*
* Usage: npx @cosmjs/cli@^0.26 --init https://raw.githubusercontent.com/CosmWasm/cosmwasm-plus/master/contracts/cw721-base/helpers.ts
*
* Create a client:
* const [addr, client] = await useOptions(pebblenetOptions).setup('password');
*
* Get the mnemonic:
* await useOptions(pebblenetOptions).recoverMnemonic(password);
*
* Create contract:
* const contract = CW721(client, pebblenetOptions.fees);
*
* Upload contract:
* const codeId = await contract.upload(addr);
*
* Instantiate contract example:
* const initMsg = {
* name: "Potato Coin",
* symbol: "TATER",
* minter: addr
* };
* const instance = await contract.instantiate(addr, codeId, initMsg, 'Potato Coin!');
* If you want to use this code inside an app, you will need several imports from https://github.com/CosmWasm/cosmjs
*/
interface Options {
readonly httpUrl: string
readonly networkId: string
readonly feeToken: string
readonly bech32prefix: string
readonly hdPath: readonly Slip10RawIndex[]
readonly faucetUrl?: string
readonly defaultKeyFile: string,
readonly fees: {
upload: StdFee,
init: StdFee,
exec: StdFee
}
}
const pebblenetGasPrice = GasPrice.fromString("0.01upebble");
const pebblenetOptions: Options = {
httpUrl: 'https://rpc.pebblenet.cosmwasm.com',
networkId: 'pebblenet-1',
bech32prefix: 'wasm',
feeToken: 'upebble',
faucetUrl: 'https://faucet.pebblenet.cosmwasm.com/credit',
hdPath: makeCosmoshubPath(0),
defaultKeyFile: path.join(process.env.HOME, ".pebblenet.key"),
fees: {
upload: calculateFee(1500000, pebblenetGasPrice),
init: calculateFee(500000, pebblenetGasPrice),
exec: calculateFee(200000, pebblenetGasPrice),
},
}
interface Network {
setup: (password: string, filename?: string) => Promise<[string, SigningCosmWasmClient]>
recoverMnemonic: (password: string, filename?: string) => Promise<string>
}
const useOptions = (options: Options): Network => {
const loadOrCreateWallet = async (options: Options, filename: string, password: string): Promise<DirectSecp256k1HdWallet> => {
let encrypted: string;
try {
encrypted = fs.readFileSync(filename, 'utf8');
} catch (err) {
// generate if no file exists
const wallet = await DirectSecp256k1HdWallet.generate(12, {hdPaths: [options.hdPath], prefix: options.bech32prefix});
const encrypted = await wallet.serialize(password);
fs.writeFileSync(filename, encrypted, 'utf8');
return wallet;
}
// otherwise, decrypt the file (we cannot put deserialize inside try or it will over-write on a bad password)
const wallet = await DirectSecp256k1HdWallet.deserialize(encrypted, password);
return wallet;
};
const connect = async (
wallet: DirectSecp256k1HdWallet,
options: Options
): Promise<SigningCosmWasmClient> => {
const clientOptions = {
prefix: options.bech32prefix
}
return await SigningCosmWasmClient.connectWithSigner(options.httpUrl, wallet, clientOptions)
};
const hitFaucet = async (
faucetUrl: string,
address: string,
denom: string
): Promise<void> => {
await axios.post(faucetUrl, { denom, address });
}
const setup = async (password: string, filename?: string): Promise<[string, SigningCosmWasmClient]> => {
const keyfile = filename || options.defaultKeyFile;
const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password);
const client = await connect(wallet, pebblenetOptions);
const [account] = await wallet.getAccounts();
// ensure we have some tokens
if (options.faucetUrl) {
const tokens = await client.getBalance(account.address, options.feeToken)
if (tokens.amount === '0') {
console.log(`Getting ${options.feeToken} from faucet`);
await hitFaucet(options.faucetUrl, account.address, options.feeToken);
}
}
return [account.address, client];
}
const recoverMnemonic = async (password: string, filename?: string): Promise<string> => {
const keyfile = filename || options.defaultKeyFile;
const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password);
return wallet.mnemonic;
}
return { setup, recoverMnemonic };
}
type TokenId = string
interface Balances {
readonly address: string
readonly amount: string // decimal as string
}
interface MintInfo {
readonly minter: string
readonly cap?: string // decimal as string
}
interface ContractInfo {
readonly name: string
readonly symbol: string
}
interface NftInfo {
readonly name: string,
readonly description: string,
readonly image: any
}
interface Access {
readonly owner: string,
readonly approvals: []
}
interface AllNftInfo {
readonly access: Access,
readonly info: NftInfo
}
interface Operators {
readonly operators: []
}
interface Count {
readonly count: number
}
interface InitMsg {
readonly name: string
readonly symbol: string
readonly minter: string
}
// Better to use this interface?
interface MintMsg {
readonly token_id: TokenId
readonly owner: string
readonly name: string
readonly description?: string
readonly image?: string
}
type Expiration = { readonly at_height: number } | { readonly at_time: number } | { readonly never: {} };
interface AllowanceResponse {
readonly allowance: string; // integer as string
readonly expires: Expiration;
}
interface AllowanceInfo {
readonly allowance: string; // integer as string
readonly spender: string; // bech32 address
readonly expires: Expiration;
}
interface AllAllowancesResponse {
readonly allowances: readonly AllowanceInfo[];
}
interface AllAccountsResponse {
// list of bech32 address that have a balance
readonly accounts: readonly string[];
}
interface TokensResponse {
readonly tokens: readonly string[];
}
interface CW721Instance {
readonly contractAddress: string
// queries
allowance: (owner: string, spender: string) => Promise<AllowanceResponse>
allAllowances: (owner: string, startAfter?: string, limit?: number) => Promise<AllAllowancesResponse>
allAccounts: (startAfter?: string, limit?: number) => Promise<readonly string[]>
minter: () => Promise<MintInfo>
contractInfo: () => Promise<ContractInfo>
nftInfo: (tokenId: TokenId) => Promise<NftInfo>
allNftInfo: (tokenId: TokenId) => Promise<AllNftInfo>
ownerOf: (tokenId: TokenId) => Promise<Access>
approvedForAll: (owner: string, include_expired?: boolean, start_after?: string, limit?: number) => Promise<Operators>
numTokens: () => Promise<Count>
tokens: (owner: string, startAfter?: string, limit?: number) => Promise<TokensResponse>
allTokens: (startAfter?: string, limit?: number) => Promise<TokensResponse>
// actions
mint: (senderAddress: string, tokenId: TokenId, owner: string, name: string, level: number, description?: string, image?: string) => Promise<string>
transferNft: (senderAddress: string, recipient: string, tokenId: TokenId) => Promise<string>
sendNft: (senderAddress: string, contract: string, token_id: TokenId, msg?: BinaryType) => Promise<string>
approve: (senderAddress: string, spender: string, tokenId: TokenId, expires?: Expiration) => Promise<string>
approveAll: (senderAddress: string, operator: string, expires?: Expiration) => Promise<string>
revoke: (senderAddress: string, spender: string, tokenId: TokenId) => Promise<string>
revokeAll: (senderAddress: string, operator: string) => Promise<string>
}
interface CW721Contract {
// upload a code blob and returns a codeId
upload: (senderAddress: string) => Promise<number>
// instantiates a cw721 contract
// codeId must come from a previous deploy
// label is the public name of the contract in listing
// if you set admin, you can run migrations on this contract (likely client.senderAddress)
instantiate: (senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string) => Promise<CW721Instance>
use: (contractAddress: string) => CW721Instance
}
export const CW721 = (client: SigningCosmWasmClient, fees: Options['fees']): CW721Contract => {
const use = (contractAddress: string): CW721Instance => {
const allowance = async (owner: string, spender: string): Promise<AllowanceResponse> => {
return client.queryContractSmart(contractAddress, { allowance: { owner, spender } });
};
const allAllowances = async (owner: string, startAfter?: string, limit?: number): Promise<AllAllowancesResponse> => {
return client.queryContractSmart(contractAddress, { all_allowances: { owner, start_after: startAfter, limit } });
};
const allAccounts = async (startAfter?: string, limit?: number): Promise<readonly string[]> => {
const accounts: AllAccountsResponse = await client.queryContractSmart(contractAddress, { all_accounts: { start_after: startAfter, limit } });
return accounts.accounts;
};
const minter = async (): Promise<MintInfo> => {
return client.queryContractSmart(contractAddress, { minter: {} });
};
const contractInfo = async (): Promise<ContractInfo> => {
return client.queryContractSmart(contractAddress, { contract_info: {} });
};
const nftInfo = async (token_id: TokenId): Promise<NftInfo> => {
return client.queryContractSmart(contractAddress, { nft_info: { token_id } });
}
const allNftInfo = async (token_id: TokenId): Promise<AllNftInfo> => {
return client.queryContractSmart(contractAddress, { all_nft_info: { token_id } });
}
const ownerOf = async (token_id: TokenId): Promise<Access> => {
return await client.queryContractSmart(contractAddress, { owner_of: { token_id } });
}
const approvedForAll = async (owner: string, include_expired?: boolean, start_after?: string, limit?: number): Promise<Operators> => {
return await client.queryContractSmart(contractAddress, { approved_for_all: { owner, include_expired, start_after, limit } })
}
// total number of tokens issued
const numTokens = async (): Promise<Count> => {
return client.queryContractSmart(contractAddress, { num_tokens: {} });
}
// list all token_ids that belong to a given owner
const tokens = async (owner: string, start_after?: string, limit?: number): Promise<TokensResponse> => {
return client.queryContractSmart(contractAddress, { tokens: { owner, start_after, limit } });
}
const allTokens = async (start_after?: string, limit?: number): Promise<TokensResponse> => {
return client.queryContractSmart(contractAddress, { all_tokens: { start_after, limit } });
}
// actions
const mint = async (senderAddress: string, token_id: TokenId, owner: string, name: string, level: number, description?: string, image?: string): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { mint: { token_id, owner, name, level, description, image } }, fees.exec);
return result.transactionHash;
}
// transfers ownership, returns transactionHash
const transferNft = async (senderAddress: string, recipient: string, token_id: TokenId): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { transfer_nft: { recipient, token_id } }, fees.exec);
return result.transactionHash;
}
// sends an nft token to another contract (TODO: msg type any needs to be revisited once receiveNft is implemented)
const sendNft = async (senderAddress: string, contract: string, token_id: TokenId, msg?: any): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { send_nft: { contract, token_id, msg } }, fees.exec)
return result.transactionHash;
}
const approve = async (senderAddress: string, spender: string, token_id: TokenId, expires?: Expiration): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { approve: { spender, token_id, expires } }, fees.exec);
return result.transactionHash;
}
const approveAll = async (senderAddress: string, operator: string, expires?: Expiration): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { approve_all: { operator, expires } }, fees.exec)
return result.transactionHash
}
const revoke = async (senderAddress: string, spender: string, token_id: TokenId): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { revoke: { spender, token_id } }, fees.exec);
return result.transactionHash;
}
const revokeAll = async (senderAddress: string, operator: string): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { revoke_all: { operator } }, fees.exec)
return result.transactionHash;
}
return {
contractAddress,
allowance,
allAllowances,
allAccounts,
minter,
contractInfo,
nftInfo,
allNftInfo,
ownerOf,
approvedForAll,
numTokens,
tokens,
allTokens,
mint,
transferNft,
sendNft,
approve,
approveAll,
revoke,
revokeAll
};
}
const downloadWasm = async (url: string): Promise<Uint8Array> => {
const r = await axios.get(url, { responseType: 'arraybuffer' })
if (r.status !== 200) {
throw new Error(`Download error: ${r.status}`)
}
return r.data
}
const upload = async (senderAddress: string): Promise<number> => {
const sourceUrl = "https://github.com/CosmWasm/cosmwasm-plus/releases/download/v0.9.0/cw721_base.wasm";
const wasm = await downloadWasm(sourceUrl);
const result = await client.upload(senderAddress, wasm, fees.upload);
return result.codeId;
}
const instantiate = async (senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<CW721Instance> => {
const result = await client.instantiate(senderAddress, codeId, initMsg, label, fees.init, { memo: `Init ${label}`, admin });
return use(result.contractAddress);
}
return { upload, instantiate, use };
}

View File

@ -0,0 +1,155 @@
{
"$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_Nullable_Empty"
}
]
}
},
"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"
}
}
},
"Empty": {
"description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
"type": "object"
},
"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)",
"oneOf": [
{
"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
}
]
},
"NftInfoResponse_for_Nullable_Empty": {
"type": "object",
"properties": {
"extension": {
"description": "You can add any custom metadata here when you extend cw721-base",
"anyOf": [
{
"$ref": "#/definitions/Empty"
},
{
"type": "null"
}
]
},
"token_uri": {
"description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema",
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}

View File

@ -0,0 +1,94 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ApprovalResponse",
"type": "object",
"required": [
"approval"
],
"properties": {
"approval": {
"$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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -0,0 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ApprovalsResponse",
"type": "object",
"required": [
"approvals"
],
"properties": {
"approvals": {
"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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,310 @@
{
"$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.",
"oneOf": [
{
"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_Nullable_Empty"
}
},
"additionalProperties": false
},
{
"description": "Burn an NFT the sender has access to",
"type": "object",
"required": [
"burn"
],
"properties": {
"burn": {
"type": "object",
"required": [
"token_id"
],
"properties": {
"token_id": {
"type": "string"
}
}
}
},
"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"
},
"Empty": {
"description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
"type": "object"
},
"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)",
"oneOf": [
{
"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
}
]
},
"MintMsg_for_Nullable_Empty": {
"type": "object",
"required": [
"owner",
"token_id"
],
"properties": {
"extension": {
"description": "Any custom extension used by this contract",
"anyOf": [
{
"$ref": "#/definitions/Empty"
},
{
"type": "null"
}
]
},
"owner": {
"description": "The owner of the newly minter NFT",
"type": "string"
},
"token_id": {
"description": "Unique ID of the NFT",
"type": "string"
},
"token_uri": {
"description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema",
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "NftInfoResponse",
"type": "object",
"properties": {
"extension": {
"description": "You can add any custom metadata here when you extend cw721-base",
"anyOf": [
{
"$ref": "#/definitions/Empty"
},
{
"type": "null"
}
]
},
"token_uri": {
"description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema",
"type": [
"string",
"null"
]
}
},
"definitions": {
"Empty": {
"description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
"type": "object"
}
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "OperatorsResponse",
"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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -0,0 +1,285 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"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": "Return operator that can access all of the owner's tokens. Return type: `ApprovalResponse`",
"type": "object",
"required": [
"approval"
],
"properties": {
"approval": {
"type": "object",
"required": [
"spender",
"token_id"
],
"properties": {
"include_expired": {
"type": [
"boolean",
"null"
]
},
"spender": {
"type": "string"
},
"token_id": {
"type": "string"
}
}
}
},
"additionalProperties": false
},
{
"description": "Return approvals that a token has Return type: `ApprovalsResponse`",
"type": "object",
"required": [
"approvals"
],
"properties": {
"approvals": {
"type": "object",
"required": [
"token_id"
],
"properties": {
"include_expired": {
"type": [
"boolean",
"null"
]
},
"token_id": {
"type": "string"
}
}
}
},
"additionalProperties": false
},
{
"description": "List all operators that can access all of the owner's tokens Return type: `OperatorsResponse`",
"type": "object",
"required": [
"all_operators"
],
"properties": {
"all_operators": {
"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
}
]
}

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,731 @@
#![cfg(test)]
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_binary, to_binary, CosmosMsg, DepsMut, Empty, Response, WasmMsg};
use cw721::{
Approval, ApprovalResponse, ContractInfoResponse, Cw721Query, Cw721ReceiveMsg, Expiration,
NftInfoResponse, OperatorsResponse, OwnerOfResponse,
};
use crate::{
ContractError, Cw721Contract, ExecuteMsg, Extension, InstantiateMsg, MintMsg, QueryMsg,
};
const MINTER: &str = "merlin";
const CONTRACT_NAME: &str = "Magic Power";
const SYMBOL: &str = "MGK";
fn setup_contract(deps: DepsMut<'_>) -> Cw721Contract<'static, Extension, Empty> {
let contract = Cw721Contract::default();
let msg = InstantiateMsg {
name: CONTRACT_NAME.to_string(),
symbol: SYMBOL.to_string(),
minter: String::from(MINTER),
};
let info = mock_info("creator", &[]);
let res = contract.instantiate(deps, mock_env(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
contract
}
#[test]
fn proper_instantiation() {
let mut deps = mock_dependencies();
let contract = Cw721Contract::<Extension, Empty>::default();
let msg = InstantiateMsg {
name: CONTRACT_NAME.to_string(),
symbol: SYMBOL.to_string(),
minter: String::from(MINTER),
};
let info = mock_info("creator", &[]);
// we can just call .unwrap() to assert this was a success
let res = contract
.instantiate(deps.as_mut(), mock_env(), info, msg)
.unwrap();
assert_eq!(0, res.messages.len());
// it worked, let's query the state
let res = contract.minter(deps.as_ref()).unwrap();
assert_eq!(MINTER, res.minter);
let info = contract.contract_info(deps.as_ref()).unwrap();
assert_eq!(
info,
ContractInfoResponse {
name: CONTRACT_NAME.to_string(),
symbol: SYMBOL.to_string(),
}
);
let count = contract.num_tokens(deps.as_ref()).unwrap();
assert_eq!(0, count.count);
// list the token_ids
let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap();
assert_eq!(0, tokens.tokens.len());
}
#[test]
fn minting() {
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());
let token_id = "petrify".to_string();
let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string();
let mint_msg = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id.clone(),
owner: String::from("medusa"),
token_uri: Some(token_uri.clone()),
extension: None,
});
// random cannot mint
let random = mock_info("random", &[]);
let err = contract
.execute(deps.as_mut(), mock_env(), random, mint_msg.clone())
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
// minter can mint
let allowed = mock_info(MINTER, &[]);
let _ = contract
.execute(deps.as_mut(), mock_env(), allowed, mint_msg)
.unwrap();
// ensure num tokens increases
let count = contract.num_tokens(deps.as_ref()).unwrap();
assert_eq!(1, count.count);
// unknown nft returns error
let _ = contract
.nft_info(deps.as_ref(), "unknown".to_string())
.unwrap_err();
// this nft info is correct
let info = contract.nft_info(deps.as_ref(), token_id.clone()).unwrap();
assert_eq!(
info,
NftInfoResponse::<Extension> {
token_uri: Some(token_uri),
extension: None,
}
);
// owner info is correct
let owner = contract
.owner_of(deps.as_ref(), mock_env(), token_id.clone(), true)
.unwrap();
assert_eq!(
owner,
OwnerOfResponse {
owner: String::from("medusa"),
approvals: vec![],
}
);
// Cannot mint same token_id again
let mint_msg2 = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id.clone(),
owner: String::from("hercules"),
token_uri: None,
extension: None,
});
let allowed = mock_info(MINTER, &[]);
let err = contract
.execute(deps.as_mut(), mock_env(), allowed, mint_msg2)
.unwrap_err();
assert_eq!(err, ContractError::Claimed {});
// list the token_ids
let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap();
assert_eq!(1, tokens.tokens.len());
assert_eq!(vec![token_id], tokens.tokens);
}
#[test]
fn burning() {
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());
let token_id = "petrify".to_string();
let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string();
let mint_msg = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id.clone(),
owner: MINTER.to_string(),
token_uri: Some(token_uri),
extension: None,
});
let burn_msg = ExecuteMsg::Burn { token_id };
// mint some NFT
let allowed = mock_info(MINTER, &[]);
let _ = contract
.execute(deps.as_mut(), mock_env(), allowed.clone(), mint_msg)
.unwrap();
// random not allowed to burn
let random = mock_info("random", &[]);
let err = contract
.execute(deps.as_mut(), mock_env(), random, burn_msg.clone())
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
let _ = contract
.execute(deps.as_mut(), mock_env(), allowed, burn_msg)
.unwrap();
// ensure num tokens decreases
let count = contract.num_tokens(deps.as_ref()).unwrap();
assert_eq!(0, count.count);
// trying to get nft returns error
let _ = contract
.nft_info(deps.as_ref(), "petrify".to_string())
.unwrap_err();
// list the token_ids
let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap();
assert!(tokens.tokens.is_empty());
}
#[test]
fn transferring_nft() {
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());
// Mint a token
let token_id = "melt".to_string();
let token_uri = "https://www.merriam-webster.com/dictionary/melt".to_string();
let mint_msg = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id.clone(),
owner: String::from("venus"),
token_uri: Some(token_uri),
extension: None,
});
let minter = mock_info(MINTER, &[]);
contract
.execute(deps.as_mut(), mock_env(), minter, mint_msg)
.unwrap();
// random cannot transfer
let random = mock_info("random", &[]);
let transfer_msg = ExecuteMsg::TransferNft {
recipient: String::from("random"),
token_id: token_id.clone(),
};
let err = contract
.execute(deps.as_mut(), mock_env(), random, transfer_msg)
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
// owner can
let random = mock_info("venus", &[]);
let transfer_msg = ExecuteMsg::TransferNft {
recipient: String::from("random"),
token_id: token_id.clone(),
};
let res = contract
.execute(deps.as_mut(), mock_env(), random, transfer_msg)
.unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "transfer_nft")
.add_attribute("sender", "venus")
.add_attribute("recipient", "random")
.add_attribute("token_id", token_id)
);
}
#[test]
fn sending_nft() {
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());
// Mint a token
let token_id = "melt".to_string();
let token_uri = "https://www.merriam-webster.com/dictionary/melt".to_string();
let mint_msg = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id.clone(),
owner: String::from("venus"),
token_uri: Some(token_uri),
extension: None,
});
let minter = mock_info(MINTER, &[]);
contract
.execute(deps.as_mut(), mock_env(), minter, mint_msg)
.unwrap();
let msg = to_binary("You now have the melting power").unwrap();
let target = String::from("another_contract");
let send_msg = ExecuteMsg::SendNft {
contract: target.clone(),
token_id: token_id.clone(),
msg: msg.clone(),
};
let random = mock_info("random", &[]);
let err = contract
.execute(deps.as_mut(), mock_env(), random, send_msg.clone())
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
// but owner can
let random = mock_info("venus", &[]);
let res = contract
.execute(deps.as_mut(), mock_env(), random, send_msg)
.unwrap();
let payload = Cw721ReceiveMsg {
sender: String::from("venus"),
token_id: token_id.clone(),
msg,
};
let expected = payload.into_cosmos_msg(target.clone()).unwrap();
// ensure expected serializes as we think it should
match &expected {
CosmosMsg::Wasm(WasmMsg::Execute { contract_addr, .. }) => {
assert_eq!(contract_addr, &target)
}
m => panic!("Unexpected message type: {:?}", m),
}
// and make sure this is the request sent by the contract
assert_eq!(
res,
Response::new()
.add_message(expected)
.add_attribute("action", "send_nft")
.add_attribute("sender", "venus")
.add_attribute("recipient", "another_contract")
.add_attribute("token_id", token_id)
);
}
#[test]
fn approving_revoking() {
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());
// Mint a token
let token_id = "grow".to_string();
let token_uri = "https://www.merriam-webster.com/dictionary/grow".to_string();
let mint_msg = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id.clone(),
owner: String::from("demeter"),
token_uri: Some(token_uri),
extension: None,
});
let minter = mock_info(MINTER, &[]);
contract
.execute(deps.as_mut(), mock_env(), minter, mint_msg)
.unwrap();
// Give random transferring power
let approve_msg = ExecuteMsg::Approve {
spender: String::from("random"),
token_id: token_id.clone(),
expires: None,
};
let owner = mock_info("demeter", &[]);
let res = contract
.execute(deps.as_mut(), mock_env(), owner, approve_msg)
.unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "approve")
.add_attribute("sender", "demeter")
.add_attribute("spender", "random")
.add_attribute("token_id", token_id.clone())
);
// test approval query
let res = contract
.approval(
deps.as_ref(),
mock_env(),
token_id.clone(),
String::from("random"),
true,
)
.unwrap();
assert_eq!(
res,
ApprovalResponse {
approval: Approval {
spender: String::from("random"),
expires: Expiration::Never {}
}
}
);
// random can now transfer
let random = mock_info("random", &[]);
let transfer_msg = ExecuteMsg::TransferNft {
recipient: String::from("person"),
token_id: token_id.clone(),
};
contract
.execute(deps.as_mut(), mock_env(), random, transfer_msg)
.unwrap();
// Approvals are removed / cleared
let query_msg = QueryMsg::OwnerOf {
token_id: token_id.clone(),
include_expired: None,
};
let res: OwnerOfResponse = from_binary(
&contract
.query(deps.as_ref(), mock_env(), query_msg.clone())
.unwrap(),
)
.unwrap();
assert_eq!(
res,
OwnerOfResponse {
owner: String::from("person"),
approvals: vec![],
}
);
// Approve, revoke, and check for empty, to test revoke
let approve_msg = ExecuteMsg::Approve {
spender: String::from("random"),
token_id: token_id.clone(),
expires: None,
};
let owner = mock_info("person", &[]);
contract
.execute(deps.as_mut(), mock_env(), owner.clone(), approve_msg)
.unwrap();
let revoke_msg = ExecuteMsg::Revoke {
spender: String::from("random"),
token_id,
};
contract
.execute(deps.as_mut(), mock_env(), owner, revoke_msg)
.unwrap();
// Approvals are now removed / cleared
let res: OwnerOfResponse = from_binary(
&contract
.query(deps.as_ref(), mock_env(), query_msg)
.unwrap(),
)
.unwrap();
assert_eq!(
res,
OwnerOfResponse {
owner: String::from("person"),
approvals: vec![],
}
);
}
#[test]
fn approving_all_revoking_all() {
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());
// Mint a couple tokens (from the same owner)
let token_id1 = "grow1".to_string();
let token_uri1 = "https://www.merriam-webster.com/dictionary/grow1".to_string();
let token_id2 = "grow2".to_string();
let token_uri2 = "https://www.merriam-webster.com/dictionary/grow2".to_string();
let mint_msg1 = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id1.clone(),
owner: String::from("demeter"),
token_uri: Some(token_uri1),
extension: None,
});
let minter = mock_info(MINTER, &[]);
contract
.execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg1)
.unwrap();
let mint_msg2 = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id2.clone(),
owner: String::from("demeter"),
token_uri: Some(token_uri2),
extension: None,
});
contract
.execute(deps.as_mut(), mock_env(), minter, mint_msg2)
.unwrap();
// paginate the token_ids
let tokens = contract.all_tokens(deps.as_ref(), None, Some(1)).unwrap();
assert_eq!(1, tokens.tokens.len());
assert_eq!(vec![token_id1.clone()], tokens.tokens);
let tokens = contract
.all_tokens(deps.as_ref(), Some(token_id1.clone()), Some(3))
.unwrap();
assert_eq!(1, tokens.tokens.len());
assert_eq!(vec![token_id2.clone()], tokens.tokens);
// demeter gives random full (operator) power over her tokens
let approve_all_msg = ExecuteMsg::ApproveAll {
operator: String::from("random"),
expires: None,
};
let owner = mock_info("demeter", &[]);
let res = contract
.execute(deps.as_mut(), mock_env(), owner, approve_all_msg)
.unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "approve_all")
.add_attribute("sender", "demeter")
.add_attribute("operator", "random")
);
// random can now transfer
let random = mock_info("random", &[]);
let transfer_msg = ExecuteMsg::TransferNft {
recipient: String::from("person"),
token_id: token_id1,
};
contract
.execute(deps.as_mut(), mock_env(), random.clone(), transfer_msg)
.unwrap();
// random can now send
let inner_msg = WasmMsg::Execute {
contract_addr: "another_contract".into(),
msg: to_binary("You now also have the growing power").unwrap(),
funds: vec![],
};
let msg: CosmosMsg = CosmosMsg::Wasm(inner_msg);
let send_msg = ExecuteMsg::SendNft {
contract: String::from("another_contract"),
token_id: token_id2,
msg: to_binary(&msg).unwrap(),
};
contract
.execute(deps.as_mut(), mock_env(), random, send_msg)
.unwrap();
// Approve_all, revoke_all, and check for empty, to test revoke_all
let approve_all_msg = ExecuteMsg::ApproveAll {
operator: String::from("operator"),
expires: None,
};
// person is now the owner of the tokens
let owner = mock_info("person", &[]);
contract
.execute(deps.as_mut(), mock_env(), owner, approve_all_msg)
.unwrap();
let res = contract
.operators(
deps.as_ref(),
mock_env(),
String::from("person"),
true,
None,
None,
)
.unwrap();
assert_eq!(
res,
OperatorsResponse {
operators: vec![cw721::Approval {
spender: String::from("operator"),
expires: Expiration::Never {}
}]
}
);
// second approval
let buddy_expires = Expiration::AtHeight(1234567);
let approve_all_msg = ExecuteMsg::ApproveAll {
operator: String::from("buddy"),
expires: Some(buddy_expires),
};
let owner = mock_info("person", &[]);
contract
.execute(deps.as_mut(), mock_env(), owner.clone(), approve_all_msg)
.unwrap();
// and paginate queries
let res = contract
.operators(
deps.as_ref(),
mock_env(),
String::from("person"),
true,
None,
Some(1),
)
.unwrap();
assert_eq!(
res,
OperatorsResponse {
operators: vec![cw721::Approval {
spender: String::from("buddy"),
expires: buddy_expires,
}]
}
);
let res = contract
.operators(
deps.as_ref(),
mock_env(),
String::from("person"),
true,
Some(String::from("buddy")),
Some(2),
)
.unwrap();
assert_eq!(
res,
OperatorsResponse {
operators: vec![cw721::Approval {
spender: String::from("operator"),
expires: Expiration::Never {}
}]
}
);
let revoke_all_msg = ExecuteMsg::RevokeAll {
operator: String::from("operator"),
};
contract
.execute(deps.as_mut(), mock_env(), owner, revoke_all_msg)
.unwrap();
// Approvals are removed / cleared without affecting others
let res = contract
.operators(
deps.as_ref(),
mock_env(),
String::from("person"),
false,
None,
None,
)
.unwrap();
assert_eq!(
res,
OperatorsResponse {
operators: vec![cw721::Approval {
spender: String::from("buddy"),
expires: buddy_expires,
}]
}
);
// ensure the filter works (nothing should be here
let mut late_env = mock_env();
late_env.block.height = 1234568; //expired
let res = contract
.operators(
deps.as_ref(),
late_env,
String::from("person"),
false,
None,
None,
)
.unwrap();
assert_eq!(0, res.operators.len());
}
#[test]
fn query_tokens_by_owner() {
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());
let minter = mock_info(MINTER, &[]);
// Mint a couple tokens (from the same owner)
let token_id1 = "grow1".to_string();
let demeter = String::from("Demeter");
let token_id2 = "grow2".to_string();
let ceres = String::from("Ceres");
let token_id3 = "sing".to_string();
let mint_msg = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id1.clone(),
owner: demeter.clone(),
token_uri: None,
extension: None,
});
contract
.execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg)
.unwrap();
let mint_msg = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id2.clone(),
owner: ceres.clone(),
token_uri: None,
extension: None,
});
contract
.execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg)
.unwrap();
let mint_msg = ExecuteMsg::Mint(MintMsg::<Extension> {
token_id: token_id3.clone(),
owner: demeter.clone(),
token_uri: None,
extension: None,
});
contract
.execute(deps.as_mut(), mock_env(), minter, mint_msg)
.unwrap();
// get all tokens in order:
let expected = vec![token_id1.clone(), token_id2.clone(), token_id3.clone()];
let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap();
assert_eq!(&expected, &tokens.tokens);
// paginate
let tokens = contract.all_tokens(deps.as_ref(), None, Some(2)).unwrap();
assert_eq!(&expected[..2], &tokens.tokens[..]);
let tokens = contract
.all_tokens(deps.as_ref(), Some(expected[1].clone()), None)
.unwrap();
assert_eq!(&expected[2..], &tokens.tokens[..]);
// get by owner
let by_ceres = vec![token_id2];
let by_demeter = vec![token_id1, token_id3];
// all tokens by owner
let tokens = contract
.tokens(deps.as_ref(), demeter.clone(), None, None)
.unwrap();
assert_eq!(&by_demeter, &tokens.tokens);
let tokens = contract.tokens(deps.as_ref(), ceres, None, None).unwrap();
assert_eq!(&by_ceres, &tokens.tokens);
// paginate for demeter
let tokens = contract
.tokens(deps.as_ref(), demeter.clone(), None, Some(1))
.unwrap();
assert_eq!(&by_demeter[..1], &tokens.tokens[..]);
let tokens = contract
.tokens(deps.as_ref(), demeter, Some(by_demeter[0].clone()), Some(3))
.unwrap();
assert_eq!(&by_demeter[1..], &tokens.tokens[..]);
}

View File

@ -0,0 +1,20 @@
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("token_id already claimed")]
Claimed {},
#[error("Cannot set approval that is already expired")]
Expired {},
#[error("Approval not found for: {spender}")]
ApprovalNotFound { spender: String },
}

View File

@ -0,0 +1,397 @@
use serde::de::DeserializeOwned;
use serde::Serialize;
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use cw2::set_contract_version;
use cw721::{ContractInfoResponse, CustomMsg, Cw721Execute, Cw721ReceiveMsg, Expiration};
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg};
use crate::state::{Approval, Cw721Contract, TokenInfo};
// version info for migration info
const CONTRACT_NAME: &str = "crates.io:cw721-base";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
impl<'a, T, C> Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
pub fn instantiate(
&self,
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response<C>> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let info = ContractInfoResponse {
name: msg.name,
symbol: msg.symbol,
};
self.contract_info.save(deps.storage, &info)?;
let minter = deps.api.addr_validate(&msg.minter)?;
self.minter.save(deps.storage, &minter)?;
Ok(Response::default())
}
pub fn execute(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg<T>,
) -> Result<Response<C>, ContractError> {
match msg {
ExecuteMsg::Mint(msg) => self.mint(deps, env, info, msg),
ExecuteMsg::Approve {
spender,
token_id,
expires,
} => self.approve(deps, env, info, spender, token_id, expires),
ExecuteMsg::Revoke { spender, token_id } => {
self.revoke(deps, env, info, spender, token_id)
}
ExecuteMsg::ApproveAll { operator, expires } => {
self.approve_all(deps, env, info, operator, expires)
}
ExecuteMsg::RevokeAll { operator } => self.revoke_all(deps, env, info, operator),
ExecuteMsg::TransferNft {
recipient,
token_id,
} => self.transfer_nft(deps, env, info, recipient, token_id),
ExecuteMsg::SendNft {
contract,
token_id,
msg,
} => self.send_nft(deps, env, info, contract, token_id, msg),
ExecuteMsg::Burn { token_id } => self.burn(deps, env, info, token_id),
}
}
}
// TODO pull this into some sort of trait extension??
impl<'a, T, C> Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
pub fn mint(
&self,
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: MintMsg<T>,
) -> Result<Response<C>, ContractError> {
let minter = self.minter.load(deps.storage)?;
if info.sender != minter {
return Err(ContractError::Unauthorized {});
}
// create the token
let token = TokenInfo {
owner: deps.api.addr_validate(&msg.owner)?,
approvals: vec![],
token_uri: msg.token_uri,
extension: msg.extension,
};
self.tokens
.update(deps.storage, &msg.token_id, |old| match old {
Some(_) => Err(ContractError::Claimed {}),
None => Ok(token),
})?;
self.increment_tokens(deps.storage)?;
Ok(Response::new()
.add_attribute("action", "mint")
.add_attribute("minter", info.sender)
.add_attribute("token_id", msg.token_id))
}
}
impl<'a, T, C> Cw721Execute<T, C> for Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
type Err = ContractError;
fn transfer_nft(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
recipient: String,
token_id: String,
) -> Result<Response<C>, ContractError> {
self._transfer_nft(deps, &env, &info, &recipient, &token_id)?;
Ok(Response::new()
.add_attribute("action", "transfer_nft")
.add_attribute("sender", info.sender)
.add_attribute("recipient", recipient)
.add_attribute("token_id", token_id))
}
fn send_nft(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
contract: String,
token_id: String,
msg: Binary,
) -> Result<Response<C>, ContractError> {
// Transfer token
self._transfer_nft(deps, &env, &info, &contract, &token_id)?;
let send = Cw721ReceiveMsg {
sender: info.sender.to_string(),
token_id: token_id.clone(),
msg,
};
// Send message
Ok(Response::new()
.add_message(send.into_cosmos_msg(contract.clone())?)
.add_attribute("action", "send_nft")
.add_attribute("sender", info.sender)
.add_attribute("recipient", contract)
.add_attribute("token_id", token_id))
}
fn approve(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
spender: String,
token_id: String,
expires: Option<Expiration>,
) -> Result<Response<C>, ContractError> {
self._update_approvals(deps, &env, &info, &spender, &token_id, true, expires)?;
Ok(Response::new()
.add_attribute("action", "approve")
.add_attribute("sender", info.sender)
.add_attribute("spender", spender)
.add_attribute("token_id", token_id))
}
fn revoke(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
spender: String,
token_id: String,
) -> Result<Response<C>, ContractError> {
self._update_approvals(deps, &env, &info, &spender, &token_id, false, None)?;
Ok(Response::new()
.add_attribute("action", "revoke")
.add_attribute("sender", info.sender)
.add_attribute("spender", spender)
.add_attribute("token_id", token_id))
}
fn approve_all(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
operator: String,
expires: Option<Expiration>,
) -> Result<Response<C>, ContractError> {
// reject expired data as invalid
let expires = expires.unwrap_or_default();
if expires.is_expired(&env.block) {
return Err(ContractError::Expired {});
}
// set the operator for us
let operator_addr = deps.api.addr_validate(&operator)?;
self.operators
.save(deps.storage, (&info.sender, &operator_addr), &expires)?;
Ok(Response::new()
.add_attribute("action", "approve_all")
.add_attribute("sender", info.sender)
.add_attribute("operator", operator))
}
fn revoke_all(
&self,
deps: DepsMut,
_env: Env,
info: MessageInfo,
operator: String,
) -> Result<Response<C>, ContractError> {
let operator_addr = deps.api.addr_validate(&operator)?;
self.operators
.remove(deps.storage, (&info.sender, &operator_addr));
Ok(Response::new()
.add_attribute("action", "revoke_all")
.add_attribute("sender", info.sender)
.add_attribute("operator", operator))
}
fn burn(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
token_id: String,
) -> Result<Response<C>, ContractError> {
let token = self.tokens.load(deps.storage, &token_id)?;
self.check_can_send(deps.as_ref(), &env, &info, &token)?;
self.tokens.remove(deps.storage, &token_id)?;
self.decrement_tokens(deps.storage)?;
Ok(Response::new()
.add_attribute("action", "burn")
.add_attribute("sender", info.sender)
.add_attribute("token_id", token_id))
}
}
// helpers
impl<'a, T, C> Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
pub fn _transfer_nft(
&self,
deps: DepsMut,
env: &Env,
info: &MessageInfo,
recipient: &str,
token_id: &str,
) -> Result<TokenInfo<T>, ContractError> {
let mut token = self.tokens.load(deps.storage, token_id)?;
// ensure we have permissions
self.check_can_send(deps.as_ref(), env, info, &token)?;
// set owner and remove existing approvals
token.owner = deps.api.addr_validate(recipient)?;
token.approvals = vec![];
self.tokens.save(deps.storage, token_id, &token)?;
Ok(token)
}
#[allow(clippy::too_many_arguments)]
pub fn _update_approvals(
&self,
deps: DepsMut,
env: &Env,
info: &MessageInfo,
spender: &str,
token_id: &str,
// if add == false, remove. if add == true, remove then set with this expiration
add: bool,
expires: Option<Expiration>,
) -> Result<TokenInfo<T>, ContractError> {
let mut token = self.tokens.load(deps.storage, token_id)?;
// ensure we have permissions
self.check_can_approve(deps.as_ref(), env, info, &token)?;
// update the approval list (remove any for the same spender before adding)
let spender_addr = deps.api.addr_validate(spender)?;
token.approvals = token
.approvals
.into_iter()
.filter(|apr| apr.spender != spender_addr)
.collect();
// only difference between approve and revoke
if add {
// reject expired data as invalid
let expires = expires.unwrap_or_default();
if expires.is_expired(&env.block) {
return Err(ContractError::Expired {});
}
let approval = Approval {
spender: spender_addr,
expires,
};
token.approvals.push(approval);
}
self.tokens.save(deps.storage, token_id, &token)?;
Ok(token)
}
/// returns true iff the sender can execute approve or reject on the contract
pub fn check_can_approve(
&self,
deps: Deps,
env: &Env,
info: &MessageInfo,
token: &TokenInfo<T>,
) -> Result<(), ContractError> {
// owner can approve
if token.owner == info.sender {
return Ok(());
}
// operator can approve
let op = self
.operators
.may_load(deps.storage, (&token.owner, &info.sender))?;
match op {
Some(ex) => {
if ex.is_expired(&env.block) {
Err(ContractError::Unauthorized {})
} else {
Ok(())
}
}
None => Err(ContractError::Unauthorized {}),
}
}
/// returns true iff the sender can transfer ownership of the token
pub fn check_can_send(
&self,
deps: Deps,
env: &Env,
info: &MessageInfo,
token: &TokenInfo<T>,
) -> Result<(), ContractError> {
// owner can send
if token.owner == info.sender {
return Ok(());
}
// any non-expired token approval can send
if token
.approvals
.iter()
.any(|apr| apr.spender == info.sender && !apr.is_expired(&env.block))
{
return Ok(());
}
// operator can send
let op = self
.operators
.may_load(deps.storage, (&token.owner, &info.sender))?;
match op {
Some(ex) => {
if ex.is_expired(&env.block) {
Err(ContractError::Unauthorized {})
} else {
Ok(())
}
}
None => Err(ContractError::Unauthorized {}),
}
}
}

View File

@ -0,0 +1,179 @@
use crate::{ExecuteMsg, QueryMsg};
use cosmwasm_std::{to_binary, Addr, CosmosMsg, QuerierWrapper, StdResult, WasmMsg, WasmQuery};
use cw721::{
AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, ContractInfoResponse,
NftInfoResponse, NumTokensResponse, OperatorsResponse, OwnerOfResponse, TokensResponse,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Cw721Contract(pub Addr);
#[allow(dead_code)]
impl Cw721Contract {
pub fn addr(&self) -> Addr {
self.0.clone()
}
pub fn call<T: Serialize>(&self, msg: ExecuteMsg<T>) -> StdResult<CosmosMsg> {
let msg = to_binary(&msg)?;
Ok(WasmMsg::Execute {
contract_addr: self.addr().into(),
msg,
funds: vec![],
}
.into())
}
pub fn query<T: DeserializeOwned>(
&self,
querier: &QuerierWrapper,
req: QueryMsg,
) -> StdResult<T> {
let query = WasmQuery::Smart {
contract_addr: self.addr().into(),
msg: to_binary(&req)?,
}
.into();
querier.query(&query)
}
/*** queries ***/
pub fn owner_of<T: Into<String>>(
&self,
querier: &QuerierWrapper,
token_id: T,
include_expired: bool,
) -> StdResult<OwnerOfResponse> {
let req = QueryMsg::OwnerOf {
token_id: token_id.into(),
include_expired: Some(include_expired),
};
self.query(querier, req)
}
pub fn approval<T: Into<String>>(
&self,
querier: &QuerierWrapper,
token_id: T,
spender: T,
include_expired: Option<bool>,
) -> StdResult<ApprovalResponse> {
let req = QueryMsg::Approval {
token_id: token_id.into(),
spender: spender.into(),
include_expired,
};
let res: ApprovalResponse = self.query(querier, req)?;
Ok(res)
}
pub fn approvals<T: Into<String>>(
&self,
querier: &QuerierWrapper,
token_id: T,
include_expired: Option<bool>,
) -> StdResult<ApprovalsResponse> {
let req = QueryMsg::Approvals {
token_id: token_id.into(),
include_expired,
};
let res: ApprovalsResponse = self.query(querier, req)?;
Ok(res)
}
pub fn all_operators<T: Into<String>>(
&self,
querier: &QuerierWrapper,
owner: T,
include_expired: bool,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<Vec<Approval>> {
let req = QueryMsg::AllOperators {
owner: owner.into(),
include_expired: Some(include_expired),
start_after,
limit,
};
let res: OperatorsResponse = self.query(querier, req)?;
Ok(res.operators)
}
pub fn num_tokens(&self, querier: &QuerierWrapper) -> StdResult<u64> {
let req = QueryMsg::NumTokens {};
let res: NumTokensResponse = self.query(querier, req)?;
Ok(res.count)
}
/// With metadata extension
pub fn contract_info(&self, querier: &QuerierWrapper) -> StdResult<ContractInfoResponse> {
let req = QueryMsg::ContractInfo {};
self.query(querier, req)
}
/// With metadata extension
pub fn nft_info<T: Into<String>, U: DeserializeOwned>(
&self,
querier: &QuerierWrapper,
token_id: T,
) -> StdResult<NftInfoResponse<U>> {
let req = QueryMsg::NftInfo {
token_id: token_id.into(),
};
self.query(querier, req)
}
/// With metadata extension
pub fn all_nft_info<T: Into<String>, U: DeserializeOwned>(
&self,
querier: &QuerierWrapper,
token_id: T,
include_expired: bool,
) -> StdResult<AllNftInfoResponse<U>> {
let req = QueryMsg::AllNftInfo {
token_id: token_id.into(),
include_expired: Some(include_expired),
};
self.query(querier, req)
}
/// With enumerable extension
pub fn tokens<T: Into<String>>(
&self,
querier: &QuerierWrapper,
owner: T,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<TokensResponse> {
let req = QueryMsg::Tokens {
owner: owner.into(),
start_after,
limit,
};
self.query(querier, req)
}
/// With enumerable extension
pub fn all_tokens(
&self,
querier: &QuerierWrapper,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<TokensResponse> {
let req = QueryMsg::AllTokens { start_after, limit };
self.query(querier, req)
}
/// returns true if the contract supports the metadata extension
pub fn has_metadata(&self, querier: &QuerierWrapper) -> bool {
self.contract_info(querier).is_ok()
}
/// returns true if the contract supports the enumerable extension
pub fn has_enumerable(&self, querier: &QuerierWrapper) -> bool {
self.tokens(querier, self.addr(), None, Some(1)).is_ok()
}
}

View File

@ -0,0 +1,52 @@
mod contract_tests;
mod error;
mod execute;
pub mod helpers;
pub mod msg;
mod query;
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, Env, MessageInfo, Response, StdResult};
// 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)
}
}

View File

@ -0,0 +1,154 @@
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
pub name: String,
/// Symbol of the NFT contract
pub symbol: String,
/// 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
pub minter: String,
}
/// 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.
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg<T> {
/// Transfer is a base message to move a token to another account without triggering actions
TransferNft { recipient: String, token_id: String },
/// Send is a base message to transfer a token to a contract and trigger an action
/// on the receiving contract.
SendNft {
contract: String,
token_id: String,
msg: Binary,
},
/// Allows operator to transfer / send the token from the owner's account.
/// If expiration is set, then this allowance has a time/height limit
Approve {
spender: String,
token_id: String,
expires: Option<Expiration>,
},
/// Remove previously granted Approval
Revoke { spender: String, token_id: String },
/// Allows operator to transfer / send any token from the owner's account.
/// If expiration is set, then this allowance has a time/height limit
ApproveAll {
operator: String,
expires: Option<Expiration>,
},
/// Remove previously granted ApproveAll permission
RevokeAll { operator: String },
/// Mint a new NFT, can only be called by the contract minter
Mint(MintMsg<T>),
/// Burn an NFT the sender has access to
Burn { token_id: String },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MintMsg<T> {
/// Unique ID of the NFT
pub token_id: String,
/// The owner of the newly minter NFT
pub owner: String,
/// Universal resource identifier for this NFT
/// Should point to a JSON file that conforms to the ERC721
/// Metadata JSON Schema
pub token_uri: Option<String>,
/// Any custom extension used by this contract
pub extension: T,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
/// Return the owner of the given token, error if token does not exist
/// Return type: OwnerOfResponse
OwnerOf {
token_id: String,
/// unset or false will filter out expired approvals, you must set to true to see them
include_expired: Option<bool>,
},
/// Return operator that can access all of the owner's tokens.
/// Return type: `ApprovalResponse`
Approval {
token_id: String,
spender: String,
include_expired: Option<bool>,
},
/// Return approvals that a token has
/// Return type: `ApprovalsResponse`
Approvals {
token_id: String,
include_expired: Option<bool>,
},
/// List all operators that can access all of the owner's tokens
/// Return type: `OperatorsResponse`
AllOperators {
owner: String,
/// unset or false will filter out expired items, you must set to true to see them
include_expired: Option<bool>,
start_after: Option<String>,
limit: Option<u32>,
},
/// Total number of tokens issued
NumTokens {},
/// With MetaData Extension.
/// Returns top-level metadata about the contract: `ContractInfoResponse`
ContractInfo {},
/// With MetaData Extension.
/// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema*
/// but directly from the contract: `NftInfoResponse`
NftInfo {
token_id: String,
},
/// With MetaData Extension.
/// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization
/// for clients: `AllNftInfo`
AllNftInfo {
token_id: String,
/// unset or false will filter out expired approvals, you must set to true to see them
include_expired: Option<bool>,
},
/// With Enumerable extension.
/// Returns all tokens owned by the given address, [] if unset.
/// Return type: TokensResponse.
Tokens {
owner: String,
start_after: Option<String>,
limit: Option<u32>,
},
/// With Enumerable extension.
/// Requires pagination. Lists all token_ids controlled by the contract.
/// Return type: TokensResponse.
AllTokens {
start_after: Option<String>,
limit: Option<u32>,
},
// Return the minter
Minter {},
}
/// Shows who can mint these tokens
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct MinterResponse {
pub minter: String,
}

View File

@ -0,0 +1,299 @@
use serde::de::DeserializeOwned;
use serde::Serialize;
use cosmwasm_std::{to_binary, Binary, BlockInfo, Deps, Env, Order, StdError, StdResult};
use cw0::maybe_addr;
use cw721::{
AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, CustomMsg,
Cw721Query, Expiration, NftInfoResponse, NumTokensResponse, OperatorsResponse, OwnerOfResponse,
TokensResponse,
};
use cw_storage_plus::Bound;
use crate::msg::{MinterResponse, QueryMsg};
use crate::state::{Approval, Cw721Contract, TokenInfo};
const DEFAULT_LIMIT: u32 = 10;
const MAX_LIMIT: u32 = 30;
impl<'a, T, C> Cw721Query<T> for Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
fn contract_info(&self, deps: Deps) -> StdResult<ContractInfoResponse> {
self.contract_info.load(deps.storage)
}
fn num_tokens(&self, deps: Deps) -> StdResult<NumTokensResponse> {
let count = self.token_count(deps.storage)?;
Ok(NumTokensResponse { count })
}
fn nft_info(&self, deps: Deps, token_id: String) -> StdResult<NftInfoResponse<T>> {
let info = self.tokens.load(deps.storage, &token_id)?;
Ok(NftInfoResponse {
token_uri: info.token_uri,
extension: info.extension,
})
}
fn owner_of(
&self,
deps: Deps,
env: Env,
token_id: String,
include_expired: bool,
) -> StdResult<OwnerOfResponse> {
let info = self.tokens.load(deps.storage, &token_id)?;
Ok(OwnerOfResponse {
owner: info.owner.to_string(),
approvals: humanize_approvals(&env.block, &info, include_expired),
})
}
/// operators returns all operators owner given access to
fn operators(
&self,
deps: Deps,
env: Env,
owner: String,
include_expired: bool,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<OperatorsResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start_addr = maybe_addr(deps.api, start_after)?;
let start = start_addr.map(|addr| Bound::exclusive(addr.as_ref()));
let owner_addr = deps.api.addr_validate(&owner)?;
let res: StdResult<Vec<_>> = self
.operators
.prefix(&owner_addr)
.range(deps.storage, start, None, Order::Ascending)
.filter(|r| {
include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block)
})
.take(limit)
.map(parse_approval)
.collect();
Ok(OperatorsResponse { operators: res? })
}
fn approval(
&self,
deps: Deps,
env: Env,
token_id: String,
spender: String,
include_expired: bool,
) -> StdResult<ApprovalResponse> {
let token = self.tokens.load(deps.storage, &token_id)?;
let filtered: Vec<_> = token
.approvals
.into_iter()
.filter(|t| t.spender == spender)
.filter(|t| include_expired || !t.is_expired(&env.block))
.map(|a| cw721::Approval {
spender: a.spender.into_string(),
expires: a.expires,
})
.collect();
if filtered.is_empty() {
return Err(StdError::not_found("Approval not found"));
}
// we expect only one item
let approval = filtered[0].clone();
Ok(ApprovalResponse { approval })
}
/// approvals returns all approvals owner given access to
fn approvals(
&self,
deps: Deps,
env: Env,
token_id: String,
include_expired: bool,
) -> StdResult<ApprovalsResponse> {
let token = self.tokens.load(deps.storage, &token_id)?;
let approvals: Vec<_> = token
.approvals
.into_iter()
.filter(|t| include_expired || !t.is_expired(&env.block))
.map(|a| cw721::Approval {
spender: a.spender.into_string(),
expires: a.expires,
})
.collect();
Ok(ApprovalsResponse { approvals })
}
fn tokens(
&self,
deps: Deps,
owner: String,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<TokensResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let owner_addr = deps.api.addr_validate(&owner)?;
let pks: Vec<_> = self
.tokens
.idx
.owner
.prefix(owner_addr)
.keys(deps.storage, start, None, Order::Ascending)
.take(limit)
.collect();
let res: Result<Vec<_>, _> = pks.iter().map(|v| String::from_utf8(v.to_vec())).collect();
let tokens = res.map_err(StdError::invalid_utf8)?;
Ok(TokensResponse { tokens })
}
fn all_tokens(
&self,
deps: Deps,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<TokensResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let tokens: StdResult<Vec<String>> = self
.tokens
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|item| item.map(|(k, _)| String::from_utf8_lossy(&k).to_string()))
.collect();
Ok(TokensResponse { tokens: tokens? })
}
fn all_nft_info(
&self,
deps: Deps,
env: Env,
token_id: String,
include_expired: bool,
) -> StdResult<AllNftInfoResponse<T>> {
let info = self.tokens.load(deps.storage, &token_id)?;
Ok(AllNftInfoResponse {
access: OwnerOfResponse {
owner: info.owner.to_string(),
approvals: humanize_approvals(&env.block, &info, include_expired),
},
info: NftInfoResponse {
token_uri: info.token_uri,
extension: info.extension,
},
})
}
}
impl<'a, T, C> Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
pub fn minter(&self, deps: Deps) -> StdResult<MinterResponse> {
let minter_addr = self.minter.load(deps.storage)?;
Ok(MinterResponse {
minter: minter_addr.to_string(),
})
}
pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Minter {} => to_binary(&self.minter(deps)?),
QueryMsg::ContractInfo {} => to_binary(&self.contract_info(deps)?),
QueryMsg::NftInfo { token_id } => to_binary(&self.nft_info(deps, token_id)?),
QueryMsg::OwnerOf {
token_id,
include_expired,
} => {
to_binary(&self.owner_of(deps, env, token_id, include_expired.unwrap_or(false))?)
}
QueryMsg::AllNftInfo {
token_id,
include_expired,
} => to_binary(&self.all_nft_info(
deps,
env,
token_id,
include_expired.unwrap_or(false),
)?),
QueryMsg::AllOperators {
owner,
include_expired,
start_after,
limit,
} => to_binary(&self.operators(
deps,
env,
owner,
include_expired.unwrap_or(false),
start_after,
limit,
)?),
QueryMsg::NumTokens {} => to_binary(&self.num_tokens(deps)?),
QueryMsg::Tokens {
owner,
start_after,
limit,
} => to_binary(&self.tokens(deps, owner, start_after, limit)?),
QueryMsg::AllTokens { start_after, limit } => {
to_binary(&self.all_tokens(deps, start_after, limit)?)
}
QueryMsg::Approval {
token_id,
spender,
include_expired,
} => to_binary(&self.approval(
deps,
env,
token_id,
spender,
include_expired.unwrap_or(false),
)?),
QueryMsg::Approvals {
token_id,
include_expired,
} => {
to_binary(&self.approvals(deps, env, token_id, include_expired.unwrap_or(false))?)
}
}
}
}
type Record<V = Vec<u8>> = (Vec<u8>, V);
fn parse_approval(item: StdResult<Record<Expiration>>) -> StdResult<cw721::Approval> {
item.and_then(|(k, expires)| {
let spender = String::from_utf8(k)?;
Ok(cw721::Approval { spender, expires })
})
}
fn humanize_approvals<T>(
block: &BlockInfo,
info: &TokenInfo<T>,
include_expired: bool,
) -> Vec<cw721::Approval> {
info.approvals
.iter()
.filter(|apr| include_expired || !apr.is_expired(block))
.map(humanize_approval)
.collect()
}
fn humanize_approval(approval: &Approval) -> cw721::Approval {
cw721::Approval {
spender: approval.spender.to_string(),
expires: approval.expires,
}
}

View File

@ -0,0 +1,141 @@
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
use cosmwasm_std::{Addr, BlockInfo, StdResult, Storage};
use cw721::{ContractInfoResponse, CustomMsg, Cw721, Expiration};
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex};
pub struct Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
{
pub contract_info: Item<'a, ContractInfoResponse>,
pub minter: Item<'a, Addr>,
pub token_count: Item<'a, u64>,
/// Stored as (granter, operator) giving operator full control over granter's account
pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>,
pub tokens: IndexedMap<'a, &'a str, TokenInfo<T>, TokenIndexes<'a, T>>,
pub(crate) _custom_response: PhantomData<C>,
}
// This is a signal, the implementations are in other files
impl<'a, T, C> Cw721<T, C> for Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
}
impl<T, C> Default for Cw721Contract<'static, T, C>
where
T: Serialize + DeserializeOwned + Clone,
{
fn default() -> Self {
Self::new(
"nft_info",
"minter",
"num_tokens",
"operators",
"tokens",
"tokens__owner",
)
}
}
impl<'a, T, C> Cw721Contract<'a, T, C>
where
T: Serialize + DeserializeOwned + Clone,
{
fn new(
contract_key: &'a str,
minter_key: &'a str,
token_count_key: &'a str,
operator_key: &'a str,
tokens_key: &'a str,
tokens_owner_key: &'a str,
) -> Self {
let indexes = TokenIndexes {
owner: MultiIndex::new(token_owner_idx, tokens_key, tokens_owner_key),
};
Self {
contract_info: Item::new(contract_key),
minter: Item::new(minter_key),
token_count: Item::new(token_count_key),
operators: Map::new(operator_key),
tokens: IndexedMap::new(tokens_key, indexes),
_custom_response: PhantomData,
}
}
pub fn token_count(&self, storage: &dyn Storage) -> StdResult<u64> {
Ok(self.token_count.may_load(storage)?.unwrap_or_default())
}
pub fn increment_tokens(&self, storage: &mut dyn Storage) -> StdResult<u64> {
let val = self.token_count(storage)? + 1;
self.token_count.save(storage, &val)?;
Ok(val)
}
pub fn decrement_tokens(&self, storage: &mut dyn Storage) -> StdResult<u64> {
let val = self.token_count(storage)? - 1;
self.token_count.save(storage, &val)?;
Ok(val)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TokenInfo<T> {
/// The owner of the newly minted NFT
pub owner: Addr,
/// Approvals are stored here, as we clear them all upon transfer and cannot accumulate much
pub approvals: Vec<Approval>,
/// Universal resource identifier for this NFT
/// Should point to a JSON file that conforms to the ERC721
/// Metadata JSON Schema
pub token_uri: Option<String>,
/// You can add any custom metadata here when you extend cw721-base
pub extension: T,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Approval {
/// Account that can transfer/send the token
pub spender: Addr,
/// When the Approval expires (maybe Expiration::never)
pub expires: Expiration,
}
impl Approval {
pub fn is_expired(&self, block: &BlockInfo) -> bool {
self.expires.is_expired(block)
}
}
pub struct TokenIndexes<'a, T>
where
T: Serialize + DeserializeOwned + Clone,
{
// pk goes to second tuple element
pub owner: MultiIndex<'a, (Addr, Vec<u8>), TokenInfo<T>>,
}
impl<'a, T> IndexList<TokenInfo<T>> for TokenIndexes<'a, T>
where
T: Serialize + DeserializeOwned + Clone,
{
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<TokenInfo<T>>> + '_> {
let v: Vec<&dyn Index<TokenInfo<T>>> = vec![&self.owner];
Box::new(v.into_iter())
}
}
pub fn token_owner_idx<T>(d: &TokenInfo<T>, k: Vec<u8>) -> (Addr, Vec<u8>) {
(d.owner.clone(), k)
}

View File

@ -0,0 +1,4 @@
[alias]
wasm = "build --release --target wasm32-unknown-unknown"
wasm-debug = "build --target wasm32-unknown-unknown"
schema = "run --example schema"

View File

@ -0,0 +1,19 @@
[package]
name = "cw721"
version = "0.10.1"
authors = ["Ethan Frey <ethanfrey@users.noreply.github.com>"]
edition = "2018"
description = "Definition and types for the CosmWasm-721 NFT interface"
license = "Apache-2.0"
repository = "https://github.com/CosmWasm/cw-nfts"
homepage = "https://cosmwasm.com"
documentation = "https://docs.cosmwasm.com"
[dependencies]
cw0 = { version = "0.8.0" }
cosmwasm-std = { version = "0.16.0" }
schemars = "0.8.6"
serde = { version = "1.0.130", default-features = false, features = ["derive"] }
[dev-dependencies]
cosmwasm-schema = { version = "1.0.0-beta2" }

View File

@ -0,0 +1,14 @@
CW721: A CosmWasm spec for non-fungible token contracts
Copyright (C) 2020-2021 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.

View File

@ -0,0 +1,122 @@
# CW721 Spec: Non Fungible Tokens
CW721 is a specification for non-fungible tokens based on CosmWasm.
The name and design is based on Ethereum's ERC721 standard,
with some enhancements. The types in here can be imported by
contracts that wish to implement this spec, or by contracts that call
to any standard cw721 contract.
The specification is split into multiple sections, a contract may only
implement some of this functionality, but must implement the base.
## Base
This handles ownership, transfers, and allowances. These must be supported
as is by all CW721 contracts. Note that all tokens must have an owner,
as well as an ID. The ID is an arbitrary string, unique within the contract.
### Messages
`TransferNft{recipient, token_id}` -
This transfers ownership of the token to `recipient` account. This is
designed to send to an address controlled by a private key and *does not*
trigger any actions on the recipient if it is a contract.
Requires `token_id` to point to a valid token, and `env.sender` to be
the owner of it, or have an allowance to transfer it.
`SendNft{contract, token_id, msg}` -
This transfers ownership of the token to `contract` account. `contract`
must be an address controlled by a smart contract, which implements
the CW721Receiver interface. The `msg` will be passed to the recipient
contract, along with the token_id.
Requires `token_id` to point to a valid token, and `env.sender` to be
the owner of it, or have an allowance to transfer it.
`Approve{spender, token_id, expires}` - Grants permission to `spender` to
transfer or send the given token. This can only be performed when
`env.sender` is the owner of the given `token_id` or an `operator`.
There can multiple spender accounts per token, and they are cleared once
the token is transfered or sent.
`Revoke{spender, token_id}` - This revokes a previously granted permission
to transfer the given `token_id`. This can only be granted when
`env.sender` is the owner of the given `token_id` or an `operator`.
`ApproveAll{operator, expires}` - Grant `operator` permission to transfer or send
all tokens owned by `env.sender`. This approval is tied to the owner, not the
tokens and applies to any future token that the owner receives as well.
`RevokeAll{operator}` - Revoke a previous `ApproveAll` permission granted
to the given `operator`.
### Queries
`OwnerOf{token_id}` - Returns the owner of the given token,
as well as anyone with approval on this particular token.
If the token is unknown, returns an error. Return type is
`OwnerResponse{owner}`.
`ApprovedForAll{owner, include_expired}` - List all operators that can
access all of the owner's tokens. Return type is `ApprovedForAllResponse`.
If `include_expired` is set, show expired owners in the results, otherwise,
ignore them.
`NumTokens{}` - Total number of tokens issued
### Receiver
The counter-part to `SendNft` is `ReceiveNft`, which must be implemented by
any contract that wishes to manage CW721 tokens. This is generally *not*
implemented by any CW721 contract.
`ReceiveNft{sender, token_id, msg}` - This is designed to handle `SendNft`
messages. The address of the contract is stored in `env.sender`
so it cannot be faked. The contract should ensure the sender matches
the token contract it expects to handle, and not allow arbitrary addresses.
The `sender` is the original account requesting to move the token
and `msg` is a `Binary` data that can be decoded into a contract-specific
message. This can be empty if we have only one default action,
or it may be a `ReceiveMsg` variant to clarify the intention. For example,
if I send to an exchange, I can specify the price I want to list the token
for.
## Metadata
### Queries
`ContractInfo{}` - This returns top-level metadata about the contract.
Namely, `name` and `symbol`.
`NftInfo{token_id}` - This returns metadata about one particular token.
The return value is based on *ERC721 Metadata JSON Schema*, but directly
from the contract, not as a Uri. Only the image link is a Uri.
`AllNftInfo{token_id}` - This returns the result of both `NftInfo`
and `OwnerOf` as one query as an optimization for clients, which may
want both info to display one NFT.
## Enumerable
### Queries
Pagination is acheived via `start_after` and `limit`. Limit is a request
set by the client, if unset, the contract will automatically set it to
`DefaultLimit` (suggested 10). If set, it will be used up to a `MaxLimit`
value (suggested 30). Contracts can define other `DefaultLimit` and `MaxLimit`
values without violating the CW721 spec, and clients should not rely on
any particular values.
If `start_after` is unset, the query returns the first results, ordered by
lexogaphically by `token_id`. If `start_after` is set, then it returns the
first `limit` tokens *after* the given one. This allows straight-forward
pagination by taking the last result returned (a `token_id`) and using it
as the `start_after` value in a future query.
`Tokens{owner, start_after, limit}` - List all token_ids that belong to a given owner.
Return type is `TokensResponse{tokens: Vec<token_id>}`.
`AllTokens{start_after, limit}` - Requires pagination. Lists all token_ids controlled by
the contract.

View File

@ -0,0 +1,41 @@
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 cosmwasm_std::Empty;
use cw721::{
AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, Cw721ExecuteMsg,
Cw721QueryMsg, Cw721ReceiveMsg, NftInfoResponse, NumTokensResponse, OperatorsResponse,
OwnerOfResponse, TokensResponse,
};
type Extension = Option<Empty>;
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!(Cw721ExecuteMsg), &out_dir);
export_schema(&schema_for!(Cw721QueryMsg), &out_dir);
export_schema(&schema_for!(Cw721ReceiveMsg), &out_dir);
export_schema_with_title(
&schema_for!(AllNftInfoResponse<Extension>),
&out_dir,
"AllNftInfoResponse",
);
export_schema(&schema_for!(ApprovalResponse), &out_dir);
export_schema(&schema_for!(ApprovalsResponse), &out_dir);
export_schema(&schema_for!(OperatorsResponse), &out_dir);
export_schema(&schema_for!(ContractInfoResponse), &out_dir);
export_schema(&schema_for!(OwnerOfResponse), &out_dir);
export_schema_with_title(
&schema_for!(NftInfoResponse<Extension>),
&out_dir,
"NftInfoResponse",
);
export_schema(&schema_for!(NumTokensResponse), &out_dir);
export_schema(&schema_for!(TokensResponse), &out_dir);
}

View File

@ -0,0 +1,155 @@
{
"$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_Nullable_Empty"
}
]
}
},
"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"
}
}
},
"Empty": {
"description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
"type": "object"
},
"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)",
"oneOf": [
{
"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
}
]
},
"NftInfoResponse_for_Nullable_Empty": {
"type": "object",
"properties": {
"extension": {
"description": "You can add any custom metadata here when you extend cw721-base",
"anyOf": [
{
"$ref": "#/definitions/Empty"
},
{
"type": "null"
}
]
},
"token_uri": {
"description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema",
"type": [
"string",
"null"
]
}
}
},
"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"
}
}
}

View File

@ -0,0 +1,94 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ApprovalResponse",
"type": "object",
"required": [
"approval"
],
"properties": {
"approval": {
"$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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -0,0 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ApprovalsResponse",
"type": "object",
"required": [
"approvals"
],
"properties": {
"approvals": {
"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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,236 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Cw721ExecuteMsg",
"oneOf": [
{
"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
}
],
"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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -0,0 +1,240 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Cw721QueryMsg",
"oneOf": [
{
"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": "Return operator that can access all of the owner's tokens. Return type: `ApprovedResponse`",
"type": "object",
"required": [
"approved"
],
"properties": {
"approved": {
"type": "object",
"required": [
"operator",
"owner"
],
"properties": {
"operator": {
"type": "string"
},
"owner": {
"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 approvals, 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
}
]
}

View File

@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Cw721ReceiveMsg",
"description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg",
"type": "object",
"required": [
"msg",
"sender",
"token_id"
],
"properties": {
"msg": {
"$ref": "#/definitions/Binary"
},
"sender": {
"type": "string"
},
"token_id": {
"type": "string"
}
},
"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"
}
}
}

View File

@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "NftInfoResponse",
"type": "object",
"properties": {
"extension": {
"description": "You can add any custom metadata here when you extend cw721-base",
"anyOf": [
{
"$ref": "#/definitions/Empty"
},
{
"type": "null"
}
]
},
"token_uri": {
"description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema",
"type": [
"string",
"null"
]
}
},
"definitions": {
"Empty": {
"description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
"type": "object"
}
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "OperatorsResponse",
"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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -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)",
"oneOf": [
{
"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"
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,15 @@
mod msg;
mod query;
mod receiver;
mod traits;
pub use cw0::Expiration;
pub use crate::msg::Cw721ExecuteMsg;
pub use crate::query::{
AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, ContractInfoResponse,
Cw721QueryMsg, NftInfoResponse, NumTokensResponse, OperatorsResponse, OwnerOfResponse,
TokensResponse,
};
pub use crate::receiver::Cw721ReceiveMsg;
pub use crate::traits::{CustomMsg, Cw721, Cw721Execute, Cw721Query};

View File

@ -0,0 +1,36 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::Binary;
use cw0::Expiration;
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Cw721ExecuteMsg {
/// Transfer is a base message to move a token to another account without triggering actions
TransferNft { recipient: String, token_id: String },
/// Send is a base message to transfer a token to a contract and trigger an action
/// on the receiving contract.
SendNft {
contract: String,
token_id: String,
msg: Binary,
},
/// Allows operator to transfer / send the token from the owner's account.
/// If expiration is set, then this allowance has a time/height limit
Approve {
spender: String,
token_id: String,
expires: Option<Expiration>,
},
/// Remove previously granted Approval
Revoke { spender: String, token_id: String },
/// Allows operator to transfer / send any token from the owner's account.
/// If expiration is set, then this allowance has a time/height limit
ApproveAll {
operator: String,
expires: Option<Expiration>,
},
/// Remove previously granted ApproveAll permission
RevokeAll { operator: String },
}

View File

@ -0,0 +1,132 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cw0::Expiration;
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Cw721QueryMsg {
/// Return the owner of the given token, error if token does not exist
/// Return type: OwnerOfResponse
OwnerOf {
token_id: String,
/// unset or false will filter out expired approvals, you must set to true to see them
include_expired: Option<bool>,
},
/// Return operator that can access all of the owner's tokens.
/// Return type: `ApprovedResponse`
Approved { owner: String, operator: String },
/// List all operators that can access all of the owner's tokens.
/// Return type: `ApprovedForAllResponse`
ApprovedForAll {
owner: String,
/// unset or false will filter out expired approvals, you must set to true to see them
include_expired: Option<bool>,
start_after: Option<String>,
limit: Option<u32>,
},
/// Total number of tokens issued
NumTokens {},
/// With MetaData Extension.
/// Returns top-level metadata about the contract: `ContractInfoResponse`
ContractInfo {},
/// With MetaData Extension.
/// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema*
/// but directly from the contract: `NftInfoResponse`
NftInfo { token_id: String },
/// With MetaData Extension.
/// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization
/// for clients: `AllNftInfo`
AllNftInfo {
token_id: String,
/// unset or false will filter out expired approvals, you must set to true to see them
include_expired: Option<bool>,
},
/// With Enumerable extension.
/// Returns all tokens owned by the given address, [] if unset.
/// Return type: TokensResponse.
Tokens {
owner: String,
start_after: Option<String>,
limit: Option<u32>,
},
/// With Enumerable extension.
/// Requires pagination. Lists all token_ids controlled by the contract.
/// Return type: TokensResponse.
AllTokens {
start_after: Option<String>,
limit: Option<u32>,
},
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct OwnerOfResponse {
/// Owner of the token
pub owner: String,
/// If set this address is approved to transfer/send the token as well
pub approvals: Vec<Approval>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Approval {
/// Account that can transfer/send the token
pub spender: String,
/// When the Approval expires (maybe Expiration::never)
pub expires: Expiration,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct ApprovalResponse {
pub approval: Approval,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct ApprovalsResponse {
pub approvals: Vec<Approval>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct OperatorsResponse {
pub operators: Vec<Approval>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct NumTokensResponse {
pub count: u64,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct ContractInfoResponse {
pub name: String,
pub symbol: String,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct NftInfoResponse<T> {
/// Universal resource identifier for this NFT
/// Should point to a JSON file that conforms to the ERC721
/// Metadata JSON Schema
pub token_uri: Option<String>,
/// You can add any custom metadata here when you extend cw721-base
pub extension: T,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct AllNftInfoResponse<T> {
/// Who can transfer the token
pub access: OwnerOfResponse,
/// Data on the token itself,
pub info: NftInfoResponse<T>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct TokensResponse {
/// Contains all token_ids in lexicographical ordering
/// If there are more than `limit`, use `start_from` in future queries
/// to achieve pagination.
pub tokens: Vec<String>,
}

View File

@ -0,0 +1,43 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::{to_binary, Binary, CosmosMsg, StdResult, WasmMsg};
/// Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub struct Cw721ReceiveMsg {
pub sender: String,
pub token_id: String,
pub msg: Binary,
}
impl Cw721ReceiveMsg {
/// serializes the message
pub fn into_binary(self) -> StdResult<Binary> {
let msg = ReceiverExecuteMsg::ReceiveNft(self);
to_binary(&msg)
}
/// creates a cosmos_msg sending this struct to the named contract
pub fn into_cosmos_msg<T: Into<String>, C>(self, contract_addr: T) -> StdResult<CosmosMsg<C>>
where
C: Clone + std::fmt::Debug + PartialEq + JsonSchema,
{
let msg = self.into_binary()?;
let execute = WasmMsg::Execute {
contract_addr: contract_addr.into(),
msg,
funds: vec![],
};
Ok(execute.into())
}
}
/// This is just a helper to properly serialize the above message.
/// The actual receiver should include this variant in the larger ExecuteMsg enum
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
enum ReceiverExecuteMsg {
ReceiveNft(Cw721ReceiveMsg),
}

View File

@ -0,0 +1,166 @@
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::query::ApprovalResponse;
use crate::{
AllNftInfoResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse,
NumTokensResponse, OperatorsResponse, OwnerOfResponse, TokensResponse,
};
use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
use cw0::Expiration;
// TODO: move this somewhere else... ideally cosmwasm-std
pub trait CustomMsg: Clone + std::fmt::Debug + PartialEq + JsonSchema {}
impl CustomMsg for Empty {}
pub trait Cw721<T, C>: Cw721Execute<T, C> + Cw721Query<T>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
}
pub trait Cw721Execute<T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
type Err: ToString;
fn transfer_nft(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
recipient: String,
token_id: String,
) -> Result<Response<C>, Self::Err>;
fn send_nft(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
contract: String,
token_id: String,
msg: Binary,
) -> Result<Response<C>, Self::Err>;
fn approve(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
spender: String,
token_id: String,
expires: Option<Expiration>,
) -> Result<Response<C>, Self::Err>;
fn revoke(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
spender: String,
token_id: String,
) -> Result<Response<C>, Self::Err>;
fn approve_all(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
operator: String,
expires: Option<Expiration>,
) -> Result<Response<C>, Self::Err>;
fn revoke_all(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
operator: String,
) -> Result<Response<C>, Self::Err>;
fn burn(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
token_id: String,
) -> Result<Response<C>, Self::Err>;
}
pub trait Cw721Query<T>
where
T: Serialize + DeserializeOwned + Clone,
{
// TODO: use custom error?
// How to handle the two derived error types?
fn contract_info(&self, deps: Deps) -> StdResult<ContractInfoResponse>;
fn num_tokens(&self, deps: Deps) -> StdResult<NumTokensResponse>;
fn nft_info(&self, deps: Deps, token_id: String) -> StdResult<NftInfoResponse<T>>;
fn owner_of(
&self,
deps: Deps,
env: Env,
token_id: String,
include_expired: bool,
) -> StdResult<OwnerOfResponse>;
fn operators(
&self,
deps: Deps,
env: Env,
owner: String,
include_expired: bool,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<OperatorsResponse>;
fn approval(
&self,
deps: Deps,
env: Env,
token_id: String,
spender: String,
include_expired: bool,
) -> StdResult<ApprovalResponse>;
fn approvals(
&self,
deps: Deps,
env: Env,
token_id: String,
include_expired: bool,
) -> StdResult<ApprovalsResponse>;
fn tokens(
&self,
deps: Deps,
owner: String,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<TokensResponse>;
fn all_tokens(
&self,
deps: Deps,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<TokensResponse>;
fn all_nft_info(
&self,
deps: Deps,
env: Env,
token_id: String,
include_expired: bool,
) -> StdResult<AllNftInfoResponse<T>>;
}