Repository: hyperledger/quilt
Pull request #244 (2019-09-13 18:32:57)

Add STREAM core primitives (STREAM-PR1)
* Bump SNAPSHOT versions to 0.18.0-SNAPSHOT
* Add minor updates to ILP-core to support STREAM
* Reorg poms for STREAM
* Add stream-core module
* Defer ConnectionManager to #241 

Signed-off-by: David Fuelling <sappenin@gmail.com>

codecov-io: # [Codecov](https://codecov.io/gh/hyperledger/quilt/pull/244?src=pr&el=h1) Report
> Merging [#244](https://codecov.io/gh/hyperledger/quilt/pull/244?src=pr&el=desc) into [master](https://codecov.io/gh/hyperledger/quilt/commit/f7d7ea516345f15fe4316668d3b43bdf4d2a8e6e?src=pr&el=desc) will **decrease** coverage by `6.6%`.
> The diff coverage is `31.39%`.

[![Impacted file tree graph](https://codecov.io/gh/hyperledger/quilt/pull/244/graphs/tree.svg?width=650&token=XEfCq6zC6W&height=150&src=pr)](https://codecov.io/gh/hyperledger/quilt/pull/244?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##             master     #244      +/-   ##
============================================
- Coverage     74.65%   68.05%   -6.61%     
- Complexity      394      408      +14     
============================================
  Files           110      140      +30     
  Lines          1752     2019     +267     
  Branches        156      160       +4     
============================================
+ Hits           1308     1374      +66     
- Misses          374      574     +200     
- Partials         70       71       +1
```


| [Impacted Files](https://codecov.io/gh/hyperledger/quilt/pull/244?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...edger/stream/frames/ConnectionNewAddressFrame.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-c3RyZWFtLXBhcmVudC9zdHJlYW0tY29yZS9zcmMvbWFpbi9qYXZhL29yZy9pbnRlcmxlZGdlci9zdHJlYW0vZnJhbWVzL0Nvbm5lY3Rpb25OZXdBZGRyZXNzRnJhbWUuamF2YQ==) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...ava/org/interledger/core/InterledgerConstants.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-aWxwLWNvcmUvc3JjL21haW4vamF2YS9vcmcvaW50ZXJsZWRnZXIvY29yZS9JbnRlcmxlZGdlckNvbnN0YW50cy5qYXZh) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [.../java/org/interledger/stream/frames/ErrorCode.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-c3RyZWFtLXBhcmVudC9zdHJlYW0tY29yZS9zcmMvbWFpbi9qYXZhL29yZy9pbnRlcmxlZGdlci9zdHJlYW0vZnJhbWVzL0Vycm9yQ29kZS5qYXZh) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...dger/stream/frames/ConnectionMaxStreamIdFrame.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-c3RyZWFtLXBhcmVudC9zdHJlYW0tY29yZS9zcmMvbWFpbi9qYXZhL29yZy9pbnRlcmxlZGdlci9zdHJlYW0vZnJhbWVzL0Nvbm5lY3Rpb25NYXhTdHJlYW1JZEZyYW1lLmphdmE=) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...rg/interledger/stream/frames/StreamMoneyFrame.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-c3RyZWFtLXBhcmVudC9zdHJlYW0tY29yZS9zcmMvbWFpbi9qYXZhL29yZy9pbnRlcmxlZGdlci9zdHJlYW0vZnJhbWVzL1N0cmVhbU1vbmV5RnJhbWUuamF2YQ==) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...ava/org/interledger/stream/frames/StreamFrame.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-c3RyZWFtLXBhcmVudC9zdHJlYW0tY29yZS9zcmMvbWFpbi9qYXZhL29yZy9pbnRlcmxlZGdlci9zdHJlYW0vZnJhbWVzL1N0cmVhbUZyYW1lLmphdmE=) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...re/src/main/java/org/interledger/core/Wrapper.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-aWxwLWNvcmUvc3JjL21haW4vamF2YS9vcmcvaW50ZXJsZWRnZXIvY29yZS9XcmFwcGVyLmphdmE=) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...rg/interledger/stream/StreamConnectionDetails.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-c3RyZWFtLXBhcmVudC9zdHJlYW0tY29yZS9zcmMvbWFpbi9qYXZhL29yZy9pbnRlcmxlZGdlci9zdHJlYW0vU3RyZWFtQ29ubmVjdGlvbkRldGFpbHMuamF2YQ==) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...org/interledger/stream/frames/StreamDataFrame.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-c3RyZWFtLXBhcmVudC9zdHJlYW0tY29yZS9zcmMvbWFpbi9qYXZhL29yZy9pbnRlcmxlZGdlci9zdHJlYW0vZnJhbWVzL1N0cmVhbURhdGFGcmFtZS5qYXZh) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...java/org/interledger/stream/ConnectionManager.java](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree#diff-c3RyZWFtLXBhcmVudC9zdHJlYW0tY29yZS9zcmMvbWFpbi9qYXZhL29yZy9pbnRlcmxlZGdlci9zdHJlYW0vQ29ubmVjdGlvbk1hbmFnZXIuamF2YQ==) | `0% <0%> (ø)` | `0 <0> (?)` | |
| ... and [51 more](https://codecov.io/gh/hyperledger/quilt/pull/244/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/hyperledger/quilt/pull/244?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/hyperledger/quilt/pull/244?src=pr&el=footer). Last update [f7d7ea5...d5731ac](https://codecov.io/gh/hyperledger/quilt/pull/244?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

sappenin: Ordinarily I wouldn't make a bunch of issues for unit test work, but since there's so much here, I figured I'd using issues so anyone can parallelize this work.

Patch:
diff --git a/README.md b/README.md
index 39324ac..27c8936 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ Next, add the following Maven dependency:
   <dependency>
     <groupId>org.interledger</groupId>
     <artifactId>java-ilp-core</artifactId>
-    <version>0.17.0-SNAPSHOT</version>
+    <version>0.18.0-SNAPSHOT</version>
   </dependency>
   ...
 </dependencies>
@@ -77,7 +77,7 @@ Next, import this library as a dependency, like this:
 ```
 dependencies {
     ...
-    compile group: 'org.interledger', name: 'java-ilp-core', version: '0.17.0-SNAPSHOT'
+    compile group: 'org.interledger', name: 'java-ilp-core', version: '0.18.0-SNAPSHOT'
     ...
 }
 ```
diff --git a/btp-core/pom.xml b/btp-core/pom.xml
index a287b42..7b19ea8 100644
--- a/btp-core/pom.xml
+++ b/btp-core/pom.xml
@@ -3,15 +3,14 @@
   <parent>
     <artifactId>quilt-parent</artifactId>
     <groupId>org.interledger</groupId>
-    <version>0.17.0-SNAPSHOT</version>
+    <version>0.18.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
   <artifactId>btp-core</artifactId>
   <packaging>jar</packaging>
 
-  <name>Bilateral Transfer Protocol Core Libs</name>
-  <url>http://maven.apache.org</url>
+  <name>Quilt :: BTP Core</name>
 
   <dependencies>
     <dependency>
diff --git a/codecs/codec-framework/pom.xml b/codecs/codec-framework/pom.xml
index 23ded13..c18a378 100644
--- a/codecs/codec-framework/pom.xml
+++ b/codecs/codec-framework/pom.xml
@@ -5,11 +5,11 @@
   <parent>
     <artifactId>codecs</artifactId>
     <groupId>org.interledger</groupId>
-    <version>0.17.0-SNAPSHOT</version>
+    <version>0.18.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
-  <name>Interledger Codec Framework</name>
+  <name>Quilt :: Codecs :: Framework</name>
   <description>A framework for writing codecContext.</description>
   <artifactId>codec-framework</artifactId>
   <packaging>jar</packaging>
@@ -47,4 +47,4 @@
 
   </dependencies>
 
-</project>
\ No newline at end of file
+</project>
diff --git a/codecs/codecs-btp/pom.xml b/codecs/codecs-btp/pom.xml
index ec69545..64c78f0 100644
--- a/codecs/codecs-btp/pom.xml
+++ b/codecs/codecs-btp/pom.xml
@@ -5,12 +5,11 @@
   <parent>
     <artifactId>codecs</artifactId>
     <groupId>org.interledger</groupId>
-    <version>0.17.0-SNAPSHOT</version>
+    <version>0.18.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
-  <name>Bilateral Transfer Protocol Core Codecs</name>
-  <url>http://maven.apache.org</url>
+  <name>Quilt :: Codecs :: BTP</name>
   <description>BTP Protocol Core Codecs.</description>
   <artifactId>codecs-btp</artifactId>
 
@@ -31,4 +30,4 @@
     </dependency>
   </dependencies>
 
-</project>
\ No newline at end of file
+</project>
diff --git a/codecs/codecs-ildcp/pom.xml b/codecs/codecs-ildcp/pom.xml
index 4ea72d0..1f115fb 100644
--- a/codecs/codecs-ildcp/pom.xml
+++ b/codecs/codecs-ildcp/pom.xml
@@ -5,12 +5,14 @@
     <parent>
         <artifactId>codecs</artifactId>
         <groupId>org.interledger</groupId>
-        <version>0.17.0-SNAPSHOT</version>
+        <version>0.18.0-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <name>Interledger DCP Core Codecs</name>
+    <name>Quilt :: Codecs :: IL-DCP</name>
     <description>Interledger Dynamic Configuration Protocol (IL-DCP) core codecs.</description>
+    <artifactId>codecs-ildcp</artifactId>
+
     <dependencies>
         <dependency>
             <groupId>junit</groupId>
@@ -33,9 +35,8 @@
         <dependency>
             <groupId>org.interledger</groupId>
             <artifactId>codecs-ilp</artifactId>
-            <version>${version}</version>
         </dependency>
     </dependencies>
-    <artifactId>codecs-ildcp</artifactId>
 
-</project>
\ No newline at end of file
+
+</project>
diff --git a/codecs/codecs-ilp/pom.xml b/codecs/codecs-ilp/pom.xml
index 09139c7..ff00b9e 100644
--- a/codecs/codecs-ilp/pom.xml
+++ b/codecs/codecs-ilp/pom.xml
@@ -3,13 +3,13 @@
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
-        <artifactId>codecs</artifactId>
         <groupId>org.interledger</groupId>
-        <version>0.17.0-SNAPSHOT</version>
+        <artifactId>codecs</artifactId>
+        <version>0.18.0-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <name>Interledger Core Codecs</name>
+    <name>Quilt :: Codecs :: ILP</name>
     <description>Codecs for the Interledger Core classes.</description>
     <artifactId>codecs-ilp</artifactId>
 
@@ -39,4 +39,4 @@
         </dependency>
     </dependencies>
 
-</project>
\ No newline at end of file
+</project>
diff --git a/codecs/pom.xml b/codecs/pom.xml
index c12f320..fc8f464 100644
--- a/codecs/pom.xml
+++ b/codecs/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <artifactId>quilt-parent</artifactId>
     <groupId>org.interledger</groupId>
-    <version>0.17.0-SNAPSHOT</version>
+    <version>0.18.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -19,4 +19,4 @@
     <module>codecs-ildcp</module>
     <module>codecs-ilp</module>
   </modules>
-</project>
\ No newline at end of file
+</project>
diff --git a/ildcp-core/pom.xml b/ildcp-core/pom.xml
index 950626e..7edae78 100644
--- a/ildcp-core/pom.xml
+++ b/ildcp-core/pom.xml
@@ -3,11 +3,11 @@
   <parent>
     <artifactId>quilt-parent</artifactId>
     <groupId>org.interledger</groupId>
-    <version>0.17.0-SNAPSHOT</version>
+    <version>0.18.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
-  <name>Interledger DCP Core</name>
+  <name>Quilt :: IL-DCP Core</name>
   <description>Interledger Dynamic Configuration Protocol (IL-DCP) core libraries.</description>
   <artifactId>ildcp-core</artifactId>
   <packaging>jar</packaging>
diff --git a/ilp-core/pom.xml b/ilp-core/pom.xml
index 436712d..1c08a5c 100644
--- a/ilp-core/pom.xml
+++ b/ilp-core/pom.xml
@@ -6,13 +6,13 @@
   <parent>
     <groupId>org.interledger</groupId>
     <artifactId>quilt-parent</artifactId>
-    <version>0.17.0-SNAPSHOT</version>
+    <version>0.18.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
-  <name>Interledger Core</name>
-  <artifactId>ilp-core</artifactId>
+  <name>Quilt :: IL-DCP Core</name>
   <description>Core libraries for implementing the Interledger suite of protocols.</description>
+  <artifactId>ilp-core</artifactId>
   <packaging>jar</packaging>
 
   <dependencies>
diff --git a/ilp-core/src/main/java/org/interledger/core/InterledgerConstants.java b/ilp-core/src/main/java/org/interledger/core/InterledgerConstants.java
new file mode 100644
index 0000000..d323b76
--- /dev/null
+++ b/ilp-core/src/main/java/org/interledger/core/InterledgerConstants.java
@@ -0,0 +1,14 @@
+package org.interledger.core;
+
+/**
+ * Constants available to all Interledger libraries.
+ */
+public class InterledgerConstants {
+
+  // An empty fulfillment, used in various scenarios peer-wise scenarios where the condition/fulfillment is
+  // unimportant for processing an ILP packet.
+  public static final InterledgerFulfillment ALL_ZEROS_FULFILLMENT = InterledgerFulfillment.of(new byte[32]);
+  public static final InterledgerCondition ALL_ZEROS_CONDITION = ALL_ZEROS_FULFILLMENT.getCondition();
+
+
+}
diff --git a/ilp-core/src/main/java/org/interledger/core/InterledgerErrorCode.java b/ilp-core/src/main/java/org/interledger/core/InterledgerErrorCode.java
index 130d8ad..a169486 100644
--- a/ilp-core/src/main/java/org/interledger/core/InterledgerErrorCode.java
+++ b/ilp-core/src/main/java/org/interledger/core/InterledgerErrorCode.java
@@ -44,57 +44,81 @@ import java.util.Objects;
  */
 public interface InterledgerErrorCode {
 
+  String F00_BAD_REQUEST_CODE = "F00";
+  String F01_INVALID_PACKET_CODE = "F01";
+  String F02_UNREACHABLE_CODE = "F02";
+  String F03_INVALID_AMOUNT_CODE = "F03";
+  String F04_INSUFFICIENT_DST_AMOUNT_CODE = "F04";
+  String F05_WRONG_CONDITION_CODE = "F05";
+  String F06_UNEXPECTED_PAYMENT_CODE = "F06";
+  String F07_CANNOT_RECEIVE_CODE = "F07";
+  String F08_AMOUNT_TOO_LARGE_CODE = "F08";
+  String F99_APPLICATION_ERROR_CODE = "F99";
+
+  String T00_INTERNAL_ERROR_CODE = "T00";
+  String T01_PEER_UNREACHABLE_CODE = "T01";
+  String T02_PEER_BUSY_CODE = "T02";
+  String T03_CONNECTOR_BUSY_CODE = "T03";
+  String T04_INSUFFICIENT_LIQUIDITY_CODE = "T04";
+  String T05_RATE_LIMITED_CODE = "T05";
+  String T99_APPLICATION_ERROR_CODE = "T99";
+
+  String R00_TRANSFER_TIMED_OUT_CODE = "R00";
+  String R01_INSUFFICIENT_SOURCE_AMOUNT_CODE = "R01";
+  String R02_INSUFFICIENT_TIMEOUT_CODE = "R02";
+  String R99_APPLICATION_ERROR_CODE = "R99";
+
   /**
    * Generic sender error.
    */
-  InterledgerErrorCode F00_BAD_REQUEST =
-      InterledgerErrorCode.of("F00", "BAD REQUEST");
+  InterledgerErrorCode F00_BAD_REQUEST
+      = InterledgerErrorCode.of(F00_BAD_REQUEST_CODE, "BAD REQUEST");
 
   /**
    * The ILP packet was syntactically invalid.
    */
-  InterledgerErrorCode F01_INVALID_PACKET =
-      InterledgerErrorCode.of("F01", "INVALID PACKET");
+  InterledgerErrorCode F01_INVALID_PACKET
+      = InterledgerErrorCode.of(F01_INVALID_PACKET_CODE, "INVALID PACKET");
 
   /**
    * There was no way to forward the payment, because the destination ILP address was wrong or the connector does not
    * have a route to the destination.
    */
-  InterledgerErrorCode F02_UNREACHABLE =
-      InterledgerErrorCode.of("F02", "UNREACHABLE");
+  InterledgerErrorCode F02_UNREACHABLE
+      = InterledgerErrorCode.of(F02_UNREACHABLE_CODE, "UNREACHABLE");
 
   /**
    * The amount is invalid, for example it contains more digits of precision than are available on the destination
    * ledger or the amount is greater than the total amount of the given asset in existence.
    */
-  InterledgerErrorCode F03_INVALID_AMOUNT =
-      InterledgerErrorCode.of("F03", "INVALID AMOUNT");
+  InterledgerErrorCode F03_INVALID_AMOUNT
+      = InterledgerErrorCode.of(F03_INVALID_AMOUNT_CODE, "INVALID AMOUNT");
 
   /**
    * The receiver deemed the amount insufficient, for example you tried to pay a $100 invoice with $10.
    */
-  InterledgerErrorCode F04_INSUFFICIENT_DST_AMOUNT =
-      InterledgerErrorCode.of("F04", "INSUFFICIENT DESTINATION AMOUNT");
+  InterledgerErrorCode F04_INSUFFICIENT_DST_AMOUNT
+      = InterledgerErrorCode.of(F04_INSUFFICIENT_DST_AMOUNT_CODE, "INSUFFICIENT DESTINATION AMOUNT");
 
   /**
    * The receiver generated a different condition and cannot fulfill the payment.
    */
   InterledgerErrorCode F05_WRONG_CONDITION =
-      InterledgerErrorCode.of("F05", "WRONG CONDITION");
+      InterledgerErrorCode.of(F05_WRONG_CONDITION_CODE, "WRONG CONDITION");
 
   /**
    * The receiver was not expecting a payment like this (the data and destination address don't make sense in that
    * combination, for example if the receiver does not understand the transport protocol used).
    */
   InterledgerErrorCode F06_UNEXPECTED_PAYMENT =
-      InterledgerErrorCode.of("F06", "UNEXPECTED PAYMENT");
+      InterledgerErrorCode.of(F06_UNEXPECTED_PAYMENT_CODE, "UNEXPECTED PAYMENT");
 
   /**
    * The receiver (beneficiary) is unable to accept this payment due to a constraint. For example, the payment would put
    * the receiver above its maximum account balance.
    */
   InterledgerErrorCode F07_CANNOT_RECEIVE
-      = InterledgerErrorCode.of("F07", "CANNOT RECEIVE");
+      = InterledgerErrorCode.of(F07_CANNOT_RECEIVE_CODE, "CANNOT RECEIVE");
 
   /**
    * The packet amount is higher than the maximum a connector is willing to forward. Senders MAY send another packet
@@ -102,46 +126,46 @@ public interface InterledgerErrorCode {
    * the data to help senders determine how much lower the packet amount should be.
    */
   InterledgerErrorCode F08_AMOUNT_TOO_LARGE
-      = InterledgerErrorCode.of("F08", "AMOUNT TOO LARGE");
+      = InterledgerErrorCode.of(F08_AMOUNT_TOO_LARGE_CODE, "AMOUNT TOO LARGE");
 
   /**
    * Reserved for application layer protocols. Applications MAY use names other than Application Error.
    */
   InterledgerErrorCode F99_APPLICATION_ERROR =
-      InterledgerErrorCode.of("F99", "APPLICATION ERROR");
+      InterledgerErrorCode.of(F99_APPLICATION_ERROR_CODE, "APPLICATION ERROR");
 
   /**
    * A generic unexpected exception. This usually indicates a bug or unhandled error case.
    */
   InterledgerErrorCode T00_INTERNAL_ERROR =
-      InterledgerErrorCode.of("T00", "INTERNAL ERROR");
+      InterledgerErrorCode.of(T00_INTERNAL_ERROR_CODE, "INTERNAL ERROR");
 
   /**
    * The connector has a route or partial route to the destination but was unable to reach the next connector. Try again
    * later.
    */
   InterledgerErrorCode T01_PEER_UNREACHABLE =
-      InterledgerErrorCode.of("T01", "PEER UNREACHABLE");
+      InterledgerErrorCode.of(T01_PEER_UNREACHABLE_CODE, "PEER UNREACHABLE");
 
   /**
    * The next connector is rejecting requests due to overloading. If a connector gets this error, they SHOULD retry the
    * payment through a different route or respond to the sender with a T03: Connector Busy error.
    */
   InterledgerErrorCode T02_PEER_BUSY =
-      InterledgerErrorCode.of("T02", "PEER BUSY");
+      InterledgerErrorCode.of(T02_PEER_BUSY_CODE, "PEER BUSY");
 
   /**
    * The connector is rejecting requests due to overloading. Try again later.
    */
   InterledgerErrorCode T03_CONNECTOR_BUSY =
-      InterledgerErrorCode.of("T03", "CONNECTOR BUSY");
+      InterledgerErrorCode.of(T03_CONNECTOR_BUSY_CODE, "CONNECTOR BUSY");
 
   /**
    * The connector would like to fulfill your request, but either the sender or a connector does not currently have
    * sufficient balance or bandwidth. Try again later.
    */
   InterledgerErrorCode T04_INSUFFICIENT_LIQUIDITY =
-      InterledgerErrorCode.of("T04", "INSUFFICIENT LIQUIDITY");
+      InterledgerErrorCode.of(T04_INSUFFICIENT_LIQUIDITY_CODE, "INSUFFICIENT LIQUIDITY");
 
   /**
    * The sender is sending too many payments and is being rate-limited by a ledger or connector. If a connector gets
@@ -149,13 +173,13 @@ public interface InterledgerErrorCode {
    * to the sender with a T03: Connector Busy error.
    */
   InterledgerErrorCode T05_RATE_LIMITED =
-      InterledgerErrorCode.of("T05", "RATE LIMITED");
+      InterledgerErrorCode.of(T05_RATE_LIMITED_CODE, "RATE LIMITED");
 
   /**
    * Reserved for application layer protocols. Applications MAY use names other than Application Error.
    */
   InterledgerErrorCode T99_APPLICATION_ERROR =
-      InterledgerErrorCode.of("T99", "APPLICATION ERROR");
+      InterledgerErrorCode.of(T99_APPLICATION_ERROR_CODE, "APPLICATION ERROR");
 
   /**
    * The transfer timed out, meaning the next party in the chain did not respond. This could be because you set your
@@ -164,7 +188,7 @@ public interface InterledgerErrorCode {
    * unreasonably long time.
    */
   InterledgerErrorCode R00_TRANSFER_TIMED_OUT =
-      InterledgerErrorCode.of("R00", "TRANSFER TIMED OUT");
+      InterledgerErrorCode.of(R00_TRANSFER_TIMED_OUT_CODE, "TRANSFER TIMED OUT");
 
   /**
    * The amount received by a connector in the path was too little to forward (zero or less). Either the sender did not
@@ -172,7 +196,7 @@ public interface InterledgerErrorCode {
    * do this indefinitely or a malicious connector could steal money from them.
    */
   InterledgerErrorCode R01_INSUFFICIENT_SOURCE_AMOUNT =
-      InterledgerErrorCode.of("R01", "INSUFFICIENT SOURCE AMOUNT");
+      InterledgerErrorCode.of(R01_INSUFFICIENT_SOURCE_AMOUNT_CODE, "INSUFFICIENT SOURCE AMOUNT");
 
   /**
    * The connector could not forward the payment, because the timeout was too low to subtract its safety margin. The
@@ -180,13 +204,13 @@ public interface InterledgerErrorCode {
    * cause them to tie up their money for an unreasonably long time.
    */
   InterledgerErrorCode R02_INSUFFICIENT_TIMEOUT =
-      InterledgerErrorCode.of("R02", "INSUFFICIENT TIMEOUT");
+      InterledgerErrorCode.of(R02_INSUFFICIENT_TIMEOUT_CODE, "INSUFFICIENT TIMEOUT");
 
   /**
    * Reserved for application layer protocols. Applications MAY use names other than Application Error.
    */
   InterledgerErrorCode R99_APPLICATION_ERROR =
-      InterledgerErrorCode.of("R99", "APPLICATION ERROR");
+      InterledgerErrorCode.of(R99_APPLICATION_ERROR_CODE, "APPLICATION ERROR");
 
   /**
    * Helper method to construct an instance of {@link InterledgerErrorCode}.
@@ -462,8 +486,6 @@ public interface InterledgerErrorCode {
             + ", errorFamily=" + errorFamily
             + '}';
       }
-
-
     }
   }
 
diff --git a/ilp-core/src/main/java/org/interledger/core/InterledgerPacketType.java b/ilp-core/src/main/java/org/interledger/core/InterledgerPacketType.java
new file mode 100644
index 0000000..589fb2b
--- /dev/null
+++ b/ilp-core/src/main/java/org/interledger/core/InterledgerPacketType.java
@@ -0,0 +1,52 @@
+package org.interledger.core;
+
+import static java.lang.String.format;
+
+import java.util.Objects;
+
+/**
+ * Per the IL-RFC-27, ILPv4 packets have three types: 12 for Prepare, 13 for Fulfill, and 14 for Reject. This enum
+ * provides some type safety for code that might need this information.
+ *
+ * @see "https://github.com/interledger/rfcs/tree/master/0027-interledger-protocol-4"
+ */
+public enum InterledgerPacketType {
+
+  PREPARE((short) 12),
+  FULFILL((short) 13),
+  REJECT((short) 14);
+
+  public static final short PREPARE_CODE = (short) 12;
+  public static final short FULFILL_CODE = (short) 13;
+  public static final short REJECT_CODE = (short) 14;
+
+  private final short type;
+
+  InterledgerPacketType(short type) {
+    this.type = type;
+  }
+
+  /**
+   * Get a new {@link InterledgerPacketType} from the type.
+   *
+   * @param code the message type type.
+   *
+   * @return A new {@link InterledgerPacketType} from the provided type
+   */
+  public static InterledgerPacketType fromCode(short code) {
+    switch (code) {
+      case PREPARE_CODE:
+        return InterledgerPacketType.PREPARE;
+      case FULFILL_CODE:
+        return InterledgerPacketType.FULFILL;
+      case REJECT_CODE:
+        return InterledgerPacketType.REJECT;
+      default:
+        throw new IllegalArgumentException(format("Unknown StreamPacketType: %s", code));
+    }
+  }
+
+  public short getType() {
+    return this.type;
+  }
+}
diff --git a/ilp-core/src/main/java/org/interledger/core/Wrapped.java b/ilp-core/src/main/java/org/interledger/core/Wrapped.java
new file mode 100644
index 0000000..f5880c9
--- /dev/null
+++ b/ilp-core/src/main/java/org/interledger/core/Wrapped.java
@@ -0,0 +1,23 @@
+package org.interledger.core;
+
+import org.immutables.value.Value;
+import org.immutables.value.Value.Style.ImplementationVisibility;
+
+/**
+ * A helper interface used to configure Immutables for generating typed identifiers in conjunction with {@link
+ * Wrapper}.
+ *
+ * @see "https://immutables.github.io/immutable.html#wrapper-types"
+ */
+@Value.Style(
+    // Detect names starting with underscore
+    typeAbstract = "_*",
+    // Generate without any suffix, just raw detected name
+    typeImmutable = "*",
+    // Make generated public, leave underscored as package private
+    visibility = ImplementationVisibility.PUBLIC,
+    // Seems unnecessary to have builder or superfluous copy method
+    defaults = @Value.Immutable(builder = false, copy = false))
+public @interface Wrapped {
+
+}
diff --git a/ilp-core/src/main/java/org/interledger/core/Wrapper.java b/ilp-core/src/main/java/org/interledger/core/Wrapper.java
new file mode 100644
index 0000000..406d2f5
--- /dev/null
+++ b/ilp-core/src/main/java/org/interledger/core/Wrapper.java
@@ -0,0 +1,41 @@
+package org.interledger.core;
+
+import org.immutables.value.Value;
+
+/**
+ * A class helper for creating type-safe wrappers using Immutables in conjunction with {@link Wrapped}.
+ *
+ * @see "http://immutables.github.io/immutable.html#wrapper-types"
+ */
+public abstract class Wrapper<T extends Comparable<T>> implements Comparable<Wrapper<T>> {
+
+  @Value.Parameter
+  public abstract T value();
+
+  @Override
+  public int compareTo(Wrapper<T> otherWrapped) {
+    return value().compareTo(otherWrapped.value());
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+
+    if (obj != null && obj instanceof Wrapper) {
+      Object otherValue = ((Wrapper) obj).value();
+      if (otherValue != null) {
+        return otherValue.equals(value());
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return value().hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "(" + value() + ")";
+  }
+}
diff --git a/jackson-datatypes/README.md b/jackson-datatypes/README.md
index f353804..52350b3 100644
--- a/jackson-datatypes/README.md
+++ b/jackson-datatypes/README.md
@@ -11,7 +11,7 @@ to your project:
 <dependency>
   <groupId>org.interledger.quilt.jackson</groupId>
   <artifactId>jackson-datatypes</artifactId>
-  <version>0.17.0-SNAPSHOT</version>
+  <version>0.18.0-SNAPSHOT</version>
 </dependency>
 ```
 
@@ -21,4 +21,4 @@ Next, configure your ObjectMapper with the following:
   ObjectMapper objectMapper = new ObjectMapper()
   // ... other Module registrations
   .registerModule(new HyperledgerQuiltModule());
-```
\ No newline at end of file
+```
diff --git a/jackson-datatypes/pom.xml b/jackson-datatypes/pom.xml
index bb7b181..a5dfdad 100644
--- a/jackson-datatypes/pom.xml
+++ b/jackson-datatypes/pom.xml
@@ -5,15 +5,13 @@
   <parent>
     <groupId>org.interledger</groupId>
     <artifactId>quilt-parent</artifactId>
-    <version>0.17.0-SNAPSHOT</version>
+    <version>0.18.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
-  <name>Interledger Jackson Datatypes</name>
+  <name>Quilt :: Jackson Datatypes</name>
+  <description>Jackson serialization/deserialization support for Hyperledger Quilt projects.</description>
   <artifactId>jackson-datatypes</artifactId>
-  <description>
-    Jackson serialization/deserialization support for Hyperledger Quilt projects.
-  </description>
 
   <dependencies>
     <dependency>
@@ -50,4 +48,4 @@
       <artifactId>ilp-core</artifactId>
     </dependency>
   </dependencies>
-</project>
\ No newline at end of file
+</project>
diff --git a/pom.xml b/pom.xml
index 8342c07..3fe3ac9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
   <groupId>org.interledger</groupId>
   <artifactId>quilt-parent</artifactId>
   <packaging>pom</packaging>
-  <version>0.17.0-SNAPSHOT</version>
+  <version>0.18.0-SNAPSHOT</version>
 
   <name>Hyperledger Quilt Parent</name>
   <description>Parent project for Hyperledger Quilt modules.</description>
@@ -91,7 +91,7 @@
     <checkstyle.config.location>checkstyle.xml</checkstyle.config.location>
     <checkstyle.violationSeverity>error</checkstyle.violationSeverity>
 
-    <jackson.version>2.10.0.pr1</jackson.version>
+    <jackson.version>2.10.0.pr2</jackson.version>
   </properties>
 
   <modules>
@@ -100,6 +100,7 @@
     <module>ildcp-core</module>
     <module>jackson-datatypes</module>
     <module>codecs</module>
+    <module>stream-parent</module>
   </modules>
 
   <dependencyManagement>
@@ -111,6 +112,24 @@
         <version>${project.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>${project.groupId}</groupId>
+        <artifactId>codecs-btp</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
+      <dependency>
+        <groupId>${project.groupId}</groupId>
+        <artifactId>codecs-ildcp</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
+      <dependency>
+        <groupId>${project.groupId}</groupId>
+        <artifactId>codecs-ilp</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>${project.groupId}</groupId>
         <artifactId>ilp-core</artifactId>
@@ -147,6 +166,12 @@
         <version>${project.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-annotations</artifactId>
+        <version>${jackson.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>javax.money</groupId>
         <artifactId>money-api</artifactId>
diff --git a/stream-parent/pom.xml b/stream-parent/pom.xml
new file mode 100644
index 0000000..24c8df0
--- /dev/null
+++ b/stream-parent/pom.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <groupId>org.interledger</groupId>
+    <artifactId>quilt-parent</artifactId>
+    <version>0.18.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <name>Quilt :: STREAM :: Parent</name>
+  <artifactId>stream-parent</artifactId>
+  <packaging>pom</packaging>
+
+  <modules>
+    <module>stream-core</module>
+  </modules>
+</project>
diff --git a/stream-parent/stream-core/pom.xml b/stream-parent/stream-core/pom.xml
new file mode 100644
index 0000000..57c258c
--- /dev/null
+++ b/stream-parent/stream-core/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <groupId>org.interledger</groupId>
+    <artifactId>stream-parent</artifactId>
+    <version>0.18.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>stream-core</artifactId>
+
+  <name>Quilt :: STREAM :: Core</name>
+  <description>Core libraries for implementing the STREAM protocol.</description>
+  <packaging>jar</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>ilp-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.immutables</groupId>
+      <artifactId>value</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/AmountTooLargeErrorData.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/AmountTooLargeErrorData.java
new file mode 100644
index 0000000..1cc064f
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/AmountTooLargeErrorData.java
@@ -0,0 +1,32 @@
+package org.interledger.stream;
+
+import org.interledger.core.Immutable;
+
+import com.google.common.primitives.UnsignedLong;
+
+/**
+ * An error returned by a Connector when a packet amount is too large.  Each of the following should be included as the
+ * `data` field in an ILP Reject packet.
+ */
+@Immutable
+public interface AmountTooLargeErrorData {
+
+  static AmountTooLargeErrorDataBuilder builder() {
+    return new AmountTooLargeErrorDataBuilder();
+  }
+
+  /**
+   * Local amount received by the connector.
+   *
+   * @return A {@link UnsignedLong} with the received amount.
+   */
+  UnsignedLong receivedAmount();
+
+  /**
+   * Maximum amount (inclusive, denominated in same units as the receivedAmount) the connector will forward.
+   *
+   * @return A {@link UnsignedLong} with the maximum amount.
+   */
+  UnsignedLong maximumAmount();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/SendMoneyResult.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/SendMoneyResult.java
new file mode 100644
index 0000000..587b50c
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/SendMoneyResult.java
@@ -0,0 +1,64 @@
+package org.interledger.stream;
+
+import org.interledger.core.Immutable;
+
+import com.google.common.primitives.UnsignedLong;
+
+import java.time.Duration;
+
+/**
+ * The result of a request to send money using the STREAM protocol.
+ */
+@Immutable
+public interface SendMoneyResult {
+
+  static SendMoneyResultBuilder builder() {
+    return new SendMoneyResultBuilder();
+  }
+
+  /**
+   * The original amount that was requested to be sent.
+   *
+   * @return An {@link UnsignedLong} representing the original amount to be sent in a given STREAM payment.
+   */
+  UnsignedLong originalAmount();
+
+  /**
+   * The actual amount, in the senders units, that was delivered to the receiver.
+   *
+   * @return An {@link UnsignedLong} representing the amount delivered.
+   */
+  UnsignedLong amountDelivered();
+
+  /**
+   * Metadata representing the number of fulfilled packets in a request to send money. Note that STREAM implementations
+   * MUST close the connection once either endpoint has sent 2^31 packets.
+   *
+   * @return An integer representing the number of fulfilled packets in a STREAM payment.
+   */
+  int numFulfilledPackets();
+
+  /**
+   * Metadata representing the number of rejected packets in a request to send money. Note that STREAM implementations
+   * MUST close the connection once either endpoint has sent 2^31 packets.
+   *
+   * @return An integer representing the number of rejected packets in a STREAM payment.
+   */
+  int numRejectPackets();
+
+  /**
+   * Metadata representing the total number of packets (fulfilled or rejected) sent over a given STREAM connection.
+   *
+   * @return An int representing the total number of packets sent over a given STREAM connection.
+   */
+  default int totalPackets() {
+    return numFulfilledPackets() + numRejectPackets();
+  }
+
+  /**
+   * Metadata representing the total time the STREAM payment took to complete or fail.
+   *
+   * @return A {@link Duration} representing the total time a STREAM payment took to complete or fail.
+   */
+  Duration sendMoneyDuration();
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamConnectionDetails.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamConnectionDetails.java
new file mode 100644
index 0000000..622469d
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamConnectionDetails.java
@@ -0,0 +1,48 @@
+package org.interledger.stream;
+
+import org.interledger.core.InterledgerAddress;
+import org.interledger.stream.ImmutableStreamConnectionDetails.Builder;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.immutables.value.Value.Immutable;
+
+/**
+ * Contains information that can be used to initiate and process a STREAM connection. This information typically
+ * conforms to the SPSP protocol.
+ *
+ * @see "https://github.com/interledger/rfcs/blob/master/0009-simple-payment-setup-protocol/0009-simple-payment-setup-protocol.md"
+ */
+@Immutable
+@JsonSerialize(as = ImmutableStreamConnectionDetails.class)
+@JsonDeserialize(as = ImmutableStreamConnectionDetails.class)
+public interface StreamConnectionDetails {
+
+  static Builder builder() {
+    return ImmutableStreamConnectionDetails.builder();
+  }
+
+  /**
+   * <p>The ultimate destination of an ILPv4 packet that is part of a STREAM.</p>
+   *
+   * <p>This address is generated such that the `shared_secret` can be re-derived from a Prepare packet's destination
+   * and the same server secret. If the address is modified in any way, the server will not be able to re-derive the
+   * secret and the packet will be rejected.
+   *
+   * @return An {@link InterledgerAddress} that can receive and fulfill ILPv4 packets.
+   */
+  @JsonProperty("destination_account")
+  InterledgerAddress destinationAddress();
+
+  /**
+   * The shared secret to be used by a specific HTTP client in a STREAM. Should be shared only by the server that
+   * generated and a specific HTTP client, and should therefore be different for each query response. Even though
+   * clients SHOULD accept base64url encoded secrets, base64 encoded secrets are recommended.
+   *
+   * @return A {@link String}.
+   */
+  @JsonProperty("shared_secret")
+  String sharedSecret();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamException.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamException.java
new file mode 100644
index 0000000..d0adad9
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamException.java
@@ -0,0 +1,72 @@
+package org.interledger.stream;
+
+/**
+ * The base exception for all STREAM exceptions.
+ */
+public class StreamException extends RuntimeException {
+
+  /**
+   * Constructs a new runtime exception with {@code null} as its detail message.  The cause is not initialized, and may
+   * subsequently be initialized by a call to {@link #initCause}.
+   */
+  public StreamException() {
+    super();
+  }
+
+  /**
+   * Constructs a new runtime exception with the specified detail message. The cause is not initialized, and may
+   * subsequently be initialized by a call to {@link #initCause}.
+   *
+   * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()}
+   *                method.
+   */
+  public StreamException(String message) {
+    super(message);
+  }
+
+  /**
+   * Constructs a new runtime exception with the specified detail message and cause.  <p>Note that the detail message
+   * associated with {@code cause} is <i>not</i> automatically incorporated in this runtime exception's detail message.
+   *
+   * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method).
+   * @param cause   the cause (which is saved for later retrieval by the {@link #getCause()} method).  (A <tt>null</tt>
+   *                value is permitted, and indicates that the cause is nonexistent or unknown.)
+   *
+   * @since 1.4
+   */
+  public StreamException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  /**
+   * Constructs a new runtime exception with the specified cause and a detail message of <tt>(cause==null ? null :
+   * cause.toString())</tt> (which typically contains the class and detail message of
+   * <tt>cause</tt>).  This constructor is useful for runtime exceptions
+   * that are little more than wrappers for other throwables.
+   *
+   * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).  (A <tt>null</tt>
+   *              value is permitted, and indicates that the cause is nonexistent or unknown.)
+   *
+   * @since 1.4
+   */
+  public StreamException(Throwable cause) {
+    super(cause);
+  }
+
+  /**
+   * Constructs a new runtime exception with the specified detail message, cause, suppression enabled or disabled, and
+   * writable stack trace enabled or disabled.
+   *
+   * @param message            the detail message.
+   * @param cause              the cause.  (A {@code null} value is permitted, and indicates that the cause is
+   *                           nonexistent or unknown.)
+   * @param enableSuppression  whether or not suppression is enabled or disabled
+   * @param writableStackTrace whether or not the stack trace should be writable
+   *
+   * @since 1.7
+   */
+  protected StreamException(String message, Throwable cause, boolean enableSuppression,
+      boolean writableStackTrace) {
+    super(message, cause, enableSuppression, writableStackTrace);
+  }
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamPacket.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamPacket.java
new file mode 100644
index 0000000..1697dc8
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamPacket.java
@@ -0,0 +1,119 @@
+package org.interledger.stream;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.Immutable;
+import org.interledger.core.InterledgerPacketType;
+import org.interledger.stream.frames.StreamFrame;
+import org.interledger.stream.frames.StreamFrameType;
+
+import java.util.List;
+
+/**
+ * <p>Interledger Payments moves assets of one party to another that consists of one or more ledger
+ * transfers, potentially across multiple ledgers.</p>
+ *
+ * <p>Interledger Payments have three major consumers:</p>
+ * <ul>
+ * <li>Connectors utilize the Interledger Address contained in the payment to route the
+ * payment.</li>
+ * <li>The receiver of a payment uses it to identify the recipient and which condition to
+ * fulfill.</li>
+ * <li>Interledger sub-protocols utilize custom data encoded in a payment to facilitate
+ * sub-protocol operations.</li>
+ * </ul>
+ *
+ * <p>When a sender prepares a transfer to start a payment, the sender attaches an ILP Payment to
+ * the transfer, in the memo field if possible. If a ledger does not support attaching the entire ILP Payment to a
+ * transfer as a memo, users of that ledger can transmit the ILP Payment using another authenticated messaging channel,
+ * but MUST be able to correlate transfers and ILP Payments.</p>
+ *
+ * <p>When a connector sees an incoming prepared transfer with an ILP Payment, the receiver reads
+ * the ILP Payment to confirm the details of the packet. For example, the connector reads the InterledgerAddress of the
+ * payment's receiver, and if the connector has a route to the receiver's account, the connector prepares a transfer to
+ * continue the payment, and attaches the same ILP Payment to the new transfer. Likewise, the receiver confirms that the
+ * amount of the ILP Payment Packet matches the amount actually delivered by the transfer. And finally, the receiver
+ * decodes the data portion of the Payment and matches the condition to the payment.</p>
+ *
+ * <p>The receiver MUST confirm the integrity of the ILP Payment, for example with a hash-based
+ * message authentication code (HMAC). If the receiver finds the transfer acceptable, the receiver releases the
+ * fulfillment for the transfer, which can be used to execute all prepared transfers that were established prior to the
+ * receiver accepting the payment.</p>
+ */
+@Immutable
+public interface StreamPacket {
+
+  int VERSION_1 = 1;
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link StreamPacketBuilder} instance.
+   */
+  static StreamPacketBuilder builder() {
+    return new StreamPacketBuilder();
+  }
+
+  /**
+   * The version of this packet, currently 1.
+   *
+   * @return This packet's STREAM version (always 1).
+   */
+  default short version() {
+    return VERSION_1;
+  }
+
+  /**
+   * ILPv4 packet type this STREAM packet MUST be sent in (12 for Prepare, 13 for Fulfill, and 14 for Reject).
+   *
+   * @return A {@link InterledgerPacketType}
+   */
+  InterledgerPacketType interledgerPacketType();
+
+  /**
+   * Identifier for an ILP request / response. Clients and Servers track their own outgoing packet sequence numbers and
+   * increment the Sequence for each ILP Prepare they send. The Receiver MUST respond with a STREAM packet that includes
+   * the same Sequence as the Sender's Prepare packet. A sender MUST discard a STREAM packet in which the Sequence does
+   * not match the STREAM packet sent with their ILP Prepare.
+   *
+   * @return A {@link UnsignedLong} for this packet's sequence.
+   */
+  UnsignedLong sequence();
+
+  /**
+   * If the STREAM packet is sent on an ILP Prepare, this represents the minimum the receiver should accept (this is a
+   * signal between the sender and receiver that cannot be tampered with by intermediate nodes, and acts as a safety net
+   * against fees that might be too high in the newtwork). If the packet is sent on an ILP Fulfill or Reject, this
+   * represents the amount that the receiver got in the Prepare.
+   *
+   * @return A {@link UnsignedLong} for this packet's prepareAmount.
+   */
+  UnsignedLong prepareAmount();
+
+  /**
+   * A list of {@link StreamFrameType}.
+   *
+   * @return A {@link List} of Stream Frames that this packet contains.
+   */
+  List<StreamFrame> frames();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamUtils.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamUtils.java
new file mode 100644
index 0000000..4e6db6e
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/StreamUtils.java
@@ -0,0 +1,104 @@
+package org.interledger.stream;
+
+import org.interledger.core.InterledgerCondition;
+import org.interledger.core.InterledgerFulfillment;
+import org.interledger.stream.crypto.Random;
+
+import com.google.common.hash.Hashing;
+import com.google.common.primitives.UnsignedLong;
+
+import java.nio.charset.Charset;
+import java.util.Objects;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Utilities to support the STREAM protocol.
+ */
+public class StreamUtils {
+
+  /**
+   * The string "ilp_stream_fulfillment" is encoded as UTF-8 or ASCII (the byte representation is the same with both
+   * encodings).
+   */
+  private static final byte[] ILP_STREAM_FULFILLMENT = "ilp_stream_fulfillment".getBytes(Charset.forName("UTF-8"));
+
+  private static final String HMAC_SHA256_ALG_NAME = "HmacSHA256";
+
+  /**
+   * If the sender does not want the receiver to be able to fulfill the payment (as for an informational quote), they
+   * can generate an unfulfillable random condition.
+   *
+   * @return A {@link InterledgerCondition} that is not fulfillable.
+   */
+  public static InterledgerCondition unfulfillableCondition() {
+    return InterledgerCondition.of(Random.randBytes(32));
+  }
+
+  /**
+   * <p>If the sender _does_ want the receiver to be able to fulfill the condition, the condition MUST be generated
+   * from a fulfillment in the following manner: First, the shared_secret is the cryptographic seed exchanged during
+   * Setup. The string "ilp_stream_fulfillment" is encoded as UTF-8 or ASCII (the byte representation is the same with
+   * both encodings). Finally, the data is the encrypted STREAM packet.</p>
+   *
+   * @param sharedSecret The cryptographic seed exchanged during STREAM Setup.
+   * @param data         The encrypted STREAM packet in ASN.1 OER bytes.
+   *
+   * @return An {@link InterledgerFulfillment} that can be used to prove a payment.
+   */
+  public static final InterledgerFulfillment generatedFulfillableFulfillment(
+      final byte[] sharedSecret, final byte[] data
+  ) {
+    Objects.requireNonNull(sharedSecret);
+    Objects.requireNonNull(data);
+
+    // hmac_key = hmac_sha256(shared_secret, "ilp_stream_fulfillment");
+    final SecretKey secretKey = new SecretKeySpec(sharedSecret, HMAC_SHA256_ALG_NAME);
+    final byte[] hmacKey = Hashing.hmacSha256(secretKey).hashBytes(ILP_STREAM_FULFILLMENT).asBytes();
+
+    // fulfillment = hmac_sha256(hmac_key, data);
+    final SecretKey hmacSecretKey = new SecretKeySpec(hmacKey, HMAC_SHA256_ALG_NAME);
+    final byte[] fulfillmentBytes = Hashing.hmacSha256(hmacSecretKey).hashBytes(data).asBytes();
+
+    return InterledgerFulfillment.of(fulfillmentBytes);
+  }
+
+  /**
+   * Compute the smaller of {@code value1} and {@code value2}.
+   *
+   * @param value1 The first value.
+   * @param value2 The second value.
+   *
+   * @return The smaller of the two supplied values.
+   */
+  public static UnsignedLong min(final UnsignedLong value1, final UnsignedLong value2) {
+    Objects.requireNonNull(value1);
+    Objects.requireNonNull(value2);
+
+    if (value1.compareTo(value2) < 0) {
+      return value1;
+    } else {
+      return value2;
+    }
+  }
+
+  /**
+   * Compute the larger of {@code value1} and {@code value2}.
+   *
+   * @param value1 The first value.
+   * @param value2 The second value.
+   *
+   * @return The smaller of the two supplied values.
+   */
+  public static UnsignedLong max(final UnsignedLong value1, final UnsignedLong value2) {
+    Objects.requireNonNull(value1);
+    Objects.requireNonNull(value2);
+
+    if (value1.compareTo(value2) > 0) {
+      return value1;
+    } else {
+      return value2;
+    }
+  }
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/EncryptionException.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/EncryptionException.java
new file mode 100644
index 0000000..c5e827c
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/EncryptionException.java
@@ -0,0 +1,19 @@
+package org.interledger.stream.crypto;
+
+public class EncryptionException extends RuntimeException {
+
+  public EncryptionException() {
+  }
+
+  public EncryptionException(String message) {
+    super(message);
+  }
+
+  public EncryptionException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  public EncryptionException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/JavaxStreamEncryptionService.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/JavaxStreamEncryptionService.java
new file mode 100644
index 0000000..cf916ba
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/JavaxStreamEncryptionService.java
@@ -0,0 +1,148 @@
+package org.interledger.stream.crypto;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.hash.Hashing;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An {@link StreamEncryptionService} that uses a JavaKeystore for underlying key storage.
+ */
+public class JavaxStreamEncryptionService implements StreamEncryptionService {
+
+  private static final String CIPHER_ALGO = "AES/GCM/NoPadding";
+
+  private static final byte[] ENCRYPTION_KEY_STRING = "ilp_stream_encryption".getBytes(Charset.forName("US-ASCII"));
+
+  /**
+   * For GCM a 12 byte random byte-array is recommend by NIST because it's faster and more secure (See page 8 in the PDF
+   * document below).
+   *
+   * @see "https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf"
+   */
+  private static final int AES_GCM_NONCE_IV_LENGTH = 12;
+
+  private static final int AUTH_TAG_LENGTH_BITS = 128;
+  private static final int AUTH_TAG_LENGTH_BYTES = AUTH_TAG_LENGTH_BITS / 8;
+
+  @Override
+  public byte[] encrypt(final byte[] sharedSecret, final byte[] plainText) throws EncryptionException {
+    Objects.requireNonNull(plainText);
+
+    // Create an initialization vector. For GCM a 12 byte random byte-array is recommend by NIST
+    // because it's faster and more secure. See here, page 8:
+    // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
+    final byte[] iv = Random.randBytes(AES_GCM_NONCE_IV_LENGTH);
+
+    return encryptWithIv(sharedSecret, plainText, iv);
+  }
+
+  @VisibleForTesting
+  protected byte[] encryptWithIv(final byte[] sharedSecret, final byte[] plainText, final byte[] iv)
+      throws EncryptionException {
+    Objects.requireNonNull(plainText);
+
+    byte[] encryptionKey = Hashing.hmacSha256(sharedSecret).hashBytes(ENCRYPTION_KEY_STRING).asBytes();
+    final SecretKey typedEncryptionKey = new SecretKeySpec(encryptionKey, "AES");
+
+    // See https://proandroiddev.com/security-best-practices-symmetric-encryption-with-aes-in-java-7616beaaade9
+    try {
+      final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
+      // 128 is the recommended authentication tag length for GCM. More info can be found in pdf mentioned above.
+      // After each encryption operation using GCM mode, callers should re-initialize the cipher objects with GCM
+      // parameters using a different IV value.
+      final GCMParameterSpec parameterSpec = new GCMParameterSpec(AUTH_TAG_LENGTH_BITS, iv);
+      cipher.init(Cipher.ENCRYPT_MODE, typedEncryptionKey, parameterSpec);
+      byte[] cipherText = cipher.doFinal(plainText);
+
+      // TODO: Fix this in JS and Rust so that the typical ordering is not wonky, and avoids this copy.
+      byte[] rearrangedCipherText = new byte[cipherText.length];
+      // Rearrange the bytes so that the tag goes first (should have put it last in the JS implementation, but oh well)
+      System.arraycopy(
+          cipherText, cipherText.length - AUTH_TAG_LENGTH_BYTES, rearrangedCipherText, 0, AUTH_TAG_LENGTH_BYTES
+      );
+      // Put the cipherText last...
+      System.arraycopy(
+          cipherText, 0, rearrangedCipherText, AUTH_TAG_LENGTH_BYTES, cipherText.length - AUTH_TAG_LENGTH_BYTES
+      );
+
+      // Concatenate to a single message
+      final ByteBuffer cipherMessageByteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
+      cipherMessageByteBuffer.put(iv);
+      cipherMessageByteBuffer.put(rearrangedCipherText);
+      final byte[] cipherMessage = cipherMessageByteBuffer.array();
+
+      // It is best practice to try to wipe sensible data like a cryptographic key or IV from memory as fast as
+      // possible. Since Java is a language with automatic memory management, we don’t have any guarantees that the
+      // following works as intended, but it should in most cases
+      Arrays.fill(iv, (byte) 0); //overwrite the content of key with zeros
+      Arrays.fill(encryptionKey, (byte) 0);
+
+      return cipherMessage;
+    } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException |
+        InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
+      throw new EncryptionException("Unable to Encrypt: ", e);
+    }
+  }
+
+  @Override
+  public byte[] decrypt(final byte[] sharedSecret, final byte[] cipherMessage) {
+    Objects.requireNonNull(cipherMessage);
+
+    byte[] encryptionKey = Hashing.hmacSha256(sharedSecret).hashBytes(ENCRYPTION_KEY_STRING).asBytes();
+    final SecretKey typedEncryptionKey = new SecretKeySpec(encryptionKey, "AES");
+
+    // First, deconstruct the message
+    try {
+      ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
+      int ivLength = AES_GCM_NONCE_IV_LENGTH;
+      if (ivLength < 12 || ivLength >= 16) { // check input parameter
+        throw new IllegalArgumentException("invalid IV length (suggested is " + AES_GCM_NONCE_IV_LENGTH);
+      }
+      byte[] iv = new byte[ivLength];
+      byteBuffer.get(iv);
+      byte[] cipherText = new byte[byteBuffer.remaining()];
+      byteBuffer.get(cipherText);
+
+      // TODO: Fix this in JS and Rust so that the typical ordering is not wonky, and avoids this copy.
+      byte[] rearrangedCipherText = new byte[cipherText.length];
+      // Rearrange the bytes so that the tag goes last (should have put it first in the JS implementation, but oh well)
+      System.arraycopy(
+          cipherText, 0, rearrangedCipherText, cipherText.length - AUTH_TAG_LENGTH_BYTES, AUTH_TAG_LENGTH_BYTES
+      );
+      // Put the cipherText last...
+      System.arraycopy(
+          cipherText, AUTH_TAG_LENGTH_BYTES, rearrangedCipherText, 0, cipherText.length - AUTH_TAG_LENGTH_BYTES
+      );
+
+      final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
+      cipher.init(Cipher.DECRYPT_MODE, typedEncryptionKey, new GCMParameterSpec(AUTH_TAG_LENGTH_BITS, iv));
+      byte[] plainText = cipher.doFinal(rearrangedCipherText);
+
+      // It is best practice to try to wipe sensible data like a cryptographic key or IV from memory as fast as
+      // possible. Since Java is a language with automatic memory management, we don’t have any guarantees that the
+      // following works as intended, but it should in most cases:
+      Arrays.fill(iv, (byte) 0); //overwrite the content of key with zeros
+      Arrays.fill(encryptionKey, (byte) 0);
+
+      return plainText;
+    } catch (Exception e) {
+      throw new EncryptionException(e.getMessage(), e);
+    }
+  }
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/Random.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/Random.java
new file mode 100644
index 0000000..7e04515
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/Random.java
@@ -0,0 +1,37 @@
+package org.interledger.stream.crypto;
+
+import java.security.SecureRandom;
+
+/**
+ * A simple wrapper of {@link SecureRandom}.
+ *
+ * @since 1.0.0
+ */
+public final class Random {
+
+  private static final ThreadLocal<SecureRandom> localRandom = new ThreadLocal<SecureRandom>() {
+    @Override
+    protected SecureRandom initialValue() {
+      return newDefaultSecureRandom();
+    }
+  };
+
+  private static SecureRandom newDefaultSecureRandom() {
+    SecureRandom retval = new SecureRandom();
+    retval.nextLong(); // force seeding
+    return retval;
+  }
+
+  /**
+   * Helper method to construct a byte array containing {@code size} random bytes of data.
+   *
+   * @param size The number of random bytes to generate.
+   *
+   * @return a random byte array of size {@code size}.
+   */
+  public static byte[] randBytes(int size) {
+    byte[] rand = new byte[size];
+    localRandom.get().nextBytes(rand);
+    return rand;
+  }
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/StreamEncryptionService.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/StreamEncryptionService.java
new file mode 100644
index 0000000..da948b9
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/StreamEncryptionService.java
@@ -0,0 +1,32 @@
+package org.interledger.stream.crypto;
+
+/**
+ * A helper class for performing encryption and decryption operations required by the STREAM protocol.
+ *
+ * @see "https://github.com/interledger/rfcs/blob/master/0029-stream/0029-stream.md#51-encryption"
+ */
+public interface StreamEncryptionService {
+
+  /**
+   * Encrypt {@code plainText} using the underlying key-store of this implementation.
+   *
+   * @param sharedSecret A byte array containing the secret STREAM connection value shared between sender and *
+   *                     receiver.
+   * @param plainText    A byte-array containing the plaintext value to encrypt.
+   *
+   * @return An byte array containing {@code plainText}, but in encrypted form.
+   */
+  byte[] encrypt(final byte[] sharedSecret, final byte[] plainText);
+
+  /**
+   * Decrypt the supplied {@code cipherText}.
+   *
+   * @param sharedSecret  A byte array containing the secret STREAM connection value shared between sender and
+   *                      receiver.
+   * @param cipherMessage A byte array containing encrypted ciphertext and an IV/nonce.
+   *
+   * @return A byte array containing the unencrypted secret value, in binary form.
+   */
+  byte[] decrypt(final byte[] sharedSecret, final byte[] cipherMessage);
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionAssetDetailsFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionAssetDetailsFrame.java
new file mode 100644
index 0000000..cabe0c6
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionAssetDetailsFrame.java
@@ -0,0 +1,59 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import org.interledger.core.Immutable;
+
+/**
+ * A STREAM frame for propagating asset details for a Connection.
+ */
+@Immutable
+public interface ConnectionAssetDetailsFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link ConnectionAssetDetailsFrameBuilder} instance.
+   */
+  static ConnectionAssetDetailsFrameBuilder builder() {
+    return new ConnectionAssetDetailsFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.ConnectionAssetDetails;
+  }
+
+  /**
+   * Asset code of endpoint that sent the frame.
+   *
+   * @return {@link String} representing the asset code.
+   */
+  String sourceAssetCode();
+
+  /**
+   * Asset scale of endpoint that sent the frame.
+   *
+   * @return A {@link Short}.
+   */
+  short sourceAssetScale();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionCloseFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionCloseFrame.java
new file mode 100644
index 0000000..3b33cfe
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionCloseFrame.java
@@ -0,0 +1,64 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import org.interledger.core.Immutable;
+
+import java.util.Optional;
+
+/**
+ * <p>Indicates that the connection was closed.</p>
+ *
+ * <p>If implementations allow half-open connections, an endpoint MAY continue sending packets after receiving a
+ * ConnectionClose frame. Otherwise, the endpoint MUST close the connection immediately.</p>
+ */
+@Immutable
+public interface ConnectionCloseFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link ConnectionCloseFrameBuilder} instance.
+   */
+  static ConnectionCloseFrameBuilder builder() {
+    return new ConnectionCloseFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.ConnectionClose;
+  }
+
+  /**
+   * Machine-readable {@link ErrorCode} indicating why the connection was closed.
+   *
+   * @return The {@link ErrorCode} for this frame.
+   */
+  ErrorCode errorCode();
+
+  /**
+   * Human-readable string intended to give more information helpful for debugging purposes.
+   *
+   * @return An optionally present error message as a {@link String}.
+   */
+  Optional<String> errorMessage();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionDataBlockedFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionDataBlockedFrame.java
new file mode 100644
index 0000000..f184ba4
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionDataBlockedFrame.java
@@ -0,0 +1,59 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.Immutable;
+
+/**
+ * <p>Used to advertise to the other stream party that the frame sender has more data to send, but this would exceed
+ * the other endpoint's advertised value as found in the latest {@link StreamDataMaxFrame} frame.</p>
+ *
+ * <p>The amounts in this frame are denominated in the units of the endpoint sending the frame, so the other endpoint
+ * must use their calculated exchange rate to determine how much more they can send for this stream.</p>
+ *
+ * <p>Note that this frame is primarily intended for debugging purposes.</p>
+ */
+@Immutable
+public interface ConnectionDataBlockedFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link ConnectionDataBlockedFrameBuilder} instance.
+   */
+  static ConnectionDataBlockedFrameBuilder builder() {
+    return new ConnectionDataBlockedFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.ConnectionDataBlocked;
+  }
+
+  /**
+   * The total number of bytes the endpoint is willing to receive on this connection.
+   *
+   * @return An {@link UnsignedLong} containing the max offset.
+   */
+  UnsignedLong maxOffset();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionDataMaxFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionDataMaxFrame.java
new file mode 100644
index 0000000..b7cd71f
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionDataMaxFrame.java
@@ -0,0 +1,53 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.Immutable;
+
+/**
+ * Indicates that the connection was closed.
+ */
+@Immutable
+public interface ConnectionDataMaxFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link ConnectionDataMaxFrameBuilder} instance.
+   */
+  static ConnectionDataMaxFrameBuilder builder() {
+    return new ConnectionDataMaxFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.ConnectionDataMax;
+  }
+
+  /**
+   * The total number of bytes the endpoint is willing to receive on this connection.
+   *
+   * @return An {@link UnsignedLong} containing the max offset.
+   */
+  UnsignedLong maxOffset();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionMaxStreamIdFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionMaxStreamIdFrame.java
new file mode 100644
index 0000000..5242def
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionMaxStreamIdFrame.java
@@ -0,0 +1,53 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.Immutable;
+
+/**
+ * Indicates that the connection was closed.
+ */
+@Immutable
+public interface ConnectionMaxStreamIdFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link ConnectionMaxStreamIdFrameBuilder} instance.
+   */
+  static ConnectionMaxStreamIdFrameBuilder builder() {
+    return new ConnectionMaxStreamIdFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.ConnectionMaxStreamId;
+  }
+
+  /**
+   * The maximum stream ID the endpoint is willing to accept.
+   *
+   * @return An {@link UnsignedLong} containing the max stream id.
+   */
+  UnsignedLong maxStreamId();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionNewAddressFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionNewAddressFrame.java
new file mode 100644
index 0000000..e9b3c47
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionNewAddressFrame.java
@@ -0,0 +1,53 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import org.interledger.core.Immutable;
+import org.interledger.core.InterledgerAddress;
+
+/**
+ * Indicates that the connection was closed.
+ */
+@Immutable
+public interface ConnectionNewAddressFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link ConnectionNewAddressFrameBuilder} instance.
+   */
+  static ConnectionNewAddressFrameBuilder builder() {
+    return new ConnectionNewAddressFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.ConnectionNewAddress;
+  }
+
+  /**
+   * The new ILP address of the endpoint that sent the frame.
+   *
+   * @return A {@link InterledgerAddress} representing the source address of the receiver.
+   */
+  InterledgerAddress sourceAddress();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionStreamIdBlockedFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionStreamIdBlockedFrame.java
new file mode 100644
index 0000000..6a94801
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ConnectionStreamIdBlockedFrame.java
@@ -0,0 +1,54 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.Immutable;
+
+/**
+ * Indicates that the connection was closed.
+ */
+@Immutable
+public interface ConnectionStreamIdBlockedFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link ConnectionStreamIdBlockedFrameBuilder} instance.
+   */
+  static ConnectionStreamIdBlockedFrameBuilder builder() {
+    return new ConnectionStreamIdBlockedFrameBuilder();
+  }
+
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.ConnectionStreamIdBlocked;
+  }
+
+  /**
+   * The maximum stream ID the endpoint wishes to open.
+   *
+   * @return An {@link UnsignedLong} containing the max stream id.
+   */
+  UnsignedLong maxStreamId();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ErrorCode.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ErrorCode.java
new file mode 100644
index 0000000..d8172c7
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ErrorCode.java
@@ -0,0 +1,132 @@
+package org.interledger.stream.frames;
+
+import static org.interledger.stream.frames.ErrorCodeConstants.APPLICATION_ERROR;
+import static org.interledger.stream.frames.ErrorCodeConstants.ENDPOINT_BUSY;
+import static org.interledger.stream.frames.ErrorCodeConstants.FLOW_CONTROL_ERROR;
+import static org.interledger.stream.frames.ErrorCodeConstants.FRAME_FORMAT_ERROR;
+import static org.interledger.stream.frames.ErrorCodeConstants.INTERNAL_ERROR;
+import static org.interledger.stream.frames.ErrorCodeConstants.NO_ERROR;
+import static org.interledger.stream.frames.ErrorCodeConstants.PROTOCOL_VIOLATION;
+import static org.interledger.stream.frames.ErrorCodeConstants.STREAM_ID_ERROR;
+import static org.interledger.stream.frames.ErrorCodeConstants.STREAM_STATE_ERROR;
+
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * Error codes are sent in StreamClose and ConnectionClose frames to indicate what caused the stream or connection to be
+ * closed.
+ */
+public enum ErrorCode {
+
+  /**
+   * Indicates the stream or connection closed normally.
+   */
+  NoError(NO_ERROR, "The stream or connection closed normally."),
+
+  /**
+   * The endpoint encountered an unexpected error.
+   */
+  InternalError(INTERNAL_ERROR, "The endpoint encountered an unexpected error."),
+
+  /**
+   * The endpoint is temporarily overloaded and unable to process the packet.
+   */
+  EndpointBusy(ENDPOINT_BUSY, "The endpoint is temporarily overloaded and unable to process the packet."),
+
+  /**
+   * The other endpoint exceeded the flow control limits advertised.
+   */
+  FlowControlError(FLOW_CONTROL_ERROR, "The other endpoint exceeded the flow control limits advertised."),
+
+  /**
+   * The other endpoint opened more streams than allowed.
+   */
+  StreamIdError(STREAM_ID_ERROR, "The other endpoint opened more streams than allowed."),
+
+  /**
+   * The other endpoint sent frames for a stream that is already closed.
+   */
+  StreamStateError(STREAM_STATE_ERROR, "The other endpoint sent frames for a stream that is already closed."),
+
+  /**
+   * The other endpoint sent a frame with invalid syntax.
+   */
+  FrameFormatError(FRAME_FORMAT_ERROR, "The other endpoint sent a frame with invalid syntax."),
+
+  /**
+   * The other endpoint sent invalid data or otherwise violated the protocol.
+   */
+  ProtocolViolation(PROTOCOL_VIOLATION, "The other endpoint sent invalid data or otherwise violated the protocol."),
+
+  /**
+   * The application using STREAM closed the stream or connection with an error.
+   */
+  ApplicationError(APPLICATION_ERROR, "The application using STREAM closed the stream or connection with an error.");
+
+  private final short code;
+  private final String description;
+
+  ErrorCode(short code, String description) {
+    this.code = Objects.requireNonNull(code);
+    this.description = Objects.requireNonNull(description);
+  }
+
+  /**
+   * Helper method to construct an instance of {@link ErrorCode}.
+   *
+   * @param code The definitive identifier of the error.
+   *
+   * @return An {@link ErrorCode}
+   */
+  public static ErrorCode of(final short code) {
+    switch (code) {
+      case NO_ERROR: {
+        return NoError;
+      }
+      case INTERNAL_ERROR: {
+        return InternalError;
+      }
+      case ENDPOINT_BUSY: {
+        return EndpointBusy;
+      }
+      case FLOW_CONTROL_ERROR: {
+        return FlowControlError;
+      }
+      case STREAM_ID_ERROR: {
+        return StreamIdError;
+      }
+      case STREAM_STATE_ERROR: {
+        return StreamStateError;
+      }
+      case FRAME_FORMAT_ERROR: {
+        return FrameFormatError;
+      }
+      case PROTOCOL_VIOLATION: {
+        return ProtocolViolation;
+      }
+      case APPLICATION_ERROR: {
+        return ApplicationError;
+      }
+      default: {
+        throw new RuntimeException("Invalid Stream Error code: " + code);
+      }
+    }
+  }
+
+  public short getCode() {
+    return code;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  @Override
+  public String toString() {
+    return new StringJoiner(", ", ErrorCode.class.getSimpleName() + "[", "]")
+        .add("code=" + code)
+        .add("description='" + description + "'")
+        .toString();
+  }
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ErrorCodeConstants.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ErrorCodeConstants.java
new file mode 100644
index 0000000..067beba
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/ErrorCodeConstants.java
@@ -0,0 +1,15 @@
+package org.interledger.stream.frames;
+
+public interface ErrorCodeConstants {
+
+  short NO_ERROR = (short) 0x01;
+  short INTERNAL_ERROR = (short) 0x02;
+  short ENDPOINT_BUSY = (short) 0x03;
+  short FLOW_CONTROL_ERROR = (short) 0x04;
+  short STREAM_ID_ERROR = (short) 0x05;
+  short STREAM_STATE_ERROR = (short) 0x06;
+  short FRAME_FORMAT_ERROR = (short) 0x07;
+  short PROTOCOL_VIOLATION = (short) 0x08;
+  short APPLICATION_ERROR = (short) 0x09;
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamCloseFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamCloseFrame.java
new file mode 100644
index 0000000..b91454f
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamCloseFrame.java
@@ -0,0 +1,74 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.immutables.value.Value;
+import org.interledger.core.Immutable;
+
+/**
+ * <p>Indicates that the Stream was closed.</p>
+ *
+ * <p>If implementations allow half-open streams, an endpoint MAY continue sending money or data for this stream after
+ * receiving a StreamClose frame. Otherwise, the endpoint MUST close the stream immediately.</p>
+ */
+@Immutable
+public interface StreamCloseFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link StreamCloseFrameBuilder} instance.
+   */
+  static StreamCloseFrameBuilder builder() {
+    return new StreamCloseFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.StreamClose;
+  }
+
+  /**
+   * Identifier of the stream this frame refers to.
+   *
+   * @return An {@link UnsignedLong} containing the max stream id.
+   */
+  UnsignedLong streamId();
+
+  /**
+   * Machine-readable {@link ErrorCode} indicating why the Stream was closed.
+   *
+   * @return A {@link ErrorCode}.
+   */
+  ErrorCode errorCode();
+
+  /**
+   * Human-readable string intended to give more information helpful for debugging purposes.
+   *
+   * @return A {@link String} with the error message.
+   */
+  @Value.Default
+  default String errorMessage() {
+    return "";
+  }
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataBlockedFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataBlockedFrame.java
new file mode 100644
index 0000000..086d1c9
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataBlockedFrame.java
@@ -0,0 +1,60 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.Immutable;
+
+/**
+ * A STREAM frame used to indicate that data has been blocked.
+ */
+@Immutable
+public interface StreamDataBlockedFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link StreamDataBlockedFrameBuilder} instance.
+   */
+  static StreamDataBlockedFrameBuilder builder() {
+    return new StreamDataBlockedFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.StreamDataBlocked;
+  }
+
+  /**
+   * Identifier of the stream this frame refers to.
+   *
+   * @return An {@link UnsignedLong} containing the stream id.
+   */
+  UnsignedLong streamId();
+
+  /**
+   * The total number of bytes the endpoint wants to send on this stream.
+   *
+   * @return An {@link UnsignedLong} containing the max offset.
+   */
+  UnsignedLong maxOffset();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataFrame.java
new file mode 100644
index 0000000..7d37e98
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataFrame.java
@@ -0,0 +1,72 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.immutables.value.Value;
+import org.interledger.core.Immutable;
+
+/**
+ * <p>Packets may be received out of order so the Offset is used to indicate the correct position of the byte segment
+ * in the overall stream. The first StreamData frame sent for a given stream MUST start with an Offset of zero.</p>
+ */
+@Immutable
+public interface StreamDataFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link StreamDataFrameBuilder} instance.
+   */
+  static StreamDataFrameBuilder builder() {
+    return new StreamDataFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.StreamData;
+  }
+
+  /**
+   * Identifier of the stream this frame refers to.
+   *
+   * @return An {@link UnsignedLong} containing the stream id for this frame.
+   */
+  UnsignedLong streamId();
+
+  /**
+   * Position of this data in the byte stream.
+   *
+   * @return An {@link UnsignedLong} containing the offset for this frame.
+   */
+  UnsignedLong offset();
+
+  /**
+   * Application data.
+   *
+   * @return A byte array with STREAM data.
+   */
+  @Value.Default
+  default byte[] data() {
+    return EMPTY_DATA;
+  }
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataMaxFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataMaxFrame.java
new file mode 100644
index 0000000..f904575
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamDataMaxFrame.java
@@ -0,0 +1,61 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.Immutable;
+
+/**
+ * <p>The amounts in this frame are denominated in the units of the endpoint sending the frame, so the other endpoint
+ * must use their calculated exchange rate to determine how much more they can send for this stream.</p>
+ */
+@Immutable
+public interface StreamDataMaxFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link StreamDataMaxFrameBuilder} instance.
+   */
+  static StreamDataMaxFrameBuilder builder() {
+    return new StreamDataMaxFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.StreamDataMax;
+  }
+
+  /**
+   * Identifier of the stream this frame refers to.
+   *
+   * @return An {@link UnsignedLong} containing the stream id for this frame.
+   */
+  UnsignedLong streamId();
+
+  /**
+   * The total number of bytes the endpoint is willing to receive on this stream.
+   *
+   * @return An {@link UnsignedLong} containing the max offset for this frame.
+   */
+  UnsignedLong maxOffset();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrame.java
new file mode 100644
index 0000000..d351be2
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrame.java
@@ -0,0 +1,38 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+/**
+ * Contains STREAM messages of various type. Implementations MUST ignore frames with unknown types. Future versions of
+ * STREAM may add new frame types.
+ */
+public interface StreamFrame {
+
+  byte[] EMPTY_DATA = new byte[0];
+
+  /**
+   * The type of this frame.
+   *
+   * @return The {@link StreamFrameType} for this Stream Frame.
+   */
+  StreamFrameType streamFrameType();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrameConstants.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrameConstants.java
new file mode 100644
index 0000000..5bcd15c
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrameConstants.java
@@ -0,0 +1,21 @@
+package org.interledger.stream.frames;
+
+public interface StreamFrameConstants {
+
+  short CONNECTION_CLOSE = (short) 0x01;
+  short CONNECTION_NEW_ADDRESS = (short) 0x02;
+  short CONNECTION_DATA_MAX = (short) 0x03;
+  short CONNECTION_DATA_BLOCKED = (short) 0x04;
+  short CONNECTION_MAX_STREAM_ID = (short) 0x05;
+  short CONNECTION_STREAM_ID_BLOCKED = (short) 0x06;
+  short CONNECTION_ASSET_DETAILS = (short) 0x07;
+
+  short STREAM_CLOSE = (short) 0x10;
+  short STREAM_MONEY = (short) 0x11;
+  short STREAM_MONEY_MAX = (short) 0x12;
+  short STREAM_MONEY_BLOCKED = (short) 0x13;
+  short STREAM_DATA = (short) 0x14;
+  short STREAM_DATA_MAX = (short) 0x15;
+  short STREAM_DATA_BLOCKED = (short) 0x16;
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrameType.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrameType.java
new file mode 100644
index 0000000..af581e0
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamFrameType.java
@@ -0,0 +1,113 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Bilateral Transfer Protocol Core Libs
+ * %%
+ * Copyright (C) 2017 - 2019 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import static java.lang.String.format;
+import static org.interledger.stream.frames.StreamFrameConstants.CONNECTION_ASSET_DETAILS;
+import static org.interledger.stream.frames.StreamFrameConstants.CONNECTION_CLOSE;
+import static org.interledger.stream.frames.StreamFrameConstants.CONNECTION_DATA_BLOCKED;
+import static org.interledger.stream.frames.StreamFrameConstants.CONNECTION_DATA_MAX;
+import static org.interledger.stream.frames.StreamFrameConstants.CONNECTION_MAX_STREAM_ID;
+import static org.interledger.stream.frames.StreamFrameConstants.CONNECTION_NEW_ADDRESS;
+import static org.interledger.stream.frames.StreamFrameConstants.CONNECTION_STREAM_ID_BLOCKED;
+import static org.interledger.stream.frames.StreamFrameConstants.STREAM_CLOSE;
+import static org.interledger.stream.frames.StreamFrameConstants.STREAM_DATA;
+import static org.interledger.stream.frames.StreamFrameConstants.STREAM_DATA_BLOCKED;
+import static org.interledger.stream.frames.StreamFrameConstants.STREAM_DATA_MAX;
+import static org.interledger.stream.frames.StreamFrameConstants.STREAM_MONEY;
+import static org.interledger.stream.frames.StreamFrameConstants.STREAM_MONEY_BLOCKED;
+import static org.interledger.stream.frames.StreamFrameConstants.STREAM_MONEY_MAX;
+
+import java.util.Objects;
+
+public enum StreamFrameType {
+
+  ConnectionClose(CONNECTION_CLOSE),
+  ConnectionNewAddress(CONNECTION_NEW_ADDRESS),
+  ConnectionDataMax(CONNECTION_DATA_MAX),
+  ConnectionDataBlocked(CONNECTION_DATA_BLOCKED),
+  ConnectionMaxStreamId(CONNECTION_MAX_STREAM_ID),
+  ConnectionStreamIdBlocked(CONNECTION_STREAM_ID_BLOCKED),
+  ConnectionAssetDetails(CONNECTION_ASSET_DETAILS),
+
+  StreamClose(STREAM_CLOSE),
+  StreamMoney(STREAM_MONEY),
+  StreamMoneyMax(STREAM_MONEY_MAX),
+  StreamMoneyBlocked(STREAM_MONEY_BLOCKED),
+  StreamData(STREAM_DATA),
+  StreamDataMax(STREAM_DATA_MAX),
+  StreamDataBlocked(STREAM_DATA_BLOCKED);
+
+  private final short code;
+
+  StreamFrameType(short code) {
+    this.code = Objects.requireNonNull(code);
+  }
+
+  /**
+   * Get a new {@link StreamFrameType} from the type.
+   *
+   * @param code the message type type.
+   *
+   * @return A new {@link StreamFrameType} from the provided type
+   */
+  public static StreamFrameType fromCode(short code) {
+
+    switch (code) {
+      case CONNECTION_CLOSE:
+        return StreamFrameType.ConnectionClose;
+      case CONNECTION_NEW_ADDRESS:
+        return StreamFrameType.ConnectionNewAddress;
+      case CONNECTION_DATA_MAX:
+        return StreamFrameType.ConnectionDataMax;
+      case CONNECTION_DATA_BLOCKED:
+        return StreamFrameType.ConnectionDataBlocked;
+      case CONNECTION_MAX_STREAM_ID:
+        return StreamFrameType.ConnectionMaxStreamId;
+      case CONNECTION_STREAM_ID_BLOCKED:
+        return StreamFrameType.ConnectionStreamIdBlocked;
+      case CONNECTION_ASSET_DETAILS:
+        return StreamFrameType.ConnectionAssetDetails;
+      case STREAM_CLOSE:
+        return StreamFrameType.StreamClose;
+      case STREAM_MONEY:
+        return StreamFrameType.StreamMoney;
+      case STREAM_MONEY_MAX:
+        return StreamFrameType.StreamMoneyMax;
+      case STREAM_MONEY_BLOCKED:
+        return StreamFrameType.StreamMoneyBlocked;
+      case STREAM_DATA:
+        return StreamFrameType.StreamData;
+      case STREAM_DATA_MAX:
+        return StreamFrameType.StreamDataMax;
+      case STREAM_DATA_BLOCKED:
+        return StreamFrameType.StreamDataBlocked;
+      default:
+        throw new IllegalArgumentException(format("Unknown StreamFrame Type: %s", code));
+    }
+
+  }
+
+  public short code() {
+    return this.code;
+  }
+}
+
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyBlockedFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyBlockedFrame.java
new file mode 100644
index 0000000..c329352
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyBlockedFrame.java
@@ -0,0 +1,75 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.immutables.value.Value;
+import org.interledger.core.Immutable;
+
+/**
+ * <p>Used to advertise to the other stream party that the frame sender has more money to send, but this would exceed
+ * the other endpoint's advertised value as found in latest {@link StreamMoneyMaxFrame} frame.</p>
+ *
+ * <p>The amounts in this frame are denominated in the units of the endpoint sending the frame, so the other endpoint
+ * must use their calculated exchange rate to determine how much more they can send for this stream.</p>
+ *
+ * <p>Note that this frame is primarily intended for debugging purposes.</p>
+ */
+@Immutable
+public interface StreamMoneyBlockedFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link StreamMoneyBlockedFrameBuilder} instance.
+   */
+  static StreamMoneyBlockedFrameBuilder builder() {
+    return new StreamMoneyBlockedFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.StreamMoneyBlocked;
+  }
+
+  /**
+   * Identifier of the stream this frame refers to.
+   */
+  UnsignedLong streamId();
+
+  /**
+   * Total amount, denominated in the units of the endpoint sending this frame, that the endpoint wants to send.
+   *
+   * @return An {@link UnsignedLong} with the max amount that should be sent.
+   */
+  UnsignedLong sendMax();
+
+  /**
+   * Total amount, denominated in the units of the endpoint sending this frame, that the endpoint has sent already.
+   *
+   * @return An {@link UnsignedLong} containing the total amount sent on this stream.
+   */
+  @Value.Default
+  default UnsignedLong totalSent() {
+    return UnsignedLong.ZERO;
+  }
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyFrame.java
new file mode 100644
index 0000000..ee04e43
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyFrame.java
@@ -0,0 +1,80 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.Immutable;
+
+/**
+ * <p>The amount of money that should go to each stream is calculated by dividing the number of shares for the given
+ * stream by the total number of shares in all of the StreamMoney frames in the packet.</p>
+ *
+ * <p>For example, if an ILP Prepare packet has an amount of 100 and three StreamMoney frames with 5, 15, and 30 shares
+ * for streams 2, 4, and 6, respectively, that would indicate that stream 2 should get 10 units, stream 4 gets 30 units,
+ * and stream 6 gets 60 units.</p>
+ *
+ * <p>If the Prepare amount is not divisible by the total number of shares, implementations SHOULD round the stream
+ * amounts down. The remainder SHOULD be allocated to the lowest-numbered open stream that has not reached its maximum
+ * receive amount.</p>
+ */
+@Immutable
+public interface StreamMoneyFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link StreamMoneyFrameBuilder} instance.
+   */
+  static StreamMoneyFrameBuilder builder() {
+    return new StreamMoneyFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.StreamMoney;
+  }
+
+  /**
+   * Identifier of the stream this frame refers to.
+   *
+   * @return A {@link UnsignedLong} with the stream id.
+   */
+  UnsignedLong streamId();
+
+  /**
+   * <p>Proportion of the ILP Prepare amount destined for the stream specified.</p>
+   *
+   * <p>The amount of money that should go to each stream is calculated by dividing the number of shares for the given
+   * stream by the total number of shares in all of the StreamMoney frames in the packet.</p>
+   *
+   * <p>For example, if an ILP Prepare packet has an amount of 100 and three StreamMoney frames with 5, 15, and 30
+   * shares for streams 2, 4, and 6, respectively, that would indicate that stream 2 should get 10 units, stream 4 gets
+   * 30 units, and stream 6 gets 60 units.</p>
+   *
+   * <p>If the Prepare amount is not divisible by the total number of shares, implementations SHOULD round the stream
+   * amounts down. The remainder SHOULD be allocated to the lowest-numbered open stream that has not reached its maximum
+   * receive amount.</p>
+   *
+   * @return An {@link UnsignedLong} with the number of shares.
+   */
+  UnsignedLong shares();
+
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyMaxFrame.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyMaxFrame.java
new file mode 100644
index 0000000..aee8b09
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/frames/StreamMoneyMaxFrame.java
@@ -0,0 +1,74 @@
+package org.interledger.stream.frames;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Core
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import com.google.common.primitives.UnsignedLong;
+import org.immutables.value.Value;
+import org.interledger.core.Immutable;
+
+/**
+ * <p>The amounts in this frame are denominated in the units of the endpoint sending the frame, so the other endpoint
+ * must use their calculated exchange rate to determine how much more they can send for this stream.</p>
+ */
+@Immutable
+public interface StreamMoneyMaxFrame extends StreamFrame {
+
+  /**
+   * Get the default builder.
+   *
+   * @return a {@link StreamMoneyMaxFrameBuilder} instance.
+   */
+  static StreamMoneyMaxFrameBuilder builder() {
+    return new StreamMoneyMaxFrameBuilder();
+  }
+
+  @Override
+  default StreamFrameType streamFrameType() {
+    return StreamFrameType.StreamMoneyMax;
+  }
+
+  /**
+   * Identifier of the stream this frame refers to.
+   *
+   * @return An {@link UnsignedLong} with stream id of this frame.
+   */
+  UnsignedLong streamId();
+
+  /**
+   * Total amount, denominated in the units of the endpoint sending this frame, that the endpoint is willing to receive
+   * on this stream.
+   *
+   * @return An {@link UnsignedLong} with receive max for this frame.
+   */
+  UnsignedLong receiveMax();
+
+  /**
+   * Total amount, denominated in the units of the endpoint sending this frame, that the endpoint has received thus
+   * far.
+   *
+   * @return An {@link UnsignedLong} with the total amount received.
+   */
+  @Value.Default
+  default UnsignedLong totalReceived() {
+    return UnsignedLong.ZERO;
+  }
+
+}


Test patch:
diff --git a/ilp-core/src/test/java/org/interledger/core/InterledgerConstantsTest.java b/ilp-core/src/test/java/org/interledger/core/InterledgerConstantsTest.java
new file mode 100644
index 0000000..e395cf1
--- /dev/null
+++ b/ilp-core/src/test/java/org/interledger/core/InterledgerConstantsTest.java
@@ -0,0 +1,14 @@
+package org.interledger.core;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class InterledgerConstantsTest {
+
+  @Test
+  public void allZerosJustLikeMyReportCard() {
+    assertThat(InterledgerConstants.ALL_ZEROS_FULFILLMENT).isEqualTo(InterledgerFulfillment.of(new byte[32]));
+    assertThat(InterledgerConstants.ALL_ZEROS_CONDITION).isEqualTo(InterledgerFulfillment.of(new byte[32]).getCondition());
+  }
+}
diff --git a/ilp-core/src/test/java/org/interledger/core/InterledgerPacketTypeTest.java b/ilp-core/src/test/java/org/interledger/core/InterledgerPacketTypeTest.java
new file mode 100644
index 0000000..399cae7
--- /dev/null
+++ b/ilp-core/src/test/java/org/interledger/core/InterledgerPacketTypeTest.java
@@ -0,0 +1,40 @@
+package org.interledger.core;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+/**
+ * Unit tests for {@link InterledgerPacketType}.
+ */
+public class InterledgerPacketTypeTest {
+
+    @Test
+    public void testInterledgerPacketType() {
+        short TEST_PREPARE_CODE = (short) 12;
+        final InterledgerPacketType preparePacketType = InterledgerPacketType.fromCode(TEST_PREPARE_CODE);
+
+        short TEST_FULFILL_CODE = (short) 13;
+        final InterledgerPacketType fulfillPacketType = InterledgerPacketType.fromCode(TEST_FULFILL_CODE);
+
+        short TEST_REJECT_CODE = (short) 14;
+        final InterledgerPacketType rejectPacketType = InterledgerPacketType.fromCode(TEST_REJECT_CODE);
+
+        assertThat(preparePacketType.getType()).isEqualTo(InterledgerPacketType.PREPARE.getType());
+        assertThat(fulfillPacketType.getType()).isEqualTo(InterledgerPacketType.FULFILL.getType());
+        assertThat(rejectPacketType.getType()).isEqualTo(InterledgerPacketType.REJECT.getType());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testIllegalInterledgerPacketType() {
+        short TEST_ILLEGAL_CODE = (short) -1;
+        try {
+            InterledgerPacketType.fromCode(TEST_ILLEGAL_CODE);
+            fail("Should have thrown an exception since illegal code was used");
+        } catch (Exception e) {
+            assertThat(e.getMessage()).isEqualTo("Unknown StreamPacketType: -1");
+            throw e;
+        }
+    }
+}
diff --git a/ilp-core/src/test/java/org/interledger/core/WrapperTest.java b/ilp-core/src/test/java/org/interledger/core/WrapperTest.java
new file mode 100644
index 0000000..a2506fa
--- /dev/null
+++ b/ilp-core/src/test/java/org/interledger/core/WrapperTest.java
@@ -0,0 +1,34 @@
+package org.interledger.core;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WrapperTest {
+
+  @Test
+  public void wrapThis() {
+    Wrapper<Integer> wrapper = new IntWrapper(42);
+
+    assertThat(wrapper).isEqualTo(wrapper);
+    assertThat(wrapper.equals(null)).isFalse();
+    assertThat(wrapper.equals(new IntWrapper(39))).isFalse();
+    assertThat(wrapper).isEqualTo(new IntWrapper(42));
+    assertThat(wrapper.hashCode()).isEqualTo(42);
+    assertThat(wrapper.toString()).isEqualTo("IntWrapper(42)");
+  }
+
+  private final class IntWrapper extends Wrapper<Integer> {
+
+    private int value;
+
+    private IntWrapper(int value) {
+      this.value = value;
+    }
+
+    @Override
+    public Integer value() {
+      return value;
+    }
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/SendMoneyResultTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/SendMoneyResultTest.java
new file mode 100644
index 0000000..ad93805
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/SendMoneyResultTest.java
@@ -0,0 +1,26 @@
+package org.interledger.stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.common.primitives.UnsignedLong;
+import org.junit.Test;
+
+import java.time.Duration;
+
+/**
+ * Unit tests for {@link SendMoneyResult}.
+ */
+public class SendMoneyResultTest {
+
+  @Test
+  public void totalPackets() {
+    SendMoneyResult result = SendMoneyResult.builder()
+        .originalAmount(UnsignedLong.ZERO)
+        .amountDelivered(UnsignedLong.ZERO)
+        .numFulfilledPackets(10)
+        .numRejectPackets(5)
+        .sendMoneyDuration(Duration.ZERO)
+        .build();
+    assertThat(result.totalPackets()).isEqualTo(15);
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/StreamPacketTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/StreamPacketTest.java
new file mode 100644
index 0000000..822bd0d
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/StreamPacketTest.java
@@ -0,0 +1,23 @@
+package org.interledger.stream;
+
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.InterledgerPacketType;
+import org.junit.Test;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.mockito.Mockito.spy;
+
+public class StreamPacketTest {
+
+  @Test
+  public void version1() {
+    StreamPacket packet = StreamPacket.builder()
+        .interledgerPacketType(InterledgerPacketType.PREPARE)
+        .sequence(UnsignedLong.ZERO)
+        .prepareAmount(UnsignedLong.ZERO)
+        .build();
+    assertThat(packet.version()).isEqualTo((short) 1);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamPacket.class).version()).isEqualTo((short) 1);
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/StreamUtilsTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/StreamUtilsTest.java
new file mode 100644
index 0000000..a0fdb9a
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/StreamUtilsTest.java
@@ -0,0 +1,96 @@
+package org.interledger.stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.fail;
+
+import org.interledger.core.InterledgerFulfillment;
+
+import com.google.common.primitives.UnsignedLong;
+import org.junit.Test;
+
+import java.security.SignatureException;
+
+/**
+ * Unit tests for {@link StreamUtils}.
+ *
+ * Note: Test values were taken from the Interledger-rs project, which mimic the Interledger-js test values.
+ *
+ * @see "https://github.com/interledger-rs/interledger-rs/blob/master/crates/interledger-stream/src/crypto.rs"
+ */
+public class StreamUtilsTest {
+
+  private static final byte[] SHARED_SECRET = new byte[] {
+      (byte) 126, (byte) 219, (byte) 117, (byte) 93, (byte) 118, (byte) 248, (byte) 249, (byte) 211, (byte) 20,
+      (byte) 211, (byte) 65, (byte) 110, (byte) 237, (byte) 80, (byte) 253, (byte) 179, (byte) 81, (byte) 146,
+      (byte) 229, (byte) 67, (byte) 231, (byte) 49, (byte) 92, (byte) 127, (byte) 254, (byte) 230, (byte) 144,
+      (byte) 102, (byte) 103, (byte) 166, (byte) 150, (byte) 36
+  };
+
+  private static final byte[] DATA = new byte[] {
+      (byte) 119, (byte) 248, (byte) 213, (byte) 234, (byte) 63, (byte) 200, (byte) 224, (byte) 140, (byte) 212,
+      (byte) 222, (byte) 105, (byte) 159, (byte) 246, (byte) 203, (byte) 66, (byte) 155, (byte) 151, (byte) 172,
+      (byte) 68, (byte) 24, (byte) 76, (byte) 232, (byte) 90, (byte) 10, (byte) 237, (byte) 146, (byte) 189, (byte) 73,
+      (byte) 248, (byte) 196, (byte) 177, (byte) 108, (byte) 115, (byte) 223
+  };
+
+  private static final byte[] FULFILLMENT = new byte[] {
+      (byte) 24, (byte) 6, (byte) 56, (byte) 73, (byte) 229, (byte) 236, (byte) 88, (byte) 227, (byte) 82, (byte) 112,
+      (byte) 152, (byte) 49, (byte) 152, (byte) 73, (byte) 182, (byte) 183, (byte) 198, (byte) 7, (byte) 233,
+      (byte) 124, (byte) 119, (byte) 65, (byte) 13, (byte) 68, (byte) 54, (byte) 108, (byte) 120, (byte) 193, (byte) 59,
+      (byte) 226, (byte) 107, (byte) 39
+  };
+
+  @Test
+  public void generatedFulfillableFulfillment() throws SignatureException {
+    InterledgerFulfillment fulfillment = StreamUtils.generatedFulfillableFulfillment(SHARED_SECRET, DATA);
+
+    assertThat(fulfillment.getPreimage(), is(FULFILLMENT));
+    assertThat(fulfillment, is(InterledgerFulfillment.of(FULFILLMENT)));
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void minWithNullFirst() {
+    try {
+      StreamUtils.min(null, UnsignedLong.ONE);
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e.getMessage(), is(nullValue()));
+      throw e;
+    }
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void minWithNullSecond() {
+    try {
+      StreamUtils.min(UnsignedLong.ONE, null);
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e.getMessage(), is(nullValue()));
+      throw e;
+    }
+  }
+
+  @Test
+  public void minTests() {
+    assertThat(StreamUtils.min(UnsignedLong.ZERO, UnsignedLong.ZERO), is(UnsignedLong.ZERO));
+    assertThat(StreamUtils.min(UnsignedLong.ONE, UnsignedLong.ZERO), is(UnsignedLong.ZERO));
+    assertThat(StreamUtils.min(UnsignedLong.ZERO, UnsignedLong.ONE), is(UnsignedLong.ZERO));
+    assertThat(StreamUtils.min(UnsignedLong.MAX_VALUE, UnsignedLong.ZERO), is(UnsignedLong.ZERO));
+    assertThat(StreamUtils.min(UnsignedLong.ZERO, UnsignedLong.MAX_VALUE), is(UnsignedLong.ZERO));
+    assertThat(StreamUtils.min(UnsignedLong.ONE, UnsignedLong.MAX_VALUE), is(UnsignedLong.ONE));
+    assertThat(StreamUtils.min(UnsignedLong.MAX_VALUE, UnsignedLong.ONE), is(UnsignedLong.ONE));
+  }
+
+  @Test
+  public void maxTests() {
+    assertThat(StreamUtils.max(UnsignedLong.ZERO, UnsignedLong.ZERO), is(UnsignedLong.ZERO));
+    assertThat(StreamUtils.max(UnsignedLong.ONE, UnsignedLong.ZERO), is(UnsignedLong.ONE));
+    assertThat(StreamUtils.max(UnsignedLong.ZERO, UnsignedLong.ONE), is(UnsignedLong.ONE));
+    assertThat(StreamUtils.max(UnsignedLong.MAX_VALUE, UnsignedLong.ZERO), is(UnsignedLong.MAX_VALUE));
+    assertThat(StreamUtils.max(UnsignedLong.ZERO, UnsignedLong.MAX_VALUE), is(UnsignedLong.MAX_VALUE));
+    assertThat(StreamUtils.max(UnsignedLong.ONE, UnsignedLong.MAX_VALUE), is(UnsignedLong.MAX_VALUE));
+    assertThat(StreamUtils.max(UnsignedLong.MAX_VALUE, UnsignedLong.ONE), is(UnsignedLong.MAX_VALUE));
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/crypto/JavaxStreamEncryptionServiceTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/crypto/JavaxStreamEncryptionServiceTest.java
new file mode 100644
index 0000000..9ce82af
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/crypto/JavaxStreamEncryptionServiceTest.java
@@ -0,0 +1,62 @@
+package org.interledger.stream.crypto;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link JavaxStreamEncryptionService}.
+ *
+ * Note: Test values were taken from the Interledger-rs project, which mimic the Interledger-js test values.
+ *
+ * @see "https://github.com/interledger-rs/interledger-rs/blob/master/crates/interledger-stream/src/crypto.rs"
+ */
+public class JavaxStreamEncryptionServiceTest {
+
+  private static final byte[] SHARED_SECRET = new byte[] {
+      (byte) 126, (byte) 219, (byte) 117, (byte) 93, (byte) 118, (byte) 248, (byte) 249, (byte) 211, (byte) 20,
+      (byte) 211, (byte) 65, 110, (byte) 237, (byte) 80, (byte) 253, (byte) 179, (byte) 81, (byte) 146, (byte) 229,
+      (byte) 67, (byte) 231, (byte) 49, (byte) 92, (byte) 127, (byte) 254, (byte) 230, (byte) 144, (byte) 102,
+      (byte) 103, (byte) 166, (byte) 150, (byte) 36
+  };
+
+  private static final byte[] PLAINTEXT = new byte[] {99, 0, 12, (byte) 255, 77, 31};
+
+  private static final byte[] CIPHERTEXT = new byte[] {
+      (byte) 119, (byte) 248, (byte) 213, (byte) 234, (byte) 63, (byte) 200, (byte) 224, (byte) 140, (byte) 212,
+      (byte) 222, (byte) 105, (byte) 159, (byte) 246, (byte) 203, (byte) 66, (byte) 155, (byte) 151, (byte) 172,
+      (byte) 68, (byte) 24, (byte) 76, (byte) 232, (byte) 90, (byte) 10, (byte) 237, (byte) 146, (byte) 189, (byte) 73,
+      (byte) 248, (byte) 196, (byte) 177, (byte) 108, (byte) 115, (byte) 223
+  };
+
+  private static final byte[] NONCE_IV = new byte[] {(byte) 119, (byte) 248, (byte) 213, (byte) 234, (byte) 63,
+      (byte) 200, (byte) 224, (byte) 140, (byte) 212, (byte) 222, (byte) 105, (byte) 159};
+
+  private JavaxStreamEncryptionService streamEncryptionService;
+
+  @Before
+  public void setUp() {
+    this.streamEncryptionService = new JavaxStreamEncryptionService();
+  }
+
+  @Test
+  public void testEncryptToSameRustJs() {
+    byte[] encryptedValue = streamEncryptionService.encryptWithIv(SHARED_SECRET, PLAINTEXT, NONCE_IV);
+    assertThat(encryptedValue, is(CIPHERTEXT));
+  }
+
+  @Test
+  public void testDecryptToSameAsRustJs() {
+    byte[] decryptedValue = streamEncryptionService.decrypt(SHARED_SECRET, CIPHERTEXT);
+    assertThat(decryptedValue, is(PLAINTEXT));
+  }
+
+  @Test
+  public void losslesslyEncryptAndDecrypts() {
+    byte[] cipherMessage = streamEncryptionService.encrypt(SHARED_SECRET, PLAINTEXT);
+    byte[] decryptedValue = streamEncryptionService.decrypt(SHARED_SECRET, cipherMessage);
+    assertThat(decryptedValue, is(PLAINTEXT));
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/ErrorCodeTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/ErrorCodeTest.java
new file mode 100644
index 0000000..686751f
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/ErrorCodeTest.java
@@ -0,0 +1,71 @@
+package org.interledger.stream.frames;
+
+import com.google.common.collect.Lists;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.interledger.stream.frames.ErrorCodeConstants.*;
+
+public class ErrorCodeTest {
+
+  private final List<Short> expected = Lists.newArrayList(
+    NO_ERROR,
+    INTERNAL_ERROR,
+    ENDPOINT_BUSY,
+    FLOW_CONTROL_ERROR,
+    STREAM_ID_ERROR,
+    STREAM_STATE_ERROR,
+    FRAME_FORMAT_ERROR,
+    PROTOCOL_VIOLATION,
+    APPLICATION_ERROR
+  );
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void of() {
+    assertThat(expected.stream().map(ErrorCode::of).collect(Collectors.toList()))
+        .isEqualTo(Arrays.asList(ErrorCode.values()));
+  }
+
+  @Test
+  public void ofThrows() {
+    expectedException.expect(RuntimeException.class);
+    ErrorCode.of((short) 0x17);
+  }
+
+  @Test
+  public void codeAndDescription() {
+    assertThat(ErrorCode.NoError.getCode()).isEqualTo((short) 0x01);
+    assertThat(ErrorCode.NoError.getDescription()).isEqualTo("The stream or connection closed normally.");
+    assertThat(ErrorCode.InternalError.getCode()).isEqualTo((short) 0x02);
+    assertThat(ErrorCode.InternalError.getDescription()).isEqualTo("The endpoint encountered an unexpected error.");
+    assertThat(ErrorCode.EndpointBusy.getCode()).isEqualTo((short) 0x03);
+    assertThat(ErrorCode.EndpointBusy.getDescription()).isEqualTo("The endpoint is temporarily overloaded and unable to process the packet.");
+    assertThat(ErrorCode.FlowControlError.getCode()).isEqualTo((short) 0x04);
+    assertThat(ErrorCode.FlowControlError.getDescription()).isEqualTo("The other endpoint exceeded the flow control limits advertised.");
+    assertThat(ErrorCode.StreamIdError.getCode()).isEqualTo((short) 0x05);
+    assertThat(ErrorCode.StreamIdError.getDescription()).isEqualTo("The other endpoint opened more streams than allowed.");
+    assertThat(ErrorCode.StreamStateError.getCode()).isEqualTo((short) 0x06);
+    assertThat(ErrorCode.StreamStateError.getDescription()).isEqualTo("The other endpoint sent frames for a stream that is already closed.");
+    assertThat(ErrorCode.FrameFormatError.getCode()).isEqualTo((short) 0x07);
+    assertThat(ErrorCode.FrameFormatError.getDescription()).isEqualTo("The other endpoint sent a frame with invalid syntax.");
+    assertThat(ErrorCode.ProtocolViolation.getCode()).isEqualTo((short) 0x08);
+    assertThat(ErrorCode.ProtocolViolation.getDescription()).isEqualTo("The other endpoint sent invalid data or otherwise violated the protocol.");
+    assertThat(ErrorCode.ApplicationError.getCode()).isEqualTo((short) 0x09);
+    assertThat(ErrorCode.ApplicationError.getDescription()).isEqualTo("The application using STREAM closed the stream or connection with an error.");
+  }
+
+  @Test
+  public void testToStringFormatting() {
+    assertThat(ErrorCode.NoError.toString()).isEqualTo("ErrorCode[code=1, description='The stream or connection closed normally.']");
+  }
+
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamCloseFrameTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamCloseFrameTest.java
new file mode 100644
index 0000000..a9472bb
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamCloseFrameTest.java
@@ -0,0 +1,20 @@
+package org.interledger.stream.frames;
+
+import com.google.common.primitives.UnsignedLong;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+
+public class StreamCloseFrameTest {
+
+  @Test
+  public void emptyMessage() {
+    StreamCloseFrame frame = StreamCloseFrame.builder()
+        .streamId(UnsignedLong.ZERO)
+        .errorCode(ErrorCode.NoError)
+        .build();
+    assertThat(frame.errorMessage()).isEmpty();
+    assertThat(spy(StreamCloseFrame.class).errorMessage()).isEmpty();
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamDataFrameTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamDataFrameTest.java
new file mode 100644
index 0000000..18adb65
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamDataFrameTest.java
@@ -0,0 +1,21 @@
+package org.interledger.stream.frames;
+
+import com.google.common.primitives.UnsignedLong;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+
+public class StreamDataFrameTest {
+
+  @Test
+  public void emptyData() {
+    StreamDataFrame frame = StreamDataFrame.builder()
+        .offset(UnsignedLong.ZERO)
+        .streamId(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.data()).isEqualTo(new byte[0]);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamDataFrame.class).data()).isEqualTo(new byte[0]);
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamFrameTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamFrameTest.java
new file mode 100644
index 0000000..fa44e9e
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamFrameTest.java
@@ -0,0 +1,13 @@
+package org.interledger.stream.frames;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class StreamFrameTest {
+
+  @Test
+  public void empty() {
+    assertThat(StreamFrame.EMPTY_DATA).isEqualTo(new byte[0]);
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamFrameTypeTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamFrameTypeTest.java
new file mode 100644
index 0000000..fe96900
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamFrameTypeTest.java
@@ -0,0 +1,231 @@
+package org.interledger.stream.frames;
+
+import com.google.common.collect.Lists;
+import com.google.common.primitives.UnsignedLong;
+import org.interledger.core.InterledgerAddress;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.interledger.stream.frames.StreamFrameConstants.*;
+import static org.mockito.Mockito.spy;
+
+
+public class StreamFrameTypeTest {
+
+  private final List<Short> expected = Lists.newArrayList(
+    CONNECTION_CLOSE,
+    CONNECTION_NEW_ADDRESS,
+    CONNECTION_DATA_MAX,
+    CONNECTION_DATA_BLOCKED,
+    CONNECTION_MAX_STREAM_ID,
+    CONNECTION_STREAM_ID_BLOCKED,
+    CONNECTION_ASSET_DETAILS,
+    STREAM_CLOSE,
+    STREAM_MONEY,
+    STREAM_MONEY_MAX,
+    STREAM_MONEY_BLOCKED,
+    STREAM_DATA,
+    STREAM_DATA_MAX,
+    STREAM_DATA_BLOCKED
+  );
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void fromCode() {
+    assertThat(expected.stream().map(StreamFrameType::fromCode).collect(Collectors.toList()))
+        .isEqualTo(Arrays.asList(StreamFrameType.values()));
+  }
+
+  @Test
+  public void getCode() {
+    assertThat(Arrays.stream(StreamFrameType.values()).map(StreamFrameType::code).collect(Collectors.toList())).isEqualTo(expected);
+  }
+
+  @Test
+  public void fromCodeGetSad() {
+    expectedException.expect(IllegalArgumentException.class);
+    StreamFrameType.fromCode((short) 0x17);
+  }
+
+  @Test
+  public void enforceHexValues() {
+    assertThat(expected).containsExactly(
+        (short) 0x01,
+        (short) 0x02,
+        (short) 0x03,
+        (short) 0x04,
+        (short) 0x05,
+        (short) 0x06,
+        (short) 0x07,
+        (short) 0x10,
+        (short) 0x11,
+        (short) 0x12,
+        (short) 0x13,
+        (short) 0x14,
+        (short) 0x15,
+        (short) 0x16
+    );
+  }
+
+  @Test
+  public void connectionAssetDetailsFrame() {
+    ConnectionAssetDetailsFrame frame = ConnectionAssetDetailsFrame.builder()
+        .sourceAssetCode("dave and busters dollars")
+        .sourceAssetScale((short) 1)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.ConnectionAssetDetails);
+    // make sure interface default method is exercised
+    assertThat(spy(ConnectionAssetDetailsFrame.class).streamFrameType()).isEqualTo(StreamFrameType.ConnectionAssetDetails);
+  }
+
+  @Test
+  public void connectionCloseFrame() {
+    ConnectionCloseFrame frame = ConnectionCloseFrame.builder()
+        .errorMessage("too many cooks!")
+        .errorCode(ErrorCode.ApplicationError)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.ConnectionClose);
+    // make sure interface default method is exercised
+    assertThat(spy(ConnectionCloseFrame.class).streamFrameType()).isEqualTo(StreamFrameType.ConnectionClose);
+  }
+
+  @Test
+  public void connectionDataBlockedFrame() {
+    ConnectionDataBlockedFrame frame = ConnectionDataBlockedFrame.builder()
+        .maxOffset(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.ConnectionDataBlocked);
+    // make sure interface default method is exercised
+    assertThat(spy(ConnectionDataBlockedFrame.class).streamFrameType()).isEqualTo(StreamFrameType.ConnectionDataBlocked);
+  }
+
+  @Test
+  public void connectionDataMaxFrame() {
+    ConnectionDataMaxFrame frame = ConnectionDataMaxFrame.builder()
+        .maxOffset(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.ConnectionDataMax);
+    // make sure interface default method is exercised
+    assertThat(spy(ConnectionDataMaxFrame.class).streamFrameType()).isEqualTo(StreamFrameType.ConnectionDataMax);
+  }
+
+  @Test
+  public void connectionMaxStreamIdFrame() {
+    ConnectionMaxStreamIdFrame frame = ConnectionMaxStreamIdFrame.builder()
+        .maxStreamId(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.ConnectionMaxStreamId);
+    // make sure interface default method is exercised
+    assertThat(spy(ConnectionMaxStreamIdFrame.class).streamFrameType()).isEqualTo(StreamFrameType.ConnectionMaxStreamId);
+  }
+
+  @Test
+  public void connectionNewAddressFrame() {
+    ConnectionNewAddressFrame frame = ConnectionNewAddressFrame.builder()
+        .sourceAddress(InterledgerAddress.of("g.shenanigans"))
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.ConnectionNewAddress);
+    // make sure interface default method is exercised
+    assertThat(spy(ConnectionNewAddressFrame.class).streamFrameType()).isEqualTo(StreamFrameType.ConnectionNewAddress);
+  }
+
+  @Test
+  public void connectionStreamIdBlockedFrame() {
+    ConnectionStreamIdBlockedFrame frame = ConnectionStreamIdBlockedFrame.builder()
+        .maxStreamId(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.ConnectionStreamIdBlocked);
+    // make sure interface default method is exercised
+    assertThat(spy(ConnectionStreamIdBlockedFrame.class).streamFrameType()).isEqualTo(StreamFrameType.ConnectionStreamIdBlocked);
+  }
+
+  @Test
+  public void streamCloseFrame() {
+    StreamCloseFrame frame = StreamCloseFrame.builder()
+        .streamId(UnsignedLong.ZERO)
+        .errorMessage("i'm all out of love")
+        .errorCode(ErrorCode.ApplicationError)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.StreamClose);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamCloseFrame.class).streamFrameType()).isEqualTo(StreamFrameType.StreamClose);
+  }
+
+  @Test
+  public void streamDataBlockedFrame() {
+    StreamDataBlockedFrame frame = StreamDataBlockedFrame.builder()
+        .maxOffset(UnsignedLong.ZERO)
+        .streamId(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.StreamDataBlocked);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamDataBlockedFrame.class).streamFrameType()).isEqualTo(StreamFrameType.StreamDataBlocked);
+  }
+
+  @Test
+  public void streamDataFrame() {
+    StreamDataFrame frame = StreamDataFrame.builder()
+        .streamId(UnsignedLong.ZERO)
+        .offset(UnsignedLong.ZERO)
+        .data("ah man i lost the recipe for ice again".getBytes())
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.StreamData);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamDataFrame.class).streamFrameType()).isEqualTo(StreamFrameType.StreamData);
+  }
+
+  @Test
+  public void streamDataMaxFrame() {
+    StreamDataMaxFrame frame = StreamDataMaxFrame.builder()
+        .streamId(UnsignedLong.ZERO)
+        .maxOffset(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.StreamDataMax);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamDataMaxFrame.class).streamFrameType()).isEqualTo(StreamFrameType.StreamDataMax);
+  }
+
+  @Test
+  public void streamMoneyBlockedFrame() {
+    StreamMoneyBlockedFrame frame = StreamMoneyBlockedFrame.builder()
+        .streamId(UnsignedLong.ZERO)
+        .sendMax(UnsignedLong.ZERO)
+        .totalSent(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.StreamMoneyBlocked);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamMoneyBlockedFrame.class).streamFrameType()).isEqualTo(StreamFrameType.StreamMoneyBlocked);
+  }
+
+  @Test
+  public void streamMoneyFrame() {
+    StreamMoneyFrame frame = StreamMoneyFrame.builder()
+        .streamId(UnsignedLong.ZERO)
+        .shares(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.StreamMoney);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamMoneyFrame.class).streamFrameType()).isEqualTo(StreamFrameType.StreamMoney);
+  }
+
+  @Test
+  public void streamMoneyMaxFrame() {
+    StreamMoneyMaxFrame frame = StreamMoneyMaxFrame.builder()
+        .totalReceived(UnsignedLong.ZERO)
+        .streamId(UnsignedLong.ZERO)
+        .receiveMax(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.streamFrameType()).isEqualTo(StreamFrameType.StreamMoneyMax);
+    // make sure interface default method is exercised
+    assertThat(spy(StreamMoneyMaxFrame.class).streamFrameType()).isEqualTo(StreamFrameType.StreamMoneyMax);
+
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamMoneyBlockedFrameTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamMoneyBlockedFrameTest.java
new file mode 100644
index 0000000..16b75b4
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamMoneyBlockedFrameTest.java
@@ -0,0 +1,20 @@
+package org.interledger.stream.frames;
+
+import com.google.common.primitives.UnsignedLong;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+
+public class StreamMoneyBlockedFrameTest {
+
+  @Test
+  public void zero() {
+    StreamMoneyBlockedFrame frame = StreamMoneyBlockedFrame.builder()
+        .sendMax(UnsignedLong.ZERO)
+        .streamId(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.totalSent()).isEqualTo(UnsignedLong.ZERO);
+    assertThat(spy(StreamMoneyBlockedFrame.class).totalSent()).isEqualTo(UnsignedLong.ZERO);
+  }
+}
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamMoneyMaxFrameTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamMoneyMaxFrameTest.java
new file mode 100644
index 0000000..47f2e06
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/frames/StreamMoneyMaxFrameTest.java
@@ -0,0 +1,20 @@
+package org.interledger.stream.frames;
+
+import com.google.common.primitives.UnsignedLong;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+
+public class StreamMoneyMaxFrameTest {
+
+  @Test
+  public void zero() {
+    StreamMoneyMaxFrame frame = StreamMoneyMaxFrame.builder()
+        .receiveMax(UnsignedLong.ZERO)
+        .streamId(UnsignedLong.ZERO)
+        .build();
+    assertThat(frame.totalReceived()).isEqualTo(UnsignedLong.ZERO);
+    assertThat(spy(StreamMoneyMaxFrame.class).totalReceived()).isEqualTo(UnsignedLong.ZERO);
+  }
+}