org.interledger
stream-core
diff --git a/examples-parent/ilp-emitters/src/main/java/org/interledger/examples/packets/IlpPacketEmitter.java b/examples-parent/ilp-emitters/src/main/java/org/interledger/examples/packets/IlpPacketEmitter.java
index 8ca81b56..2bf1b80f 100644
--- a/examples-parent/ilp-emitters/src/main/java/org/interledger/examples/packets/IlpPacketEmitter.java
+++ b/examples-parent/ilp-emitters/src/main/java/org/interledger/examples/packets/IlpPacketEmitter.java
@@ -16,6 +16,7 @@
import org.interledger.core.InterledgerPreparePacketBuilder;
import org.interledger.core.InterledgerRejectPacket;
import org.interledger.core.SharedSecret;
+import org.interledger.link.PingLoopbackLink;
import org.interledger.stream.Denomination;
import org.interledger.stream.StreamPacket;
import org.interledger.stream.crypto.JavaxStreamEncryptionService;
@@ -50,9 +51,11 @@ public class IlpPacketEmitter {
private static final Logger LOGGER = LoggerFactory.getLogger(IlpPacketEmitter.class);
+ private static final InterledgerAddress PING_DESTINATION_ADDRESS = InterledgerAddress.of("test.connie");
+
private static final InterledgerAddress DESTINATION_ADDRESS = InterledgerAddress
- .of("example.connie.bob.QeJvQtFp7eRiNhnoAg9PkusR");
- private static final InterledgerAddress OPERATOR_ADDRESS = InterledgerAddress.of("example.connie");
+ .of("test.connie.bob.QeJvQtFp7eRiNhnoAg9PkusR");
+ private static final InterledgerAddress OPERATOR_ADDRESS = InterledgerAddress.of("test.connie");
private static final SharedSecret SHARED_SECRET = SharedSecret
.of(Base64.getDecoder().decode("nHYRcu5KM5pyw8XehssZtvhEgCgkKP4Do5kJUpk84G4"));
@@ -62,6 +65,7 @@ public class IlpPacketEmitter {
public static void main(String[] args) throws IOException {
emitPacketsWithNoData();
emitPacketsWithStreamPayloads();
+ emitUnidrectionalPingPacket();
}
private static void emitPacketsWithNoData() {
@@ -81,6 +85,18 @@ private static void emitPacketsWithNoData() {
emitPacketToFile("/tmp/testFulfillPacket.bin", fulfillPacket);
}
+ private static void emitUnidrectionalPingPacket() {
+
+ final InterledgerPreparePacket preparePacket = InterledgerPreparePacket.builder()
+ .destination(PING_DESTINATION_ADDRESS)
+ .expiresAt(Instant.now().plus(365, ChronoUnit.DAYS))
+ .executionCondition(PingLoopbackLink.PING_PROTOCOL_CONDITION)
+ .amount(UnsignedLong.ONE)
+ .build();
+
+ emitPacketToFile("/tmp/testUnidirectionalPingPacket.bin", preparePacket);
+ }
+
private static void emitPacketsWithStreamPayloads() throws IOException {
final InterledgerPreparePacket preparePacketWithStreamFrames = preparePacketWithStreamFrames().build();
emitPacketToFile("/tmp/testPreparePacketWithStreamFrames.bin", preparePacketWithStreamFrames);
diff --git a/link-parent/link-stateless-spsp-receiver/pom.xml b/link-parent/link-stateless-spsp-receiver/pom.xml
new file mode 100644
index 00000000..9217fba8
--- /dev/null
+++ b/link-parent/link-stateless-spsp-receiver/pom.xml
@@ -0,0 +1,80 @@
+
+
+
+ org.interledger
+ link-parent
+ HEAD-SNAPSHOT
+
+ 4.0.0
+
+ Quilt :: Link :: Stateless SPSP Receiver
+ link-stateless-spsp-receiver
+ A Link implementation for a stateless SPSP receiver.
+ jar
+
+
+
+ ${project.groupId}
+ codecs-framework
+
+
+ ${project.groupId}
+ ilp-core
+
+
+ ${project.groupId}
+ link-core
+
+
+ ${project.groupId}
+ stream-receiver
+
+
+ com.auth0
+ java-jwt
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.google.guava
+ guava
+
+
+ com.squareup.okhttp3
+ okhttp
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.immutables
+ value
+ provided
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.zalando
+ problem
+
+
+
+
diff --git a/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLink.java b/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLink.java
new file mode 100644
index 00000000..d2f8a3b4
--- /dev/null
+++ b/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLink.java
@@ -0,0 +1,78 @@
+package org.interledger.link.spsp;
+
+import org.interledger.core.InterledgerAddress;
+import org.interledger.core.InterledgerPreparePacket;
+import org.interledger.core.InterledgerResponsePacket;
+import org.interledger.link.AbstractLink;
+import org.interledger.link.Link;
+import org.interledger.link.LinkHandler;
+import org.interledger.link.LinkType;
+import org.interledger.link.exceptions.LinkHandlerAlreadyRegisteredException;
+import org.interledger.stream.Denomination;
+import org.interledger.stream.receiver.StreamReceiver;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * A {@link Link} that attempts to fulfill packets using an SPSP receiver.
+ */
+public class StatelessSpspReceiverLink extends AbstractLink
+ implements Link {
+
+ public static final String LINK_TYPE_STRING = "STATELESS_SPSP_RECEIVER";
+ public static final LinkType LINK_TYPE = LinkType.of(LINK_TYPE_STRING);
+
+ private final StreamReceiver streamReceiver;
+ private final Denomination denomination;
+
+ /**
+ * Required-Args Constructor.
+ *
+ * @param operatorAddressSupplier A supplier for the ILP address of this node operating this Link. This value may be
+ * uninitialized, for example, in cases where the Link obtains its address from a
+ * parent node using IL-DCP. If an ILP address has not been assigned, or it has not
+ * been obtained via IL-DCP, then this value will by default be {@link Link#SELF}.
+ * @param linkSettings A {@link StatelessSpspReceiverLinkSettings} for this Link.
+ * @param streamReceiver A {@link StreamReceiver} that can fulfill packets.
+ */
+ public StatelessSpspReceiverLink(
+ final Supplier operatorAddressSupplier,
+ final StatelessSpspReceiverLinkSettings linkSettings,
+ final StreamReceiver streamReceiver
+ ) {
+ super(operatorAddressSupplier, linkSettings);
+ this.denomination = Denomination.builder()
+ .assetCode(linkSettings.assetCode())
+ .assetScale((short) linkSettings.assetScale())
+ .build();
+ this.streamReceiver = Objects.requireNonNull(streamReceiver);
+ }
+
+ @Override
+ public void registerLinkHandler(final LinkHandler ilpDataHandler) throws LinkHandlerAlreadyRegisteredException {
+ throw new RuntimeException(
+ "StatelessSpspReceiver links never emit data, and thus should not have a registered DataHandler."
+ );
+ }
+
+ @Override
+ public InterledgerResponsePacket sendPacket(final InterledgerPreparePacket preparePacket) {
+ Objects.requireNonNull(preparePacket, "preparePacket must not be null");
+
+ return streamReceiver.receiveMoney(preparePacket, this.getOperatorAddressSupplier().get(), this.denomination)
+ .map(fulfillPacket -> {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Packet fulfilled! preparePacket={} fulfillPacket={}", preparePacket, fulfillPacket);
+ }
+ return fulfillPacket;
+ },
+ rejectPacket -> {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Packet rejected! preparePacket={} rejectPacket={}", preparePacket, rejectPacket);
+ }
+ return rejectPacket;
+ }
+ );
+ }
+}
diff --git a/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLinkFactory.java b/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLinkFactory.java
new file mode 100644
index 00000000..8e47402e
--- /dev/null
+++ b/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLinkFactory.java
@@ -0,0 +1,70 @@
+package org.interledger.link.spsp;
+
+import org.interledger.core.InterledgerAddress;
+import org.interledger.link.Link;
+import org.interledger.link.LinkFactory;
+import org.interledger.link.LinkId;
+import org.interledger.link.LinkSettings;
+import org.interledger.link.LinkType;
+import org.interledger.link.PacketRejector;
+import org.interledger.link.exceptions.LinkException;
+import org.interledger.stream.receiver.StatelessStreamReceiver;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * An implementation of {@link LinkFactory} for constructing instances of {@link StatelessSpspReceiverLink}.
+ */
+public class StatelessSpspReceiverLinkFactory implements LinkFactory {
+
+ private final PacketRejector packetRejector;
+ private final StatelessStreamReceiver statelessStreamReceiver;
+
+ /**
+ * Required-args Constructor.
+ *
+ * @param packetRejector An instance of {@link PacketRejector}.
+ * @param statelessStreamReceiver A {@link StatelessStreamReceiver} for encrypting/decrypting STREAM packets.
+ */
+ public StatelessSpspReceiverLinkFactory(
+ final PacketRejector packetRejector, final StatelessStreamReceiver statelessStreamReceiver
+ ) {
+ this.packetRejector = Objects.requireNonNull(packetRejector, "packetRejector must not be null");
+ this.statelessStreamReceiver = Objects
+ .requireNonNull(statelessStreamReceiver, "statelessStreamReceiver must not be null");
+ }
+
+
+ @Override
+ public Link> constructLink(
+ final Supplier operatorAddressSupplier, final LinkSettings linkSettings
+ ) {
+ Objects.requireNonNull(operatorAddressSupplier, "operatorAddressSupplier must not be null");
+ Objects.requireNonNull(linkSettings, "linkSettings must not be null");
+
+ if (!this.supports(linkSettings.getLinkType())) {
+ throw new LinkException(
+ String.format("LinkType not supported by this factory. linkType=%s", linkSettings.getLinkType()),
+ LinkId.of("n/a")
+ );
+ }
+
+ Preconditions.checkArgument(
+ StatelessSpspReceiverLinkSettings.class.isAssignableFrom(linkSettings.getClass()),
+ "Constructing an instance of StatelessSpspReceiverLink requires an instance of StatelessSpspReceiverLinkSettings"
+ );
+
+ return new StatelessSpspReceiverLink(
+ operatorAddressSupplier, (StatelessSpspReceiverLinkSettings) linkSettings, statelessStreamReceiver
+ );
+ }
+
+ @Override
+ public boolean supports(LinkType linkType) {
+ return StatelessSpspReceiverLink.LINK_TYPE.equals(linkType);
+ }
+
+}
diff --git a/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLinkSettings.java b/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLinkSettings.java
new file mode 100644
index 00000000..8a96c1ef
--- /dev/null
+++ b/link-parent/link-stateless-spsp-receiver/src/main/java/org/interledger/link/spsp/StatelessSpspReceiverLinkSettings.java
@@ -0,0 +1,50 @@
+package org.interledger.link.spsp;
+
+import org.interledger.link.LinkSettings;
+import org.interledger.link.LinkType;
+
+import org.immutables.value.Value;
+import org.immutables.value.Value.Derived;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * An extension of {@link LinkSettings} for Stateless SPSP receiver links.
+ */
+public interface StatelessSpspReceiverLinkSettings extends LinkSettings {
+
+ static ImmutableStatelessSpspReceiverLinkSettings.Builder builder() {
+ return ImmutableStatelessSpspReceiverLinkSettings.builder();
+ }
+
+ @Override
+ default LinkType getLinkType() {
+ return StatelessSpspReceiverLink.LINK_TYPE;
+ }
+
+ /**
+ * Currency code or other asset identifier that will be used to select the correct rate for this account.
+ */
+ String assetCode();
+
+ /**
+ * Interledger amounts are integers, but most currencies are typically represented as # fractional units, e.g. cents.
+ * This property defines how many Interledger units make # up one regular unit. For dollars, this would usually be set
+ * to 9, so that Interledger # amounts are expressed in nano-dollars.
+ *
+ * @return an int representing this account's asset scale.
+ */
+ int assetScale();
+
+ @Value.Immutable
+ abstract class AbstractStatelessSpspReceiverLinkSettings implements StatelessSpspReceiverLinkSettings {
+
+ @Derived
+ @Override
+ public LinkType getLinkType() {
+ return StatelessSpspReceiverLink.LINK_TYPE;
+ }
+
+ }
+}
diff --git a/link-parent/link-stateless-spsp-receiver/src/test/java/org/interledger/link/spsp/StatelessSpspReceiverLinkFactoryTest.java b/link-parent/link-stateless-spsp-receiver/src/test/java/org/interledger/link/spsp/StatelessSpspReceiverLinkFactoryTest.java
new file mode 100644
index 00000000..9c27eb73
--- /dev/null
+++ b/link-parent/link-stateless-spsp-receiver/src/test/java/org/interledger/link/spsp/StatelessSpspReceiverLinkFactoryTest.java
@@ -0,0 +1,112 @@
+package org.interledger.link.spsp;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.interledger.core.InterledgerAddress;
+import org.interledger.link.LinkId;
+import org.interledger.link.LinkSettings;
+import org.interledger.link.LinkType;
+import org.interledger.link.PacketRejector;
+import org.interledger.link.exceptions.LinkException;
+import org.interledger.stream.receiver.StatelessStreamReceiver;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link StatelessSpspReceiverLinkFactory}.
+ */
+public class StatelessSpspReceiverLinkFactoryTest {
+
+ private static final InterledgerAddress OPERATOR_ADDRESS = InterledgerAddress.of("test.operator");
+ private final LinkId linkId = LinkId.of("foo");
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Mock
+ private LinkSettings linkSettingsMock;
+
+ @Mock
+ private PacketRejector packetRejectorMock;
+
+ @Mock
+ private StatelessStreamReceiver statelessStreamReceiverMock;
+
+ private StatelessSpspReceiverLinkFactory statelessSpspReceiverLinkFactory;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ this.statelessSpspReceiverLinkFactory = new StatelessSpspReceiverLinkFactory(
+ packetRejectorMock, statelessStreamReceiverMock
+ );
+ }
+
+ @Test
+ public void constructWithNulPacketRejector() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("packetRejector must not be null");
+
+ new StatelessSpspReceiverLinkFactory(null, statelessStreamReceiverMock);
+ }
+
+ @Test
+ public void constructWithNulStatelessStreamReceiver() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("statelessStreamReceiver must not be null");
+
+ new StatelessSpspReceiverLinkFactory(packetRejectorMock, null);
+ }
+
+ @Test
+ public void supports() {
+ assertThat(statelessSpspReceiverLinkFactory.supports(StatelessSpspReceiverLink.LINK_TYPE)).isEqualTo(true);
+ assertThat(statelessSpspReceiverLinkFactory.supports(LinkType.of("foo"))).isEqualTo(false);
+ }
+
+ @Test
+ public void constructLinkWithNullOperator() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("operatorAddressSupplier must not be null");
+
+ statelessSpspReceiverLinkFactory.constructLink(null, linkSettingsMock);
+ }
+
+ @Test
+ public void constructLinkWithNullLinkSettings() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("linkSettings must not be null");
+
+ statelessSpspReceiverLinkFactory.constructLink(() -> OPERATOR_ADDRESS, null);
+ }
+
+ @Test
+ public void constructLinkWithUnsupportedLinkType() {
+ expectedException.expect(LinkException.class);
+ expectedException.expectMessage("LinkType not supported by this factory. linkType=LinkType(FOO)");
+
+ LinkSettings linkSettings = LinkSettings.builder()
+ .linkType(LinkType.of("foo"))
+ .build();
+ statelessSpspReceiverLinkFactory.constructLink(() -> OPERATOR_ADDRESS, linkSettings);
+ }
+
+ @Test
+ public void constructLink() {
+ LinkSettings linkSettings = StatelessSpspReceiverLinkSettings.builder()
+ .assetCode("USD")
+ .assetScale((short) 9)
+ .build();
+ StatelessSpspReceiverLink link = (StatelessSpspReceiverLink) statelessSpspReceiverLinkFactory
+ .constructLink(() -> OPERATOR_ADDRESS, linkSettings);
+
+ assertThat(link.getLinkSettings().assetCode()).isEqualTo("USD");
+ assertThat(link.getLinkSettings().assetScale()).isEqualTo(9);
+ assertThat(link.getOperatorAddressSupplier().get()).isEqualTo(OPERATOR_ADDRESS);
+ }
+}
diff --git a/link-parent/link-stateless-spsp-receiver/src/test/java/org/interledger/link/spsp/StatelessSpspReceiverLinkTest.java b/link-parent/link-stateless-spsp-receiver/src/test/java/org/interledger/link/spsp/StatelessSpspReceiverLinkTest.java
new file mode 100644
index 00000000..ffc90062
--- /dev/null
+++ b/link-parent/link-stateless-spsp-receiver/src/test/java/org/interledger/link/spsp/StatelessSpspReceiverLinkTest.java
@@ -0,0 +1,105 @@
+package org.interledger.link.spsp;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.interledger.core.InterledgerConstants.ALL_ZEROS_FULFILLMENT;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import org.interledger.core.DateUtils;
+import org.interledger.core.InterledgerAddress;
+import org.interledger.core.InterledgerConstants;
+import org.interledger.core.InterledgerFulfillPacket;
+import org.interledger.core.InterledgerPreparePacket;
+import org.interledger.link.LinkId;
+import org.interledger.stream.receiver.StreamReceiver;
+
+import com.google.common.primitives.UnsignedLong;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Unit tests for {@link StatelessSpspReceiverLink}.
+ */
+public class StatelessSpspReceiverLinkTest {
+
+ private static final InterledgerAddress OPERATOR_ADDRESS = InterledgerAddress.of("test.foo");
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Mock
+ private StreamReceiver streamReceiverMock;
+
+ private StatelessSpspReceiverLink link;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ this.link = new StatelessSpspReceiverLink(
+ () -> OPERATOR_ADDRESS,
+ StatelessSpspReceiverLinkSettings.builder().assetCode("XRP").assetScale((short) 9).build(),
+ streamReceiverMock
+ );
+ link.setLinkId(LinkId.of("foo"));
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void registerLinkHandler() {
+ try {
+ link.registerLinkHandler(incomingPreparePacket -> null);
+ } catch (Exception e) {
+ assertThat(e.getMessage())
+ .isEqualTo("StatelessSpspReceiver links never emit data, and thus should not have a registered DataHandler.");
+ throw e;
+ }
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void sendPacketWithNull() {
+ try {
+ link.sendPacket(null);
+ } catch (NullPointerException e) {
+ assertThat(e.getMessage()).isEqualTo("preparePacket must not be null");
+ throw e;
+ }
+ }
+
+ @Test
+ public void sendPacket() {
+ final InterledgerFulfillPacket actualFulfillPacket = InterledgerFulfillPacket.builder()
+ .fulfillment(ALL_ZEROS_FULFILLMENT)
+ .build();
+ when(streamReceiverMock.receiveMoney(any(), any(), any())).thenReturn(actualFulfillPacket);
+
+ final InterledgerPreparePacket preparePacket = preparePacket();
+ link.sendPacket(preparePacket).handle(
+ fulfillPacket -> {
+ assertThat(fulfillPacket).isEqualTo(actualFulfillPacket);
+ },
+ rejectPacket -> {
+ logger.error("rejectPacket={}", rejectPacket);
+ fail("Expected a Fulfill");
+ }
+ );
+ }
+
+ private InterledgerPreparePacket preparePacket() {
+ return InterledgerPreparePacket.builder()
+ .amount(UnsignedLong.valueOf(10L))
+ .executionCondition(InterledgerConstants.ALL_ZEROS_CONDITION)
+ .destination(OPERATOR_ADDRESS)
+ .expiresAt(DateUtils.now())
+ .data(new byte[32])
+ .build();
+ }
+}
diff --git a/link-parent/pom.xml b/link-parent/pom.xml
index 649f4fc0..c747c85c 100644
--- a/link-parent/pom.xml
+++ b/link-parent/pom.xml
@@ -16,6 +16,7 @@
link-core
link-ilp-over-http
+ link-stateless-spsp-receiver
diff --git a/quilt-bom/pom.xml b/quilt-bom/pom.xml
index 93776ddd..817f3645 100644
--- a/quilt-bom/pom.xml
+++ b/quilt-bom/pom.xml
@@ -71,6 +71,11 @@
link-ilp-over-http
${project.version}
+