cosmwasm: initialize
This commit is contained in:
parent
03487819ae
commit
c7d8075555
|
@ -73,6 +73,15 @@ jobs:
|
|||
node-version: "16"
|
||||
- run: cd terra && make test
|
||||
|
||||
terra-2:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
- run: cd cosmwasm && make test
|
||||
|
||||
# Run linters, Go tests and other outside-of-Tilt things.
|
||||
lint-and-tests:
|
||||
# The linter is slow enough that we want to run it on the self-hosted runner
|
||||
|
|
|
@ -20,6 +20,8 @@ bigtable-writer.json
|
|||
/solana/artifacts/
|
||||
/terra/artifacts/
|
||||
!/terra/artifacts/cw20_base.wasm
|
||||
/cosmwasm/artifacts/
|
||||
!/cosmwasm/artifacts/cw20_base.wasm
|
||||
/solana/artifacts-testnet/
|
||||
/solana/artifacts-devnet/
|
||||
/solana/artifacts-mainnet/
|
||||
|
|
|
@ -40,4 +40,5 @@ FROM scratch AS const-export
|
|||
COPY --from=const-build /scripts/.env.0x ethereum/.env
|
||||
COPY --from=const-build /scripts/.env.hex solana/.env
|
||||
COPY --from=const-build /scripts/.env.hex terra/tools/.env
|
||||
COPY --from=const-build /scripts/.env.hex cosmwasm/tools/.env
|
||||
COPY --from=const-build /scripts/.env.hex algorand/.env
|
||||
|
|
45
Tiltfile
45
Tiltfile
|
@ -191,7 +191,7 @@ def build_node_yaml():
|
|||
|
||||
k8s_yaml_with_ns(build_node_yaml())
|
||||
|
||||
guardian_resource_deps = ["proto-gen", "eth-devnet", "eth-devnet2", "terra-terrad"]
|
||||
guardian_resource_deps = ["proto-gen", "eth-devnet", "eth-devnet2", "terra-terrad", "terra2-terrad"]
|
||||
if solana:
|
||||
guardian_resource_deps = guardian_resource_deps + ["solana-devnet"]
|
||||
|
||||
|
@ -438,7 +438,7 @@ if ci_tests:
|
|||
|
||||
k8s_resource(
|
||||
"ci-tests",
|
||||
resource_deps = ["proto-gen-web", "wasm-gen", "eth-devnet", "eth-devnet2", "terra-terrad", "terra-fcd", "solana-devnet", "spy", "guardian"],
|
||||
resource_deps = ["proto-gen-web", "wasm-gen", "eth-devnet", "eth-devnet2", "terra-terrad", "terra-fcd", "terra2-terrad", "terra2-fcd", "solana-devnet", "spy", "guardian"],
|
||||
labels = ["ci"],
|
||||
trigger_mode = trigger_mode,
|
||||
)
|
||||
|
@ -561,6 +561,47 @@ k8s_resource(
|
|||
trigger_mode = trigger_mode,
|
||||
)
|
||||
|
||||
# terra 2 devnet
|
||||
|
||||
docker_build(
|
||||
ref = "terra2-image",
|
||||
context = "./cosmwasm/devnet",
|
||||
dockerfile = "cosmwasm/devnet/Dockerfile",
|
||||
)
|
||||
|
||||
docker_build(
|
||||
ref = "terra2-contracts",
|
||||
context = "./cosmwasm",
|
||||
dockerfile = "./cosmwasm/Dockerfile",
|
||||
)
|
||||
|
||||
k8s_yaml_with_ns("devnet/terra2-devnet.yaml")
|
||||
|
||||
k8s_resource(
|
||||
"terra2-terrad",
|
||||
port_forwards = [
|
||||
port_forward(26657, name = "Terra 2 RPC [:26657]", host = webHost),
|
||||
port_forward(1317, name = "Terra 2 LCD [:1317]", host = webHost),
|
||||
],
|
||||
resource_deps = ["const-gen"],
|
||||
labels = ["terra2"],
|
||||
trigger_mode = trigger_mode,
|
||||
)
|
||||
|
||||
k8s_resource(
|
||||
"terra2-postgres",
|
||||
labels = ["terra2"],
|
||||
trigger_mode = trigger_mode,
|
||||
)
|
||||
|
||||
k8s_resource(
|
||||
"terra2-fcd",
|
||||
resource_deps = ["terra2-terrad", "terra2-postgres"],
|
||||
port_forwards = [port_forward(3060, name = "Terra 2 FCD [:3060]", host = webHost)],
|
||||
labels = ["terra2"],
|
||||
trigger_mode = trigger_mode,
|
||||
)
|
||||
|
||||
if algorand:
|
||||
k8s_yaml_with_ns("devnet/algorand-devnet.yaml")
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
target
|
||||
tools/node_modules
|
||||
tools/dist
|
|
@ -0,0 +1,3 @@
|
|||
LocalTerra
|
||||
artifacts
|
||||
!artifacts/cw20_base.wasm
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,22 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"contracts/cw20-wrapped",
|
||||
"contracts/wormhole",
|
||||
"contracts/token-bridge",
|
||||
"contracts/nft-bridge",
|
||||
"contracts/cw721-wrapped",
|
||||
"packages/cw721",
|
||||
"contracts/cw721-base",
|
||||
"contracts/mock-bridge-integration",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
rpath = false
|
||||
lto = true
|
||||
debug-assertions = false
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
incremental = false
|
||||
overflow-checks = true
|
|
@ -0,0 +1,42 @@
|
|||
# This is a multi-stage docker file:
|
||||
# 1. The first stage builds the contracts
|
||||
# 2. The second is an empty image with only the wasm files (useful for exporting)
|
||||
# 3. The third creates a node.js environment to deploy the contracts to devnet
|
||||
FROM cosmwasm/workspace-optimizer:0.12.6@sha256:e6565a5e87c830ef3e8775a9035006b38ad0aaf0a96319158c802457b1dd1d08 AS builder
|
||||
COPY Cargo.lock /code/
|
||||
COPY Cargo.toml /code/
|
||||
COPY contracts /code/contracts
|
||||
COPY packages /code/packages
|
||||
|
||||
# Support additional root CAs
|
||||
COPY README.md cert.pem* /certs/
|
||||
# Alpine
|
||||
RUN if [ -e /certs/cert.pem ]; then cp /certs/cert.pem /etc/ssl/cert.pem; fi
|
||||
|
||||
RUN optimize_workspace.sh
|
||||
|
||||
FROM scratch as artifacts
|
||||
COPY --from=builder /code/artifacts /
|
||||
|
||||
# Contract deployment stage
|
||||
FROM node:16-buster-slim@sha256:93c9fc3550f5f7d159f282027228e90e3a7f8bf38544758024f005e82607f546
|
||||
|
||||
# Support additional root CAs
|
||||
COPY README.md cert.pem* /certs/
|
||||
# Node
|
||||
ENV NODE_EXTRA_CA_CERTS=/certs/cert.pem
|
||||
ENV NODE_OPTIONS=--use-openssl-ca
|
||||
# npm
|
||||
RUN if [ -e /certs/cert.pem ]; then npm config set cafile /certs/cert.pem; fi
|
||||
|
||||
RUN apt update && apt install netcat curl jq -y
|
||||
|
||||
WORKDIR /app/tools
|
||||
|
||||
COPY --from=artifacts / /app/artifacts
|
||||
COPY ./artifacts/cw20_base.wasm /app/artifacts/
|
||||
|
||||
COPY ./tools/package.json ./tools/package-lock.json /app/tools/
|
||||
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
|
||||
npm ci
|
||||
COPY ./tools /app/tools
|
|
@ -0,0 +1,87 @@
|
|||
bridge_SOURCE=wormhole
|
||||
token_bridge_SOURCE=token_bridge_terra
|
||||
nft_bridge_SOURCE=nft_bridge
|
||||
|
||||
SOURCE_FILES=$(shell find . -name "*.rs" -or -name "*.lock" -or -name "*.toml" | grep -v target)
|
||||
|
||||
PACKAGES=$(shell find . -name "Cargo.toml" | grep -E 'packages|contracts' | xargs cat | grep "name *=" | cut -d' ' -f3 | sed s/\"//g | sed s/-/_/g)
|
||||
WASMS=$(patsubst %, artifacts/%.wasm, $(PACKAGES))
|
||||
|
||||
-include ../Makefile.help
|
||||
|
||||
.PHONY: artifacts
|
||||
## Build contracts.
|
||||
artifacts: artifacts/checksums.txt
|
||||
|
||||
VALID_mainnet=1
|
||||
VALID_testnet=1
|
||||
VALID_devnet=1
|
||||
.PHONY: check-network
|
||||
check-network:
|
||||
ifndef VALID_$(NETWORK)
|
||||
$(error Invalid or missing NETWORK. Please call with `$(MAKE) $(MAKECMDGOALS) NETWORK=[mainnet | testnet | devnet]`)
|
||||
endif
|
||||
|
||||
$(WASMS) artifacts/checksums.txt: $(SOURCE_FILES)
|
||||
DOCKER_BUILDKIT=1 docker build --target artifacts -o artifacts .
|
||||
|
||||
payer-$(NETWORK).json:
|
||||
$(error Missing private key in payer-$(NETWORK).json)
|
||||
|
||||
.PHONY: deploy/bridge
|
||||
## Deploy core bridge
|
||||
deploy/bridge: bridge-code-id-$(NETWORK).txt
|
||||
|
||||
.PHONY: deploy/token_bridge
|
||||
## Deploy token bridge
|
||||
deploy/token_bridge: token_bridge-code-id-$(NETWORK).txt
|
||||
|
||||
.PHONY: deploy/nft_bridge
|
||||
## Deploy NFT bridge
|
||||
deploy/nft_bridge: nft_bridge-code-id-$(NETWORK).txt
|
||||
|
||||
%-code-id-$(NETWORK).txt: check-network tools/node_modules payer-$(NETWORK).json artifacts
|
||||
@echo "Deploying artifacts/$($*_SOURCE).wasm on $(NETWORK)"
|
||||
@node tools/deploy_single.js \
|
||||
--network $(NETWORK) \
|
||||
--artifact artifacts/$($*_SOURCE).wasm \
|
||||
--mnemonic "$$(cat payer-$(NETWORK).json)" \
|
||||
| grep -i "code id" | sed s/[^0-9]//g \
|
||||
> $@
|
||||
@echo "Deployed at code id $$(cat $@) (stored in $@)"
|
||||
|
||||
tools/node_modules: tools/package-lock.json
|
||||
cd tools && npm ci
|
||||
|
||||
LocalTerra:
|
||||
mkdir LocalTerra && \
|
||||
cd LocalTerra && \
|
||||
git init && \
|
||||
git remote add origin https://www.github.com/terra-money/LocalTerra.git && \
|
||||
git fetch --depth 1 origin 958ff795f261f5ff2efc7b56604e2434eb76f7c4 && \
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
test/node_modules: test/package-lock.json
|
||||
cd test && npm ci
|
||||
|
||||
.PHONY: unit-test
|
||||
## Run unit tests
|
||||
unit-test:
|
||||
cargo test -p wormhole-bridge-terra
|
||||
cargo test -p token-bridge-terra
|
||||
|
||||
.PHONY: test
|
||||
## Run unit and integration tests
|
||||
test: artifacts test/node_modules LocalTerra unit-test
|
||||
@if pgrep terrad; then echo "Error: terrad already running. Stop it before running tests"; exit 1; fi
|
||||
cd LocalTerra && docker compose up --detach
|
||||
sleep 5
|
||||
cd test && npm run test || (cd ../LocalTerra && docker compose down && exit 1)
|
||||
cd LocalTerra && docker compose down
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f $(WASMS)
|
||||
rm -f artifacts/checksums.txt
|
||||
rm -rf tools/node_modules
|
||||
rm -rf test/node_modules
|
|
@ -0,0 +1,136 @@
|
|||
# Terra Wormhole Contract Deployment
|
||||
|
||||
This readme describes the steps for building, verifying, and deploying Terra smart contracts for Wormhole.
|
||||
|
||||
**WARNING**: *This process is only Linux host compatible at this time.*
|
||||
|
||||
## Verify Tilt
|
||||
|
||||
Before building Terra contracts, ensure that the specific commit you will be
|
||||
building from passes in tilt. This that ensures basic functionality of the
|
||||
Terra smart contracts that you are about to build and deploy.
|
||||
|
||||
## Build Contracts
|
||||
|
||||
The following command can be used to build Terra contracts via Docker.
|
||||
|
||||
Build Target Options: [`mainnet`|`testnet`|`devnet`|
|
||||
|
||||
These network names correspond to the naming convention used by wormhole
|
||||
elsewhere. This means that `mainnet` corresponds to Terra `mainnet`,
|
||||
`testnet` corresponds to Terra `testnet`, and `devnet` is `localterra`.
|
||||
|
||||
```console
|
||||
wormhole/terra $ make artifacts
|
||||
```
|
||||
|
||||
Upon completion, the compiled bytecode for the Terra contracts will be placed
|
||||
into the `artifacts` directory.
|
||||
|
||||
## Verify Checksums
|
||||
|
||||
Now that you have built the Terra contracts, you should ask a peer to build
|
||||
using the same process and compare the equivalent checksums.txt files to make
|
||||
sure the contract bytecode(s) are deterministic.
|
||||
|
||||
```console
|
||||
wormhole/terra $ cat artifacts/checksums.txt
|
||||
```
|
||||
|
||||
Once you have verified the Terra contracts are deterministic with a peer, you can now move to the deploy step.
|
||||
|
||||
## Run tests
|
||||
|
||||
**Disclaimer: Currently the only test that exists is for the token bridge's transfer.**
|
||||
|
||||
You can run the integration test suite on the artifacts you built.
|
||||
|
||||
```console
|
||||
wormhole/terra $ make test
|
||||
```
|
||||
|
||||
This command deploys your artifacts and performs various interactions with your
|
||||
contracts in a LocalTerra node. Any new functionality (including expected errors)
|
||||
to the contracts should be added to this test suite.
|
||||
|
||||
## Deploy Contracts
|
||||
|
||||
Now that you have built and verified checksums, you can now deploy one or more relevant contracts to the Terra blockchain.
|
||||
|
||||
Deploy Target Options: [`mainnet`|`testnet`|`devnet`]
|
||||
|
||||
You will need to define a `payer-DEPLOY_TARGET.json` for the relevant deploy
|
||||
target (eg. `payer-testnet.json`). This will contain the relevant wallet
|
||||
private key that you will be using to deploy the contracts.
|
||||
|
||||
```console
|
||||
wormhole/terra $ make deploy/bridge
|
||||
wormhole/terra $ make deploy/token_bridge
|
||||
wormhole/terra $ make deploy/nft_bridge
|
||||
```
|
||||
|
||||
For each deployed contract, you will get a code id for that relevant
|
||||
contract for the deployment, make note of these so you can use them in
|
||||
the next step for on-chain verification.
|
||||
|
||||
## Verify On-Chain
|
||||
|
||||
Now that you have deployed one or more contracts on-chain, you can verify the
|
||||
onchain bytecode and make sure it matches the same checksums you identified
|
||||
above.
|
||||
|
||||
For each contract you wish to verify on-chain, you will need the following elements:
|
||||
|
||||
- Path to the contracts bytecode (eg. `artifacts-testnet/token_bridge.wasm`)
|
||||
- Terra code id for the relevant contract (eg. `59614`)
|
||||
- A network to verify on (`mainnet`, `testnet`, or `devnet`)
|
||||
|
||||
Below is how to verify all three contracts:
|
||||
|
||||
```console
|
||||
wormhole/terra $ ./verify artifacts/wormhole.wasm NEW_BRIDGE_CODE_ID
|
||||
wormhole/terra $ ./verify artifacts/token_bridge.wasm NEW_TOKEN_BRIDGE_CODE_ID
|
||||
wormhole/terra $ ./verify artifacts/nft_bridge.wasm NEW_NFT_BRIDGE_CODE_ID
|
||||
```
|
||||
Example: `./verify artifacts/token_bridge.wasm 59614`
|
||||
|
||||
For each contract, you should expect a `Successfully verified` output message.
|
||||
If all contracts can be successfully verified, you can engage in Wormhole
|
||||
protocol governance to obtain an authorized VAA for the contract upgrade(s).
|
||||
|
||||
A verification failure should never happen, and is a sign of some error in the
|
||||
deployment process. Do not proceed with governance until you can verify the
|
||||
on-chain bytecode with the locally compiled bytecode.
|
||||
|
||||
|
||||
## Governance
|
||||
|
||||
### Mainnet
|
||||
|
||||
Upgrades on mainnet have to go through governance. Once the code is deployed in
|
||||
the previous step, an unsigned governance VAA can be generated
|
||||
|
||||
```sh
|
||||
./generate_governance -m token_bridge -c 59614 > token-bridge-upgrade-59614.prototxt
|
||||
```
|
||||
|
||||
This will write to the `token-bridge-upgrade-59614.prototxt` file, which can
|
||||
now be shared with the guardians to vote on.
|
||||
|
||||
Once the guardians have reached quorum, the VAA may be submitted from any
|
||||
funded wallet: TODO - make this easier and more unified
|
||||
|
||||
``` sh
|
||||
node main.js terra execute_governance_vaa <signed VAA (hex)> --rpc "https://lcd.terra.dev" --chain_id "columbus-5" --mnemonic "..." --token_bridge "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf"
|
||||
```
|
||||
|
||||
### Testnet
|
||||
|
||||
For the contracts on testnet, the deployer wallet retains the upgrade
|
||||
authority, so these don't have to go through governance.
|
||||
|
||||
For example, to migrate the token bridge to 59614, run in `tools/`:
|
||||
|
||||
``` sh
|
||||
node migrate_testnet.js --code_id 59614 --contract terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a --mnemonic "..."
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
# Terra Wormhole Contracts
|
||||
|
||||
The Wormhole Terra integration is developed and maintained by Everstake / @ysavchenko.
|
|
@ -0,0 +1,5 @@
|
|||
[alias]
|
||||
wasm = "build --release --target wasm32-unknown-unknown"
|
||||
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib --features backtraces"
|
||||
integration-test = "test --test integration"
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "cw20-wrapped"
|
||||
version = "0.1.0"
|
||||
authors = ["Yuriy Savchenko <yuriy.savchenko@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "Wrapped CW20 token contract"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
# use library feature to disable all init/handle/query exports
|
||||
library = []
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { version = "0.16.0" }
|
||||
cosmwasm-storage = { version = "0.16.0" }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
cw2 = { version = "0.8.0" }
|
||||
cw20 = { version = "0.8.0" }
|
||||
cw20-legacy = { version = "0.2.0", features = ["library"]}
|
||||
cw-storage-plus = { version = "0.8.0" }
|
||||
thiserror = { version = "1.0.20" }
|
|
@ -0,0 +1,392 @@
|
|||
use cosmwasm_std::{
|
||||
to_binary,
|
||||
Binary,
|
||||
CosmosMsg,
|
||||
Deps,
|
||||
DepsMut,
|
||||
Env,
|
||||
MessageInfo,
|
||||
Response,
|
||||
StdError,
|
||||
StdResult,
|
||||
Uint128,
|
||||
WasmMsg,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "library"))]
|
||||
use cosmwasm_std::entry_point;
|
||||
|
||||
use cw2::set_contract_version;
|
||||
use cw20_legacy::{
|
||||
allowances::{
|
||||
execute_burn_from,
|
||||
execute_decrease_allowance,
|
||||
execute_increase_allowance,
|
||||
execute_send_from,
|
||||
execute_transfer_from,
|
||||
query_allowance,
|
||||
},
|
||||
contract::{
|
||||
execute_mint,
|
||||
execute_send,
|
||||
execute_transfer,
|
||||
query_balance,
|
||||
},
|
||||
state::{
|
||||
MinterData,
|
||||
TokenInfo,
|
||||
TOKEN_INFO,
|
||||
},
|
||||
ContractError,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
msg::{
|
||||
ExecuteMsg,
|
||||
InstantiateMsg,
|
||||
MigrateMsg,
|
||||
QueryMsg,
|
||||
WrappedAssetInfoResponse,
|
||||
},
|
||||
state::{
|
||||
wrapped_asset_info,
|
||||
wrapped_asset_info_read,
|
||||
WrappedAssetInfo,
|
||||
},
|
||||
};
|
||||
use cw20::TokenInfoResponse;
|
||||
use std::string::String;
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
// version info for migration info
|
||||
const CONTRACT_NAME: &str = "crates.io:cw20-base";
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> StdResult<Response> {
|
||||
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
// store token info using cw20-base format
|
||||
let data = TokenInfo {
|
||||
name: msg.name,
|
||||
symbol: msg.symbol,
|
||||
decimals: msg.decimals,
|
||||
total_supply: Uint128::new(0),
|
||||
// set creator as minter
|
||||
mint: Some(MinterData {
|
||||
minter: deps.api.addr_canonicalize(info.sender.as_str())?,
|
||||
cap: None,
|
||||
}),
|
||||
};
|
||||
TOKEN_INFO.save(deps.storage, &data)?;
|
||||
|
||||
// save wrapped asset info
|
||||
let data = WrappedAssetInfo {
|
||||
asset_chain: msg.asset_chain,
|
||||
asset_address: msg.asset_address,
|
||||
bridge: deps.api.addr_canonicalize(info.sender.as_str())?,
|
||||
};
|
||||
wrapped_asset_info(deps.storage).save(&data)?;
|
||||
|
||||
if let Some(mint_info) = msg.mint {
|
||||
execute_mint(deps, env, info, mint_info.recipient, mint_info.amount)
|
||||
.map_err(|e| StdError::generic_err(format!("{}", e)))?;
|
||||
}
|
||||
|
||||
if let Some(hook) = msg.init_hook {
|
||||
Ok(
|
||||
Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: hook.contract_addr,
|
||||
msg: hook.msg,
|
||||
funds: vec![],
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
Ok(Response::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn execute(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, ContractError> {
|
||||
match msg {
|
||||
// these all come from cw20-base to implement the cw20 standard
|
||||
ExecuteMsg::Transfer { recipient, amount } => {
|
||||
execute_transfer(deps, env, info, recipient, amount)
|
||||
}
|
||||
ExecuteMsg::Burn { account, amount } => execute_burn_from(deps, env, info, account, amount),
|
||||
ExecuteMsg::Send {
|
||||
contract,
|
||||
amount,
|
||||
msg,
|
||||
} => execute_send(deps, env, info, contract, amount, msg),
|
||||
ExecuteMsg::Mint { recipient, amount } => {
|
||||
execute_mint_wrapped(deps, env, info, recipient, amount)
|
||||
}
|
||||
ExecuteMsg::IncreaseAllowance {
|
||||
spender,
|
||||
amount,
|
||||
expires,
|
||||
} => execute_increase_allowance(deps, env, info, spender, amount, expires),
|
||||
ExecuteMsg::DecreaseAllowance {
|
||||
spender,
|
||||
amount,
|
||||
expires,
|
||||
} => execute_decrease_allowance(deps, env, info, spender, amount, expires),
|
||||
ExecuteMsg::TransferFrom {
|
||||
owner,
|
||||
recipient,
|
||||
amount,
|
||||
} => execute_transfer_from(deps, env, info, owner, recipient, amount),
|
||||
ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount),
|
||||
ExecuteMsg::SendFrom {
|
||||
owner,
|
||||
contract,
|
||||
amount,
|
||||
msg,
|
||||
} => execute_send_from(deps, env, info, owner, contract, amount, msg),
|
||||
ExecuteMsg::UpdateMetadata { name, symbol } => {
|
||||
execute_update_metadata(deps, env, info, name, symbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_mint_wrapped(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
recipient: HumanAddr,
|
||||
amount: Uint128,
|
||||
) -> Result<Response, ContractError> {
|
||||
// Only bridge can mint
|
||||
let wrapped_info = wrapped_asset_info_read(deps.storage).load()?;
|
||||
if wrapped_info.bridge != deps.api.addr_canonicalize(info.sender.as_str())? {
|
||||
return Err(ContractError::Unauthorized {});
|
||||
}
|
||||
|
||||
execute_mint(deps, env, info, recipient, amount)
|
||||
}
|
||||
|
||||
fn execute_update_metadata(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
info: MessageInfo,
|
||||
name: String,
|
||||
symbol: String,
|
||||
) -> Result<Response, ContractError> {
|
||||
// Only bridge can update.
|
||||
let wrapped_info = wrapped_asset_info_read(deps.storage).load()?;
|
||||
if wrapped_info.bridge != deps.api.addr_canonicalize(info.sender.as_str())? {
|
||||
return Err(ContractError::Unauthorized {});
|
||||
}
|
||||
|
||||
let mut state = TOKEN_INFO.load(deps.storage)?;
|
||||
state.name = name;
|
||||
state.symbol = symbol;
|
||||
TOKEN_INFO.save(deps.storage, &state)?;
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
|
||||
match msg {
|
||||
QueryMsg::WrappedAssetInfo {} => to_binary(&query_wrapped_asset_info(deps)?),
|
||||
// inherited from cw20-base
|
||||
QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?),
|
||||
QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?),
|
||||
QueryMsg::Allowance { owner, spender } => {
|
||||
to_binary(&query_allowance(deps, owner, spender)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub fn query_token_info(deps: Deps) -> StdResult<TokenInfoResponse> {
|
||||
let info = TOKEN_INFO.load(deps.storage)?;
|
||||
Ok(TokenInfoResponse {
|
||||
name: info.name + " (Wormhole)",
|
||||
symbol: info.symbol,
|
||||
decimals: info.decimals,
|
||||
total_supply: info.total_supply,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_wrapped_asset_info(deps: Deps) -> StdResult<WrappedAssetInfoResponse> {
|
||||
let info = wrapped_asset_info_read(deps.storage).load()?;
|
||||
Ok(WrappedAssetInfoResponse {
|
||||
asset_chain: info.asset_chain,
|
||||
asset_address: info.asset_address,
|
||||
bridge: deps.api.addr_humanize(&info.bridge)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmwasm_std::testing::{
|
||||
mock_dependencies,
|
||||
mock_env,
|
||||
mock_info,
|
||||
};
|
||||
use cw20::TokenInfoResponse;
|
||||
|
||||
fn get_balance(deps: Deps, address: HumanAddr) -> Uint128 {
|
||||
query_balance(deps, address.into()).unwrap().balance
|
||||
}
|
||||
|
||||
fn do_init(mut deps: DepsMut, creator: &HumanAddr) {
|
||||
let init_msg = InstantiateMsg {
|
||||
name: "Integers".to_string(),
|
||||
symbol: "INT".to_string(),
|
||||
asset_chain: 1,
|
||||
asset_address: vec![1; 32].into(),
|
||||
decimals: 10,
|
||||
mint: None,
|
||||
init_hook: None,
|
||||
};
|
||||
let env = mock_env();
|
||||
let info = mock_info(creator, &[]);
|
||||
let res = instantiate(deps.branch(), env, info, init_msg).unwrap();
|
||||
assert_eq!(0, res.messages.len());
|
||||
|
||||
assert_eq!(
|
||||
query_token_info(deps.as_ref()).unwrap(),
|
||||
TokenInfoResponse {
|
||||
name: "Integers (Wormhole)".to_string(),
|
||||
symbol: "INT".to_string(),
|
||||
decimals: 10,
|
||||
total_supply: Uint128::from(0u128),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
query_wrapped_asset_info(deps.as_ref()).unwrap(),
|
||||
WrappedAssetInfoResponse {
|
||||
asset_chain: 1,
|
||||
asset_address: vec![1; 32].into(),
|
||||
bridge: deps.api.addr_validate(creator).unwrap(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn do_init_and_mint(
|
||||
mut deps: DepsMut,
|
||||
creator: &HumanAddr,
|
||||
mint_to: &HumanAddr,
|
||||
amount: Uint128,
|
||||
) {
|
||||
do_init(deps.branch(), creator);
|
||||
|
||||
let msg = ExecuteMsg::Mint {
|
||||
recipient: mint_to.clone(),
|
||||
amount,
|
||||
};
|
||||
|
||||
let env = mock_env();
|
||||
let info = mock_info(creator, &[]);
|
||||
let res = execute(deps.branch(), env, info, msg.clone()).unwrap();
|
||||
assert_eq!(0, res.messages.len());
|
||||
assert_eq!(get_balance(deps.as_ref(), mint_to.clone(),), amount);
|
||||
|
||||
assert_eq!(
|
||||
query_token_info(deps.as_ref()).unwrap(),
|
||||
TokenInfoResponse {
|
||||
name: "Integers (Wormhole)".to_string(),
|
||||
symbol: "INT".to_string(),
|
||||
decimals: 10,
|
||||
total_supply: amount,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_mint_by_minter() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let minter = HumanAddr::from("minter");
|
||||
let recipient = HumanAddr::from("recipient");
|
||||
let amount = Uint128::new(222_222_222);
|
||||
do_init_and_mint(deps.as_mut(), &minter, &recipient, amount);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn others_cannot_mint() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let minter = HumanAddr::from("minter");
|
||||
let recipient = HumanAddr::from("recipient");
|
||||
do_init(deps.as_mut(), &minter);
|
||||
|
||||
let amount = Uint128::new(222_222_222);
|
||||
let msg = ExecuteMsg::Mint {
|
||||
recipient: recipient.clone(),
|
||||
amount,
|
||||
};
|
||||
|
||||
let other_address = HumanAddr::from("other");
|
||||
let env = mock_env();
|
||||
let info = mock_info(&other_address, &[]);
|
||||
let res = execute(deps.as_mut(), env, info, msg);
|
||||
assert_eq!(
|
||||
format!("{}", res.unwrap_err()),
|
||||
format!("{}", crate::error::ContractError::Unauthorized {})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_balance_success() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let minter = HumanAddr::from("minter");
|
||||
let owner = HumanAddr::from("owner");
|
||||
let amount_initial = Uint128::new(222_222_222);
|
||||
do_init_and_mint(deps.as_mut(), &minter, &owner, amount_initial);
|
||||
|
||||
// Transfer
|
||||
let recipient = HumanAddr::from("recipient");
|
||||
let amount_transfer = Uint128::new(222_222);
|
||||
let msg = ExecuteMsg::Transfer {
|
||||
recipient: recipient.clone(),
|
||||
amount: amount_transfer,
|
||||
};
|
||||
|
||||
let env = mock_env();
|
||||
let info = mock_info(&owner, &[]);
|
||||
let res = execute(deps.as_mut(), env, info, msg.clone()).unwrap();
|
||||
assert_eq!(0, res.messages.len());
|
||||
assert_eq!(get_balance(deps.as_ref(), owner), Uint128::new(222_000_000));
|
||||
assert_eq!(get_balance(deps.as_ref(), recipient), amount_transfer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_balance_not_enough() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let minter = HumanAddr::from("minter");
|
||||
let owner = HumanAddr::from("owner");
|
||||
let amount_initial = Uint128::new(222_221);
|
||||
do_init_and_mint(deps.as_mut(), &minter, &owner, amount_initial);
|
||||
|
||||
// Transfer
|
||||
let recipient = HumanAddr::from("recipient");
|
||||
let amount_transfer = Uint128::new(222_222);
|
||||
let msg = ExecuteMsg::Transfer {
|
||||
recipient: recipient.clone(),
|
||||
amount: amount_transfer,
|
||||
};
|
||||
|
||||
let env = mock_env();
|
||||
let info = mock_info(&owner, &[]);
|
||||
let _ = execute(deps.as_mut(), env, info, msg.clone()).unwrap_err(); // Will panic if no error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
use cosmwasm_std::StdError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ContractError {
|
||||
// CW20 errors
|
||||
#[error("{0}")]
|
||||
Std(#[from] StdError),
|
||||
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized {},
|
||||
|
||||
#[error("Cannot set to own account")]
|
||||
CannotSetOwnAccount {},
|
||||
|
||||
#[error("Invalid zero amount")]
|
||||
InvalidZeroAmount {},
|
||||
|
||||
#[error("Allowance is expired")]
|
||||
Expired {},
|
||||
|
||||
#[error("No allowance for this account")]
|
||||
NoAllowance {},
|
||||
|
||||
#[error("Minting cannot exceed the cap")]
|
||||
CannotExceedCap {},
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
mod error;
|
||||
|
||||
pub mod contract;
|
||||
pub mod msg;
|
||||
pub mod state;
|
||||
|
||||
pub use crate::error::ContractError;
|
|
@ -0,0 +1,128 @@
|
|||
#![allow(clippy::field_reassign_with_default)]
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use cosmwasm_std::{
|
||||
Addr,
|
||||
Binary,
|
||||
Uint128,
|
||||
};
|
||||
use cw20::Expiration;
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InstantiateMsg {
|
||||
pub name: String,
|
||||
pub symbol: String,
|
||||
pub asset_chain: u16,
|
||||
pub asset_address: Binary,
|
||||
pub decimals: u8,
|
||||
pub mint: Option<InitMint>,
|
||||
pub init_hook: Option<InitHook>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InitHook {
|
||||
pub msg: Binary,
|
||||
pub contract_addr: HumanAddr,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InitMint {
|
||||
pub recipient: HumanAddr,
|
||||
pub amount: Uint128,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
/// Implements CW20. Transfer is a base message to move tokens to another account without triggering actions
|
||||
Transfer {
|
||||
recipient: HumanAddr,
|
||||
amount: Uint128,
|
||||
},
|
||||
/// Slightly different than CW20. Burn is a base message to destroy tokens forever
|
||||
Burn { account: HumanAddr, amount: Uint128 },
|
||||
/// Implements CW20. Send is a base message to transfer tokens to a contract and trigger an action
|
||||
/// on the receiving contract.
|
||||
Send {
|
||||
contract: HumanAddr,
|
||||
amount: Uint128,
|
||||
msg: Binary,
|
||||
},
|
||||
/// Implements CW20 "mintable" extension. If authorized, creates amount new tokens
|
||||
/// and adds to the recipient balance.
|
||||
Mint {
|
||||
recipient: HumanAddr,
|
||||
amount: Uint128,
|
||||
},
|
||||
/// Implements CW20 "approval" extension. Allows spender to access an additional amount tokens
|
||||
/// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance
|
||||
/// expiration with this one.
|
||||
IncreaseAllowance {
|
||||
spender: HumanAddr,
|
||||
amount: Uint128,
|
||||
expires: Option<Expiration>,
|
||||
},
|
||||
/// Implements CW20 "approval" extension. Lowers the spender's access of tokens
|
||||
/// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current
|
||||
/// allowance expiration with this one.
|
||||
DecreaseAllowance {
|
||||
spender: HumanAddr,
|
||||
amount: Uint128,
|
||||
expires: Option<Expiration>,
|
||||
},
|
||||
/// Implements CW20 "approval" extension. Transfers amount tokens from owner -> recipient
|
||||
/// if `env.sender` has sufficient pre-approval.
|
||||
TransferFrom {
|
||||
owner: HumanAddr,
|
||||
recipient: HumanAddr,
|
||||
amount: Uint128,
|
||||
},
|
||||
/// Implements CW20 "approval" extension. Sends amount tokens from owner -> contract
|
||||
/// if `env.sender` has sufficient pre-approval.
|
||||
SendFrom {
|
||||
owner: HumanAddr,
|
||||
contract: HumanAddr,
|
||||
amount: Uint128,
|
||||
msg: Binary,
|
||||
},
|
||||
/// Implements CW20 "approval" extension. Destroys tokens forever
|
||||
BurnFrom { owner: HumanAddr, amount: Uint128 },
|
||||
/// Extend Interface with the ability to update token metadata.
|
||||
UpdateMetadata { name: String, symbol: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
// Generic information about the wrapped asset
|
||||
WrappedAssetInfo {},
|
||||
/// Implements CW20. Returns the current balance of the given address, 0 if unset.
|
||||
Balance {
|
||||
address: HumanAddr,
|
||||
},
|
||||
/// Implements CW20. Returns metadata on the contract - name, decimals, supply, etc.
|
||||
TokenInfo {},
|
||||
/// Implements CW20 "allowance" extension.
|
||||
/// Returns how much spender can use from owner account, 0 if unset.
|
||||
Allowance {
|
||||
owner: HumanAddr,
|
||||
spender: HumanAddr,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct WrappedAssetInfoResponse {
|
||||
pub asset_chain: u16, // Asset chain id
|
||||
pub asset_address: Binary, // Asset smart contract address in the original chain
|
||||
pub bridge: Addr, // Bridge address, authorized to mint and burn wrapped tokens
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use cosmwasm_std::{
|
||||
Binary,
|
||||
CanonicalAddr,
|
||||
Storage,
|
||||
};
|
||||
use cosmwasm_storage::{
|
||||
singleton,
|
||||
singleton_read,
|
||||
ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
|
||||
pub const KEY_WRAPPED_ASSET: &[u8] = b"wrappedAsset";
|
||||
|
||||
// Created at initialization and reference original asset and bridge address
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct WrappedAssetInfo {
|
||||
pub asset_chain: u16, // Asset chain id
|
||||
pub asset_address: Binary, // Asset smart contract address on the original chain
|
||||
pub bridge: CanonicalAddr, // Bridge address, authorized to mint and burn wrapped tokens
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_info(storage: &mut dyn Storage) -> Singleton<WrappedAssetInfo> {
|
||||
singleton(storage, KEY_WRAPPED_ASSET)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_info_read(
|
||||
storage: &dyn Storage,
|
||||
) -> ReadonlySingleton<WrappedAssetInfo> {
|
||||
singleton_read(storage, KEY_WRAPPED_ASSET)
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
use cosmwasm_std::{
|
||||
from_slice,
|
||||
testing::{
|
||||
mock_dependencies,
|
||||
mock_env,
|
||||
mock_info,
|
||||
MockApi,
|
||||
MockQuerier,
|
||||
MockStorage,
|
||||
},
|
||||
Addr,
|
||||
Api,
|
||||
OwnedDeps,
|
||||
Response,
|
||||
Storage,
|
||||
Uint128,
|
||||
};
|
||||
use cosmwasm_storage::to_length_prefixed;
|
||||
use cw20::TokenInfoResponse;
|
||||
use cw20_wrapped::{
|
||||
contract::{
|
||||
execute,
|
||||
instantiate,
|
||||
query,
|
||||
},
|
||||
msg::{
|
||||
ExecuteMsg,
|
||||
InstantiateMsg,
|
||||
QueryMsg,
|
||||
WrappedAssetInfoResponse,
|
||||
},
|
||||
state::{
|
||||
WrappedAssetInfo,
|
||||
KEY_WRAPPED_ASSET,
|
||||
},
|
||||
ContractError,
|
||||
};
|
||||
|
||||
static INITIALIZER: &str = "addr0000";
|
||||
static RECIPIENT: &str = "addr2222";
|
||||
static SENDER: &str = "addr3333";
|
||||
|
||||
fn get_wrapped_asset_info<S: Storage>(storage: &S) -> WrappedAssetInfo {
|
||||
let key = to_length_prefixed(KEY_WRAPPED_ASSET);
|
||||
let data = storage
|
||||
.get(&key)
|
||||
.expect("data should exist");
|
||||
from_slice(&data).expect("invalid data")
|
||||
}
|
||||
|
||||
fn do_init() -> OwnedDeps<MockStorage, MockApi, MockQuerier> {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let init_msg = InstantiateMsg {
|
||||
name: "Integers".into(),
|
||||
symbol: "INT".into(),
|
||||
asset_chain: 1,
|
||||
asset_address: vec![1; 32].into(),
|
||||
decimals: 10,
|
||||
mint: None,
|
||||
init_hook: None,
|
||||
};
|
||||
let env = mock_env();
|
||||
let info = mock_info(INITIALIZER, &[]);
|
||||
let res: Response = instantiate(deps.as_mut(), env, info, init_msg).unwrap();
|
||||
assert_eq!(0, res.messages.len());
|
||||
|
||||
// query the store directly
|
||||
let bridge = deps.api.addr_canonicalize(INITIALIZER).unwrap();
|
||||
assert_eq!(
|
||||
get_wrapped_asset_info(&deps.storage),
|
||||
WrappedAssetInfo {
|
||||
asset_chain: 1,
|
||||
asset_address: vec![1; 32].into(),
|
||||
bridge,
|
||||
}
|
||||
);
|
||||
|
||||
deps
|
||||
}
|
||||
|
||||
fn do_mint(
|
||||
deps: &mut OwnedDeps<MockStorage, MockApi, MockQuerier>,
|
||||
recipient: &Addr,
|
||||
amount: &Uint128,
|
||||
) {
|
||||
let mint_msg = ExecuteMsg::Mint {
|
||||
recipient: recipient.to_string(),
|
||||
amount: amount.clone(),
|
||||
};
|
||||
let info = mock_info(INITIALIZER, &[]);
|
||||
let handle_response: Response = execute(deps.as_mut(), mock_env(), info, mint_msg).unwrap();
|
||||
assert_eq!(0, handle_response.messages.len());
|
||||
}
|
||||
|
||||
fn do_transfer(
|
||||
deps: &mut OwnedDeps<MockStorage, MockApi, MockQuerier>,
|
||||
sender: &Addr,
|
||||
recipient: &Addr,
|
||||
amount: &Uint128,
|
||||
) {
|
||||
let transfer_msg = ExecuteMsg::Transfer {
|
||||
recipient: recipient.to_string(),
|
||||
amount: amount.clone(),
|
||||
};
|
||||
let env = mock_env();
|
||||
let info = mock_info(sender.as_str(), &[]);
|
||||
let handle_response: Response = execute(deps.as_mut(), env, info, transfer_msg).unwrap();
|
||||
assert_eq!(0, handle_response.messages.len());
|
||||
}
|
||||
|
||||
fn check_balance(
|
||||
deps: &OwnedDeps<MockStorage, MockApi, MockQuerier>,
|
||||
address: &Addr,
|
||||
amount: &Uint128,
|
||||
) {
|
||||
let query_response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::Balance {
|
||||
address: address.to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
query_response.as_slice(),
|
||||
format!("{{\"balance\":\"{}\"}}", amount.u128()).as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
fn check_token_details(deps: &OwnedDeps<MockStorage, MockApi, MockQuerier>, supply: Uint128) {
|
||||
let query_response = query(deps.as_ref(), mock_env(), QueryMsg::TokenInfo {}).unwrap();
|
||||
assert_eq!(
|
||||
from_slice::<TokenInfoResponse>(query_response.as_slice()).unwrap(),
|
||||
TokenInfoResponse {
|
||||
name: "Integers (Wormhole)".into(),
|
||||
symbol: "INT".into(),
|
||||
decimals: 10,
|
||||
total_supply: supply,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_works() {
|
||||
let mut deps = do_init();
|
||||
check_token_details(&mut deps, Uint128::new(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_works() {
|
||||
let deps = do_init();
|
||||
|
||||
let query_response = query(deps.as_ref(), mock_env(), QueryMsg::WrappedAssetInfo {}).unwrap();
|
||||
assert_eq!(
|
||||
from_slice::<WrappedAssetInfoResponse>(&query_response.as_slice()).unwrap(),
|
||||
WrappedAssetInfoResponse {
|
||||
asset_chain: 1,
|
||||
asset_address: vec![1; 32].into(),
|
||||
bridge: Addr::unchecked(INITIALIZER),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mint_works() {
|
||||
let mut deps = do_init();
|
||||
|
||||
let recipient = Addr::unchecked(RECIPIENT);
|
||||
do_mint(&mut deps, &recipient, &Uint128::new(123_123_123));
|
||||
|
||||
check_balance(&deps, &recipient, &Uint128::new(123_123_123));
|
||||
check_token_details(&deps, Uint128::new(123_123_123));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn others_cannot_mint() {
|
||||
let mut deps = do_init();
|
||||
|
||||
let mint_msg = ExecuteMsg::Mint {
|
||||
recipient: RECIPIENT.into(),
|
||||
amount: Uint128::new(123_123_123),
|
||||
};
|
||||
let env = mock_env();
|
||||
let info = mock_info(RECIPIENT, &[]);
|
||||
let handle_result = execute(deps.as_mut(), env, info, mint_msg);
|
||||
assert_eq!(
|
||||
format!("{}", handle_result.unwrap_err()),
|
||||
format!("{}", ContractError::Unauthorized {})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_works() {
|
||||
let mut deps = do_init();
|
||||
|
||||
let sender = Addr::unchecked(SENDER);
|
||||
let recipient = Addr::unchecked(RECIPIENT);
|
||||
do_mint(&mut deps, &sender, &Uint128::new(123_123_123));
|
||||
do_transfer(&mut deps, &sender, &recipient, &Uint128::new(123_123_000));
|
||||
|
||||
check_balance(&mut deps, &sender, &Uint128::new(123));
|
||||
check_balance(&mut deps, &recipient, &Uint128::new(123_123_000));
|
||||
}
|
|
@ -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"
|
|
@ -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" }
|
|
@ -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.
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
|
@ -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 };
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,756 @@
|
|||
#![cfg(test)]
|
||||
use cosmwasm_std::{
|
||||
from_binary,
|
||||
testing::{
|
||||
mock_dependencies,
|
||||
mock_env,
|
||||
mock_info,
|
||||
},
|
||||
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[..]);
|
||||
}
|
|
@ -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 },
|
||||
}
|
|
@ -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 {}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
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;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "cw721-wrapped"
|
||||
version = "0.10.1"
|
||||
edition = "2018"
|
||||
description = "Wrapped CW721 token contract"
|
||||
|
||||
exclude = [
|
||||
"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]
|
||||
cw2 = { version = "0.8.0" }
|
||||
cw721-base = { path = "../../contracts/cw721-base", version = "0.10.0", features = ["library"] }
|
||||
cw721 = { path = "../../packages/cw721", version = "0.10.1" }
|
||||
cosmwasm-std = { version = "0.16.0" }
|
||||
cosmwasm-storage = { 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" }
|
|
@ -0,0 +1,59 @@
|
|||
use std::{
|
||||
env::current_dir,
|
||||
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<()>), &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);
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
{
|
||||
"$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_Metadata"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Trait"
|
||||
}
|
||||
},
|
||||
"background_color": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"external_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"image": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"image_data": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"youtube_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"NftInfoResponse_for_Nullable_Metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extension": {
|
||||
"description": "You can add any custom metadata here when you extend cw721-base",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Metadata"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Trait": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"trait_type",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"display_type": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"trait_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,389 @@
|
|||
{
|
||||
"$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_Metadata"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Trait"
|
||||
}
|
||||
},
|
||||
"background_color": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"external_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"image": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"image_data": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"youtube_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"MintMsg_for_Nullable_Metadata": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"owner",
|
||||
"token_id"
|
||||
],
|
||||
"properties": {
|
||||
"extension": {
|
||||
"description": "Any custom extension used by this contract",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Metadata"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Trait": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"trait_type",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"display_type": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"trait_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"$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/Metadata"
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"Metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Trait"
|
||||
}
|
||||
},
|
||||
"background_color": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"external_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"image": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"image_data": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"youtube_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Trait": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"trait_type",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"display_type": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"trait_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
pub mod msg;
|
||||
pub mod state;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
pub use cosmwasm_std::to_binary;
|
||||
use cosmwasm_std::Empty;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)]
|
||||
pub struct Trait {
|
||||
pub display_type: Option<String>,
|
||||
pub trait_type: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub type Extension = Option<Empty>;
|
||||
|
||||
pub type Cw721MetadataContract<'a> = cw721_base::Cw721Contract<'a, Extension, Empty>;
|
||||
pub type ExecuteMsg = cw721_base::ExecuteMsg<Extension>;
|
||||
|
||||
#[cfg(not(feature = "library"))]
|
||||
pub mod entry {
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::msg::{
|
||||
InstantiateMsg,
|
||||
WrappedAssetInfoResponse,
|
||||
};
|
||||
pub use crate::{
|
||||
msg::QueryMsg,
|
||||
state::{
|
||||
wrapped_asset_info,
|
||||
wrapped_asset_info_read,
|
||||
WrappedAssetInfo,
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
use cosmwasm_std::{
|
||||
entry_point,
|
||||
to_binary,
|
||||
Binary,
|
||||
CosmosMsg,
|
||||
Deps,
|
||||
DepsMut,
|
||||
Env,
|
||||
MessageInfo,
|
||||
Response,
|
||||
StdError,
|
||||
StdResult,
|
||||
WasmMsg,
|
||||
};
|
||||
use cw721::Cw721Query;
|
||||
|
||||
// version info for migration info
|
||||
const CONTRACT_NAME: &str = "crates.io:cw721-wrapped";
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
// This is a simple type to let us handle empty extensions
|
||||
|
||||
// This makes a conscious choice on the various generics used by the contract
|
||||
#[entry_point]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> StdResult<Response> {
|
||||
let base = Cw721MetadataContract::default();
|
||||
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
let contract_info = cw721::ContractInfoResponse {
|
||||
name: msg.name,
|
||||
symbol: msg.symbol,
|
||||
};
|
||||
base.contract_info.save(deps.storage, &contract_info)?;
|
||||
let minter = deps.api.addr_validate(&msg.minter)?;
|
||||
base.minter.save(deps.storage, &minter)?;
|
||||
|
||||
// save wrapped asset info
|
||||
let data =
|
||||
WrappedAssetInfo {
|
||||
asset_chain: msg.asset_chain,
|
||||
asset_address: msg.asset_address.to_vec().try_into().map_err(
|
||||
|_err| -> StdError {
|
||||
StdError::GenericErr {
|
||||
msg: "WrongSize".to_string(),
|
||||
}
|
||||
},
|
||||
)?,
|
||||
bridge: deps.api.addr_canonicalize(&info.sender.as_str())?,
|
||||
};
|
||||
wrapped_asset_info(deps.storage).save(&data)?;
|
||||
|
||||
if let Some(mint_msg) = msg.mint {
|
||||
execute(deps, env, info, ExecuteMsg::Mint(mint_msg))
|
||||
.map_err(|e| StdError::generic_err(format!("{}", e)))?;
|
||||
}
|
||||
|
||||
if let Some(hook) = msg.init_hook {
|
||||
Ok(
|
||||
Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: hook.contract_addr,
|
||||
msg: hook.msg,
|
||||
funds: vec![],
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
Ok(Response::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn execute(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, cw721_base::ContractError> {
|
||||
Cw721MetadataContract::default().execute(deps, env, info, msg)
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
|
||||
let base = Cw721MetadataContract::default();
|
||||
match msg {
|
||||
QueryMsg::WrappedAssetInfo {} => to_binary(&query_wrapped_asset_info(deps)?),
|
||||
QueryMsg::OwnerOf {
|
||||
token_id,
|
||||
include_expired,
|
||||
} => {
|
||||
to_binary(&base.owner_of(deps, env, token_id, include_expired.unwrap_or(false))?)
|
||||
}
|
||||
QueryMsg::Approval {
|
||||
token_id,
|
||||
spender,
|
||||
include_expired,
|
||||
} => to_binary(&base.approval(
|
||||
deps,
|
||||
env,
|
||||
token_id,
|
||||
spender,
|
||||
include_expired.unwrap_or(false),
|
||||
)?),
|
||||
QueryMsg::Approvals {
|
||||
token_id,
|
||||
include_expired,
|
||||
} => {
|
||||
to_binary(&base.approvals(deps, env, token_id, include_expired.unwrap_or(false))?)
|
||||
}
|
||||
QueryMsg::AllOperators {
|
||||
owner,
|
||||
include_expired,
|
||||
start_after,
|
||||
limit,
|
||||
} => to_binary(&base.operators(
|
||||
deps,
|
||||
env,
|
||||
owner,
|
||||
include_expired.unwrap_or(false),
|
||||
start_after,
|
||||
limit,
|
||||
)?),
|
||||
QueryMsg::NumTokens {} => to_binary(&base.num_tokens(deps)?),
|
||||
QueryMsg::Tokens {
|
||||
owner,
|
||||
start_after,
|
||||
limit,
|
||||
} => to_binary(&base.tokens(deps, owner, start_after, limit)?),
|
||||
QueryMsg::AllTokens { start_after, limit } => {
|
||||
to_binary(&base.all_tokens(deps, start_after, limit)?)
|
||||
}
|
||||
QueryMsg::Minter {} => to_binary(&base.minter(deps)?),
|
||||
QueryMsg::ContractInfo {} => to_binary(&base.contract_info(deps)?),
|
||||
QueryMsg::NftInfo { token_id } => to_binary(&base.nft_info(deps, token_id)?),
|
||||
QueryMsg::AllNftInfo {
|
||||
token_id,
|
||||
include_expired,
|
||||
} => to_binary(&base.all_nft_info(
|
||||
deps,
|
||||
env,
|
||||
token_id,
|
||||
include_expired.unwrap_or(false),
|
||||
)?),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_wrapped_asset_info(deps: Deps) -> StdResult<WrappedAssetInfoResponse> {
|
||||
let info = wrapped_asset_info_read(deps.storage).load()?;
|
||||
Ok(WrappedAssetInfoResponse {
|
||||
asset_chain: info.asset_chain,
|
||||
asset_address: info.asset_address,
|
||||
bridge: deps.api.addr_humanize(&info.bridge)?,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
pub use cw721_base::MintMsg;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use cosmwasm_std::{
|
||||
Addr,
|
||||
Binary,
|
||||
Empty,
|
||||
};
|
||||
|
||||
pub use cw721_base::msg::ExecuteMsg;
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
/// The cw721_base crate allows an extension point which we don't care about
|
||||
pub type NoExt = Option<Empty>;
|
||||
|
||||
#[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,
|
||||
|
||||
/// Native chain of the NFT
|
||||
pub asset_chain: u16,
|
||||
|
||||
/// Native address of the NFT
|
||||
pub asset_address: Binary,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// Mint a new NFT upon creation
|
||||
pub mint: Option<MintMsg<NoExt>>,
|
||||
|
||||
/// Generic callback - used to register the newly instantiated asset
|
||||
pub init_hook: Option<InitHook>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InitHook {
|
||||
pub msg: Binary,
|
||||
pub contract_addr: HumanAddr,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
/// Generic information about the wrapped asset
|
||||
WrappedAssetInfo {},
|
||||
|
||||
/// 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 {},
|
||||
|
||||
/// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema*
|
||||
/// but directly from the contract: `NftInfoResponse`
|
||||
NftInfo {
|
||||
token_id: String,
|
||||
},
|
||||
|
||||
/// 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>,
|
||||
},
|
||||
|
||||
/// Returns all tokens owned by the given address, [] if unset.
|
||||
/// Return type: TokensResponse.
|
||||
Tokens {
|
||||
owner: String,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// 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 {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct WrappedAssetInfoResponse {
|
||||
pub asset_chain: u16, // Asset chain id
|
||||
pub asset_address: Binary, // Asset smart contract address in the original chain
|
||||
pub bridge: Addr, // Bridge address, authorized to mint and burn wrapped tokens
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use cosmwasm_std::{
|
||||
Binary,
|
||||
CanonicalAddr,
|
||||
Storage,
|
||||
};
|
||||
use cosmwasm_storage::{
|
||||
singleton,
|
||||
singleton_read,
|
||||
ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
|
||||
pub const KEY_WRAPPED_ASSET: &[u8] = b"wrappedAsset";
|
||||
|
||||
// Created at initialization and reference original asset and bridge address
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct WrappedAssetInfo {
|
||||
pub asset_chain: u16, // Asset chain id
|
||||
pub asset_address: Binary, // Asset smart contract address on the original chain
|
||||
pub bridge: CanonicalAddr, // Bridge address, authorized to mint and burn wrapped tokens
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_info(storage: &mut dyn Storage) -> Singleton<WrappedAssetInfo> {
|
||||
singleton(storage, KEY_WRAPPED_ASSET)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_info_read(storage: &dyn Storage) -> ReadonlySingleton<WrappedAssetInfo> {
|
||||
singleton_read(storage, KEY_WRAPPED_ASSET)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
[alias]
|
||||
wasm = "build --release --target wasm32-unknown-unknown"
|
||||
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib --features backtraces"
|
||||
integration-test = "test --test integration"
|
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "mock-bridge-integration"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
description = "Mock Bridge Integration for Transfer w/ Payload"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
# use library feature to disable all init/handle/query exports
|
||||
library = []
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { version = "0.16.0" }
|
||||
cosmwasm-storage = { version = "0.16.0" }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
cw20 = "0.8.0"
|
||||
cw20-base = { version = "0.8.0", features = ["library"] }
|
||||
cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] }
|
||||
terraswap = "2.4.0"
|
||||
thiserror = { version = "1.0.20" }
|
||||
k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] }
|
||||
sha3 = { version = "0.9.1", default-features = false }
|
||||
generic-array = { version = "0.14.4" }
|
||||
hex = "0.4.2"
|
||||
lazy_static = "1.4.0"
|
||||
bigint = "4"
|
||||
|
||||
wormhole-bridge-terra = { path = "../wormhole", features = ["library"] }
|
||||
token-bridge-terra = { path = "../token-bridge", features = ["library"] }
|
||||
|
||||
[dev-dependencies]
|
||||
cosmwasm-vm = { version = "0.16.0", default-features = false }
|
||||
serde_json = "1.0"
|
|
@ -0,0 +1,120 @@
|
|||
use cosmwasm_std::{
|
||||
entry_point,
|
||||
to_binary,
|
||||
Binary,
|
||||
CosmosMsg,
|
||||
Deps,
|
||||
DepsMut,
|
||||
Env,
|
||||
MessageInfo,
|
||||
QueryRequest,
|
||||
Reply,
|
||||
Response,
|
||||
StdError,
|
||||
StdResult,
|
||||
SubMsg,
|
||||
WasmMsg,
|
||||
WasmQuery,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
msg::{
|
||||
ExecuteMsg,
|
||||
InstantiateMsg,
|
||||
MigrateMsg,
|
||||
QueryMsg,
|
||||
},
|
||||
state::{
|
||||
Config,
|
||||
config,
|
||||
config_read,
|
||||
},
|
||||
};
|
||||
|
||||
use token_bridge_terra::{
|
||||
msg::{
|
||||
ExecuteMsg as TokenBridgeExecuteMsg,
|
||||
QueryMsg as TokenBridgeQueryMessage,
|
||||
},
|
||||
msg::TransferInfoResponse,
|
||||
};
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
_info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> StdResult<Response> {
|
||||
let state = Config {
|
||||
token_bridge_contract: msg.token_bridge_contract,
|
||||
};
|
||||
config(deps.storage).save(&state)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
|
||||
match msg {
|
||||
ExecuteMsg::CompleteTransferWithPayload { data } => {
|
||||
complete_transfer_with_payload(deps, env, info, &data)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn reply(_deps: DepsMut, _env: Env, _msg: Reply) -> StdResult<Response> {
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult<Binary> {
|
||||
Err(StdError::generic_err("not implemented"))
|
||||
}
|
||||
|
||||
fn complete_transfer_with_payload(
|
||||
mut deps: DepsMut,
|
||||
_env: Env,
|
||||
info: MessageInfo,
|
||||
data: &Binary,
|
||||
) -> StdResult<Response> {
|
||||
let cfg = config_read(deps.storage).load()?;
|
||||
|
||||
let messages = vec![SubMsg::reply_on_success(
|
||||
CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: cfg.token_bridge_contract,
|
||||
msg: to_binary(&TokenBridgeExecuteMsg::CompleteTransferWithPayload {
|
||||
data: data.clone(), relayer: info.sender.to_string()
|
||||
})?,
|
||||
funds: vec![],
|
||||
}),
|
||||
1,
|
||||
),];
|
||||
|
||||
let transfer_info = parse_transfer_vaa(deps.as_ref(), data)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_submessages(messages)
|
||||
.add_attribute("action", "complete_transfer_with_payload")
|
||||
.add_attribute("transfer_payload", Binary::from(transfer_info.payload).to_base64()))
|
||||
}
|
||||
|
||||
fn parse_transfer_vaa(
|
||||
deps: Deps,
|
||||
data: &Binary,
|
||||
) -> StdResult<TransferInfoResponse> {
|
||||
let cfg = config_read(deps.storage).load()?;
|
||||
let transfer_info: TransferInfoResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
|
||||
contract_addr: cfg.token_bridge_contract,
|
||||
msg: to_binary(&TokenBridgeQueryMessage::TransferInfo {
|
||||
vaa: data.clone(),
|
||||
})?,
|
||||
}))?;
|
||||
Ok(transfer_info)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
#[cfg(test)]
|
||||
extern crate lazy_static;
|
||||
|
||||
pub mod contract;
|
||||
pub mod msg;
|
||||
pub mod state;
|
|
@ -0,0 +1,31 @@
|
|||
use cosmwasm_std::Binary;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InstantiateMsg {
|
||||
pub token_bridge_contract: HumanAddr,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
CompleteTransferWithPayload {
|
||||
data: Binary,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
WrappedRegistry { chain: u16, address: Binary },
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use cosmwasm_std::Storage;
|
||||
use cosmwasm_storage::{
|
||||
singleton,
|
||||
singleton_read,
|
||||
ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
pub static CONFIG_KEY: &[u8] = b"config";
|
||||
|
||||
// Guardian set information
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct Config {
|
||||
pub token_bridge_contract: HumanAddr,
|
||||
}
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<Config> {
|
||||
singleton(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<Config> {
|
||||
singleton_read(storage, CONFIG_KEY)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
[alias]
|
||||
wasm = "build --release --target wasm32-unknown-unknown"
|
||||
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib --features backtraces"
|
||||
integration-test = "test --test integration"
|
|
@ -0,0 +1,30 @@
|
|||
[package]
|
||||
name = "nft-bridge"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
description = "Wormhole NFT bridge"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
# use library feature to disable all init/handle/query exports
|
||||
library = []
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { version = "0.16.0" }
|
||||
cosmwasm-storage = { version = "0.16.0" }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
cw721-wrapped = { path = "../cw721-wrapped", features = ["library"] }
|
||||
cw721-base = { path = "../../contracts/cw721-base", version = "0.10.0", features = ["library"] }
|
||||
cw721 = { path = "../../packages/cw721" }
|
||||
wormhole-bridge-terra = { path = "../wormhole", features = ["library"] }
|
||||
sha3 = { version = "0.9.1", default-features = false }
|
||||
hex = "0.4.2"
|
||||
bigint = "4"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
lazy_static = "1.4.0"
|
|
@ -0,0 +1,589 @@
|
|||
use crate::{
|
||||
msg::WrappedRegistryResponse,
|
||||
state::{
|
||||
spl_cache,
|
||||
spl_cache_read,
|
||||
wrapped_asset,
|
||||
BoundedVec,
|
||||
SplCacheItem,
|
||||
},
|
||||
token_id::{
|
||||
from_external_token_id,
|
||||
to_external_token_id,
|
||||
},
|
||||
CHAIN_ID,
|
||||
};
|
||||
use cosmwasm_std::{
|
||||
entry_point,
|
||||
to_binary,
|
||||
Binary,
|
||||
CanonicalAddr,
|
||||
CosmosMsg,
|
||||
Deps,
|
||||
DepsMut,
|
||||
Empty,
|
||||
Env,
|
||||
MessageInfo,
|
||||
QueryRequest,
|
||||
Response,
|
||||
StdError,
|
||||
StdResult,
|
||||
WasmMsg,
|
||||
WasmQuery, Order,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
msg::{
|
||||
ExecuteMsg,
|
||||
InstantiateMsg,
|
||||
MigrateMsg,
|
||||
QueryMsg,
|
||||
},
|
||||
state::{
|
||||
bridge_contracts,
|
||||
bridge_contracts_read,
|
||||
config,
|
||||
config_read,
|
||||
wrapped_asset_address,
|
||||
wrapped_asset_address_read,
|
||||
wrapped_asset_read,
|
||||
Action,
|
||||
ConfigInfo,
|
||||
RegisterChain,
|
||||
TokenBridgeMessage,
|
||||
TransferInfo,
|
||||
UpgradeContract,
|
||||
},
|
||||
};
|
||||
use wormhole::{
|
||||
byte_utils::{
|
||||
extend_address_to_32,
|
||||
extend_address_to_32_array,
|
||||
get_string_from_32,
|
||||
string_to_array,
|
||||
ByteUtils,
|
||||
},
|
||||
error::ContractError,
|
||||
};
|
||||
|
||||
use wormhole::msg::{
|
||||
ExecuteMsg as WormholeExecuteMsg,
|
||||
QueryMsg as WormholeQueryMsg,
|
||||
};
|
||||
|
||||
use wormhole::state::{
|
||||
vaa_archive_add,
|
||||
vaa_archive_check,
|
||||
GovernancePacket,
|
||||
ParsedVAA,
|
||||
};
|
||||
|
||||
use sha3::{
|
||||
Digest,
|
||||
Keccak256,
|
||||
};
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
const WRAPPED_ASSET_UPDATING: &str = "updating";
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
_info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> StdResult<Response> {
|
||||
// Save general wormhole info
|
||||
let state = ConfigInfo {
|
||||
gov_chain: msg.gov_chain,
|
||||
gov_address: msg.gov_address.as_slice().to_vec(),
|
||||
wormhole_contract: msg.wormhole_contract,
|
||||
wrapped_asset_code_id: msg.wrapped_asset_code_id,
|
||||
};
|
||||
config(deps.storage).save(&state)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
pub fn parse_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
|
||||
let cfg = config_read(deps.storage).load()?;
|
||||
let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
|
||||
contract_addr: cfg.wormhole_contract,
|
||||
msg: to_binary(&WormholeQueryMsg::VerifyVAA {
|
||||
vaa: data.clone(),
|
||||
block_time,
|
||||
})?,
|
||||
}))?;
|
||||
Ok(vaa)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
|
||||
match msg {
|
||||
ExecuteMsg::InitiateTransfer {
|
||||
contract_addr,
|
||||
token_id,
|
||||
recipient_chain,
|
||||
recipient,
|
||||
nonce,
|
||||
} => handle_initiate_transfer(
|
||||
deps,
|
||||
env,
|
||||
info,
|
||||
contract_addr,
|
||||
token_id,
|
||||
recipient_chain,
|
||||
recipient.to_array()?,
|
||||
nonce,
|
||||
),
|
||||
ExecuteMsg::SubmitVaa { data } => submit_vaa(deps, env, info, &data),
|
||||
ExecuteMsg::RegisterAssetHook { asset_id } => {
|
||||
handle_register_asset(deps, env, info, asset_id.as_slice())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_vaa(
|
||||
mut deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
data: &Binary,
|
||||
) -> StdResult<Response> {
|
||||
let state = config_read(deps.storage).load()?;
|
||||
|
||||
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
|
||||
let data = vaa.payload;
|
||||
|
||||
if vaa_archive_check(deps.storage, vaa.hash.as_slice()) {
|
||||
return ContractError::VaaAlreadyExecuted.std_err();
|
||||
}
|
||||
vaa_archive_add(deps.storage, vaa.hash.as_slice())?;
|
||||
|
||||
// check if vaa is from governance
|
||||
if state.gov_chain == vaa.emitter_chain && state.gov_address == vaa.emitter_address {
|
||||
return handle_governance_payload(deps, env, &data);
|
||||
}
|
||||
|
||||
let message = TokenBridgeMessage::deserialize(&data)?;
|
||||
|
||||
match message.action {
|
||||
Action::TRANSFER => handle_complete_transfer(
|
||||
deps,
|
||||
env,
|
||||
info,
|
||||
vaa.emitter_chain,
|
||||
vaa.emitter_address,
|
||||
TransferInfo::deserialize(&message.payload)?,
|
||||
),
|
||||
_ => ContractError::InvalidVAAAction.std_err(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_governance_payload(deps: DepsMut, env: Env, data: &[u8]) -> StdResult<Response> {
|
||||
let gov_packet = GovernancePacket::deserialize(data)?;
|
||||
let module = get_string_from_32(&gov_packet.module);
|
||||
|
||||
if module != "NFTBridge" {
|
||||
return Err(StdError::generic_err("this is not a valid module"));
|
||||
}
|
||||
|
||||
if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID {
|
||||
return Err(StdError::generic_err(
|
||||
"the governance VAA is for another chain",
|
||||
));
|
||||
}
|
||||
|
||||
match gov_packet.action {
|
||||
1u8 => handle_register_chain(deps, env, RegisterChain::deserialize(&gov_packet.payload)?),
|
||||
2u8 => handle_upgrade_contract(
|
||||
deps,
|
||||
env,
|
||||
UpgradeContract::deserialize(&gov_packet.payload)?,
|
||||
),
|
||||
_ => ContractError::InvalidVAAAction.std_err(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_upgrade_contract(
|
||||
_deps: DepsMut,
|
||||
env: Env,
|
||||
upgrade_contract: UpgradeContract,
|
||||
) -> StdResult<Response> {
|
||||
Ok(Response::new()
|
||||
.add_message(CosmosMsg::Wasm(WasmMsg::Migrate {
|
||||
contract_addr: env.contract.address.to_string(),
|
||||
new_code_id: upgrade_contract.new_contract,
|
||||
msg: to_binary(&MigrateMsg {})?,
|
||||
}))
|
||||
.add_attribute("action", "contract_upgrade"))
|
||||
}
|
||||
|
||||
fn handle_register_chain(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
register_chain: RegisterChain,
|
||||
) -> StdResult<Response> {
|
||||
let RegisterChain {
|
||||
chain_id,
|
||||
chain_address,
|
||||
} = register_chain;
|
||||
|
||||
let existing = bridge_contracts_read(deps.storage).load(&chain_id.to_be_bytes());
|
||||
if existing.is_ok() {
|
||||
return Err(StdError::generic_err(
|
||||
"bridge contract already exists for this chain",
|
||||
));
|
||||
}
|
||||
|
||||
let mut bucket = bridge_contracts(deps.storage);
|
||||
bucket.save(&chain_id.to_be_bytes(), &chain_address)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("chain_id", chain_id.to_string())
|
||||
.add_attribute("chain_address", hex::encode(chain_address)))
|
||||
}
|
||||
|
||||
fn handle_complete_transfer(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
_info: MessageInfo,
|
||||
emitter_chain: u16,
|
||||
emitter_address: Vec<u8>,
|
||||
transfer_info: TransferInfo,
|
||||
) -> StdResult<Response> {
|
||||
let cfg = config_read(deps.storage).load()?;
|
||||
|
||||
let expected_contract =
|
||||
bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?;
|
||||
|
||||
// must be sent by a registered token bridge contract
|
||||
if expected_contract != emitter_address {
|
||||
return Err(StdError::generic_err("invalid emitter"));
|
||||
}
|
||||
|
||||
if transfer_info.recipient_chain != CHAIN_ID {
|
||||
return Err(StdError::generic_err(
|
||||
"this transfer is not directed at this chain",
|
||||
));
|
||||
}
|
||||
|
||||
let token_chain = transfer_info.nft_chain;
|
||||
let target_address = &(&transfer_info.recipient[..]).get_address(0);
|
||||
|
||||
let mut messages = vec![];
|
||||
|
||||
let recipient = deps
|
||||
.api
|
||||
.addr_humanize(target_address)
|
||||
.or_else(|_| ContractError::WrongTargetAddressFormat.std_err())?;
|
||||
|
||||
let contract_addr;
|
||||
|
||||
let token_id = from_external_token_id(
|
||||
deps.storage,
|
||||
token_chain,
|
||||
&transfer_info.nft_address,
|
||||
&transfer_info.token_id,
|
||||
)?;
|
||||
|
||||
if token_chain != CHAIN_ID {
|
||||
// NFT is not native to this chain, so we need a wrapper
|
||||
let asset_address = transfer_info.nft_address;
|
||||
let asset_id = build_asset_id(token_chain, &asset_address);
|
||||
|
||||
let token_uri = String::from_utf8(transfer_info.uri.to_vec())
|
||||
.map_err(|_| StdError::generic_err("could not parse uri string"))?;
|
||||
|
||||
let mint_msg = cw721_base::msg::MintMsg {
|
||||
token_id,
|
||||
owner: recipient.to_string(),
|
||||
token_uri: Some(token_uri),
|
||||
extension: None,
|
||||
};
|
||||
|
||||
// Check if this asset is already deployed
|
||||
if let Ok(wrapped_addr) = wrapped_asset_read(deps.storage).load(&asset_id) {
|
||||
contract_addr = wrapped_addr;
|
||||
// Asset already deployed, just mint
|
||||
|
||||
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: contract_addr.clone(),
|
||||
msg: to_binary(&cw721_base::msg::ExecuteMsg::Mint(mint_msg))?,
|
||||
funds: vec![],
|
||||
}));
|
||||
} else {
|
||||
contract_addr = env.contract.address.clone().into_string();
|
||||
wrapped_asset(deps.storage)
|
||||
.save(&asset_id, &HumanAddr::from(WRAPPED_ASSET_UPDATING))?;
|
||||
|
||||
let (name, symbol) = if token_chain == 1 {
|
||||
let spl_cache_item = SplCacheItem {
|
||||
name: transfer_info.name,
|
||||
symbol: transfer_info.symbol,
|
||||
};
|
||||
spl_cache(deps.storage).save(&transfer_info.token_id, &spl_cache_item)?;
|
||||
// Solana NFTs all use the same NFT contract, so unify the name
|
||||
(
|
||||
"Wormhole Bridged Solana-NFT".to_string(),
|
||||
"WORMSPLNFT".to_string(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
get_string_from_32(&transfer_info.name),
|
||||
get_string_from_32(&transfer_info.symbol),
|
||||
)
|
||||
};
|
||||
messages.push(CosmosMsg::Wasm(WasmMsg::Instantiate {
|
||||
admin: Some(contract_addr.clone()),
|
||||
code_id: cfg.wrapped_asset_code_id,
|
||||
msg: to_binary(&cw721_wrapped::msg::InstantiateMsg {
|
||||
name,
|
||||
symbol,
|
||||
asset_chain: token_chain,
|
||||
asset_address: (&transfer_info.nft_address[..]).into(),
|
||||
minter: env.contract.address.into_string(),
|
||||
mint: Some(mint_msg),
|
||||
init_hook: Some(cw721_wrapped::msg::InitHook {
|
||||
msg: cw721_wrapped::to_binary(&ExecuteMsg::RegisterAssetHook {
|
||||
asset_id: asset_id.to_vec().into(),
|
||||
})
|
||||
.map_err(|_| StdError::generic_err("couldn't convert to binary"))?,
|
||||
contract_addr: contract_addr.clone(),
|
||||
}),
|
||||
})?,
|
||||
funds: vec![],
|
||||
label: String::new(),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// Native NFT, transfer from custody
|
||||
let token_address = (&transfer_info.nft_address[..]).get_address(0);
|
||||
|
||||
contract_addr = deps.api.addr_humanize(&token_address)?.to_string();
|
||||
|
||||
messages.push(CosmosMsg::<Empty>::Wasm(WasmMsg::Execute {
|
||||
contract_addr: contract_addr.clone(),
|
||||
msg: to_binary(&cw721_base::msg::ExecuteMsg::<Option<Empty>>::TransferNft {
|
||||
recipient: recipient.to_string(),
|
||||
token_id,
|
||||
})?,
|
||||
funds: vec![],
|
||||
}));
|
||||
}
|
||||
Ok(Response::new()
|
||||
.add_messages(messages)
|
||||
.add_attribute("action", "complete_transfer")
|
||||
.add_attribute("recipient", recipient)
|
||||
.add_attribute("contract", contract_addr))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_initiate_transfer(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
asset: HumanAddr,
|
||||
token_id: String,
|
||||
recipient_chain: u16,
|
||||
recipient: [u8; 32],
|
||||
nonce: u32,
|
||||
) -> StdResult<Response> {
|
||||
if recipient_chain == CHAIN_ID {
|
||||
return ContractError::SameSourceAndTarget.std_err();
|
||||
}
|
||||
|
||||
let asset_chain: u16;
|
||||
let asset_address: [u8; 32];
|
||||
|
||||
let cfg: ConfigInfo = config_read(deps.storage).load()?;
|
||||
let asset_canonical: CanonicalAddr = deps.api.addr_canonicalize(&asset)?;
|
||||
|
||||
let mut messages: Vec<CosmosMsg> = vec![];
|
||||
|
||||
if wrapped_asset_address_read(deps.storage).load(asset_canonical.as_slice()).is_ok() {
|
||||
// This is a deployed wrapped asset, burn it
|
||||
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: asset.clone(),
|
||||
msg: to_binary(&cw721_wrapped::msg::ExecuteMsg::Burn::<Option<Empty>> {
|
||||
token_id: token_id.clone(),
|
||||
})?,
|
||||
funds: vec![],
|
||||
}));
|
||||
|
||||
let wrapped_token_info: cw721_wrapped::msg::WrappedAssetInfoResponse = deps
|
||||
.querier
|
||||
.custom_query(&QueryRequest::<Empty>::Wasm(WasmQuery::Smart {
|
||||
contract_addr: asset.clone(),
|
||||
msg: to_binary(&cw721_wrapped::msg::QueryMsg::WrappedAssetInfo {})?,
|
||||
}))?;
|
||||
|
||||
asset_address = wrapped_token_info.asset_address.to_array()?;
|
||||
asset_chain = wrapped_token_info.asset_chain;
|
||||
} else {
|
||||
// Native NFT, lock it up
|
||||
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: asset.clone(),
|
||||
msg: to_binary(&cw721_base::msg::ExecuteMsg::<Option<Empty>>::TransferNft {
|
||||
recipient: env.contract.address.to_string(),
|
||||
token_id: token_id.clone(),
|
||||
})?,
|
||||
funds: vec![],
|
||||
}));
|
||||
|
||||
asset_chain = CHAIN_ID;
|
||||
asset_address = extend_address_to_32_array(&asset_canonical);
|
||||
};
|
||||
|
||||
let external_token_id =
|
||||
to_external_token_id(deps.storage, asset_chain, &asset_address, token_id.clone())?;
|
||||
|
||||
let symbol: [u8; 32];
|
||||
let name: [u8; 32];
|
||||
|
||||
if asset_chain == 1 {
|
||||
let SplCacheItem {
|
||||
name: cached_name,
|
||||
symbol: cached_symbol,
|
||||
} = spl_cache_read(deps.storage).load(&external_token_id)?;
|
||||
symbol = cached_symbol;
|
||||
name = cached_name;
|
||||
} else {
|
||||
let response: cw721::ContractInfoResponse =
|
||||
deps.querier
|
||||
.custom_query(&QueryRequest::<Empty>::Wasm(WasmQuery::Smart {
|
||||
contract_addr: asset.clone(),
|
||||
msg: to_binary(&cw721_base::msg::QueryMsg::ContractInfo {})?,
|
||||
}))?;
|
||||
name = string_to_array(&response.name);
|
||||
symbol = string_to_array(&response.symbol);
|
||||
}
|
||||
|
||||
let cw721::NftInfoResponse::<Option<Empty>> { token_uri, .. } =
|
||||
deps.querier
|
||||
.custom_query(&QueryRequest::<Empty>::Wasm(WasmQuery::Smart {
|
||||
contract_addr: asset,
|
||||
msg: to_binary(&cw721_base::msg::QueryMsg::NftInfo {
|
||||
token_id: token_id.clone(),
|
||||
})?,
|
||||
}))?;
|
||||
|
||||
let transfer_info = TransferInfo {
|
||||
nft_address: asset_address,
|
||||
nft_chain: asset_chain,
|
||||
symbol,
|
||||
name,
|
||||
token_id: external_token_id,
|
||||
uri: BoundedVec::new(token_uri.unwrap().into())?,
|
||||
recipient,
|
||||
recipient_chain,
|
||||
};
|
||||
|
||||
let token_bridge_message = TokenBridgeMessage {
|
||||
action: Action::TRANSFER,
|
||||
payload: transfer_info.serialize(),
|
||||
};
|
||||
|
||||
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: cfg.wormhole_contract,
|
||||
msg: to_binary(&WormholeExecuteMsg::PostMessage {
|
||||
message: Binary::from(token_bridge_message.serialize()),
|
||||
nonce,
|
||||
})?,
|
||||
funds: vec![],
|
||||
}));
|
||||
|
||||
Ok(Response::new()
|
||||
.add_messages(messages)
|
||||
.add_attribute("transfer.token_chain", asset_chain.to_string())
|
||||
.add_attribute("transfer.token", hex::encode(asset_address))
|
||||
.add_attribute("transfer.token_id", token_id)
|
||||
.add_attribute("transfer.external_token_id", hex::encode(external_token_id))
|
||||
.add_attribute(
|
||||
"transfer.sender",
|
||||
hex::encode(extend_address_to_32(
|
||||
&deps.api.addr_canonicalize(info.sender.as_str())?,
|
||||
)),
|
||||
)
|
||||
.add_attribute("transfer.recipient_chain", recipient_chain.to_string())
|
||||
.add_attribute("transfer.recipient", hex::encode(recipient))
|
||||
.add_attribute("transfer.nonce", nonce.to_string())
|
||||
.add_attribute("transfer.block_time", env.block.time.seconds().to_string()))
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
|
||||
match msg {
|
||||
QueryMsg::WrappedRegistry { chain, address } => {
|
||||
to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?)
|
||||
}
|
||||
QueryMsg::AllWrappedAssets { } => {
|
||||
to_binary(&query_all_wrapped_assets(deps)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle wrapped asset registration messages
|
||||
fn handle_register_asset(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
info: MessageInfo,
|
||||
asset_id: &[u8],
|
||||
) -> StdResult<Response> {
|
||||
let mut bucket = wrapped_asset(deps.storage);
|
||||
let result = bucket
|
||||
.load(asset_id)
|
||||
.map_err(|_| ContractError::RegistrationForbidden.std())?;
|
||||
if result != WRAPPED_ASSET_UPDATING {
|
||||
return ContractError::AssetAlreadyRegistered.std_err();
|
||||
}
|
||||
|
||||
bucket.save(asset_id, &info.sender.to_string())?;
|
||||
|
||||
let contract_address: CanonicalAddr = deps.api.addr_canonicalize(info.sender.as_str())?;
|
||||
wrapped_asset_address(deps.storage).save(contract_address.as_slice(), &asset_id.to_vec())?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "register_asset")
|
||||
.add_attribute("asset_id", format!("{:?}", asset_id))
|
||||
.add_attribute("contract_addr", info.sender))
|
||||
}
|
||||
|
||||
pub fn query_wrapped_registry(
|
||||
deps: Deps,
|
||||
chain: u16,
|
||||
address: &[u8],
|
||||
) -> StdResult<WrappedRegistryResponse> {
|
||||
let asset_id = build_asset_id(chain, address);
|
||||
// Check if this asset is already deployed
|
||||
match wrapped_asset_read(deps.storage).load(&asset_id) {
|
||||
Ok(address) => Ok(WrappedRegistryResponse { address }),
|
||||
Err(_) => ContractError::AssetNotFound.std_err(),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_all_wrapped_assets(deps: Deps) -> StdResult<Vec<String>> {
|
||||
let bucket = wrapped_asset_address_read(deps.storage);
|
||||
let mut result = vec![];
|
||||
for item in bucket.range(None, None, Order::Ascending) {
|
||||
let contract_address = item?.0.into();
|
||||
result.push(deps.api.addr_humanize(&contract_address)?.to_string())
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
||||
fn build_asset_id(chain: u16, address: &[u8]) -> Vec<u8> {
|
||||
let mut asset_id: Vec<u8> = vec![];
|
||||
asset_id.extend_from_slice(&chain.to_be_bytes());
|
||||
asset_id.extend_from_slice(address);
|
||||
|
||||
let mut hasher = Keccak256::new();
|
||||
hasher.update(asset_id);
|
||||
hasher.finalize().to_vec()
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
pub mod contract;
|
||||
pub mod msg;
|
||||
pub mod state;
|
||||
pub mod token_id;
|
||||
|
||||
// Chain ID of Terra
|
||||
const CHAIN_ID: u16 = 3;
|
|
@ -0,0 +1,61 @@
|
|||
use cosmwasm_std::Binary;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InstantiateMsg {
|
||||
// governance contract details
|
||||
pub gov_chain: u16,
|
||||
pub gov_address: Binary,
|
||||
|
||||
pub wormhole_contract: HumanAddr,
|
||||
pub wrapped_asset_code_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
RegisterAssetHook {
|
||||
asset_id: Binary,
|
||||
},
|
||||
|
||||
InitiateTransfer {
|
||||
contract_addr: String,
|
||||
token_id: String,
|
||||
recipient_chain: u16,
|
||||
recipient: Binary,
|
||||
nonce: u32,
|
||||
},
|
||||
|
||||
SubmitVaa {
|
||||
data: Binary,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
WrappedRegistry { chain: u16, address: Binary },
|
||||
AllWrappedAssets { },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct WrappedRegistryResponse {
|
||||
pub address: HumanAddr,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WormholeQueryMsg {
|
||||
VerifyVAA { vaa: Binary, block_time: u64 },
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
use std::convert::TryInto;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use cosmwasm_std::{
|
||||
StdError,
|
||||
StdResult,
|
||||
Storage,
|
||||
};
|
||||
use cosmwasm_storage::{
|
||||
bucket,
|
||||
bucket_read,
|
||||
singleton,
|
||||
singleton_read,
|
||||
Bucket,
|
||||
ReadonlyBucket,
|
||||
ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
|
||||
use wormhole::byte_utils::ByteUtils;
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
pub static CONFIG_KEY: &[u8] = b"config";
|
||||
pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset";
|
||||
pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address";
|
||||
pub static BRIDGE_CONTRACTS_KEY: &[u8] = b"bridge_contracts";
|
||||
pub static TOKEN_ID_HASHES_KEY: &[u8] = b"token_id_hashes";
|
||||
pub static SPL_CACHE_KEY: &[u8] = b"spl_cache";
|
||||
|
||||
// Guardian set information
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct ConfigInfo {
|
||||
// governance contract details
|
||||
pub gov_chain: u16,
|
||||
pub gov_address: Vec<u8>,
|
||||
|
||||
pub wormhole_contract: HumanAddr,
|
||||
pub wrapped_asset_code_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct SplCacheItem {
|
||||
pub name: [u8; 32],
|
||||
pub symbol: [u8; 32],
|
||||
}
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<ConfigInfo> {
|
||||
singleton(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<ConfigInfo> {
|
||||
singleton_read(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn bridge_contracts(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
|
||||
bucket(storage, BRIDGE_CONTRACTS_KEY)
|
||||
}
|
||||
|
||||
pub fn bridge_contracts_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
|
||||
bucket_read(storage, BRIDGE_CONTRACTS_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset(storage: &mut dyn Storage) -> Bucket<HumanAddr> {
|
||||
bucket(storage, WRAPPED_ASSET_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_read(storage: &dyn Storage) -> ReadonlyBucket<HumanAddr> {
|
||||
bucket_read(storage, WRAPPED_ASSET_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_address(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
|
||||
bucket(storage, WRAPPED_ASSET_ADDRESS_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_address_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
|
||||
bucket_read(storage, WRAPPED_ASSET_ADDRESS_KEY)
|
||||
}
|
||||
|
||||
pub fn spl_cache(storage: &mut dyn Storage) -> Bucket<SplCacheItem> {
|
||||
bucket(storage, SPL_CACHE_KEY)
|
||||
}
|
||||
|
||||
pub fn spl_cache_read(storage: &dyn Storage) -> ReadonlyBucket<SplCacheItem> {
|
||||
bucket_read(storage, SPL_CACHE_KEY)
|
||||
}
|
||||
|
||||
pub fn token_id_hashes(storage: &mut dyn Storage, chain: u16, address: [u8; 32]) -> Bucket<String> {
|
||||
Bucket::multilevel(
|
||||
storage,
|
||||
&[TOKEN_ID_HASHES_KEY, &chain.to_be_bytes(), &address],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn token_id_hashes_read(
|
||||
storage: &mut dyn Storage,
|
||||
chain: u16,
|
||||
address: [u8; 32],
|
||||
) -> ReadonlyBucket<String> {
|
||||
ReadonlyBucket::multilevel(
|
||||
storage,
|
||||
&[TOKEN_ID_HASHES_KEY, &chain.to_be_bytes(), &address],
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Action;
|
||||
|
||||
impl Action {
|
||||
pub const TRANSFER: u8 = 1;
|
||||
}
|
||||
|
||||
// 0 u8 action
|
||||
// 1 [u8] payload
|
||||
pub struct TokenBridgeMessage {
|
||||
pub action: u8,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TokenBridgeMessage {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let action = data.get_u8(0);
|
||||
let payload = &data[1..];
|
||||
|
||||
Ok(TokenBridgeMessage {
|
||||
action,
|
||||
payload: payload.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
[self.action.to_be_bytes().to_vec(), self.payload.clone()].concat()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[repr(transparent)]
|
||||
pub struct BoundedVec<T, const N: usize> {
|
||||
vec: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> BoundedVec<T, N> {
|
||||
pub fn new(vec: Vec<T>) -> StdResult<Self> {
|
||||
if vec.len() > N {
|
||||
return Result::Err(StdError::GenericErr {
|
||||
msg: format!("vector length exceeds {}", N),
|
||||
});
|
||||
};
|
||||
Ok(Self { vec })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_vec(&self) -> Vec<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.vec.clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.vec.len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.vec.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct TransferInfo {
|
||||
pub nft_address: [u8; 32],
|
||||
pub nft_chain: u16,
|
||||
pub symbol: [u8; 32],
|
||||
pub name: [u8; 32],
|
||||
pub token_id: [u8; 32],
|
||||
pub uri: BoundedVec<u8, 200>, // max 200 bytes due to Solana
|
||||
pub recipient: [u8; 32],
|
||||
pub recipient_chain: u16,
|
||||
}
|
||||
|
||||
impl TransferInfo {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let mut offset: usize = 0; // offset into data in bytes
|
||||
let nft_address = data.get_const_bytes::<32>(offset);
|
||||
offset += 32;
|
||||
let nft_chain = data.get_u16(offset);
|
||||
offset += 2;
|
||||
let symbol = data.get_const_bytes::<32>(offset);
|
||||
offset += 32;
|
||||
let name = data.get_const_bytes::<32>(offset);
|
||||
offset += 32;
|
||||
let token_id = data.get_const_bytes::<32>(offset);
|
||||
offset += 32;
|
||||
let uri_length: usize = data.get_u8(offset).into();
|
||||
offset += 1;
|
||||
let uri = data.get_bytes(offset, uri_length).to_vec();
|
||||
offset += uri_length;
|
||||
let recipient = data.get_const_bytes::<32>(offset);
|
||||
offset += 32;
|
||||
let recipient_chain = data.get_u16(offset);
|
||||
offset += 2;
|
||||
|
||||
if data.len() != offset {
|
||||
return Result::Err(StdError::GenericErr {
|
||||
msg: format!(
|
||||
"Invalid transfer length, expected {}, but got {}",
|
||||
offset,
|
||||
data.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(TransferInfo {
|
||||
nft_address,
|
||||
nft_chain,
|
||||
symbol,
|
||||
name,
|
||||
token_id,
|
||||
uri: BoundedVec::new(uri.to_vec())?,
|
||||
recipient,
|
||||
recipient_chain,
|
||||
})
|
||||
}
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
[
|
||||
self.nft_address.to_vec(),
|
||||
self.nft_chain.to_be_bytes().to_vec(),
|
||||
self.symbol.to_vec(),
|
||||
self.name.to_vec(),
|
||||
self.token_id.to_vec(),
|
||||
vec![self.uri.len().try_into().unwrap()], // won't panic, because uri.len() is less than 200
|
||||
self.uri.to_vec(),
|
||||
self.recipient.to_vec(),
|
||||
self.recipient_chain.to_be_bytes().to_vec(),
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpgradeContract {
|
||||
pub new_contract: u64,
|
||||
}
|
||||
|
||||
pub struct RegisterChain {
|
||||
pub chain_id: u16,
|
||||
pub chain_address: Vec<u8>,
|
||||
}
|
||||
|
||||
impl UpgradeContract {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let new_contract = data.get_u64(24);
|
||||
Ok(UpgradeContract { new_contract })
|
||||
}
|
||||
}
|
||||
|
||||
impl RegisterChain {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let chain_id = data.get_u16(0);
|
||||
let chain_address = data[2..].to_vec();
|
||||
|
||||
Ok(RegisterChain {
|
||||
chain_id,
|
||||
chain_address,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
use bigint::U256;
|
||||
use cosmwasm_std::{
|
||||
StdError,
|
||||
StdResult,
|
||||
Storage,
|
||||
};
|
||||
|
||||
use sha3::{
|
||||
digest::{consts::U32, generic_array::GenericArray},
|
||||
Digest, Keccak256,
|
||||
};
|
||||
use wormhole::byte_utils::ByteUtils;
|
||||
|
||||
use crate::{
|
||||
state::{
|
||||
token_id_hashes,
|
||||
token_id_hashes_read,
|
||||
},
|
||||
CHAIN_ID,
|
||||
};
|
||||
|
||||
// NOTE: [External and internal token id conversion]
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
// The CW721 NFT standard allows token ids to be arbitrarily long (utf8)
|
||||
// strings, while the token_ids in VAA payloads are always 32 bytes (and not
|
||||
// necessarily valid utf8).
|
||||
//
|
||||
// We call a token id that's in string format an "internal id", and a token id
|
||||
// that's in 32 byte format an "external id". Note that whether a token id is in
|
||||
// internal format or external format doesn't imply which chain the token id
|
||||
// originates from. We can have a terra (native) token id in both internal and
|
||||
// external formats, and likewise we can have an ethereum token in in both
|
||||
// internal and external formats.
|
||||
//
|
||||
// To support seamless transfers through the bridge, we need a way to have a
|
||||
// 1-to-1 mapping from internal ids to external ids.
|
||||
// When a foreign (such as ethereum or solana) token id first comes through, we
|
||||
// simply render it into a string by formatting it as a decimal number. Then,
|
||||
// when we want to transfer such a token back through the bridge, we simply
|
||||
// parse the string back into a u256 (32 byte) number.
|
||||
//
|
||||
// When a native token id first leaves through the bridge, we turn its id into a
|
||||
// 32 byte hash (keccak256). This hash is the external id. We store a mapping
|
||||
//
|
||||
// (chain_id, nft_address, keccak256(internal_id)) => internal_id
|
||||
//
|
||||
// so that we can turn it back into an internal id when it comes back through
|
||||
// the bridge. When the token is sent back, we could choose to delete the hash
|
||||
// from the store, but we do not. This way, external token verifiers will be
|
||||
// able to verify NFT origins even for NFTs that have been transferred back.
|
||||
//
|
||||
// If two token ids within the same contract have the same keccak256 hash, then
|
||||
// it's possible to lose tokens, but this is very unlikely.
|
||||
|
||||
pub fn from_external_token_id(
|
||||
storage: &mut dyn Storage,
|
||||
nft_chain: u16,
|
||||
nft_address: &[u8; 32],
|
||||
token_id_external: &[u8; 32],
|
||||
) -> StdResult<String> {
|
||||
if nft_chain == CHAIN_ID {
|
||||
token_id_hashes_read(storage, nft_chain, *nft_address).load(token_id_external)
|
||||
} else {
|
||||
Ok(format!("{}", U256::from_big_endian(token_id_external)))
|
||||
}
|
||||
}
|
||||
|
||||
fn hash(token_id: &str) -> GenericArray<u8, U32> {
|
||||
let mut hasher = Keccak256::new();
|
||||
hasher.update(token_id);
|
||||
hasher.finalize()
|
||||
}
|
||||
|
||||
pub fn to_external_token_id(
|
||||
storage: &mut dyn Storage,
|
||||
nft_chain: u16,
|
||||
nft_address: &[u8; 32],
|
||||
token_id_internal: String,
|
||||
) -> StdResult<[u8; 32]> {
|
||||
if nft_chain == CHAIN_ID {
|
||||
let hash = hash(&token_id_internal);
|
||||
token_id_hashes(storage, nft_chain, *nft_address).save(&hash, &token_id_internal)?;
|
||||
Ok(hash.as_slice().get_const_bytes(0))
|
||||
} else {
|
||||
let mut bytes = [0; 32];
|
||||
U256::from_dec_str(&token_id_internal)
|
||||
.map_err(|_| {
|
||||
StdError::generic_err(format!(
|
||||
"{} could not be parsed as a decimal number",
|
||||
token_id_internal
|
||||
))
|
||||
})?
|
||||
.to_big_endian(&mut bytes);
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
[alias]
|
||||
wasm = "build --release --target wasm32-unknown-unknown"
|
||||
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib --features backtraces"
|
||||
integration-test = "test --test integration"
|
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "token-bridge-terra"
|
||||
version = "0.1.0"
|
||||
authors = ["Yuriy Savchenko <yuriy.savchenko@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "Wormhole token bridge"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
# use library feature to disable all init/handle/query exports
|
||||
library = []
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { version = "0.16.0" }
|
||||
cosmwasm-storage = { version = "0.16.0" }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
cw20 = "0.8.0"
|
||||
cw20-base = { version = "0.8.0", features = ["library"] }
|
||||
cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] }
|
||||
terraswap = "2.4.0"
|
||||
wormhole-bridge-terra = { path = "../wormhole", features = ["library"] }
|
||||
thiserror = { version = "1.0.20" }
|
||||
k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] }
|
||||
sha3 = { version = "0.9.1", default-features = false }
|
||||
generic-array = { version = "0.14.4" }
|
||||
hex = "0.4.2"
|
||||
lazy_static = "1.4.0"
|
||||
bigint = "4"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
|
@ -0,0 +1,114 @@
|
|||
static WASM: &[u8] = include_bytes!("../../../target/wasm32-unknown-unknown/release/wormhole.wasm");
|
||||
|
||||
use cosmwasm_std::{
|
||||
from_slice,
|
||||
Coin,
|
||||
Env,
|
||||
HumanAddr,
|
||||
InitResponse,
|
||||
};
|
||||
use cosmwasm_storage::to_length_prefixed;
|
||||
use cosmwasm_vm::{
|
||||
testing::{
|
||||
init,
|
||||
mock_env,
|
||||
mock_instance,
|
||||
MockApi,
|
||||
MockQuerier,
|
||||
MockStorage,
|
||||
},
|
||||
Api,
|
||||
Instance,
|
||||
Storage,
|
||||
};
|
||||
|
||||
use wormhole::{
|
||||
msg::InitMsg,
|
||||
state::{
|
||||
ConfigInfo,
|
||||
GuardianAddress,
|
||||
GuardianSetInfo,
|
||||
CONFIG_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
use hex;
|
||||
|
||||
enum TestAddress {
|
||||
INITIALIZER,
|
||||
}
|
||||
|
||||
impl TestAddress {
|
||||
fn value(&self) -> HumanAddr {
|
||||
match self {
|
||||
TestAddress::INITIALIZER => HumanAddr::from("initializer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mock_env_height(signer: &HumanAddr, height: u64, time: u64) -> Env {
|
||||
let mut env = mock_env(signer, &[]);
|
||||
env.block.height = height;
|
||||
env.block.time = time;
|
||||
env
|
||||
}
|
||||
|
||||
fn get_config_info<S: Storage>(storage: &S) -> ConfigInfo {
|
||||
let key = to_length_prefixed(CONFIG_KEY);
|
||||
let data = storage
|
||||
.get(&key)
|
||||
.0
|
||||
.expect("error getting data")
|
||||
.expect("data should exist");
|
||||
from_slice(&data).expect("invalid data")
|
||||
}
|
||||
|
||||
fn do_init(
|
||||
height: u64,
|
||||
guardians: &Vec<GuardianAddress>,
|
||||
) -> Instance<MockStorage, MockApi, MockQuerier> {
|
||||
let mut deps = mock_instance(WASM, &[]);
|
||||
let init_msg = InitMsg {
|
||||
initial_guardian_set: GuardianSetInfo {
|
||||
addresses: guardians.clone(),
|
||||
expiration_time: 100,
|
||||
},
|
||||
guardian_set_expirity: 50,
|
||||
wrapped_asset_code_id: 999,
|
||||
};
|
||||
let env = mock_env_height(&TestAddress::INITIALIZER.value(), height, 0);
|
||||
let owner = deps
|
||||
.api
|
||||
.canonical_address(&TestAddress::INITIALIZER.value())
|
||||
.0
|
||||
.unwrap();
|
||||
let res: InitResponse = init(&mut deps, env, init_msg).unwrap();
|
||||
assert_eq!(0, res.messages.len());
|
||||
|
||||
// query the store directly
|
||||
deps.with_storage(|storage| {
|
||||
assert_eq!(
|
||||
get_config_info(storage),
|
||||
ConfigInfo {
|
||||
guardian_set_index: 0,
|
||||
guardian_set_expirity: 50,
|
||||
wrapped_asset_code_id: 999,
|
||||
owner,
|
||||
fee: Coin::new(10000, "uluna"),
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
deps
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_works() {
|
||||
let guardians = vec![GuardianAddress::from(GuardianAddress {
|
||||
bytes: hex::decode("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe")
|
||||
.expect("Decoding failed")
|
||||
.into(),
|
||||
})];
|
||||
let _deps = do_init(111, &guardians);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,9 @@
|
|||
#[cfg(test)]
|
||||
extern crate lazy_static;
|
||||
|
||||
pub mod contract;
|
||||
pub mod msg;
|
||||
pub mod state;
|
||||
|
||||
#[cfg(test)]
|
||||
mod testing;
|
|
@ -0,0 +1,98 @@
|
|||
use cosmwasm_std::{
|
||||
Binary,
|
||||
Uint128,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
use terraswap::asset::{
|
||||
Asset,
|
||||
AssetInfo,
|
||||
};
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InstantiateMsg {
|
||||
// governance contract details
|
||||
pub gov_chain: u16,
|
||||
pub gov_address: Binary,
|
||||
|
||||
pub wormhole_contract: HumanAddr,
|
||||
pub wrapped_asset_code_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
RegisterAssetHook {
|
||||
asset_id: Binary,
|
||||
},
|
||||
|
||||
DepositTokens {},
|
||||
WithdrawTokens {
|
||||
asset: AssetInfo,
|
||||
},
|
||||
|
||||
InitiateTransfer {
|
||||
asset: Asset,
|
||||
recipient_chain: u16,
|
||||
recipient: Binary,
|
||||
fee: Uint128,
|
||||
nonce: u32,
|
||||
},
|
||||
|
||||
InitiateTransferWithPayload {
|
||||
asset: Asset,
|
||||
recipient_chain: u16,
|
||||
recipient: Binary,
|
||||
fee: Uint128,
|
||||
payload: Binary,
|
||||
nonce: u32,
|
||||
},
|
||||
|
||||
SubmitVaa {
|
||||
data: Binary,
|
||||
},
|
||||
|
||||
CreateAssetMeta {
|
||||
asset_info: AssetInfo,
|
||||
nonce: u32,
|
||||
},
|
||||
|
||||
CompleteTransferWithPayload {
|
||||
data: Binary,
|
||||
relayer: HumanAddr,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
WrappedRegistry { chain: u16, address: Binary },
|
||||
TransferInfo { vaa: Binary },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct WrappedRegistryResponse {
|
||||
pub address: HumanAddr,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct TransferInfoResponse {
|
||||
pub amount: Uint128,
|
||||
pub token_address: Vec<u8>,
|
||||
pub token_chain: u16,
|
||||
pub recipient: Vec<u8>,
|
||||
pub recipient_chain: u16,
|
||||
pub fee: Uint128,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use cosmwasm_std::{
|
||||
CanonicalAddr,
|
||||
StdError,
|
||||
StdResult,
|
||||
Storage,
|
||||
Uint128,
|
||||
};
|
||||
use cosmwasm_storage::{
|
||||
bucket,
|
||||
bucket_read,
|
||||
singleton,
|
||||
singleton_read,
|
||||
Bucket,
|
||||
ReadonlyBucket,
|
||||
ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
|
||||
use wormhole::byte_utils::ByteUtils;
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
pub static CONFIG_KEY: &[u8] = b"config";
|
||||
pub static TRANSFER_TMP_KEY: &[u8] = b"transfer_tmp";
|
||||
pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset";
|
||||
pub static WRAPPED_ASSET_SEQ_KEY: &[u8] = b"wrapped_seq_asset";
|
||||
pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address";
|
||||
pub static BRIDGE_CONTRACTS: &[u8] = b"bridge_contracts";
|
||||
pub static BRIDGE_DEPOSITS: &[u8] = b"bridge_deposits";
|
||||
pub static NATIVE_COUNTER: &[u8] = b"native_counter";
|
||||
|
||||
// Guardian set information
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct ConfigInfo {
|
||||
// governance contract details
|
||||
pub gov_chain: u16,
|
||||
pub gov_address: Vec<u8>,
|
||||
|
||||
pub wormhole_contract: HumanAddr,
|
||||
pub wrapped_asset_code_id: u64,
|
||||
}
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<ConfigInfo> {
|
||||
singleton(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<ConfigInfo> {
|
||||
singleton_read(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn bridge_deposit(storage: &mut dyn Storage) -> Bucket<Uint128> {
|
||||
bucket(storage, BRIDGE_DEPOSITS)
|
||||
}
|
||||
|
||||
pub fn bridge_deposit_read(storage: &dyn Storage) -> ReadonlyBucket<Uint128> {
|
||||
bucket_read(storage, BRIDGE_DEPOSITS)
|
||||
}
|
||||
|
||||
pub fn bridge_contracts(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
|
||||
bucket(storage, BRIDGE_CONTRACTS)
|
||||
}
|
||||
|
||||
pub fn bridge_contracts_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
|
||||
bucket_read(storage, BRIDGE_CONTRACTS)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset(storage: &mut dyn Storage) -> Bucket<HumanAddr> {
|
||||
bucket(storage, WRAPPED_ASSET_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_read(storage: &dyn Storage) -> ReadonlyBucket<HumanAddr> {
|
||||
bucket_read(storage, WRAPPED_ASSET_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_seq(storage: &mut dyn Storage) -> Bucket<u64> {
|
||||
bucket(storage, WRAPPED_ASSET_SEQ_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_seq_read(storage: &mut dyn Storage) -> ReadonlyBucket<u64> {
|
||||
bucket_read(storage, WRAPPED_ASSET_SEQ_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_address(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
|
||||
bucket(storage, WRAPPED_ASSET_ADDRESS_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_address_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
|
||||
bucket_read(storage, WRAPPED_ASSET_ADDRESS_KEY)
|
||||
}
|
||||
|
||||
type Serialized128 = String;
|
||||
|
||||
/// Structure to keep track of an active CW20 transfer, required to pass state through to the reply
|
||||
/// handler for submessages during a transfer.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct TransferState {
|
||||
pub account: String,
|
||||
pub message: Vec<u8>,
|
||||
pub multiplier: Serialized128,
|
||||
pub nonce: u32,
|
||||
pub previous_balance: Serialized128,
|
||||
pub token_address: HumanAddr,
|
||||
pub token_canonical: CanonicalAddr,
|
||||
}
|
||||
|
||||
pub fn wrapped_transfer_tmp(storage: &mut dyn Storage) -> Singleton<TransferState> {
|
||||
singleton(storage, TRANSFER_TMP_KEY)
|
||||
}
|
||||
|
||||
pub fn send_native(
|
||||
storage: &mut dyn Storage,
|
||||
asset_address: &CanonicalAddr,
|
||||
amount: Uint128,
|
||||
) -> StdResult<()> {
|
||||
let mut counter_bucket = bucket(storage, NATIVE_COUNTER);
|
||||
let new_total = amount
|
||||
+ counter_bucket
|
||||
.load(asset_address.as_slice())
|
||||
.unwrap_or(Uint128::zero());
|
||||
if new_total > Uint128::new(u64::MAX as u128) {
|
||||
return Err(StdError::generic_err(
|
||||
"transfer exceeds max outstanding bridged token amount",
|
||||
));
|
||||
}
|
||||
counter_bucket.save(asset_address.as_slice(), &new_total)
|
||||
}
|
||||
|
||||
pub fn receive_native(
|
||||
storage: &mut dyn Storage,
|
||||
asset_address: &CanonicalAddr,
|
||||
amount: Uint128,
|
||||
) -> StdResult<()> {
|
||||
let mut counter_bucket = bucket(storage, NATIVE_COUNTER);
|
||||
let total: Uint128 = counter_bucket.load(asset_address.as_slice())?;
|
||||
let result = total.checked_sub(amount)?;
|
||||
counter_bucket.save(asset_address.as_slice(), &result)
|
||||
}
|
||||
|
||||
pub struct Action;
|
||||
|
||||
impl Action {
|
||||
pub const TRANSFER: u8 = 1;
|
||||
pub const ATTEST_META: u8 = 2;
|
||||
pub const TRANSFER_WITH_PAYLOAD: u8 = 3;
|
||||
}
|
||||
|
||||
// 0 u8 action
|
||||
// 1 [u8] payload
|
||||
|
||||
pub struct TokenBridgeMessage {
|
||||
pub action: u8,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TokenBridgeMessage {
|
||||
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
|
||||
let data = data.as_slice();
|
||||
let action = data.get_u8(0);
|
||||
let payload = &data[1..];
|
||||
|
||||
Ok(TokenBridgeMessage {
|
||||
action,
|
||||
payload: payload.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
[self.action.to_be_bytes().to_vec(), self.payload.clone()].concat()
|
||||
}
|
||||
}
|
||||
|
||||
// 0 u256 amount
|
||||
// 32 [u8; 32] token_address
|
||||
// 64 u16 token_chain
|
||||
// 66 [u8; 32] recipient
|
||||
// 98 u16 recipient_chain
|
||||
// 100 u256 fee
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct TransferInfo {
|
||||
pub amount: (u128, u128),
|
||||
pub token_address: Vec<u8>,
|
||||
pub token_chain: u16,
|
||||
pub recipient: Vec<u8>,
|
||||
pub recipient_chain: u16,
|
||||
pub fee: (u128, u128),
|
||||
}
|
||||
|
||||
impl TransferInfo {
|
||||
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
|
||||
let data = data.as_slice();
|
||||
let amount = data.get_u256(0);
|
||||
let token_address = data.get_bytes32(32).to_vec();
|
||||
let token_chain = data.get_u16(64);
|
||||
let recipient = data.get_bytes32(66).to_vec();
|
||||
let recipient_chain = data.get_u16(98);
|
||||
let fee = data.get_u256(100);
|
||||
|
||||
Ok(TransferInfo {
|
||||
amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
recipient,
|
||||
recipient_chain,
|
||||
fee,
|
||||
})
|
||||
}
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
[
|
||||
self.amount.0.to_be_bytes().to_vec(),
|
||||
self.amount.1.to_be_bytes().to_vec(),
|
||||
self.token_address.clone(),
|
||||
self.token_chain.to_be_bytes().to_vec(),
|
||||
self.recipient.to_vec(),
|
||||
self.recipient_chain.to_be_bytes().to_vec(),
|
||||
self.fee.0.to_be_bytes().to_vec(),
|
||||
self.fee.1.to_be_bytes().to_vec(),
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
}
|
||||
|
||||
// 0 u256 amount
|
||||
// 32 [u8; 32] token_address
|
||||
// 64 u16 token_chain
|
||||
// 66 [u8; 32] recipient
|
||||
// 98 u16 recipient_chain
|
||||
// 100 u256 fee
|
||||
// 132 [u8] payload
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct TransferWithPayloadInfo {
|
||||
pub transfer_info: TransferInfo,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TransferWithPayloadInfo {
|
||||
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
|
||||
let transfer_info = TransferInfo::deserialize(data)?;
|
||||
let payload = TransferWithPayloadInfo::get_payload(data);
|
||||
|
||||
Ok(TransferWithPayloadInfo {
|
||||
transfer_info,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
[self.transfer_info.serialize(), self.payload.clone()].concat()
|
||||
}
|
||||
pub fn get_payload(data: &Vec<u8>) -> Vec<u8> {
|
||||
return data[132..].to_vec();
|
||||
}
|
||||
}
|
||||
|
||||
// 0 [32]uint8 TokenAddress
|
||||
// 32 uint16 TokenChain
|
||||
// 34 uint8 Decimals
|
||||
// 35 [32]uint8 Symbol
|
||||
// 67 [32]uint8 Name
|
||||
|
||||
pub struct AssetMeta {
|
||||
pub token_address: Vec<u8>,
|
||||
pub token_chain: u16,
|
||||
pub decimals: u8,
|
||||
pub symbol: Vec<u8>,
|
||||
pub name: Vec<u8>,
|
||||
}
|
||||
|
||||
impl AssetMeta {
|
||||
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
|
||||
let data = data.as_slice();
|
||||
let token_address = data.get_bytes32(0).to_vec();
|
||||
let token_chain = data.get_u16(32);
|
||||
let decimals = data.get_u8(34);
|
||||
let symbol = data.get_bytes32(35).to_vec();
|
||||
let name = data.get_bytes32(67).to_vec();
|
||||
|
||||
Ok(AssetMeta {
|
||||
token_chain,
|
||||
token_address,
|
||||
decimals,
|
||||
symbol,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
[
|
||||
self.token_address.clone(),
|
||||
self.token_chain.to_be_bytes().to_vec(),
|
||||
self.decimals.to_be_bytes().to_vec(),
|
||||
self.symbol.clone(),
|
||||
self.name.clone(),
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpgradeContract {
|
||||
pub new_contract: u64,
|
||||
}
|
||||
|
||||
pub struct RegisterChain {
|
||||
pub chain_id: u16,
|
||||
pub chain_address: Vec<u8>,
|
||||
}
|
||||
|
||||
impl UpgradeContract {
|
||||
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
|
||||
let data = data.as_slice();
|
||||
let new_contract = data.get_u64(24);
|
||||
Ok(UpgradeContract { new_contract })
|
||||
}
|
||||
}
|
||||
|
||||
impl RegisterChain {
|
||||
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
|
||||
let data = data.as_slice();
|
||||
let chain_id = data.get_u16(0);
|
||||
let chain_address = data[2..].to_vec();
|
||||
|
||||
Ok(RegisterChain {
|
||||
chain_id,
|
||||
chain_address,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
mod tests;
|
|
@ -0,0 +1,197 @@
|
|||
use cosmwasm_std::{
|
||||
Binary,
|
||||
StdResult,
|
||||
};
|
||||
|
||||
use wormhole::state::ParsedVAA;
|
||||
|
||||
use crate::{
|
||||
contract::{
|
||||
build_asset_id,
|
||||
build_native_id,
|
||||
},
|
||||
state::{
|
||||
Action,
|
||||
TokenBridgeMessage,
|
||||
TransferInfo,
|
||||
TransferWithPayloadInfo,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn binary_check() -> StdResult<()> {
|
||||
let x = vec![
|
||||
1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 96u8, 180u8, 94u8, 195u8, 0u8, 0u8, 0u8,
|
||||
1u8, 0u8, 3u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 38u8, 229u8,
|
||||
4u8, 215u8, 149u8, 163u8, 42u8, 54u8, 156u8, 236u8, 173u8, 168u8, 72u8, 220u8, 100u8, 90u8,
|
||||
154u8, 159u8, 160u8, 215u8, 0u8, 91u8, 48u8, 44u8, 48u8, 44u8, 51u8, 44u8, 48u8, 44u8,
|
||||
48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
|
||||
44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 53u8, 55u8, 44u8, 52u8, 54u8, 44u8, 50u8, 53u8,
|
||||
53u8, 44u8, 53u8, 48u8, 44u8, 50u8, 52u8, 51u8, 44u8, 49u8, 48u8, 54u8, 44u8, 49u8, 50u8,
|
||||
50u8, 44u8, 49u8, 49u8, 48u8, 44u8, 49u8, 50u8, 53u8, 44u8, 56u8, 56u8, 44u8, 55u8, 51u8,
|
||||
44u8, 49u8, 56u8, 57u8, 44u8, 50u8, 48u8, 55u8, 44u8, 49u8, 48u8, 52u8, 44u8, 56u8, 51u8,
|
||||
44u8, 49u8, 49u8, 57u8, 44u8, 49u8, 50u8, 55u8, 44u8, 49u8, 57u8, 50u8, 44u8, 49u8, 52u8,
|
||||
55u8, 44u8, 56u8, 57u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8,
|
||||
48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
|
||||
44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8,
|
||||
48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
|
||||
44u8, 48u8, 44u8, 48u8, 44u8, 51u8, 44u8, 50u8, 51u8, 50u8, 44u8, 48u8, 44u8, 51u8, 44u8,
|
||||
48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8,
|
||||
44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 53u8, 51u8, 44u8, 49u8, 49u8, 54u8,
|
||||
44u8, 52u8, 56u8, 44u8, 49u8, 49u8, 54u8, 44u8, 49u8, 52u8, 57u8, 44u8, 49u8, 48u8, 56u8,
|
||||
44u8, 49u8, 49u8, 51u8, 44u8, 56u8, 44u8, 48u8, 44u8, 50u8, 51u8, 50u8, 44u8, 52u8, 57u8,
|
||||
44u8, 49u8, 53u8, 50u8, 44u8, 49u8, 44u8, 50u8, 56u8, 44u8, 50u8, 48u8, 51u8, 44u8, 50u8,
|
||||
49u8, 50u8, 44u8, 50u8, 50u8, 49u8, 44u8, 50u8, 52u8, 49u8, 44u8, 56u8, 53u8, 44u8, 49u8,
|
||||
48u8, 57u8, 93u8,
|
||||
];
|
||||
let b = Binary::from(x.clone());
|
||||
let y: Vec<u8> = b.into();
|
||||
assert_eq!(x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_native_and_asset_ids() -> StdResult<()> {
|
||||
let denom = "uusd";
|
||||
let native_id = build_native_id(denom);
|
||||
|
||||
let expected_native_id = vec![
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 117u8,
|
||||
117u8, 115u8, 100u8,
|
||||
];
|
||||
assert_eq!(&native_id, &expected_native_id, "native_id != expected");
|
||||
|
||||
// weth
|
||||
let chain = 2u16;
|
||||
let token_address = "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
|
||||
let token_address = hex::decode(token_address).unwrap();
|
||||
let asset_id = build_asset_id(chain, token_address.as_slice());
|
||||
|
||||
let expected_asset_id = vec![
|
||||
171u8, 106u8, 233u8, 80u8, 14u8, 139u8, 124u8, 78u8, 181u8, 77u8, 142u8, 76u8, 109u8, 81u8,
|
||||
55u8, 100u8, 139u8, 159u8, 42u8, 85u8, 172u8, 234u8, 0u8, 114u8, 11u8, 82u8, 40u8, 40u8,
|
||||
50u8, 73u8, 211u8, 135u8,
|
||||
];
|
||||
assert_eq!(&asset_id, &expected_asset_id, "asset_id != expected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_transfer_vaa() -> StdResult<()> {
|
||||
let signed_vaa = "\
|
||||
010000000001003f3179d5bb17b6f2ecc13741ca3f78d922043e99e09975e390\
|
||||
4332d2418bb3f16d7ac93ca8401f8bed1cf9827bc806ecf7c5a283340f033bf4\
|
||||
72724abf1d274f00000000000000000000010000000000000000000000000000\
|
||||
00000000000000000000000000000000ffff0000000000000000000100000000\
|
||||
00000000000000000000000000000000000000000000000005f5e10001000000\
|
||||
0000000000000000000000000000000000000000000000007575736400030000\
|
||||
00000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db30003\
|
||||
00000000000000000000000000000000000000000000000000000000000f4240";
|
||||
let signed_vaa = hex::decode(signed_vaa).unwrap();
|
||||
|
||||
let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?;
|
||||
let message = TokenBridgeMessage::deserialize(&parsed.payload)?;
|
||||
assert_eq!(
|
||||
message.action,
|
||||
Action::TRANSFER,
|
||||
"message.action != expected"
|
||||
);
|
||||
|
||||
let info = TransferInfo::deserialize(&message.payload)?;
|
||||
|
||||
let amount = (0u128, 100_000_000u128);
|
||||
assert_eq!(info.amount, amount, "info.amount != expected");
|
||||
|
||||
let token_address = "0100000000000000000000000000000000000000000000000000000075757364";
|
||||
let token_address = hex::decode(token_address).unwrap();
|
||||
assert_eq!(
|
||||
info.token_address, token_address,
|
||||
"info.token_address != expected"
|
||||
);
|
||||
|
||||
let token_chain = 3u16;
|
||||
assert_eq!(
|
||||
info.token_chain, token_chain,
|
||||
"info.token_chain != expected"
|
||||
);
|
||||
|
||||
let recipient = "000000000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db3";
|
||||
let recipient = hex::decode(recipient).unwrap();
|
||||
assert_eq!(info.recipient, recipient, "info.recipient != expected");
|
||||
|
||||
let recipient_chain = 3u16;
|
||||
assert_eq!(
|
||||
info.recipient_chain, recipient_chain,
|
||||
"info.recipient_chain != expected"
|
||||
);
|
||||
|
||||
let fee = (0u128, 1_000_000u128);
|
||||
assert_eq!(info.fee, fee, "info.fee != expected");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_transfer_with_payload_vaa() -> StdResult<()> {
|
||||
let signed_vaa = "\
|
||||
010000000001002b0e392ebe370e718b91dcafbba21094efd8e7f1f12e28bd90\
|
||||
a178b4dfbbc708675152a3cd2edd20e8e018600026b73b6c6cbf02622903409e\
|
||||
8b48ab7fa30ef001000000010000000100010000000000000000000000000000\
|
||||
00000000000000000000000000000000ffff0000000000000002000300000000\
|
||||
00000000000000000000000000000000000000000000000005f5e10001000000\
|
||||
0000000000000000000000000000000000000000000000007575736400030000\
|
||||
000000000000000000008cec800d24df11e556e708461c98122df4a2c3b10003\
|
||||
00000000000000000000000000000000000000000000000000000000000f4240\
|
||||
416c6c20796f75722062617365206172652062656c6f6e6720746f207573";
|
||||
let signed_vaa = hex::decode(signed_vaa).unwrap();
|
||||
|
||||
let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?;
|
||||
let message = TokenBridgeMessage::deserialize(&parsed.payload)?;
|
||||
assert_eq!(
|
||||
message.action,
|
||||
Action::TRANSFER_WITH_PAYLOAD,
|
||||
"message.action != expected"
|
||||
);
|
||||
|
||||
let info_with_payload = TransferWithPayloadInfo::deserialize(&message.payload)?;
|
||||
let info = info_with_payload.transfer_info;
|
||||
|
||||
let amount = (0u128, 100_000_000u128);
|
||||
assert_eq!(info.amount, amount, "info.amount != expected");
|
||||
|
||||
let token_address = "0100000000000000000000000000000000000000000000000000000075757364";
|
||||
let token_address = hex::decode(token_address).unwrap();
|
||||
assert_eq!(
|
||||
info.token_address, token_address,
|
||||
"info.token_address != expected"
|
||||
);
|
||||
|
||||
let token_chain = 3u16;
|
||||
assert_eq!(
|
||||
info.token_chain, token_chain,
|
||||
"info.token_chain != expected"
|
||||
);
|
||||
|
||||
let recipient = "0000000000000000000000008cec800d24df11e556e708461c98122df4a2c3b1";
|
||||
let recipient = hex::decode(recipient).unwrap();
|
||||
assert_eq!(info.recipient, recipient, "info.recipient != expected");
|
||||
|
||||
let recipient_chain = 3u16;
|
||||
assert_eq!(
|
||||
info.recipient_chain, recipient_chain,
|
||||
"info.recipient_chain != expected"
|
||||
);
|
||||
|
||||
let fee = (0u128, 1_000_000u128);
|
||||
assert_eq!(info.fee, fee, "info.fee != expected");
|
||||
|
||||
let transfer_payload = "All your base are belong to us";
|
||||
let transfer_payload = transfer_payload.as_bytes();
|
||||
assert_eq!(
|
||||
info_with_payload.payload.as_slice(),
|
||||
transfer_payload,
|
||||
"info.payload != expected"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
[alias]
|
||||
wasm = "build --release --target wasm32-unknown-unknown"
|
||||
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib --features backtraces"
|
||||
integration-test = "test --test integration"
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "wormhole-bridge-terra"
|
||||
version = "0.1.0"
|
||||
authors = ["Yuriy Savchenko <yuriy.savchenko@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "Wormhole contract"
|
||||
|
||||
[lib]
|
||||
name = "wormhole"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
# use library feature to disable all init/handle/query exports
|
||||
library = []
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { version = "0.16.0" }
|
||||
cosmwasm-storage = { version = "0.16.0" }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
cw20 = "0.8.0"
|
||||
cw20-base = { version = "0.8.0", features = ["library"] }
|
||||
cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] }
|
||||
thiserror = { version = "1.0.20" }
|
||||
k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] }
|
||||
getrandom = { version = "0.2", features = ["custom"] }
|
||||
sha3 = { version = "0.9.1", default-features = false }
|
||||
generic-array = { version = "0.14.4" }
|
||||
hex = "0.4.2"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
|
@ -0,0 +1,97 @@
|
|||
use cosmwasm_std::CanonicalAddr;
|
||||
|
||||
pub trait ByteUtils {
|
||||
fn get_u8(&self, index: usize) -> u8;
|
||||
fn get_u16(&self, index: usize) -> u16;
|
||||
fn get_u32(&self, index: usize) -> u32;
|
||||
fn get_u64(&self, index: usize) -> u64;
|
||||
|
||||
fn get_u128_be(&self, index: usize) -> u128;
|
||||
/// High 128 then low 128
|
||||
fn get_u256(&self, index: usize) -> (u128, u128);
|
||||
fn get_address(&self, index: usize) -> CanonicalAddr;
|
||||
fn get_bytes32(&self, index: usize) -> &[u8];
|
||||
fn get_bytes(&self, index: usize, bytes: usize) -> &[u8];
|
||||
fn get_const_bytes<const N: usize>(&self, index: usize) -> [u8; N];
|
||||
}
|
||||
|
||||
impl ByteUtils for &[u8] {
|
||||
fn get_u8(&self, index: usize) -> u8 {
|
||||
self[index]
|
||||
}
|
||||
fn get_u16(&self, index: usize) -> u16 {
|
||||
let mut bytes: [u8; 16 / 8] = [0; 16 / 8];
|
||||
bytes.copy_from_slice(&self[index..index + 2]);
|
||||
u16::from_be_bytes(bytes)
|
||||
}
|
||||
fn get_u32(&self, index: usize) -> u32 {
|
||||
let mut bytes: [u8; 32 / 8] = [0; 32 / 8];
|
||||
bytes.copy_from_slice(&self[index..index + 4]);
|
||||
u32::from_be_bytes(bytes)
|
||||
}
|
||||
fn get_u64(&self, index: usize) -> u64 {
|
||||
let mut bytes: [u8; 64 / 8] = [0; 64 / 8];
|
||||
bytes.copy_from_slice(&self[index..index + 8]);
|
||||
u64::from_be_bytes(bytes)
|
||||
}
|
||||
fn get_u128_be(&self, index: usize) -> u128 {
|
||||
let mut bytes: [u8; 128 / 8] = [0; 128 / 8];
|
||||
bytes.copy_from_slice(&self[index..index + 128 / 8]);
|
||||
u128::from_be_bytes(bytes)
|
||||
}
|
||||
fn get_u256(&self, index: usize) -> (u128, u128) {
|
||||
(self.get_u128_be(index), self.get_u128_be(index + 128 / 8))
|
||||
}
|
||||
fn get_address(&self, index: usize) -> CanonicalAddr {
|
||||
// 32 bytes are reserved for addresses, but only the last 20 bytes are taken by the actual address
|
||||
CanonicalAddr::from(&self[index + 32 - 20..index + 32])
|
||||
}
|
||||
fn get_bytes32(&self, index: usize) -> &[u8] {
|
||||
&self[index..index + 32]
|
||||
}
|
||||
|
||||
fn get_bytes(&self, index: usize, bytes: usize) -> &[u8] {
|
||||
&self[index..index + bytes]
|
||||
}
|
||||
|
||||
fn get_const_bytes<const N: usize>(&self, index: usize) -> [u8; N] {
|
||||
let mut bytes: [u8; N] = [0; N];
|
||||
bytes.copy_from_slice(&self[index..index + N]);
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// Left-pad a 20 byte address with 0s
|
||||
pub fn extend_address_to_32(addr: &CanonicalAddr) -> Vec<u8> {
|
||||
extend_address_to_32_array(addr).to_vec()
|
||||
}
|
||||
|
||||
pub fn extend_address_to_32_array(addr: &CanonicalAddr) -> [u8; 32] {
|
||||
let mut v: Vec<u8> = vec![0; 12];
|
||||
v.extend(addr.as_slice());
|
||||
let mut result: [u8; 32] = [0; 32];
|
||||
result.copy_from_slice(&v);
|
||||
result
|
||||
}
|
||||
|
||||
/// Turn a string into a fixed length array. If the string is shorter than the
|
||||
/// resulting array, it gets padded with \0s on the right. If longer, it gets
|
||||
/// truncated.
|
||||
pub fn string_to_array<const N: usize>(s: &str) -> [u8; N] {
|
||||
let bytes = s.as_bytes();
|
||||
let len = usize::min(N, bytes.len());
|
||||
let zeros = vec![0; N - len];
|
||||
let padded = [bytes[..len].to_vec(), zeros].concat();
|
||||
let mut result: [u8; N] = [0; N];
|
||||
result.copy_from_slice(&padded);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn extend_string_to_32(s: &str) -> Vec<u8> {
|
||||
string_to_array::<32>(s).to_vec()
|
||||
}
|
||||
|
||||
pub fn get_string_from_32(v: &[u8]) -> String {
|
||||
let s = String::from_utf8_lossy(v);
|
||||
s.chars().filter(|c| c != &'\0').collect()
|
||||
}
|
|
@ -0,0 +1,418 @@
|
|||
use cosmwasm_std::{
|
||||
has_coins,
|
||||
to_binary,
|
||||
BankMsg,
|
||||
Binary,
|
||||
Coin,
|
||||
CosmosMsg,
|
||||
Deps,
|
||||
DepsMut,
|
||||
Env,
|
||||
MessageInfo,
|
||||
Response,
|
||||
StdError,
|
||||
StdResult,
|
||||
Storage,
|
||||
WasmMsg,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "library"))]
|
||||
use cosmwasm_std::entry_point;
|
||||
|
||||
use crate::{
|
||||
byte_utils::{
|
||||
extend_address_to_32,
|
||||
ByteUtils,
|
||||
},
|
||||
error::ContractError,
|
||||
msg::{
|
||||
ExecuteMsg,
|
||||
GetAddressHexResponse,
|
||||
GetStateResponse,
|
||||
GuardianSetInfoResponse,
|
||||
InstantiateMsg,
|
||||
MigrateMsg,
|
||||
QueryMsg,
|
||||
},
|
||||
state::{
|
||||
config,
|
||||
config_read,
|
||||
guardian_set_get,
|
||||
guardian_set_set,
|
||||
sequence_read,
|
||||
sequence_set,
|
||||
vaa_archive_add,
|
||||
vaa_archive_check,
|
||||
ConfigInfo,
|
||||
ContractUpgrade,
|
||||
GovernancePacket,
|
||||
GuardianAddress,
|
||||
GuardianSetInfo,
|
||||
GuardianSetUpgrade,
|
||||
ParsedVAA,
|
||||
SetFee,
|
||||
TransferFee,
|
||||
},
|
||||
};
|
||||
|
||||
use k256::{
|
||||
ecdsa::{
|
||||
recoverable::{
|
||||
Id as RecoverableId,
|
||||
Signature as RecoverableSignature,
|
||||
},
|
||||
Signature,
|
||||
VerifyingKey,
|
||||
},
|
||||
EncodedPoint,
|
||||
};
|
||||
use sha3::{
|
||||
Digest,
|
||||
Keccak256,
|
||||
};
|
||||
|
||||
use generic_array::GenericArray;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
// Chain ID of Terra
|
||||
const CHAIN_ID: u16 = 3;
|
||||
|
||||
// Lock assets fee amount and denomination
|
||||
const FEE_AMOUNT: u128 = 0;
|
||||
pub const FEE_DENOMINATION: &str = "uluna";
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
_info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> StdResult<Response> {
|
||||
// Save general wormhole info
|
||||
let state = ConfigInfo {
|
||||
gov_chain: msg.gov_chain,
|
||||
gov_address: msg.gov_address.as_slice().to_vec(),
|
||||
guardian_set_index: 0,
|
||||
guardian_set_expirity: msg.guardian_set_expirity,
|
||||
fee: Coin::new(FEE_AMOUNT, FEE_DENOMINATION), // 0.01 Luna (or 10000 uluna) fee by default
|
||||
};
|
||||
config(deps.storage).save(&state)?;
|
||||
|
||||
// Add initial guardian set to storage
|
||||
guardian_set_set(
|
||||
deps.storage,
|
||||
state.guardian_set_index,
|
||||
&msg.initial_guardian_set,
|
||||
)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
|
||||
match msg {
|
||||
ExecuteMsg::PostMessage { message, nonce } => {
|
||||
handle_post_message(deps, env, info, message.as_slice(), nonce)
|
||||
}
|
||||
ExecuteMsg::SubmitVAA { vaa } => handle_submit_vaa(deps, env, info, vaa.as_slice()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process VAA message signed by quardians
|
||||
fn handle_submit_vaa(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
_info: MessageInfo,
|
||||
data: &[u8],
|
||||
) -> StdResult<Response> {
|
||||
let state = config_read(deps.storage).load()?;
|
||||
|
||||
let vaa = parse_and_verify_vaa(deps.storage, data, env.block.time.seconds())?;
|
||||
vaa_archive_add(deps.storage, vaa.hash.as_slice())?;
|
||||
|
||||
if state.gov_chain == vaa.emitter_chain && state.gov_address == vaa.emitter_address {
|
||||
if state.guardian_set_index != vaa.guardian_set_index {
|
||||
return Err(StdError::generic_err(
|
||||
"governance VAAs must be signed by the current guardian set",
|
||||
));
|
||||
}
|
||||
return handle_governance_payload(deps, env, &vaa.payload);
|
||||
}
|
||||
|
||||
ContractError::InvalidVAAAction.std_err()
|
||||
}
|
||||
|
||||
fn handle_governance_payload(deps: DepsMut, env: Env, data: &[u8]) -> StdResult<Response> {
|
||||
let gov_packet = GovernancePacket::deserialize(data)?;
|
||||
|
||||
let module = String::from_utf8(gov_packet.module).unwrap();
|
||||
let module: String = module.chars().filter(|c| c != &'\0').collect();
|
||||
|
||||
if module != "Core" {
|
||||
return Err(StdError::generic_err("this is not a valid module"));
|
||||
}
|
||||
|
||||
if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID {
|
||||
return Err(StdError::generic_err(
|
||||
"the governance VAA is for another chain",
|
||||
));
|
||||
}
|
||||
|
||||
match gov_packet.action {
|
||||
1u8 => vaa_update_contract(deps, env, &gov_packet.payload),
|
||||
2u8 => vaa_update_guardian_set(deps, env, &gov_packet.payload),
|
||||
3u8 => handle_set_fee(deps, env, &gov_packet.payload),
|
||||
4u8 => handle_transfer_fee(deps, env, &gov_packet.payload),
|
||||
_ => ContractError::InvalidVAAAction.std_err(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses raw VAA data into a struct and verifies whether it contains sufficient signatures of an
|
||||
/// active guardian set i.e. is valid according to Wormhole consensus rules
|
||||
fn parse_and_verify_vaa(
|
||||
storage: &dyn Storage,
|
||||
data: &[u8],
|
||||
block_time: u64,
|
||||
) -> StdResult<ParsedVAA> {
|
||||
let vaa = ParsedVAA::deserialize(data)?;
|
||||
|
||||
if vaa.version != 1 {
|
||||
return ContractError::InvalidVersion.std_err();
|
||||
}
|
||||
|
||||
// Check if VAA with this hash was already accepted
|
||||
if vaa_archive_check(storage, vaa.hash.as_slice()) {
|
||||
return ContractError::VaaAlreadyExecuted.std_err();
|
||||
}
|
||||
|
||||
// Load and check guardian set
|
||||
let guardian_set = guardian_set_get(storage, vaa.guardian_set_index);
|
||||
let guardian_set: GuardianSetInfo =
|
||||
guardian_set.or_else(|_| ContractError::InvalidGuardianSetIndex.std_err())?;
|
||||
|
||||
if guardian_set.expiration_time != 0 && guardian_set.expiration_time < block_time {
|
||||
return ContractError::GuardianSetExpired.std_err();
|
||||
}
|
||||
if (vaa.len_signers as usize) < guardian_set.quorum() {
|
||||
return ContractError::NoQuorum.std_err();
|
||||
}
|
||||
|
||||
// Verify guardian signatures
|
||||
let mut last_index: i32 = -1;
|
||||
let mut pos = ParsedVAA::HEADER_LEN;
|
||||
|
||||
for _ in 0..vaa.len_signers {
|
||||
if pos + ParsedVAA::SIGNATURE_LEN > data.len() {
|
||||
return ContractError::InvalidVAA.std_err();
|
||||
}
|
||||
let index = data.get_u8(pos) as i32;
|
||||
if index <= last_index {
|
||||
return ContractError::WrongGuardianIndexOrder.std_err();
|
||||
}
|
||||
last_index = index;
|
||||
|
||||
let signature = Signature::try_from(
|
||||
&data[pos + ParsedVAA::SIG_DATA_POS
|
||||
..pos + ParsedVAA::SIG_DATA_POS + ParsedVAA::SIG_DATA_LEN],
|
||||
)
|
||||
.or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
|
||||
let id = RecoverableId::new(data.get_u8(pos + ParsedVAA::SIG_RECOVERY_POS))
|
||||
.or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
|
||||
let recoverable_signature = RecoverableSignature::new(&signature, id)
|
||||
.or_else(|_| ContractError::CannotDecodeSignature.std_err())?;
|
||||
|
||||
let verify_key = recoverable_signature
|
||||
.recover_verify_key_from_digest_bytes(GenericArray::from_slice(vaa.hash.as_slice()))
|
||||
.or_else(|_| ContractError::CannotRecoverKey.std_err())?;
|
||||
|
||||
let index = index as usize;
|
||||
if index >= guardian_set.addresses.len() {
|
||||
return ContractError::TooManySignatures.std_err();
|
||||
}
|
||||
if !keys_equal(&verify_key, &guardian_set.addresses[index]) {
|
||||
return ContractError::GuardianSignatureError.std_err();
|
||||
}
|
||||
pos += ParsedVAA::SIGNATURE_LEN;
|
||||
}
|
||||
|
||||
Ok(vaa)
|
||||
}
|
||||
|
||||
fn vaa_update_guardian_set(deps: DepsMut, env: Env, data: &[u8]) -> StdResult<Response> {
|
||||
/* Payload format
|
||||
0 uint32 new_index
|
||||
4 uint8 len(keys)
|
||||
5 [][20]uint8 guardian addresses
|
||||
*/
|
||||
|
||||
let mut state = config_read(deps.storage).load()?;
|
||||
|
||||
let GuardianSetUpgrade {
|
||||
new_guardian_set_index,
|
||||
new_guardian_set,
|
||||
} = GuardianSetUpgrade::deserialize(data)?;
|
||||
|
||||
if new_guardian_set_index != state.guardian_set_index + 1 {
|
||||
return ContractError::GuardianSetIndexIncreaseError.std_err();
|
||||
}
|
||||
|
||||
let old_guardian_set_index = state.guardian_set_index;
|
||||
|
||||
state.guardian_set_index = new_guardian_set_index;
|
||||
|
||||
guardian_set_set(deps.storage, state.guardian_set_index, &new_guardian_set)?;
|
||||
|
||||
config(deps.storage).save(&state)?;
|
||||
|
||||
let mut old_guardian_set = guardian_set_get(deps.storage, old_guardian_set_index)?;
|
||||
old_guardian_set.expiration_time = env.block.time.seconds() + state.guardian_set_expirity;
|
||||
guardian_set_set(deps.storage, old_guardian_set_index, &old_guardian_set)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "guardian_set_change")
|
||||
.add_attribute("old", old_guardian_set_index.to_string())
|
||||
.add_attribute("new", state.guardian_set_index.to_string()))
|
||||
}
|
||||
|
||||
fn vaa_update_contract(_deps: DepsMut, env: Env, data: &[u8]) -> StdResult<Response> {
|
||||
/* Payload format
|
||||
0 [][32]uint8 new_contract
|
||||
*/
|
||||
|
||||
let ContractUpgrade { new_contract } = ContractUpgrade::deserialize(data)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_message(CosmosMsg::Wasm(WasmMsg::Migrate {
|
||||
contract_addr: env.contract.address.to_string(),
|
||||
new_code_id: new_contract,
|
||||
msg: to_binary(&MigrateMsg {})?,
|
||||
}))
|
||||
.add_attribute("action", "contract_upgrade"))
|
||||
}
|
||||
|
||||
pub fn handle_set_fee(deps: DepsMut, _env: Env, data: &[u8]) -> StdResult<Response> {
|
||||
let set_fee_msg = SetFee::deserialize(data)?;
|
||||
|
||||
// Save new fees
|
||||
let mut state = config_read(deps.storage).load()?;
|
||||
state.fee = set_fee_msg.fee;
|
||||
config(deps.storage).save(&state)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "fee_change")
|
||||
.add_attribute("new_fee.amount", state.fee.amount)
|
||||
.add_attribute("new_fee.denom", state.fee.denom))
|
||||
}
|
||||
|
||||
pub fn handle_transfer_fee(deps: DepsMut, _env: Env, data: &[u8]) -> StdResult<Response> {
|
||||
let transfer_msg = TransferFee::deserialize(data)?;
|
||||
|
||||
Ok(Response::new().add_message(CosmosMsg::Bank(BankMsg::Send {
|
||||
to_address: deps.api.addr_humanize(&transfer_msg.recipient)?.to_string(),
|
||||
amount: vec![transfer_msg.amount],
|
||||
})))
|
||||
}
|
||||
|
||||
fn handle_post_message(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
message: &[u8],
|
||||
nonce: u32,
|
||||
) -> StdResult<Response> {
|
||||
let state = config_read(deps.storage).load()?;
|
||||
let fee = state.fee;
|
||||
|
||||
// Check fee
|
||||
if fee.amount.u128() > 0 && !has_coins(info.funds.as_ref(), &fee) {
|
||||
return ContractError::FeeTooLow.std_err();
|
||||
}
|
||||
|
||||
let emitter = extend_address_to_32(&deps.api.addr_canonicalize(info.sender.as_str())?);
|
||||
let sequence = sequence_read(deps.storage, emitter.as_slice());
|
||||
sequence_set(deps.storage, emitter.as_slice(), sequence + 1)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("message.message", hex::encode(message))
|
||||
.add_attribute("message.sender", hex::encode(emitter))
|
||||
.add_attribute("message.chain_id", CHAIN_ID.to_string())
|
||||
.add_attribute("message.nonce", nonce.to_string())
|
||||
.add_attribute("message.sequence", sequence.to_string())
|
||||
.add_attribute("message.block_time", env.block.time.seconds().to_string()))
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "library"), entry_point)]
|
||||
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
|
||||
match msg {
|
||||
QueryMsg::GuardianSetInfo {} => to_binary(&query_guardian_set_info(deps)?),
|
||||
QueryMsg::VerifyVAA { vaa, block_time } => to_binary(&query_parse_and_verify_vaa(
|
||||
deps,
|
||||
vaa.as_slice(),
|
||||
block_time,
|
||||
)?),
|
||||
QueryMsg::GetState {} => to_binary(&query_state(deps)?),
|
||||
QueryMsg::QueryAddressHex { address } => to_binary(&query_address_hex(deps, &address)?),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_guardian_set_info(deps: Deps) -> StdResult<GuardianSetInfoResponse> {
|
||||
let state = config_read(deps.storage).load()?;
|
||||
let guardian_set = guardian_set_get(deps.storage, state.guardian_set_index)?;
|
||||
let res = GuardianSetInfoResponse {
|
||||
guardian_set_index: state.guardian_set_index,
|
||||
addresses: guardian_set.addresses,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn query_parse_and_verify_vaa(
|
||||
deps: Deps,
|
||||
data: &[u8],
|
||||
block_time: u64,
|
||||
) -> StdResult<ParsedVAA> {
|
||||
parse_and_verify_vaa(deps.storage, data, block_time)
|
||||
}
|
||||
|
||||
// returns the hex of the 32 byte address we use for some address on this chain
|
||||
pub fn query_address_hex(deps: Deps, address: &HumanAddr) -> StdResult<GetAddressHexResponse> {
|
||||
Ok(GetAddressHexResponse {
|
||||
hex: hex::encode(extend_address_to_32(&deps.api.addr_canonicalize(address)?)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_state(deps: Deps) -> StdResult<GetStateResponse> {
|
||||
let state = config_read(deps.storage).load()?;
|
||||
let res = GetStateResponse { fee: state.fee };
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn keys_equal(a: &VerifyingKey, b: &GuardianAddress) -> bool {
|
||||
let mut hasher = Keccak256::new();
|
||||
|
||||
let point = if let Some(p) = EncodedPoint::from(a).decompress() {
|
||||
p
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
hasher.update(&point.as_bytes()[1..]);
|
||||
let a = &hasher.finalize()[12..];
|
||||
|
||||
let b = &b.bytes;
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
for (ai, bi) in a.iter().zip(b.as_slice().iter()) {
|
||||
if ai != bi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
use cosmwasm_std::StdError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ContractError {
|
||||
/// Invalid VAA version
|
||||
#[error("InvalidVersion")]
|
||||
InvalidVersion,
|
||||
|
||||
/// Guardian set with this index does not exist
|
||||
#[error("InvalidGuardianSetIndex")]
|
||||
InvalidGuardianSetIndex,
|
||||
|
||||
/// Guardian set expiration date is zero or in the past
|
||||
#[error("GuardianSetExpired")]
|
||||
GuardianSetExpired,
|
||||
|
||||
/// Not enough signers on the VAA
|
||||
#[error("NoQuorum")]
|
||||
NoQuorum,
|
||||
|
||||
/// Wrong guardian index order, order must be ascending
|
||||
#[error("WrongGuardianIndexOrder")]
|
||||
WrongGuardianIndexOrder,
|
||||
|
||||
/// Some problem with signature decoding from bytes
|
||||
#[error("CannotDecodeSignature")]
|
||||
CannotDecodeSignature,
|
||||
|
||||
/// Some problem with public key recovery from the signature
|
||||
#[error("CannotRecoverKey")]
|
||||
CannotRecoverKey,
|
||||
|
||||
/// Recovered pubkey from signature does not match guardian address
|
||||
#[error("GuardianSignatureError")]
|
||||
GuardianSignatureError,
|
||||
|
||||
/// VAA action code not recognized
|
||||
#[error("InvalidVAAAction")]
|
||||
InvalidVAAAction,
|
||||
|
||||
/// VAA guardian set is not current
|
||||
#[error("NotCurrentGuardianSet")]
|
||||
NotCurrentGuardianSet,
|
||||
|
||||
/// Only 128-bit amounts are supported
|
||||
#[error("AmountTooHigh")]
|
||||
AmountTooHigh,
|
||||
|
||||
/// Amount should be higher than zero
|
||||
#[error("AmountTooLow")]
|
||||
AmountTooLow,
|
||||
|
||||
/// Source and target chain ids must be different
|
||||
#[error("SameSourceAndTarget")]
|
||||
SameSourceAndTarget,
|
||||
|
||||
/// Target chain id must be the same as the current CHAIN_ID
|
||||
#[error("WrongTargetChain")]
|
||||
WrongTargetChain,
|
||||
|
||||
/// Wrapped asset init hook sent twice for the same asset id
|
||||
#[error("AssetAlreadyRegistered")]
|
||||
AssetAlreadyRegistered,
|
||||
|
||||
/// Guardian set must increase in steps of 1
|
||||
#[error("GuardianSetIndexIncreaseError")]
|
||||
GuardianSetIndexIncreaseError,
|
||||
|
||||
/// VAA was already executed
|
||||
#[error("VaaAlreadyExecuted")]
|
||||
VaaAlreadyExecuted,
|
||||
|
||||
/// Message sender not permitted to execute this operation
|
||||
#[error("PermissionDenied")]
|
||||
PermissionDenied,
|
||||
|
||||
/// Could not decode target address from canonical to human-readable form
|
||||
#[error("WrongTargetAddressFormat")]
|
||||
WrongTargetAddressFormat,
|
||||
|
||||
/// More signatures than active guardians found
|
||||
#[error("TooManySignatures")]
|
||||
TooManySignatures,
|
||||
|
||||
/// Wrapped asset not found in the registry
|
||||
#[error("AssetNotFound")]
|
||||
AssetNotFound,
|
||||
|
||||
/// Generic error when there is a problem with VAA structure
|
||||
#[error("InvalidVAA")]
|
||||
InvalidVAA,
|
||||
|
||||
/// Thrown when fee is enabled for the action, but was not sent with the transaction
|
||||
#[error("FeeTooLow")]
|
||||
FeeTooLow,
|
||||
|
||||
/// Registering asset outside of the wormhole
|
||||
#[error("RegistrationForbidden")]
|
||||
RegistrationForbidden,
|
||||
}
|
||||
|
||||
impl ContractError {
|
||||
pub fn std(&self) -> StdError {
|
||||
StdError::GenericErr {
|
||||
msg: format!("{}", self),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn std_err<T>(&self) -> Result<T, StdError> {
|
||||
Err(self.std())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
pub mod byte_utils;
|
||||
pub mod contract;
|
||||
pub mod error;
|
||||
pub mod msg;
|
||||
pub mod state;
|
||||
|
||||
pub use crate::error::ContractError;
|
||||
|
||||
#[cfg(test)]
|
||||
mod testing;
|
|
@ -0,0 +1,71 @@
|
|||
use cosmwasm_std::{
|
||||
Binary,
|
||||
Coin,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use crate::state::{
|
||||
GuardianAddress,
|
||||
GuardianSetInfo,
|
||||
};
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InstantiateMsg {
|
||||
pub gov_chain: u16,
|
||||
pub gov_address: Binary,
|
||||
|
||||
pub initial_guardian_set: GuardianSetInfo,
|
||||
pub guardian_set_expirity: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
SubmitVAA { vaa: Binary },
|
||||
PostMessage { message: Binary, nonce: u32 },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
GuardianSetInfo {},
|
||||
VerifyVAA { vaa: Binary, block_time: u64 },
|
||||
GetState {},
|
||||
QueryAddressHex { address: HumanAddr },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GuardianSetInfoResponse {
|
||||
pub guardian_set_index: u32, // Current guardian set index
|
||||
pub addresses: Vec<GuardianAddress>, // List of querdian addresses
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct WrappedRegistryResponse {
|
||||
pub address: HumanAddr,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GetStateResponse {
|
||||
pub fee: Coin,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GetAddressHexResponse {
|
||||
pub hex: String,
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
use schemars::{
|
||||
JsonSchema,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use cosmwasm_std::{
|
||||
Binary,
|
||||
CanonicalAddr,
|
||||
Coin,
|
||||
StdResult,
|
||||
Storage,
|
||||
Uint128,
|
||||
};
|
||||
use cosmwasm_storage::{
|
||||
bucket,
|
||||
bucket_read,
|
||||
singleton,
|
||||
singleton_read,
|
||||
Bucket,
|
||||
ReadonlyBucket,
|
||||
ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
byte_utils::ByteUtils,
|
||||
error::ContractError,
|
||||
};
|
||||
|
||||
use sha3::{
|
||||
Digest,
|
||||
Keccak256,
|
||||
};
|
||||
|
||||
type HumanAddr = String;
|
||||
|
||||
pub static CONFIG_KEY: &[u8] = b"config";
|
||||
pub static GUARDIAN_SET_KEY: &[u8] = b"guardian_set";
|
||||
pub static SEQUENCE_KEY: &[u8] = b"sequence";
|
||||
pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset";
|
||||
pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address";
|
||||
|
||||
// Guardian set information
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct ConfigInfo {
|
||||
// Current active guardian set
|
||||
pub guardian_set_index: u32,
|
||||
|
||||
// Period for which a guardian set stays active after it has been replaced
|
||||
pub guardian_set_expirity: u64,
|
||||
|
||||
// governance contract details
|
||||
pub gov_chain: u16,
|
||||
pub gov_address: Vec<u8>,
|
||||
|
||||
// Message sending fee
|
||||
pub fee: Coin,
|
||||
}
|
||||
|
||||
// Validator Action Approval(VAA) data
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct ParsedVAA {
|
||||
pub version: u8,
|
||||
pub guardian_set_index: u32,
|
||||
pub timestamp: u32,
|
||||
pub nonce: u32,
|
||||
pub len_signers: u8,
|
||||
|
||||
pub emitter_chain: u16,
|
||||
pub emitter_address: Vec<u8>,
|
||||
pub sequence: u64,
|
||||
pub consistency_level: u8,
|
||||
pub payload: Vec<u8>,
|
||||
|
||||
pub hash: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ParsedVAA {
|
||||
/* VAA format:
|
||||
|
||||
header (length 6):
|
||||
0 uint8 version (0x01)
|
||||
1 uint32 guardian set index
|
||||
5 uint8 len signatures
|
||||
|
||||
per signature (length 66):
|
||||
0 uint8 index of the signer (in guardian keys)
|
||||
1 [65]uint8 signature
|
||||
|
||||
body:
|
||||
0 uint32 timestamp (unix in seconds)
|
||||
4 uint32 nonce
|
||||
8 uint16 emitter_chain
|
||||
10 [32]uint8 emitter_address
|
||||
42 uint64 sequence
|
||||
50 uint8 consistency_level
|
||||
51 []uint8 payload
|
||||
*/
|
||||
|
||||
pub const HEADER_LEN: usize = 6;
|
||||
pub const SIGNATURE_LEN: usize = 66;
|
||||
|
||||
pub const GUARDIAN_SET_INDEX_POS: usize = 1;
|
||||
pub const LEN_SIGNER_POS: usize = 5;
|
||||
|
||||
pub const VAA_NONCE_POS: usize = 4;
|
||||
pub const VAA_EMITTER_CHAIN_POS: usize = 8;
|
||||
pub const VAA_EMITTER_ADDRESS_POS: usize = 10;
|
||||
pub const VAA_SEQUENCE_POS: usize = 42;
|
||||
pub const VAA_CONSISTENCY_LEVEL_POS: usize = 50;
|
||||
pub const VAA_PAYLOAD_POS: usize = 51;
|
||||
|
||||
// Signature data offsets in the signature block
|
||||
pub const SIG_DATA_POS: usize = 1;
|
||||
// Signature length minus recovery id at the end
|
||||
pub const SIG_DATA_LEN: usize = 64;
|
||||
// Recovery byte is last after the main signature
|
||||
pub const SIG_RECOVERY_POS: usize = Self::SIG_DATA_POS + Self::SIG_DATA_LEN;
|
||||
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let version = data.get_u8(0);
|
||||
|
||||
// Load 4 bytes starting from index 1
|
||||
let guardian_set_index: u32 = data.get_u32(Self::GUARDIAN_SET_INDEX_POS);
|
||||
let len_signers = data.get_u8(Self::LEN_SIGNER_POS) as usize;
|
||||
let body_offset: usize = Self::HEADER_LEN + Self::SIGNATURE_LEN * len_signers as usize;
|
||||
|
||||
// Hash the body
|
||||
if body_offset >= data.len() {
|
||||
return ContractError::InvalidVAA.std_err();
|
||||
}
|
||||
let body = &data[body_offset..];
|
||||
let mut hasher = Keccak256::new();
|
||||
hasher.update(body);
|
||||
let hash = hasher.finalize().to_vec();
|
||||
|
||||
// Rehash the hash
|
||||
let mut hasher = Keccak256::new();
|
||||
hasher.update(hash);
|
||||
let hash = hasher.finalize().to_vec();
|
||||
|
||||
// Signatures valid, apply VAA
|
||||
if body_offset + Self::VAA_PAYLOAD_POS > data.len() {
|
||||
return ContractError::InvalidVAA.std_err();
|
||||
}
|
||||
|
||||
let timestamp = data.get_u32(body_offset);
|
||||
let nonce = data.get_u32(body_offset + Self::VAA_NONCE_POS);
|
||||
let emitter_chain = data.get_u16(body_offset + Self::VAA_EMITTER_CHAIN_POS);
|
||||
let emitter_address = data
|
||||
.get_bytes32(body_offset + Self::VAA_EMITTER_ADDRESS_POS)
|
||||
.to_vec();
|
||||
let sequence = data.get_u64(body_offset + Self::VAA_SEQUENCE_POS);
|
||||
let consistency_level = data.get_u8(body_offset + Self::VAA_CONSISTENCY_LEVEL_POS);
|
||||
let payload = data[body_offset + Self::VAA_PAYLOAD_POS..].to_vec();
|
||||
|
||||
Ok(ParsedVAA {
|
||||
version,
|
||||
guardian_set_index,
|
||||
timestamp,
|
||||
nonce,
|
||||
len_signers: len_signers as u8,
|
||||
emitter_chain,
|
||||
emitter_address,
|
||||
sequence,
|
||||
consistency_level,
|
||||
payload,
|
||||
hash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Guardian address
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct GuardianAddress {
|
||||
pub bytes: Binary, // 20-byte addresses
|
||||
}
|
||||
|
||||
use crate::contract::FEE_DENOMINATION;
|
||||
#[cfg(test)]
|
||||
use hex;
|
||||
|
||||
#[cfg(test)]
|
||||
impl GuardianAddress {
|
||||
pub fn from(string: &str) -> GuardianAddress {
|
||||
GuardianAddress {
|
||||
bytes: hex::decode(string).expect("Decoding failed").into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guardian set information
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct GuardianSetInfo {
|
||||
pub addresses: Vec<GuardianAddress>,
|
||||
// List of guardian addresses
|
||||
pub expiration_time: u64, // Guardian set expiration time
|
||||
}
|
||||
|
||||
impl GuardianSetInfo {
|
||||
pub fn quorum(&self) -> usize {
|
||||
// allow quorum of 0 for testing purposes...
|
||||
if self.addresses.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
((self.addresses.len() * 10 / 3) * 2) / 10 + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Wormhole contract generic information
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct WormholeInfo {
|
||||
// Period for which a guardian set stays active after it has been replaced
|
||||
pub guardian_set_expirity: u64,
|
||||
}
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<ConfigInfo> {
|
||||
singleton(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<ConfigInfo> {
|
||||
singleton_read(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn guardian_set_set(
|
||||
storage: &mut dyn Storage,
|
||||
index: u32,
|
||||
data: &GuardianSetInfo,
|
||||
) -> StdResult<()> {
|
||||
bucket(storage, GUARDIAN_SET_KEY).save(&index.to_be_bytes(), data)
|
||||
}
|
||||
|
||||
pub fn guardian_set_get(storage: &dyn Storage, index: u32) -> StdResult<GuardianSetInfo> {
|
||||
bucket_read(storage, GUARDIAN_SET_KEY).load(&index.to_be_bytes())
|
||||
}
|
||||
|
||||
pub fn sequence_set(storage: &mut dyn Storage, emitter: &[u8], sequence: u64) -> StdResult<()> {
|
||||
bucket(storage, SEQUENCE_KEY).save(emitter, &sequence)
|
||||
}
|
||||
|
||||
pub fn sequence_read(storage: &dyn Storage, emitter: &[u8]) -> u64 {
|
||||
bucket_read(storage, SEQUENCE_KEY)
|
||||
.load(emitter)
|
||||
.or::<u64>(Ok(0))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn vaa_archive_add(storage: &mut dyn Storage, hash: &[u8]) -> StdResult<()> {
|
||||
bucket(storage, GUARDIAN_SET_KEY).save(hash, &true)
|
||||
}
|
||||
|
||||
pub fn vaa_archive_check(storage: &dyn Storage, hash: &[u8]) -> bool {
|
||||
bucket_read(storage, GUARDIAN_SET_KEY)
|
||||
.load(hash)
|
||||
.or::<bool>(Ok(false))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn wrapped_asset(storage: &mut dyn Storage) -> Bucket<HumanAddr> {
|
||||
bucket(storage, WRAPPED_ASSET_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_read(storage: &dyn Storage) -> ReadonlyBucket<HumanAddr> {
|
||||
bucket_read(storage, WRAPPED_ASSET_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_address(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
|
||||
bucket(storage, WRAPPED_ASSET_ADDRESS_KEY)
|
||||
}
|
||||
|
||||
pub fn wrapped_asset_address_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
|
||||
bucket_read(storage, WRAPPED_ASSET_ADDRESS_KEY)
|
||||
}
|
||||
|
||||
pub struct GovernancePacket {
|
||||
pub module: Vec<u8>,
|
||||
pub action: u8,
|
||||
pub chain: u16,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl GovernancePacket {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let module = data.get_bytes32(0).to_vec();
|
||||
let action = data.get_u8(32);
|
||||
let chain = data.get_u16(33);
|
||||
let payload = data[35..].to_vec();
|
||||
|
||||
Ok(GovernancePacket {
|
||||
module,
|
||||
action,
|
||||
chain,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// action 1
|
||||
pub struct ContractUpgrade {
|
||||
pub new_contract: u64,
|
||||
}
|
||||
|
||||
// action 2
|
||||
pub struct GuardianSetUpgrade {
|
||||
pub new_guardian_set_index: u32,
|
||||
pub new_guardian_set: GuardianSetInfo,
|
||||
}
|
||||
|
||||
impl ContractUpgrade {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let new_contract = data.get_u64(24);
|
||||
Ok(ContractUpgrade {
|
||||
new_contract,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GuardianSetUpgrade {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
const ADDRESS_LEN: usize = 20;
|
||||
|
||||
let new_guardian_set_index = data.get_u32(0);
|
||||
|
||||
let n_guardians = data.get_u8(4);
|
||||
|
||||
let mut addresses = vec![];
|
||||
|
||||
for i in 0..n_guardians {
|
||||
let pos = 5 + (i as usize) * ADDRESS_LEN;
|
||||
if pos + ADDRESS_LEN > data.len() {
|
||||
return ContractError::InvalidVAA.std_err();
|
||||
}
|
||||
|
||||
addresses.push(GuardianAddress {
|
||||
bytes: data[pos..pos + ADDRESS_LEN].to_vec().into(),
|
||||
});
|
||||
}
|
||||
|
||||
let new_guardian_set = GuardianSetInfo {
|
||||
addresses,
|
||||
expiration_time: 0,
|
||||
};
|
||||
|
||||
Ok(GuardianSetUpgrade {
|
||||
new_guardian_set_index,
|
||||
new_guardian_set,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// action 3
|
||||
pub struct SetFee {
|
||||
pub fee: Coin,
|
||||
}
|
||||
|
||||
impl SetFee {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let (_, amount) = data.get_u256(0);
|
||||
let fee = Coin {
|
||||
denom: String::from(FEE_DENOMINATION),
|
||||
amount: Uint128::new(amount),
|
||||
};
|
||||
Ok(SetFee { fee })
|
||||
}
|
||||
}
|
||||
|
||||
// action 4
|
||||
pub struct TransferFee {
|
||||
pub amount: Coin,
|
||||
pub recipient: CanonicalAddr,
|
||||
}
|
||||
|
||||
impl TransferFee {
|
||||
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
|
||||
let recipient = data.get_address(0);
|
||||
|
||||
let (_, amount) = data.get_u256(32);
|
||||
let amount = Coin {
|
||||
denom: String::from(FEE_DENOMINATION),
|
||||
amount: Uint128::new(amount),
|
||||
};
|
||||
Ok(TransferFee { amount, recipient })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
mod tests;
|
|
@ -0,0 +1,162 @@
|
|||
use cosmwasm_std::StdResult;
|
||||
|
||||
use crate::state::{GuardianAddress, GuardianSetInfo, ParsedVAA};
|
||||
|
||||
#[test]
|
||||
fn quardian_set_quorum() {
|
||||
let num_guardians_trials: Vec<usize> = vec![
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 25, 100,
|
||||
];
|
||||
|
||||
let expected_quorums: Vec<usize> = vec![
|
||||
1, 2, 3, 3, 4, 5, 5, 6, 7, 7, 8, 9, 14, 17, 67,
|
||||
];
|
||||
|
||||
let make_guardian_set = |n: usize| -> GuardianSetInfo {
|
||||
let mut addresses = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
addresses.push(
|
||||
GuardianAddress {
|
||||
bytes: Vec::new().into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
GuardianSetInfo {
|
||||
addresses,
|
||||
expiration_time: 0,
|
||||
}
|
||||
};
|
||||
|
||||
for (i, &num_guardians) in num_guardians_trials.iter().enumerate() {
|
||||
let quorum = make_guardian_set(num_guardians).quorum();
|
||||
assert_eq!(quorum, expected_quorums[i], "quorum != expected");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_round_1() -> StdResult<()> {
|
||||
let signed_vaa = "\
|
||||
080000000901007bfa71192f886ab6819fa4862e34b4d178962958d9b2e3d943\
|
||||
7338c9e5fde1443b809d2886eaa69e0f0158ea517675d96243c9209c3fe1d94d\
|
||||
5b19866654c6980000000b150000000500020001020304000000000000000000\
|
||||
000000000000000000000000000000000000000000000000000a0261626364";
|
||||
let signed_vaa = hex::decode(signed_vaa).unwrap();
|
||||
|
||||
let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?;
|
||||
|
||||
let version = 8u8;
|
||||
assert_eq!(parsed.version, version, "parsed.version != expected");
|
||||
|
||||
let guardian_set_index = 9u32;
|
||||
assert_eq!(parsed.guardian_set_index, guardian_set_index, "parsed.guardian_set_index != expected");
|
||||
|
||||
let timestamp = 2837u32;
|
||||
assert_eq!(parsed.timestamp, timestamp, "parsed.timestamp != expected");
|
||||
|
||||
let nonce = 5u32;
|
||||
assert_eq!(parsed.nonce, nonce, "parsed.nonce != expected");
|
||||
|
||||
let len_signers = 1u8;
|
||||
assert_eq!(parsed.len_signers, len_signers, "parsed.len_signers != expected");
|
||||
|
||||
let emitter_chain = 2u16;
|
||||
assert_eq!(parsed.emitter_chain, emitter_chain, "parsed.emitter_chain != expected");
|
||||
|
||||
let emitter_address = "0001020304000000000000000000000000000000000000000000000000000000";
|
||||
let emitter_address = hex::decode(emitter_address).unwrap();
|
||||
assert_eq!(parsed.emitter_address, emitter_address, "parsed.emitter_address != expected");
|
||||
|
||||
let sequence = 10u64;
|
||||
assert_eq!(parsed.sequence, sequence, "parsed.sequence != expected");
|
||||
|
||||
let consistency_level = 2u8;
|
||||
assert_eq!(parsed.consistency_level, consistency_level, "parsed.consistency_level != expected");
|
||||
|
||||
let payload = vec![97u8, 98u8, 99u8, 100u8];
|
||||
assert_eq!(parsed.payload, payload, "parsed.payload != expected");
|
||||
|
||||
let hash = vec![
|
||||
164u8, 44u8, 82u8, 103u8, 33u8, 170u8, 183u8, 178u8,
|
||||
188u8, 204u8, 35u8, 53u8, 78u8, 148u8, 160u8, 153u8,
|
||||
122u8, 252u8, 84u8, 211u8, 26u8, 204u8, 128u8, 215u8,
|
||||
37u8, 232u8, 222u8, 186u8, 222u8, 186u8, 98u8, 94u8,
|
||||
];
|
||||
assert_eq!(parsed.hash, hash, "parsed.hash != expected");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_round_2() -> StdResult<()> {
|
||||
let signed_vaa = "\
|
||||
010000000001003f3179d5bb17b6f2ecc13741ca3f78d922043e99e09975e390\
|
||||
4332d2418bb3f16d7ac93ca8401f8bed1cf9827bc806ecf7c5a283340f033bf4\
|
||||
72724abf1d274f00000000000000000000010000000000000000000000000000\
|
||||
00000000000000000000000000000000ffff0000000000000000000100000000\
|
||||
00000000000000000000000000000000000000000000000005f5e10001000000\
|
||||
0000000000000000000000000000000000000000000000007575736400030000\
|
||||
00000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db30003\
|
||||
00000000000000000000000000000000000000000000000000000000000f4240";
|
||||
let signed_vaa = hex::decode(signed_vaa).unwrap();
|
||||
|
||||
let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?;
|
||||
|
||||
let version = 1u8;
|
||||
assert_eq!(parsed.version, version, "parsed.version != expected");
|
||||
|
||||
let guardian_set_index = 0u32;
|
||||
assert_eq!(parsed.guardian_set_index, guardian_set_index, "parsed.guardian_set_index != expected");
|
||||
|
||||
let timestamp = 0u32;
|
||||
assert_eq!(parsed.timestamp, timestamp, "parsed.timestamp != expected");
|
||||
|
||||
let nonce = 0u32;
|
||||
assert_eq!(parsed.nonce, nonce, "parsed.nonce != expected");
|
||||
|
||||
let len_signers = 1u8;
|
||||
assert_eq!(parsed.len_signers, len_signers, "parsed.len_signers != expected");
|
||||
|
||||
let emitter_chain = 1u16;
|
||||
assert_eq!(parsed.emitter_chain, emitter_chain, "parsed.emitter_chain != expected");
|
||||
|
||||
let emitter_address = "000000000000000000000000000000000000000000000000000000000000ffff";
|
||||
let emitter_address = hex::decode(emitter_address).unwrap();
|
||||
assert_eq!(parsed.emitter_address, emitter_address, "parsed.emitter_address != expected");
|
||||
|
||||
let sequence = 0u64;
|
||||
assert_eq!(parsed.sequence, sequence, "parsed.sequence != expected");
|
||||
|
||||
let consistency_level = 0u8;
|
||||
assert_eq!(parsed.consistency_level, consistency_level, "parsed.consistency_level != expected");
|
||||
|
||||
let payload = vec![
|
||||
1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 5u8, 245u8, 225u8,
|
||||
0u8, 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 117u8, 117u8, 115u8,
|
||||
100u8, 0u8, 3u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 247u8,
|
||||
247u8, 221u8, 232u8, 72u8, 231u8, 69u8, 10u8, 2u8,
|
||||
156u8, 208u8, 169u8, 189u8, 155u8, 218u8, 228u8, 181u8,
|
||||
20u8, 125u8, 179u8, 0u8, 3u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
|
||||
0u8, 0u8, 15u8, 66u8, 64u8,
|
||||
];
|
||||
assert_eq!(parsed.payload, payload, "parsed.payload != expected");
|
||||
|
||||
let hash = vec![
|
||||
114u8, 108u8, 111u8, 78u8, 204u8, 83u8, 150u8, 170u8,
|
||||
240u8, 15u8, 193u8, 176u8, 165u8, 87u8, 174u8, 230u8,
|
||||
94u8, 222u8, 106u8, 206u8, 179u8, 203u8, 193u8, 187u8,
|
||||
1u8, 148u8, 17u8, 40u8, 248u8, 214u8, 147u8, 68u8,
|
||||
];
|
||||
assert_eq!(parsed.hash, hash, "parsed.hash != expected");
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
use cosmwasm_std::{
|
||||
from_slice,
|
||||
testing::{
|
||||
mock_dependencies,
|
||||
mock_env,
|
||||
mock_info,
|
||||
MockApi,
|
||||
MockQuerier,
|
||||
MockStorage,
|
||||
},
|
||||
Coin,
|
||||
OwnedDeps,
|
||||
Response,
|
||||
Storage,
|
||||
};
|
||||
use cosmwasm_storage::to_length_prefixed;
|
||||
|
||||
use wormhole::{
|
||||
contract::instantiate,
|
||||
msg::InstantiateMsg,
|
||||
state::{
|
||||
ConfigInfo,
|
||||
GuardianAddress,
|
||||
GuardianSetInfo,
|
||||
CONFIG_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
use hex;
|
||||
|
||||
static INITIALIZER: &str = "initializer";
|
||||
static GOV_ADDR: &[u8] = b"GOVERNANCE_ADDRESS";
|
||||
|
||||
fn get_config_info<S: Storage>(storage: &S) -> ConfigInfo {
|
||||
let key = to_length_prefixed(CONFIG_KEY);
|
||||
let data = storage
|
||||
.get(&key)
|
||||
.expect("data should exist");
|
||||
from_slice(&data).expect("invalid data")
|
||||
}
|
||||
|
||||
fn do_init(guardians: &[GuardianAddress]) -> OwnedDeps<MockStorage, MockApi, MockQuerier> {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let init_msg = InstantiateMsg {
|
||||
gov_chain: 0,
|
||||
gov_address: GOV_ADDR.into(),
|
||||
initial_guardian_set: GuardianSetInfo {
|
||||
addresses: guardians.to_vec(),
|
||||
expiration_time: 100,
|
||||
},
|
||||
guardian_set_expirity: 50,
|
||||
};
|
||||
let env = mock_env();
|
||||
let info = mock_info(INITIALIZER, &[]);
|
||||
let res: Response = instantiate(deps.as_mut(), env, info, init_msg).unwrap();
|
||||
assert_eq!(0, res.messages.len());
|
||||
|
||||
// query the store directly
|
||||
assert_eq!(
|
||||
get_config_info(&deps.storage),
|
||||
ConfigInfo {
|
||||
guardian_set_index: 0,
|
||||
guardian_set_expirity: 50,
|
||||
gov_chain: 0,
|
||||
gov_address: GOV_ADDR.to_vec(),
|
||||
fee: Coin::new(0, "uluna"),
|
||||
}
|
||||
);
|
||||
deps
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_works() {
|
||||
let guardians = [GuardianAddress::from(GuardianAddress {
|
||||
bytes: hex::decode("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe")
|
||||
.expect("Decoding failed")
|
||||
.into(),
|
||||
})];
|
||||
let _deps = do_init(&guardians);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
FROM terramoney/localterra-core:bombay@sha256:0f93576ae0716f835b2adbd5ac550ef90063b536e99c4dd682b6dd905927f261
|
||||
|
||||
ADD config /root/.terra/config
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue