Skip to content
This repository has been archived by the owner on Aug 18, 2019. It is now read-only.

Commit

Permalink
Add more unit tests
Browse files Browse the repository at this point in the history
* Create various Manager classes for Connector and Ledger
* Introduce Delivered vs Forwarded per interledger/rfcs#77
* Fix test harness.
  • Loading branch information
sappenin committed Nov 18, 2016
1 parent 45cce6d commit 4a3eb6d
Show file tree
Hide file tree
Showing 30 changed files with 1,507 additions and 983 deletions.
9 changes: 8 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<docker.image.prefix>ilp-connector-java</docker.image.prefix>
<appengine.sdk.version>1.9.42</appengine.sdk.version>
<jackson.version>2.8.2</jackson.version>
<guava.version>20.0</guava.version>
</properties>

<repositories>
Expand Down Expand Up @@ -218,6 +219,12 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<version>${guava.version}</version>
</dependency>

<!--<dependency>-->
<!--<groupId>org.interledger</groupId>-->
<!--<artifactId>java-ilp-ledger-simple</artifactId>-->
Expand Down Expand Up @@ -309,7 +316,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
<version>${guava.version}</version>
</dependency>

<dependency>
Expand Down
25 changes: 4 additions & 21 deletions src/main/java/money/fluid/ilp/connector/Connector.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package money.fluid.ilp.connector;

import money.fluid.ilp.connector.managers.ledgers.LedgerManager;
import money.fluid.ilp.connector.services.routing.RoutingService;
import money.fluid.ilp.ledger.model.ConnectorInfo;
import money.fluid.ilp.ledger.model.LedgerId;
import money.fluid.ilp.ledgerclient.InMemoryLedgerClient;
import money.fluid.ilp.ledgerclient.LedgerClient;

import java.util.Optional;
import java.util.Set;

/**
* An interface that defines an ILP connector.
Expand All @@ -16,25 +11,13 @@ public interface Connector {

ConnectorInfo getConnectorInfo();

Set<LedgerClient> getLedgerClients();

RoutingService getRoutingService();

LedgerManager getLedgerManager();

// TODO: Add startup and shutdown hooks?

void shutdown();

/**
* Given the specified {@link LedgerId}, find any instances of {@link LedgerClient} for which this connector is
* listening to events for. In general, a Conenctor will have only a single client listening to a given ledger, but
* it's possible there are more than one.
*
* @param ledgerId The {@link LedgerId} that uniquely identifies the {@link InMemoryLedgerClient} to return.
* @return
*/
default Optional<LedgerClient> getLedgerClient(final LedgerId ledgerId) {
return this.getLedgerClients().stream()
.filter(ledgerClient -> ledgerClient.getLedgerInfo().getLedgerId().equals(ledgerId))
.findAny();
}

}
328 changes: 173 additions & 155 deletions src/main/java/money/fluid/ilp/connector/DefaultConnector.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package money.fluid.ilp.connector.managers.ledgers;

import lombok.Getter;
import money.fluid.ilp.connector.model.ids.ConnectorId;
import money.fluid.ilp.connector.model.ids.IlpTransactionId;
import money.fluid.ilp.ledger.model.LedgerId;
import money.fluid.ilp.ledgerclient.LedgerClient;
import org.interledgerx.ilp.core.DeliveredLedgerTransfer;
import org.interledgerx.ilp.core.ForwardedLedgerTransfer;
import org.interledgerx.ilp.core.IlpAddress;
import org.interledgerx.ilp.core.LedgerTransferRejectedReason;

import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
* A default implementation of {@link LedgerManager} that uses an in-memory cache to track pending transfers
* and act upon them in a timely manner, as well as to allow them to be expired automatically.
*/
@Getter
public class DefaultLedgerManager implements LedgerManager {

private final ConnectorId connectorId;
private final Set<LedgerClient> ledgerClients;

// The Connector needs to track the pending transfers, not the event handlers. This is because event handler1 will
// receive a transfer for 1 ledger and make another transfer on another ledger. When the transfer executes, the 2nd
// handler won't have the originating ledger's info, so it gets it from here.
// TODO: Consider holding the entire transfer here, or loading-cache that is backed by a Database.
// TODO: This should be backed by a datastore since these transfers should not be lost until they are expired or fulfilled.
private final PendingTransferManager pendingTransferManager;

/**
* Required-args Constructor.
*
* @param connectorId
* @param ledgerClients
* @param pendingTransferManager
*/
public DefaultLedgerManager(
final ConnectorId connectorId,
final Set<LedgerClient> ledgerClients,
final PendingTransferManager pendingTransferManager
) {
this.connectorId = Objects.requireNonNull(connectorId);
this.ledgerClients = Objects.requireNonNull(ledgerClients);
this.pendingTransferManager = Objects.requireNonNull(pendingTransferManager);
}

@Override
public void deliverPayment(final LedgerId sourceLedgerId, final DeliveredLedgerTransfer ledgerTransfer) {
Objects.requireNonNull(sourceLedgerId);
Objects.requireNonNull(ledgerTransfer);

// Track the transfer for later...
// final NoteToSelf noteToSelf = NoteToSelf.builder().originatingLedgerId(
// ledgerId.getLedgerInfo().getLedgerId()).build();

// Because this is a delivery, the ledgerTransfer should have the local destination adress.
this.findLedgerClientSafely(ledgerTransfer.getLedgerId()).send(ledgerTransfer);

// Track the pending payment before sending to the ledger...
this.pendingTransferManager.addPendingTransfer(
PendingTransfer.of(
ledgerTransfer,
sourceLedgerId
));
}

@Override
public void forwardPayment(final LedgerId sourceLedgerId, final ForwardedLedgerTransfer ledgerTransfer) {
Objects.requireNonNull(sourceLedgerId);
Objects.requireNonNull(ledgerTransfer);

// Track the transfer for later...
// final NoteToSelf noteToSelf = NoteToSelf.builder().originatingLedgerId(
// ledgerId.getLedgerInfo().getLedgerId()).build();

// TODO: This method is specifying the ledgerId as calculated by the Connector, but perhaps it should be determining the LedgerId?
this.findLedgerClientSafely(ledgerTransfer.getLedgerId()).send(ledgerTransfer);

// Track the pending payment before sending to the ledger...
this.pendingTransferManager.addPendingTransfer(PendingTransfer.of(
ledgerTransfer,
sourceLedgerId
));
}

@Override
public void rejectPayment(
final IlpTransactionId ilpTransactionId, final LedgerTransferRejectedReason ledgerTransferRejectedReason
) {
Objects.requireNonNull(ilpTransactionId);

this.getOriginatingLedgerId(ilpTransactionId).ifPresent((ledgerId -> {
this.findLedgerClientSafely(ledgerId).rejectTransfer(ilpTransactionId, ledgerTransferRejectedReason);
}));
// Remove the pending payment _after_ sending to the ledger...
this.pendingTransferManager.removePendingTransfer(ilpTransactionId);
}

/**
* Given an {@link IlpTransactionId}, return the {@link LedgerId} that initiated this transfer. This method is used
* to fulfill and reject pending transfers.
*
* @param ilpTransactionId
* @return
*/
@Override
public Optional<LedgerId> getOriginatingLedgerId(final IlpTransactionId ilpTransactionId) {
Objects.requireNonNull(ilpTransactionId);
return this.getPendingTransferManager()
.getPendingTransfer(ilpTransactionId)
.map(pendingTransfer -> pendingTransfer.getLedgerId());
}

@Override
public IlpAddress getConnectorAccountOnLedger(final LedgerId ledgerId) {
return this.findLedgerClientSafely(ledgerId).getConnectionInfo().getLedgerAccountId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package money.fluid.ilp.connector.managers.ledgers;

import lombok.RequiredArgsConstructor;
import money.fluid.ilp.connector.model.ids.IlpTransactionId;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
* An implementation of {@link PendingTransferManager} that stores all pending transfers in-memory, and expires them
* after {@code defaultExpirationSeconds} seconds.
* <p>
* This implementation uses a simple Guava cache to hold any pending transfers, and expires entries at the
* appropriate time, but all at once (as opposed to on a per-transfer basis).
* <p>
* WARNING: This implementation should not be used in a production environment since it does NOT utilize a
* persistent datastore to store pending transfers. This has a variety of implications, but for example, if a
* Connector using this implementation were restarted, it would lose its ability to expire pending transfers, which
* could cause a Connector to lose money.
*/
@RequiredArgsConstructor
public class InMemoryPendingTransferManager implements PendingTransferManager {

//private final Logger logger = LoggerFactory.getLogger(this.getClass());

private final Map<IlpTransactionId, PendingTransfer> pendingTransfers;

/**
* No-args Constructor.
*/
public InMemoryPendingTransferManager() {
this.pendingTransfers = new HashMap<>();
}

@Override
public void addPendingTransfer(final PendingTransfer pendingTransfer) {
this.pendingTransfers.put(
pendingTransfer.getLedgerTransfer().getInterledgerPacketHeader().getIlpTransactionId(),
pendingTransfer
);
}

@Override
public void removePendingTransfer(final IlpTransactionId ilpTransactionId) {
this.pendingTransfers.remove(ilpTransactionId);
}

@Override
public Optional<PendingTransfer> getPendingTransfer(IlpTransactionId ilpTransactionId) {
return Optional.ofNullable(this.pendingTransfers.get(ilpTransactionId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package money.fluid.ilp.connector.managers.ledgers;

import money.fluid.ilp.connector.model.ids.ConnectorId;
import money.fluid.ilp.connector.model.ids.IlpTransactionId;
import money.fluid.ilp.ledger.model.LedgerId;
import money.fluid.ilp.ledgerclient.LedgerClient;
import org.interledgerx.ilp.core.DeliveredLedgerTransfer;
import org.interledgerx.ilp.core.ForwardedLedgerTransfer;
import org.interledgerx.ilp.core.IlpAddress;
import org.interledgerx.ilp.core.Ledger;
import org.interledgerx.ilp.core.LedgerTransferRejectedReason;

import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
* An interface that defines how a Connector can communicate with and manage any connected {@link Ledger} to facilitate
* ILP transactions, including ledger transfers whose ILP destination address exists on a directly connected {@link
* Ledger} (i.e., locally delivered payment), as well as transfers that must be serviced by a different connector (i.e.,
* a forwarded payment).
* <p>
* In ILP, a connector will complete its part of an interledger payment by reacting to funds received on one ledger and
* sending funds to another account on a different ledger to fullfil its portion of ILP. Because this type of activity
* involves holding Connector funds on multiple ledgers (i.e., tying up Connector liquidity), this service also handles
* ledger transaction timeouts by tracking pending payments, reversing timed-out escrows, and executing escrows in
* response to a valid fulfillment.
*/
public interface LedgerManager {

/**
* Begin the process of initiating a transfer. This includes the following steps:
* <p>
* <pre>
* <ol>
* <li>Create a local ledger transfer, including the cryptographic condition, and authorize this transfer on
* the local ledger.</li>
* <li>Wait for the local ledger to put the sender's funds on hold and notify this connector that this has been
* completed.</li>
* <li>Receive the notification from the Ledger, and extract the ILP packet to determine if the payment should
* be forwarded.</li>
*
* </ol>
* </pre>
*/
//void initiateTransfer();

/**
* Get the {@link ConnectorId} for the Connector this {@link LedgerManager} is operating for.
*
* @return
*/
ConnectorId getConnectorId();

Set<LedgerClient> getLedgerClients();

/**
* Delivery occurs when the best matching routing table entry is a local ledger. This method facilitates such a
* payment to the appropriate locally connected ledger.
*
* @param sourceLedgerId The {@link LedgerId} of the ledger that should be notified when the {@code ledgerTransfer}
* is either fulfilled, rejected, or timed-out.
* @param ledgerTransfer
* @see "/~https://github.com/interledger/rfcs/issues/77"
*/
void deliverPayment(final LedgerId sourceLedgerId, final DeliveredLedgerTransfer ledgerTransfer);

/**
* Forwarding occurs when the best matching (longest prefix) routing table entry names another connector. In other
* words, when a connector has no direct access to the destination ledger.
*
* @param sourceLedgerId The {@link LedgerId} of the ledger that should be notified when the {@code ledgerTransfer}
* is either fulfilled, rejected, or timed-out.
* @param ledgerTransfer
*/
void forwardPayment(final LedgerId sourceLedgerId, final ForwardedLedgerTransfer ledgerTransfer);

/**
* Rejection of a payment occurs when the routing table entry identifies an account that this ledger cannot route
* to. In this case, any ILP transactions are unwound and all asset transfers are reversed.
*/
void rejectPayment(
final IlpTransactionId ilpTransactionId, final LedgerTransferRejectedReason ledgerTransferRejectedReason
);

/**
* Given an {@link IlpTransactionId}, return the {@link LedgerId} that initiated the transfer. This method is used
* to fulfill and reject pending transfers.
*
* @param ilpTransactionId
* @return
*/
Optional<LedgerId> getOriginatingLedgerId(final IlpTransactionId ilpTransactionId);

/**
* Given the specified {@link LedgerId}, find any instances of {@link LedgerClient} for which this connector is
* listening to events for. In general, a Conenctor will have only a single client listening to a given ledger, but
* it's possible there are more than one.
*
* @param ledgerId The {@link LedgerId} that uniquely identifies the {@link LedgerClient} to return.
* @return
*/
default Optional<LedgerClient> findLedgerClient(final LedgerId ledgerId) {
Objects.requireNonNull(ledgerId);
return this.getLedgerClients().stream()
.filter(ledgerClient -> ledgerClient.getLedgerInfo().getLedgerId().equals(ledgerId))
.filter(ledgerClient -> ledgerClient.isConnected())
.findAny();
}

/**
* Helper method to get the proper {@link LedgerClient} for the indicated {@link LedgerId}, or throw an
* exception if the Ledger cannot be found, is disconnected, or is otherwise not available.
*
* @param ledgerId
* @return
* @throws RuntimeException if no connected {@link LedgerClient} can be found.
*/
default LedgerClient findLedgerClientSafely(final LedgerId ledgerId) {
return this.findLedgerClient(ledgerId).orElseThrow(
() -> new RuntimeException(String.format("No LedgerClient found for LedgerId: ", ledgerId)));
}

/**
* For a given {@link LedgerId}, return the {@link IlpAddress} of the account that the connector should transact
* with for that ledger.
*
* @param ledgerId
*/
default IlpAddress getConnectorAccountOnLedger(final LedgerId ledgerId) {
return this.findLedgerClientSafely(ledgerId).getConnectionInfo().getLedgerAccountId();
}
}
Loading

0 comments on commit 4a3eb6d

Please sign in to comment.