├── .github └── workflows │ ├── rust.yaml │ └── wasm.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml └── src ├── asset.rs ├── asset_info.rs ├── asset_list.rs ├── error.rs ├── lib.rs └── testing ├── custom_mock_querier.rs ├── cw20_querier.rs ├── helpers.rs └── mod.rs /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Rust 3 | jobs: 4 | check: 5 | name: Check 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout sources 9 | uses: actions/checkout@v2 10 | - name: Install toolchain 11 | uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | override: true 15 | profile: minimal 16 | - name: Check for errors 17 | uses: actions-rs/cargo@v1 18 | with: 19 | command: check 20 | args: --locked --tests 21 | env: 22 | RUST_BACKTRACE: 1 23 | test: 24 | name: Test 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout sources 28 | uses: actions/checkout@v2 29 | - name: Install toolchain 30 | uses: actions-rs/toolchain@v1 31 | with: 32 | toolchain: stable 33 | override: true 34 | profile: minimal 35 | - name: Run tests 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: test 39 | args: --lib --locked --tests 40 | env: 41 | RUST_BACKTRACE: 1 42 | clippy: 43 | name: Clippy 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout sources 47 | uses: actions/checkout@v2 48 | - name: Install toolchain 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: nightly 52 | override: true 53 | profile: minimal 54 | components: clippy 55 | - name: Run clippy 56 | uses: actions-rs/cargo@v1 57 | with: 58 | command: clippy 59 | args: --tests -- -D warnings 60 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: CosmWasm 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout sources 9 | uses: actions/checkout@v2 10 | - name: Install toolchain 11 | uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | target: wasm32-unknown-unknown 15 | override: true 16 | profile: minimal 17 | - name: Build contracts 18 | uses: actions-rs/cargo@v1 19 | with: 20 | command: build 21 | args: --locked --lib --release --target wasm32-unknown-unknown 22 | env: 23 | RUST_BACKTRACE: 1 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rust 2 | target/ 3 | 4 | # editors 5 | .idea/ 6 | .vscode/ 7 | 8 | # macos 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "allocator-api2" 19 | version = "0.2.18" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 22 | 23 | [[package]] 24 | name = "ark-bls12-381" 25 | version = "0.4.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" 28 | dependencies = [ 29 | "ark-ec", 30 | "ark-ff", 31 | "ark-serialize", 32 | "ark-std", 33 | ] 34 | 35 | [[package]] 36 | name = "ark-ec" 37 | version = "0.4.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" 40 | dependencies = [ 41 | "ark-ff", 42 | "ark-poly", 43 | "ark-serialize", 44 | "ark-std", 45 | "derivative", 46 | "hashbrown 0.13.2", 47 | "itertools", 48 | "num-traits", 49 | "rayon", 50 | "zeroize", 51 | ] 52 | 53 | [[package]] 54 | name = "ark-ff" 55 | version = "0.4.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" 58 | dependencies = [ 59 | "ark-ff-asm", 60 | "ark-ff-macros", 61 | "ark-serialize", 62 | "ark-std", 63 | "derivative", 64 | "digest", 65 | "itertools", 66 | "num-bigint", 67 | "num-traits", 68 | "paste", 69 | "rayon", 70 | "rustc_version", 71 | "zeroize", 72 | ] 73 | 74 | [[package]] 75 | name = "ark-ff-asm" 76 | version = "0.4.2" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" 79 | dependencies = [ 80 | "quote", 81 | "syn 1.0.109", 82 | ] 83 | 84 | [[package]] 85 | name = "ark-ff-macros" 86 | version = "0.4.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" 89 | dependencies = [ 90 | "num-bigint", 91 | "num-traits", 92 | "proc-macro2", 93 | "quote", 94 | "syn 1.0.109", 95 | ] 96 | 97 | [[package]] 98 | name = "ark-poly" 99 | version = "0.4.2" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" 102 | dependencies = [ 103 | "ark-ff", 104 | "ark-serialize", 105 | "ark-std", 106 | "derivative", 107 | "hashbrown 0.13.2", 108 | ] 109 | 110 | [[package]] 111 | name = "ark-serialize" 112 | version = "0.4.2" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" 115 | dependencies = [ 116 | "ark-serialize-derive", 117 | "ark-std", 118 | "digest", 119 | "num-bigint", 120 | ] 121 | 122 | [[package]] 123 | name = "ark-serialize-derive" 124 | version = "0.4.2" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" 127 | dependencies = [ 128 | "proc-macro2", 129 | "quote", 130 | "syn 1.0.109", 131 | ] 132 | 133 | [[package]] 134 | name = "ark-std" 135 | version = "0.4.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" 138 | dependencies = [ 139 | "num-traits", 140 | "rand", 141 | "rayon", 142 | ] 143 | 144 | [[package]] 145 | name = "autocfg" 146 | version = "1.3.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 149 | 150 | [[package]] 151 | name = "base16ct" 152 | version = "0.2.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 155 | 156 | [[package]] 157 | name = "base64" 158 | version = "0.22.1" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 161 | 162 | [[package]] 163 | name = "bech32" 164 | version = "0.11.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" 167 | 168 | [[package]] 169 | name = "block-buffer" 170 | version = "0.10.4" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 173 | dependencies = [ 174 | "generic-array", 175 | ] 176 | 177 | [[package]] 178 | name = "bnum" 179 | version = "0.11.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" 182 | 183 | [[package]] 184 | name = "byteorder" 185 | version = "1.5.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 188 | 189 | [[package]] 190 | name = "cfg-if" 191 | version = "1.0.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 194 | 195 | [[package]] 196 | name = "const-oid" 197 | version = "0.9.6" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 200 | 201 | [[package]] 202 | name = "cosmwasm-core" 203 | version = "2.1.3" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "d905990ef3afb5753bb709dc7de88e9e370aa32bcc2f31731d4b533b63e82490" 206 | 207 | [[package]] 208 | name = "cosmwasm-crypto" 209 | version = "2.1.3" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "5b2a7bd9c1dd9a377a4dc0f4ad97d24b03c33798cd5a6d7ceb8869b41c5d2f2d" 212 | dependencies = [ 213 | "ark-bls12-381", 214 | "ark-ec", 215 | "ark-ff", 216 | "ark-serialize", 217 | "cosmwasm-core", 218 | "digest", 219 | "ecdsa", 220 | "ed25519-zebra", 221 | "k256", 222 | "num-traits", 223 | "p256", 224 | "rand_core", 225 | "rayon", 226 | "sha2", 227 | "thiserror", 228 | ] 229 | 230 | [[package]] 231 | name = "cosmwasm-derive" 232 | version = "2.1.3" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "029910b409398fdf81955d7301b906caf81f2c42b013ea074fbd89720229c424" 235 | dependencies = [ 236 | "proc-macro2", 237 | "quote", 238 | "syn 2.0.75", 239 | ] 240 | 241 | [[package]] 242 | name = "cosmwasm-schema" 243 | version = "2.1.3" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "4bc0d4d85e83438ab9a0fea9348446f7268bc016aacfebce37e998559f151294" 246 | dependencies = [ 247 | "cosmwasm-schema-derive", 248 | "schemars", 249 | "serde", 250 | "serde_json", 251 | "thiserror", 252 | ] 253 | 254 | [[package]] 255 | name = "cosmwasm-schema-derive" 256 | version = "2.1.3" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "edf5c8adac41bb7751c050d7c4c18675be19ee128714454454575e894424eeef" 259 | dependencies = [ 260 | "proc-macro2", 261 | "quote", 262 | "syn 2.0.75", 263 | ] 264 | 265 | [[package]] 266 | name = "cosmwasm-std" 267 | version = "2.1.3" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "51dec99a2e478715c0a4277f0dbeadbb8466500eb7dec873d0924edd086e77f1" 270 | dependencies = [ 271 | "base64", 272 | "bech32", 273 | "bnum", 274 | "cosmwasm-core", 275 | "cosmwasm-crypto", 276 | "cosmwasm-derive", 277 | "derive_more", 278 | "hex", 279 | "rand_core", 280 | "schemars", 281 | "serde", 282 | "serde-json-wasm", 283 | "sha2", 284 | "static_assertions", 285 | "thiserror", 286 | ] 287 | 288 | [[package]] 289 | name = "cpufeatures" 290 | version = "0.2.13" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" 293 | dependencies = [ 294 | "libc", 295 | ] 296 | 297 | [[package]] 298 | name = "crossbeam-deque" 299 | version = "0.8.5" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 302 | dependencies = [ 303 | "crossbeam-epoch", 304 | "crossbeam-utils", 305 | ] 306 | 307 | [[package]] 308 | name = "crossbeam-epoch" 309 | version = "0.9.18" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 312 | dependencies = [ 313 | "crossbeam-utils", 314 | ] 315 | 316 | [[package]] 317 | name = "crossbeam-utils" 318 | version = "0.8.20" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 321 | 322 | [[package]] 323 | name = "crypto-bigint" 324 | version = "0.5.5" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 327 | dependencies = [ 328 | "generic-array", 329 | "rand_core", 330 | "subtle", 331 | "zeroize", 332 | ] 333 | 334 | [[package]] 335 | name = "crypto-common" 336 | version = "0.1.6" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 339 | dependencies = [ 340 | "generic-array", 341 | "typenum", 342 | ] 343 | 344 | [[package]] 345 | name = "curve25519-dalek" 346 | version = "4.1.3" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 349 | dependencies = [ 350 | "cfg-if", 351 | "cpufeatures", 352 | "curve25519-dalek-derive", 353 | "digest", 354 | "fiat-crypto", 355 | "rustc_version", 356 | "subtle", 357 | "zeroize", 358 | ] 359 | 360 | [[package]] 361 | name = "curve25519-dalek-derive" 362 | version = "0.1.1" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 365 | dependencies = [ 366 | "proc-macro2", 367 | "quote", 368 | "syn 2.0.75", 369 | ] 370 | 371 | [[package]] 372 | name = "cw-address-like" 373 | version = "2.0.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "73553ee4dad5b1678977ff603e72c3fdd41518ca2b0bd9b245b21e4c72eafa9e" 376 | dependencies = [ 377 | "cosmwasm-std", 378 | ] 379 | 380 | [[package]] 381 | name = "cw-asset" 382 | version = "4.0.0" 383 | dependencies = [ 384 | "cosmwasm-schema", 385 | "cosmwasm-std", 386 | "cw-address-like", 387 | "cw-storage-plus", 388 | "cw20", 389 | "serde", 390 | "thiserror", 391 | ] 392 | 393 | [[package]] 394 | name = "cw-storage-plus" 395 | version = "2.0.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "f13360e9007f51998d42b1bc6b7fa0141f74feae61ed5fd1e5b0a89eec7b5de1" 398 | dependencies = [ 399 | "cosmwasm-std", 400 | "schemars", 401 | "serde", 402 | ] 403 | 404 | [[package]] 405 | name = "cw-utils" 406 | version = "2.0.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "07dfee7f12f802431a856984a32bce1cb7da1e6c006b5409e3981035ce562dec" 409 | dependencies = [ 410 | "cosmwasm-schema", 411 | "cosmwasm-std", 412 | "schemars", 413 | "serde", 414 | "thiserror", 415 | ] 416 | 417 | [[package]] 418 | name = "cw20" 419 | version = "2.0.0" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "a42212b6bf29bbdda693743697c621894723f35d3db0d5df930be22903d0e27c" 422 | dependencies = [ 423 | "cosmwasm-schema", 424 | "cosmwasm-std", 425 | "cw-utils", 426 | "schemars", 427 | "serde", 428 | ] 429 | 430 | [[package]] 431 | name = "der" 432 | version = "0.7.9" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" 435 | dependencies = [ 436 | "const-oid", 437 | "zeroize", 438 | ] 439 | 440 | [[package]] 441 | name = "derivative" 442 | version = "2.2.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 445 | dependencies = [ 446 | "proc-macro2", 447 | "quote", 448 | "syn 1.0.109", 449 | ] 450 | 451 | [[package]] 452 | name = "derive_more" 453 | version = "1.0.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 456 | dependencies = [ 457 | "derive_more-impl", 458 | ] 459 | 460 | [[package]] 461 | name = "derive_more-impl" 462 | version = "1.0.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 465 | dependencies = [ 466 | "proc-macro2", 467 | "quote", 468 | "syn 2.0.75", 469 | "unicode-xid", 470 | ] 471 | 472 | [[package]] 473 | name = "digest" 474 | version = "0.10.7" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 477 | dependencies = [ 478 | "block-buffer", 479 | "const-oid", 480 | "crypto-common", 481 | "subtle", 482 | ] 483 | 484 | [[package]] 485 | name = "dyn-clone" 486 | version = "1.0.17" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" 489 | 490 | [[package]] 491 | name = "ecdsa" 492 | version = "0.16.9" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 495 | dependencies = [ 496 | "der", 497 | "digest", 498 | "elliptic-curve", 499 | "rfc6979", 500 | "signature", 501 | ] 502 | 503 | [[package]] 504 | name = "ed25519" 505 | version = "2.2.3" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" 508 | dependencies = [ 509 | "signature", 510 | ] 511 | 512 | [[package]] 513 | name = "ed25519-zebra" 514 | version = "4.0.3" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" 517 | dependencies = [ 518 | "curve25519-dalek", 519 | "ed25519", 520 | "hashbrown 0.14.5", 521 | "hex", 522 | "rand_core", 523 | "sha2", 524 | "zeroize", 525 | ] 526 | 527 | [[package]] 528 | name = "either" 529 | version = "1.13.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 532 | 533 | [[package]] 534 | name = "elliptic-curve" 535 | version = "0.13.8" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 538 | dependencies = [ 539 | "base16ct", 540 | "crypto-bigint", 541 | "digest", 542 | "ff", 543 | "generic-array", 544 | "group", 545 | "rand_core", 546 | "sec1", 547 | "subtle", 548 | "zeroize", 549 | ] 550 | 551 | [[package]] 552 | name = "ff" 553 | version = "0.13.0" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" 556 | dependencies = [ 557 | "rand_core", 558 | "subtle", 559 | ] 560 | 561 | [[package]] 562 | name = "fiat-crypto" 563 | version = "0.2.9" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 566 | 567 | [[package]] 568 | name = "generic-array" 569 | version = "0.14.7" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 572 | dependencies = [ 573 | "typenum", 574 | "version_check", 575 | "zeroize", 576 | ] 577 | 578 | [[package]] 579 | name = "getrandom" 580 | version = "0.2.15" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 583 | dependencies = [ 584 | "cfg-if", 585 | "libc", 586 | "wasi", 587 | ] 588 | 589 | [[package]] 590 | name = "group" 591 | version = "0.13.0" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 594 | dependencies = [ 595 | "ff", 596 | "rand_core", 597 | "subtle", 598 | ] 599 | 600 | [[package]] 601 | name = "hashbrown" 602 | version = "0.13.2" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 605 | dependencies = [ 606 | "ahash", 607 | ] 608 | 609 | [[package]] 610 | name = "hashbrown" 611 | version = "0.14.5" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 614 | dependencies = [ 615 | "ahash", 616 | "allocator-api2", 617 | ] 618 | 619 | [[package]] 620 | name = "hex" 621 | version = "0.4.3" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 624 | 625 | [[package]] 626 | name = "hmac" 627 | version = "0.12.1" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 630 | dependencies = [ 631 | "digest", 632 | ] 633 | 634 | [[package]] 635 | name = "itertools" 636 | version = "0.10.5" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 639 | dependencies = [ 640 | "either", 641 | ] 642 | 643 | [[package]] 644 | name = "itoa" 645 | version = "1.0.11" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 648 | 649 | [[package]] 650 | name = "k256" 651 | version = "0.13.3" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" 654 | dependencies = [ 655 | "cfg-if", 656 | "ecdsa", 657 | "elliptic-curve", 658 | "sha2", 659 | ] 660 | 661 | [[package]] 662 | name = "libc" 663 | version = "0.2.158" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 666 | 667 | [[package]] 668 | name = "memchr" 669 | version = "2.7.4" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 672 | 673 | [[package]] 674 | name = "num-bigint" 675 | version = "0.4.6" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 678 | dependencies = [ 679 | "num-integer", 680 | "num-traits", 681 | ] 682 | 683 | [[package]] 684 | name = "num-integer" 685 | version = "0.1.46" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 688 | dependencies = [ 689 | "num-traits", 690 | ] 691 | 692 | [[package]] 693 | name = "num-traits" 694 | version = "0.2.19" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 697 | dependencies = [ 698 | "autocfg", 699 | ] 700 | 701 | [[package]] 702 | name = "once_cell" 703 | version = "1.19.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 706 | 707 | [[package]] 708 | name = "p256" 709 | version = "0.13.2" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 712 | dependencies = [ 713 | "ecdsa", 714 | "elliptic-curve", 715 | "primeorder", 716 | "sha2", 717 | ] 718 | 719 | [[package]] 720 | name = "paste" 721 | version = "1.0.15" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 724 | 725 | [[package]] 726 | name = "ppv-lite86" 727 | version = "0.2.20" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 730 | dependencies = [ 731 | "zerocopy", 732 | ] 733 | 734 | [[package]] 735 | name = "primeorder" 736 | version = "0.13.6" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 739 | dependencies = [ 740 | "elliptic-curve", 741 | ] 742 | 743 | [[package]] 744 | name = "proc-macro2" 745 | version = "1.0.86" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 748 | dependencies = [ 749 | "unicode-ident", 750 | ] 751 | 752 | [[package]] 753 | name = "quote" 754 | version = "1.0.37" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 757 | dependencies = [ 758 | "proc-macro2", 759 | ] 760 | 761 | [[package]] 762 | name = "rand" 763 | version = "0.8.5" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 766 | dependencies = [ 767 | "rand_chacha", 768 | "rand_core", 769 | ] 770 | 771 | [[package]] 772 | name = "rand_chacha" 773 | version = "0.3.1" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 776 | dependencies = [ 777 | "ppv-lite86", 778 | "rand_core", 779 | ] 780 | 781 | [[package]] 782 | name = "rand_core" 783 | version = "0.6.4" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 786 | dependencies = [ 787 | "getrandom", 788 | ] 789 | 790 | [[package]] 791 | name = "rayon" 792 | version = "1.10.0" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 795 | dependencies = [ 796 | "either", 797 | "rayon-core", 798 | ] 799 | 800 | [[package]] 801 | name = "rayon-core" 802 | version = "1.12.1" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 805 | dependencies = [ 806 | "crossbeam-deque", 807 | "crossbeam-utils", 808 | ] 809 | 810 | [[package]] 811 | name = "rfc6979" 812 | version = "0.4.0" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 815 | dependencies = [ 816 | "hmac", 817 | "subtle", 818 | ] 819 | 820 | [[package]] 821 | name = "rustc_version" 822 | version = "0.4.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 825 | dependencies = [ 826 | "semver", 827 | ] 828 | 829 | [[package]] 830 | name = "ryu" 831 | version = "1.0.18" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 834 | 835 | [[package]] 836 | name = "schemars" 837 | version = "0.8.21" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" 840 | dependencies = [ 841 | "dyn-clone", 842 | "schemars_derive", 843 | "serde", 844 | "serde_json", 845 | ] 846 | 847 | [[package]] 848 | name = "schemars_derive" 849 | version = "0.8.21" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" 852 | dependencies = [ 853 | "proc-macro2", 854 | "quote", 855 | "serde_derive_internals", 856 | "syn 2.0.75", 857 | ] 858 | 859 | [[package]] 860 | name = "sec1" 861 | version = "0.7.3" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 864 | dependencies = [ 865 | "base16ct", 866 | "der", 867 | "generic-array", 868 | "subtle", 869 | "zeroize", 870 | ] 871 | 872 | [[package]] 873 | name = "semver" 874 | version = "1.0.23" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 877 | 878 | [[package]] 879 | name = "serde" 880 | version = "1.0.208" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" 883 | dependencies = [ 884 | "serde_derive", 885 | ] 886 | 887 | [[package]] 888 | name = "serde-json-wasm" 889 | version = "1.0.1" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "f05da0d153dd4595bdffd5099dc0e9ce425b205ee648eb93437ff7302af8c9a5" 892 | dependencies = [ 893 | "serde", 894 | ] 895 | 896 | [[package]] 897 | name = "serde_derive" 898 | version = "1.0.208" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" 901 | dependencies = [ 902 | "proc-macro2", 903 | "quote", 904 | "syn 2.0.75", 905 | ] 906 | 907 | [[package]] 908 | name = "serde_derive_internals" 909 | version = "0.29.1" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" 912 | dependencies = [ 913 | "proc-macro2", 914 | "quote", 915 | "syn 2.0.75", 916 | ] 917 | 918 | [[package]] 919 | name = "serde_json" 920 | version = "1.0.125" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" 923 | dependencies = [ 924 | "itoa", 925 | "memchr", 926 | "ryu", 927 | "serde", 928 | ] 929 | 930 | [[package]] 931 | name = "sha2" 932 | version = "0.10.8" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 935 | dependencies = [ 936 | "cfg-if", 937 | "cpufeatures", 938 | "digest", 939 | ] 940 | 941 | [[package]] 942 | name = "signature" 943 | version = "2.2.0" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 946 | dependencies = [ 947 | "digest", 948 | "rand_core", 949 | ] 950 | 951 | [[package]] 952 | name = "static_assertions" 953 | version = "1.1.0" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 956 | 957 | [[package]] 958 | name = "subtle" 959 | version = "2.6.1" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 962 | 963 | [[package]] 964 | name = "syn" 965 | version = "1.0.109" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 968 | dependencies = [ 969 | "proc-macro2", 970 | "quote", 971 | "unicode-ident", 972 | ] 973 | 974 | [[package]] 975 | name = "syn" 976 | version = "2.0.75" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" 979 | dependencies = [ 980 | "proc-macro2", 981 | "quote", 982 | "unicode-ident", 983 | ] 984 | 985 | [[package]] 986 | name = "thiserror" 987 | version = "1.0.63" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 990 | dependencies = [ 991 | "thiserror-impl", 992 | ] 993 | 994 | [[package]] 995 | name = "thiserror-impl" 996 | version = "1.0.63" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 999 | dependencies = [ 1000 | "proc-macro2", 1001 | "quote", 1002 | "syn 2.0.75", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "typenum" 1007 | version = "1.17.0" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1010 | 1011 | [[package]] 1012 | name = "unicode-ident" 1013 | version = "1.0.12" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1016 | 1017 | [[package]] 1018 | name = "unicode-xid" 1019 | version = "0.2.5" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" 1022 | 1023 | [[package]] 1024 | name = "version_check" 1025 | version = "0.9.5" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1028 | 1029 | [[package]] 1030 | name = "wasi" 1031 | version = "0.11.0+wasi-snapshot-preview1" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1034 | 1035 | [[package]] 1036 | name = "zerocopy" 1037 | version = "0.7.35" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1040 | dependencies = [ 1041 | "byteorder", 1042 | "zerocopy-derive", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "zerocopy-derive" 1047 | version = "0.7.35" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1050 | dependencies = [ 1051 | "proc-macro2", 1052 | "quote", 1053 | "syn 2.0.75", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "zeroize" 1058 | version = "1.8.1" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1061 | dependencies = [ 1062 | "zeroize_derive", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "zeroize_derive" 1067 | version = "1.4.2" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 1070 | dependencies = [ 1071 | "proc-macro2", 1072 | "quote", 1073 | "syn 2.0.75", 1074 | ] 1075 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw-asset" 3 | description = "Helper library for interacting with Cosmos assets (native coins and CW20 tokens)" 4 | version = "4.0.0" 5 | authors = ["Larry Engineer "] 6 | edition = "2021" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/mars-protocol/cw-asset" 9 | 10 | [dependencies] 11 | cosmwasm-schema = "2.0.0" 12 | cosmwasm-std = "2.0.0" 13 | cw20 = "2.0.0" 14 | cw-address-like = "2.0.0" 15 | cw-storage-plus = "2.0.0" 16 | thiserror = "1.0.56" 17 | 18 | [dev-dependencies] 19 | serde = { version = "1.0", default-features = false, features = ["derive"] } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cw-asset` 2 | 3 | A unified representation of various types of Cosmos fungible assets, and helper functions for interacting with them 4 | 5 | ## Links 6 | 7 | - [Documentations on docs.rs](https://docs.rs/cw-asset/latest/cw_asset/) 8 | - [Audit report by Halborn](https://github.com/HalbornSecurity/PublicReports/blob/master/CosmWasm%20Smart%20Contract%20Audits/Mars_CW_Asset_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf) 9 | 10 | ## License 11 | 12 | Contents of this repository are open source under [Apache 2.0](./LICENSE). 13 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # https://rust-lang.github.io/rustfmt 2 | format_code_in_doc_comments = true # requires nightly 3 | group_imports = "StdExternalCrate" # requires nightly 4 | imports_granularity = "Crate" # requires nightly 5 | match_block_trailing_comma = true 6 | max_width = 100 7 | use_small_heuristics = "off" 8 | -------------------------------------------------------------------------------- /src/asset.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, fmt, str::FromStr}; 2 | 3 | use cosmwasm_schema::cw_serde; 4 | use cosmwasm_std::{to_json_binary, Addr, Api, BankMsg, Binary, Coin, CosmosMsg, Uint128, WasmMsg}; 5 | use cw20::Cw20ExecuteMsg; 6 | use cw_address_like::AddressLike; 7 | 8 | use crate::{AssetError, AssetInfo, AssetInfoBase, AssetInfoUnchecked}; 9 | 10 | /// Represents a fungible asset with a known amount 11 | /// 12 | /// Each asset instance contains two values: `info`, which specifies the asset's 13 | /// type (CW20 or native), and its `amount`, which specifies the asset's amount. 14 | #[cw_serde] 15 | pub struct AssetBase { 16 | /// Specifies the asset's type (CW20 or native) 17 | pub info: AssetInfoBase, 18 | /// Specifies the asset's amount 19 | pub amount: Uint128, 20 | } 21 | 22 | impl AssetBase { 23 | /// Create a new **asset** instance based on given asset info and amount 24 | /// 25 | /// To create an unchecked instance, the `info` parameter may be either 26 | /// checked or unchecked; to create a checked instance, the `info` paramter 27 | /// must also be checked. 28 | /// 29 | /// ```rust 30 | /// use cosmwasm_std::Addr; 31 | /// use cw_asset::{Asset, AssetInfo}; 32 | /// 33 | /// let info1 = AssetInfo::cw20(Addr::unchecked("token_addr")); 34 | /// let asset1 = Asset::new(info1, 12345u128); 35 | /// 36 | /// let info2 = AssetInfo::native("uusd"); 37 | /// let asset2 = Asset::new(info2, 67890u128); 38 | /// ``` 39 | pub fn new>, B: Into>(info: A, amount: B) -> Self { 40 | Self { 41 | info: info.into(), 42 | amount: amount.into(), 43 | } 44 | } 45 | 46 | /// Create a new **asset** instance representing a native coin of given denom and amount 47 | /// 48 | /// ```rust 49 | /// use cw_asset::Asset; 50 | /// 51 | /// let asset = Asset::native("uusd", 12345u128); 52 | /// ``` 53 | pub fn native, B: Into>(denom: A, amount: B) -> Self { 54 | Self { 55 | info: AssetInfoBase::native(denom), 56 | amount: amount.into(), 57 | } 58 | } 59 | 60 | /// Create a new **asset** instance representing a CW20 token of given 61 | /// contract address and amount. 62 | /// 63 | /// ```rust 64 | /// use cosmwasm_std::Addr; 65 | /// use cw_asset::Asset; 66 | /// 67 | /// let asset = Asset::cw20(Addr::unchecked("token_addr"), 12345u128); 68 | /// ``` 69 | pub fn cw20, B: Into>(contract_addr: A, amount: B) -> Self { 70 | Self { 71 | info: AssetInfoBase::cw20(contract_addr), 72 | amount: amount.into(), 73 | } 74 | } 75 | } 76 | 77 | // Represents an **asset** instance that may contain unverified data; to be used 78 | // in messages. 79 | pub type AssetUnchecked = AssetBase; 80 | 81 | // Represents an **asset** instance containing only verified data; to be saved 82 | // in contract storage. 83 | pub type Asset = AssetBase; 84 | 85 | impl FromStr for AssetUnchecked { 86 | type Err = AssetError; 87 | 88 | fn from_str(s: &str) -> Result { 89 | let words: Vec<&str> = s.split(':').collect(); 90 | 91 | let info = match words[0] { 92 | "native" | "cw20" => { 93 | if words.len() != 3 { 94 | return Err(AssetError::InvalidAssetFormat { 95 | received: s.into(), 96 | }); 97 | } 98 | AssetInfoUnchecked::from_str(&format!("{}:{}", words[0], words[1]))? 99 | }, 100 | ty => { 101 | return Err(AssetError::InvalidAssetType { 102 | ty: ty.into(), 103 | }); 104 | }, 105 | }; 106 | 107 | let amount_str = words[words.len() - 1]; 108 | let amount = Uint128::from_str(amount_str).map_err(|_| AssetError::InvalidAssetAmount { 109 | amount: amount_str.into(), 110 | })?; 111 | 112 | Ok(AssetUnchecked { 113 | info, 114 | amount, 115 | }) 116 | } 117 | } 118 | 119 | impl From for AssetUnchecked { 120 | fn from(asset: Asset) -> Self { 121 | AssetUnchecked { 122 | info: asset.info.into(), 123 | amount: asset.amount, 124 | } 125 | } 126 | } 127 | 128 | impl AssetUnchecked { 129 | /// Parse a string of the format `{amount}{denom}` into an `AssetUnchecked` 130 | /// object. This is the format that Cosmos SDK uses to stringify native 131 | /// coins. For example: 132 | /// 133 | /// - `12345uatom` 134 | /// - `69420ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2` 135 | /// - `88888factory/osmo1z926ax906k0ycsuckele6x5hh66e2m4m6ry7dn` 136 | /// 137 | /// Since native coin denoms can only start with a non-numerial character, 138 | /// while its amount can only contain numerical characters, we simply 139 | /// consider the first non-numerical character and all that comes after as 140 | /// the denom, while all that comes before it as the amount. This is the 141 | /// approach used in the [Steak Hub contract](https://github.com/st4k3h0us3/steak-contracts/blob/v1.0.0/contracts/hub/src/helpers.rs#L48-L68). 142 | pub fn from_sdk_string(s: &str) -> Result { 143 | for (i, c) in s.chars().enumerate() { 144 | if !c.is_ascii_digit() { 145 | let amount = Uint128::from_str(&s[..i])?; 146 | let denom = &s[i..]; 147 | return Ok(Self::native(denom, amount)); 148 | } 149 | } 150 | 151 | Err(AssetError::InvalidSdkCoin { 152 | coin_str: s.into(), 153 | }) 154 | } 155 | 156 | /// Validate data contained in an _unchecked_ **asset** instnace, return a 157 | /// new _checked_ **asset** instance: 158 | /// 159 | /// - For CW20 tokens, assert the contract address is valid; 160 | /// - For SDK coins, assert that the denom is included in a given whitelist; 161 | /// skip if the whitelist is not provided. 162 | /// 163 | /// ```rust 164 | /// use cosmwasm_std::{Addr, Api}; 165 | /// use cw_asset::{Asset, AssetUnchecked}; 166 | /// 167 | /// fn validate_asset(api: &dyn Api, asset_unchecked: &AssetUnchecked) { 168 | /// match asset_unchecked.check(api, Some(&["uatom", "uluna"])) { 169 | /// Ok(asset) => println!("asset is valid: {}", asset.to_string()), 170 | /// Err(err) => println!("asset is invalid! reason: {}", err), 171 | /// } 172 | /// } 173 | /// ``` 174 | pub fn check( 175 | &self, 176 | api: &dyn Api, 177 | optional_whitelist: Option<&[&str]>, 178 | ) -> Result { 179 | Ok(Asset { 180 | info: self.info.check(api, optional_whitelist)?, 181 | amount: self.amount, 182 | }) 183 | } 184 | } 185 | 186 | impl fmt::Display for Asset { 187 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 188 | write!(f, "{}:{}", self.info, self.amount) 189 | } 190 | } 191 | 192 | impl From for Asset { 193 | fn from(coin: Coin) -> Self { 194 | Self { 195 | info: AssetInfo::Native(coin.denom), 196 | amount: coin.amount, 197 | } 198 | } 199 | } 200 | 201 | impl From<&Coin> for Asset { 202 | fn from(coin: &Coin) -> Self { 203 | coin.clone().into() 204 | } 205 | } 206 | 207 | impl TryFrom for Coin { 208 | type Error = AssetError; 209 | 210 | fn try_from(asset: Asset) -> Result { 211 | match &asset.info { 212 | AssetInfo::Native(denom) => Ok(Coin { 213 | denom: denom.clone(), 214 | amount: asset.amount, 215 | }), 216 | AssetInfo::Cw20(_) => Err(AssetError::CannotCastToStdCoin { 217 | asset: asset.to_string(), 218 | }), 219 | } 220 | } 221 | } 222 | 223 | impl TryFrom<&Asset> for Coin { 224 | type Error = AssetError; 225 | 226 | fn try_from(asset: &Asset) -> Result { 227 | Coin::try_from(asset.clone()) 228 | } 229 | } 230 | 231 | impl std::cmp::PartialEq for Coin { 232 | fn eq(&self, other: &Asset) -> bool { 233 | match &other.info { 234 | AssetInfo::Native(denom) => self.denom == *denom && self.amount == other.amount, 235 | AssetInfo::Cw20(_) => false, 236 | } 237 | } 238 | } 239 | 240 | impl std::cmp::PartialEq for Asset { 241 | fn eq(&self, other: &Coin) -> bool { 242 | other == self 243 | } 244 | } 245 | 246 | impl Asset { 247 | /// Generate a message that sends a CW20 token to the specified recipient 248 | /// with a binary payload. 249 | /// 250 | /// NOTE: Only works for CW20 tokens. Returns error if invoked on an `Asset` 251 | /// instance representing a native coin, as native coins do not have an 252 | /// equivalent method mplemented. 253 | /// 254 | /// ```rust 255 | /// use serde::Serialize; 256 | /// 257 | /// #[derive(Serialize)] 258 | /// enum MockReceiveMsg { 259 | /// MockCommand {}, 260 | /// } 261 | /// 262 | /// use cosmwasm_std::{to_json_binary, Addr, Response}; 263 | /// use cw_asset::{Asset, AssetError}; 264 | /// 265 | /// fn send_asset( 266 | /// asset: &Asset, 267 | /// contract_addr: &Addr, 268 | /// msg: &MockReceiveMsg, 269 | /// ) -> Result { 270 | /// let msg = asset.send_msg(contract_addr, to_json_binary(msg)?)?; 271 | /// 272 | /// Ok(Response::new().add_message(msg).add_attribute("asset_sent", asset.to_string())) 273 | /// } 274 | /// ``` 275 | pub fn send_msg>(&self, to: A, msg: Binary) -> Result { 276 | match &self.info { 277 | AssetInfo::Cw20(contract_addr) => Ok(CosmosMsg::Wasm(WasmMsg::Execute { 278 | contract_addr: contract_addr.into(), 279 | msg: to_json_binary(&Cw20ExecuteMsg::Send { 280 | contract: to.into(), 281 | amount: self.amount, 282 | msg, 283 | })?, 284 | funds: vec![], 285 | })), 286 | AssetInfo::Native(_) => Err(AssetError::UnavailableMethodForNative { 287 | method: "send".into(), 288 | }), 289 | } 290 | } 291 | 292 | /// Generate a message that transfers the asset from the sender to to a 293 | /// specified account. 294 | /// 295 | /// ```rust 296 | /// use cosmwasm_std::{Addr, Response}; 297 | /// use cw_asset::{Asset, AssetError}; 298 | /// 299 | /// fn transfer_asset(asset: &Asset, recipient_addr: &Addr) -> Result { 300 | /// let msg = asset.transfer_msg(recipient_addr)?; 301 | /// 302 | /// Ok(Response::new().add_message(msg).add_attribute("asset_sent", asset.to_string())) 303 | /// } 304 | /// ``` 305 | pub fn transfer_msg>(&self, to: A) -> Result { 306 | match &self.info { 307 | AssetInfo::Native(denom) => Ok(CosmosMsg::Bank(BankMsg::Send { 308 | to_address: to.into(), 309 | amount: vec![Coin { 310 | denom: denom.clone(), 311 | amount: self.amount, 312 | }], 313 | })), 314 | AssetInfo::Cw20(contract_addr) => Ok(CosmosMsg::Wasm(WasmMsg::Execute { 315 | contract_addr: contract_addr.into(), 316 | msg: to_json_binary(&Cw20ExecuteMsg::Transfer { 317 | recipient: to.into(), 318 | amount: self.amount, 319 | })?, 320 | funds: vec![], 321 | })), 322 | } 323 | } 324 | 325 | /// Generate a message that draws the asset from the account specified by 326 | /// `from` to the one specified by `to`. 327 | /// 328 | /// NOTE: Only works for CW20 tokens. Returns error if invoked on an `Asset` 329 | /// instance representing a native coin, as native coins do not have an 330 | /// equivalent method implemented. 331 | /// 332 | /// ```rust 333 | /// use cosmwasm_std::{Addr, Response}; 334 | /// use cw_asset::{Asset, AssetError}; 335 | /// 336 | /// fn draw_asset( 337 | /// asset: &Asset, 338 | /// user_addr: &Addr, 339 | /// contract_addr: &Addr, 340 | /// ) -> Result { 341 | /// let msg = asset.transfer_from_msg(user_addr, contract_addr)?; 342 | /// 343 | /// Ok(Response::new().add_message(msg).add_attribute("asset_drawn", asset.to_string())) 344 | /// } 345 | /// ``` 346 | pub fn transfer_from_msg, B: Into>( 347 | &self, 348 | from: A, 349 | to: B, 350 | ) -> Result { 351 | match &self.info { 352 | AssetInfo::Cw20(contract_addr) => Ok(CosmosMsg::Wasm(WasmMsg::Execute { 353 | contract_addr: contract_addr.into(), 354 | msg: to_json_binary(&Cw20ExecuteMsg::TransferFrom { 355 | owner: from.into(), 356 | recipient: to.into(), 357 | amount: self.amount, 358 | })?, 359 | funds: vec![], 360 | })), 361 | AssetInfo::Native(_) => Err(AssetError::UnavailableMethodForNative { 362 | method: "transfer_from".into(), 363 | }), 364 | } 365 | } 366 | } 367 | 368 | //------------------------------------------------------------------------------ 369 | // Tests 370 | //------------------------------------------------------------------------------ 371 | 372 | #[cfg(test)] 373 | mod tests { 374 | use cosmwasm_std::{testing::MockApi, StdError}; 375 | use serde::Serialize; 376 | 377 | use super::*; 378 | use crate::AssetInfoUnchecked; 379 | 380 | #[derive(Serialize)] 381 | enum MockExecuteMsg { 382 | MockCommand {}, 383 | } 384 | 385 | #[test] 386 | fn creating_instances() { 387 | let info = AssetInfo::native("uusd"); 388 | let asset = Asset::new(info, 123456u128); 389 | assert_eq!( 390 | asset, 391 | Asset { 392 | info: AssetInfo::Native(String::from("uusd")), 393 | amount: Uint128::new(123456u128) 394 | }, 395 | ); 396 | 397 | let asset = Asset::cw20(Addr::unchecked("mock_token"), 123456u128); 398 | assert_eq!( 399 | asset, 400 | Asset { 401 | info: AssetInfo::Cw20(Addr::unchecked("mock_token")), 402 | amount: Uint128::new(123456u128) 403 | }, 404 | ); 405 | 406 | let asset = Asset::native("uusd", 123456u128); 407 | assert_eq!( 408 | asset, 409 | Asset { 410 | info: AssetInfo::Native(String::from("uusd")), 411 | amount: Uint128::new(123456u128) 412 | }, 413 | ) 414 | } 415 | 416 | #[test] 417 | fn casting_coin() { 418 | let uusd = Asset::native("uusd", 69u128); 419 | let uusd_coin = Coin { 420 | denom: String::from("uusd"), 421 | amount: Uint128::new(69), 422 | }; 423 | assert_eq!(Coin::try_from(&uusd).unwrap(), uusd_coin); 424 | assert_eq!(Coin::try_from(uusd).unwrap(), uusd_coin); 425 | 426 | let astro = Asset::cw20(Addr::unchecked("astro_token"), 69u128); 427 | assert_eq!( 428 | Coin::try_from(&astro), 429 | Err(AssetError::CannotCastToStdCoin { 430 | asset: "cw20:astro_token:69".into(), 431 | }), 432 | ); 433 | assert_eq!( 434 | Coin::try_from(astro), 435 | Err(AssetError::CannotCastToStdCoin { 436 | asset: "cw20:astro_token:69".into(), 437 | }), 438 | ); 439 | } 440 | 441 | #[test] 442 | fn comparing() { 443 | let uluna1 = Asset::native("uluna", 69u128); 444 | let uluna2 = Asset::native("uluna", 420u128); 445 | let uusd = Asset::native("uusd", 69u128); 446 | let astro = Asset::cw20(Addr::unchecked("astro_token"), 69u128); 447 | 448 | assert!(uluna1 != uluna2); 449 | assert!(uluna1 != uusd); 450 | assert!(astro == astro.clone()); 451 | } 452 | 453 | #[test] 454 | fn comparing_coin() { 455 | let uluna = Asset::native("uluna", 69u128); 456 | let uusd_1 = Asset::native("uusd", 69u128); 457 | let uusd_2 = Asset::native("uusd", 420u128); 458 | let uusd_coin = Coin { 459 | denom: String::from("uusd"), 460 | amount: Uint128::new(69), 461 | }; 462 | let astro = Asset::cw20(Addr::unchecked("astro_token"), 69u128); 463 | 464 | assert!(uluna != uusd_coin); 465 | assert!(uusd_coin != uluna); 466 | assert!(uusd_1 == uusd_coin); 467 | assert!(uusd_coin == uusd_1); 468 | assert!(uusd_2 != uusd_coin); 469 | assert!(uusd_coin != uusd_2); 470 | assert!(astro != uusd_coin); 471 | assert!(uusd_coin != astro); 472 | } 473 | 474 | #[test] 475 | fn from_string() { 476 | let s = ""; 477 | assert_eq!( 478 | AssetUnchecked::from_str(s), 479 | Err(AssetError::InvalidAssetType { 480 | ty: "".into(), 481 | }), 482 | ); 483 | 484 | let s = "native:uusd:12345:67890"; 485 | assert_eq!( 486 | AssetUnchecked::from_str(s), 487 | Err(AssetError::InvalidAssetFormat { 488 | received: s.into(), 489 | }), 490 | ); 491 | 492 | let s = "cw721:galactic_punk:1"; 493 | assert_eq!( 494 | AssetUnchecked::from_str(s), 495 | Err(AssetError::InvalidAssetType { 496 | ty: "cw721".into(), 497 | }), 498 | ); 499 | 500 | let s = "native:uusd:ngmi"; 501 | assert_eq!( 502 | AssetUnchecked::from_str(s), 503 | Err(AssetError::InvalidAssetAmount { 504 | amount: "ngmi".into(), 505 | }), 506 | ); 507 | 508 | let s = "native:uusd:12345"; 509 | assert_eq!(AssetUnchecked::from_str(s).unwrap(), AssetUnchecked::native("uusd", 12345u128)); 510 | 511 | let s = "cw20:mock_token:12345"; 512 | assert_eq!( 513 | AssetUnchecked::from_str(s).unwrap(), 514 | AssetUnchecked::cw20("mock_token", 12345u128), 515 | ); 516 | } 517 | 518 | #[test] 519 | fn from_sdk_string() { 520 | let asset = AssetUnchecked::from_sdk_string("12345uatom").unwrap(); 521 | assert_eq!(asset, AssetUnchecked::native("uatom", 12345u128)); 522 | 523 | let asset = AssetUnchecked::from_sdk_string( 524 | "69420ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", 525 | ) 526 | .unwrap(); 527 | assert_eq!( 528 | asset, 529 | AssetUnchecked::native( 530 | "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", 531 | 69420u128 532 | ), 533 | ); 534 | 535 | let asset = AssetUnchecked::from_sdk_string( 536 | "88888factory/osmo1z926ax906k0ycsuckele6x5hh66e2m4m6ry7dn", 537 | ) 538 | .unwrap(); 539 | assert_eq!( 540 | asset, 541 | AssetUnchecked::native( 542 | "factory/osmo1z926ax906k0ycsuckele6x5hh66e2m4m6ry7dn", 543 | 88888u128 544 | ), 545 | ); 546 | 547 | let err = AssetUnchecked::from_sdk_string("ngmi"); 548 | assert!(err.is_err()); 549 | } 550 | 551 | #[test] 552 | fn to_string() { 553 | let asset = Asset::native("uusd", 69420u128); 554 | assert_eq!(asset.to_string(), String::from("native:uusd:69420")); 555 | 556 | let asset = Asset::cw20(Addr::unchecked("mock_token"), 88888u128); 557 | assert_eq!(asset.to_string(), String::from("cw20:mock_token:88888")); 558 | } 559 | 560 | #[test] 561 | fn checking() { 562 | let api = MockApi::default(); 563 | let token_addr = api.addr_make("mock_token"); 564 | 565 | let checked = Asset::cw20(token_addr, 12345u128); 566 | let unchecked: AssetUnchecked = checked.clone().into(); 567 | assert_eq!(unchecked.check(&api, None).unwrap(), checked); 568 | 569 | let checked = Asset::native("uusd", 12345u128); 570 | let unchecked: AssetUnchecked = checked.clone().into(); 571 | assert_eq!(unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])).unwrap(), checked); 572 | 573 | let unchecked = AssetUnchecked::new(AssetInfoUnchecked::native("uatom"), 12345u128); 574 | assert_eq!( 575 | unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])), 576 | Err(AssetError::UnacceptedDenom { 577 | denom: "uatom".into(), 578 | whitelist: "uusd|uluna|uosmo".into(), 579 | }), 580 | ); 581 | } 582 | 583 | #[test] 584 | fn checking_uppercase() { 585 | let api = MockApi::default(); 586 | let mut token_addr = api.addr_make("mock_token"); 587 | token_addr = Addr::unchecked(token_addr.into_string().to_uppercase()); 588 | 589 | let unchecked = AssetUnchecked::cw20(token_addr, 12345u128); 590 | assert_eq!( 591 | unchecked.check(&api, None).unwrap_err(), 592 | StdError::generic_err("Invalid input: address not normalized").into(), 593 | ); 594 | } 595 | 596 | #[test] 597 | fn creating_messages() { 598 | let token = Asset::cw20(Addr::unchecked("mock_token"), 123456u128); 599 | let coin = Asset::native("uusd", 123456u128); 600 | 601 | let bin_msg = to_json_binary(&MockExecuteMsg::MockCommand {}).unwrap(); 602 | let msg = token.send_msg("mock_contract", bin_msg.clone()).unwrap(); 603 | assert_eq!( 604 | msg, 605 | CosmosMsg::Wasm(WasmMsg::Execute { 606 | contract_addr: String::from("mock_token"), 607 | msg: to_json_binary(&Cw20ExecuteMsg::Send { 608 | contract: String::from("mock_contract"), 609 | amount: Uint128::new(123456), 610 | msg: to_json_binary(&MockExecuteMsg::MockCommand {}).unwrap() 611 | }) 612 | .unwrap(), 613 | funds: vec![] 614 | }) 615 | ); 616 | 617 | let err = coin.send_msg("mock_contract", bin_msg); 618 | assert_eq!( 619 | err, 620 | Err(AssetError::UnavailableMethodForNative { 621 | method: "send".into(), 622 | }), 623 | ); 624 | 625 | let msg = token.transfer_msg("alice").unwrap(); 626 | assert_eq!( 627 | msg, 628 | CosmosMsg::Wasm(WasmMsg::Execute { 629 | contract_addr: String::from("mock_token"), 630 | msg: to_json_binary(&Cw20ExecuteMsg::Transfer { 631 | recipient: String::from("alice"), 632 | amount: Uint128::new(123456) 633 | }) 634 | .unwrap(), 635 | funds: vec![] 636 | }), 637 | ); 638 | 639 | let msg = coin.transfer_msg("alice").unwrap(); 640 | assert_eq!( 641 | msg, 642 | CosmosMsg::Bank(BankMsg::Send { 643 | to_address: String::from("alice"), 644 | amount: vec![Coin::new(123456u128, "uusd")] 645 | }), 646 | ); 647 | 648 | let msg = token.transfer_from_msg("bob", "charlie").unwrap(); 649 | assert_eq!( 650 | msg, 651 | CosmosMsg::Wasm(WasmMsg::Execute { 652 | contract_addr: String::from("mock_token"), 653 | msg: to_json_binary(&Cw20ExecuteMsg::TransferFrom { 654 | owner: String::from("bob"), 655 | recipient: String::from("charlie"), 656 | amount: Uint128::new(123456) 657 | }) 658 | .unwrap(), 659 | funds: vec![] 660 | }), 661 | ); 662 | let err = coin.transfer_from_msg("bob", "charlie"); 663 | assert_eq!( 664 | err, 665 | Err(AssetError::UnavailableMethodForNative { 666 | method: "transfer_from".into(), 667 | }), 668 | ); 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /src/asset_info.rs: -------------------------------------------------------------------------------- 1 | use std::{any::type_name, fmt, str::FromStr}; 2 | 3 | use cosmwasm_schema::cw_serde; 4 | use cosmwasm_std::{ 5 | to_json_binary, Addr, Api, BalanceResponse, BankQuery, QuerierWrapper, QueryRequest, StdError, 6 | StdResult, Uint128, WasmQuery, 7 | }; 8 | use cw20::{BalanceResponse as Cw20BalanceResponse, Cw20QueryMsg}; 9 | use cw_address_like::AddressLike; 10 | use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey}; 11 | 12 | use crate::AssetError; 13 | 14 | /// Represents the type of an fungible asset. 15 | /// 16 | /// Each **asset info** instance can be one of three variants: 17 | /// 18 | /// - Native SDK coins. To create an **asset info** instance of this type, 19 | /// provide the denomination. 20 | /// - CW20 tokens. To create an **asset info** instance of this type, provide 21 | /// the contract address. 22 | #[cw_serde] 23 | #[derive(Eq, PartialOrd, Ord, Hash)] 24 | #[non_exhaustive] 25 | pub enum AssetInfoBase { 26 | Native(String), 27 | Cw20(T), 28 | } 29 | 30 | impl AssetInfoBase { 31 | /// Create an **asset info** instance of the _native_ variant by providing 32 | /// the coin's denomination. 33 | /// 34 | /// ```rust 35 | /// use cw_asset::AssetInfo; 36 | /// 37 | /// let info = AssetInfo::native("uusd"); 38 | /// ``` 39 | pub fn native>(denom: A) -> Self { 40 | AssetInfoBase::Native(denom.into()) 41 | } 42 | 43 | /// Create an **asset info** instance of the _CW20_ variant 44 | /// 45 | /// ```rust 46 | /// use cosmwasm_std::Addr; 47 | /// use cw_asset::AssetInfo; 48 | /// 49 | /// let info = AssetInfo::cw20(Addr::unchecked("token_addr")); 50 | /// ``` 51 | pub fn cw20>(contract_addr: A) -> Self { 52 | AssetInfoBase::Cw20(contract_addr.into()) 53 | } 54 | } 55 | 56 | /// Represents an **asset info** instance that may contain unverified data; to 57 | /// be used in messages. 58 | pub type AssetInfoUnchecked = AssetInfoBase; 59 | 60 | /// Represents an **asset info** instance containing only verified data; to be 61 | /// saved in contract storage. 62 | pub type AssetInfo = AssetInfoBase; 63 | 64 | impl AssetInfo { 65 | /// Return the `denom` or `addr` wrapped within [AssetInfo] 66 | pub fn inner(&self) -> String { 67 | match self { 68 | AssetInfoBase::Native(denom) => denom.clone(), 69 | AssetInfoBase::Cw20(addr) => addr.into(), 70 | } 71 | } 72 | } 73 | 74 | impl FromStr for AssetInfoUnchecked { 75 | type Err = AssetError; 76 | 77 | fn from_str(s: &str) -> Result { 78 | let words: Vec<&str> = s.split(':').collect(); 79 | 80 | match words[0] { 81 | "native" => { 82 | if words.len() != 2 { 83 | return Err(AssetError::InvalidAssetInfoFormat { 84 | received: s.into(), 85 | should_be: "native:{denom}".into(), 86 | }); 87 | } 88 | Ok(AssetInfoUnchecked::Native(String::from(words[1]))) 89 | }, 90 | "cw20" => { 91 | if words.len() != 2 { 92 | return Err(AssetError::InvalidAssetInfoFormat { 93 | received: s.into(), 94 | should_be: "cw20:{contract_addr}".into(), 95 | }); 96 | } 97 | Ok(AssetInfoUnchecked::Cw20(String::from(words[1]))) 98 | }, 99 | ty => Err(AssetError::InvalidAssetType { 100 | ty: ty.into(), 101 | }), 102 | } 103 | } 104 | } 105 | 106 | impl From for AssetInfoUnchecked { 107 | fn from(asset_info: AssetInfo) -> Self { 108 | match asset_info { 109 | AssetInfo::Cw20(contract_addr) => AssetInfoUnchecked::Cw20(contract_addr.into()), 110 | AssetInfo::Native(denom) => AssetInfoUnchecked::Native(denom), 111 | } 112 | } 113 | } 114 | 115 | impl From<&AssetInfo> for AssetInfoUnchecked { 116 | fn from(asset_info: &AssetInfo) -> Self { 117 | match asset_info { 118 | AssetInfo::Cw20(contract_addr) => AssetInfoUnchecked::Cw20(contract_addr.into()), 119 | AssetInfo::Native(denom) => AssetInfoUnchecked::Native(denom.into()), 120 | } 121 | } 122 | } 123 | 124 | impl AssetInfoUnchecked { 125 | /// Validate data contained in an _unchecked_ **asset info** instance; 126 | /// return a new _checked_ **asset info** instance: 127 | /// 128 | /// - For CW20 tokens, assert the contract address is valid; 129 | /// - For SDK coins, assert that the denom is included in a given whitelist; 130 | /// skip if the whitelist is not provided. 131 | /// 132 | /// 133 | /// ```rust 134 | /// use cosmwasm_std::{Addr, Api, StdResult}; 135 | /// use cw_asset::{AssetInfo, AssetInfoUnchecked}; 136 | /// 137 | /// fn validate_asset_info(api: &dyn Api, info_unchecked: &AssetInfoUnchecked) { 138 | /// match info_unchecked.check(api, Some(&["uatom", "uluna"])) { 139 | /// Ok(info) => println!("asset info is valid: {}", info.to_string()), 140 | /// Err(err) => println!("asset is invalid! reason: {}", err), 141 | /// } 142 | /// } 143 | /// ``` 144 | pub fn check( 145 | &self, 146 | api: &dyn Api, 147 | optional_whitelist: Option<&[&str]>, 148 | ) -> Result { 149 | match self { 150 | AssetInfoUnchecked::Native(denom) => { 151 | if let Some(whitelist) = optional_whitelist { 152 | if !whitelist.contains(&&denom[..]) { 153 | return Err(AssetError::UnacceptedDenom { 154 | denom: denom.clone(), 155 | whitelist: whitelist.join("|"), 156 | }); 157 | } 158 | } 159 | Ok(AssetInfo::Native(denom.clone())) 160 | }, 161 | AssetInfoUnchecked::Cw20(contract_addr) => { 162 | Ok(AssetInfo::Cw20(api.addr_validate(contract_addr)?)) 163 | }, 164 | } 165 | } 166 | } 167 | 168 | impl fmt::Display for AssetInfo { 169 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 170 | match self { 171 | AssetInfo::Cw20(contract_addr) => write!(f, "cw20:{contract_addr}"), 172 | AssetInfo::Native(denom) => write!(f, "native:{denom}"), 173 | } 174 | } 175 | } 176 | 177 | impl AssetInfo { 178 | /// Query an address' balance of the asset 179 | /// 180 | /// ```rust 181 | /// use cosmwasm_std::{Addr, Deps, Uint128}; 182 | /// use cw_asset::{AssetError, AssetInfo}; 183 | /// 184 | /// fn query_uusd_balance(deps: Deps, account_addr: &Addr) -> Result { 185 | /// let info = AssetInfo::native("uusd"); 186 | /// info.query_balance(&deps.querier, "account_addr") 187 | /// } 188 | /// ``` 189 | pub fn query_balance>( 190 | &self, 191 | querier: &QuerierWrapper, 192 | address: T, 193 | ) -> Result { 194 | match self { 195 | AssetInfo::Native(denom) => { 196 | let response: BalanceResponse = 197 | querier.query(&QueryRequest::Bank(BankQuery::Balance { 198 | address: address.into(), 199 | denom: denom.clone(), 200 | }))?; 201 | Ok(response.amount.amount) 202 | }, 203 | AssetInfo::Cw20(contract_addr) => { 204 | let response: Cw20BalanceResponse = 205 | querier.query(&QueryRequest::Wasm(WasmQuery::Smart { 206 | contract_addr: contract_addr.into(), 207 | msg: to_json_binary(&Cw20QueryMsg::Balance { 208 | address: address.into(), 209 | })?, 210 | }))?; 211 | Ok(response.balance) 212 | }, 213 | } 214 | } 215 | 216 | /// Implemented as private function to prevent from_str from being called on AssetInfo 217 | fn from_str(s: &str) -> Result { 218 | let words: Vec<&str> = s.split(':').collect(); 219 | 220 | match words[0] { 221 | "native" => { 222 | if words.len() != 2 { 223 | return Err(AssetError::InvalidAssetInfoFormat { 224 | received: s.into(), 225 | should_be: "native:{denom}".into(), 226 | }); 227 | } 228 | Ok(AssetInfo::Native(String::from(words[1]))) 229 | }, 230 | "cw20" => { 231 | if words.len() != 2 { 232 | return Err(AssetError::InvalidAssetInfoFormat { 233 | received: s.into(), 234 | should_be: "cw20:{contract_addr}".into(), 235 | }); 236 | } 237 | Ok(AssetInfo::Cw20(Addr::unchecked(words[1]))) 238 | }, 239 | ty => Err(AssetError::InvalidAssetType { 240 | ty: ty.into(), 241 | }), 242 | } 243 | } 244 | } 245 | 246 | impl<'a> PrimaryKey<'a> for &AssetInfo { 247 | type Prefix = String; 248 | type SubPrefix = (); 249 | type Suffix = String; 250 | type SuperSuffix = Self; 251 | 252 | fn key(&self) -> Vec { 253 | let mut keys = vec![]; 254 | match &self { 255 | AssetInfo::Cw20(addr) => { 256 | keys.extend("cw20:".key()); 257 | keys.extend(addr.key()); 258 | }, 259 | AssetInfo::Native(denom) => { 260 | keys.extend("native:".key()); 261 | keys.extend(denom.key()); 262 | }, 263 | }; 264 | keys 265 | } 266 | } 267 | 268 | impl KeyDeserialize for &AssetInfo { 269 | const KEY_ELEMS: u16 = 1; 270 | 271 | type Output = AssetInfo; 272 | 273 | #[inline(always)] 274 | fn from_vec(mut value: Vec) -> StdResult { 275 | // ignore length prefix 276 | // we're allowed to do this because we set the key's namespace ourselves 277 | // in PrimaryKey (first key) 278 | value.drain(0..2); 279 | 280 | // parse the bytes into an utf8 string 281 | let s = String::from_utf8(value)?; 282 | 283 | // cast the AssetError to StdError::ParseError 284 | AssetInfo::from_str(&s).map_err(|err| StdError::parse_err(type_name::(), err)) 285 | } 286 | } 287 | 288 | impl<'a> Prefixer<'a> for &AssetInfo { 289 | fn prefix(&self) -> Vec { 290 | self.key() 291 | } 292 | } 293 | 294 | //------------------------------------------------------------------------------ 295 | // Tests 296 | //------------------------------------------------------------------------------ 297 | 298 | #[cfg(test)] 299 | mod test { 300 | use std::collections::{BTreeMap, HashMap}; 301 | 302 | use cosmwasm_std::{testing::MockApi, Coin}; 303 | 304 | use super::{super::testing::mock_dependencies, *}; 305 | 306 | #[test] 307 | fn creating_instances() { 308 | let info = AssetInfo::cw20(Addr::unchecked("mock_token")); 309 | assert_eq!(info, AssetInfo::Cw20(Addr::unchecked("mock_token"))); 310 | 311 | let info = AssetInfo::native("uusd"); 312 | assert_eq!(info, AssetInfo::Native(String::from("uusd"))); 313 | } 314 | 315 | #[test] 316 | fn comparing() { 317 | let uluna = AssetInfo::native("uluna"); 318 | let uusd = AssetInfo::native("uusd"); 319 | let astro = AssetInfo::cw20(Addr::unchecked("astro_token")); 320 | let mars = AssetInfo::cw20(Addr::unchecked("mars_token")); 321 | 322 | assert!(uluna != uusd); 323 | assert!(uluna != astro); 324 | assert!(astro != mars); 325 | assert!(uluna == uluna.clone()); 326 | assert!(astro == astro.clone()); 327 | } 328 | 329 | #[test] 330 | fn from_string() { 331 | let s = ""; 332 | assert_eq!( 333 | AssetInfoUnchecked::from_str(s), 334 | Err(AssetError::InvalidAssetType { 335 | ty: "".into() 336 | }), 337 | ); 338 | 339 | let s = "native:uusd:12345"; 340 | assert_eq!( 341 | AssetInfoUnchecked::from_str(s), 342 | Err(AssetError::InvalidAssetInfoFormat { 343 | received: s.into(), 344 | should_be: "native:{denom}".into(), 345 | }), 346 | ); 347 | 348 | let s = "cw721:galactic_punk"; 349 | assert_eq!( 350 | AssetInfoUnchecked::from_str(s), 351 | Err(AssetError::InvalidAssetType { 352 | ty: "cw721".into(), 353 | }) 354 | ); 355 | 356 | let s = "native:uusd"; 357 | assert_eq!(AssetInfoUnchecked::from_str(s).unwrap(), AssetInfoUnchecked::native("uusd"),); 358 | 359 | let s = "cw20:mock_token"; 360 | assert_eq!( 361 | AssetInfoUnchecked::from_str(s).unwrap(), 362 | AssetInfoUnchecked::cw20("mock_token"), 363 | ); 364 | } 365 | 366 | #[test] 367 | fn to_string() { 368 | let info = AssetInfo::native("uusd"); 369 | assert_eq!(info.to_string(), String::from("native:uusd")); 370 | 371 | let info = AssetInfo::cw20(Addr::unchecked("mock_token")); 372 | assert_eq!(info.to_string(), String::from("cw20:mock_token")); 373 | } 374 | 375 | #[test] 376 | fn checking() { 377 | let api = MockApi::default(); 378 | let token_addr = api.addr_make("mock_token"); 379 | 380 | let checked = AssetInfo::cw20(token_addr); 381 | let unchecked: AssetInfoUnchecked = checked.clone().into(); 382 | assert_eq!(unchecked.check(&api, None).unwrap(), checked); 383 | 384 | let checked = AssetInfo::native("uusd"); 385 | let unchecked: AssetInfoUnchecked = checked.clone().into(); 386 | assert_eq!(unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])).unwrap(), checked); 387 | 388 | let unchecked = AssetInfoUnchecked::native("uatom"); 389 | assert_eq!( 390 | unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])), 391 | Err(AssetError::UnacceptedDenom { 392 | denom: "uatom".into(), 393 | whitelist: "uusd|uluna|uosmo".into(), 394 | }), 395 | ); 396 | } 397 | 398 | #[test] 399 | fn checking_uppercase() { 400 | let api = MockApi::default(); 401 | let mut token_addr = api.addr_make("mock_token"); 402 | token_addr = Addr::unchecked(token_addr.into_string().to_uppercase()); 403 | 404 | let unchecked = AssetInfoUnchecked::cw20(token_addr); 405 | assert_eq!( 406 | unchecked.check(&api, None).unwrap_err(), 407 | StdError::generic_err("Invalid input: address not normalized").into(), 408 | ); 409 | } 410 | 411 | #[test] 412 | fn querying_balance() { 413 | let mut deps = mock_dependencies(); 414 | deps.querier.set_base_balances("alice", &[Coin::new(12345u128, "uusd")]); 415 | deps.querier.set_cw20_balance("mock_token", "bob", 67890); 416 | 417 | let info1 = AssetInfo::native("uusd"); 418 | let balance1 = info1.query_balance(&deps.as_ref().querier, "alice").unwrap(); 419 | assert_eq!(balance1, Uint128::new(12345)); 420 | 421 | let info2 = AssetInfo::cw20(Addr::unchecked("mock_token")); 422 | let balance2 = info2.query_balance(&deps.as_ref().querier, "bob").unwrap(); 423 | assert_eq!(balance2, Uint128::new(67890)); 424 | } 425 | 426 | use cosmwasm_std::{Addr, Order}; 427 | use cw_storage_plus::{Bound, Map}; 428 | 429 | fn mock_key() -> AssetInfo { 430 | AssetInfo::native("uusd") 431 | } 432 | 433 | fn mock_keys() -> (AssetInfo, AssetInfo, AssetInfo) { 434 | ( 435 | AssetInfo::native("uusd"), 436 | AssetInfo::cw20(Addr::unchecked("mock_token")), 437 | AssetInfo::cw20(Addr::unchecked("mock_token2")), 438 | ) 439 | } 440 | 441 | #[test] 442 | fn storage_key_works() { 443 | let mut deps = mock_dependencies(); 444 | let key = mock_key(); 445 | let map: Map<&AssetInfo, u64> = Map::new("map"); 446 | 447 | map.save(deps.as_mut().storage, &key, &42069).unwrap(); 448 | 449 | assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069); 450 | 451 | let items = map 452 | .range(deps.as_ref().storage, None, None, Order::Ascending) 453 | .map(|item| item.unwrap()) 454 | .collect::>(); 455 | 456 | assert_eq!(items.len(), 1); 457 | assert_eq!(items[0], (key, 42069)); 458 | } 459 | 460 | #[test] 461 | fn composite_key_works() { 462 | let mut deps = mock_dependencies(); 463 | let key = mock_key(); 464 | let map: Map<(&AssetInfo, Addr), u64> = Map::new("map"); 465 | 466 | map.save(deps.as_mut().storage, (&key, Addr::unchecked("larry")), &42069).unwrap(); 467 | 468 | map.save(deps.as_mut().storage, (&key, Addr::unchecked("jake")), &69420).unwrap(); 469 | 470 | let items = map 471 | .prefix(&key) 472 | .range(deps.as_ref().storage, None, None, Order::Ascending) 473 | .map(|item| item.unwrap()) 474 | .collect::>(); 475 | 476 | assert_eq!(items.len(), 2); 477 | assert_eq!(items[0], (Addr::unchecked("jake"), 69420)); 478 | assert_eq!(items[1], (Addr::unchecked("larry"), 42069)); 479 | } 480 | 481 | #[test] 482 | fn triple_asset_key_works() { 483 | let mut deps = mock_dependencies(); 484 | let map: Map<(&AssetInfo, &AssetInfo, &AssetInfo), u64> = Map::new("map"); 485 | 486 | let (key1, key2, key3) = mock_keys(); 487 | map.save(deps.as_mut().storage, (&key1, &key2, &key3), &42069).unwrap(); 488 | map.save(deps.as_mut().storage, (&key1, &key1, &key2), &11).unwrap(); 489 | map.save(deps.as_mut().storage, (&key1, &key1, &key3), &69420).unwrap(); 490 | 491 | let items = map 492 | .prefix((&key1, &key1)) 493 | .range(deps.as_ref().storage, None, None, Order::Ascending) 494 | .map(|item| item.unwrap()) 495 | .collect::>(); 496 | assert_eq!(items.len(), 2); 497 | assert_eq!(items[1], (key3.clone(), 69420)); 498 | assert_eq!(items[0], (key2.clone(), 11)); 499 | 500 | let val1 = map.load(deps.as_ref().storage, (&key1, &key2, &key3)).unwrap(); 501 | assert_eq!(val1, 42069); 502 | } 503 | 504 | #[test] 505 | fn std_maps_asset_info() { 506 | let mut map: HashMap = HashMap::new(); 507 | 508 | let asset_cw20 = AssetInfo::cw20(Addr::unchecked("cosmwasm1")); 509 | let asset_native = AssetInfo::native(Addr::unchecked("native1")); 510 | let asset_fake_native = AssetInfo::native(Addr::unchecked("cosmwasm1")); 511 | 512 | map.insert(asset_cw20.clone(), 1); 513 | map.insert(asset_native.clone(), 2); 514 | map.insert(asset_fake_native.clone(), 3); 515 | 516 | assert_eq!(&1, map.get(&asset_cw20).unwrap()); 517 | assert_eq!(&2, map.get(&asset_native).unwrap()); 518 | assert_eq!(&3, map.get(&asset_fake_native).unwrap()); 519 | 520 | let mut map: BTreeMap = BTreeMap::new(); 521 | 522 | map.insert(asset_cw20.clone(), 1); 523 | map.insert(asset_native.clone(), 2); 524 | map.insert(asset_fake_native.clone(), 3); 525 | 526 | assert_eq!(&1, map.get(&asset_cw20).unwrap()); 527 | assert_eq!(&2, map.get(&asset_native).unwrap()); 528 | assert_eq!(&3, map.get(&asset_fake_native).unwrap()); 529 | } 530 | 531 | #[test] 532 | fn inner() { 533 | assert_eq!(AssetInfo::native("denom").inner(), "denom".to_string()); 534 | assert_eq!(AssetInfo::cw20(Addr::unchecked("addr")).inner(), "addr".to_string()) 535 | } 536 | 537 | #[test] 538 | fn prefix() { 539 | let mut deps = mock_dependencies(); 540 | let map: Map<&AssetInfo, u64> = Map::new("map"); 541 | 542 | let asset_cw20_1 = AssetInfo::cw20(Addr::unchecked("cosmwasm1")); 543 | let asset_cw20_2 = AssetInfo::cw20(Addr::unchecked("cosmwasm2")); 544 | let asset_cw20_3 = AssetInfo::cw20(Addr::unchecked("cosmwasm3")); 545 | 546 | let asset_native_1 = AssetInfo::native(Addr::unchecked("native1")); 547 | let asset_native_2 = AssetInfo::native(Addr::unchecked("native2")); 548 | let asset_native_3 = AssetInfo::native(Addr::unchecked("native3")); 549 | 550 | map.save(deps.as_mut().storage, &asset_cw20_1, &1).unwrap(); 551 | map.save(deps.as_mut().storage, &asset_cw20_3, &3).unwrap(); 552 | map.save(deps.as_mut().storage, &asset_cw20_2, &2).unwrap(); 553 | 554 | map.save(deps.as_mut().storage, &asset_native_2, &20).unwrap(); 555 | map.save(deps.as_mut().storage, &asset_native_3, &30).unwrap(); 556 | map.save(deps.as_mut().storage, &asset_native_1, &10).unwrap(); 557 | 558 | // --- Ascending --- 559 | 560 | // no bound 561 | let cw20_ascending = map 562 | .prefix("cw20:".to_string()) 563 | .range(deps.as_ref().storage, None, None, Order::Ascending) 564 | .collect::>>() 565 | .unwrap(); 566 | 567 | assert_eq!( 568 | vec![ 569 | ("cosmwasm1".to_string(), 1), 570 | ("cosmwasm2".to_string(), 2), 571 | ("cosmwasm3".to_string(), 3) 572 | ], 573 | cw20_ascending 574 | ); 575 | 576 | // bound on min 577 | let native_ascending = map 578 | .prefix("native:".to_string()) 579 | .range( 580 | deps.as_ref().storage, 581 | Some(Bound::exclusive(asset_native_1.inner())), 582 | None, 583 | Order::Ascending, 584 | ) 585 | .collect::>>() 586 | .unwrap(); 587 | 588 | assert_eq!( 589 | vec![ 590 | // ("native1".to_string(), 10), - out of bound 591 | ("native2".to_string(), 20), 592 | ("native3".to_string(), 30) 593 | ], 594 | native_ascending 595 | ); 596 | 597 | // --- Descending --- 598 | 599 | // no bound 600 | let cw20_descending = map 601 | .prefix("cw20:".to_string()) 602 | .range(deps.as_ref().storage, None, None, Order::Descending) 603 | .collect::>>() 604 | .unwrap(); 605 | 606 | assert_eq!( 607 | vec![ 608 | ("cosmwasm3".to_string(), 3), 609 | ("cosmwasm2".to_string(), 2), 610 | ("cosmwasm1".to_string(), 1) 611 | ], 612 | cw20_descending 613 | ); 614 | 615 | // bound on max 616 | let native_descending = map 617 | .prefix("native:".to_string()) 618 | .range( 619 | deps.as_ref().storage, 620 | None, 621 | Some(Bound::exclusive(asset_native_3.inner())), 622 | Order::Descending, 623 | ) 624 | .collect::>>() 625 | .unwrap(); 626 | 627 | assert_eq!( 628 | vec![ 629 | // ("native3".to_string(), 30), - out of bound 630 | ("native2".to_string(), 20), 631 | ("native1".to_string(), 10) 632 | ], 633 | native_descending 634 | ); 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /src/asset_list.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, str::FromStr}; 2 | 3 | use cosmwasm_schema::cw_serde; 4 | use cosmwasm_std::{Addr, Api, Coin, CosmosMsg}; 5 | use cw_address_like::AddressLike; 6 | 7 | use crate::{Asset, AssetBase, AssetError, AssetInfo, AssetUnchecked}; 8 | 9 | /// Represents a list of fungible tokens, each with a known amount 10 | #[cw_serde] 11 | pub struct AssetListBase(Vec>); 12 | 13 | #[allow(clippy::derivable_impls)] // clippy says `Default` can be derived here, but actually it can't 14 | impl Default for AssetListBase { 15 | fn default() -> Self { 16 | Self(vec![]) 17 | } 18 | } 19 | 20 | /// Represents an **asset list** instance that may contain unverified data; to 21 | /// be used in messages. 22 | pub type AssetListUnchecked = AssetListBase; 23 | 24 | /// Represents an **asset list** instance containing only verified data; to be 25 | /// used in contract storage. 26 | pub type AssetList = AssetListBase; 27 | 28 | impl FromStr for AssetListUnchecked { 29 | type Err = AssetError; 30 | 31 | fn from_str(s: &str) -> Result { 32 | if s.is_empty() { 33 | return Ok(Self(vec![])); 34 | } 35 | 36 | s.split(',').map(AssetUnchecked::from_str).collect::>().map(Self) 37 | } 38 | } 39 | 40 | impl From for AssetListUnchecked { 41 | fn from(list: AssetList) -> Self { 42 | Self(list.to_vec().iter().cloned().map(|asset| asset.into()).collect()) 43 | } 44 | } 45 | 46 | impl AssetListUnchecked { 47 | /// Validate data contained in an _unchecked_ **asset list** instance, 48 | /// return a new _checked_ **asset list** instance: 49 | /// 50 | /// - For CW20 tokens, assert the contract address is valid 51 | /// - For SDK coins, assert that the denom is included in a given whitelist; skip if the 52 | /// whitelist is not provided 53 | /// 54 | /// ```rust 55 | /// use cosmwasm_std::{Addr, Api, StdResult}; 56 | /// use cw_asset::{Asset, AssetList, AssetListUnchecked, AssetUnchecked}; 57 | /// 58 | /// fn validate_assets(api: &dyn Api, list_unchecked: &AssetListUnchecked) { 59 | /// match list_unchecked.check(api, Some(&["uatom", "uluna"])) { 60 | /// Ok(list) => println!("asset list is valid: {}", list.to_string()), 61 | /// Err(err) => println!("asset list is invalid! reason: {}", err), 62 | /// } 63 | /// } 64 | /// ``` 65 | pub fn check( 66 | &self, 67 | api: &dyn Api, 68 | optional_whitelist: Option<&[&str]>, 69 | ) -> Result { 70 | self.0 71 | .iter() 72 | .map(|asset| asset.check(api, optional_whitelist)) 73 | .collect::, _>>() 74 | .map(AssetList::from) 75 | } 76 | } 77 | 78 | impl fmt::Display for AssetList { 79 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 80 | let s = if self.is_empty() { 81 | "[]".to_string() 82 | } else { 83 | self.0.iter().map(|asset| asset.to_string()).collect::>().join(",") 84 | }; 85 | 86 | write!(f, "{s}") 87 | } 88 | } 89 | 90 | impl std::ops::Index for AssetList { 91 | type Output = Asset; 92 | 93 | fn index(&self, index: usize) -> &Self::Output { 94 | &self.0[index] 95 | } 96 | } 97 | 98 | impl std::ops::Index for &AssetList { 99 | type Output = Asset; 100 | 101 | fn index(&self, index: usize) -> &Self::Output { 102 | &self.0[index] 103 | } 104 | } 105 | 106 | impl<'a> IntoIterator for &'a AssetList { 107 | type Item = &'a Asset; 108 | type IntoIter = std::slice::Iter<'a, Asset>; 109 | 110 | fn into_iter(self) -> Self::IntoIter { 111 | self.0.iter() 112 | } 113 | } 114 | 115 | impl From> for AssetList { 116 | fn from(vec: Vec) -> Self { 117 | Self(vec) 118 | } 119 | } 120 | 121 | impl From<&Vec> for AssetList { 122 | fn from(vec: &Vec) -> Self { 123 | Self(vec.clone()) 124 | } 125 | } 126 | 127 | impl From<&[Asset]> for AssetList { 128 | fn from(vec: &[Asset]) -> Self { 129 | vec.to_vec().into() 130 | } 131 | } 132 | 133 | impl From> for AssetList { 134 | fn from(coins: Vec) -> Self { 135 | (&coins).into() 136 | } 137 | } 138 | 139 | impl From<&Vec> for AssetList { 140 | fn from(coins: &Vec) -> Self { 141 | Self(coins.iter().map(|coin| coin.into()).collect()) 142 | } 143 | } 144 | 145 | impl From<&[Coin]> for AssetList { 146 | fn from(coins: &[Coin]) -> Self { 147 | coins.to_vec().into() 148 | } 149 | } 150 | 151 | impl AssetList { 152 | /// Create a new, empty asset list 153 | /// 154 | /// ```rust 155 | /// use cw_asset::AssetList; 156 | /// 157 | /// let list = AssetList::new(); 158 | /// let len = list.len(); // should be zero 159 | /// ``` 160 | pub fn new() -> Self { 161 | AssetListBase::default() 162 | } 163 | 164 | /// Return a copy of the underlying vector 165 | /// 166 | /// ```rust 167 | /// use cw_asset::{Asset, AssetList}; 168 | /// 169 | /// let list = 170 | /// AssetList::from(vec![Asset::native("uluna", 12345u128), Asset::native("uusd", 67890u128)]); 171 | /// 172 | /// let vec: Vec = list.to_vec(); 173 | /// ``` 174 | pub fn to_vec(&self) -> Vec { 175 | self.0.clone() 176 | } 177 | 178 | /// Return length of the asset list 179 | /// 180 | /// ```rust 181 | /// use cw_asset::{Asset, AssetList}; 182 | /// 183 | /// let list = 184 | /// AssetList::from(vec![Asset::native("uluna", 12345u128), Asset::native("uusd", 67890u128)]); 185 | /// 186 | /// let len = list.len(); // should be two 187 | /// ``` 188 | // NOTE: I do have `is_empty` implemented, but clippy still throws a warnin 189 | // saying I don't have it. Must be a clippy bug... 190 | #[allow(clippy::len_without_is_empty)] 191 | pub fn len(&self) -> usize { 192 | self.0.len() 193 | } 194 | 195 | /// Return whether the asset list is empty 196 | /// 197 | /// ```rust 198 | /// use cw_asset::{Asset, AssetList}; 199 | /// 200 | /// let mut list = AssetList::from(vec![Asset::native("uluna", 12345u128)]); 201 | /// let is_empty = list.is_empty(); // should be `false` 202 | /// 203 | /// list.deduct(&Asset::native("uluna", 12345u128)).unwrap(); 204 | /// let is_empty = list.is_empty(); // should be `true` 205 | /// ``` 206 | pub fn is_empty(&self) -> bool { 207 | self.0.is_empty() 208 | } 209 | 210 | /// Find an asset in the list that matches the provided asset info 211 | /// 212 | /// Return `Some(&asset)` if found, where `&asset` is a reference to the 213 | /// asset found; `None` if not found. 214 | /// 215 | /// A case where is method is useful is to find how much asset the user sent 216 | /// along with a message: 217 | /// 218 | /// ```rust 219 | /// use cosmwasm_std::MessageInfo; 220 | /// use cw_asset::{AssetInfo, AssetList}; 221 | /// 222 | /// fn find_uusd_received_amount(info: &MessageInfo) { 223 | /// let list = AssetList::from(&info.funds); 224 | /// match list.find(&AssetInfo::native("uusd")) { 225 | /// Some(asset) => println!("received {} uusd", asset.amount), 226 | /// None => println!("did not receive any uusd"), 227 | /// } 228 | /// } 229 | /// ``` 230 | pub fn find(&self, info: &AssetInfo) -> Option<&Asset> { 231 | self.0.iter().find(|asset| asset.info == *info) 232 | } 233 | 234 | /// Apply a mutation on each of the asset 235 | /// 236 | /// An example case where this is useful is to scale the amount of each 237 | /// asset in the list by a certain factor: 238 | /// 239 | /// ```rust 240 | /// use cw_asset::{Asset, AssetInfo, AssetList}; 241 | /// 242 | /// let mut list = 243 | /// AssetList::from(vec![Asset::native("uluna", 12345u128), Asset::native("uusd", 67890u128)]); 244 | /// 245 | /// let list_halved = list.apply(|a| a.amount = a.amount.multiply_ratio(1u128, 2u128)); 246 | /// ``` 247 | pub fn apply(&mut self, f: F) -> &mut Self { 248 | self.0.iter_mut().for_each(f); 249 | self 250 | } 251 | 252 | /// Removes all assets in the list that has zero amount 253 | /// 254 | /// ```rust 255 | /// use cw_asset::{Asset, AssetList}; 256 | /// 257 | /// let mut list = 258 | /// AssetList::from(vec![Asset::native("uluna", 12345u128), Asset::native("uusd", 0u128)]); 259 | /// let mut len = list.len(); // should be two 260 | /// 261 | /// list.purge(); 262 | /// len = list.len(); // should be one 263 | /// ``` 264 | pub fn purge(&mut self) -> &mut Self { 265 | self.0.retain(|asset| !asset.amount.is_zero()); 266 | self 267 | } 268 | 269 | /// Add a new asset to the list 270 | /// 271 | /// If asset of the same kind already exists in the list, then increment its 272 | /// amount; if not, append to the end of the list. 273 | /// 274 | /// NOTE: `purge` is automatically performed following the addition, so 275 | /// adding an asset with zero amount has no effect. 276 | /// 277 | /// ```rust 278 | /// use cw_asset::{Asset, AssetInfo, AssetList}; 279 | /// 280 | /// let mut list = AssetList::from(vec![ 281 | /// Asset::native("uluna", 12345u128), 282 | /// ]); 283 | /// 284 | /// list.add(&Asset::native("uusd", 67890u128)); 285 | /// let mut len = list.len(); // should be two 286 | /// 287 | /// list.add(&Asset::native("uluna", 11111u128)); 288 | /// len = list.len(); // should still be two 289 | /// 290 | /// let uluna_amount = list 291 | /// .find(&AssetInfo::native("uluna")) 292 | /// .unwrap() 293 | /// .amount; // should have increased to 23456 294 | /// ``` 295 | pub fn add(&mut self, asset_to_add: &Asset) -> Result<&mut Self, AssetError> { 296 | match self.0.iter_mut().find(|asset| asset.info == asset_to_add.info) { 297 | Some(asset) => { 298 | asset.amount = asset.amount.checked_add(asset_to_add.amount)?; 299 | }, 300 | None => { 301 | self.0.push(asset_to_add.clone()); 302 | }, 303 | } 304 | Ok(self.purge()) 305 | } 306 | 307 | /// Add multiple new assets to the list 308 | /// 309 | /// ```rust 310 | /// use cw_asset::{Asset, AssetInfo, AssetList}; 311 | /// 312 | /// let mut list = AssetList::from(vec![ 313 | /// Asset::native("uluna", 12345u128), 314 | /// ]); 315 | /// 316 | /// list.add_many(&AssetList::from(vec![ 317 | /// Asset::native("uusd", 67890u128), 318 | /// Asset::native("uluna", 11111u128), 319 | /// ])); 320 | /// 321 | /// let uusd_amount = list 322 | /// .find(&AssetInfo::native("uusd")) 323 | /// .unwrap() 324 | /// .amount; // should be 67890 325 | /// 326 | /// let uluna_amount = list 327 | /// .find(&AssetInfo::native("uluna")) 328 | /// .unwrap() 329 | /// .amount; // should have increased to 23456 330 | /// ``` 331 | pub fn add_many(&mut self, assets_to_add: &AssetList) -> Result<&mut Self, AssetError> { 332 | for asset in &assets_to_add.0 { 333 | self.add(asset)?; 334 | } 335 | Ok(self) 336 | } 337 | 338 | /// Deduct an asset from the list 339 | /// 340 | /// The asset of the same kind and equal or greater amount must already 341 | /// exist in the list. If so, deduct the amount from the asset; ifnot, throw 342 | /// an error. 343 | /// 344 | /// NOTE: `purge` is automatically performed following the addition. 345 | /// Therefore, if an asset's amount is reduced to zero, it will be removed 346 | /// from the list. 347 | /// 348 | /// ``` 349 | /// use cw_asset::{Asset, AssetInfo, AssetList}; 350 | /// 351 | /// let mut list = AssetList::from(vec![ 352 | /// Asset::native("uluna", 12345u128), 353 | /// ]); 354 | /// 355 | /// list.deduct(&Asset::native("uluna", 10000u128)).unwrap(); 356 | /// 357 | /// let uluna_amount = list 358 | /// .find(&AssetInfo::native("uluna")) 359 | /// .unwrap() 360 | /// .amount; // should have reduced to 2345 361 | /// 362 | /// list.deduct(&Asset::native("uluna", 2345u128)); 363 | /// 364 | /// let len = list.len(); // should be zero, as uluna is purged from the list 365 | /// ``` 366 | pub fn deduct(&mut self, asset_to_deduct: &Asset) -> Result<&mut Self, AssetError> { 367 | match self.0.iter_mut().find(|asset| asset.info == asset_to_deduct.info) { 368 | Some(asset) => { 369 | asset.amount = asset.amount.checked_sub(asset_to_deduct.amount)?; 370 | }, 371 | None => { 372 | return Err(AssetError::NotFoundInList { 373 | info: asset_to_deduct.info.to_string(), 374 | }); 375 | }, 376 | } 377 | Ok(self.purge()) 378 | } 379 | 380 | /// Deduct multiple assets from the list 381 | /// 382 | /// ```rust 383 | /// use cw_asset::{Asset, AssetInfo, AssetList}; 384 | /// 385 | /// let mut list = AssetList::from(vec![ 386 | /// Asset::native("uluna", 12345u128), 387 | /// Asset::native("uusd", 67890u128), 388 | /// ]); 389 | /// 390 | /// list.deduct_many(&AssetList::from(vec![ 391 | /// Asset::native("uluna", 2345u128), 392 | /// Asset::native("uusd", 67890u128), 393 | /// ])).unwrap(); 394 | /// 395 | /// let uluna_amount = list 396 | /// .find(&AssetInfo::native("uluna")) 397 | /// .unwrap() 398 | /// .amount; // should have reduced to 2345 399 | /// 400 | /// let len = list.len(); // should be zero, as uusd is purged from the list 401 | /// ``` 402 | pub fn deduct_many(&mut self, assets_to_deduct: &AssetList) -> Result<&mut Self, AssetError> { 403 | for asset in &assets_to_deduct.0 { 404 | self.deduct(asset)?; 405 | } 406 | Ok(self) 407 | } 408 | 409 | /// Generate a transfer messages for every asset in the list 410 | /// 411 | /// ```rust 412 | /// use cosmwasm_std::{Addr, Response}; 413 | /// use cw_asset::{AssetError, AssetList}; 414 | /// 415 | /// fn transfer_assets(list: &AssetList, recipient_addr: &Addr) -> Result { 416 | /// let msgs = list.transfer_msgs(recipient_addr)?; 417 | /// 418 | /// Ok(Response::new().add_messages(msgs).add_attribute("assets_sent", list.to_string())) 419 | /// } 420 | /// ``` 421 | pub fn transfer_msgs + Clone>( 422 | &self, 423 | to: A, 424 | ) -> Result, AssetError> { 425 | self.0.iter().map(|asset| asset.transfer_msg(to.clone())).collect() 426 | } 427 | } 428 | 429 | //------------------------------------------------------------------------------ 430 | // Tests 431 | //------------------------------------------------------------------------------ 432 | 433 | #[cfg(test)] 434 | mod test_helpers { 435 | use super::*; 436 | use crate::Asset; 437 | 438 | pub fn uluna() -> AssetInfo { 439 | AssetInfo::native("uluna") 440 | } 441 | 442 | pub fn uusd() -> AssetInfo { 443 | AssetInfo::native("uusd") 444 | } 445 | 446 | pub fn mock_token() -> AssetInfo { 447 | AssetInfo::cw20(Addr::unchecked("cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q")) 448 | } 449 | 450 | pub fn mock_list() -> AssetList { 451 | AssetList::from(vec![Asset::native("uusd", 69420u128), Asset::new(mock_token(), 88888u128)]) 452 | } 453 | } 454 | 455 | #[cfg(test)] 456 | mod tests { 457 | use cosmwasm_std::{ 458 | testing::MockApi, to_json_binary, BankMsg, Coin, CosmosMsg, OverflowError, 459 | OverflowOperation, StdError, Uint128, WasmMsg, 460 | }; 461 | use cw20::Cw20ExecuteMsg; 462 | 463 | use super::{ 464 | super::asset::{Asset, AssetUnchecked}, 465 | test_helpers::{mock_list, mock_token, uluna, uusd}, 466 | *, 467 | }; 468 | 469 | #[test] 470 | fn from_string() { 471 | let s = ""; 472 | assert_eq!(AssetListUnchecked::from_str(s).unwrap(), AssetListBase::(vec![])); 473 | 474 | let s = "native:uusd:69420,cw20:mock_token"; 475 | assert_eq!( 476 | AssetListUnchecked::from_str(s), 477 | Err(AssetError::InvalidAssetFormat { 478 | received: "cw20:mock_token".into(), 479 | }), 480 | ); 481 | 482 | let s = "native:uusd:69420,cw721:galactic_punk:1"; 483 | assert_eq!( 484 | AssetListUnchecked::from_str(s), 485 | Err(AssetError::InvalidAssetType { 486 | ty: "cw721".into(), 487 | }), 488 | ); 489 | 490 | let s = "native:uusd:69420,cw20:mock_token:ngmi"; 491 | assert_eq!( 492 | AssetListUnchecked::from_str(s), 493 | Err(AssetError::InvalidAssetAmount { 494 | amount: "ngmi".into(), 495 | }), 496 | ); 497 | 498 | let s = "native:uusd:69420,cw20:cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q:88888"; 499 | assert_eq!(AssetListUnchecked::from_str(s).unwrap(), AssetListUnchecked::from(mock_list())); 500 | } 501 | 502 | #[test] 503 | fn to_string() { 504 | let list = mock_list(); 505 | assert_eq!( 506 | list.to_string(), 507 | String::from( 508 | "native:uusd:69420,cw20:cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q:88888" 509 | ) 510 | ); 511 | 512 | let list = AssetList::from(vec![] as Vec); 513 | assert_eq!(list.to_string(), String::from("[]")); 514 | } 515 | 516 | #[test] 517 | fn indexing() { 518 | let list = mock_list(); 519 | let vec = list.to_vec(); 520 | assert_eq!(list[0], vec[0]); 521 | assert_eq!(list[1], vec[1]); 522 | } 523 | 524 | #[test] 525 | fn iterating() { 526 | let list = mock_list(); 527 | 528 | let strs: Vec = list.into_iter().map(|asset| asset.to_string()).collect(); 529 | assert_eq!( 530 | strs, 531 | vec![ 532 | String::from("native:uusd:69420"), 533 | String::from("cw20:cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q:88888"), 534 | ] 535 | ); 536 | } 537 | 538 | #[test] 539 | fn checking() { 540 | let api = MockApi::default().with_prefix("cosmos"); 541 | 542 | let checked = mock_list(); 543 | let unchecked: AssetListUnchecked = checked.clone().into(); 544 | assert_eq!(unchecked.check(&api, None).unwrap(), checked); 545 | assert_eq!(unchecked.check(&api, Some(&["uusd", "uluna"])).unwrap(), checked); 546 | assert_eq!( 547 | unchecked.check(&api, Some(&["uatom", "uosmo", "uscrt"])), 548 | Err(AssetError::UnacceptedDenom { 549 | denom: "uusd".into(), 550 | whitelist: "uatom|uosmo|uscrt".into(), 551 | }), 552 | ); 553 | } 554 | 555 | #[test] 556 | fn checking_uppercase() { 557 | let api = MockApi::default(); 558 | let mut token_addr = api.addr_make("mock_token"); 559 | token_addr = Addr::unchecked(token_addr.into_string().to_uppercase()); 560 | 561 | let unchecked = AssetListBase(vec![ 562 | AssetUnchecked::native("uusd", 69420u128), 563 | AssetUnchecked::cw20(token_addr, 88888u128), 564 | ]); 565 | 566 | assert_eq!( 567 | unchecked.check(&api, None).unwrap_err(), 568 | StdError::generic_err("Invalid input: address not normalized").into(), 569 | ); 570 | } 571 | 572 | #[test] 573 | fn finding() { 574 | let list = mock_list(); 575 | 576 | let asset_option = list.find(&uusd()); 577 | assert_eq!(asset_option, Some(&Asset::new(uusd(), 69420u128))); 578 | 579 | let asset_option = list.find(&mock_token()); 580 | assert_eq!(asset_option, Some(&Asset::new(mock_token(), 88888u128))); 581 | } 582 | 583 | #[test] 584 | fn applying() { 585 | let mut list = mock_list(); 586 | 587 | list.apply(|asset: &mut Asset| asset.amount = asset.amount.multiply_ratio(1u128, 2u128)); 588 | assert_eq!( 589 | list, 590 | AssetList::from(vec![ 591 | Asset::native("uusd", 34710u128), 592 | Asset::new(mock_token(), 44444u128) 593 | ]), 594 | ); 595 | } 596 | 597 | #[test] 598 | fn adding() { 599 | let mut list = mock_list(); 600 | 601 | list.add(&Asset::new(uluna(), 12345u128)).unwrap(); 602 | let asset = list.find(&uluna()).unwrap(); 603 | assert_eq!(asset.amount, Uint128::new(12345)); 604 | 605 | list.add(&Asset::new(uusd(), 1u128)).unwrap(); 606 | let asset = list.find(&uusd()).unwrap(); 607 | assert_eq!(asset.amount, Uint128::new(69421)); 608 | } 609 | 610 | #[test] 611 | fn adding_many() { 612 | let mut list = mock_list(); 613 | list.add_many(&mock_list()).unwrap(); 614 | 615 | let expected = mock_list().apply(|a| a.amount *= Uint128::new(2)).clone(); 616 | assert_eq!(list, expected); 617 | } 618 | 619 | #[test] 620 | fn deducting() { 621 | let mut list = mock_list(); 622 | 623 | list.deduct(&Asset::new(uusd(), 12345u128)).unwrap(); 624 | let asset = list.find(&uusd()).unwrap(); 625 | assert_eq!(asset.amount, Uint128::new(57075)); 626 | 627 | list.deduct(&Asset::new(uusd(), 57075u128)).unwrap(); 628 | let asset_option = list.find(&uusd()); 629 | assert_eq!(asset_option, None); 630 | 631 | let err = list.deduct(&Asset::new(uusd(), 57075u128)); 632 | assert_eq!( 633 | err, 634 | Err(AssetError::NotFoundInList { 635 | info: "native:uusd".into(), 636 | }), 637 | ); 638 | 639 | list.deduct(&Asset::new(mock_token(), 12345u128)).unwrap(); 640 | let asset = list.find(&mock_token()).unwrap(); 641 | assert_eq!(asset.amount, Uint128::new(76543)); 642 | 643 | let err = list.deduct(&Asset::new(mock_token(), 99999u128)); 644 | assert_eq!(err, Err(OverflowError::new(OverflowOperation::Sub,).into()),); 645 | } 646 | 647 | #[test] 648 | fn deducting_many() { 649 | let mut list = mock_list(); 650 | list.deduct_many(&mock_list()).unwrap(); 651 | assert_eq!(list, AssetList::new()); 652 | } 653 | 654 | #[test] 655 | fn creating_messages() { 656 | let list = mock_list(); 657 | let msgs = list.transfer_msgs("alice").unwrap(); 658 | assert_eq!( 659 | msgs, 660 | vec![ 661 | CosmosMsg::Bank(BankMsg::Send { 662 | to_address: String::from("alice"), 663 | amount: vec![Coin::new(69420u128, "uusd")] 664 | }), 665 | CosmosMsg::Wasm(WasmMsg::Execute { 666 | contract_addr: String::from("cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q"), 667 | msg: to_json_binary(&Cw20ExecuteMsg::Transfer { 668 | recipient: String::from("alice"), 669 | amount: Uint128::new(88888) 670 | }) 671 | .unwrap(), 672 | funds: vec![] 673 | }), 674 | ], 675 | ); 676 | } 677 | } 678 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{OverflowError, StdError}; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, PartialEq)] 5 | pub enum AssetError { 6 | #[error("std error encountered while handling assets: {0}")] 7 | Std(#[from] StdError), 8 | 9 | #[error("overflow error encountered while handling assets: {0}")] 10 | Overflow(#[from] OverflowError), 11 | 12 | #[error("invalid asset type `{ty}`; must be either `native` or `cw20`")] 13 | InvalidAssetType { 14 | ty: String, 15 | }, 16 | 17 | #[error("invalid asset info `{received}`; must be in the format `{should_be}`")] 18 | InvalidAssetInfoFormat { 19 | /// The incorrect string that was received 20 | received: String, 21 | 22 | /// The correct string format that is expected 23 | should_be: String, 24 | }, 25 | 26 | #[error("invalid asset `{received}`; must be in the format `native:{{denom}}:{{amount}}` or `cw20:{{contract_addr}}:{{amount}}`")] 27 | InvalidAssetFormat { 28 | received: String, 29 | }, 30 | 31 | #[error("invalid asset amount `{amount}`; must be an 128-bit unsigned integer")] 32 | InvalidAssetAmount { 33 | amount: String, 34 | }, 35 | 36 | #[error("failed to parse sdk coin string `{coin_str}`")] 37 | InvalidSdkCoin { 38 | coin_str: String, 39 | }, 40 | 41 | #[error("denom `{denom}` is not in the whitelist; must be `{whitelist}`")] 42 | UnacceptedDenom { 43 | denom: String, 44 | whitelist: String, 45 | }, 46 | 47 | #[error("asset `{info}` is not found in asset list")] 48 | NotFoundInList { 49 | info: String, 50 | }, 51 | 52 | #[error("native coins do not have the `{method}` method")] 53 | UnavailableMethodForNative { 54 | method: String, 55 | }, 56 | 57 | #[error("cannot cast asset {asset} to cosmwasm_std::Coin")] 58 | CannotCastToStdCoin { 59 | asset: String, 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | //! A unified representation of various types of Cosmos fungible assets, and helper functions for 3 | //! interacting with them 4 | //! 5 | //! ## Basic usage 6 | //! 7 | //! The following code generates messages the sends some SDK coins and CW20 tokens to a recipient: 8 | //! 9 | //! ```rust 10 | //! use cosmwasm_std::{Api, Response}; 11 | //! use cw_asset::{Asset, AssetError}; 12 | //! 13 | //! fn transfer_two_assets(api: &dyn Api) -> Result { 14 | //! let asset1 = Asset::native("uusd", 12345u128); 15 | //! let msg1 = asset1.transfer_msg("recipient_addr")?; 16 | //! 17 | //! let asset2 = Asset::cw20(api.addr_validate("token_addr")?, 67890u128); 18 | //! let msg2 = asset1.transfer_msg("recipient_addr")?; 19 | //! 20 | //! Ok(Response::new() 21 | //! .add_message(msg1) 22 | //! .add_message(msg2) 23 | //! .add_attribute("asset_sent", asset1.to_string()) 24 | //! .add_attribute("asset_sent", asset2.to_string())) 25 | //! } 26 | //! ``` 27 | //! 28 | //! ## Asset list 29 | //! 30 | //! An [`AssetList`] struct is also provided for dealing with multiple assets at the same time: 31 | //! 32 | //! ```rust 33 | //! use cosmwasm_std::{Api, Response}; 34 | //! use cw_asset::{Asset, AssetError, AssetList}; 35 | //! 36 | //! fn transfer_multiple_assets(api: &dyn Api) -> Result { 37 | //! let assets = AssetList::from(vec![ 38 | //! Asset::native("uusd", 12345u128), 39 | //! Asset::cw20(api.addr_validate("token_addr")?, 67890u128), 40 | //! ]); 41 | //! 42 | //! let msgs = assets.transfer_msgs(api.addr_validate("recipient_addr")?)?; 43 | //! 44 | //! Ok(Response::new().add_messages(msgs).add_attribute("assets_sent", assets.to_string())) 45 | //! } 46 | //! ``` 47 | //! 48 | //! ## Use in messages 49 | //! 50 | //! [`Asset`] and [`AssetList`] each comes with an _unchecked_ counterpart which contains unverified 51 | //! addresses and/or denoms, and implements traits that allow them to be serialized into JSON, so 52 | //! that they can be directly used in Cosmos messages: 53 | //! 54 | //! ```rust 55 | //! use cosmwasm_schema::cw_serde; 56 | //! use cw_asset::AssetUnchecked; 57 | //! 58 | //! #[cw_serde] 59 | //! pub enum ExecuteMsg { 60 | //! Deposit { 61 | //! asset: AssetUnchecked, 62 | //! }, 63 | //! } 64 | //! ``` 65 | //! 66 | //! Although [`Asset`] and [`AssetList`] _also_ implement the related traits, hence can also be used 67 | //! in messages, it is not recommended to do so; it is a good security practice to never trust 68 | //! addresses passed in by messages to be valid. Instead, also validate them yourselves: 69 | //! 70 | //! ```rust 71 | //! use cosmwasm_std::{Api, StdResult}; 72 | //! use cw_asset::{Asset, AssetError, AssetUnchecked}; 73 | //! 74 | //! const ACCEPTED_DENOMS: &[&str] = &["uatom", "uosmo", "uluna"]; 75 | //! 76 | //! fn validate_deposit(api: &dyn Api, asset_unchecked: AssetUnchecked) -> Result<(), AssetError> { 77 | //! let asset: Asset = asset_unchecked.check(api, Some(ACCEPTED_DENOMS))?; 78 | //! Ok(()) 79 | //! } 80 | //! ``` 81 | 82 | mod asset; 83 | mod asset_info; 84 | mod asset_list; 85 | mod error; 86 | 87 | pub use asset::{Asset, AssetBase, AssetUnchecked}; 88 | pub use asset_info::{AssetInfo, AssetInfoBase, AssetInfoUnchecked}; 89 | pub use asset_list::{AssetList, AssetListBase, AssetListUnchecked}; 90 | pub use error::AssetError; 91 | 92 | #[cfg(test)] 93 | mod testing; 94 | -------------------------------------------------------------------------------- /src/testing/custom_mock_querier.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{ 2 | from_json, testing::MockQuerier, Addr, Coin, Empty, Querier, QuerierResult, QueryRequest, 3 | StdResult, SystemError, WasmQuery, 4 | }; 5 | use cw20::Cw20QueryMsg; 6 | 7 | use super::cw20_querier::Cw20Querier; 8 | 9 | pub struct CustomMockQuerier { 10 | base: MockQuerier, 11 | cw20_querier: Cw20Querier, 12 | } 13 | 14 | impl Default for CustomMockQuerier { 15 | fn default() -> Self { 16 | CustomMockQuerier { 17 | base: MockQuerier::::new(&[]), 18 | cw20_querier: Cw20Querier::default(), 19 | } 20 | } 21 | } 22 | 23 | impl Querier for CustomMockQuerier { 24 | fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { 25 | let request: QueryRequest = match from_json(bin_request) { 26 | Ok(v) => v, 27 | Err(e) => { 28 | return Err(SystemError::InvalidRequest { 29 | error: format!("[mock]: failed to parse query request {e}"), 30 | request: bin_request.into(), 31 | }) 32 | .into() 33 | }, 34 | }; 35 | self.handle_query(&request) 36 | } 37 | } 38 | 39 | impl CustomMockQuerier { 40 | pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { 41 | match request { 42 | QueryRequest::Wasm(WasmQuery::Smart { 43 | contract_addr, 44 | msg, 45 | }) => { 46 | let contract_addr = Addr::unchecked(contract_addr); 47 | 48 | let parse_cw20_query: StdResult = from_json(msg); 49 | if let Ok(cw20_query) = parse_cw20_query { 50 | return self.cw20_querier.handle_query(&contract_addr, cw20_query); 51 | } 52 | 53 | panic!("[mock]: unsupported wasm query {msg:?}"); 54 | }, 55 | 56 | _ => self.base.handle_query(request), 57 | } 58 | } 59 | 60 | pub fn set_base_balances(&mut self, address: &str, balances: &[Coin]) { 61 | self.base.bank.update_balance(address, balances.to_vec()); 62 | } 63 | 64 | pub fn set_cw20_balance(&mut self, contract: &str, user: &str, balance: u128) { 65 | self.cw20_querier.set_balance(contract, user, balance); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/testing/cw20_querier.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use cosmwasm_std::{to_json_binary, Addr, QuerierResult, SystemError, Uint128}; 4 | use cw20::{BalanceResponse, Cw20QueryMsg}; 5 | 6 | #[derive(Default)] 7 | pub struct Cw20Querier { 8 | balances: HashMap>, 9 | } 10 | 11 | impl Cw20Querier { 12 | pub fn handle_query(&self, contract_addr: &Addr, query: Cw20QueryMsg) -> QuerierResult { 13 | match query { 14 | Cw20QueryMsg::Balance { 15 | address, 16 | } => { 17 | let contract_balances = match self.balances.get(contract_addr) { 18 | Some(balances) => balances, 19 | None => { 20 | return Err(SystemError::InvalidRequest { 21 | error: format!( 22 | "[mock]: cw20 balances not set for token {contract_addr:?}", 23 | ), 24 | request: Default::default(), 25 | }) 26 | .into() 27 | }, 28 | }; 29 | 30 | let balance = match contract_balances.get(&Addr::unchecked(&address)) { 31 | Some(balance) => balance, 32 | None => { 33 | return Err(SystemError::InvalidRequest { 34 | error: format!("[mock]: cw20 balance not set for user {address:?}"), 35 | request: Default::default(), 36 | }) 37 | .into() 38 | }, 39 | }; 40 | 41 | Ok(to_json_binary(&BalanceResponse { 42 | balance: *balance, 43 | }) 44 | .into()) 45 | .into() 46 | }, 47 | 48 | query => Err(SystemError::InvalidRequest { 49 | error: format!("[mock]: unsupported cw20 query {query:?}"), 50 | request: Default::default(), 51 | }) 52 | .into(), 53 | } 54 | } 55 | 56 | pub fn set_balance(&mut self, contract: &str, user: &str, balance: u128) { 57 | let contract_addr = Addr::unchecked(contract); 58 | let user_addr = Addr::unchecked(user); 59 | 60 | let contract_balances = self.balances.entry(contract_addr).or_default(); 61 | contract_balances.insert(user_addr, Uint128::new(balance)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/testing/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use cosmwasm_std::{ 4 | testing::{MockApi, MockStorage}, 5 | OwnedDeps, 6 | }; 7 | 8 | use super::CustomMockQuerier; 9 | 10 | pub fn mock_dependencies() -> OwnedDeps { 11 | OwnedDeps { 12 | storage: MockStorage::default(), 13 | api: MockApi::default(), 14 | querier: CustomMockQuerier::default(), 15 | custom_query_type: PhantomData, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/testing/mod.rs: -------------------------------------------------------------------------------- 1 | mod custom_mock_querier; 2 | mod cw20_querier; 3 | mod helpers; 4 | 5 | #[allow(unused_imports)] 6 | pub use custom_mock_querier::CustomMockQuerier; 7 | pub use helpers::mock_dependencies; 8 | --------------------------------------------------------------------------------