Repository: DA0-DA0/dao-contracts
Pull request #604 (2023-01-03 23:30:00)

Refactoring and improvements on cw-escrow-streams
Fixing issues discussed in https://github.com/DA0-DA0/dao-contracts/pull/537


Patch:
diff --git a/contracts/external/cw-escrow-streams/README.md b/contracts/external/cw-escrow-streams/README.md
index aaec86fc..d96e2b1b 100644
--- a/contracts/external/cw-escrow-streams/README.md
+++ b/contracts/external/cw-escrow-streams/README.md
@@ -12,13 +12,48 @@ junod tx wasm instantiate <code-id> '{"admin": "juno12xyz..."}'  --label "cw-pay
 
 ## Creating a Native Token Stream
 
-TODO: Update upon native token completion
+```sh
+params='{
+  "create": {
+      "owner": <owner_key>,
+      "recipient": <recipient_key>,
+       "denom": {
+        "Native": "ujunox"
+        },
+      "balance":0,
+      "start_time":"2000000000",
+      "end_time":"4000000000",
+      "title":"",
+      "description":"",
+      "is_detachable":false,
+    }
+}';
+junod tx wasm execute <contract-address> "$params" --amount 10000000ujunox --from <your-key> 
 
+```
 ## 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. 
 
+```sh
+CW20_SEND='{"send": {"contract": "<contract-address>", "amount": "1000000", "msg": "$params"}}'
+junod tx wasm execute <cw20-address> "$CW20_SEND" --from <your-key> 
+```
 ## Distribute payments
 Streamed payments can be claimed continously at any point after the start time by triggering a Distribute message.
-
+```sh
+junod tx wasm execute <contract-address> "'{"distribute": {"id": "<stream_id>"}}'" --from <your-key> 
+```
 ## Linking Streams
-TODO: describe this feature and how it works.
+Streams can be linked, first stream can distribute native tokens and the linked one for distributing CW20s. 
+Linked streams are mirrored, so stopping one will stop the other stream, the same goes for resuming.
+```sh
+link_params='{
+  "link_stream": {
+    "ids":{
+      "0": <left_id>,
+      "1": <right_id>,
+    }
+ }
+}';
+junod tx wasm execute <contract-address> "'{"link_stream": "$link_params"}'" --from <your-key> 
+```
\ No newline at end of file
diff --git a/contracts/external/cw-escrow-streams/schema/cw-escrow-streams.json b/contracts/external/cw-escrow-streams/schema/cw-escrow-streams.json
index b2421446..1ce3255f 100644
--- a/contracts/external/cw-escrow-streams/schema/cw-escrow-streams.json
+++ b/contracts/external/cw-escrow-streams/schema/cw-escrow-streams.json
@@ -45,7 +45,7 @@
             ],
             "properties": {
               "params": {
-                "$ref": "#/definitions/StreamParams"
+                "$ref": "#/definitions/UncheckedStreamData"
               }
             },
             "additionalProperties": false
@@ -112,12 +112,7 @@
             ],
             "properties": {
               "ids": {
-                "type": "array",
-                "items": {
-                  "type": "integer",
-                  "format": "uint64",
-                  "minimum": 0.0
-                }
+                "$ref": "#/definitions/StreamIds"
               }
             },
             "additionalProperties": false
@@ -196,16 +191,54 @@
       }
     ],
     "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.",
+      "Cw20ReceiveMsg": {
+        "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg",
+        "type": "object",
+        "required": [
+          "amount",
+          "msg",
+          "sender"
+        ],
+        "properties": {
+          "amount": {
+            "$ref": "#/definitions/Uint128"
+          },
+          "msg": {
+            "$ref": "#/definitions/Binary"
+          },
+          "sender": {
+            "type": "string"
+          }
+        },
+        "additionalProperties": false
+      },
+      "StreamIds": {
+        "type": "array",
+        "items": [
+          {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          },
+          {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          }
+        ],
+        "maxItems": 2,
+        "minItems": 2
+      },
+      "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"
+      },
+      "UncheckedDenom": {
+        "description": "A denom that has not been checked to confirm it points to a valid asset.",
         "oneOf": [
           {
             "description": "A native (bank module) asset.",
@@ -228,53 +261,30 @@
             ],
             "properties": {
               "cw20": {
-                "$ref": "#/definitions/Addr"
+                "type": "string"
               }
             },
             "additionalProperties": false
           }
         ]
       },
-      "Cw20ReceiveMsg": {
-        "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg",
-        "type": "object",
-        "required": [
-          "amount",
-          "msg",
-          "sender"
-        ],
-        "properties": {
-          "amount": {
-            "$ref": "#/definitions/Uint128"
-          },
-          "msg": {
-            "$ref": "#/definitions/Binary"
-          },
-          "sender": {
-            "type": "string"
-          }
-        },
-        "additionalProperties": false
-      },
-      "StreamParams": {
+      "UncheckedStreamData": {
         "type": "object",
         "required": [
-          "admin",
           "balance",
           "denom",
           "end_time",
+          "is_detachable",
+          "owner",
           "recipient",
           "start_time"
         ],
         "properties": {
-          "admin": {
-            "type": "string"
-          },
           "balance": {
             "$ref": "#/definitions/Uint128"
           },
           "denom": {
-            "$ref": "#/definitions/CheckedDenom"
+            "$ref": "#/definitions/UncheckedDenom"
           },
           "description": {
             "type": [
@@ -288,10 +298,10 @@
             "minimum": 0.0
           },
           "is_detachable": {
-            "type": [
-              "boolean",
-              "null"
-            ]
+            "type": "boolean"
+          },
+          "owner": {
+            "type": "string"
           },
           "recipient": {
             "type": "string"
@@ -309,10 +319,6 @@
           }
         },
         "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"
       }
     }
   },
@@ -411,21 +417,18 @@
       "title": "StreamResponse",
       "type": "object",
       "required": [
-        "admin",
         "balance",
         "claimed_balance",
         "denom",
         "end_time",
         "id",
         "is_detachable",
+        "owner",
         "paused",
         "recipient",
         "start_time"
       ],
       "properties": {
-        "admin": {
-          "type": "string"
-        },
         "balance": {
           "$ref": "#/definitions/Uint128"
         },
@@ -465,6 +468,9 @@
           "format": "uint64",
           "minimum": 0.0
         },
+        "owner": {
+          "type": "string"
+        },
         "paused": {
           "description": "Whether the payroll stream is currently paused",
           "type": "boolean"
@@ -599,21 +605,18 @@
         "StreamResponse": {
           "type": "object",
           "required": [
-            "admin",
             "balance",
             "claimed_balance",
             "denom",
             "end_time",
             "id",
             "is_detachable",
+            "owner",
             "paused",
             "recipient",
             "start_time"
           ],
           "properties": {
-            "admin": {
-              "type": "string"
-            },
             "balance": {
               "$ref": "#/definitions/Uint128"
             },
@@ -653,6 +656,9 @@
               "format": "uint64",
               "minimum": 0.0
             },
+            "owner": {
+              "type": "string"
+            },
             "paused": {
               "description": "Whether the payroll stream is currently paused",
               "type": "boolean"
diff --git a/contracts/external/cw-escrow-streams/src/contract.rs b/contracts/external/cw-escrow-streams/src/contract.rs
index 8f7b4de0..2eb7a98a 100644
--- a/contracts/external/cw-escrow-streams/src/contract.rs
+++ b/contracts/external/cw-escrow-streams/src/contract.rs
@@ -2,7 +2,7 @@
 use cosmwasm_std::entry_point;
 use cosmwasm_std::{
     from_binary, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError,
-    StdResult, Storage, Uint128,
+    StdResult, Storage,
 };
 use cw2::set_contract_version;
 use cw20::Cw20ReceiveMsg;
@@ -13,7 +13,7 @@ use crate::error::ContractError;
 use crate::linking::*;
 use crate::msg::{
     ConfigResponse, ExecuteMsg, InstantiateMsg, ListStreamsResponse, QueryMsg, ReceiveMsg,
-    StreamParams, StreamResponse,
+    StreamResponse, UncheckedStreamData,
 };
 use crate::state::{Config, Stream, StreamId, CONFIG, STREAMS, STREAM_SEQ};
 
@@ -31,10 +31,11 @@ pub fn instantiate(
 
     // 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 admin = msg
+        .admin
+        .map(|a| deps.api.addr_validate(&a))
+        .transpose()?
+        .unwrap_or(info.sender);
 
     let config = Config {
         admin: admin.clone(),
@@ -57,8 +58,7 @@ pub fn execute(
 ) -> 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::Create { params } => execute_create(env, deps, info, params),
         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),
@@ -67,7 +67,20 @@ pub fn execute(
         ExecuteMsg::DetachStream { id } => execute_detach_stream(env, deps, &info, id),
     }
 }
-
+pub fn execute_create(
+    env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    params: UncheckedStreamData,
+) -> Result<Response, ContractError> {
+    if let Some(coin) = info.funds.first() {
+        let mut data = params;
+        data.balance = coin.amount;
+        data.denom = UncheckedDenom::Native(coin.denom.clone());
+        return execute_create_stream(env, deps, data);
+    }
+    Err(ContractError::NoFundsAttached {})
+}
 pub fn execute_pause_stream(
     env: Env,
     deps: DepsMut,
@@ -79,7 +92,7 @@ pub fn execute_pause_stream(
             let mut stream = STREAMS
                 .may_load(storage, stream_id)?
                 .ok_or(ContractError::StreamNotFound { stream_id: id })?;
-            if stream.admin != info.sender {
+            if stream.owner != info.sender {
                 return Err(ContractError::Unauthorized {});
             }
             if stream.paused {
@@ -91,18 +104,23 @@ pub fn execute_pause_stream(
             Ok(stream)
         };
 
-    // TODO this is weird... needs comments at the very least
+    // Pausing left stream right stream should also be paused
     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()))
+        .add_attribute("owner", stream.owner.to_string())
+        .add_attribute("paused_time", stream.paused_time.unwrap().to_string())
+        .add_attribute(
+            "link_id",
+            stream
+                .link_id
+                .map_or(String::from("not linked"), |n| n.to_string()),
+        ))
 }
 
 pub fn execute_remove_stream(
@@ -115,7 +133,7 @@ pub fn execute_remove_stream(
     let stream = STREAMS
         .may_load(deps.storage, id)?
         .ok_or(ContractError::StreamNotFound { stream_id: id })?;
-    if stream.admin != info.sender {
+    if stream.owner != info.sender {
         return Err(ContractError::Unauthorized {});
     }
 
@@ -127,12 +145,12 @@ pub fn execute_remove_stream(
     // Transfer any remaining balance to the owner
     let transfer_to_admin_msg = stream
         .denom
-        .get_transfer_to_message(&stream.admin, stream.balance)?;
+        .get_transfer_to_message(&stream.owner, stream.balance)?;
 
     Ok(Response::new()
         .add_attribute("method", "remove_stream")
         .add_attribute("stream_id", id.to_string())
-        .add_attribute("admin", info.sender)
+        .add_attribute("owner", info.sender)
         .add_attribute("removed_time", env.block.time.to_string())
         .add_message(transfer_to_admin_msg))
 }
@@ -148,7 +166,7 @@ pub fn execute_resume_stream(
             let mut stream = STREAMS
                 .may_load(storage, stream_id)?
                 .ok_or(ContractError::StreamNotFound { stream_id: id })?;
-            if stream.admin != info.sender {
+            if stream.owner != info.sender {
                 return Err(ContractError::Unauthorized {});
             }
             if !stream.paused {
@@ -170,7 +188,7 @@ pub fn execute_resume_stream(
     let response = Response::new()
         .add_attribute("method", "resume_stream")
         .add_attribute("stream_id", id.to_string())
-        .add_attribute("admin", info.sender)
+        .add_attribute("owner", info.sender)
         .add_attribute("rate_per_second", rate_per_second)
         .add_attribute("resume_time", env.block.time.to_string())
         .add_attribute(
@@ -185,50 +203,10 @@ pub fn execute_resume_stream(
 pub fn execute_create_stream(
     env: Env,
     deps: DepsMut,
-    params: StreamParams,
+    params: UncheckedStreamData,
 ) -> 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 stream_data = params.into_checked(env, deps.as_ref())?;
+    let stream: Stream = stream_data.into();
 
     let id = STREAM_SEQ.load(deps.storage)?;
     let id = id + 1;
@@ -238,10 +216,10 @@ pub fn execute_create_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()))
+        .add_attribute("owner", stream.owner.to_string())
+        .add_attribute("recipient", stream.recipient)
+        .add_attribute("start_time", stream.start_time.to_string())
+        .add_attribute("end_time", stream.end_time.to_string()))
 }
 
 pub fn execute_receive(
@@ -250,31 +228,29 @@ pub fn execute_receive(
     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,
+            owner,
+            recipient,
+            balance,
             start_time,
             end_time,
-            recipient,
+            title,
+            description,
             is_detachable,
         } => execute_create_stream(
             env,
             deps,
-            StreamParams {
-                admin: admin.unwrap_or_else(|| receive_msg.sender.clone()),
+            UncheckedStreamData {
+                owner,
                 recipient,
-                balance: receive_msg.amount,
-                denom: checked_denom,
+                balance: balance.unwrap_or(receive_msg.amount),
+                denom: UncheckedDenom::Cw20(info.sender.to_string()),
                 start_time,
                 end_time,
-                title: None,
-                description: None,
+                title,
+                description,
                 is_detachable,
             },
         ),
@@ -343,7 +319,7 @@ fn query_stream(deps: Deps, id: u64) -> StdResult<StreamResponse> {
     let stream = STREAMS.load(deps.storage, id)?;
     Ok(StreamResponse {
         id,
-        admin: stream.admin.into(),
+        owner: stream.owner.into(),
         recipient: stream.recipient.into(),
         balance: stream.balance,
         claimed_balance: stream.claimed_balance,
@@ -379,7 +355,7 @@ fn query_list_streams(
 fn map_stream(item: StdResult<(u64, Stream)>) -> StdResult<StreamResponse> {
     item.map(|(id, stream)| StreamResponse {
         id,
-        admin: stream.admin.to_string(),
+        owner: stream.owner.to_string(),
         recipient: stream.recipient.to_string(),
         balance: stream.balance,
         claimed_balance: stream.claimed_balance,
diff --git a/contracts/external/cw-escrow-streams/src/error.rs b/contracts/external/cw-escrow-streams/src/error.rs
index 1d674660..3c580928 100644
--- a/contracts/external/cw-escrow-streams/src/error.rs
+++ b/contracts/external/cw-escrow-streams/src/error.rs
@@ -30,6 +30,9 @@ pub enum ContractError {
     #[error("No tokens have vested for this stream")]
     NoFundsToClaim { claimed: Uint128 },
 
+    #[error("No funds attached")]
+    NoFundsAttached {},
+
     #[error("Stream does not exist")]
     StreamNotFound { stream_id: StreamId },
 
diff --git a/contracts/external/cw-escrow-streams/src/linking.rs b/contracts/external/cw-escrow-streams/src/linking.rs
index e34443ae..8ac41a3f 100644
--- a/contracts/external/cw-escrow-streams/src/linking.rs
+++ b/contracts/external/cw-escrow-streams/src/linking.rs
@@ -1,7 +1,7 @@
 use cosmwasm_std::{DepsMut, Env, MessageInfo, Response};
 
 use crate::{
-    state::{StreamId, StreamIds, StreamIdsExtensions, STREAMS},
+    state::{StreamId, StreamIds, STREAMS},
     ContractError,
 };
 
@@ -15,16 +15,13 @@ pub(crate) fn execute_link_stream(
     info: &MessageInfo,
     ids: StreamIds,
 ) -> Result<Response, ContractError> {
-    ids.validate()?;
-    // TODO no unwrap
-    let left_stream_id = *ids.first().unwrap();
-    let right_stream_id = *ids.second().unwrap();
+    let (left_stream_id, right_stream_id) = ids.into_checked()?.into_inner();
 
     let mut left_stream =
         STREAMS
             .may_load(deps.storage, left_stream_id)?
             .ok_or(ContractError::StreamNotFound {
-                stream_id: *ids.first().unwrap(),
+                stream_id: left_stream_id,
             })?;
 
     let mut right_stream =
@@ -34,7 +31,7 @@ pub(crate) fn execute_link_stream(
                 stream_id: right_stream_id,
             })?;
 
-    if !(left_stream.admin == info.sender && right_stream.admin == info.sender) {
+    if !(left_stream.owner == info.sender && right_stream.owner == info.sender) {
         return Err(ContractError::Unauthorized {});
     }
     left_stream.link_id = Some(right_stream_id);
@@ -47,7 +44,7 @@ pub(crate) fn execute_link_stream(
         .add_attribute("method", "link")
         .add_attribute("left_stream_id", left_stream_id.to_string())
         .add_attribute("right_stream_id", right_stream_id.to_string())
-        .add_attribute("admin", info.sender.clone());
+        .add_attribute("owner", info.sender.clone());
 
     Ok(response)
 }
@@ -71,7 +68,7 @@ pub(crate) fn execute_detach_stream(
         return Err(ContractError::StreamNotDetachable {});
     }
 
-    if !(detach_stream.admin == info.sender && linked_stream.admin == info.sender) {
+    if !(detach_stream.owner == info.sender && linked_stream.owner == info.sender) {
         return Err(ContractError::Unauthorized {});
     }
 
@@ -89,7 +86,7 @@ pub(crate) fn execute_detach_stream(
         .add_attribute("method", "link")
         .add_attribute("detach_stream_id", id.to_string())
         .add_attribute("linked_stream_id", link_id.to_string())
-        .add_attribute("admin", info.sender.clone());
+        .add_attribute("owner", info.sender.clone());
 
     Ok(response)
 }
diff --git a/contracts/external/cw-escrow-streams/src/msg.rs b/contracts/external/cw-escrow-streams/src/msg.rs
index 0b594214..b8ca42c8 100644
--- a/contracts/external/cw-escrow-streams/src/msg.rs
+++ b/contracts/external/cw-escrow-streams/src/msg.rs
@@ -1,7 +1,7 @@
 use cosmwasm_schema::{cw_serde, QueryResponses};
-use cosmwasm_std::Uint128;
+use cosmwasm_std::{Addr, Uint128};
 use cw20::Cw20ReceiveMsg;
-use cw_denom::CheckedDenom;
+use cw_denom::{CheckedDenom, UncheckedDenom};
 
 use crate::state::{StreamId, StreamIds};
 
@@ -15,7 +15,7 @@ pub enum ExecuteMsg {
     Receive(Cw20ReceiveMsg),
     // TODO be able to create steam with native token
     Create {
-        params: StreamParams,
+        params: UncheckedStreamData,
     },
     Distribute {
         id: StreamId, // Stream id
@@ -40,29 +40,40 @@ pub enum ExecuteMsg {
 // Receiver setup
 #[cw_serde]
 pub enum ReceiveMsg {
-    // TODO support all StreamParams or delete them
     CreateStream {
-        admin: Option<String>,
+        owner: String,
         recipient: String,
+        balance: Option<Uint128>,
         start_time: u64,
         end_time: u64,
-        // TODO just make this a bool
-        is_detachable: Option<bool>,
+        title: Option<String>,
+        description: Option<String>,
+        is_detachable: bool,
     },
 }
-
 #[cw_serde]
-pub struct StreamParams {
-    pub admin: String,
+pub struct UncheckedStreamData {
+    pub owner: String,
     pub recipient: String,
     pub balance: Uint128,
+    pub denom: UncheckedDenom,
+    pub start_time: u64,
+    pub end_time: u64,
+    pub title: Option<String>,
+    pub description: Option<String>,
+    pub is_detachable: bool,
+}
+#[cw_serde]
+pub struct CheckedStreamData {
+    pub owner: Addr,
+    pub recipient: Addr,
+    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>,
+    pub is_detachable: bool,
 }
 
 #[cw_serde]
@@ -87,7 +98,7 @@ pub struct ConfigResponse {
 #[cw_serde]
 pub struct StreamResponse {
     pub id: u64,
-    pub admin: String,
+    pub owner: String,
     pub recipient: String,
     pub balance: Uint128,
     pub claimed_balance: Uint128,
diff --git a/contracts/external/cw-escrow-streams/src/state.rs b/contracts/external/cw-escrow-streams/src/state.rs
index e111f838..f05b125c 100644
--- a/contracts/external/cw-escrow-streams/src/state.rs
+++ b/contracts/external/cw-escrow-streams/src/state.rs
@@ -1,9 +1,14 @@
+use std::ops::Div;
+
 use cosmwasm_schema::cw_serde;
-use cosmwasm_std::{Addr, Response, Timestamp, Uint128};
+use cosmwasm_std::{Addr, Deps, Env, Response, Timestamp, Uint128};
 use cw_storage_plus::{Item, Map};
 use serde::{Deserialize, Serialize};
 
-use crate::ContractError;
+use crate::{
+    msg::{CheckedStreamData, UncheckedStreamData},
+    ContractError,
+};
 use cw_denom::CheckedDenom;
 
 #[cw_serde]
@@ -14,7 +19,7 @@ pub const CONFIG: Item<Config> = Item::new("config");
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
 pub struct Stream {
-    pub admin: Addr,
+    pub owner: Addr,
     pub recipient: Addr,
     /// Balance in Native and Cw20 tokens
     pub balance: Uint128,
@@ -38,6 +43,27 @@ pub struct Stream {
     /// Each stream must then resume on their own, or be fully removed to re-link.
     pub is_detachable: bool,
 }
+impl From<CheckedStreamData> for Stream {
+    fn from(data: CheckedStreamData) -> Stream {
+        Stream {
+            owner: data.owner,
+            recipient: data.recipient,
+            balance: data.balance,
+            claimed_balance: Uint128::zero(),
+            denom: data.denom,
+            start_time: data.start_time,
+            end_time: data.end_time,
+            paused_time: None,
+            paused_duration: None,
+            paused: false,
+            title: data.title,
+            description: data.description,
+            link_id: None,
+            // TODO Should not be an option type
+            is_detachable: data.is_detachable,
+        }
+    }
+}
 
 impl Stream {
     pub(crate) fn can_distribute_more(&self) -> bool {
@@ -55,26 +81,18 @@ impl Stream {
         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();
-
-            let passed: u128 = diff
-                .checked_sub(paused_duration.unwrap_or_default())
-                .unwrap_or_default()
-                .into();
-
-            let rate_per_second = balance.checked_div(duration.into()).unwrap_or_default();
-
-            return Ok((
-                (Uint128::from(passed) * rate_per_second)
-                    .checked_sub(claimed_balance)
-                    .unwrap_or_default(),
+        let diff_end = std::cmp::min(block_time.seconds(), end_time);
+        let duration: u64 = end_time - start_time;
+        let paused_dur = paused_duration.unwrap_or(0);
+        if duration > 0 && start_time < diff_end {
+            let diff = diff_end - start_time;
+            let passed: u128 = (diff - paused_dur).into();
+            let rate_per_second = balance.div(Uint128::from(duration));
+            let claim_info = (
+                (Uint128::from(passed) * rate_per_second) - claimed_balance,
                 rate_per_second,
-            ));
+            );
+            return Ok(claim_info);
         }
         Ok((Uint128::new(0), Uint128::new(0)))
     }
@@ -103,27 +121,59 @@ impl Stream {
 }
 
 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 {});
+#[cw_serde]
+pub struct StreamIds(pub (StreamId, StreamId));
+impl StreamIds {
+    pub fn new(l: StreamId, r: StreamId) -> Result<Self, ContractError> {
+        if l == r {
+            Err(ContractError::StreamsShouldNotBeEqual {})
+        } else {
+            Ok(Self((l, r)))
         }
-        Ok(())
+    }
+    pub fn into_checked(self) -> Result<Self, ContractError> {
+        Self::new(*self.left(), *self.right())
+    }
+    pub fn into_inner(self) -> (StreamId, StreamId) {
+        self.0
+    }
+    pub fn left(&self) -> &StreamId {
+        &self.0 .0
+    }
+    pub fn right(&self) -> &StreamId {
+        &self.0 .1
     }
 }
+pub type ContractResult = Result<Response, ContractError>;
 
 pub const STREAM_SEQ: Item<u64> = Item::new("stream_seq");
 pub const STREAMS: Map<StreamId, Stream> = Map::new("stream");
+
+impl UncheckedStreamData {
+    pub fn into_checked(self, env: Env, deps: Deps) -> Result<CheckedStreamData, ContractError> {
+        let owner = deps.api.addr_validate(&self.owner)?;
+        let recipient = deps.api.addr_validate(&self.recipient)?;
+
+        if self.start_time >= self.end_time {
+            return Err(ContractError::InvalidStartTime {});
+        }
+
+        let block_time = env.block.time.seconds();
+
+        if self.end_time <= block_time {
+            return Err(ContractError::InvalidEndTime {});
+        }
+
+        Ok(CheckedStreamData {
+            owner,
+            recipient,
+            balance: self.balance,
+            denom: self.denom.into_checked(deps)?,
+            start_time: self.start_time,
+            end_time: self.end_time,
+            title: self.title,
+            description: self.description,
+            is_detachable: self.is_detachable,
+        })
+    }
+}
diff --git a/typescript/contracts/cw-escrow-streams/CwEscrowStreams.client.ts b/typescript/contracts/cw-escrow-streams/CwEscrowStreams.client.ts
index 8d989653..0ffc6800 100644
--- a/typescript/contracts/cw-escrow-streams/CwEscrowStreams.client.ts
+++ b/typescript/contracts/cw-escrow-streams/CwEscrowStreams.client.ts
@@ -6,7 +6,7 @@
 
 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";
+import { InstantiateMsg, ExecuteMsg, Uint128, Binary, UncheckedDenom, StreamIds, Cw20ReceiveMsg, UncheckedStreamData, QueryMsg, ConfigResponse, CheckedDenom, Addr, StreamResponse, ListStreamsResponse } from "./CwEscrowStreams.types";
 export interface CwEscrowStreamsReadOnlyInterface {
   contractAddress: string;
   getConfig: () => Promise<ConfigResponse>;
@@ -81,7 +81,7 @@ export interface CwEscrowStreamsInterface extends CwEscrowStreamsReadOnlyInterfa
   create: ({
     params
   }: {
-    params: StreamParams;
+    params: UncheckedStreamData;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   distribute: ({
     id
@@ -96,7 +96,7 @@ export interface CwEscrowStreamsInterface extends CwEscrowStreamsReadOnlyInterfa
   linkStream: ({
     ids
   }: {
-    ids: number[];
+    ids: StreamIds;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   detachStream: ({
     id
@@ -154,7 +154,7 @@ export class CwEscrowStreamsClient extends CwEscrowStreamsQueryClient implements
   create = async ({
     params
   }: {
-    params: StreamParams;
+    params: UncheckedStreamData;
   }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
     return await this.client.execute(this.sender, this.contractAddress, {
       create: {
@@ -187,7 +187,7 @@ export class CwEscrowStreamsClient extends CwEscrowStreamsQueryClient implements
   linkStream = async ({
     ids
   }: {
-    ids: number[];
+    ids: StreamIds;
   }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
     return await this.client.execute(this.sender, this.contractAddress, {
       link_stream: {
diff --git a/typescript/contracts/cw-escrow-streams/CwEscrowStreams.types.ts b/typescript/contracts/cw-escrow-streams/CwEscrowStreams.types.ts
index b9ce5633..3398f969 100644
--- a/typescript/contracts/cw-escrow-streams/CwEscrowStreams.types.ts
+++ b/typescript/contracts/cw-escrow-streams/CwEscrowStreams.types.ts
@@ -11,7 +11,7 @@ export type ExecuteMsg = {
   receive: Cw20ReceiveMsg;
 } | {
   create: {
-    params: StreamParams;
+    params: UncheckedStreamData;
   };
 } | {
   distribute: {
@@ -23,7 +23,7 @@ export type ExecuteMsg = {
   };
 } | {
   link_stream: {
-    ids: number[];
+    ids: StreamIds;
   };
 } | {
   detach_stream: {
@@ -40,24 +40,24 @@ export type ExecuteMsg = {
 };
 export type Uint128 = string;
 export type Binary = string;
-export type CheckedDenom = {
+export type UncheckedDenom = {
   native: string;
 } | {
-  cw20: Addr;
+  cw20: string;
 };
-export type Addr = string;
+export type StreamIds = [number, number];
 export interface Cw20ReceiveMsg {
   amount: Uint128;
   msg: Binary;
   sender: string;
 }
-export interface StreamParams {
-  admin: string;
+export interface UncheckedStreamData {
   balance: Uint128;
-  denom: CheckedDenom;
+  denom: UncheckedDenom;
   description?: string | null;
   end_time: number;
-  is_detachable?: boolean | null;
+  is_detachable: boolean;
+  owner: string;
   recipient: string;
   start_time: number;
   title?: string | null;
@@ -77,8 +77,13 @@ export type QueryMsg = {
 export interface ConfigResponse {
   admin: string;
 }
+export type CheckedDenom = {
+  native: string;
+} | {
+  cw20: Addr;
+};
+export type Addr = string;
 export interface StreamResponse {
-  admin: string;
   balance: Uint128;
   claimed_balance: Uint128;
   denom: CheckedDenom;
@@ -87,6 +92,7 @@ export interface StreamResponse {
   id: number;
   is_detachable: boolean;
   link_id?: number | null;
+  owner: string;
   paused: boolean;
   paused_duration?: number | null;
   paused_time?: number | null;


Test patch:
diff --git a/contracts/external/cw-escrow-streams/src/tests.rs b/contracts/external/cw-escrow-streams/src/tests.rs
index 45362d89..bf82c797 100644
--- a/contracts/external/cw-escrow-streams/src/tests.rs
+++ b/contracts/external/cw-escrow-streams/src/tests.rs
@@ -1,14 +1,15 @@
-use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg};
-use crate::state::{Stream, StreamId, StreamIdsExtensions};
+use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg, UncheckedStreamData};
+use crate::state::{Stream, StreamId, StreamIds};
 use crate::ContractError;
 
 use cosmwasm_std::testing::mock_info;
-use cosmwasm_std::{to_binary, Addr, Empty, Uint128};
+use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty, Uint128};
 use cw20::{Cw20Coin, Cw20ExecuteMsg};
-use cw_denom::CheckedDenom;
+use cw_denom::{CheckedDenom, UncheckedDenom};
 
-use cw_multi_test::{App, Contract, ContractWrapper, Executor};
+use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor};
 use dao_testing::contracts::cw20_base_contract;
+pub const NATIVE_DENOM: &str = "ujunox";
 
 fn cw_payroll_contract() -> Box<dyn Contract<Empty>> {
     let contract = ContractWrapper::new(
@@ -26,13 +27,27 @@ fn get_stream(app: &App, cw_payroll_addr: Addr, id: u64) -> Stream {
 }
 
 fn setup_app_and_instantiate_contracts(admin: Option<String>) -> (App, Addr, Addr) {
-    let mut app = App::default();
-
+    let mut app = AppBuilder::new().build(|router, _, storage| {
+        router
+            .bank
+            .init_balance(
+                storage,
+                &Addr::unchecked("alice"),
+                vec![coin(500_000, NATIVE_DENOM.to_string())],
+            )
+            .unwrap();
+        router
+            .bank
+            .init_balance(
+                storage,
+                &Addr::unchecked("bob"),
+                vec![coin(500_000, NATIVE_DENOM.to_string())],
+            )
+            .unwrap();
+    });
     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,
@@ -74,6 +89,64 @@ fn setup_app_and_instantiate_contracts(admin: Option<String>) -> (App, Addr, Add
     (app, cw20_addr, cw_payroll_addr)
 }
 
+#[test]
+fn test_create_stream() {
+    let (mut app, _cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
+    let amount = Uint128::new(1000);
+    let denom = "ujunox".to_string();
+    let unchecked_denom = UncheckedDenom::Native(denom.clone());
+    // let checked_denom = CheckedDenom::Native(denom.clone());
+    // let info = mock_info("alice", &[]);
+
+    let recipient = Addr::unchecked("bob");
+
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let sender = Addr::unchecked("alice");
+
+    let params = UncheckedStreamData {
+        owner: sender.to_string(),
+        recipient: recipient.to_string(),
+        balance: Uint128::new(0),
+        denom: unchecked_denom,
+        start_time,
+        end_time,
+        title: None,
+        description: None,
+        is_detachable: true,
+    };
+    println!("creating stream");
+    app.execute_contract(
+        sender,
+        cw_payroll_addr,
+        &ExecuteMsg::Create { params },
+        &[Coin::new(amount.u128(), denom)],
+    )
+    .unwrap();
+
+    // assert_eq!(
+    //     get_stream(&app, cw_payroll_addr, 1),
+    //     Stream {
+    //         owner: info.sender,
+    //         recipient: Addr::unchecked("bob"),
+    //         balance: Uint128::new(10),
+    //         claimed_balance: Uint128::new(0),
+    //         denom: checked_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
+}
+
 #[test]
 fn test_execute_distribute() {
     let (mut app, cw20_addr, cw_payroll_addr) = setup_app_and_instantiate_contracts(None);
@@ -92,11 +165,14 @@ fn test_execute_distribute() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -106,10 +182,10 @@ fn test_execute_distribute() {
     assert_eq!(
         get_stream(&app, cw_payroll_addr.clone(), 1),
         Stream {
-            admin: info.sender.clone(),
+            owner: info.sender.clone(),
             recipient: Addr::unchecked("bob"),
             balance: amount,
-            claimed_balance: claimed.clone(),
+            claimed_balance: claimed,
             denom: denom.clone(),
             start_time,
             end_time,
@@ -154,9 +230,9 @@ fn test_execute_distribute() {
 
     // Check final balances after distribution
     assert_eq!(
-        get_stream(&app, cw_payroll_addr.clone(), 1),
+        get_stream(&app, cw_payroll_addr, 1),
         Stream {
-            admin: info.sender,
+            owner: info.sender,
             recipient: Addr::unchecked("bob"),
             balance: Uint128::new(750),
             claimed_balance: Uint128::new(250),
@@ -240,11 +316,14 @@ fn test_execute_pause_stream() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -262,11 +341,11 @@ fn test_execute_pause_stream() {
     .unwrap();
 
     let denom = CheckedDenom::Cw20(Addr::unchecked("contract0"));
-    let saved_stream = get_stream(&app, cw_payroll_addr.clone(), stream_id);
+    let saved_stream = get_stream(&app, cw_payroll_addr, stream_id);
     assert_eq!(
         saved_stream,
         Stream {
-            admin: Addr::unchecked("alice"),
+            owner: Addr::unchecked("alice"),
             recipient: Addr::unchecked("bob"),
             balance: amount,
             claimed_balance: Uint128::zero(),
@@ -299,11 +378,14 @@ fn test_invalid_start_time() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -404,16 +486,18 @@ fn test_execute_remove_stream() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(alice.to_string()),
+            owner: alice.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: false,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
-    app.execute_contract(alice, cw20_addr.clone(), &msg, &[])
-        .unwrap();
+    app.execute_contract(alice, cw20_addr, &msg, &[]).unwrap();
 
     let stream_id: StreamId = 1;
 
@@ -450,18 +534,21 @@ fn test_execute_link_stream_invalid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: false,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
     app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
         .unwrap();
 
-    let ids = vec![1, 2];
+    let ids = StreamIds::new(1, 2).unwrap();
 
     //Link stream and verify error returned
     let error: ContractError = app
@@ -478,31 +565,22 @@ fn test_execute_link_stream_invalid() {
     assert_eq!(
         error,
         ContractError::StreamNotFound {
-            stream_id: *ids.second().unwrap()
+            stream_id: *ids.right()
         }
     );
 
-    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()),
+            owner: info.sender.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -510,14 +588,14 @@ fn test_execute_link_stream_invalid() {
         .unwrap();
 
     let sender = Addr::unchecked("bob").to_string();
-    let ids = vec![1, 2];
+    let ids = StreamIds::new(1, 2).unwrap();
 
     let unauthorized_info = mock_info(&sender, &[]);
     let error: ContractError = app
         .execute_contract(
             unauthorized_info.sender,
-            cw_payroll_addr.clone(),
-            &ExecuteMsg::LinkStream { ids: ids.clone() },
+            cw_payroll_addr,
+            &ExecuteMsg::LinkStream { ids },
             &[],
         )
         .unwrap_err()
@@ -542,11 +620,14 @@ fn test_execute_link_stream_valid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -558,30 +639,34 @@ fn test_execute_link_stream_valid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
     app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
         .unwrap();
 
-    let ids = vec![1, 2];
+    let ids = StreamIds::new(1, 2).unwrap();
+    let (left_id, right_id) = ids.clone().into_inner();
     app.execute_contract(
-        info.sender.clone(),
+        info.sender,
         cw_payroll_addr.clone(),
-        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &ExecuteMsg::LinkStream { ids },
         &[],
     )
     .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()));
+    let left_stream = get_stream(&app, cw_payroll_addr.clone(), left_id);
+    let right_stream = get_stream(&app, cw_payroll_addr, right_id);
+    assert_eq!(left_stream.link_id, Some(right_id));
+    assert_eq!(right_stream.link_id, Some(left_id));
 }
 
 #[test]
@@ -599,11 +684,14 @@ fn test_execute_detach_stream_valid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -615,39 +703,40 @@ fn test_execute_detach_stream_valid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: None,
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
     app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
         .unwrap();
 
-    let ids = vec![1, 2];
-
+    let ids = StreamIds::new(1, 2).unwrap();
+    let (left_id, right_id) = ids.clone().into_inner();
     app.execute_contract(
         info.sender.clone(),
         cw_payroll_addr.clone(),
-        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &ExecuteMsg::LinkStream { ids },
         &[],
     )
     .unwrap();
 
     app.execute_contract(
-        info.sender.clone(),
+        info.sender,
         cw_payroll_addr.clone(),
-        &ExecuteMsg::DetachStream {
-            id: *ids.first().unwrap(),
-        },
+        &ExecuteMsg::DetachStream { id: left_id },
         &[],
     )
     .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());
+    let left_stream = get_stream(&app, cw_payroll_addr.clone(), left_id);
+    let right_stream = get_stream(&app, cw_payroll_addr, right_id);
 
     assert!(left_stream.paused);
     assert!(left_stream.paused_time.is_some());
@@ -674,11 +763,14 @@ fn test_execute_detach_stream_invalid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: Some(false),
+            is_detachable: false,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -690,55 +782,49 @@ fn test_execute_detach_stream_invalid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: Some(false),
+            is_detachable: false,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
     app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
         .unwrap();
 
-    let ids = vec![1, 2];
+    let ids = StreamIds::new(1, 2).unwrap();
 
     app.execute_contract(
         info.sender.clone(),
         cw_payroll_addr.clone(),
-        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &ExecuteMsg::LinkStream { ids },
         &[],
     )
     .unwrap();
 
-    let ids = vec![11, 22];
+    let ids = StreamIds::new(11, 22).unwrap();
+    let (left_id, right_id) = ids.into_inner();
     let error: ContractError = app
         .execute_contract(
             info.sender.clone(),
             cw_payroll_addr.clone(),
-            &ExecuteMsg::DetachStream {
-                id: *ids.first().unwrap(),
-            },
+            &ExecuteMsg::DetachStream { id: left_id },
             &[],
         )
         .unwrap_err()
         .downcast()
         .unwrap();
-    assert_eq!(
-        error,
-        ContractError::StreamNotFound {
-            stream_id: *ids.first().unwrap()
-        }
-    );
+    assert_eq!(error, ContractError::StreamNotFound { stream_id: left_id });
 
-    let ids = vec![1, 22];
     let error: ContractError = app
         .execute_contract(
             info.sender.clone(),
             cw_payroll_addr.clone(),
-            &ExecuteMsg::DetachStream {
-                id: *ids.second().unwrap(),
-            },
+            &ExecuteMsg::DetachStream { id: right_id },
             &[],
         )
         .unwrap_err()
@@ -747,18 +833,17 @@ fn test_execute_detach_stream_invalid() {
     assert_eq!(
         error,
         ContractError::StreamNotFound {
-            stream_id: *ids.second().unwrap()
+            stream_id: right_id
         }
     );
 
-    let ids = vec![1, 2];
+    let (_, right_id) = StreamIds::new(1, 2).unwrap().into_inner();
+
     let error: ContractError = app
         .execute_contract(
             info.sender.clone(),
             cw_payroll_addr.clone(),
-            &ExecuteMsg::DetachStream {
-                id: *ids.second().unwrap(),
-            },
+            &ExecuteMsg::DetachStream { id: right_id },
             &[],
         )
         .unwrap_err()
@@ -773,11 +858,14 @@ fn test_execute_detach_stream_invalid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: Some(true),
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -789,18 +877,21 @@ fn test_execute_detach_stream_invalid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: Some(true),
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
-    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+    app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
         .unwrap();
 
-    let ids = vec![3, 4];
+    let ids = StreamIds::new(3, 4).unwrap();
     app.execute_contract(
         info.sender,
         cw_payroll_addr.clone(),
@@ -812,10 +903,8 @@ fn test_execute_detach_stream_invalid() {
     let error: ContractError = app
         .execute_contract(
             unauthorized_info.sender,
-            cw_payroll_addr.clone(),
-            &ExecuteMsg::DetachStream {
-                id: *ids.first().unwrap(),
-            },
+            cw_payroll_addr,
+            &ExecuteMsg::DetachStream { id: *ids.left() },
             &[],
         )
         .unwrap_err()
@@ -840,11 +929,14 @@ fn test_execute_resume_stream_valid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: Some(true),
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -856,23 +948,26 @@ fn test_execute_resume_stream_valid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient,
             start_time,
             end_time,
-            is_detachable: Some(true),
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
-    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+    app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
         .unwrap();
 
-    let ids = vec![1, 2];
-
+    let ids = StreamIds::new(1, 2).unwrap();
+    let (left_id, right_id) = ids.clone().into_inner();
     app.execute_contract(
         info.sender.clone(),
         cw_payroll_addr.clone(),
-        &ExecuteMsg::LinkStream { ids: ids.clone() },
+        &ExecuteMsg::LinkStream { ids },
         &[],
     )
     .unwrap();
@@ -880,15 +975,13 @@ fn test_execute_resume_stream_valid() {
     app.execute_contract(
         info.sender.clone(),
         cw_payroll_addr.clone(),
-        &ExecuteMsg::DetachStream {
-            id: *ids.first().unwrap(),
-        },
+        &ExecuteMsg::DetachStream { id: left_id },
         &[],
     )
     .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());
+    let left_stream = get_stream(&app, cw_payroll_addr.clone(), left_id);
+    let right_stream = get_stream(&app, cw_payroll_addr.clone(), right_id);
 
     assert!(left_stream.paused);
     assert!(left_stream.paused_time.is_some());
@@ -899,17 +992,15 @@ fn test_execute_resume_stream_valid() {
     assert!(right_stream.link_id.is_none());
 
     app.execute_contract(
-        info.sender.clone(),
+        info.sender,
         cw_payroll_addr.clone(),
-        &ExecuteMsg::ResumeStream {
-            id: *ids.first().unwrap(),
-        },
+        &ExecuteMsg::ResumeStream { id: left_id },
         &[],
     )
     .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());
+    let left_stream = get_stream(&app, cw_payroll_addr.clone(), left_id);
+    let right_stream = get_stream(&app, cw_payroll_addr, right_id);
 
     assert!(!left_stream.paused);
     assert!(left_stream.paused_time.is_none());
@@ -936,11 +1027,14 @@ fn test_execute_resume_stream_invalid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: Some(true),
+            is_detachable: true,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
@@ -952,18 +1046,21 @@ fn test_execute_resume_stream_invalid() {
         contract: cw_payroll_addr.to_string(),
         amount,
         msg: to_binary(&ReceiveMsg::CreateStream {
-            admin: Some(info.sender.to_string()),
+            owner: info.sender.to_string(),
             recipient: recipient.clone(),
             start_time,
             end_time,
-            is_detachable: Some(false),
+            is_detachable: false,
+            balance: None,
+            title: None,
+            description: None,
         })
         .unwrap(),
     };
-    app.execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+    app.execute_contract(info.sender.clone(), cw20_addr, &msg, &[])
         .unwrap();
 
-    let ids = vec![1, 2];
+    let ids = StreamIds::new(1, 2).unwrap();
     app.execute_contract(
         info.sender.clone(),
         cw_payroll_addr.clone(),
@@ -974,11 +1071,9 @@ fn test_execute_resume_stream_invalid() {
 
     let error: ContractError = app
         .execute_contract(
-            info.sender.clone(),
+            info.sender,
             cw_payroll_addr.clone(),
-            &ExecuteMsg::ResumeStream {
-                id: *ids.second().unwrap(),
-            },
+            &ExecuteMsg::ResumeStream { id: *ids.right() },
             &[],
         )
         .unwrap_err()
@@ -990,10 +1085,8 @@ fn test_execute_resume_stream_invalid() {
     let error: ContractError = app
         .execute_contract(
             unauthorized_info.sender,
-            cw_payroll_addr.clone(),
-            &ExecuteMsg::DetachStream {
-                id: *ids.first().unwrap(),
-            },
+            cw_payroll_addr,
+            &ExecuteMsg::DetachStream { id: *ids.left() },
             &[],
         )
         .unwrap_err()