diff --git a/README.md b/README.md index fcad7708..d2a1a36d 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Modules in this library can be included in your Maven project by adding the Mave org.interledger ilp-core - 1.0.2 + 1.0.3 ... @@ -120,7 +120,7 @@ Modules in this library can be included in your Gradle project by adding the Mav ``` dependencies { ... - compile group: 'org.interledger', name: 'ilp-core', version: '1.0.2' + compile group: 'org.interledger', name: 'ilp-core', version: '1.0.3' ... } ``` diff --git a/examples-parent/ilp-emitters/pom.xml b/examples-parent/ilp-emitters/pom.xml index af82c253..62a951c3 100644 --- a/examples-parent/ilp-emitters/pom.xml +++ b/examples-parent/ilp-emitters/pom.xml @@ -55,6 +55,10 @@ org.interledger codecs-ildcp + + org.interledger + link-core + 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}
+ + ${project.groupId} + link-stateless-spsp-receiver + ${project.version} + ${project.groupId} spsp-client