Repository: DA0-DA0/dao-contracts
Pull request #600 (2023-01-02 05:07:33)

WIP Payroll fixups
Feel free to merge this into your branch, will be doing some more work tomorrow most likely. We have some SubDAOs that want to use this ASAP.

This PR:
- uses `cw-denom` to drastically simplify code
- uses `cw-multi-test` for better testing
- adds a bunch of outstanding TODOs

There is still a lot of work to be done. Namely:
- [ ] Native tokens
- [ ] Better tests on the math and edge cases
- [ ] Though for how this might support cliffs and vesting schedules
- [ ] Get rid of unwraps() in favor of real error messages
- [ ] Make clippy happy

codecov-commenter: # [Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/600?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) Report
Base: **92.76**% // Head: **94.27**% // Increases project coverage by **`+1.51%`** :tada:
> Coverage data is based on head [(`328f4a8`)](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/600?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) compared to base [(`777236c`)](https://codecov.io/gh/DA0-DA0/dao-contracts/commit/777236c22d5f380716df0b5c06528fb4587693b4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).
> Patch coverage: 92.30% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@                  Coverage Diff                   @@
##           croncat/cw-payroll     #600      +/-   ##
======================================================
+ Coverage               92.76%   94.27%   +1.51%     
======================================================
  Files                      47       45       -2     
  Lines                    4255     4090     -165     
======================================================
- Hits                     3947     3856      -91     
+ Misses                    308      234      -74     
```


| [Impacted Files](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/600?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) | Coverage Δ | |
|---|---|---|
| [contracts/external/cw-payroll/src/state.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/600/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2V4dGVybmFsL2N3LXBheXJvbGwvc3JjL3N0YXRlLnJz) | `90.90% <88.23%> (-4.22%)` | :arrow_down: |
| [contracts/external/cw-payroll/src/contract.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/600/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2V4dGVybmFsL2N3LXBheXJvbGwvc3JjL2NvbnRyYWN0LnJz) | `78.60% <93.18%> (+1.16%)` | :arrow_up: |
| [contracts/external/cw-payroll/src/linking.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/600/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2V4dGVybmFsL2N3LXBheXJvbGwvc3JjL2xpbmtpbmcucnM=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/600?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).

github-actions[bot]: <h1>Cosm-Orc Gas Usage</h1> 
 
 | Contract | Op Name | Gas Used | Old Gas Used | Gas Diff | File | 
 | --- | --- | --- | --- | --- | --- | 
| dao_core | Instantiate__inst_admin_create_dao | 1202326 | 1273968 | -5.6235% | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Store__Store | 47122220 | 5668751 | +731.2628% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_core | Instantiate__exc_admin_msgs_create_dao | 1201276 | 1272918 | -5.6282% | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_admin_msgs_create_dao_with_admin | 1202326 | 1273968 | -5.6235% | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_items_create_dao | 1202326 | 1273968 | -5.6235% | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_stake_create_dao | 1201252 | 1272894 | -5.6283% | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__inst_dao_no_admin | 1201276 | 1272918 | -5.6282% | ci/integration-tests/src/helpers/helper.rs:98 |
| cw20_base | Store__Store | 34872950 | 4180849 | +734.1117% | ci/integration-tests/src/helpers/chain.rs:97 |
| cw20_stake | Store__Store | 29865200 | 3693999 | +708.4788% | ci/integration-tests/src/helpers/chain.rs:97 |
| cw4_group | Store__Store | 23049820 | 2783570 | +728.0668% | ci/integration-tests/src/helpers/chain.rs:97 |
| cw721_base | Store__Store | 28006860 | 3369402 | +731.2116% | ci/integration-tests/src/helpers/chain.rs:97 |
| cw721_base | Instantiate__instantiate_cw721_base | 163837 | 164837 | -0.6067% | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:22 |
| cw_admin_factory | Store__Store | 16448830 | 2003453 | +721.0240% | ci/integration-tests/src/helpers/chain.rs:97 |
| cw_token_swap | Store__Store | 19508590 | 2365061 | +724.8663% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_approval_single | Store__Store | 40985430 | 4903233 | +735.8858% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_approver | Store__Store | 30464370 | 3659835 | +732.3974% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_multiple | Store__Store | 35316690 | 4233291 | +734.2609% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_single | Store__Store | 34653610 | 4154927 | +734.0366% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_proposal_multiple | Store__Store | 53320060 | 6360832 | +738.2561% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_proposal_single | Store__Store | 55810350 | 6661184 | +737.8443% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw20_staked | Store__Store | 30142510 | 3623617 | +731.8349% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw4 | Store__Store | 22096010 | 2670847 | +727.3035% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw721_staked | Store__Store | 27639680 | 3327711 | +730.5914% | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw721_staked | Instantiate__instantiate_dao_voting_cw721_staked | 178007 | 179007 | -0.5586% | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:49 |
| dao_voting_cw721_staked | Execute__claim_nfts | 8103298 | 5710869 | +41.8926% | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:120 |
| dao_voting_native_staked | Store__Store | 24479775 | 2960295 | +726.9370% | ci/integration-tests/src/helpers/chain.rs:97 |
| stake_cw20_external_rewards | Store__Store | 25709730 | 3097923 | +729.9022% | ci/integration-tests/src/helpers/chain.rs:97 |
| stake_cw20_reward_distributor | Store__Store | 20071680 | 2431608 | +725.4488% | ci/integration-tests/src/helpers/chain.rs:97 |
| multiple_contracts | Execute__batch_cw721_stake_max_claims | 43191012 | 4196439 | +929.2301% | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:245 |
 
 <details><summary>Raw Report for 328f4a82911d6b499faa7d9506ea790e72543806 </summary><br/> 

| Contract | Op Name | Gas Used | Gas Wanted | File | 
 | --- | --- | --- | --- | --- |
| dao_core | Instantiate__inst_admin_create_dao | 1202326 | 1780667 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Store__Store | 47122220 | 70660481 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_core | Execute__exc_admin_msgs_pause_dao | 194613 | 269199 | ci/integration-tests/src/tests/cw_core_test.rs:76 |
| dao_core | Instantiate__exc_admin_msgs_create_dao | 1201276 | 1779092 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_admin_msgs_create_dao_with_admin | 1202326 | 1780667 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Execute__exc_items_rm | 192820 | 266508 | ci/integration-tests/src/tests/cw_core_test.rs:171 |
| dao_core | Execute__exc_items_set | 194487 | 269010 | ci/integration-tests/src/tests/cw_core_test.rs:136 |
| dao_core | Instantiate__exc_items_create_dao | 1202326 | 1780667 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_stake_create_dao | 1201252 | 1779056 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__inst_dao_no_admin | 1201276 | 1779092 | ci/integration-tests/src/helpers/helper.rs:98 |
| cw20_base | Store__Store | 34872950 | 52286576 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw20_base | Execute__exc_stake_stake_tokens | 234799 | 321344 | ci/integration-tests/src/tests/cw20_stake_test.rs:76 |
| cw20_stake | Store__Store | 29865200 | 44774951 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw4_group | Store__Store | 23049820 | 34551881 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw721_base | Store__Store | 28006860 | 41987441 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw721_base | Execute__mint_nft | 140542 | 188094 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:87 |
| cw721_base | Execute__stake_nft | 234591 | 329163 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:70 |
| cw721_base | Instantiate__instantiate_cw721_base | 163837 | 223038 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:22 |
| cw_admin_factory | Store__Store | 16448830 | 24650396 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw_payroll | Store__Store | 25029820 | 37521881 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw_token_swap | Store__Store | 19508590 | 29240036 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_approval_single | Store__Store | 40985430 | 61455296 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_approver | Store__Store | 30464370 | 45673706 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_multiple | Store__Store | 35316690 | 52952186 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_single | Store__Store | 34653610 | 51957566 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_proposal_multiple | Store__Store | 53320060 | 79957241 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_proposal_single | Store__Store | 55810350 | 83692676 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw20_staked | Store__Store | 30142510 | 45190916 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw4 | Store__Store | 22096010 | 33121166 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw721_staked | Store__Store | 27639680 | 41436671 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw721_staked | Execute__unstake_nfts | 230318 | 322757 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:105 |
| dao_voting_cw721_staked | Instantiate__instantiate_dao_voting_cw721_staked | 178007 | 244293 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:49 |
| dao_voting_cw721_staked | Execute__claim_nfts | 8103298 | 12131660 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:120 |
| dao_voting_native_staked | Store__Store | 24479775 | 36696809 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_staking_denom_staked | Store__Store | 18085630 | 27105596 | ci/integration-tests/src/helpers/chain.rs:97 |
| stake_cw20_external_rewards | Store__Store | 25709730 | 38541746 | ci/integration-tests/src/helpers/chain.rs:97 |
| stake_cw20_reward_distributor | Store__Store | 20071680 | 30084671 | ci/integration-tests/src/helpers/chain.rs:97 |
| multiple_contracts | Execute__batch_cw721_stake_max_claims | 43191012 | 64755422 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:245 |
</details>

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 24a2d9ab..9ce20a6c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -674,6 +674,24 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw-escrow-streams"
+version = "0.0.1"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cw-denom",
+ "cw-multi-test",
+ "cw-storage-plus 0.16.0",
+ "cw-utils 0.16.0",
+ "cw2 0.16.0",
+ "cw20 0.16.0",
+ "cw20-base",
+ "dao-testing",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "cw-hooks"
 version = "2.0.0-beta"
@@ -725,21 +743,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "cw-payroll"
-version = "0.0.1"
-dependencies = [
- "cosmwasm-schema",
- "cosmwasm-std",
- "cosmwasm-storage",
- "cw-storage-plus 0.16.0",
- "cw-utils 0.16.0",
- "cw2 0.16.0",
- "cw20 0.16.0",
- "serde",
- "thiserror",
-]
-
 [[package]]
 name = "cw-proposal-single"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index bebe20e2..3d39bcf9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -84,4 +84,4 @@ dao-interface = { path = "./packages/dao-interface" }
 dao-voting = { path = "./packages/dao-voting" }
 cw-proposal-single-v1 = { package = "cw-proposal-single", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
 voting-v1 = { package = "voting", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
-cw-utils-v1 = {package = "cw-utils", version = "0.13"}
+cw-utils-v1 = { package = "cw-utils", version = "0.13" }
diff --git a/contracts/external/cw-payroll/.cargo/config b/contracts/external/cw-escrow-streams/.cargo/config
similarity index 100%
rename from contracts/external/cw-payroll/.cargo/config
rename to contracts/external/cw-escrow-streams/.cargo/config
diff --git a/contracts/external/cw-payroll/Cargo.toml b/contracts/external/cw-escrow-streams/Cargo.toml
similarity index 62%
rename from contracts/external/cw-payroll/Cargo.toml
rename to contracts/external/cw-escrow-streams/Cargo.toml
index 7616de00..0085e8cb 100644
--- a/contracts/external/cw-payroll/Cargo.toml
+++ b/contracts/external/cw-escrow-streams/Cargo.toml
@@ -1,8 +1,9 @@
 [package]
-name = "cw-payroll"
+name = "cw-escrow-streams"
 version = "0.0.1"
-authors = ["Vernon Johnson <vtj2105@columbia.edu>", "tjtc <meow@cron.cat>"]
-edition = "2018"
+authors = ["Vernon Johnson <vtj2105@columbia.edu>", "tjtc <meow@cron.cat>", "Jake Hartnell <jake@no-reply.com>"]
+description = "This contract enables the creation of native and cw20 token streams, which allows a cw20 payment to be vested continuously over time."
+edition = "2021"
 
 [lib]
 crate-type = ["cdylib", "rlib"]
@@ -15,8 +16,8 @@ library = []
 
 [dependencies]
 cosmwasm-std = { workspace = true }
-cosmwasm-storage = { workspace = true }
 cosmwasm-schema = { workspace = true }
+cw-denom = { workspace = true }
 cw-storage-plus = { workspace = true }
 cw-utils = { workspace = true }
 cw2 = { workspace = true }
@@ -25,4 +26,6 @@ thiserror = { workspace = true }
 serde = { workspace = true }
 
 [dev-dependencies]
-
+cw-multi-test = { workspace = true }
+cw20-base = { workspace = true }
+dao-testing = { workspace = true }
diff --git a/contracts/external/cw-escrow-streams/README.md b/contracts/external/cw-escrow-streams/README.md
new file mode 100644
index 00000000..aaec86fc
--- /dev/null
+++ b/contracts/external/cw-escrow-streams/README.md
@@ -0,0 +1,24 @@
+# CW Escrow Streams
+
+This contract enables the creation of native and cw20 token streams, which allows a cw20 payment to be vested continuously over time. This contract is forked off of [cw20-streams](https://github.com/CosmWasm/cw-tokens/tree/main/contracts/cw20-streams) to enable additional features.
+
+## Instantiation
+
+To instantiate a new instance of this contract you must specify a contract owner.
+
+```sh
+junod tx wasm instantiate <code-id> '{"admin": "juno12xyz..."}'  --label "cw-payroll contract" --from <your-key> 
+```
+
+## Creating a Native Token Stream
+
+TODO: Update upon native token completion
+
+## Creating a CW20 Stream
+A stream can be created using the cw20 [Send / Receive](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md#receiver) flow. This involves triggering a Send message from the cw20 token contract, with a Receive callback that's sent to the token streaming contract. The callback message must include the start time and end time of the stream in seconds, as well as the payment recipient. 
+
+## Distribute payments
+Streamed payments can be claimed continously at any point after the start time by triggering a Distribute message.
+
+## Linking Streams
+TODO: describe this feature and how it works.
diff --git a/contracts/external/cw-payroll/examples/schema.rs b/contracts/external/cw-escrow-streams/examples/schema.rs
similarity index 70%
rename from contracts/external/cw-payroll/examples/schema.rs
rename to contracts/external/cw-escrow-streams/examples/schema.rs
index 61ca45cf..8ee304a5 100644
--- a/contracts/external/cw-payroll/examples/schema.rs
+++ b/contracts/external/cw-escrow-streams/examples/schema.rs
@@ -1,5 +1,5 @@
 use cosmwasm_schema::write_api;
-use cw_payroll::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+use cw_escrow_streams::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
 
 fn main() {
     write_api! {
diff --git a/contracts/external/cw-payroll/schema/cw-payroll.json b/contracts/external/cw-escrow-streams/schema/cw-escrow-streams.json
similarity index 79%
rename from contracts/external/cw-payroll/schema/cw-payroll.json
rename to contracts/external/cw-escrow-streams/schema/cw-escrow-streams.json
index 572a226b..b2421446 100644
--- a/contracts/external/cw-payroll/schema/cw-payroll.json
+++ b/contracts/external/cw-escrow-streams/schema/cw-escrow-streams.json
@@ -1,5 +1,5 @@
 {
-  "contract_name": "cw-payroll",
+  "contract_name": "cw-escrow-streams",
   "contract_version": "0.0.1",
   "idl_version": "1.0.0",
   "instantiate": {
@@ -32,6 +32,27 @@
         },
         "additionalProperties": false
       },
+      {
+        "type": "object",
+        "required": [
+          "create"
+        ],
+        "properties": {
+          "create": {
+            "type": "object",
+            "required": [
+              "params"
+            ],
+            "properties": {
+              "params": {
+                "$ref": "#/definitions/StreamParams"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
       {
         "type": "object",
         "required": [
@@ -175,10 +196,45 @@
       }
     ],
     "definitions": {
+      "Addr": {
+        "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
+        "type": "string"
+      },
       "Binary": {
         "description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
         "type": "string"
       },
+      "CheckedDenom": {
+        "description": "A denom that has been checked to point to a valid asset. This enum should never be constructed literally and should always be built by calling `into_checked` on an `UncheckedDenom` instance.",
+        "oneOf": [
+          {
+            "description": "A native (bank module) asset.",
+            "type": "object",
+            "required": [
+              "native"
+            ],
+            "properties": {
+              "native": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "A cw20 asset.",
+            "type": "object",
+            "required": [
+              "cw20"
+            ],
+            "properties": {
+              "cw20": {
+                "$ref": "#/definitions/Addr"
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
       "Cw20ReceiveMsg": {
         "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg",
         "type": "object",
@@ -200,6 +256,60 @@
         },
         "additionalProperties": false
       },
+      "StreamParams": {
+        "type": "object",
+        "required": [
+          "admin",
+          "balance",
+          "denom",
+          "end_time",
+          "recipient",
+          "start_time"
+        ],
+        "properties": {
+          "admin": {
+            "type": "string"
+          },
+          "balance": {
+            "$ref": "#/definitions/Uint128"
+          },
+          "denom": {
+            "$ref": "#/definitions/CheckedDenom"
+          },
+          "description": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "end_time": {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          },
+          "is_detachable": {
+            "type": [
+              "boolean",
+              "null"
+            ]
+          },
+          "recipient": {
+            "type": "string"
+          },
+          "start_time": {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          },
+          "title": {
+            "type": [
+              "string",
+              "null"
+            ]
+          }
+        },
+        "additionalProperties": false
+      },
       "Uint128": {
         "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
         "type": "string"
@@ -304,6 +414,7 @@
         "admin",
         "balance",
         "claimed_balance",
+        "denom",
         "end_time",
         "id",
         "is_detachable",
@@ -316,10 +427,13 @@
           "type": "string"
         },
         "balance": {
-          "$ref": "#/definitions/WrappedBalance"
+          "$ref": "#/definitions/Uint128"
         },
         "claimed_balance": {
-          "$ref": "#/definitions/WrappedBalance"
+          "$ref": "#/definitions/Uint128"
+        },
+        "denom": {
+          "$ref": "#/definitions/CheckedDenom"
         },
         "description": {
           "description": "Human readable description for this payroll contract",
@@ -393,77 +507,40 @@
           "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
           "type": "string"
         },
-        "Balance": {
+        "CheckedDenom": {
+          "description": "A denom that has been checked to point to a valid asset. This enum should never be constructed literally and should always be built by calling `into_checked` on an `UncheckedDenom` instance.",
           "oneOf": [
             {
+              "description": "A native (bank module) asset.",
               "type": "object",
               "required": [
                 "native"
               ],
               "properties": {
                 "native": {
-                  "$ref": "#/definitions/NativeBalance"
+                  "type": "string"
                 }
               },
               "additionalProperties": false
             },
             {
+              "description": "A cw20 asset.",
               "type": "object",
               "required": [
                 "cw20"
               ],
               "properties": {
                 "cw20": {
-                  "$ref": "#/definitions/Cw20CoinVerified"
+                  "$ref": "#/definitions/Addr"
                 }
               },
               "additionalProperties": false
             }
           ]
         },
-        "Coin": {
-          "type": "object",
-          "required": [
-            "amount",
-            "denom"
-          ],
-          "properties": {
-            "amount": {
-              "$ref": "#/definitions/Uint128"
-            },
-            "denom": {
-              "type": "string"
-            }
-          }
-        },
-        "Cw20CoinVerified": {
-          "type": "object",
-          "required": [
-            "address",
-            "amount"
-          ],
-          "properties": {
-            "address": {
-              "$ref": "#/definitions/Addr"
-            },
-            "amount": {
-              "$ref": "#/definitions/Uint128"
-            }
-          },
-          "additionalProperties": false
-        },
-        "NativeBalance": {
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/Coin"
-          }
-        },
         "Uint128": {
           "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
           "type": "string"
-        },
-        "WrappedBalance": {
-          "$ref": "#/definitions/Balance"
         }
       }
     },
@@ -488,77 +565,44 @@
           "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
           "type": "string"
         },
-        "Balance": {
+        "CheckedDenom": {
+          "description": "A denom that has been checked to point to a valid asset. This enum should never be constructed literally and should always be built by calling `into_checked` on an `UncheckedDenom` instance.",
           "oneOf": [
             {
+              "description": "A native (bank module) asset.",
               "type": "object",
               "required": [
                 "native"
               ],
               "properties": {
                 "native": {
-                  "$ref": "#/definitions/NativeBalance"
+                  "type": "string"
                 }
               },
               "additionalProperties": false
             },
             {
+              "description": "A cw20 asset.",
               "type": "object",
               "required": [
                 "cw20"
               ],
               "properties": {
                 "cw20": {
-                  "$ref": "#/definitions/Cw20CoinVerified"
+                  "$ref": "#/definitions/Addr"
                 }
               },
               "additionalProperties": false
             }
           ]
         },
-        "Coin": {
-          "type": "object",
-          "required": [
-            "amount",
-            "denom"
-          ],
-          "properties": {
-            "amount": {
-              "$ref": "#/definitions/Uint128"
-            },
-            "denom": {
-              "type": "string"
-            }
-          }
-        },
-        "Cw20CoinVerified": {
-          "type": "object",
-          "required": [
-            "address",
-            "amount"
-          ],
-          "properties": {
-            "address": {
-              "$ref": "#/definitions/Addr"
-            },
-            "amount": {
-              "$ref": "#/definitions/Uint128"
-            }
-          },
-          "additionalProperties": false
-        },
-        "NativeBalance": {
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/Coin"
-          }
-        },
         "StreamResponse": {
           "type": "object",
           "required": [
             "admin",
             "balance",
             "claimed_balance",
+            "denom",
             "end_time",
             "id",
             "is_detachable",
@@ -571,10 +615,13 @@
               "type": "string"
             },
             "balance": {
-              "$ref": "#/definitions/WrappedBalance"
+              "$ref": "#/definitions/Uint128"
             },
             "claimed_balance": {
-              "$ref": "#/definitions/WrappedBalance"
+              "$ref": "#/definitions/Uint128"
+            },
+            "denom": {
+              "$ref": "#/definitions/CheckedDenom"
             },
             "description": {
               "description": "Human readable description for this payroll contract",
@@ -647,9 +694,6 @@
         "Uint128": {
           "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
           "type": "string"
-        },
-        "WrappedBalance": {
-          "$ref": "#/definitions/Balance"
         }
       }
     }
diff --git a/contracts/external/cw-escrow-streams/src/contract.rs b/contracts/external/cw-escrow-streams/src/contract.rs
new file mode 100644
index 00000000..8f7b4de0
--- /dev/null
+++ b/contracts/external/cw-escrow-streams/src/contract.rs
@@ -0,0 +1,397 @@
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cosmwasm_std::{
+    from_binary, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError,
+    StdResult, Storage, Uint128,
+};
+use cw2::set_contract_version;
+use cw20::Cw20ReceiveMsg;
+use cw_denom::UncheckedDenom;
+use cw_storage_plus::Bound;
+
+use crate::error::ContractError;
+use crate::linking::*;
+use crate::msg::{
+    ConfigResponse, ExecuteMsg, InstantiateMsg, ListStreamsResponse, QueryMsg, ReceiveMsg,
+    StreamParams, StreamResponse,
+};
+use crate::state::{Config, Stream, StreamId, CONFIG, STREAMS, STREAM_SEQ};
+
+const CONTRACT_NAME: &str = "crates.io:cw-escrow-streams";
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn instantiate(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    msg: InstantiateMsg,
+) -> Result<Response, ContractError> {
+    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
+
+    // TODO what is the point of this admin? It doesn't do anything.
+    // Delete if it doesn't do anything
+    let admin = match msg.admin {
+        Some(ad) => deps.api.addr_validate(&ad)?,
+        None => info.sender,
+    };
+
+    let config = Config {
+        admin: admin.clone(),
+    };
+    CONFIG.save(deps.storage, &config)?;
+
+    STREAM_SEQ.save(deps.storage, &0u64)?;
+
+    Ok(Response::new()
+        .add_attribute("method", "instantiate")
+        .add_attribute("admin", admin))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<Response, ContractError> {
+    match msg {
+        ExecuteMsg::Receive(msg) => execute_receive(env, deps, info, msg),
+        // TODO should be able to create and fund with a native token
+        ExecuteMsg::Create { .. } => unimplemented!(),
+        ExecuteMsg::Distribute { id } => execute_distribute(env, deps, id),
+        ExecuteMsg::PauseStream { id } => execute_pause_stream(env, deps, info, id),
+        ExecuteMsg::ResumeStream { id } => execute_resume_stream(env, deps, info, id),
+        ExecuteMsg::RemoveStream { id } => execute_remove_stream(env, deps, info, id),
+        ExecuteMsg::LinkStream { ids } => execute_link_stream(deps, &info, ids),
+        ExecuteMsg::DetachStream { id } => execute_detach_stream(env, deps, &info, id),
+    }
+}
+
+pub fn execute_pause_stream(
+    env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    id: StreamId,
+) -> Result<Response, ContractError> {
+    let pause_stream_local =
+        |stream_id: StreamId, storage: &mut dyn Storage| -> Result<Stream, ContractError> {
+            let mut stream = STREAMS
+                .may_load(storage, stream_id)?
+                .ok_or(ContractError::StreamNotFound { stream_id: id })?;
+            if stream.admin != info.sender {
+                return Err(ContractError::Unauthorized {});
+            }
+            if stream.paused {
+                return Err(ContractError::StreamAlreadyPaused {});
+            }
+            stream.paused_time = Some(env.block.time.seconds());
+            stream.paused = true;
+            STREAMS.save(storage, id, &stream)?;
+            Ok(stream)
+        };
+
+    // TODO this is weird... needs comments at the very least
+    let stream = pause_stream_local(id, deps.storage)?;
+    if let Some(link_id) = stream.link_id {
+        pause_stream_local(link_id, deps.storage)?;
+    }
+
+    Ok(Response::new()
+        .add_attribute("method", "pause_stream")
+        .add_attribute("paused", stream.paused.to_string())
+        .add_attribute("stream_id", id.to_string())
+        .add_attribute("admin", info.sender)
+        .add_attribute("paused_time", stream.paused_time.unwrap().to_string()))
+}
+
+pub fn execute_remove_stream(
+    env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    id: StreamId,
+) -> Result<Response, ContractError> {
+    // Check that sender is admin
+    let stream = STREAMS
+        .may_load(deps.storage, id)?
+        .ok_or(ContractError::StreamNotFound { stream_id: id })?;
+    if stream.admin != info.sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    if let Some(link_id) = stream.link_id {
+        return Err(ContractError::LinkedStreamDeleteNotAllowed { link_id });
+    }
+    STREAMS.remove(deps.storage, id);
+
+    // Transfer any remaining balance to the owner
+    let transfer_to_admin_msg = stream
+        .denom
+        .get_transfer_to_message(&stream.admin, stream.balance)?;
+
+    Ok(Response::new()
+        .add_attribute("method", "remove_stream")
+        .add_attribute("stream_id", id.to_string())
+        .add_attribute("admin", info.sender)
+        .add_attribute("removed_time", env.block.time.to_string())
+        .add_message(transfer_to_admin_msg))
+}
+
+pub fn execute_resume_stream(
+    env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    id: StreamId,
+) -> Result<Response, ContractError> {
+    let resume_stream_local =
+        |stream_id: StreamId, storage: &mut dyn Storage| -> Result<Stream, ContractError> {
+            let mut stream = STREAMS
+                .may_load(storage, stream_id)?
+                .ok_or(ContractError::StreamNotFound { stream_id: id })?;
+            if stream.admin != info.sender {
+                return Err(ContractError::Unauthorized {});
+            }
+            if !stream.paused {
+                return Err(ContractError::StreamNotPaused {});
+            }
+            stream.paused_duration = stream.calc_pause_duration(env.block.time);
+            stream.paused = false;
+            stream.paused_time = None;
+            STREAMS.save(storage, id, &stream)?;
+            Ok(stream)
+        };
+
+    let stream = resume_stream_local(id, deps.storage)?;
+    if let Some(link_id) = stream.link_id {
+        resume_stream_local(link_id, deps.storage).unwrap();
+    }
+
+    let (_, rate_per_second) = stream.calc_distribution_rate(env.block.time)?;
+    let response = Response::new()
+        .add_attribute("method", "resume_stream")
+        .add_attribute("stream_id", id.to_string())
+        .add_attribute("admin", info.sender)
+        .add_attribute("rate_per_second", rate_per_second)
+        .add_attribute("resume_time", env.block.time.to_string())
+        .add_attribute(
+            "paused_duration",
+            stream.paused_duration.unwrap().to_string(),
+        )
+        .add_attribute("resume_time", env.block.time.to_string());
+
+    Ok(response)
+}
+
+pub fn execute_create_stream(
+    env: Env,
+    deps: DepsMut,
+    params: StreamParams,
+) -> Result<Response, ContractError> {
+    let StreamParams {
+        admin,
+        recipient,
+        balance,
+        denom,
+        start_time,
+        end_time,
+        title,
+        description,
+        is_detachable,
+    } = params;
+
+    let admin = deps.api.addr_validate(&admin)?;
+    let recipient = deps.api.addr_validate(&recipient)?;
+
+    if start_time > end_time {
+        return Err(ContractError::InvalidStartTime {});
+    }
+
+    let block_time = env.block.time.seconds();
+
+    if end_time <= block_time {
+        return Err(ContractError::InvalidEndTime {});
+    }
+
+    let stream = Stream {
+        admin: admin.clone(),
+        recipient: recipient.clone(),
+        balance,
+        claimed_balance: Uint128::zero(),
+        denom,
+        start_time,
+        end_time,
+        paused_time: None,
+        paused_duration: None,
+        paused: false,
+        title,
+        description,
+        link_id: None,
+        // TODO Should not be an option type
+        is_detachable: is_detachable.unwrap_or(true),
+    };
+
+    let id = STREAM_SEQ.load(deps.storage)?;
+    let id = id + 1;
+    STREAM_SEQ.save(deps.storage, &id)?;
+    STREAMS.save(deps.storage, id, &stream)?;
+
+    Ok(Response::new()
+        .add_attribute("method", "create_stream")
+        .add_attribute("stream_id", id.to_string())
+        .add_attribute("admin", admin)
+        .add_attribute("recipient", recipient)
+        .add_attribute("start_time", start_time.to_string())
+        .add_attribute("end_time", end_time.to_string()))
+}
+
+pub fn execute_receive(
+    env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    receive_msg: Cw20ReceiveMsg,
+) -> Result<Response, ContractError> {
+    deps.api.addr_validate(&info.sender.clone().into_string())?;
+    let msg: ReceiveMsg = from_binary(&receive_msg.msg)?;
+    let checked_denom =
+        UncheckedDenom::Cw20(info.sender.to_string()).into_checked(deps.as_ref())?;
+
+    // TODO should support all stream params
+    match msg {
+        ReceiveMsg::CreateStream {
+            admin,
+            start_time,
+            end_time,
+            recipient,
+            is_detachable,
+        } => execute_create_stream(
+            env,
+            deps,
+            StreamParams {
+                admin: admin.unwrap_or_else(|| receive_msg.sender.clone()),
+                recipient,
+                balance: receive_msg.amount,
+                denom: checked_denom,
+                start_time,
+                end_time,
+                title: None,
+                description: None,
+                is_detachable,
+            },
+        ),
+    }
+}
+
+pub fn execute_distribute(env: Env, deps: DepsMut, id: u64) -> Result<Response, ContractError> {
+    let mut stream = STREAMS
+        .may_load(deps.storage, id)?
+        .ok_or(ContractError::StreamNotFound { stream_id: id })?;
+
+    let (available_claims, _) = stream.calc_distribution_rate(env.block.time)?;
+
+    if !stream.can_distribute_more() || available_claims.u128() == 0 {
+        return Err(ContractError::NoFundsToClaim {
+            claimed: stream.claimed_balance,
+        });
+    }
+
+    // Update claimed amount
+    stream.claimed_balance = stream
+        .claimed_balance
+        .checked_add(available_claims)
+        .map_err(StdError::overflow)?;
+
+    // Update remaining balance
+    stream.balance = stream
+        .balance
+        .checked_sub(available_claims)
+        .map_err(StdError::overflow)?;
+
+    // Save updated stream
+    STREAMS.save(deps.storage, id, &stream)?;
+
+    // Get transfer message
+    let transfer_msg = stream
+        .denom
+        .get_transfer_to_message(&stream.recipient, available_claims)?;
+
+    Ok(Response::new()
+        .add_attribute("method", "distribute")
+        .add_attribute("vested", available_claims)
+        .add_attribute("stream_id", id.to_string())
+        .add_message(transfer_msg))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::GetConfig {} => to_binary(&query_config(deps)?),
+        QueryMsg::GetStream { id } => to_binary(&query_stream(deps, id)?),
+        QueryMsg::ListStreams { start, limit } => {
+            to_binary(&query_list_streams(deps, start, limit)?)
+        }
+    }
+}
+
+fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
+    let config = CONFIG.load(deps.storage)?;
+    Ok(ConfigResponse {
+        admin: config.admin.into(),
+    })
+}
+
+fn query_stream(deps: Deps, id: u64) -> StdResult<StreamResponse> {
+    let stream = STREAMS.load(deps.storage, id)?;
+    Ok(StreamResponse {
+        id,
+        admin: stream.admin.into(),
+        recipient: stream.recipient.into(),
+        balance: stream.balance,
+        claimed_balance: stream.claimed_balance,
+        denom: stream.denom,
+        start_time: stream.start_time,
+        end_time: stream.end_time,
+        title: stream.title,
+        description: stream.description,
+        paused_time: stream.paused_time,
+        paused_duration: stream.paused_duration,
+        paused: stream.paused,
+        is_detachable: stream.is_detachable,
+        link_id: stream.link_id,
+    })
+}
+
+fn query_list_streams(
+    deps: Deps,
+    start: Option<u8>,
+    limit: Option<u8>,
+) -> StdResult<ListStreamsResponse> {
+    let start = start.map(Bound::inclusive);
+    let limit = limit.unwrap_or(5);
+
+    let streams = STREAMS
+        .range(deps.storage, start, None, Order::Ascending)
+        .take(limit.into())
+        .map(map_stream)
+        .collect::<StdResult<Vec<_>>>()?;
+    Ok(ListStreamsResponse { streams })
+}
+
+fn map_stream(item: StdResult<(u64, Stream)>) -> StdResult<StreamResponse> {
+    item.map(|(id, stream)| StreamResponse {
+        id,
+        admin: stream.admin.to_string(),
+        recipient: stream.recipient.to_string(),
+        balance: stream.balance,
+        claimed_balance: stream.claimed_balance,
+        denom: stream.denom,
+        start_time: stream.start_time,
+        end_time: stream.end_time,
+        title: stream.title,
+        description: stream.description,
+        paused_time: stream.paused_time,
+        paused_duration: stream.paused_duration,
+        paused: stream.paused,
+        is_detachable: stream.is_detachable,
+        link_id: stream.link_id,
+    })
+}
diff --git a/contracts/external/cw-payroll/src/error.rs b/contracts/external/cw-escrow-streams/src/error.rs
similarity index 66%
rename from contracts/external/cw-payroll/src/error.rs
rename to contracts/external/cw-escrow-streams/src/error.rs
index 3491a9e5..1d674660 100644
--- a/contracts/external/cw-payroll/src/error.rs
+++ b/contracts/external/cw-escrow-streams/src/error.rs
@@ -1,36 +1,17 @@
-use cosmwasm_std::{StdError, Uint128};
+use cosmwasm_std::{Addr, StdError, Uint128};
+use cw_denom::DenomError;
 use thiserror::Error;
 
-use cosmwasm_std::Addr;
-
-use crate::{balance::WrappedBalance, msg::StreamId};
-
-#[derive(Error, Debug, PartialEq)]
-pub enum GenericError {
-    #[error("{0}")]
-    Std(#[from] StdError),
-
-    #[error("No coin balance found")]
-    EmptyBalance {},
-
-    #[error("Not enough cw20 balance of {addr}, need {lack} more")]
-    NotEnoughCw20 { addr: String, lack: Uint128 },
-
-    #[error("Not enough native balance of {denom}, need {lack} more")]
-    NotEnoughNative { denom: String, lack: Uint128 },
-
-    #[error("invalid cosmwasm message")]
-    InvalidWasmMsg {},
-
-    #[error("Numerical overflow")]
-    IntegerOverflow {},
-}
+use crate::state::StreamId;
 
 #[derive(Error, Debug, PartialEq)]
 pub enum ContractError {
     #[error("{0}")]
     Std(#[from] StdError),
 
+    #[error(transparent)]
+    Denom(#[from] DenomError),
+
     #[error("Not authorized to perform action")]
     Unauthorized {},
 
@@ -47,14 +28,11 @@ pub enum ContractError {
     NotStreamRecipient { recipient: Addr },
 
     #[error("No tokens have vested for this stream")]
-    NoFundsToClaim { claimed: WrappedBalance },
+    NoFundsToClaim { claimed: Uint128 },
 
     #[error("Stream does not exist")]
     StreamNotFound { stream_id: StreamId },
 
-    #[error("Amount must be greater than duration")]
-    AmountLessThanDuration {},
-
     #[error("Stream recipient cannot be the stream owner")]
     InvalidRecipient {},
 
diff --git a/contracts/external/cw-payroll/src/lib.rs b/contracts/external/cw-escrow-streams/src/lib.rs
similarity index 81%
rename from contracts/external/cw-payroll/src/lib.rs
rename to contracts/external/cw-escrow-streams/src/lib.rs
index 194715d1..70d529b7 100644
--- a/contracts/external/cw-payroll/src/lib.rs
+++ b/contracts/external/cw-escrow-streams/src/lib.rs
@@ -1,7 +1,10 @@
-pub mod balance;
 pub mod contract;
 mod error;
 pub mod linking;
 pub mod msg;
 pub mod state;
+
 pub use crate::error::ContractError;
+
+#[cfg(test)]
+mod tests;
diff --git a/contracts/external/cw-payroll/src/linking.rs b/contracts/external/cw-escrow-streams/src/linking.rs
similarity index 88%
rename from contracts/external/cw-payroll/src/linking.rs
rename to contracts/external/cw-escrow-streams/src/linking.rs
index 6648c729..e34443ae 100644
--- a/contracts/external/cw-payroll/src/linking.rs
+++ b/contracts/external/cw-escrow-streams/src/linking.rs
@@ -1,8 +1,7 @@
 use cosmwasm_std::{DepsMut, Env, MessageInfo, Response};
 
 use crate::{
-    msg::{StreamId, StreamIds, StreamIdsExtensions},
-    state::{save_stream, STREAMS},
+    state::{StreamId, StreamIds, StreamIdsExtensions, STREAMS},
     ContractError,
 };
 
@@ -17,6 +16,7 @@ pub(crate) fn execute_link_stream(
     ids: StreamIds,
 ) -> Result<Response, ContractError> {
     ids.validate()?;
+    // TODO no unwrap
     let left_stream_id = *ids.first().unwrap();
     let right_stream_id = *ids.second().unwrap();
 
@@ -40,8 +40,8 @@ pub(crate) fn execute_link_stream(
     left_stream.link_id = Some(right_stream_id);
     right_stream.link_id = Some(left_stream_id);
 
-    save_stream(deps.storage, left_stream_id, &left_stream).unwrap();
-    save_stream(deps.storage, right_stream_id, &right_stream).unwrap();
+    STREAMS.save(deps.storage, left_stream_id, &left_stream)?;
+    STREAMS.save(deps.storage, right_stream_id, &right_stream)?;
 
     let response = Response::new()
         .add_attribute("method", "link")
@@ -82,8 +82,9 @@ pub(crate) fn execute_detach_stream(
     linked_stream.paused = true;
     linked_stream.link_id = None;
 
-    save_stream(deps.storage, id, &detach_stream).unwrap();
-    save_stream(deps.storage, link_id, &linked_stream).unwrap();
+    STREAMS.save(deps.storage, id, &detach_stream)?;
+    STREAMS.save(deps.storage, link_id, &linked_stream)?;
+
     let response = Response::new()
         .add_attribute("method", "link")
         .add_attribute("detach_stream_id", id.to_string())
diff --git a/contracts/external/cw-payroll/src/msg.rs b/contracts/external/cw-escrow-streams/src/msg.rs
similarity index 72%
rename from contracts/external/cw-payroll/src/msg.rs
rename to contracts/external/cw-escrow-streams/src/msg.rs
index 69a760d2..0b594214 100644
--- a/contracts/external/cw-payroll/src/msg.rs
+++ b/contracts/external/cw-escrow-streams/src/msg.rs
@@ -1,39 +1,22 @@
 use cosmwasm_schema::{cw_serde, QueryResponses};
-use cosmwasm_std::Response;
+use cosmwasm_std::Uint128;
 use cw20::Cw20ReceiveMsg;
+use cw_denom::CheckedDenom;
 
-use crate::{balance::WrappedBalance, ContractError};
+use crate::state::{StreamId, StreamIds};
 
 #[cw_serde]
 pub struct InstantiateMsg {
     pub admin: Option<String>,
 }
-pub type StreamId = u64;
-pub type StreamIds = Vec<StreamId>;
-pub type ContractResult = Result<Response, ContractError>;
-
-pub(crate) trait StreamIdsExtensions {
-    fn second(&self) -> Option<&StreamId>;
-    fn validate(&self) -> Result<(), ContractError>;
-}
-impl StreamIdsExtensions for StreamIds {
-    fn second(&self) -> Option<&StreamId> {
-        self.get(1)
-    }
-    fn validate(&self) -> Result<(), ContractError> {
-        if self.len() != 2 {
-            return Err(ContractError::InvalidStreamIds {});
-        }
-        if self.first() == self.second() {
-            return Err(ContractError::StreamsShouldNotBeEqual {});
-        }
-        Ok(())
-    }
-}
 
 #[cw_serde]
 pub enum ExecuteMsg {
     Receive(Cw20ReceiveMsg),
+    // TODO be able to create steam with native token
+    Create {
+        params: StreamParams,
+    },
     Distribute {
         id: StreamId, // Stream id
     },
@@ -57,11 +40,13 @@ pub enum ExecuteMsg {
 // Receiver setup
 #[cw_serde]
 pub enum ReceiveMsg {
+    // TODO support all StreamParams or delete them
     CreateStream {
         admin: Option<String>,
         recipient: String,
         start_time: u64,
         end_time: u64,
+        // TODO just make this a bool
         is_detachable: Option<bool>,
     },
 }
@@ -70,11 +55,13 @@ pub enum ReceiveMsg {
 pub struct StreamParams {
     pub admin: String,
     pub recipient: String,
-    pub balance: WrappedBalance,
+    pub balance: Uint128,
+    pub denom: CheckedDenom,
     pub start_time: u64,
     pub end_time: u64,
     pub title: Option<String>,
     pub description: Option<String>,
+    // TODO just make this a bool
     pub is_detachable: Option<bool>,
 }
 
@@ -102,8 +89,9 @@ pub struct StreamResponse {
     pub id: u64,
     pub admin: String,
     pub recipient: String,
-    pub balance: WrappedBalance,
-    pub claimed_balance: WrappedBalance,
+    pub balance: Uint128,
+    pub claimed_balance: Uint128,
+    pub denom: CheckedDenom,
     pub start_time: u64,
     pub end_time: u64,
     pub paused_time: Option<u64>,
diff --git a/contracts/external/cw-payroll/src/state.rs b/contracts/external/cw-escrow-streams/src/state.rs
similarity index 58%
rename from contracts/external/cw-payroll/src/state.rs
rename to contracts/external/cw-escrow-streams/src/state.rs
index ae40a45e..e111f838 100644
--- a/contracts/external/cw-payroll/src/state.rs
+++ b/contracts/external/cw-escrow-streams/src/state.rs
@@ -1,15 +1,15 @@
 use cosmwasm_schema::cw_serde;
-use cosmwasm_std::{Addr, DepsMut, StdResult, Storage, Timestamp, Uint128};
+use cosmwasm_std::{Addr, Response, Timestamp, Uint128};
 use cw_storage_plus::{Item, Map};
 use serde::{Deserialize, Serialize};
 
-use crate::{balance::WrappedBalance, msg::StreamId};
+use crate::ContractError;
+use cw_denom::CheckedDenom;
 
 #[cw_serde]
 pub struct Config {
     pub admin: Addr,
 }
-
 pub const CONFIG: Item<Config> = Item::new("config");
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
@@ -17,8 +17,9 @@ pub struct Stream {
     pub admin: Addr,
     pub recipient: Addr,
     /// Balance in Native and Cw20 tokens
-    pub balance: WrappedBalance,
-    pub claimed_balance: WrappedBalance,
+    pub balance: Uint128,
+    pub claimed_balance: Uint128,
+    pub denom: CheckedDenom,
     pub start_time: u64,
     pub end_time: u64,
     pub paused_time: Option<u64>,
@@ -40,20 +41,23 @@ pub struct Stream {
 
 impl Stream {
     pub(crate) fn can_distribute_more(&self) -> bool {
-        if self.balance.amount() == 0 {
+        if self.balance == Uint128::zero() {
             return false;
         }
-        self.claimed_balance.amount() < self.balance.amount()
+        self.claimed_balance < self.balance
     }
+
     pub(crate) fn calc_distribution_rate_core(
         start_time: u64,
         end_time: u64,
         block_time: Timestamp,
         paused_duration: Option<u64>,
-        balance: &WrappedBalance,
-        claimed: &WrappedBalance,
-    ) -> (Uint128, Uint128) {
+        balance: Uint128,
+        claimed_balance: Uint128,
+    ) -> Result<(Uint128, Uint128), ContractError> {
+        // TODO rename? This is not strictly speaking block time
         let block_time = std::cmp::min(block_time.seconds(), end_time);
+        // TODO NO UNWRAP OR DEFAULT, throw real errors
         let duration: u64 = end_time.checked_sub(start_time).unwrap_or_default();
         if duration > 0 {
             let diff = block_time.checked_sub(start_time).unwrap_or_default();
@@ -63,32 +67,32 @@ impl Stream {
                 .unwrap_or_default()
                 .into();
 
-            let rate_per_second = balance
-                .amount()
-                .checked_div(duration.into())
-                .unwrap_or_default();
+            let rate_per_second = balance.checked_div(duration.into()).unwrap_or_default();
 
-            return (
-                Uint128::from(
-                    (passed * rate_per_second)
-                        .checked_sub(claimed.amount())
-                        .unwrap_or_default(),
-                ),
-                Uint128::from(rate_per_second),
-            );
+            return Ok((
+                (Uint128::from(passed) * rate_per_second)
+                    .checked_sub(claimed_balance)
+                    .unwrap_or_default(),
+                rate_per_second,
+            ));
         }
-        (Uint128::new(0), Uint128::new(0))
+        Ok((Uint128::new(0), Uint128::new(0)))
     }
-    pub(crate) fn calc_distribution_rate(&self, block_time: Timestamp) -> (Uint128, Uint128) {
+
+    pub(crate) fn calc_distribution_rate(
+        &self,
+        block_time: Timestamp,
+    ) -> Result<(Uint128, Uint128), ContractError> {
         Stream::calc_distribution_rate_core(
             self.start_time,
             self.end_time,
             block_time,
             self.paused_duration,
-            &self.balance,
-            &self.claimed_balance,
+            self.balance,
+            self.claimed_balance,
         )
     }
+
     pub(crate) fn calc_pause_duration(&self, block_time: Timestamp) -> Option<u64> {
         let end = std::cmp::min(block_time.seconds(), self.end_time);
         self.paused_duration.unwrap_or_default().checked_add(
@@ -97,26 +101,29 @@ impl Stream {
         )
     }
 }
-pub const STREAM_SEQ: Item<u64> = Item::new("stream_seq");
-pub const STREAMS: Map<StreamId, Stream> = Map::new("stream");
 
-pub fn add_stream(deps: DepsMut, stream: &Stream) -> StdResult<StreamId> {
-    let id = STREAM_SEQ.load(deps.storage)?;
-    let id = id.checked_add(1).unwrap();
-    STREAM_SEQ.save(deps.storage, &id)?;
-    STREAMS.save(deps.storage, id, stream)?;
-    Ok(id)
+pub type StreamId = u64;
+pub type StreamIds = Vec<StreamId>;
+pub type ContractResult = Result<Response, ContractError>;
+
+pub(crate) trait StreamIdsExtensions {
+    fn second(&self) -> Option<&StreamId>;
+    fn validate(&self) -> Result<(), ContractError>;
 }
-pub fn save_stream(
-    storage: &mut dyn Storage,
-    id: StreamId,
-    stream: &Stream,
-) -> StdResult<StreamId> {
-    STREAMS.save(storage, id, stream)?;
-    Ok(id)
+impl StreamIdsExtensions for StreamIds {
+    fn second(&self) -> Option<&StreamId> {
+        self.get(1)
+    }
+    fn validate(&self) -> Result<(), ContractError> {
+        if self.len() != 2 {
+            return Err(ContractError::InvalidStreamIds {});
+        }
+        if self.first() == self.second() {
+            return Err(ContractError::StreamsShouldNotBeEqual {});
+        }
+        Ok(())
+    }
 }
 
-pub fn remove_stream(storage: &mut dyn Storage, stream_id: StreamId) -> StdResult<StreamId> {
-    STREAMS.remove(storage, stream_id);
-    Ok(stream_id)
-}
+pub const STREAM_SEQ: Item<u64> = Item::new("stream_seq");
+pub const STREAMS: Map<StreamId, Stream> = Map::new("stream");
diff --git a/contracts/external/cw-payroll/README.md b/contracts/external/cw-payroll/README.md
deleted file mode 100644
index f954aede..00000000
--- a/contracts/external/cw-payroll/README.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# CW Payroll
-
-This contract enables the creation of native && cw20 token streams, which allows a cw20 payment to be vested continuously over time. This contract is forked off of [cw20-streams](https://github.com/CosmWasm/cw-tokens/tree/main/contracts/cw20-streams) to enable additional features required by DAOs for payroll. Key items being: Admin, configurations for vesting, allowing external parties to distribute & more.
-
-## Instantiation
-
-To instantiate a new instance of this contract you must specify a contract owner.
-
-## Creating a Native Token Stream
-
-TBD: Update upon native token completion
-
-## Creating a CW20 Stream
-A stream can be created using the cw20 [Send / Receive](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md#receiver) flow. This involves triggering a Send message from the cw20 token contract, with a Receive callback that's sent to the token streaming contract. The callback message must include the start time and end time of the stream in seconds, as well as the payment recipient. 
-
-## Distribute payments
-Streamed payments can be claimed continously at any point after the start time by triggering a Distribute message.
-
-## Development
-### Compiling
-
-To generate a development build run:
-```
-cargo build
-```
-
-To generate an optimized build run:
-
-```
-docker run --rm -v "$(pwd)":/code \
-  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
-  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
-  cosmwasm/rust-optimizer:0.12.4
-```
-
-### Testing
-To execute unit tests run:
-```
-cargo test
-```
-
-### Lint
-To lint repo run:
-```
-cargo fmt
-```
-
diff --git a/contracts/external/cw-payroll/src/balance.rs b/contracts/external/cw-payroll/src/balance.rs
deleted file mode 100644
index 951dda68..00000000
--- a/contracts/external/cw-payroll/src/balance.rs
+++ /dev/null
@@ -1,229 +0,0 @@
-use cosmwasm_schema::cw_serde;
-use cosmwasm_std::{Addr, Coin, OverflowError, OverflowOperation::Sub, StdError, Uint128};
-use cw20::{Balance, Cw20CoinVerified, Cw20ReceiveMsg};
-use cw_utils::NativeBalance;
-
-use crate::error::GenericError;
-#[cw_serde]
-#[derive(Default)]
-pub struct WrappedBalance(Balance);
-
-impl WrappedBalance {
-    pub fn is_native(&self) -> bool {
-        matches!(self.0, Balance::Native(_))
-    }
-    pub fn is_cw20(&self) -> bool {
-        matches!(self.0, Balance::Cw20(_))
-    }
-    pub fn native(&self) -> Option<&Coin> {
-        match &self.0 {
-            Balance::Native(nb) => nb.0.get(0),
-            Balance::Cw20(_) => None,
-        }
-    }
-
-    pub fn cw20(&self) -> Option<&Cw20CoinVerified> {
-        match &self.0 {
-            Balance::Native(_) => None,
-            Balance::Cw20(cw20) => Some(cw20),
-        }
-    }
-    pub fn new_native_from_coin(native: Coin) -> Self {
-        WrappedBalance(Balance::Native(NativeBalance(vec![native])))
-    }
-    pub fn new_native(denom: String, amount: Uint128) -> Self {
-        Self::new_native_from_coin(Coin { denom, amount })
-    }
-
-    pub fn new_cw20_from_coin(cw20: Cw20CoinVerified) -> Self {
-        WrappedBalance(Balance::Cw20(cw20))
-    }
-    pub fn new_cw20(address: Addr, amount: Uint128) -> Self {
-        Self::new_cw20_from_coin(Cw20CoinVerified { address, amount })
-    }
-    pub fn amount(&self) -> u128 {
-        match &self.0 {
-            Balance::Native(nb) => {
-                if let Some(it) = nb.0.get(0) {
-                    it.amount.u128()
-                } else {
-                    0
-                }
-            }
-            Balance::Cw20(cw20) => cw20.amount.u128(),
-        }
-    }
-}
-impl From<Balance> for WrappedBalance {
-    fn from(balance: Balance) -> WrappedBalance {
-        WrappedBalance(balance)
-    }
-}
-impl From<Cw20ReceiveMsg> for WrappedBalance {
-    fn from(msg: Cw20ReceiveMsg) -> WrappedBalance {
-        WrappedBalance::new_cw20(Addr::unchecked(msg.sender), msg.amount)
-    }
-}
-impl From<WrappedBalance> for Balance {
-    fn from(wb: WrappedBalance) -> Balance {
-        wb.0
-    }
-}
-impl From<WrappedBalance> for Option<Balance> {
-    fn from(wb: WrappedBalance) -> Option<Balance> {
-        Some(wb.0)
-    }
-}
-
-pub trait FindAndMutate<'a, T, Rhs = &'a T>
-where
-    Self: IntoIterator<Item = T>,
-{
-    /// Safely adding and adding amount
-    fn find_checked_add(&mut self, add: Rhs) -> Result<(), GenericError>;
-    /// Safely finding and subtracting amount and remove it if it's zero
-    fn find_checked_sub(&mut self, sub: Rhs) -> Result<(), GenericError>;
-}
-pub trait BalancesOperations<'a, T, Rhs> {
-    fn checked_add_coins(&mut self, add: Rhs) -> Result<(), GenericError>;
-    fn checked_sub_coins(&mut self, sub: Rhs) -> Result<(), GenericError>;
-}
-impl<'a, T, Rhs> BalancesOperations<'a, T, Rhs> for Vec<T>
-where
-    Rhs: IntoIterator<Item = &'a T>,
-    Self: FindAndMutate<'a, T>,
-    T: 'a,
-{
-    fn checked_add_coins(&mut self, add: Rhs) -> Result<(), GenericError> {
-        for add_token in add {
-            self.find_checked_add(add_token)?;
-        }
-        Ok(())
-    }
-
-    fn checked_sub_coins(&mut self, sub: Rhs) -> Result<(), GenericError> {
-        for sub_token in sub {
-            self.find_checked_sub(sub_token)?;
-        }
-        Ok(())
-    }
-}
-impl FindAndMutate<'_, Coin> for Vec<Coin> {
-    fn find_checked_add(&mut self, add: &Coin) -> Result<(), GenericError> {
-        let token = self.iter_mut().find(|exist| exist.denom == add.denom);
-        match token {
-            Some(exist) => {
-                exist.amount = exist
-                    .amount
-                    .checked_add(add.amount)
-                    .map_err(StdError::overflow)?
-            }
-            None => self.push(add.clone()),
-        }
-        Ok(())
-    }
-
-    fn find_checked_sub(&mut self, sub: &Coin) -> Result<(), GenericError> {
-        let coin = self.iter().position(|exist| exist.denom == sub.denom);
-        match coin {
-            Some(exist) => {
-                match self[exist].amount.cmp(&sub.amount) {
-                    std::cmp::Ordering::Less => {
-                        return Err(GenericError::Std(StdError::overflow(OverflowError::new(
-                            Sub,
-                            self[exist].amount,
-                            sub.amount,
-                        ))))
-                    }
-                    std::cmp::Ordering::Equal => {
-                        self.swap_remove(exist);
-                    }
-                    std::cmp::Ordering::Greater => self[exist].amount -= sub.amount,
-                };
-                Ok(())
-            }
-            None => Err(GenericError::EmptyBalance {}),
-        }
-    }
-}
-
-impl FindAndMutate<'_, Cw20CoinVerified> for Vec<Cw20CoinVerified> {
-    fn find_checked_add(&mut self, add: &Cw20CoinVerified) -> Result<(), GenericError> {
-        let token = self.iter_mut().find(|exist| exist.address == add.address);
-        match token {
-            Some(exist) => {
-                exist.amount = exist
-                    .amount
-                    .checked_add(add.amount)
-                    .map_err(StdError::overflow)?
-            }
-            None => self.push(add.clone()),
-        }
-        Ok(())
-    }
-
-    fn find_checked_sub(&mut self, sub: &Cw20CoinVerified) -> Result<(), GenericError> {
-        let coin_p = self.iter().position(|exist| exist.address == sub.address);
-        match coin_p {
-            Some(exist) => {
-                match self[exist].amount.cmp(&sub.amount) {
-                    std::cmp::Ordering::Less => {
-                        return Err(GenericError::Std(StdError::overflow(OverflowError::new(
-                            Sub,
-                            self[exist].amount,
-                            sub.amount,
-                        ))))
-                    }
-                    std::cmp::Ordering::Equal => {
-                        self.swap_remove(exist);
-                    }
-                    std::cmp::Ordering::Greater => self[exist].amount -= sub.amount,
-                };
-
-                Ok(())
-            }
-            None => Err(GenericError::EmptyBalance {}),
-        }
-    }
-}
-impl WrappedBalance {
-    pub fn checked_add_native(&mut self, add: &[Coin]) -> Result<(), GenericError> {
-        if let Balance::Native(nb) = self.0.clone() {
-            let mut coins = nb.0;
-            coins.checked_add_coins(add).unwrap();
-            self.0 = Balance::Native(NativeBalance(coins));
-            return Ok(());
-        }
-        Err(GenericError::EmptyBalance {})
-    }
-
-    pub fn checked_add_cw20(&mut self, add: &[Cw20CoinVerified]) -> Result<(), GenericError> {
-        if let Balance::Cw20(cw20) = self.0.clone() {
-            let mut coins = vec![cw20];
-            coins.checked_add_coins(add).unwrap();
-            self.0 = Balance::Cw20(coins.get(0).unwrap().clone());
-            return Ok(());
-        }
-        Err(GenericError::EmptyBalance {})
-    }
-
-    pub fn checked_sub_native(&mut self, sub: &[Coin]) -> Result<(), GenericError> {
-        if let Balance::Native(nb) = self.0.clone() {
-            let mut coins = nb.0;
-            coins.checked_sub_coins(sub).unwrap();
-            self.0 = Balance::Native(NativeBalance(coins));
-            return Ok(());
-        }
-        Err(GenericError::EmptyBalance {})
-    }
-
-    pub fn checked_sub_cw20(&mut self, sub: &[Cw20CoinVerified]) -> Result<(), GenericError> {
-        if let Balance::Cw20(cw20) = self.0.clone() {
-            let mut coins = vec![cw20];
-            coins.checked_sub_coins(sub).unwrap();
-            self.0 = Balance::Cw20(coins.get(0).unwrap().clone());
-            return Ok(());
-        }
-        Err(GenericError::EmptyBalance {})
-    }
-}
diff --git a/contracts/external/cw-payroll/src/contract.rs b/contracts/external/cw-payroll/src/contract.rs
deleted file mode 100644
index 0d5dacc5..00000000
--- a/contracts/external/cw-payroll/src/contract.rs
+++ /dev/null
@@ -1,1462 +0,0 @@
-use std::vec;
-
-use crate::balance::*;
-use crate::error::{ContractError, GenericError};
-use crate::linking::*;
-use crate::msg::{
-    ConfigResponse, ContractResult, ExecuteMsg, InstantiateMsg, ListStreamsResponse, QueryMsg,
-    ReceiveMsg, StreamId, StreamParams, StreamResponse,
-};
-use crate::state::{
-    add_stream, remove_stream, save_stream, Config, Stream, CONFIG, STREAMS, STREAM_SEQ,
-};
-#[cfg(not(feature = "library"))]
-use cosmwasm_std::entry_point;
-use cosmwasm_std::{
-    from_binary, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo,
-    Order, Response, StdResult, Storage, Uint128,
-};
-use cw2::set_contract_version;
-use cw20::{Cw20CoinVerified, Cw20Contract, Cw20ExecuteMsg, Cw20ReceiveMsg};
-use cw_storage_plus::Bound;
-
-const CONTRACT_NAME: &str = "crates.io:cw-escrow-streams";
-const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
-
-#[cfg_attr(not(feature = "library"), entry_point)]
-pub fn instantiate(
-    deps: DepsMut,
-    _env: Env,
-    info: MessageInfo,
-    msg: InstantiateMsg,
-) -> ContractResult {
-    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
-
-    let admin = match msg.admin {
-        Some(ad) => deps.api.addr_validate(&ad)?,
-        None => info.sender,
-    };
-
-    let config = Config {
-        admin: admin.clone(),
-    };
-    CONFIG.save(deps.storage, &config)?;
-
-    STREAM_SEQ.save(deps.storage, &0u64)?;
-
-    Ok(Response::new()
-        .add_attribute("method", "instantiate")
-        .add_attribute("admin", admin))
-}
-
-#[cfg_attr(not(feature = "library"), entry_point)]
-pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> ContractResult {
-    match msg {
-        ExecuteMsg::Receive(msg) => execute_receive(env, deps, info, msg),
-        ExecuteMsg::Distribute { id } => execute_distribute(env, deps, id),
-        ExecuteMsg::PauseStream { id } => execute_pause_stream(env, deps, info, id),
-        ExecuteMsg::ResumeStream { id } => execute_resume_stream(env, deps, info, id),
-        ExecuteMsg::RemoveStream { id } => execute_remove_stream(env, deps, info, id),
-        ExecuteMsg::LinkStream { ids } => execute_link_stream(deps, &info, ids),
-        ExecuteMsg::DetachStream { id } => execute_detach_stream(env, deps, &info, id),
-    }
-}
-
-pub fn execute_pause_stream(
-    env: Env,
-    deps: DepsMut,
-    info: MessageInfo,
-    id: StreamId,
-) -> ContractResult {
-    let pause_stream_local =
-        |stream_id: StreamId, storage: &mut dyn Storage| -> Result<Stream, ContractError> {
-            let mut stream = STREAMS
-                .may_load(storage, stream_id)?
-                .ok_or(ContractError::StreamNotFound { stream_id: id })?;
-            if stream.admin != info.sender {
-                return Err(ContractError::Unauthorized {});
-            }
-            if stream.paused {
-                return Err(ContractError::StreamAlreadyPaused {});
-            }
-            stream.paused_time = Some(env.block.time.seconds());
-            stream.paused = true;
-            save_stream(storage, id, &stream).unwrap();
-            Ok(stream)
-        };
-    let stream = pause_stream_local(id, deps.storage).unwrap();
-
-    if let Some(link_id) = stream.link_id {
-        pause_stream_local(link_id, deps.storage).unwrap();
-    }
-    let response = Response::new()
-        .add_attribute("method", "pause_stream")
-        .add_attribute("paused", stream.paused.to_string())
-        .add_attribute("stream_id", id.to_string())
-        .add_attribute("admin", info.sender)
-        .add_attribute("paused_time", stream.paused_time.unwrap().to_string());
-
-    Ok(response)
-}
-pub fn execute_remove_stream(
-    env: Env,
-    deps: DepsMut,
-    info: MessageInfo,
-    id: StreamId,
-) -> ContractResult {
-    let stream = STREAMS
-        .may_load(deps.storage, id)?
-        .ok_or(ContractError::StreamNotFound { stream_id: id })?;
-
-    if stream.admin != info.sender {
-        return Err(ContractError::Unauthorized {});
-    }
-    if let Some(link_id) = stream.link_id {
-        return Err(ContractError::LinkedStreamDeleteNotAllowed { link_id });
-    }
-    remove_stream(deps.storage, id).unwrap();
-    let response = Response::new()
-        .add_attribute("method", "remove_stream")
-        .add_attribute("stream_id", id.to_string())
-        .add_attribute("admin", info.sender)
-        .add_attribute("removed_time", env.block.time.to_string())
-        .add_message(
-            create_balance_transfer_msg(&stream.balance, None, stream.admin.clone().into())
-                .unwrap(),
-        );
-    Ok(response)
-}
-fn create_balance_transfer_msg(
-    balance: &WrappedBalance,
-    amount: Option<Uint128>,
-    recepient: String,
-) -> Result<CosmosMsg, ContractError> {
-    if let Some(native_balance) = balance.native() {
-        let msg = CosmosMsg::Bank(BankMsg::Send {
-            to_address: recepient,
-            amount: vec![Coin {
-                denom: native_balance.denom.clone(),
-                amount: amount.unwrap_or(native_balance.amount),
-            }],
-        });
-        return Ok(msg);
-    } else if let Some(cw20_balance) = balance.cw20() {
-        let cw20 = Cw20Contract(cw20_balance.address.clone());
-        let msg = cw20.call(Cw20ExecuteMsg::Transfer {
-            recipient: recepient,
-            amount: amount.unwrap_or(cw20_balance.amount),
-        })?;
-        return Ok(msg);
-    }
-    Err(ContractError::CouldNotCreateBankMessage {})
-}
-pub fn execute_resume_stream(
-    env: Env,
-    deps: DepsMut,
-    info: MessageInfo,
-    id: StreamId,
-) -> ContractResult {
-    let resume_stream_local =
-        |stream_id: StreamId, storage: &mut dyn Storage| -> Result<Stream, ContractError> {
-            let mut stream = STREAMS
-                .may_load(storage, stream_id)?
-                .ok_or(ContractError::StreamNotFound { stream_id: id })?;
-            if stream.admin != info.sender {
-                return Err(ContractError::Unauthorized {});
-            }
-            if !stream.paused {
-                return Err(ContractError::StreamNotPaused {});
-            }
-            stream.paused_duration = stream.calc_pause_duration(env.block.time);
-            stream.paused = false;
-            stream.paused_time = None;
-            save_stream(storage, id, &stream).unwrap();
-            Ok(stream)
-        };
-
-    let stream = resume_stream_local(id, deps.storage)?;
-    if let Some(link_id) = stream.link_id {
-        resume_stream_local(link_id, deps.storage).unwrap();
-    }
-    let (_, rate_per_second) = stream.calc_distribution_rate(env.block.time);
-    let response = Response::new()
-        .add_attribute("method", "resume_stream")
-        .add_attribute("stream_id", id.to_string())
-        .add_attribute("admin", info.sender)
-        .add_attribute("rate_per_second", rate_per_second)
-        .add_attribute("resume_time", env.block.time.to_string())
-        .add_attribute(
-            "paused_duration",
-            stream.paused_duration.unwrap().to_string(),
-        )
-        .add_attribute("resume_time", env.block.time.to_string());
-
-    Ok(response)
-}
-pub fn execute_create_stream(env: Env, deps: DepsMut, params: StreamParams) -> ContractResult {
-    let StreamParams {
-        admin,
-        recipient,
-        balance,
-        start_time,
-        end_time,
-        title,
-        description,
-        is_detachable,
-    } = params;
-
-    let admin = deps.api.addr_validate(&admin)?;
-    let recipient = deps.api.addr_validate(&recipient)?;
-
-    if start_time > end_time {
-        return Err(ContractError::InvalidStartTime {});
-    }
-
-    let block_time = env.block.time.seconds();
-
-    if end_time <= block_time {
-        return Err(ContractError::InvalidEndTime {});
-    }
-
-    let duration: u128 = (end_time - start_time).into();
-    let balance_amount = balance.amount();
-    let refund: u128 = balance_amount
-        .checked_rem(duration)
-        .ok_or(GenericError::IntegerOverflow {})
-        .unwrap();
-
-    if balance_amount < duration {
-        return Err(ContractError::AmountLessThanDuration {});
-    }
-
-    let claimed = if balance.is_native() {
-        WrappedBalance::default()
-    } else {
-        WrappedBalance::new_cw20(balance.cw20().unwrap().address.clone(), Uint128::new(0))
-    };
-    let stream = Stream {
-        admin: admin.clone(),
-        recipient: recipient.clone(),
-        balance: balance.clone(),
-        claimed_balance: claimed,
-        start_time,
-        end_time,
-        paused_time: None,
-        paused_duration: None,
-        paused: false,
-        title,
-        description,
-        link_id: None,
-        is_detachable: is_detachable.unwrap_or(true),
-    };
-    let id = add_stream(deps, &stream)?;
-
-    let mut response = Response::new()
-        .add_attribute("method", "create_stream")
-        .add_attribute("stream_id", id.to_string())
-        .add_attribute("admin", admin.clone())
-        .add_attribute("recipient", recipient)
-        .add_attribute("start_time", start_time.to_string())
-        .add_attribute("end_time", end_time.to_string());
-
-    if refund > 0 {
-        response = response.add_messages(create_balance_transfer_msg(
-            &balance,
-            Some(Uint128::new(refund)),
-            admin.into(),
-        ));
-    }
-    Ok(response)
-}
-
-pub fn execute_receive(
-    env: Env,
-    deps: DepsMut,
-    info: MessageInfo,
-    wrapped: Cw20ReceiveMsg,
-) -> ContractResult {
-    deps.api.addr_validate(&info.sender.into_string())?;
-    let msg: ReceiveMsg = from_binary(&wrapped.msg)?;
-    match msg {
-        ReceiveMsg::CreateStream {
-            admin,
-            start_time,
-            end_time,
-            recipient,
-            is_detachable,
-        } => execute_create_stream(
-            env,
-            deps,
-            StreamParams {
-                admin: admin.unwrap_or_else(|| wrapped.sender.clone()),
-                recipient,
-                balance: WrappedBalance::from(wrapped),
-                start_time,
-                end_time,
-                title: None,
-                description: None,
-                is_detachable,
-            },
-        ),
-    }
-}
-
-pub fn execute_distribute(env: Env, deps: DepsMut, id: u64) -> ContractResult {
-    let mut stream = STREAMS
-        .may_load(deps.storage, id)?
-        .ok_or(ContractError::StreamNotFound { stream_id: id })?;
-
-    let mut msgs: Vec<CosmosMsg> = vec![];
-    let (available_claims, _) = stream.calc_distribution_rate(env.block.time);
-
-    if !stream.can_distribute_more() || available_claims.u128() == 0 {
-        return Err(ContractError::NoFundsToClaim {
-            claimed: stream.claimed_balance,
-        });
-    }
-    if let Some(native_balance) = stream.balance.native() {
-        let to_claim = Coin {
-            denom: native_balance.denom.clone(),
-            amount: available_claims,
-        };
-
-        stream
-            .claimed_balance
-            .checked_add_native(&[to_claim.clone()])
-            .unwrap();
-        let msg = CosmosMsg::Bank(BankMsg::Send {
-            to_address: stream.recipient.clone().into(),
-            amount: vec![Coin {
-                denom: native_balance.denom.clone(),
-                amount: to_claim.amount,
-            }],
-        });
-        msgs.push(msg);
-        stream.balance.checked_sub_native(&[to_claim]).unwrap();
-    } else if let Some(cw20) = stream.balance.cw20() {
-        let to_claim = Cw20CoinVerified {
-            address: cw20.address.clone(),
-            amount: available_claims,
-        };
-        stream
-            .claimed_balance
-            .checked_add_cw20(&[to_claim.clone()])
-            .unwrap();
-
-        let cw20 = Cw20Contract(cw20.address.clone());
-        let msg = cw20.call(Cw20ExecuteMsg::Transfer {
-            recipient: stream.recipient.clone().into(),
-            amount: to_claim.amount,
-        })?;
-        msgs.push(msg);
-        stream.balance.checked_sub_cw20(&[to_claim]).unwrap();
-    }
-
-    save_stream(deps.storage, id, &stream).unwrap();
-
-    let mut res = Response::new()
-        .add_attribute("method", "distribute")
-        .add_attribute("vested", available_claims)
-        .add_attribute("stream_id", id.to_string());
-    if !msgs.is_empty() {
-        res = res.add_messages(msgs);
-    }
-    Ok(res)
-}
-
-#[cfg_attr(not(feature = "library"), entry_point)]
-pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
-    match msg {
-        QueryMsg::GetConfig {} => to_binary(&query_config(deps)?),
-        QueryMsg::GetStream { id } => to_binary(&query_stream(deps, id)?),
-        QueryMsg::ListStreams { start, limit } => {
-            to_binary(&query_list_streams(deps, start, limit)?)
-        }
-    }
-}
-
-fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
-    let config = CONFIG.load(deps.storage)?;
-    Ok(ConfigResponse {
-        admin: config.admin.into(),
-    })
-}
-
-fn query_stream(deps: Deps, id: u64) -> StdResult<StreamResponse> {
-    let stream = STREAMS.load(deps.storage, id)?;
-    Ok(StreamResponse {
-        id,
-        admin: stream.admin.into(),
-        recipient: stream.recipient.into(),
-        balance: stream.balance,
-        claimed_balance: stream.claimed_balance,
-        start_time: stream.start_time,
-        end_time: stream.end_time,
-        title: stream.title,
-        description: stream.description,
-        paused_time: stream.paused_time,
-        paused_duration: stream.paused_duration,
-        paused: stream.paused,
-        is_detachable: stream.is_detachable,
-        link_id: stream.link_id,
-    })
-}
-
-fn query_list_streams(
-    deps: Deps,
-    start: Option<u8>,
-    limit: Option<u8>,
-) -> StdResult<ListStreamsResponse> {
-    let start = start.map(Bound::inclusive);
-    let limit = limit.unwrap_or(5);
-
-    let streams = STREAMS
-        .range(deps.storage, start, None, Order::Ascending)
-        .take(limit.into())
-        .map(map_stream)
-        .collect::<StdResult<Vec<_>>>()?;
-    Ok(ListStreamsResponse { streams })
-}
-
-fn map_stream(item: StdResult<(u64, Stream)>) -> StdResult<StreamResponse> {
-    item.map(|(id, stream)| StreamResponse {
-        id,
-        admin: stream.admin.to_string(),
-        recipient: stream.recipient.to_string(),
-        balance: stream.balance,
-        claimed_balance: stream.claimed_balance,
-        start_time: stream.start_time,
-        end_time: stream.end_time,
-        title: stream.title,
-        description: stream.description,
-        paused_time: stream.paused_time,
-        paused_duration: stream.paused_duration,
-        paused: stream.paused,
-        is_detachable: stream.is_detachable,
-        link_id: stream.link_id,
-    })
-}
-
-#[cfg(test)]
-mod tests {
-
-    use crate::msg::StreamIdsExtensions;
-
-    use super::*;
-    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
-    use cosmwasm_std::{Addr, CosmosMsg, StdError, WasmMsg};
-
-    fn get_stream(deps: Deps, id: u64) -> StdResult<Stream> {
-        let msg = QueryMsg::GetStream { id };
-        let res = query(deps, mock_env(), msg)?;
-        from_binary::<Stream>(&res)
-    }
-
-    #[test]
-    fn test_initialization() {
-        let mut deps = mock_dependencies();
-        let msg = InstantiateMsg { admin: None };
-
-        let info = mock_info("creator", &[]);
-        instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
-
-        let msg = QueryMsg::GetConfig {};
-        let res = query(deps.as_ref(), mock_env(), msg).unwrap();
-        let config: Config = from_binary(&res).unwrap();
-
-        assert_eq!(
-            config,
-            Config {
-                admin: Addr::unchecked("creator")
-            }
-        );
-    }
-
-    #[test]
-    fn test_execute_distribute() {
-        let mut deps = mock_dependencies();
-        let msg = InstantiateMsg { admin: None };
-        let info = mock_info("cw20", &[]);
-        instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let admin_addr = Addr::unchecked("cw20");
-        let sender = admin_addr.to_string();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(1000);
-
-        let balance = WrappedBalance::new_cw20(Addr::unchecked("cw20"), amount);
-        let claimed = WrappedBalance::new_cw20(Addr::unchecked("cw20"), Uint128::new(0));
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(300).seconds();
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender),
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), env.clone(), info, msg).unwrap();
-
-        assert_eq!(
-            get_stream(deps.as_ref(), 1).unwrap(),
-            Stream {
-                admin: admin_addr.clone(),
-                recipient: Addr::unchecked("bob"),
-                balance,
-                claimed_balance: claimed.clone(),
-                start_time,
-                end_time,
-                title: None,
-                description: None,
-                paused: false,
-                paused_time: None,
-                paused_duration: None,
-                link_id: None,
-                is_detachable: true,
-            }
-        );
-
-        // Stream has not started
-        let mut info = mock_info("owner", &[]);
-        info.sender = Addr::unchecked("bob");
-        let msg = ExecuteMsg::Distribute { id: 1 };
-        let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
-        assert_eq!(err, ContractError::NoFundsToClaim { claimed });
-
-        // Stream has started so tokens have vested
-        let msg = ExecuteMsg::Distribute { id: 1 };
-        let mut info = mock_info("owner", &[]);
-        let mut env = mock_env();
-        let sender = Addr::unchecked("bob");
-        info.sender = sender;
-        env.block.time = env.block.time.plus_seconds(150);
-        let res = execute(deps.as_mut(), env, info, msg).unwrap();
-        let msg = res.messages[0].clone().msg;
-
-        assert_eq!(
-            msg,
-            CosmosMsg::Wasm(WasmMsg::Execute {
-                contract_addr: String::from("cw20"),
-                msg: to_binary(&Cw20ExecuteMsg::Transfer {
-                    recipient: String::from("bob"),
-                    amount: Uint128::new(250)
-                })
-                .unwrap(),
-                funds: vec![]
-            })
-        );
-        assert_eq!(
-            get_stream(deps.as_ref(), 1).unwrap(),
-            Stream {
-                admin: admin_addr,
-                recipient: Addr::unchecked("bob"),
-                balance: WrappedBalance::new_cw20(Addr::unchecked("cw20"), Uint128::new(750)),
-                claimed_balance: WrappedBalance::new_cw20(
-                    Addr::unchecked("cw20"),
-                    Uint128::new(250)
-                ),
-                start_time,
-                end_time,
-                title: None,
-                description: None,
-                paused: false,
-                paused_time: None,
-                paused_duration: None,
-                link_id: None,
-                is_detachable: true,
-            }
-        );
-
-        //Check final balances after distribution
-    }
-
-    #[test]
-    fn test_create_stream_with_refund() {
-        let mut deps = mock_dependencies();
-        let msg = InstantiateMsg { admin: None };
-        let info = mock_info("cw20", &[]);
-        instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let recipient = Addr::unchecked("bob");
-        let sender = info.sender.clone().into_string();
-        let sender_addr = info.sender.clone();
-
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: None,
-                recipient: recipient.into_string(),
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-
-        // Make sure remaining funds were refunded if duration didn't divide evenly into amount
-        let res = execute(deps.as_mut(), env, info, msg).unwrap();
-        let refund_msg = res.messages[0].clone().msg;
-        assert_eq!(
-            refund_msg,
-            CosmosMsg::Wasm(WasmMsg::Execute {
-                contract_addr: String::from("cw20"),
-                msg: to_binary(&Cw20ExecuteMsg::Transfer {
-                    recipient: sender,
-                    amount: Uint128::new(50)
-                })
-                .unwrap(),
-                funds: vec![]
-            })
-        );
-
-        let balance_amount = Uint128::new(350);
-        let balance = WrappedBalance::new_cw20(Addr::unchecked("cw20"), balance_amount);
-
-        assert_eq!(
-            get_stream(deps.as_ref(), 1).unwrap(),
-            Stream {
-                admin: sender_addr,
-                recipient: Addr::unchecked("bob"),
-                balance, // original amount - refund
-                claimed_balance: WrappedBalance::new_cw20(Addr::unchecked("cw20"), Uint128::new(0)),
-                start_time,
-                end_time,
-                title: None,
-                description: None,
-                paused: false,
-                paused_time: None,
-                paused_duration: None,
-                link_id: None,
-                is_detachable: true,
-            }
-        );
-    }
-    #[test]
-    fn test_execute_pause_stream() {
-        let mut deps = mock_dependencies();
-        let msg = InstantiateMsg { admin: None };
-        let sender = Addr::unchecked("alice").to_string();
-
-        let sender_addr = Addr::unchecked("alice");
-        let info = mock_info(&sender, &[]);
-        instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender),
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let stream_id: StreamId = 1;
-
-        let _ = execute(
-            deps.as_mut(),
-            mock_env(),
-            info,
-            ExecuteMsg::PauseStream { id: stream_id },
-        )
-        .unwrap();
-        let balance = WrappedBalance::new_cw20(Addr::unchecked("alice"), amount);
-        let saved_stream = get_stream(deps.as_ref(), stream_id).unwrap();
-        assert_eq!(
-            saved_stream,
-            Stream {
-                admin: Addr::unchecked("alice"),
-                recipient: Addr::unchecked("bob"),
-                balance, // original amount - refund
-                claimed_balance: WrappedBalance::new_cw20(sender_addr, Uint128::new(0)),
-                start_time,
-                end_time,
-                title: None,
-                description: None,
-                paused: true,
-                paused_time: saved_stream.paused_time,
-                paused_duration: saved_stream.paused_duration,
-                link_id: None,
-                is_detachable: true,
-            }
-        );
-    }
-    #[test]
-    fn test_invalid_start_time() {
-        let mut deps = mock_dependencies();
-
-        let msg = InstantiateMsg { admin: None };
-        let mut info = mock_info("alice", &[]);
-        instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let sender = Addr::unchecked("alice").to_string();
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(100);
-        let start_time = mock_env().block.time.plus_seconds(100).seconds();
-        let end_time = mock_env().block.time.plus_seconds(20).seconds();
-
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender,
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: None,
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        info.sender = Addr::unchecked("cw20");
-        let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
-        assert_eq!(err, ContractError::InvalidStartTime {});
-    }
-
-    #[test]
-    fn invalid_cw20_addr() {
-        let mut deps = mock_dependencies();
-
-        let msg = InstantiateMsg { admin: None };
-        let mut info = mock_info("alice", &[]);
-        instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let sender = Addr::unchecked("alice").to_string();
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(100);
-        let start_time = mock_env().block.time.plus_seconds(100).seconds();
-        let end_time = mock_env().block.time.plus_seconds(200).seconds();
-
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender,
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: None,
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        info.sender = Addr::unchecked("wrongCw20");
-        let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
-        assert_eq!(
-            err,
-            ContractError::Std(cosmwasm_std::StdError::GenericErr {
-                msg: "Invalid input: address not normalized".to_string()
-            })
-        );
-    }
-
-    #[test]
-    fn test_invalid_deposit_amount() {
-        let mut deps = mock_dependencies();
-
-        let msg = InstantiateMsg { admin: None };
-        let mut info = mock_info("alice", &[]);
-        instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let sender = Addr::unchecked("alice").to_string();
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(3);
-        let start_time = mock_env().block.time.plus_seconds(100).seconds();
-        let end_time = mock_env().block.time.plus_seconds(200).seconds();
-
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender,
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: None,
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        info.sender = Addr::unchecked("cw20");
-        let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
-        assert_eq!(err, ContractError::AmountLessThanDuration {});
-    }
-    #[test]
-    fn test_execute_remove_stream() {
-        let mut deps = mock_dependencies();
-        let sender = Addr::unchecked("alice").to_string();
-        let info = mock_info(&sender, &[]);
-        instantiate(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            InstantiateMsg { admin: None },
-        )
-        .unwrap();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let stream_id: StreamId = 1;
-
-        //Remove stream and verify not found error returned
-        let remove_response = execute(
-            deps.as_mut(),
-            mock_env(),
-            info,
-            ExecuteMsg::RemoveStream { id: stream_id },
-        )
-        .unwrap();
-        let error = get_stream(deps.as_ref(), stream_id).unwrap_err();
-        assert_eq!(
-            error,
-            StdError::NotFound {
-                kind: "cw_payroll::state::Stream".to_string()
-            }
-        );
-
-        let refund_msg = remove_response.messages[0].clone().msg;
-        assert_eq!(
-            refund_msg,
-            CosmosMsg::Wasm(WasmMsg::Execute {
-                contract_addr: sender.clone(),
-                msg: to_binary(&Cw20ExecuteMsg::Transfer {
-                    recipient: sender,
-                    amount
-                })
-                .unwrap(),
-                funds: vec![]
-            })
-        );
-    }
-    #[test]
-    fn test_execute_link_stream_invalid() {
-        let mut deps = mock_dependencies();
-        let sender = Addr::unchecked("alice").to_string();
-
-        let info = mock_info(&sender, &[]);
-
-        instantiate(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            InstantiateMsg { admin: None },
-        )
-        .unwrap();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        //Create stream 1
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let ids = vec![1, 2];
-
-        //Link stream and verify error returned
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::LinkStream { ids: ids.clone() },
-        )
-        .unwrap_err();
-        assert_eq!(
-            error,
-            ContractError::StreamNotFound {
-                stream_id: *ids.second().unwrap()
-            }
-        );
-
-        let ids = vec![1, 1];
-
-        //Link stream and verify error returned
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::LinkStream { ids },
-        )
-        .unwrap_err();
-        assert_eq!(error, ContractError::StreamsShouldNotBeEqual {});
-
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender),
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info, msg).unwrap();
-
-        let sender = Addr::unchecked("bob").to_string();
-        let ids = vec![1, 2];
-
-        let unauthorized_info = mock_info(&sender, &[]);
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            unauthorized_info,
-            ExecuteMsg::LinkStream { ids },
-        )
-        .unwrap_err();
-        assert_eq!(error, ContractError::Unauthorized {});
-    }
-
-    #[test]
-
-    fn test_execute_link_stream_valid() {
-        let mut deps = mock_dependencies();
-        let sender = Addr::unchecked("alice").to_string();
-
-        let info = mock_info(&sender, &[]);
-
-        instantiate(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            InstantiateMsg { admin: None },
-        )
-        .unwrap();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        //Create stream 1
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        //Create stream 2
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender),
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-        let ids = vec![1, 2];
-        let response = execute(
-            deps.as_mut(),
-            mock_env(),
-            info,
-            ExecuteMsg::LinkStream { ids: ids.clone() },
-        )
-        .unwrap();
-
-        let left_stream = get_stream(deps.as_ref(), *ids.first().unwrap()).unwrap();
-        let right_stream = get_stream(deps.as_ref(), *ids.second().unwrap()).unwrap();
-        assert_eq!(left_stream.link_id, Some(*ids.second().unwrap()));
-        assert_eq!(right_stream.link_id, Some(*ids.first().unwrap()));
-        assert!(response
-            .attributes
-            .iter()
-            .any(|f| { f.key == "left_stream_id" }));
-        assert!(response
-            .attributes
-            .iter()
-            .any(|f| { f.key == "right_stream_id" }));
-    }
-    #[test]
-    fn test_execute_detach_stream_valid() {
-        let mut deps = mock_dependencies();
-        let sender = Addr::unchecked("alice").to_string();
-
-        let info = mock_info(&sender, &[]);
-
-        instantiate(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            InstantiateMsg { admin: None },
-        )
-        .unwrap();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        //Create stream 1
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        //Create stream 2
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender),
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-        let ids = vec![1, 2];
-
-        execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::LinkStream { ids: ids.clone() },
-        )
-        .unwrap();
-
-        execute(
-            deps.as_mut(),
-            mock_env(),
-            info,
-            ExecuteMsg::DetachStream {
-                id: *ids.first().unwrap(),
-            },
-        )
-        .unwrap();
-        let left_stream = get_stream(deps.as_ref(), *ids.first().unwrap()).unwrap();
-        let right_stream = get_stream(deps.as_ref(), *ids.second().unwrap()).unwrap();
-
-        assert!(left_stream.paused);
-        assert!(left_stream.paused_time.is_some());
-        assert!(left_stream.link_id.is_none());
-
-        assert!(right_stream.paused);
-        assert!(right_stream.paused_time.is_some());
-        assert!(right_stream.link_id.is_none());
-    }
-
-    #[test]
-    fn test_execute_detach_stream_invalid() {
-        let mut deps = mock_dependencies();
-        let sender = Addr::unchecked("alice").to_string();
-
-        let info = mock_info(&sender, &[]);
-
-        instantiate(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            InstantiateMsg { admin: None },
-        )
-        .unwrap();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        //Create stream 1
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: Some(false),
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        //Create stream 2
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: Some(false),
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-        let ids = vec![1, 2];
-
-        execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::LinkStream { ids },
-        )
-        .unwrap();
-        let ids = vec![11, 22];
-
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::DetachStream {
-                id: *ids.first().unwrap(),
-            },
-        )
-        .unwrap_err();
-
-        assert_eq!(
-            error,
-            ContractError::StreamNotFound {
-                stream_id: *ids.first().unwrap()
-            }
-        );
-        let ids = vec![1, 22];
-
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::DetachStream {
-                id: *ids.second().unwrap(),
-            },
-        )
-        .unwrap_err();
-
-        assert_eq!(
-            error,
-            ContractError::StreamNotFound {
-                stream_id: *ids.second().unwrap()
-            }
-        );
-        let ids = vec![1, 2];
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::DetachStream {
-                id: *ids.second().unwrap(),
-            },
-        )
-        .unwrap_err();
-
-        assert_eq!(error, ContractError::StreamNotDetachable {});
-
-        let unauthorized_info = mock_info(&recipient, &[]);
-
-        //Create stream 1
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: Some(true),
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        //Create stream 2
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender),
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: Some(true),
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-        let ids = vec![3, 4];
-        execute(
-            deps.as_mut(),
-            mock_env(),
-            info,
-            ExecuteMsg::LinkStream { ids: ids.clone() },
-        )
-        .unwrap();
-
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            unauthorized_info,
-            ExecuteMsg::DetachStream {
-                id: *ids.first().unwrap(),
-            },
-        )
-        .unwrap_err();
-
-        assert_eq!(error, ContractError::Unauthorized {});
-    }
-
-    #[test]
-    fn test_execute_resume_stream_valid() {
-        let mut deps = mock_dependencies();
-        let sender = Addr::unchecked("alice").to_string();
-
-        let info = mock_info(&sender, &[]);
-
-        instantiate(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            InstantiateMsg { admin: None },
-        )
-        .unwrap();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        //Create stream 1
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        //Create stream 2
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender),
-                recipient,
-                start_time,
-                end_time,
-                is_detachable: None,
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-        let ids = vec![1, 2];
-
-        execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::LinkStream { ids: ids.clone() },
-        )
-        .unwrap();
-
-        execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::DetachStream {
-                id: *ids.first().unwrap(),
-            },
-        )
-        .unwrap();
-        let left_stream = get_stream(deps.as_ref(), *ids.first().unwrap()).unwrap();
-        let right_stream = get_stream(deps.as_ref(), *ids.second().unwrap()).unwrap();
-
-        assert!(left_stream.paused);
-        assert!(left_stream.paused_time.is_some());
-        assert!(left_stream.link_id.is_none());
-
-        assert!(right_stream.paused);
-        assert!(right_stream.paused_time.is_some());
-        assert!(right_stream.link_id.is_none());
-
-        execute(
-            deps.as_mut(),
-            mock_env(),
-            info,
-            ExecuteMsg::ResumeStream {
-                id: *ids.first().unwrap(),
-            },
-        )
-        .unwrap();
-        let left_stream = get_stream(deps.as_ref(), *ids.first().unwrap()).unwrap();
-        let right_stream = get_stream(deps.as_ref(), *ids.second().unwrap()).unwrap();
-
-        assert!(!left_stream.paused);
-        assert!(left_stream.paused_time.is_none());
-
-        assert!(right_stream.paused);
-        assert!(right_stream.paused_time.is_some());
-    }
-
-    #[test]
-    fn test_execute_resume_stream_invalid() {
-        let mut deps = mock_dependencies();
-        let sender = Addr::unchecked("alice").to_string();
-
-        let info = mock_info(&sender, &[]);
-
-        instantiate(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            InstantiateMsg { admin: None },
-        )
-        .unwrap();
-
-        let recipient = Addr::unchecked("bob").to_string();
-        let amount = Uint128::new(350);
-        let env = mock_env();
-        let start_time = env.block.time.plus_seconds(100).seconds();
-        let end_time = env.block.time.plus_seconds(400).seconds();
-
-        //Create stream 1
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender.clone()),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: Some(true),
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        //Create stream 2
-        let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
-            sender: sender.clone(),
-            amount,
-            msg: to_binary(&ReceiveMsg::CreateStream {
-                admin: Some(sender),
-                recipient: recipient.clone(),
-                start_time,
-                end_time,
-                is_detachable: Some(false),
-            })
-            .unwrap(),
-        });
-        execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
-
-        let ids = vec![1, 2];
-        execute(
-            deps.as_mut(),
-            mock_env(),
-            info.clone(),
-            ExecuteMsg::LinkStream { ids },
-        )
-        .unwrap();
-        let ids = vec![1, 2];
-
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            info,
-            ExecuteMsg::ResumeStream {
-                id: *ids.second().unwrap(),
-            },
-        )
-        .unwrap_err();
-        assert_eq!(error, ContractError::StreamNotPaused {});
-
-        let ids = vec![1, 2];
-        let unauthorized_info = mock_info(&recipient, &[]);
-        let error = execute(
-            deps.as_mut(),
-            mock_env(),
-            unauthorized_info,
-            ExecuteMsg::DetachStream {
-                id: *ids.first().unwrap(),
-            },
-        )
-        .unwrap_err();
-
-        assert_eq!(error, ContractError::StreamNotDetachable {});
-    }
-}
diff --git a/typescript/contracts/cw-escrow-streams/CwEscrowStreams.client.ts b/typescript/contracts/cw-escrow-streams/CwEscrowStreams.client.ts
new file mode 100644
index 00000000..8d989653
--- /dev/null
+++ b/typescript/contracts/cw-escrow-streams/CwEscrowStreams.client.ts
@@ -0,0 +1,231 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
+import { Coin, StdFee } from "@cosmjs/amino";
+import { InstantiateMsg, ExecuteMsg, Uint128, Binary, CheckedDenom, Addr, Cw20ReceiveMsg, StreamParams, QueryMsg, ConfigResponse, StreamResponse, ListStreamsResponse } from "./CwEscrowStreams.types";
+export interface CwEscrowStreamsReadOnlyInterface {
+  contractAddress: string;
+  getConfig: () => Promise<ConfigResponse>;
+  getStream: ({
+    id
+  }: {
+    id: number;
+  }) => Promise<StreamResponse>;
+  listStreams: ({
+    limit,
+    start
+  }: {
+    limit?: number;
+    start?: number;
+  }) => Promise<ListStreamsResponse>;
+}
+export class CwEscrowStreamsQueryClient implements CwEscrowStreamsReadOnlyInterface {
+  client: CosmWasmClient;
+  contractAddress: string;
+
+  constructor(client: CosmWasmClient, contractAddress: string) {
+    this.client = client;
+    this.contractAddress = contractAddress;
+    this.getConfig = this.getConfig.bind(this);
+    this.getStream = this.getStream.bind(this);
+    this.listStreams = this.listStreams.bind(this);
+  }
+
+  getConfig = async (): Promise<ConfigResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      get_config: {}
+    });
+  };
+  getStream = async ({
+    id
+  }: {
+    id: number;
+  }): Promise<StreamResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      get_stream: {
+        id
+      }
+    });
+  };
+  listStreams = async ({
+    limit,
+    start
+  }: {
+    limit?: number;
+    start?: number;
+  }): Promise<ListStreamsResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      list_streams: {
+        limit,
+        start
+      }
+    });
+  };
+}
+export interface CwEscrowStreamsInterface extends CwEscrowStreamsReadOnlyInterface {
+  contractAddress: string;
+  sender: string;
+  receive: ({
+    amount,
+    msg,
+    sender
+  }: {
+    amount: Uint128;
+    msg: Binary;
+    sender: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  create: ({
+    params
+  }: {
+    params: StreamParams;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  distribute: ({
+    id
+  }: {
+    id: number;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  pauseStream: ({
+    id
+  }: {
+    id: number;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  linkStream: ({
+    ids
+  }: {
+    ids: number[];
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  detachStream: ({
+    id
+  }: {
+    id: number;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  resumeStream: ({
+    id
+  }: {
+    id: number;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  removeStream: ({
+    id
+  }: {
+    id: number;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+}
+export class CwEscrowStreamsClient extends CwEscrowStreamsQueryClient implements CwEscrowStreamsInterface {
+  client: SigningCosmWasmClient;
+  sender: string;
+  contractAddress: string;
+
+  constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) {
+    super(client, contractAddress);
+    this.client = client;
+    this.sender = sender;
+    this.contractAddress = contractAddress;
+    this.receive = this.receive.bind(this);
+    this.create = this.create.bind(this);
+    this.distribute = this.distribute.bind(this);
+    this.pauseStream = this.pauseStream.bind(this);
+    this.linkStream = this.linkStream.bind(this);
+    this.detachStream = this.detachStream.bind(this);
+    this.resumeStream = this.resumeStream.bind(this);
+    this.removeStream = this.removeStream.bind(this);
+  }
+
+  receive = async ({
+    amount,
+    msg,
+    sender
+  }: {
+    amount: Uint128;
+    msg: Binary;
+    sender: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      receive: {
+        amount,
+        msg,
+        sender
+      }
+    }, fee, memo, funds);
+  };
+  create = async ({
+    params
+  }: {
+    params: StreamParams;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      create: {
+        params
+      }
+    }, fee, memo, funds);
+  };
+  distribute = async ({
+    id
+  }: {
+    id: number;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      distribute: {
+        id
+      }
+    }, fee, memo, funds);
+  };
+  pauseStream = async ({
+    id
+  }: {
+    id: number;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      pause_stream: {
+        id
+      }
+    }, fee, memo, funds);
+  };
+  linkStream = async ({
+    ids
+  }: {
+    ids: number[];
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      link_stream: {
+        ids
+      }
+    }, fee, memo, funds);
+  };
+  detachStream = async ({
+    id
+  }: {
+    id: number;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      detach_stream: {
+        id
+      }
+    }, fee, memo, funds);
+  };
+  resumeStream = async ({
+    id
+  }: {
+    id: number;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      resume_stream: {
+        id
+      }
+    }, fee, memo, funds);
+  };
+  removeStream = async ({
+    id
+  }: {
+    id: number;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      remove_stream: {
+        id
+      }
+    }, fee, memo, funds);
+  };
+}
\ No newline at end of file
diff --git a/typescript/contracts/cw-escrow-streams/CwEscrowStreams.types.ts b/typescript/contracts/cw-escrow-streams/CwEscrowStreams.types.ts
new file mode 100644
index 00000000..b9ce5633
--- /dev/null
+++ b/typescript/contracts/cw-escrow-streams/CwEscrowStreams.types.ts
@@ -0,0 +1,99 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+export interface InstantiateMsg {
+  admin?: string | null;
+}
+export type ExecuteMsg = {
+  receive: Cw20ReceiveMsg;
+} | {
+  create: {
+    params: StreamParams;
+  };
+} | {
+  distribute: {
+    id: number;
+  };
+} | {
+  pause_stream: {
+    id: number;
+  };
+} | {
+  link_stream: {
+    ids: number[];
+  };
+} | {
+  detach_stream: {
+    id: number;
+  };
+} | {
+  resume_stream: {
+    id: number;
+  };
+} | {
+  remove_stream: {
+    id: number;
+  };
+};
+export type Uint128 = string;
+export type Binary = string;
+export type CheckedDenom = {
+  native: string;
+} | {
+  cw20: Addr;
+};
+export type Addr = string;
+export interface Cw20ReceiveMsg {
+  amount: Uint128;
+  msg: Binary;
+  sender: string;
+}
+export interface StreamParams {
+  admin: string;
+  balance: Uint128;
+  denom: CheckedDenom;
+  description?: string | null;
+  end_time: number;
+  is_detachable?: boolean | null;
+  recipient: string;
+  start_time: number;
+  title?: string | null;
+}
+export type QueryMsg = {
+  get_config: {};
+} | {
+  get_stream: {
+    id: number;
+  };
+} | {
+  list_streams: {
+    limit?: number | null;
+    start?: number | null;
+  };
+};
+export interface ConfigResponse {
+  admin: string;
+}
+export interface StreamResponse {
+  admin: string;
+  balance: Uint128;
+  claimed_balance: Uint128;
+  denom: CheckedDenom;
+  description?: string | null;
+  end_time: number;
+  id: number;
+  is_detachable: boolean;
+  link_id?: number | null;
+  paused: boolean;
+  paused_duration?: number | null;
+  paused_time?: number | null;
+  recipient: string;
+  start_time: number;
+  title?: string | null;
+}
+export interface ListStreamsResponse {
+  streams: StreamResponse[];
+}
\ No newline at end of file
diff --git a/typescript/contracts/cw-escrow-streams/bundle.ts b/typescript/contracts/cw-escrow-streams/bundle.ts
new file mode 100644
index 00000000..3b6df0d0
--- /dev/null
+++ b/typescript/contracts/cw-escrow-streams/bundle.ts
@@ -0,0 +1,13 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+import * as _6 from "./CwEscrowStreams.types";
+import * as _7 from "./CwEscrowStreams.client";
+export namespace contracts {
+  export const CwEscrowStreams = { ..._6,
+    ..._7
+  };
+}
\ No newline at end of file
diff --git a/typescript/contracts/cw-payroll/CwPayroll.client.ts b/typescript/contracts/cw-payroll/CwPayroll.client.ts
index 9b37d72a..f919e58a 100644
--- a/typescript/contracts/cw-payroll/CwPayroll.client.ts
+++ b/typescript/contracts/cw-payroll/CwPayroll.client.ts
@@ -5,8 +5,8 @@
 */
 
 import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
-import { StdFee } from "@cosmjs/amino";
-import { InstantiateMsg, ExecuteMsg, Uint128, Binary, Cw20ReceiveMsg, QueryMsg, ConfigResponse, WrappedBalance, NativeBalance, Addr, WrappedBalance1, StreamResponse, Coin, Cw20CoinVerified, ListStreamsResponse } from "./CwPayroll.types";
+import { Coin, StdFee } from "@cosmjs/amino";
+import { InstantiateMsg, ExecuteMsg, Uint128, Binary, Cw20ReceiveMsg, QueryMsg, ConfigResponse, CheckedDenom, Addr, StreamResponse, ListStreamsResponse } from "./CwPayroll.types";
 export interface CwPayrollReadOnlyInterface {
   contractAddress: string;
   getConfig: () => Promise<ConfigResponse>;
@@ -78,6 +78,7 @@ export interface CwPayrollInterface extends CwPayrollReadOnlyInterface {
     msg: Binary;
     sender: string;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  create: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   distribute: ({
     id
   }: {
@@ -120,6 +121,7 @@ export class CwPayrollClient extends CwPayrollQueryClient implements CwPayrollIn
     this.sender = sender;
     this.contractAddress = contractAddress;
     this.receive = this.receive.bind(this);
+    this.create = this.create.bind(this);
     this.distribute = this.distribute.bind(this);
     this.pauseStream = this.pauseStream.bind(this);
     this.linkStream = this.linkStream.bind(this);
@@ -145,6 +147,11 @@ export class CwPayrollClient extends CwPayrollQueryClient implements CwPayrollIn
       }
     }, fee, memo, funds);
   };
+  create = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      create: {}
+    }, fee, memo, funds);
+  };
   distribute = async ({
     id
   }: {
diff --git a/typescript/contracts/cw-payroll/CwPayroll.types.ts b/typescript/contracts/cw-payroll/CwPayroll.types.ts
index 874bc579..946f689b 100644
--- a/typescript/contracts/cw-payroll/CwPayroll.types.ts
+++ b/typescript/contracts/cw-payroll/CwPayroll.types.ts
@@ -9,6 +9,8 @@ export interface InstantiateMsg {
 }
 export type ExecuteMsg = {
   receive: Cw20ReceiveMsg;
+} | {
+  create: {};
 } | {
   distribute: {
     id: number;
@@ -56,22 +58,17 @@ export type QueryMsg = {
 export interface ConfigResponse {
   admin: string;
 }
-export type WrappedBalance = {
-  native: NativeBalance;
+export type CheckedDenom = {
+  native: string;
 } | {
-  cw20: Cw20CoinVerified;
+  cw20: Addr;
 };
-export type NativeBalance = Coin[];
 export type Addr = string;
-export type WrappedBalance1 = {
-  native: NativeBalance;
-} | {
-  cw20: Cw20CoinVerified;
-};
 export interface StreamResponse {
   admin: string;
-  balance: WrappedBalance;
-  claimed_balance: WrappedBalance1;
+  balance: Uint128;
+  claimed_balance: Uint128;
+  denom: CheckedDenom;
   description?: string | null;
   end_time: number;
   id: number;
@@ -84,15 +81,6 @@ export interface StreamResponse {
   start_time: number;
   title?: string | null;
 }
-export interface Coin {
-  amount: Uint128;
-  denom: string;
-  [k: string]: unknown;
-}
-export interface Cw20CoinVerified {
-  address: Addr;
-  amount: Uint128;
-}
 export interface ListStreamsResponse {
   streams: StreamResponse[];
 }
\ No newline at end of file


Test patch:
diff --git a/contracts/external/cw-escrow-streams/src/tests.rs b/contracts/external/cw-escrow-streams/src/tests.rs
new file mode 100644
index 00000000..45362d89
--- /dev/null
+++ b/contracts/external/cw-escrow-streams/src/tests.rs
@@ -0,0 +1,1003 @@
+use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg};
+use crate::state::{Stream, StreamId, StreamIdsExtensions};
+use crate::ContractError;
+
+use cosmwasm_std::testing::mock_info;
+use cosmwasm_std::{to_binary, Addr, Empty, Uint128};
+use cw20::{Cw20Coin, Cw20ExecuteMsg};
+use cw_denom::CheckedDenom;
+
+use cw_multi_test::{App, Contract, ContractWrapper, Executor};
+use dao_testing::contracts::cw20_base_contract;
+
+fn cw_payroll_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        crate::contract::execute,
+        crate::contract::instantiate,
+        crate::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn get_stream(app: &App, cw_payroll_addr: Addr, id: u64) -> Stream {
+    app.wrap()
+        .query_wasm_smart(cw_payroll_addr, &QueryMsg::GetStream { id })
+        .unwrap()
+}
+
+fn setup_app_and_instantiate_contracts(admin: Option<String>) -> (App, Addr, Addr) {
+    let mut app = App::default();
+
+    let cw20_code_id = app.store_code(cw20_base_contract());
+    let cw_payroll_code_id = app.store_code(cw_payroll_contract());
+
+    // TODO mint alice and bob native tokens as well
+
+    let cw20_addr = app
+        .instantiate_contract(
+            cw20_code_id,
+            Addr::unchecked("ekez"),
+            &cw20_base::msg::InstantiateMsg {
+                name: "cw20 token".to_string(),
+                symbol: "cwtwenty".to_string(),
+                decimals: 6,
+                initial_balances: vec![
+                    Cw20Coin {
+                        address: "alice".to_string(),
+                        amount: Uint128::new(10000),
+                    },
+                    Cw20Coin {
+                        address: "bob".to_string(),
+                        amount: Uint128::new(1000),
+                    },
+                ],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "cw20-base",
+            None,
+        )
+        .unwrap();
+
+    let cw_payroll_addr = app
+        .instantiate_contract(
+            cw_payroll_code_id,
+            Addr::unchecked("ekez"),
+            &InstantiateMsg { admin },
+            &[],
+            "cw-payroll",
+            None,
+        )
+        .unwrap();
+
+    (app, cw20_addr, cw_payroll_addr)
+}
+
+#[test]
+fn test_execute_distribute() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let info = mock_info("alice", &[]);
+
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(1000);
+
+    let denom = CheckedDenom::Cw20(Addr::unchecked("contract0"));
+    let claimed = Uint128::zero();
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
+        .unwrap();
+
+    assert_eq!(
+        get_stream(&app, cw_payroll_addr.clone(), 1),
+        Stream {
+            admin: info.sender.clone(),
+            recipient: Addr::unchecked("bob"),
+            balance: amount,
+            claimed_balance: claimed.clone(),
+            denom: denom.clone(),
+            start_time,
+            end_time,
+            title: None,
+            description: None,
+            paused: false,
+            paused_time: None,
+            paused_duration: None,
+            link_id: None,
+            is_detachable: true,
+        }
+    );
+
+    let bob = Addr::unchecked("bob");
+
+    // Stream has not started
+    let err: ContractError = app
+        .execute_contract(
+            bob.clone(),
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::Distribute { id: 1 },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::NoFundsToClaim { claimed });
+
+    // Advance the clock
+    app.update_block(|block| {
+        block.time = block.time.plus_seconds(150);
+    });
+
+    // Stream has started so tokens have vested
+    app.execute_contract(
+        bob,
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::Distribute { id: 1 },
+        &[],
+    )
+    .unwrap();
+
+    // Check final balances after distribution
+    assert_eq!(
+        get_stream(&app, cw_payroll_addr.clone(), 1),
+        Stream {
+            admin: info.sender,
+            recipient: Addr::unchecked("bob"),
+            balance: Uint128::new(750),
+            claimed_balance: Uint128::new(250),
+            denom,
+            start_time,
+            end_time,
+            title: None,
+            description: None,
+            paused: false,
+            paused_time: None,
+            paused_duration: None,
+            link_id: None,
+            is_detachable: true,
+        }
+    );
+
+    // TODO check bob and alice's balances
+}
+
+// TODO I really don't like this refund thing, it's weird
+// #[test]
+// fn test_create_stream_with_refund() {
+//     let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+//     let info = mock_info("alice", &[]);
+//     let recipient = Addr::unchecked("bob").to_string();
+//     let amount = Uint128::new(350);
+//     let start_time = app.block_info().time.plus_seconds(100).seconds();
+//     let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+//     let msg = Cw20ExecuteMsg::Send {
+//         contract: cw_payroll_addr.to_string(),
+//         amount,
+//         msg: to_binary(&ReceiveMsg::CreateStream {
+//             admin: Some(info.sender.to_string()),
+//             recipient,
+//             start_time,
+//             end_time,
+//             is_detachable: None,
+//         })
+//         .unwrap(),
+//     };
+//     app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
+//         .unwrap();
+
+//     let balance = Uint128::new(350);
+//     let denom = CheckedDenom::Cw20(Addr::unchecked("contract0"));
+
+//     assert_eq!(
+//         get_stream(&app, cw_payroll_addr.clone(), 1),
+//         Stream {
+//             admin: info.sender,
+//             recipient: Addr::unchecked("bob"),
+//             balance, // original amount - refund
+//             claimed_balance: Uint128::zero(),
+//             denom,
+//             start_time,
+//             end_time,
+//             title: None,
+//             description: None,
+//             paused: false,
+//             paused_time: None,
+//             paused_duration: None,
+//             link_id: None,
+//             is_detachable: true,
+//         }
+//     );
+// }
+
+#[test]
+fn test_execute_pause_stream() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let info = mock_info("alice", &[]);
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(350);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
+        .unwrap();
+
+    let stream_id: StreamId = 1;
+
+    app.execute_contract(
+        info.sender,
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::PauseStream { id: stream_id },
+        &[],
+    )
+    .unwrap();
+
+    let denom = CheckedDenom::Cw20(Addr::unchecked("contract0"));
+    let saved_stream = get_stream(&app, cw_payroll_addr.clone(), stream_id);
+    assert_eq!(
+        saved_stream,
+        Stream {
+            admin: Addr::unchecked("alice"),
+            recipient: Addr::unchecked("bob"),
+            balance: amount,
+            claimed_balance: Uint128::zero(),
+            denom,
+            start_time,
+            end_time,
+            title: None,
+            description: None,
+            paused: true,
+            paused_time: saved_stream.paused_time,
+            paused_duration: saved_stream.paused_duration,
+            link_id: None,
+            is_detachable: true,
+        }
+    );
+}
+
+#[test]
+fn test_invalid_start_time() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let info = mock_info("alice", &[]);
+
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(100);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(20).seconds();
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+
+    let err: ContractError = app
+        .execute_contract(info.sender, cw20_addr, &msg, &[])
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    assert_eq!(err, ContractError::InvalidStartTime {});
+}
+
+//// TODO rethink this test for multitest
+// #[test]
+// fn invalid_cw20_addr() {
+//     let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+//     let info = mock_info("alice", &[]);
+//     let recipient = Addr::unchecked("bob").to_string();
+//     let amount = Uint128::new(100);
+//     let start_time = app.block_info().time.plus_seconds(100).seconds();
+//     let end_time = app.block_info().time.plus_seconds(200).seconds();
+
+//     let msg = Cw20ExecuteMsg::Send {
+//         contract: cw_payroll_addr.to_string(),
+//         amount,
+//         msg: to_binary(&ReceiveMsg::CreateStream {
+//             admin: Some(info.sender.to_string()),
+//             recipient,
+//             start_time,
+//             end_time,
+//             is_detachable: None,
+//         })
+//         .unwrap(),
+//     };
+
+//     // TODO clean up
+//     info.sender = Addr::unchecked("wrongCw20");
+
+//     let err: ContractError = app
+//         .execute_contract(info.sender, cw20_addr, &msg, &[])
+//         .unwrap_err()
+//         .downcast()
+//         .unwrap();
+
+//     assert_eq!(
+//         err,
+//         ContractError::Std(cosmwasm_std::StdError::GenericErr {
+//             msg: "Invalid input: address not normalized".to_string()
+//         })
+//     );
+// }
+
+// //// TODO Do we want this?
+// // #[test]
+// // fn test_invalid_deposit_amount() {
+// //     let mut deps = mock_dependencies();
+
+// //     let msg = InstantiateMsg { admin: None };
+// //     let mut info = mock_info("alice", &[]);
+// //     instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
+
+// //     let sender = Addr::unchecked("alice").to_string();
+// //     let recipient = Addr::unchecked("bob").to_string();
+// //     let amount = Uint128::new(3);
+// //     let start_time = app.block_info().time.plus_seconds(100).seconds();
+// //     let end_time = app.block_info().time.plus_seconds(200).seconds();
+
+// //     let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
+// //         sender,
+// //         amount,
+// //         msg: to_binary(&ReceiveMsg::CreateStream {
+// //             admin: None,
+// //             recipient,
+// //             start_time,
+// //             end_time,
+// //             is_detachable: None,
+// //         })
+// //         .unwrap(),
+// //     });
+// //     info.sender = Addr::unchecked("cw20");
+// //     let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
+// //     assert_eq!(err, ContractError::AmountLessThanDuration {});
+// // }
+
+#[test]
+fn test_execute_remove_stream() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let alice = Addr::unchecked("alice");
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(350);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(alice.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(alice, cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    let stream_id: StreamId = 1;
+
+    // Remove stream and verify not found error returned
+    let remove_response = app
+        .execute_contract(
+            Addr::unchecked("alice"),
+            cw_payroll_addr,
+            &ExecuteMsg::RemoveStream { id: stream_id },
+            &[],
+        )
+        .unwrap();
+
+    // Make sure refund happened successfully
+    assert_eq!(
+        remove_response.events[3].attributes[4].value,
+        "350".to_string()
+    )
+}
+
+#[test]
+fn test_execute_link_stream_invalid() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let info = mock_info("alice", &[]);
+
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(350);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+    //Create stream 1
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    let ids = vec![1, 2];
+
+    //Link stream and verify error returned
+    let error: ContractError = app
+        .execute_contract(
+            info.sender.clone(),
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::LinkStream { ids: ids.clone() },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    assert_eq!(
+        error,
+        ContractError::StreamNotFound {
+            stream_id: *ids.second().unwrap()
+        }
+    );
+
+    let ids = vec![1, 1];
+
+    //Link stream and verify error returned
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &[],
+    )
+    .unwrap_err();
+    // assert_eq!(error, ContractError::StreamsShouldNotBeEqual {});
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender, cw20_addr, &msg, &[])
+        .unwrap();
+
+    let sender = Addr::unchecked("bob").to_string();
+    let ids = vec![1, 2];
+
+    let unauthorized_info = mock_info(&sender, &[]);
+    let error: ContractError = app
+        .execute_contract(
+            unauthorized_info.sender,
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::LinkStream { ids: ids.clone() },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(error, ContractError::Unauthorized {});
+}
+
+#[test]
+fn test_execute_link_stream_valid() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let info = mock_info("alice", &[]);
+
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(350);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+    //Create stream 1
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    //Create stream 2
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
+        .unwrap();
+
+    let ids = vec![1, 2];
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &[],
+    )
+    .unwrap();
+
+    let left_stream = get_stream(&app, cw_payroll_addr.clone(), *ids.first().unwrap());
+    let right_stream = get_stream(&app, cw_payroll_addr.clone(), *ids.second().unwrap());
+    assert_eq!(left_stream.link_id, Some(*ids.second().unwrap()));
+    assert_eq!(right_stream.link_id, Some(*ids.first().unwrap()));
+}
+
+#[test]
+fn test_execute_detach_stream_valid() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let info = mock_info("alice", &[]);
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(350);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+    // Create stream 1
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    // Create stream 2
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: None,
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
+        .unwrap();
+
+    let ids = vec![1, 2];
+
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::DetachStream {
+            id: *ids.first().unwrap(),
+        },
+        &[],
+    )
+    .unwrap();
+
+    let left_stream = get_stream(&app, cw_payroll_addr.clone(), *ids.first().unwrap());
+    let right_stream = get_stream(&app, cw_payroll_addr.clone(), *ids.second().unwrap());
+
+    assert!(left_stream.paused);
+    assert!(left_stream.paused_time.is_some());
+    assert!(left_stream.link_id.is_none());
+
+    assert!(right_stream.paused);
+    assert!(right_stream.paused_time.is_some());
+    assert!(right_stream.link_id.is_none());
+}
+
+#[test]
+fn test_execute_detach_stream_invalid() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let info = mock_info("alice", &[]);
+
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(350);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+    // Create stream 1
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: Some(false),
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    // Create stream 2
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: Some(false),
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    let ids = vec![1, 2];
+
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &[],
+    )
+    .unwrap();
+
+    let ids = vec![11, 22];
+    let error: ContractError = app
+        .execute_contract(
+            info.sender.clone(),
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::DetachStream {
+                id: *ids.first().unwrap(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(
+        error,
+        ContractError::StreamNotFound {
+            stream_id: *ids.first().unwrap()
+        }
+    );
+
+    let ids = vec![1, 22];
+    let error: ContractError = app
+        .execute_contract(
+            info.sender.clone(),
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::DetachStream {
+                id: *ids.second().unwrap(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(
+        error,
+        ContractError::StreamNotFound {
+            stream_id: *ids.second().unwrap()
+        }
+    );
+
+    let ids = vec![1, 2];
+    let error: ContractError = app
+        .execute_contract(
+            info.sender.clone(),
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::DetachStream {
+                id: *ids.second().unwrap(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(error, ContractError::StreamNotDetachable {});
+
+    let unauthorized_info = mock_info(&recipient, &[]);
+
+    // Create stream 1
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: Some(true),
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    // Create stream 2
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: Some(true),
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    let ids = vec![3, 4];
+    app.execute_contract(
+        info.sender,
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &[],
+    )
+    .unwrap();
+
+    let error: ContractError = app
+        .execute_contract(
+            unauthorized_info.sender,
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::DetachStream {
+                id: *ids.first().unwrap(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    assert_eq!(error, ContractError::Unauthorized {});
+}
+
+#[test]
+fn test_execute_resume_stream_valid() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let info = mock_info("alice", &[]);
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(350);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+    // Create stream 1
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: Some(true),
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    // Create stream 2
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient,
+            start_time,
+            end_time,
+            is_detachable: Some(true),
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    let ids = vec![1, 2];
+
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::DetachStream {
+            id: *ids.first().unwrap(),
+        },
+        &[],
+    )
+    .unwrap();
+
+    let left_stream = get_stream(&app, cw_payroll_addr.clone(), *ids.first().unwrap());
+    let right_stream = get_stream(&app, cw_payroll_addr.clone(), *ids.second().unwrap());
+
+    assert!(left_stream.paused);
+    assert!(left_stream.paused_time.is_some());
+    assert!(left_stream.link_id.is_none());
+
+    assert!(right_stream.paused);
+    assert!(right_stream.paused_time.is_some());
+    assert!(right_stream.link_id.is_none());
+
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::ResumeStream {
+            id: *ids.first().unwrap(),
+        },
+        &[],
+    )
+    .unwrap();
+
+    let left_stream = get_stream(&app, cw_payroll_addr.clone(), *ids.first().unwrap());
+    let right_stream = get_stream(&app, cw_payroll_addr.clone(), *ids.second().unwrap());
+
+    assert!(!left_stream.paused);
+    assert!(left_stream.paused_time.is_none());
+
+    assert!(right_stream.paused);
+    assert!(right_stream.paused_time.is_some());
+}
+
+#[test]
+fn test_execute_resume_stream_invalid() {
+    let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+
+    let sender = Addr::unchecked("alice").to_string();
+
+    let info = mock_info(&sender, &[]);
+
+    let recipient = Addr::unchecked("bob").to_string();
+    let amount = Uint128::new(350);
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(400).seconds();
+
+    // Create stream 1
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: Some(true),
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    // Create stream 2
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_payroll_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::CreateStream {
+            admin: Some(info.sender.to_string()),
+            recipient: recipient.clone(),
+            start_time,
+            end_time,
+            is_detachable: Some(false),
+        })
+        .unwrap(),
+    };
+    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    let ids = vec![1, 2];
+    app.execute_contract(
+        info.sender.clone(),
+        cw_payroll_addr.clone(),
+        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &[],
+    )
+    .unwrap();
+
+    let error: ContractError = app
+        .execute_contract(
+            info.sender.clone(),
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::ResumeStream {
+                id: *ids.second().unwrap(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(error, ContractError::StreamNotPaused {});
+
+    let unauthorized_info = mock_info(&recipient, &[]);
+    let error: ContractError = app
+        .execute_contract(
+            unauthorized_info.sender,
+            cw_payroll_addr.clone(),
+            &ExecuteMsg::DetachStream {
+                id: *ids.first().unwrap(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(error, ContractError::StreamNotDetachable {});
+}