From 4a3eb6d967c7a867b8ff9924da248a8110b0ee45 Mon Sep 17 00:00:00 2001 From: David Fuelling Date: Fri, 18 Nov 2016 13:14:39 -0700 Subject: [PATCH] Add more unit tests * Create various Manager classes for Connector and Ledger * Introduce Delivered vs Forwarded per /~https://github.com/interledger/rfcs/issues/77 * Fix test harness. --- pom.xml | 9 +- .../money/fluid/ilp/connector/Connector.java | 25 +- .../fluid/ilp/connector/DefaultConnector.java | 328 ++++++------ .../ledgers/DefaultLedgerManager.java | 122 +++++ .../InMemoryPendingTransferManager.java | 53 ++ .../managers/ledgers/LedgerManager.java | 133 +++++ .../managers/ledgers/PendingTransfer.java | 34 ++ .../ledgers/PendingTransferManager.java | 39 ++ .../services/ledgers/LedgerLookupService.java | 25 - .../ledgers/LedgerLookupServiceImpl.java | 22 - .../ledgers/plugins/DefaultLedgerPlugin.java | 102 ---- .../ledgers/plugins/LedgerPlugin.java | 177 ------- .../impl/InfiniteLiquidityQuoteService.java | 487 +++++++++--------- .../services/routing/RoutingService.java | 34 +- .../services/transfers/TransferService.java | 36 -- .../money/fluid/ilp/ledger/EscrowManager.java | 97 +--- .../ilp/ledger/LedgerAccountManager.java | 1 - .../inmemory/EscrowExpirationHandler.java | 19 + .../inmemory/InMemoryEscrowManager.java | 192 +++++++ .../ilp/ledger/inmemory/InMemoryLedger.java | 87 +++- .../model/DeliveredLedgerTransferImpl.java | 44 ++ ....java => ForwardedLedgerTransferImpl.java} | 29 +- .../model/InitialLedgerTransferImpl.java | 58 +++ .../ledgerclient/InMemoryLedgerClient.java | 27 +- .../fluid/ilp/ledgerclient/LedgerClient.java | 8 +- .../ilp/core/DeliveredLedgerTransfer.java | 34 ++ .../ilp/core/ForwardedLedgerTransfer.java | 23 + .../interledgerx/ilp/core/LedgerTransfer.java | 31 +- .../ilp/core/events/LedgerEventHandler.java | 3 +- .../ilp/connector/IlpInMemoryTestHarness.java | 211 +++++++- 30 files changed, 1507 insertions(+), 983 deletions(-) create mode 100644 src/main/java/money/fluid/ilp/connector/managers/ledgers/DefaultLedgerManager.java create mode 100644 src/main/java/money/fluid/ilp/connector/managers/ledgers/InMemoryPendingTransferManager.java create mode 100644 src/main/java/money/fluid/ilp/connector/managers/ledgers/LedgerManager.java create mode 100644 src/main/java/money/fluid/ilp/connector/managers/ledgers/PendingTransfer.java create mode 100644 src/main/java/money/fluid/ilp/connector/managers/ledgers/PendingTransferManager.java delete mode 100644 src/main/java/money/fluid/ilp/connector/services/ledgers/LedgerLookupService.java delete mode 100644 src/main/java/money/fluid/ilp/connector/services/ledgers/LedgerLookupServiceImpl.java delete mode 100644 src/main/java/money/fluid/ilp/connector/services/ledgers/plugins/DefaultLedgerPlugin.java delete mode 100644 src/main/java/money/fluid/ilp/connector/services/ledgers/plugins/LedgerPlugin.java delete mode 100644 src/main/java/money/fluid/ilp/connector/services/transfers/TransferService.java create mode 100644 src/main/java/money/fluid/ilp/ledger/inmemory/EscrowExpirationHandler.java create mode 100644 src/main/java/money/fluid/ilp/ledger/inmemory/InMemoryEscrowManager.java create mode 100644 src/main/java/money/fluid/ilp/ledger/inmemory/model/DeliveredLedgerTransferImpl.java rename src/main/java/money/fluid/ilp/ledger/inmemory/model/{DefaultLedgerTransfer.java => ForwardedLedgerTransferImpl.java} (56%) create mode 100644 src/main/java/money/fluid/ilp/ledger/inmemory/model/InitialLedgerTransferImpl.java create mode 100644 src/main/java/org/interledgerx/ilp/core/DeliveredLedgerTransfer.java create mode 100644 src/main/java/org/interledgerx/ilp/core/ForwardedLedgerTransfer.java diff --git a/pom.xml b/pom.xml index af825de..a659bb9 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ ilp-connector-java 1.9.42 2.8.2 + 20.0 @@ -218,6 +219,12 @@ test + + com.google.guava + guava-testlib + ${guava.version} + + @@ -309,7 +316,7 @@ com.google.guava guava - 20.0 + ${guava.version} diff --git a/src/main/java/money/fluid/ilp/connector/Connector.java b/src/main/java/money/fluid/ilp/connector/Connector.java index edaf69b..104f331 100644 --- a/src/main/java/money/fluid/ilp/connector/Connector.java +++ b/src/main/java/money/fluid/ilp/connector/Connector.java @@ -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. @@ -16,25 +11,13 @@ public interface Connector { ConnectorInfo getConnectorInfo(); - Set 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 getLedgerClient(final LedgerId ledgerId) { - return this.getLedgerClients().stream() - .filter(ledgerClient -> ledgerClient.getLedgerInfo().getLedgerId().equals(ledgerId)) - .findAny(); - } + } diff --git a/src/main/java/money/fluid/ilp/connector/DefaultConnector.java b/src/main/java/money/fluid/ilp/connector/DefaultConnector.java index f8b88cb..332c9c2 100644 --- a/src/main/java/money/fluid/ilp/connector/DefaultConnector.java +++ b/src/main/java/money/fluid/ilp/connector/DefaultConnector.java @@ -2,18 +2,19 @@ import lombok.Getter; import lombok.ToString; -import money.fluid.ilp.connector.model.ids.IlpTransactionId; +import money.fluid.ilp.connector.managers.ledgers.LedgerManager; import money.fluid.ilp.connector.services.routing.Route; import money.fluid.ilp.connector.services.routing.RoutingService; import money.fluid.ilp.ledger.inmemory.events.AbstractEventBusLedgerEventHandler; -import money.fluid.ilp.ledger.inmemory.model.DefaultLedgerTransfer; +import money.fluid.ilp.ledger.inmemory.model.DeliveredLedgerTransferImpl; +import money.fluid.ilp.ledger.inmemory.model.ForwardedLedgerTransferImpl; import money.fluid.ilp.ledger.model.ConnectorInfo; import money.fluid.ilp.ledger.model.LedgerId; -import money.fluid.ilp.ledger.model.NoteToSelf; 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.LedgerInfo; -import org.interledgerx.ilp.core.LedgerTransfer; import org.interledgerx.ilp.core.LedgerTransferRejectedReason; import org.interledgerx.ilp.core.events.LedgerConnectedEvent; import org.interledgerx.ilp.core.events.LedgerDirectTransferEvent; @@ -25,50 +26,38 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; /** - * A default implementation of an ILP {@link Connector}. + * A default implementation of an Interledger {@link Connector}. + * + * @see "http://www.interledger.org" */ @Getter @ToString public class DefaultConnector implements Connector { private final ConnectorInfo connectorInfo; - // Specified at Connector initialization time, these are all ledgers that this Connector can connect to...required in - // order for a Connector to move money along a given Route. - private final Set ledgerClients; - private final RoutingService routingService; - //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 Map pendingTransfers; + private final LedgerManager ledgerManager; /** * Required-args Constructor. Allows for full DI support of all dependencies. * * @param connectorInfo - * @param ledgerClients * @param routingService + * @param ledgerManager */ public DefaultConnector( final ConnectorInfo connectorInfo, - final Set ledgerClients, - final RoutingService routingService + final RoutingService routingService, + final LedgerManager ledgerManager ) { this.connectorInfo = Objects.requireNonNull(connectorInfo); - this.ledgerClients = Objects.requireNonNull(ledgerClients); this.routingService = Objects.requireNonNull(routingService); - - this.pendingTransfers = new HashMap<>(); + this.ledgerManager = Objects.requireNonNull(ledgerManager); this.initialize(); } @@ -80,7 +69,7 @@ private void initialize() { // The Connector has a set of Ledgers that are pre-configured. This initializer registers a LedgerEventHandler // with each one. Alternative implementations might register N event handlers per Connector (e.g., a handler to // handle each event instead of a single handler that handles all events). - this.ledgerClients.stream().forEach( + this.getLedgerManager().getLedgerClients().stream().forEach( ledgerClient -> { // Establish a connection first... ledgerClient.connect(); @@ -92,20 +81,37 @@ private void initialize() { @Override public void shutdown() { // Shutdown any connections... - this.ledgerClients.stream().forEach((LedgerClient::disconnect)); + this.getLedgerManager().getLedgerClients().stream().forEach((LedgerClient::disconnect)); + } + + /** + * Determines if a given {@link IlpAddress} has a routing table entry that names a local ledger. In other words, + * _this_ connector has direct access to the destination address's ledger. + * + * @param destinationAddress An instance of {@link IlpAddress} representing the ultimate destination of an ILP + * transfer. + * @return {@code true} if {@link Route#getDestinationAddress()} exists in a ledger that this Connector has an + * actively connected LedgerClient for; {@code false} otherwise. + * @see "/~https://github.com/interledger/rfcs/issues/77" + */ + public boolean isTransferLocallyDeliverable(final IlpAddress destinationAddress) { + Objects.requireNonNull(destinationAddress); + return this.getLedgerManager().findLedgerClient(destinationAddress.getLedgerId()).isPresent(); } /** - * Helper method to determine which ledger client should be used for a given {@link IlpAddress}. + * Determines if a given {@link IlpAddress} has a routing table entry that names another connector. In other words, + * _this_ connector has no direct access to the destination address's ledger. * - * @param ledgerId - * @return + * @param destinationAddress An instance of {@link IlpAddress} representing the ultimate destination of an ILP + * transfer. + * @return {@code true} if {@code destinationAddress} exists in a route that this Connector has an actively + * connected LedgerClient for; {@code false} otherwise. + * @see "/~https://github.com/interledger/rfcs/issues/77" */ - public Optional findLedgerClient(final LedgerId ledgerId) { - Objects.requireNonNull(ledgerId); - return this.getLedgerClients().stream() - .filter(ledgerClient -> ledgerClient.getLedgerInfo().getLedgerId().equals(ledgerId)) - .findFirst(); + public boolean isTransferRemotelyForwardable(final IlpAddress destinationAddress) { + Objects.requireNonNull(destinationAddress); + return this.getRoutingService().bestHopForDestinationAmount(destinationAddress).isPresent(); } /** @@ -149,29 +155,31 @@ protected void handleEvent(final LedgerDisonnectedEvent ledgerDisonnectedEvent) /** * @param ledgerDirectTransferEvent An instance of {@link LedgerDirectTransferEvent}. * @deprecated TODO: It seems like this event is intended to be used when no ILP escrow is required. However, - * it seems like this should // never be used, and instead we should always use escrow, even for optimistic - * transactions, so that a recipient // who rejects a payment doesn't have to actually send money back to a + * it seems like this should never be used, and instead we should always use escrow, even for optimistic + * transactions, so that a recipient who rejects a payment doesn't have to actually send money back to a * sender. */ @Override @Deprecated protected void handleEvent(final LedgerDirectTransferEvent ledgerDirectTransferEvent) { - this.getListeningConnector().getPendingTransfers().remove( - ledgerDirectTransferEvent.getIlpPacketHeader().getIlpTransactionId() - ); +// this.getListeningConnector().getPendingTransfers().remove( +// ledgerDirectTransferEvent.getIlpPacketHeader().getIlpTransactionId() +// ); } /** * When a transfer has been prepared on a particular ledger, it means that some payor (a person or a connector, * e.g.) has put money on-hold in the ledger that this listener is in charge of monitoring on behalf of the * connector that this handler operates. In other words, it means this Connector has funds that are in-escrow, - * waiting to be captured by it. In order to capture those funds, this connector must "forward" another - * corresponding payment in the "next-hop" ledger, in hopes of fulfilling the ILP protocol so that this ledger's - * escrow releases the funds on hold waiting for this connector. + * waiting to be captured by it. *

- * The connector associated with this handler must also store, for later rollback or execution, a record of the - * next-hop transfer for auditing purposes, as well as update its advertised routing tables to reflect the - * reduction in liquidity that processing this event entails. + * In order to capture those funds, this connector must "deliver" another corresponding payment in the + * "next-hop" ledger, in hopes of fulfilling the ILP protocol so that this ledger's escrow releases the funds on + * hold waiting for this connector. + *

+ * The Connector (associated with this {@link LedgerEventHandler}) must also store, for later rollback or + * execution, a record of the next-hop transfer for auditing purposes, as well as to update its advertised + * routing tables to reflect the reduction in liquidity that processing this ILP transfer entails. * * @param ledgerTransferPreparedEvent An instance of {@link LedgerTransferPreparedEvent}. * @return @@ -182,90 +190,88 @@ protected void handleEvent(final LedgerTransferPreparedEvent ledgerTransferPrepa // TODO: implement fraud and velocity checks here, if possible. If a given sender or ledger is behaving // badly, we don't want to suffer for it. - // First, what's the best route to the destination address? We rely on the routing table to let us know. - final Optional optRoute = this.listeningConnector.getRoutingService().bestHopForDestinationAmount( - ledgerTransferPreparedEvent.getIlpPacketHeader().getDestinationAddress(), - ledgerTransferPreparedEvent.getAmount() - ); - - if (optRoute.isPresent()) { - final Route route = optRoute.get(); - - // TODO: Consider putting getConnectorLedger(ledgerId) into the connector, and return an optional. - final LedgerClient ledgerClient = this.listeningConnector.getLedgerClient( - route.getSourceAddress().getLedgerId() - ).orElseThrow(() -> new RuntimeException( - String.format( - "Routing table had a route but Connector has no DefaultLedgerClient for LedgerId %s!", - ledgerTransferPreparedEvent.getLedgerInfo().getLedgerId() - )) - ); - - // TODO: Is this correct? The connector is saying to escrow money locally from itself to a local recipient, - // but it's unclear who that recipient is. Thus, for this transfer the connector leaves it to the Ledger - // to determine the recipient based upon its own business rules. - // Create a new LedgerTransfer for the next hop, and send it to that - final LedgerTransfer transfer = new DefaultLedgerTransfer( + final IlpAddress destinationAddress = ledgerTransferPreparedEvent.getIlpPacketHeader().getDestinationAddress(); + if (this.getListeningConnector().isTransferLocallyDeliverable(destinationAddress)) { + + /////////////////////////////// + // Deliver the Transfer Payment via a new Transfer + /////////////////////////////// + // The source account of the new transfer should be the connector's account on the next-hop ledger. Since + // this is a "deliverable" payment, the local source account should be this Connector's account on the + // destination ledger, which can be gleaned from the ILP destination address. + final LedgerId ledgerId = ledgerTransferPreparedEvent.getIlpPacketHeader().getDestinationAddress().getLedgerId(); + final IlpAddress ledgerLocalSourceAddress = this.getListeningConnector().getLedgerManager() + .getConnectorAccountOnLedger(ledgerId); + // Since this is a "deliverable" payment, the next-hopt destination account should be the destination + // account in the ILP header. + final IlpAddress ledgerLocalDestinationAddress = ledgerTransferPreparedEvent.getIlpPacketHeader().getDestinationAddress(); + + // Send a payment to the ILP destination address (the actual recipient) on the destination ledger... + final DeliveredLedgerTransfer transfer = new DeliveredLedgerTransferImpl( ledgerTransferPreparedEvent.getIlpPacketHeader(), - - // TODO: Should this come from the routing table, or should the routing table return a ledgerId, - // and then _this_ connector can determine what it's ledgerId is on that ledger? - //route.getSourceAddress(), - ledgerClient.getConnectionInfo().getLedgerAccountId(), - - // The connector doesn't actually know who the local destination account is. Thus, it's empty, - // but perhaps it shouldn't even exist? - //Optional.ofNullable(route.getDestinationAddress()), - Optional.empty(), - ledgerTransferPreparedEvent.getAmount(), + ledgerId, + ledgerLocalSourceAddress, + ledgerLocalDestinationAddress, Optional.empty(), Optional.empty() - //.condition(ledgerTransferPreparedEvent.getOptCondition) - //.data(ledgerClient.getLedgerInfo()) - // The source address on the next hop ledger that this Connector owns. - // TODO: The ledger will determine the destination account id to use for escrow, right? - // .destinationAddress( - // ledgerTransferPreparedEvent.getIlpPacketHeader().getLocalDestinationAddress()) ); - - /////////////////////// - // Instruct the Ledger to send the transfer. - /////////////////////// - - // TODO: This will put some funds on hold in that ledger, so we need to update our routing tables potentially. - // TODO: Handle events idempotently. In other words, don't process the same transfer twice in an unsafe fashion. - - // TODO: Depending on how this is used, we might just store the LedgerId of the source ledger for this transfer. - // Track the transfer for later... - final NoteToSelf noteToSelf = NoteToSelf.builder().originatingLedgerId( - ledgerTransferPreparedEvent.getLedgerInfo().getLedgerId()).build(); - - // TODO: Consider making pending transfers part of the ConnectorInterface -- not the actual map access, - // but methods to add and remove... - this.getListeningConnector().getPendingTransfers().put( - ledgerTransferPreparedEvent.getIlpPacketHeader().getIlpTransactionId(), - noteToSelf + this.getListeningConnector().getLedgerManager().deliverPayment( + // This is the ledgerId to notify in the event of a rejection, timeout, or fulfillment. It's not + // the same as the ledger in the transfer. + ledgerTransferPreparedEvent.getLedgerInfo().getLedgerId(), + transfer + ); + } else if (this.getListeningConnector().isTransferRemotelyForwardable(destinationAddress)) { + // First, what's the best route to the destination address? We rely on the routing table to let us know. + final Optional optRoute = this.listeningConnector.getRoutingService().bestHopForDestinationAmount( + ledgerTransferPreparedEvent.getIlpPacketHeader().getDestinationAddress(), + ledgerTransferPreparedEvent.getAmount() ); - ledgerClient.send(transfer); + if (optRoute.isPresent()) { + final Route route = optRoute.get(); + + // Send a payment to the next-hop connector on a new ledger per the routing table... + + // The newSourceAccount should be this Connector's ledger-account on the new ledger from the route. + final LedgerId ledgerId = route.getSourceAddress().getLedgerId(); + + final IlpAddress ledgerLocalSourceAddress = this.getListeningConnector().getLedgerManager() + .getConnectorAccountOnLedger(ledgerId); + + final ForwardedLedgerTransfer transfer = new ForwardedLedgerTransferImpl( + ledgerTransferPreparedEvent.getIlpPacketHeader(), + ledgerId, + ledgerLocalSourceAddress + //.condition(ledgerTransferPreparedEvent.getOptCondition) + //.data(ledgerClient.getLedgerInfo()) + // The source address on the next hop ledger that this Connector owns. + // TODO: The ledger will determine the destination account id to use for escrow, right? + // .destinationAddress( + // ledgerTransferPreparedEvent.getIlpPacketHeader().getLocalDestinationAddress()) + ); + + this.getListeningConnector().getLedgerManager().forwardPayment( + // This is the ledgerId to notify in the event of a rejection, timeout, or fulfillment. It's not + // the same as the ledger in the transfer. + ledgerTransferPreparedEvent.getLedgerInfo().getLedgerId(), + transfer + ); + } else { + // Reject the transfer due to no Route (which is comparable to not having a ledger). + //final LedgerClient ledgerClient = this.getListeningConnector().findLedgerClientSafely( + // ledgerTransferPreparedEvent.getLedgerInfo().getLedgerId()); + this.getListeningConnector().getLedgerManager().rejectPayment( + ledgerTransferPreparedEvent.getIlpPacketHeader().getIlpTransactionId(), + LedgerTransferRejectedReason.NO_ROUTE_TO_LEDGER + ); + } } else { - // If there's no route, then we need to reject the transfer and then throw an exception... - final LedgerId ledgerId = ledgerTransferPreparedEvent.getLedgerInfo().getLedgerId(); - final LedgerClient ledgerClient = this.getListeningConnector().getLedgerClient(ledgerId) - .orElseThrow(() -> new RuntimeException( - String.format( - "Received ledgerTransferPreparedEvent for disconnected Ledger %s", - ledgerTransferPreparedEvent.getLedgerInfo().getLedgerId() - ))); - ledgerClient.rejectTransfer( + // Reject the transfer due to no Ledger Connection... + this.getListeningConnector().getLedgerManager().rejectPayment( ledgerTransferPreparedEvent.getIlpPacketHeader().getIlpTransactionId(), LedgerTransferRejectedReason.NO_ROUTE_TO_LEDGER ); - - // For now, if the prepared transfer can't be routed by this connector, then the connector does this. - // But is this correct? - // TODO: Query the list or js code to determine what should happen here... - throw new RuntimeException(String.format("No route to Ledger %s", ledgerId)); } } @@ -283,33 +289,38 @@ protected void handleEvent(final LedgerTransferPreparedEvent ledgerTransferPrepa */ @Override protected void handleEvent(final LedgerTransferExecutedEvent ledgerTransferExecutedEvent) { + // TODO: From the LedgerTransferEvent doc: "Ledger plugins MUST ensure that the data in the noteToSelf either isn't shared with any untrusted party or encrypted before it is shared." // If this value is signed via the fulfilment, then does it matter if it's encrypted? In other words, what's secret about this information? // Perhaps the ILP Transaction Id? If that's the case, then perhaps it would be preferable for the Ledger to store this data internally // The ledger that this executed transfer came from, so we can pass the fulfillment back. - final LedgerId originatingLedgerId = Optional.ofNullable( - this.getListeningConnector().getPendingTransfers().get( - ledgerTransferExecutedEvent.getIlpPacketHeader().getIlpTransactionId()) - ) - .map(noteToSelf -> noteToSelf.getOriginatingLedgerId()) - .orElseThrow(() -> new RuntimeException( - "No pending transfer existed to determine which ledger to pass executed fulfilment back to!")); - - - // This Connector needs to send the fufillment condition back to the ledger that originally triggered the ILP - // transaction in the first place. - final Optional optLedgerClient = this.getListeningConnector().findLedgerClient( - originatingLedgerId); - if (optLedgerClient.isPresent()) { - optLedgerClient.get().fulfillCondition( - ledgerTransferExecutedEvent.getIlpPacketHeader().getIlpTransactionId()); + final Optional optOriginatingLedgerId = this.getListeningConnector().getLedgerManager() + .getOriginatingLedgerId( + ledgerTransferExecutedEvent.getIlpPacketHeader().getIlpTransactionId() + ); + + if (optOriginatingLedgerId.isPresent()) { + final LedgerId originatingLedgerId = optOriginatingLedgerId.get(); + + // This Connector needs to send the fufillment condition back to the ledger that originally triggered + // the ILP transaction in the first place. + final Optional optLedgerClient = this.getListeningConnector().getLedgerManager() + .findLedgerClient(originatingLedgerId); + if (optLedgerClient.isPresent()) { + optLedgerClient.get().fulfillCondition( + ledgerTransferExecutedEvent.getIlpPacketHeader().getIlpTransactionId()); + } else { + logger.warn("No LedgerClient existed for LedgerId: {}", originatingLedgerId); + } } else { - logger.warn("No LedgerClient existed for LedgerId: {}", originatingLedgerId); + logger.error( + "No originating LedgerId to execute for ILP Transaction {}!", + ledgerTransferExecutedEvent.getIlpPacketHeader().getIlpTransactionId() + ); } } - @Override protected void handleEvent(final LedgerTransferRejectedEvent ledgerTransferRejectedEvent) { // TODO: From the LedgerTransferEvent doc: "Ledger plugins MUST ensure that the data in the noteToSelf either @@ -320,25 +331,32 @@ protected void handleEvent(final LedgerTransferRejectedEvent ledgerTransferRejec // be preferable for the Ledger to store this data internally. // The ledger that this executed transfer came from, so we can pass the fulfillment back. - final LedgerId originatingLedgerId = - Optional.ofNullable(this.getListeningConnector().getPendingTransfers().get( - ledgerTransferRejectedEvent.getIlpPacketHeader().getIlpTransactionId()) - ) - .map(noteToSelf -> noteToSelf.getOriginatingLedgerId()) - .orElseThrow(() -> new RuntimeException( - "No pending transfer existed to determine which ledger to pass rejected fulfilment back to!")); - - // This Connector needs to send the rejection back to the ledger that originally triggered the ILP - // transaction in the first place. - final Optional optLedgerClient = this.getListeningConnector().findLedgerClient( - originatingLedgerId); - if (optLedgerClient.isPresent()) { - optLedgerClient.get().rejectTransfer( - ledgerTransferRejectedEvent.getIlpPacketHeader().getIlpTransactionId(), - LedgerTransferRejectedReason.REJECTED_BY_RECEIVER - ); + final Optional optOriginatingLedgerId = this.getListeningConnector().getLedgerManager() + .getOriginatingLedgerId( + ledgerTransferRejectedEvent.getIlpPacketHeader().getIlpTransactionId() + ); + + if (optOriginatingLedgerId.isPresent()) { + final LedgerId originatingLedgerId = optOriginatingLedgerId.get(); + + // This Connector needs to send the rejection back to the ledger that originally triggered the ILP + // transaction in the first place. + final Optional optLedgerClient = this.getListeningConnector().getLedgerManager() + .findLedgerClient(originatingLedgerId); + + if (optLedgerClient.isPresent()) { + optLedgerClient.get().rejectTransfer( + ledgerTransferRejectedEvent.getIlpPacketHeader().getIlpTransactionId(), + LedgerTransferRejectedReason.REJECTED_BY_RECEIVER + ); + } else { + logger.warn("No LedgerClient existed for LedgerId: {}", originatingLedgerId); + } } else { - logger.warn("No LedgerClient existed for LedgerId: {}", originatingLedgerId); + logger.error( + "No originating LedgerId to execute for ILP Transaction {}!", + ledgerTransferRejectedEvent.getIlpPacketHeader().getIlpTransactionId() + ); } } } diff --git a/src/main/java/money/fluid/ilp/connector/managers/ledgers/DefaultLedgerManager.java b/src/main/java/money/fluid/ilp/connector/managers/ledgers/DefaultLedgerManager.java new file mode 100644 index 0000000..e77ae3a --- /dev/null +++ b/src/main/java/money/fluid/ilp/connector/managers/ledgers/DefaultLedgerManager.java @@ -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 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 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 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(); + } +} diff --git a/src/main/java/money/fluid/ilp/connector/managers/ledgers/InMemoryPendingTransferManager.java b/src/main/java/money/fluid/ilp/connector/managers/ledgers/InMemoryPendingTransferManager.java new file mode 100644 index 0000000..ac81120 --- /dev/null +++ b/src/main/java/money/fluid/ilp/connector/managers/ledgers/InMemoryPendingTransferManager.java @@ -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. + *

+ * 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). + *

+ * 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 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 getPendingTransfer(IlpTransactionId ilpTransactionId) { + return Optional.ofNullable(this.pendingTransfers.get(ilpTransactionId)); + } +} diff --git a/src/main/java/money/fluid/ilp/connector/managers/ledgers/LedgerManager.java b/src/main/java/money/fluid/ilp/connector/managers/ledgers/LedgerManager.java new file mode 100644 index 0000000..0c3a55a --- /dev/null +++ b/src/main/java/money/fluid/ilp/connector/managers/ledgers/LedgerManager.java @@ -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). + *

+ * 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: + *

+ *

+     *  
    + *
  1. Create a local ledger transfer, including the cryptographic condition, and authorize this transfer on + * the local ledger.
  2. + *
  3. Wait for the local ledger to put the sender's funds on hold and notify this connector that this has been + * completed.
  4. + *
  5. Receive the notification from the Ledger, and extract the ILP packet to determine if the payment should + * be forwarded.
  6. + * + *
+ *
+ */ + //void initiateTransfer(); + + /** + * Get the {@link ConnectorId} for the Connector this {@link LedgerManager} is operating for. + * + * @return + */ + ConnectorId getConnectorId(); + + Set 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 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 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(); + } +} diff --git a/src/main/java/money/fluid/ilp/connector/managers/ledgers/PendingTransfer.java b/src/main/java/money/fluid/ilp/connector/managers/ledgers/PendingTransfer.java new file mode 100644 index 0000000..ee353d6 --- /dev/null +++ b/src/main/java/money/fluid/ilp/connector/managers/ledgers/PendingTransfer.java @@ -0,0 +1,34 @@ +package money.fluid.ilp.connector.managers.ledgers; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import money.fluid.ilp.ledger.model.LedgerId; +import org.interledgerx.ilp.core.LedgerTransfer; + +/** + * A class that holds all neccesary information about a pending ILP transfer. + */ +@RequiredArgsConstructor +@Getter +@ToString +@EqualsAndHashCode +// TODO: Transform this into an interface. +public class PendingTransfer { + + @NonNull + private final LedgerTransfer ledgerTransfer; + + // Add a LedgerId here so that when this is timed out, the manager can grab the client and reject + @NonNull + private final LedgerId ledgerId; + + public static PendingTransfer of(final LedgerTransfer ledgerTransfer, final LedgerId ledgerIdToNotify) { + return new PendingTransfer(ledgerTransfer, ledgerIdToNotify); + } + + // TODO: Consider NoteToSelf. From the JS impl it appears that each transfer has its own identifier in addition + // to the ILP transaction id, so this may need to be refactored. +} diff --git a/src/main/java/money/fluid/ilp/connector/managers/ledgers/PendingTransferManager.java b/src/main/java/money/fluid/ilp/connector/managers/ledgers/PendingTransferManager.java new file mode 100644 index 0000000..6aa3037 --- /dev/null +++ b/src/main/java/money/fluid/ilp/connector/managers/ledgers/PendingTransferManager.java @@ -0,0 +1,39 @@ +package money.fluid.ilp.connector.managers.ledgers; + +import money.fluid.ilp.connector.model.ids.IlpTransactionId; + +import java.util.Optional; + +/** + * A service that tracks pending transfers. + *

+ * Pending transfers are transfers that a Connector has made on a connected ledger in order to complete its own portion + * of the ILP protocol. Because each pending transfer reduces the liquidity of the connector, it is important for the + * Connector to be able to properly track these transfers at all times. + *

+ * This service is responsible for persistent storage and caching of these pending transfers. + */ +public interface PendingTransferManager { + + /** + * Add a {@link PendingTransfer} to this manager for future management. + * + * @param pendingTransfer + */ + void addPendingTransfer(PendingTransfer pendingTransfer); + + /** + * Remove a {@link PendingTransfer} from this manager. + * + * @param ilpTransactionId + */ + void removePendingTransfer(IlpTransactionId ilpTransactionId); + + /** + * Get a pending transfer by {@link IlpTransactionId}. + * + * @param ilpTransactionId + * @return + */ + Optional getPendingTransfer(IlpTransactionId ilpTransactionId); +} \ No newline at end of file diff --git a/src/main/java/money/fluid/ilp/connector/services/ledgers/LedgerLookupService.java b/src/main/java/money/fluid/ilp/connector/services/ledgers/LedgerLookupService.java deleted file mode 100644 index fcd5a0b..0000000 --- a/src/main/java/money/fluid/ilp/connector/services/ledgers/LedgerLookupService.java +++ /dev/null @@ -1,25 +0,0 @@ -package money.fluid.ilp.connector.services.ledgers; - - -import com.sappenin.utils.StringId; -import money.fluid.ilp.connector.model.Account; -import org.interledgerx.ilp.core.Ledger; - -import java.util.Optional; - -/** - * A service for looking up information about a ledger. - */ -public interface LedgerLookupService { - - /** - * Given a ledger identifier that is controlled by the owner of this connector, determine the account that should be - * used. This mechanism allows the connector to know about the other connectors that it can work with in order to - * provide quoting and transfer services. - * - * @param ledgerId - * @return - */ - Optional getAccountForLedger(final StringId ledgerId); - -} diff --git a/src/main/java/money/fluid/ilp/connector/services/ledgers/LedgerLookupServiceImpl.java b/src/main/java/money/fluid/ilp/connector/services/ledgers/LedgerLookupServiceImpl.java deleted file mode 100644 index 8dcd204..0000000 --- a/src/main/java/money/fluid/ilp/connector/services/ledgers/LedgerLookupServiceImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package money.fluid.ilp.connector.services.ledgers; - -import com.sappenin.utils.StringId; -import money.fluid.ilp.connector.model.Account; -import org.interledgerx.ilp.core.Ledger; - -import java.util.Optional; - -/** - * A default implementation of {@link LedgerLookupService}. - */ -public class LedgerLookupServiceImpl implements LedgerLookupService { - - // TODO: Create a new implementation that backs this information by a proper datastore, or other mechanism for up-to-date data. - @Override - public Optional getAccountForLedger(final StringId ledgerId) { - - // TODO:FIXME! - return null; - //return Optional.of(new Account.Builder().withAccountId(new StringId<>("sappenin")).build()); - } -} diff --git a/src/main/java/money/fluid/ilp/connector/services/ledgers/plugins/DefaultLedgerPlugin.java b/src/main/java/money/fluid/ilp/connector/services/ledgers/plugins/DefaultLedgerPlugin.java deleted file mode 100644 index 6b2175c..0000000 --- a/src/main/java/money/fluid/ilp/connector/services/ledgers/plugins/DefaultLedgerPlugin.java +++ /dev/null @@ -1,102 +0,0 @@ -package money.fluid.ilp.connector.services.ledgers.plugins; - -import org.interledgerx.ilp.core.Ledger; - -/** - * A default implementation of {@link LedgerPlugin}. - * - * @deprecated Remove this class and merge it into {@link Ledger} and LedgerAccountManager. - */ -@Deprecated -public class DefaultLedgerPlugin {// implements LedgerPlugin { - -// @Getter -// private final String prefix; -// @Getter -// private final String account; -// -// private final Ledger ledger; -// -// /** -// * Required-args Constructor. -// * -// * @param prefix -// * @param account -// * @param ledger An instance of {@link Ledger} that this plugin can operate upon. -// */ -// public DefaultLedgerPlugin(final String prefix, final String account, final Ledger ledger) { -// this.prefix = Objects.requireNonNull(prefix); -// this.account = Objects.requireNonNull(account); -// this.ledger = Objects.requireNonNull(ledger); -// } -// -// @Override -// public void send(final LedgerTransfer transfer) { -// Preconditions.checkNotNull(transfer); -// -// // final LedgerAccountManager accountManager = LedgerAccountManagerFactory.getAccountManagerSingleton(); -// // final LedgerAccount from = accountManager.getAccountByName(transfer.getSourceAccountLocalIdentifier()); -// // final LedgerAccount to = accountManager.getAccountByName(transfer.getDestinationAccountLocalIdentifier()); -// if (to.equals(from)) { -// throw new RuntimeException("Accounts are the same!"); -// } -// -// final MonetaryAmount amount = MoneyUtils.toMonetaryAmount(transfer.getAmount(), info.getCurrencyCode()); -// if (from.getBalance().isGreaterThanOrEqualTo(amount)) { -// from.debit(amount); -// to.credit(amount); -// } else { -// throw new InsufficientAmountException(amount.toString()); -// } -// -// // For Local Transfer, the only event is a LedgerDirectTransferEvent. -// final LedgerDirectTransferEvent ledgerTransferExecutedEvent = new LedgerDirectTransferEvent( -// this, transfer.getHeader(), transfer.getSourceAccountLocalIdentifier(), -// transfer.getDestinationAccountLocalIdentifier(), transfer.getAmount() -// ); -// this.notifyEventHandlers(ledgerTransferExecutedEvent); -// } -// -// /** -// * If the specified {@code destinationAddress} has a corresponding account on this ledger, then the account is -// * considered 'local', and this method will return {@code true}. Otherwise, the account is considered 'non-local', -// * and this method will return {@code false}. -// * -// * @param destinationAddress A {@link String} representing an ILP address of the ultimate destination account that -// * funds will be transferred to as part of a {@link LedgerTransfer}. -// * @return -// */ -// private boolean isLocalAccount(final String destinationAddress) { -// // TODO: Refactor SimpleLedgerAddressParser for DI and then thread-safety. -// final SimpleLedgerAddressParser parser = new SimpleLedgerAddressParser(); -// parser.parse(destinationAddress); -// final String accountName = parser.getAccountName(); -// return account != null; -// } -// -// /** -// * Completes the supplied {@link LedgerTransfer} locally without utilizing any conditions or connectors. -// * -// * @param transfer An instance of {@link LedgerTransfer} to complete locally. -// */ -// private void sendLocally(final LedgerTransfer transfer) { -// -// } -// -// /** -// * Attempts to complete the supplied {@link LedgerTransfer} using ILP. -// * -// * @param ledgerTransfer -// */ -// private void sendRemote(final LedgerTransfer ledgerTransfer) { -// -// // Since this is a remote ILP transfer, this ledger needs to determine a Connector account to send through. -// // This module determines a destination account on the local ledger for this interledger address. -// // In this case it is the account of a connector. -// // -// // It passes the chosen amount and the local destination account to the local ledger interface. -// -// -// } - -} diff --git a/src/main/java/money/fluid/ilp/connector/services/ledgers/plugins/LedgerPlugin.java b/src/main/java/money/fluid/ilp/connector/services/ledgers/plugins/LedgerPlugin.java deleted file mode 100644 index cc705ad..0000000 --- a/src/main/java/money/fluid/ilp/connector/services/ledgers/plugins/LedgerPlugin.java +++ /dev/null @@ -1,177 +0,0 @@ -package money.fluid.ilp.connector.services.ledgers.plugins; - -import org.interledgerx.ilp.core.LedgerTransfer; - -/** - * A ledger abstraction interface for Interledger clients and connectors to communicate and route payments across - * different ledger protocols. - * - * @see "/~https://github.com/interledger/rfcs/blob/master/0004-ledger-plugin-interface/0004-ledger-plugin-interface.md" - */ -public interface LedgerPlugin { - - /** - * Initiates a ledger-local transfer. - * - * @param transfer LedgerTransfer - */ - void send(LedgerTransfer transfer); - - /** - * Get the ledger plugin's ILP address prefix. This is used to determine whether a given ILP address is local to - * this ledger plugin and thus can be reached using this plugin's send method. - *

- * Example Return Value: "us.fed.some-bank" - * - * @return - */ - String getPrefix(); - - /** - * Get the ledger plugin's ILP address. This is given to senders to receive transfers to this account. - *

- * The mapping from the ILP address to the local ledger address is dependent on the ledger / ledger plugin. An ILP - * address could be the ., or a token could be used in place of the actual - * account name or number. - *

- * Example Return Value: "us.fed.some-bank.my-account" - * - * @return - */ - String getAccount(); - - // TODO: Implement the stuff below! - - -// /** -// * Returns true if and only if this ledger plugin can connect to the ledger described by the authentication data -// * provided. -// *

-// * Ledger plugins are queried in precedence order and the first plugin that returns true for this method will be -// * used to talk to the given ledger. -// * -// * @return -// */ -// Boolean canConnect(String auth); -// -// /** -// * Initiate ledger event subscriptions. -// *

-// * Once connect is called the ledger plugin MUST attempt to subscribe to and report ledger events. Once the -// * connection is established, the ledger plugin should emit the connect event. If the connection is lost, the ledger -// * plugin SHOULD emit the disconnect event. -// */ -// void connect(); -// -// void disconnect(); -// -// Boolean isConnected(); -// -// LedgerInfo getLedgerInfo(); -// -// MonetaryAmount getBalance(); -// -// Collection getConnectors(); -// -// void send(); -// -// /** -// * Submit a fulfillment to a ledger. The ledger plugin or the ledger MUST automatically detect whether the -// * fulfillment is an execution or cancellation condition fulfillment. -// */ -// void fulfillCondition(TransferId transferId, String fulfillment); -// -// /** -// * @param transferId -// * @param replyMessage -// */ -// void replyToTransfer(TransferId transferId, String replyMessage); - -// /** -// * Plugin options are passed in to the LedgerPlugin constructor when a plugin is being instantiated. -// */ -// interface PluginOptions { -// /** -// * A JSON object that encapsulates authentication information and ledger properties. The format of this object -// * is specific to each ledger plugin. -// *

-// * For example: -// *

-//         * {
-//         *  "account": "https://red.ilpdemo.org/ledger/accounts/alice",
-//         *  "password": "alice"
-//         * }
-//         * 
-// */ -// String getAuth(); -// -// /** -// * Provides callback hooks to the host's persistence layer. -// *

-// * Persistence MAY be required for internal use by some ledger plugins. For this purpose hosts MAY be configured -// * with a persistence layer. -// *

-// * Method names are based on the popular LevelUP/LevelDOWN packages. -// */ -// Optional getStore(); -// } -// -// -// /** -// * An interface for handling ILP events. -// */ -// interface EventHandler { -// -// /** -// * Handle the event {@code EventType#CONNECT}. -// * -// * @param event An instance of {@link Event} that contains an instance of type {@link EventType}. -// * @param -// */ -// void onConnect(Event event); -// -// /** -// * Handle the event {@code EventType#DISCONNECT}. -// * -// * @param event An instance of {@link Event} that contains an instance of type {@link EventType}. -// * @param -// */ -// void onDisconnect(Event event); -// -// /** -// * Helper method to set the handler for "connect" events. -// * -// * @param c An instance of {@link Consumer}. -// */ -// void setConnectHandler(Consumer c); -// -// /** -// * Helper method to set the handler for "connect" events. -// * -// * @param c An instance of {@link Consumer}. -// */ -// void setDisconnectHandler(Consumer c); -// } -// -// enum EventType { -// -// CONNECT("event.connect"), -// -// DISCONNECT("event.disconnect"), -// -// ERROR("event.error"); -// -// @Getter -// private final String value; -// -// EventType(final String value) { -// this.value = value; -// } -// -// @Override -// public String toString() { -// return this.value; -// } -// } -} - diff --git a/src/main/java/money/fluid/ilp/connector/services/quoting/impl/InfiniteLiquidityQuoteService.java b/src/main/java/money/fluid/ilp/connector/services/quoting/impl/InfiniteLiquidityQuoteService.java index ad44e0c..0b5f585 100644 --- a/src/main/java/money/fluid/ilp/connector/services/quoting/impl/InfiniteLiquidityQuoteService.java +++ b/src/main/java/money/fluid/ilp/connector/services/quoting/impl/InfiniteLiquidityQuoteService.java @@ -1,274 +1,255 @@ package money.fluid.ilp.connector.services.quoting.impl; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import money.fluid.ilp.connector.model.quotes.Credit; -import money.fluid.ilp.connector.services.ledgers.LedgerLookupService; import money.fluid.ilp.connector.services.quoting.QuoteService; -import money.fluid.ilp.connector.services.quoting.meta.MeService; -import money.fluid.ilp.connector.exceptions.InsufficientFundsException; -import money.fluid.ilp.connector.model.Account; -import money.fluid.ilp.connector.model.quotes.Quote; -import money.fluid.ilp.connector.model.quotes.QuoteRequest; -import money.fluid.ilp.connector.model.quotes.Transaction; -import money.fluid.ilp.connector.model.quotes.Transfer; -import money.fluid.ilp.connector.model.quotes.TransferRequest; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; -import javax.money.MonetaryAmount; -import java.math.BigDecimal; -import java.util.Collection; -import java.util.Objects; -import java.util.Optional; - /** * An implementation of {@link QuoteService} that operates as if it has infinite liquidity for quoting purposes. In * other words, this service will always return a valid quote that includes a 2% service fee. */ @Service @Qualifier("Inifinite") -public class InfiniteLiquidityQuoteService implements QuoteService { - - private final MeService meService; - private final LedgerLookupService ledgerLookupService; - - /** - * Required args Constructor. - * - * @param meService An instance of {@link MeService}. - * @param ledgerLookupService - */ - public InfiniteLiquidityQuoteService(final MeService meService, final LedgerLookupService ledgerLookupService) { - this.meService = Objects.requireNonNull(meService); - this.ledgerLookupService = Objects.requireNonNull(ledgerLookupService); - } - - /** - * The main entry-point for creating a quote for the supplied transfer requests. This method must do several - * things: - *

- *

  1. Determine which accounts on the sender and destination are owned by the connector.
  2. Determine - * which account (sender or destination) will collect the commission.
- *

- * Note: Asset accounts are increased with a "debit" and decreased with a "credit". - * - * @param sourceTransferRequest An instance of {@link Transfer} with information about the source of the asset - * being transferred. - * @param destinationTransferRequest An instance of {@link Transfer} with information about the destination of the - * asset being transferred. - * @return - */ - //@Override - public Quote getQuote( - final TransferRequest sourceTransferRequest, final TransferRequest destinationTransferRequest - ) { - - Objects.requireNonNull(sourceTransferRequest); - Objects.requireNonNull(destinationTransferRequest); - - final Collection sourceTransfers; - final Collection destinationTransfers; - - // Take a commission from the source first, and advertise where commission payments will go? - if (sourceTransferRequest.getOptAmount().isPresent()) { - - // This amount requested by the transferor to send to a transferee. - final MonetaryAmount sourceTransferAmount = sourceTransferRequest.getOptAmount().get(); - - - // Get the Account on the source ledger. - - final Optional optCommissionAccount = this.ledgerLookupService.getAccountForLedger( - sourceTransferRequest.getLedgerId()); - if (!optCommissionAccount.isPresent()) { - throw new InsufficientFundsException("This connector has no Account on the requested source Ledger!"); - } else { - final Account commissionAccount = optCommissionAccount.get(); - - // Calculate the commission based upon the sender amount. - final MonetaryAmount commission = sourceTransferRequest.getOptAmount().get().multiply( - DEFAULT_COMMISSION_RATE); - - - // Make 2 source transfers (1 for the real transfer and 1 for the commission) and 1 destination transfer. +public class InfiniteLiquidityQuoteService { // implements QuoteService { - // Add to this asset in order to capture the commission. - // TODO: FIXME! - final Credit sourceCommisionDebit = null; - //new Credit.Builder().withAccountId(commissionAccount.getLedgerAccountId()).withAmount(commission).build(); - - final MonetaryAmount adjustedSourceTransferAmount = this.computeSourceCreditAfterCommission( - sourceTransferAmount, commission); - - - // Reduce this asset in order to transfer funds. - // TODO: FIXME! - final Credit sourceCredit = null; - //new Credit.Builder().withAccountId( - // this.ledgerLookupService.getAccountForLedger(sourceTransferRequest.getLedgerId())).withAmount( - // adjustedSourceTransferAmount).build(); - - -// // TODO: The accountId of the source transfer should probably be available. However, if this connector -// // doesn't need to know that information, then consider using a debitQuote object that doesn't have this value as opposed to a Debit object. +// private final MeService meService; // -// // The reduction of the asset in the sender's ledger. -// final Credit commissionCredit = new Credit.Builder().withAmount(commission).withAccountId(null).build(); -// final Debit commissionDebit = new Debit.Builder().withAmount(commission).withAccountId(this.meService.get).build(); +// /** +// * Required args Constructor. +// * +// * @param meService An instance of {@link MeService}. +// * @param ledgerLookupService +// */ +// public InfiniteLiquidityQuoteService(final MeService meService, final LedgerLookupService ledgerLookupService) { +// this.meService = Objects.requireNonNull(meService); +// this.ledgerLookupService = Objects.requireNonNull(ledgerLookupService); +// } // -// // Adjust the source amount down to account for the commission being taken out. +// /** +// * The main entry-point for creating a quote for the supplied transfer requests. This method must do several +// * things: +// *

+// *

  1. Determine which accounts on the sender and destination are owned by the connector.
  2. Determine +// * which account (sender or destination) will collect the commission.
+// *

+// * Note: Asset accounts are increased with a "debit" and decreased with a "credit". +// * +// * @param sourceTransferRequest An instance of {@link Transfer} with information about the source of the asset +// * being transferred. +// * @param destinationTransferRequest An instance of {@link Transfer} with information about the destination of the +// * asset being transferred. +// * @return +// */ +// //@Override +// public Quote getQuote( +// final TransferRequest sourceTransferRequest, final TransferRequest destinationTransferRequest +// ) { // +// Objects.requireNonNull(sourceTransferRequest); +// Objects.requireNonNull(destinationTransferRequest); // -// final Debit sourceTransferDebit = new Debit.Builder().withAmount() - - // The commission source will be from the original source of funds. - - // The amount of asset to add to the source connector asset account, as a Credit - // TODO: FIXME! - final Credit commissionDebit = null; - //new Credit.Builder().withAmount(commission).withAccountId(null).build(); - - //TODO FIXME! - Object commissionCredit = null; - final Transfer commissionSourceTransfer = new Transfer.Builder(null) - //.withLedgerId(sourceTransferRequest.getLedgerId()) - //.withExpiryDuration(sourceTransferRequest.getOptExpiryDuration()) - //.withCredits(commissionCredit) - //.withAmount(Optional.of(commission)) - .build(); - - // This originalTransfer will go to this connector's ledger account used for collecting commissions. - // TODO: FIXME! - final Transfer commissionDestinationTransfer = new Transfer.Builder(null) - //.withLedgerId(this.meService.getLedgerId()) - //.withAmount(Optional.of(commission)) - //.withExpiryDuration(Optional.of(this.meService.getDefaultExpiration())) - .build(); - - - final Transfer adjustedSourceTransfer = this.reduceTransferForCommission( - sourceTransferRequest, commission); - sourceTransfers = ImmutableList.of(commissionSourceTransfer, adjustedSourceTransfer); - // No adjustment for the destination transfer - destinationTransfers = ImmutableList.of( - commissionDestinationTransfer.toTransfer(), destinationTransferRequest.toTransfer()); - - - } - - - } else if (destinationTransferRequest.getOptAmount().isPresent()) { - - // Calculate the commission based upon the receiver amount. - final MonetaryAmount commission = destinationTransferRequest.getOptAmount().get().multiply( - DEFAULT_COMMISSION_RATE); - - // TODO: How to compute a source and destination???? - - // This commission will come from the destination of funds. - // TODO: FIXME! - final Transfer commissionSourceTransfer = null; -// new Transfer.Builder() -// .withLedgerId(destinationTransferRequest.getLedgerId()) -// .withAmount(Optional.of(commission)) -// .withExpiryDuration(destinationTransferRequest.getOptExpiryDuration()) +// final Collection sourceTransfers; +// final Collection destinationTransfers; +// +// // Take a commission from the source first, and advertise where commission payments will go? +// if (sourceTransferRequest.getOptAmount().isPresent()) { +// +// // This amount requested by the transferor to send to a transferee. +// final MonetaryAmount sourceTransferAmount = sourceTransferRequest.getOptAmount().get(); +// +// +// // Get the Account on the source ledger. +// +// final Optional optCommissionAccount = this.ledgerLookupService.getAccountForLedger( +// sourceTransferRequest.getLedgerId()); +// if (!optCommissionAccount.isPresent()) { +// throw new InsufficientFundsException("This connector has no Account on the requested source Ledger!"); +// } else { +// final Account commissionAccount = optCommissionAccount.get(); +// +// // Calculate the commission based upon the sender amount. +// final MonetaryAmount commission = sourceTransferRequest.getOptAmount().get().multiply( +// DEFAULT_COMMISSION_RATE); +// +// +// // Make 2 source transfers (1 for the real transfer and 1 for the commission) and 1 destination transfer. +// +// // Add to this asset in order to capture the commission. +// // TODO: FIXME! +// final Credit sourceCommisionDebit = null; +// //new Credit.Builder().withAccountId(commissionAccount.getLedgerAccountId()).withAmount(commission).build(); +// +// final MonetaryAmount adjustedSourceTransferAmount = this.computeSourceCreditAfterCommission( +// sourceTransferAmount, commission); +// +// +// // Reduce this asset in order to transfer funds. +// // TODO: FIXME! +// final Credit sourceCredit = null; +// //new Credit.Builder().withAccountId( +// // this.ledgerLookupService.getAccountForLedger(sourceTransferRequest.getLedgerId())).withAmount( +// // adjustedSourceTransferAmount).build(); +// +// +//// // TODO: The accountId of the source transfer should probably be available. However, if this connector +//// // doesn't need to know that information, then consider using a debitQuote object that doesn't have this value as opposed to a Debit object. +//// +//// // The reduction of the asset in the sender's ledger. +//// final Credit commissionCredit = new Credit.Builder().withAmount(commission).withAccountId(null).build(); +//// final Debit commissionDebit = new Debit.Builder().withAmount(commission).withAccountId(this.meService.get).build(); +//// +//// // Adjust the source amount down to account for the commission being taken out. +//// +//// +//// final Debit sourceTransferDebit = new Debit.Builder().withAmount() +// +// // The commission source will be from the original source of funds. +// +// // The amount of asset to add to the source connector asset account, as a Credit +// // TODO: FIXME! +// final Credit commissionDebit = null; +// //new Credit.Builder().withAmount(commission).withAccountId(null).build(); +// +// //TODO FIXME! +// Object commissionCredit = null; +// final Transfer commissionSourceTransfer = new Transfer.Builder(null) +// //.withLedgerId(sourceTransferRequest.getLedgerId()) +// //.withExpiryDuration(sourceTransferRequest.getOptExpiryDuration()) +// //.withCredits(commissionCredit) +// //.withAmount(Optional.of(commission)) +// .build(); +// +// // This originalTransfer will go to this connector's ledger account used for collecting commissions. +// // TODO: FIXME! +// final Transfer commissionDestinationTransfer = new Transfer.Builder(null) +// //.withLedgerId(this.meService.getLedgerId()) +// //.withAmount(Optional.of(commission)) +// //.withExpiryDuration(Optional.of(this.meService.getDefaultExpiration())) +// .build(); +// +// +// final Transfer adjustedSourceTransfer = this.reduceTransferForCommission( +// sourceTransferRequest, commission); +// sourceTransfers = ImmutableList.of(commissionSourceTransfer, adjustedSourceTransfer); +// // No adjustment for the destination transfer +// destinationTransfers = ImmutableList.of( +// commissionDestinationTransfer.toTransfer(), destinationTransferRequest.toTransfer()); +// +// +// } +// +// +// } else if (destinationTransferRequest.getOptAmount().isPresent()) { +// +// // Calculate the commission based upon the receiver amount. +// final MonetaryAmount commission = destinationTransferRequest.getOptAmount().get().multiply( +// DEFAULT_COMMISSION_RATE); +// +// // TODO: How to compute a source and destination???? +// +// // This commission will come from the destination of funds. +// // TODO: FIXME! +// final Transfer commissionSourceTransfer = null; +//// new Transfer.Builder() +//// .withLedgerId(destinationTransferRequest.getLedgerId()) +//// .withAmount(Optional.of(commission)) +//// .withExpiryDuration(destinationTransferRequest.getOptExpiryDuration()) +//// .build(); +// +// // This commission will go to the destination connector's ledger account used for collecting commissions, which will have an account owned by the owner of this connector. +// // TODO: FIXME! +// final Transfer commissionDestinationTransfer = new Transfer.Builder(null) +// //.withLedgerId(destinationTransferRequest.getLedgerId()) +// //.withAmount(Optional.of(commission)) +// //.withExpiryDuration(destinationTransferRequest.getOptExpiryDuration()) // .build(); - - // This commission will go to the destination connector's ledger account used for collecting commissions, which will have an account owned by the owner of this connector. - // TODO: FIXME! - final Transfer commissionDestinationTransfer = new Transfer.Builder(null) - //.withLedgerId(destinationTransferRequest.getLedgerId()) - //.withAmount(Optional.of(commission)) - //.withExpiryDuration(destinationTransferRequest.getOptExpiryDuration()) - .build(); - - - // Adjust the source amount down to account for the commission being taken out. - final Transfer adjustedSourceTransfer = this.reduceTransferForCommission(sourceTransferRequest, commission); - sourceTransfers = ImmutableList.of(commissionSourceTransfer, adjustedSourceTransfer); - - // No adjustment for the destination transfer - // TODO: FIXME! - destinationTransfers = ImmutableList.of( - commissionDestinationTransfer.toTransfer(), destinationTransferRequest.toTransfer()); - - - } else { - throw new RuntimeException("Either the source or destination amount must be specified!"); - } - - - // FIXME! - Transaction transaction = null; - return new Quote.Builder(transaction) - //.withSourceTransfers(sourceTransfers) - //.withDestinationTransfers(destinationTransfers) - .build(); - } - - // TODO: FIXME! - private Transfer reduceTransferForCommission(TransferRequest sourceTransferRequest, MonetaryAmount commission) { - return null; - } - - private MonetaryAmount computeSourceCreditAfterCommission( - final MonetaryAmount transferAmount, final MonetaryAmount commission - ) { - Objects.requireNonNull(transferAmount); - Objects.requireNonNull(commission); - - // The original transfer minus the commission. - final MonetaryAmount adjustedTransferAmount = transferAmount.subtract(commission); - - if (adjustedTransferAmount.isNegativeOrZero()) { - throw new InsufficientFundsException( - "Not enough money in the source transfer to charge a commission and still make a transfer!"); - } else { - return adjustedTransferAmount; - } - } - - - // 1% Commission - private static final BigDecimal DEFAULT_COMMISSION_RATE = new BigDecimal("0.01"); - - /** - * Calculates a commission for an Interledger originalTransfer. For this implementation, the commission amount is a - * fixed 1% of the source or destination amount. - * - * @param optSourceAmount - * @param optDestinationAmount - * @return - */ - @VisibleForTesting - MonetaryAmount calculateCommission( - final Optional optSourceAmount, - final Optional optDestinationAmount - ) { - - Objects.requireNonNull(optSourceAmount); - Objects.requireNonNull(optDestinationAmount); - - if (optSourceAmount.isPresent()) { - return optSourceAmount.get().multiply(DEFAULT_COMMISSION_RATE); - } else if (optDestinationAmount.isPresent()) { - - } else { - throw new RuntimeException("Either the source or destination amount must be specified!"); - } - - //TODO: FIXME! - return null; - } - - // TODO: IMPLEMENT ME? - @Override - public Quote getQuote( - QuoteRequest sourceQuoteRequest, QuoteRequest destinationQuoteRequest - ) { - return null; - } +// +// +// // Adjust the source amount down to account for the commission being taken out. +// final Transfer adjustedSourceTransfer = this.reduceTransferForCommission(sourceTransferRequest, commission); +// sourceTransfers = ImmutableList.of(commissionSourceTransfer, adjustedSourceTransfer); +// +// // No adjustment for the destination transfer +// // TODO: FIXME! +// destinationTransfers = ImmutableList.of( +// commissionDestinationTransfer.toTransfer(), destinationTransferRequest.toTransfer()); +// +// +// } else { +// throw new RuntimeException("Either the source or destination amount must be specified!"); +// } +// +// +// // FIXME! +// Transaction transaction = null; +// return new Quote.Builder(transaction) +// //.withSourceTransfers(sourceTransfers) +// //.withDestinationTransfers(destinationTransfers) +// .build(); +// } +// +// // TODO: FIXME! +// private Transfer reduceTransferForCommission(TransferRequest sourceTransferRequest, MonetaryAmount commission) { +// return null; +// } +// +// private MonetaryAmount computeSourceCreditAfterCommission( +// final MonetaryAmount transferAmount, final MonetaryAmount commission +// ) { +// Objects.requireNonNull(transferAmount); +// Objects.requireNonNull(commission); +// +// // The original transfer minus the commission. +// final MonetaryAmount adjustedTransferAmount = transferAmount.subtract(commission); +// +// if (adjustedTransferAmount.isNegativeOrZero()) { +// throw new InsufficientFundsException( +// "Not enough money in the source transfer to charge a commission and still make a transfer!"); +// } else { +// return adjustedTransferAmount; +// } +// } +// +// +// // 1% Commission +// private static final BigDecimal DEFAULT_COMMISSION_RATE = new BigDecimal("0.01"); +// +// /** +// * Calculates a commission for an Interledger originalTransfer. For this implementation, the commission amount is a +// * fixed 1% of the source or destination amount. +// * +// * @param optSourceAmount +// * @param optDestinationAmount +// * @return +// */ +// @VisibleForTesting +// MonetaryAmount calculateCommission( +// final Optional optSourceAmount, +// final Optional optDestinationAmount +// ) { +// +// Objects.requireNonNull(optSourceAmount); +// Objects.requireNonNull(optDestinationAmount); +// +// if (optSourceAmount.isPresent()) { +// return optSourceAmount.get().multiply(DEFAULT_COMMISSION_RATE); +// } else if (optDestinationAmount.isPresent()) { +// +// } else { +// throw new RuntimeException("Either the source or destination amount must be specified!"); +// } +// +// //TODO: FIXME! +// return null; +// } +// +// // TODO: IMPLEMENT ME? +// @Override +// public Quote getQuote( +// QuoteRequest sourceQuoteRequest, QuoteRequest destinationQuoteRequest +// ) { +// return null; +// } // public abstract class QuoteHandler { diff --git a/src/main/java/money/fluid/ilp/connector/services/routing/RoutingService.java b/src/main/java/money/fluid/ilp/connector/services/routing/RoutingService.java index 8d2d432..c17618b 100644 --- a/src/main/java/money/fluid/ilp/connector/services/routing/RoutingService.java +++ b/src/main/java/money/fluid/ilp/connector/services/routing/RoutingService.java @@ -16,20 +16,20 @@ public interface RoutingService { *

* NOTE: A connector may advertise more than a single route for a given destination prefix. * - * @param destinationLedgerAddressPrefix The {@link LedgerAddressPrefix} of the ledger/ledger type that this route - * will route towards. This might be an entire {@link IlpAddress}, such - * as "fed.us.chase", or it might just be an ILP address prefix, like "fed" - * or "fed.us". - * @param nextHopIlpAddressForConnector The ILP {@link IlpAddress} of the connector account on a given ledger - * that can accept funds to complete an ILP transfer. In other words, this - * is the "source account" on the indicated ledger that the next-hop ledger - * will use to initiate the next hop ILP transaction to complete this - * overall transfer. - * @param routeRate An instance of {@link RouteRate} that provides concrete pricing for a - * pair of ledgers. For example, if a route from A->C can be serviced via - * ledgers A and B, then A and B would be the source ledgers, respectively. - * The ultimate "rate" for a given Route is based upon multiple pieces of - * data points such as the overall transfer amount, execution speed, etc. + * @param destinationLedgerAddressPrefix The {@link LedgerAddressPrefix} of the ledger/ledger type that this route + * will route towards. This might be an entire {@link IlpAddress}, such as + * "fed.us.chase", or it might just be an ILP address prefix, like "fed" or + * "fed.us". + * @param nextHopIlpAddressForConnector The ILP {@link IlpAddress} of the connector account on a given ledger that + * can accept funds to complete an ILP transfer. In other words, this is the + * "source account" on the indicated ledger that the next-hop ledger will use + * to initiate the next hop ILP transaction to complete this overall + * transfer. + * @param routeRate An instance of {@link RouteRate} that provides concrete pricing for a pair + * of ledgers. For example, if a route from A->C can be serviced via ledgers + * A and B, then A and B would be the source ledgers, respectively. The + * ultimate "rate" for a given Route is based upon multiple pieces of data + * points such as the overall transfer amount, execution speed, etc. */ void addRoute( final LedgerAddressPrefix destinationLedgerAddressPrefix, @@ -78,6 +78,12 @@ Optional bestHopForDestinationAmount( final IlpAddress destinationAddress, final MonetaryAmount destinationAmount ); + // Any amount...returns if there's at least a route available... + Optional bestHopForDestinationAmount( + final IlpAddress destinationAddress + ); + + // TODO: Create unit tests for this implementation! //class Default implements RoutingService { diff --git a/src/main/java/money/fluid/ilp/connector/services/transfers/TransferService.java b/src/main/java/money/fluid/ilp/connector/services/transfers/TransferService.java deleted file mode 100644 index 7aa9dab..0000000 --- a/src/main/java/money/fluid/ilp/connector/services/transfers/TransferService.java +++ /dev/null @@ -1,36 +0,0 @@ -package money.fluid.ilp.connector.services.transfers; - -/** - * This interface defines a contract for initiating and completing ledger transfers, both for transfers that can be - * serviced by this connector, as well as transfers that must be serviced by multiple connectors (i.e., a multi-hop - * payment). - *

- * In ILP, a connector will facilitate an interledger payment upon receiving a notification for a transfer in which it - * is credited. That "source" transfer must have a ilp_header in its credit's memo that specifies the payment's - * destination and amount. As soon as the source transfer is prepared, the connector will authorize the debits from its - * account(s) on the destination ledger. - */ -public interface TransferService { - - - /** - * Begin the process of initiating a transfer. This includes the following steps: - *

- *

-     *  
    - *
  1. Create a local ledger transfer, including the cryptographic condition, and authorize this transfer on - * the local ledger.
  2. - *
  3. Wait for the local ledger to put the sender's funds on hold and notify this connector that this has been - * completed.
  4. - *
  5. Receive the notification from the Ledger, and extract the ILP packet to determine if the payment should - * be forwarded.
  6. - * - *
- *
- */ - void initiateTransfer(); - - - void forwardPayment(); - -} diff --git a/src/main/java/money/fluid/ilp/ledger/EscrowManager.java b/src/main/java/money/fluid/ilp/ledger/EscrowManager.java index eb52c7b..f8c90db 100644 --- a/src/main/java/money/fluid/ilp/ledger/EscrowManager.java +++ b/src/main/java/money/fluid/ilp/ledger/EscrowManager.java @@ -1,56 +1,16 @@ package money.fluid.ilp.ledger; import money.fluid.ilp.connector.model.ids.IlpTransactionId; -import money.fluid.ilp.connector.model.ids.LedgerAccountId; import money.fluid.ilp.ledger.inmemory.exceptions.EscrowException; import money.fluid.ilp.ledger.inmemory.model.Escrow; import money.fluid.ilp.ledger.inmemory.model.EscrowInputs; -import org.interledgerx.ilp.core.IlpAddress; -import org.interledgerx.ilp.core.LedgerInfo; - -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; /** - * A class that manages escrow for a Ledger. A ledger will hold some assets in escrow that, when executed, will be + * A service that manages escrow for a Ledger. A ledger will hold some assets in escrow that, when executed, will be * credited to the target account for an escrow. Conversely, if an escrow is reversed, then the assets will be credited * back to the source account for that escrow. - *

- * This implementation allows for only a single-escrow account per ledger. However, more complicated implementations - * might allow for a more advanced mapping between escrow source-accounts, escrow accounts, and escrow destination - * accounts. */ -// TODO: Make this an interface! -public class EscrowManager { - - // TODO A proper ledger will want to track the various states of the initiateEscrow for auditing. - - private final LedgerInfo ledgerInfo; - - // The main ledger to move funds around in... - private final LedgerAccountManager ledgerAccountManager; - - // The ILP account to add and remove escrow from... - private final IlpAddress escrowAccountAddress; - - // TODO: Is this necessary? A real ledger would always want to have an accounting of where the money in its initiateEscrow account has come from at any given time. This could be modeled as another "ledger", or it could merely be an audit log? - - // Indicates any amounts that are currently in initiateEscrow from a given source ledger account. - private final Map escrows = new ConcurrentHashMap(); - - public EscrowManager( - final LedgerInfo ledgerInfo, final LedgerAccountId escrowAccountId, - final LedgerAccountManager ledgerAccountManager - ) { - this.ledgerInfo = Objects.requireNonNull(ledgerInfo); - this.ledgerAccountManager = Objects.requireNonNull(ledgerAccountManager); - - // __escrow__ account! - this.escrowAccountAddress = IlpAddress.of(escrowAccountId, ledgerInfo.getLedgerId()); - } - +public interface EscrowManager { /** * Create an initiateEscrow transaction by debiting an {@code amount} of the associated ledger's asset from {@code * {@link EscrowInputs#getSourceAddress()} and crediting the same amount into the initiateEscrow account for the @@ -60,20 +20,7 @@ public EscrowManager( * initiateEscrow transaction. * @return */ - public Escrow initiateEscrow(final EscrowInputs escrowInputs) { - Objects.requireNonNull(escrowInputs); - - // TODO: Make this operation atomic. If either fails, the initiateEscrow would be corrupted! - - // 1. Debit the sender's account - ledgerAccountManager.debitAccount(escrowInputs.getSourceAddress(), escrowInputs.getAmount()); - // 2. Credit the initiateEscrow account for the sourceAccountId, and put money in there for holding... - ledgerAccountManager.creditAccount(this.escrowAccountAddress, escrowInputs.getAmount()); - // 3. Add the initiateEscrow to the map for later storage. - final Escrow escrow = new Escrow(escrowInputs, escrowAccountAddress); - this.escrows.put(escrowInputs.getInterledgerPacketHeader().getIlpTransactionId(), escrow); - return escrow; - } + Escrow initiateEscrow(final EscrowInputs escrowInputs); /** * For a given pending escrow transaction identified by {@code ilpTransactionId}, execute the escrow by crediting @@ -84,24 +31,7 @@ public Escrow initiateEscrow(final EscrowInputs escrowInputs) { * @return * @throws EscrowException if the escrow execution failed for any reason. */ - public Escrow executeEscrow(final IlpTransactionId ilpTransactionId) { - Objects.requireNonNull(ilpTransactionId); - - // TODO: Make this operation atomic. If either fails, the initiateEscrow would be corrupted! - - return Optional.ofNullable(this.escrows.get(ilpTransactionId)) - .map(escrow -> { - // 1. Debit the sender's account - ledgerAccountManager.debitAccount( - this.escrowAccountAddress, - escrow.getAmount() - ); - // 2. Credit the initiateEscrow account. - ledgerAccountManager.creditAccount(escrow.getLocalDestinationAddress(), escrow.getAmount()); - return this.escrows.remove(escrow.getIlpPacketHeader().getIlpTransactionId()); - }) - .orElseThrow(() -> new EscrowException("No escrow existed for ILPTransaction: " + ilpTransactionId)); - } + Escrow executeEscrow(final IlpTransactionId ilpTransactionId); /** * For a given pending escrow transaction identified by {@code ilpTransactionId}, reverse the escrow by crediting @@ -112,22 +42,5 @@ public Escrow executeEscrow(final IlpTransactionId ilpTransactionId) { * @return * @throws EscrowException if the escrow execution failed for any reason. */ - public Escrow reverseEscrow(final IlpTransactionId ilpTransactionId) { - Objects.requireNonNull(ilpTransactionId); - - // TODO: Make this operation atomic. If either fails, the initiateEscrow would be corrupted! - - return Optional.ofNullable(this.escrows.get(ilpTransactionId)) - .map(escrow -> { - // 1. Debit the sender's account - ledgerAccountManager.debitAccount( - this.escrowAccountAddress, - escrow.getAmount() - ); - // 2. Credit the initiateEscrow account. - ledgerAccountManager.creditAccount(escrow.getLocalSourceAddress(), escrow.getAmount()); - return this.escrows.remove(escrow.getIlpPacketHeader().getIlpTransactionId()); - }) - .orElseThrow(() -> new EscrowException("No escrow existed for ILPTransaction: " + ilpTransactionId)); - } + Escrow reverseEscrow(final IlpTransactionId ilpTransactionId); } diff --git a/src/main/java/money/fluid/ilp/ledger/LedgerAccountManager.java b/src/main/java/money/fluid/ilp/ledger/LedgerAccountManager.java index ceb5abd..5183f31 100644 --- a/src/main/java/money/fluid/ilp/ledger/LedgerAccountManager.java +++ b/src/main/java/money/fluid/ilp/ledger/LedgerAccountManager.java @@ -1,7 +1,6 @@ package money.fluid.ilp.ledger; -import money.fluid.ilp.connector.model.ids.ConnectorId; import money.fluid.ilp.ledger.inmemory.exceptions.InvalidAccountException; import money.fluid.ilp.ledger.inmemory.model.SimpleLedgerAccount; import money.fluid.ilp.ledger.model.LedgerAccount; diff --git a/src/main/java/money/fluid/ilp/ledger/inmemory/EscrowExpirationHandler.java b/src/main/java/money/fluid/ilp/ledger/inmemory/EscrowExpirationHandler.java new file mode 100644 index 0000000..dd1b4dd --- /dev/null +++ b/src/main/java/money/fluid/ilp/ledger/inmemory/EscrowExpirationHandler.java @@ -0,0 +1,19 @@ +package money.fluid.ilp.ledger.inmemory; + +import money.fluid.ilp.ledger.inmemory.model.Escrow; + +/** + * An internal-use-only interface that connects Guava cache timeouts to this EscrowManager. Only really valid for this + * implementation. + *

+ * NOTE: This interface is purposefully not part of EscrowManager because this interface only exists for the in-memory + * Ledger impl. + */ +interface EscrowExpirationHandler { + /** + * Called when an instance of {@link Escrow} has timed-out and is no longer valid. + * + * @param expiredEscrow + */ + void onEscrowTimedOut(final Escrow expiredEscrow); +} diff --git a/src/main/java/money/fluid/ilp/ledger/inmemory/InMemoryEscrowManager.java b/src/main/java/money/fluid/ilp/ledger/inmemory/InMemoryEscrowManager.java new file mode 100644 index 0000000..d964d5a --- /dev/null +++ b/src/main/java/money/fluid/ilp/ledger/inmemory/InMemoryEscrowManager.java @@ -0,0 +1,192 @@ +package money.fluid.ilp.ledger.inmemory; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.RemovalCause; +import com.google.common.cache.RemovalListener; +import com.google.common.cache.RemovalNotification; +import lombok.Getter; +import money.fluid.ilp.connector.model.ids.IlpTransactionId; +import money.fluid.ilp.connector.model.ids.LedgerAccountId; +import money.fluid.ilp.ledger.EscrowManager; +import money.fluid.ilp.ledger.LedgerAccountManager; +import money.fluid.ilp.ledger.inmemory.exceptions.EscrowException; +import money.fluid.ilp.ledger.inmemory.model.Escrow; +import money.fluid.ilp.ledger.inmemory.model.EscrowInputs; +import org.interledgerx.ilp.core.IlpAddress; +import org.interledgerx.ilp.core.LedgerInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; + +/** + * An in-memory implementation of {@link EscrowManager} that tracks Escrow without any sort of data persistence + * (meaning, all escrows go away when the runtime process terminates). + *

+ * This implementation allows for only a single-escrow account per ledger. However, more complicated implementations + * might allow for a more advanced mapping between escrow source-accounts, escrow accounts, and escrow destination + * accounts. + *

+ * WARNING: This implementation should not be used in a production environment since it does NOT utilize a + * persistent datastore to store escrow information. + */ +@Getter +public class InMemoryEscrowManager implements EscrowManager, RemovalListener, EscrowExpirationHandler { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + // TODO A proper ledger will want to track the various states of the initiateEscrow for auditing. + + private final LedgerInfo ledgerInfo; + + // The main ledger to move funds around in... + private final LedgerAccountManager ledgerAccountManager; + + // The ILP account to add and remove escrow from... + private final IlpAddress escrowAccountAddress; + + // Indicates any amounts that are currently in initiateEscrow from a given source ledger account. + //private final Map escrows = new ConcurrentHashMap(); + private Cache escrows; + private volatile EscrowExpirationHandler escrowExpirationHandler; + + public InMemoryEscrowManager( + final LedgerInfo ledgerInfo, + final LedgerAccountId escrowAccountId, + final LedgerAccountManager ledgerAccountManager, + final CacheBuilder escrowCacheBuilder + ) { + this.ledgerInfo = Objects.requireNonNull(ledgerInfo); + this.ledgerAccountManager = Objects.requireNonNull(ledgerAccountManager); + + // __escrow__ account! + this.escrowAccountAddress = IlpAddress.of(escrowAccountId, ledgerInfo.getLedgerId()); + + this.escrows = Objects.requireNonNull(escrowCacheBuilder) + .removalListener(this) + .build(); + this.escrowExpirationHandler = this; + } + + /** + * Create an initiateEscrow transaction by debiting an {@code amount} of the associated ledger's asset from {@code + * {@link EscrowInputs#getSourceAddress()} and crediting the same amount into the initiateEscrow account for the + * associated ledger. + * + * @param escrowInputs An instance of {@link EscrowInputs} with all information required to initiate an + * initiateEscrow transaction. + * @return + */ + public Escrow initiateEscrow(final EscrowInputs escrowInputs) { + Objects.requireNonNull(escrowInputs); + + // WARNING: This operation is notatomic. If either fails, the initiateEscrow would be corrupted! + + // 1. Debit the sender's account + ledgerAccountManager.debitAccount(escrowInputs.getSourceAddress(), escrowInputs.getAmount()); + // 2. Credit the initiateEscrow account for the sourceAccountId, and put money in there for holding... + ledgerAccountManager.creditAccount(this.escrowAccountAddress, escrowInputs.getAmount()); + // 3. Add the initiateEscrow to the map for later storage. + final Escrow escrow = new Escrow(escrowInputs, escrowAccountAddress); + this.escrows.put(escrowInputs.getInterledgerPacketHeader().getIlpTransactionId(), escrow); + return escrow; + } + + /** + * For a given pending escrow transaction identified by {@code ilpTransactionId}, execute the escrow by crediting + * {@code amount} to the account identified by {@link Escrow#getLocalDestinationAddress()} and debiting an identical + * amount from this ledger's escrow holding account. + * + * @param ilpTransactionId An instance of {@link IlpTransactionId} that identifies the pending escrow transaction. + * @return + * @throws EscrowException if the escrow execution failed for any reason. + */ + public Escrow executeEscrow(final IlpTransactionId ilpTransactionId) { + Objects.requireNonNull(ilpTransactionId); + + // WARNING: This operation is notatomic. If either fails, the initiateEscrow would be corrupted! + + return Optional.ofNullable(this.escrows.getIfPresent(ilpTransactionId)) + .map(escrow -> { + // 1. Debit the sender's account + ledgerAccountManager.debitAccount( + this.escrowAccountAddress, + escrow.getAmount() + ); + // 2. Credit the initiateEscrow account. + ledgerAccountManager.creditAccount(escrow.getLocalDestinationAddress(), escrow.getAmount()); + + this.escrows.invalidate(escrow.getIlpPacketHeader().getIlpTransactionId()); + return escrow; + }) + .orElseThrow(() -> new EscrowException("No escrow existed for ILPTransaction: " + ilpTransactionId)); + } + + /** + * For a given pending escrow transaction identified by {@code ilpTransactionId}, reverse the escrow by crediting + * {@code amount} to the account identified by {@link Escrow#getLocalSourceAddress()} and debiting an identical + * amount from this ledger's escrow holding account. + * + * @param ilpTransactionId An instance of {@link IlpTransactionId} that identifies the pending escrow transaction. + * @return + * @throws EscrowException if the escrow execution failed for any reason. + */ + public Escrow reverseEscrow(final IlpTransactionId ilpTransactionId) { + Objects.requireNonNull(ilpTransactionId); + + // WARNING: This operation is notatomic. If either fails, the initiateEscrow would be corrupted! + + return Optional.ofNullable(this.escrows.getIfPresent(ilpTransactionId)) + .map(escrow -> { + // 1. Debit the sender's account + ledgerAccountManager.debitAccount( + this.escrowAccountAddress, + escrow.getAmount() + ); + // 2. Credit the initiateEscrow account. + ledgerAccountManager.creditAccount(escrow.getLocalSourceAddress(), escrow.getAmount()); + + this.escrows.invalidate(escrow.getIlpPacketHeader().getIlpTransactionId()); + return escrow; + }) + .orElseThrow(() -> new EscrowException("No escrow existed for ILPTransaction: " + ilpTransactionId)); + } + + // Not part of the EscrowManager interface because this only connects the Guava Cache to the EscrowManager. + public void setEscrowExpirationHandler(final EscrowExpirationHandler escrowExpirationHandler) { + this.escrowExpirationHandler = Objects.requireNonNull(escrowExpirationHandler); + } + + // A default implementation. Constructors should initialize this to something useful, if desired. + @Override + public void onEscrowTimedOut(final Escrow timedOutEscrow) { + logger.warn("No escrow timeout handler assigned to {}", this); + } + + /** + * This method will be called by the Guava Cache whenever an entry is evicted. Since the Guava Cache is merely + * an implementation detail of this {@link EscrowManager} implementation, this method merely connects + * the cache to the {@link EscrowExpirationHandler}. + * + * @param notification + */ + @Override + public void onRemoval(final RemovalNotification notification) { + if (notification.getCause().equals(RemovalCause.EXPIRED)) { + logger.info("Ledger {} escrow timed out : {}", this.getLedgerInfo().getLedgerId(), notification); + this.escrowExpirationHandler.onEscrowTimedOut(notification.getValue()); + } else if (notification.getCause().equals(RemovalCause.EXPLICIT)) { + logger.info("Ledger {} escrow explicitely removed : {}", this.getLedgerInfo().getLedgerId(), notification); + } else if (notification.getCause().equals(RemovalCause.SIZE)) { + logger.info("Ledger {} Escrow removed due to SIZE: {}", notification); + } else { + throw new RuntimeException("Unhandled cache eviction: " + notification); + } + } + + @Override + public String toString() { + return this.getLedgerInfo().getLedgerId().getId(); + } +} diff --git a/src/main/java/money/fluid/ilp/ledger/inmemory/InMemoryLedger.java b/src/main/java/money/fluid/ilp/ledger/inmemory/InMemoryLedger.java index 52b77fd..cf0e5bc 100644 --- a/src/main/java/money/fluid/ilp/ledger/inmemory/InMemoryLedger.java +++ b/src/main/java/money/fluid/ilp/ledger/inmemory/InMemoryLedger.java @@ -1,6 +1,8 @@ package money.fluid.ilp.ledger.inmemory; import com.google.common.base.Preconditions; +import com.google.common.base.Ticker; +import com.google.common.cache.CacheBuilder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; @@ -10,7 +12,6 @@ import money.fluid.ilp.connector.model.ids.ConnectorId; import money.fluid.ilp.connector.model.ids.IlpTransactionId; import money.fluid.ilp.connector.model.ids.LedgerAccountId; -import money.fluid.ilp.ledger.EscrowManager; import money.fluid.ilp.ledger.LedgerAccountManager; import money.fluid.ilp.ledger.QuotingService; import money.fluid.ilp.ledger.inmemory.exceptions.AccountNotFoundException; @@ -38,6 +39,8 @@ import org.interledgerx.ilp.core.events.LedgerTransferPreparedEvent; import org.interledgerx.ilp.core.events.LedgerTransferRejectedEvent; import org.interledgerx.ilp.core.exceptions.InsufficientAmountException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.money.MonetaryAmount; import java.util.Collection; @@ -49,6 +52,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; /** * An implementation of {@link Ledger} that simulates a real ledger supporting ILP functionality. Ordinarily, a @@ -56,9 +60,12 @@ * external connectivity. This implementation runs "in-memory", so its event emissions don't need to involve any RPCs. */ @RequiredArgsConstructor +@Getter @ToString @EqualsAndHashCode -public class InMemoryLedger implements Ledger { +public class InMemoryLedger implements Ledger, EscrowExpirationHandler { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); // TODO: In a real ledger, should be configurable. private static final LedgerAccountId ESCROW = LedgerAccountId.of("__escrow__"); @@ -67,7 +74,6 @@ public class InMemoryLedger implements Ledger { private final String name; @NonNull - @Getter private final LedgerInfo ledgerInfo; @NonNull @@ -76,19 +82,20 @@ public class InMemoryLedger implements Ledger { ///////////// ///////////// - @Getter @NonNull private final LedgerAccountManager ledgerAccountManager; // Each Ledger has a ConnectionManager that processes callbacks. - @Getter @NonNull private final InMemoryLedgerConnectionManager ledgerConnectionManager; @NonNull - private final EscrowManager escrowManager; + private final InMemoryEscrowManager escrowManager; - public InMemoryLedger(final String name, final LedgerInfo ledgerInfo, final QuotingService quotingService) { + public InMemoryLedger( + final String name, final LedgerInfo ledgerInfo, final QuotingService quotingService, + final long defaultExpirationSeconds, final Ticker ticker + ) { this.name = name; this.ledgerInfo = ledgerInfo; @@ -102,8 +109,14 @@ public InMemoryLedger(final String name, final LedgerInfo ledgerInfo, final Quot // Create an Escrow Account in this ledger... final IlpAddress escrowAccountAddress = IlpAddress.of(ESCROW, ledgerInfo.getLedgerId()); this.getLedgerAccountManager().createAccount(escrowAccountAddress); - this.escrowManager = new EscrowManager( - ledgerInfo, escrowAccountAddress.getLedgerAccountId(), ledgerAccountManager); + + final CacheBuilder escrowCacheBuilder = CacheBuilder.newBuilder() + .expireAfterWrite(defaultExpirationSeconds, TimeUnit.SECONDS) + .ticker(ticker); + this.escrowManager = new InMemoryEscrowManager( + ledgerInfo, escrowAccountAddress.getLedgerAccountId(), ledgerAccountManager, escrowCacheBuilder + ); + this.escrowManager.setEscrowExpirationHandler(this); } /** @@ -157,8 +170,17 @@ public void rejectTransfer( ); // Given a source address (for the Connector) ask the ledger for the connectorId. - final ConnectorId connectorId = this.getSourceConnector(reversedEscrow); - this.getLedgerConnectionManager().notifyEventListeners(connectorId, event); + final Optional optConnectorId = this.getSourceConnector(reversedEscrow); + + if (optConnectorId.isPresent()) { + this.getLedgerConnectionManager().notifyEventListeners(optConnectorId.get(), event); + } else { + logger.error( + "Unable to Reject Transfer '{}' because Connector '{}' was not connected!", + ilpTransactionId, + reversedEscrow.getLocalSourceAddress() + ); + } } } @@ -189,8 +211,16 @@ public void fulfillCondition(final IlpTransactionId ilpTransactionId) { ); // Given a source address (for the Connector) ask the ledger for the connectorId. - final ConnectorId connectorId = this.getSourceConnector(executedEscrow); - this.getLedgerConnectionManager().notifyEventListeners(connectorId, event); + final Optional optConnectorId = this.getSourceConnector(executedEscrow); + if (optConnectorId.isPresent()) { + this.getLedgerConnectionManager().notifyEventListeners(optConnectorId.get(), event); + } else { + logger.error( + "Unable to Reject Transfer '{}' because Connector '{}' was not connected!", + ilpTransactionId, + executedEscrow.getLocalSourceAddress() + ); + } } } @@ -212,18 +242,16 @@ public void fulfillCondition(final IlpTransactionId ilpTransactionId) { * won't actually be able to fulfil the payment/rejection, because it wasn't the original connector. Thus, * the ledger has to intelligently track "pending transfers" just like the Connector does. */ - private ConnectorId getSourceConnector(final Escrow escrow) { //final IlpTransactionId ilpTransactionId) { + private Optional getSourceConnector( + final Escrow escrow + ) { //final IlpTransactionId ilpTransactionId) { return this.getLedgerConnectionManager().ledgerEventListeners .values().stream() .filter(ledgerEventListener -> ledgerEventListener.getConnectorInfo().getOptLedgerAccountId().isPresent()) .filter(ledgerEventListener -> ledgerEventListener.getConnectorInfo().getOptLedgerAccountId().get().equals( escrow.getLocalSourceAddress().getLedgerAccountId())) .map(ledgerEventListener -> ledgerEventListener.getConnectorInfo().getConnectorId()) - .findFirst() - .orElseThrow(() -> new RuntimeException(String.format( - "Unable to find ConnectorId for escrow source account: %s.", - escrow.getLocalSourceAddress() - ))); + .findFirst(); } /** @@ -254,10 +282,10 @@ private Optional getLocalLedgerIlpAddressForConnector(final Connecto */ private void sendLocally(final LedgerTransfer transfer) { // Process the transfer locally... - final MonetaryAmount amount = transfer.getAmount(); + final MonetaryAmount amount = transfer.getInterledgerPacketHeader().getAmount(); - final LedgerAccount localSourceAccount = this.getLedgerAccountManager().getAccount( - transfer.getLocalSourceAddress()).get(); + final LedgerAccount localSourceAccount = this.getLedgerAccountManager() + .getAccount(transfer.getLocalSourceAddress()).get(); final LedgerAccount localDestinationAccount = this.getLedgerAccountManager().getAccount( transfer.getInterledgerPacketHeader().getDestinationAddress()).get(); @@ -270,7 +298,7 @@ private void sendLocally(final LedgerTransfer transfer) { .interledgerPacketHeader(transfer.getInterledgerPacketHeader()) .sourceAddress(localSourceAccount.getIlpIdentifier()) .destinationAddress(localDestinationAccount.getIlpIdentifier()) - .amount(transfer.getAmount()) + .amount(transfer.getInterledgerPacketHeader().getAmount()) .build(); this.escrowManager.initiateEscrow(escrowInputs); @@ -303,7 +331,6 @@ private void sendRemotely(final LedgerTransfer transfer) { this.getLocalLedgerIlpAddressForConnector(connectorInfo.getConnectorId()) .map(connectorIlpAddress -> { // Compute the local source account for the transfer. This is going to come from the event. - //final IlpAddress localSourceAccountId = transfer.getInterledgerPacketHeader().getSourceAddress(); final IlpAddress localSourceAccountId = transfer.getLocalSourceAddress(); // The local destination account for the transfer is the designated Connector's account. This is @@ -314,7 +341,7 @@ private void sendRemotely(final LedgerTransfer transfer) { .interledgerPacketHeader(transfer.getInterledgerPacketHeader()) .sourceAddress(localSourceAccountId) .destinationAddress(localDestinationAccountId) - .amount(transfer.getAmount()) + .amount(transfer.getInterledgerPacketHeader().getAmount()) .build(); // Execute Escrow! @@ -342,7 +369,7 @@ private void sendRemotely(final LedgerTransfer transfer) { throw new InvalidQuoteRequestException("No Connector available"); } - // TODO: Handle multiple debits? + // TODO: Handle multiple debits? See discussion in /~https://github.com/interledgerjs/ilp-connector/pull/159. Seems like the idea is to _not_ handle this, but it's a big breaking change so just hasn't happened yet. // TODO: store the transfer status somewhere? // for (Debit debit : newTransfer.getDebits()) { @@ -351,6 +378,14 @@ private void sendRemotely(final LedgerTransfer transfer) { // newTransfer.setTransferStatus(TransferStatus.PROPOSED); } + @Override + public void onEscrowTimedOut(final Escrow expiredEscrow) { + this.rejectTransfer( + expiredEscrow.getIlpPacketHeader().getIlpTransactionId(), + LedgerTransferRejectedReason.TIMEOUT + ); + } + // /** // * Helper method to transform an instance of {@link LedgerTransferInputs} into an instance of {@link LedgerTransfer} diff --git a/src/main/java/money/fluid/ilp/ledger/inmemory/model/DeliveredLedgerTransferImpl.java b/src/main/java/money/fluid/ilp/ledger/inmemory/model/DeliveredLedgerTransferImpl.java new file mode 100644 index 0000000..c5fbae1 --- /dev/null +++ b/src/main/java/money/fluid/ilp/ledger/inmemory/model/DeliveredLedgerTransferImpl.java @@ -0,0 +1,44 @@ +package money.fluid.ilp.ledger.inmemory.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import money.fluid.ilp.ledger.model.LedgerId; +import money.fluid.ilp.ledger.model.NoteToSelf; +import org.interledgerx.ilp.core.DeliveredLedgerTransfer; +import org.interledgerx.ilp.core.IlpAddress; +import org.interledgerx.ilp.core.InterledgerPacketHeader; + +import java.util.Optional; + +@RequiredArgsConstructor +@Builder +@Getter +@ToString +@EqualsAndHashCode +public class DeliveredLedgerTransferImpl implements DeliveredLedgerTransfer { + + @NonNull + private final InterledgerPacketHeader interledgerPacketHeader; + + @NonNull + private final LedgerId ledgerId; + + @NonNull + private final IlpAddress localSourceAddress; + + @NonNull + private final IlpAddress localDestinationAddress; + +// @NonNull +// private final MonetaryAmount amount; + + @NonNull + private final Optional optData; + + @NonNull + private final Optional optNoteToSelf; +} diff --git a/src/main/java/money/fluid/ilp/ledger/inmemory/model/DefaultLedgerTransfer.java b/src/main/java/money/fluid/ilp/ledger/inmemory/model/ForwardedLedgerTransferImpl.java similarity index 56% rename from src/main/java/money/fluid/ilp/ledger/inmemory/model/DefaultLedgerTransfer.java rename to src/main/java/money/fluid/ilp/ledger/inmemory/model/ForwardedLedgerTransferImpl.java index a8b2352..00bcf21 100644 --- a/src/main/java/money/fluid/ilp/ledger/inmemory/model/DefaultLedgerTransfer.java +++ b/src/main/java/money/fluid/ilp/ledger/inmemory/model/ForwardedLedgerTransferImpl.java @@ -6,34 +6,29 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.ToString; -import money.fluid.ilp.connector.model.ids.IlpTransactionId; +import money.fluid.ilp.ledger.model.LedgerId; import money.fluid.ilp.ledger.model.NoteToSelf; +import org.interledgerx.ilp.core.ForwardedLedgerTransfer; import org.interledgerx.ilp.core.IlpAddress; import org.interledgerx.ilp.core.InterledgerPacketHeader; -import org.interledgerx.ilp.core.LedgerTransfer; -import javax.money.MonetaryAmount; import java.util.Optional; -import java.util.UUID; @RequiredArgsConstructor @Builder @Getter @ToString @EqualsAndHashCode -public class DefaultLedgerTransfer implements LedgerTransfer { +public class ForwardedLedgerTransferImpl implements ForwardedLedgerTransfer { @NonNull private final InterledgerPacketHeader interledgerPacketHeader; @NonNull - private final IlpAddress localSourceAddress; - - @NonNull - private final Optional optLocalDestinationAddress; + private final LedgerId ledgerId; @NonNull - private final MonetaryAmount amount; + private final IlpAddress localSourceAddress; @NonNull private final Optional optData; @@ -45,15 +40,17 @@ public class DefaultLedgerTransfer implements LedgerTransfer * Required-args Consstructor for creation of an instance with no condition nor expiry, using the source ILP address * as the local source address. */ - public DefaultLedgerTransfer( - final IlpAddress sourceAddress, final IlpAddress destinationAddress, final MonetaryAmount amount + public ForwardedLedgerTransferImpl( + final InterledgerPacketHeader interledgerPacketHeader, final LedgerId ledgerId, + final IlpAddress localSourceAddress ) { this( - new InterledgerPacketHeader( - IlpTransactionId.of(UUID.randomUUID().toString()), sourceAddress, destinationAddress, amount - ), - sourceAddress, Optional.empty(), amount, Optional.empty(), + interledgerPacketHeader, + ledgerId, + localSourceAddress, + Optional.empty(), Optional.empty() ); } + } diff --git a/src/main/java/money/fluid/ilp/ledger/inmemory/model/InitialLedgerTransferImpl.java b/src/main/java/money/fluid/ilp/ledger/inmemory/model/InitialLedgerTransferImpl.java new file mode 100644 index 0000000..b98d76b --- /dev/null +++ b/src/main/java/money/fluid/ilp/ledger/inmemory/model/InitialLedgerTransferImpl.java @@ -0,0 +1,58 @@ +package money.fluid.ilp.ledger.inmemory.model; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import money.fluid.ilp.connector.model.ids.IlpTransactionId; +import money.fluid.ilp.ledger.model.NoteToSelf; +import org.interledgerx.ilp.core.IlpAddress; +import org.interledgerx.ilp.core.InterledgerPacketHeader; +import org.interledgerx.ilp.core.LedgerTransfer; + +import javax.money.MonetaryAmount; +import java.util.Optional; + +/** + * An implementation of {@link LedgerTransfer} that is generally created by the first ledger in an ILP transaction, and + * includes only ILP-related info. + */ +@RequiredArgsConstructor +@Builder +@Getter +@ToString +@EqualsAndHashCode +public class InitialLedgerTransferImpl implements LedgerTransfer { + + @NonNull + private final InterledgerPacketHeader interledgerPacketHeader; + + @NonNull + private final Optional optData; + + @NonNull + private final Optional optNoteToSelf; + + + /** + * Required-args Constructor for creation of an instance with no condition nor expiry, using the source ILP address + * as the local source address. + */ + public InitialLedgerTransferImpl( + final IlpTransactionId ilpTransactionId, final IlpAddress sourceAddress, + final IlpAddress destinationAddress, final MonetaryAmount amount + ) { + this( + new InterledgerPacketHeader(ilpTransactionId, sourceAddress, destinationAddress, amount), + Optional.empty(), + Optional.empty() + ); + } + + @Override + public IlpAddress getLocalSourceAddress() { + return this.getInterledgerPacketHeader().getSourceAddress(); + } +} diff --git a/src/main/java/money/fluid/ilp/ledgerclient/InMemoryLedgerClient.java b/src/main/java/money/fluid/ilp/ledgerclient/InMemoryLedgerClient.java index 7925dfa..a6ff40a 100644 --- a/src/main/java/money/fluid/ilp/ledgerclient/InMemoryLedgerClient.java +++ b/src/main/java/money/fluid/ilp/ledgerclient/InMemoryLedgerClient.java @@ -17,6 +17,7 @@ import java.util.HashSet; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; /** * An implementation of {@link LedgerClient} that communicates with an in-memory ledger. Normally, a {@link @@ -40,6 +41,11 @@ public class InMemoryLedgerClient implements LedgerClient { @NonNull private Set ledgerEventHandlers; + private static final boolean CONNECTED = true; + private static final boolean NOT_CONNECTED = false; + + private AtomicBoolean connected; + /** * Default Constructor. Initializes an empty {@link Set} of {@link LedgerEventHandler}. * @@ -52,6 +58,7 @@ public InMemoryLedgerClient( this.connectionInfo = Objects.requireNonNull(connectionInfo); this.inMemoryLedger = Objects.requireNonNull(ledger); this.ledgerEventHandlers = new HashSet<>(); + this.connected = new AtomicBoolean(NOT_CONNECTED); } /////////////// @@ -63,16 +70,28 @@ public LedgerInfo getLedgerInfo() { @Override public void connect() { - // This merely establishes a connection to the Ledger. Managers for this client (e.g., a Connector) may register - // and unregister handlers at will. - this.inMemoryLedger.getLedgerConnectionManager().connect(this.connectionInfo); + if (!this.isConnected()) { + // This merely establishes a connection to the Ledger. Managers for this client (e.g., a Connector) may register + // and unregister handlers at will. + this.inMemoryLedger.getLedgerConnectionManager().connect(this.connectionInfo); + this.connected.compareAndSet(NOT_CONNECTED, CONNECTED); + } } @Override public void disconnect() { - this.inMemoryLedger.getLedgerConnectionManager().disconnect(this.connectionInfo.getConnectorId()); + if (this.isConnected()) { + this.inMemoryLedger.getLedgerConnectionManager().disconnect(this.connectionInfo.getConnectorId()); + this.connected.compareAndSet(CONNECTED, NOT_CONNECTED); + } + } + + @Override + public boolean isConnected() { + return this.connected.get(); } + /** * Initiate an ILP transfer. */ diff --git a/src/main/java/money/fluid/ilp/ledgerclient/LedgerClient.java b/src/main/java/money/fluid/ilp/ledgerclient/LedgerClient.java index 48051ed..6cbc6aa 100644 --- a/src/main/java/money/fluid/ilp/ledgerclient/LedgerClient.java +++ b/src/main/java/money/fluid/ilp/ledgerclient/LedgerClient.java @@ -48,6 +48,13 @@ public interface LedgerClient { */ void disconnect(); + /** + * Indicates if this {@link LedgerClient} is currently connected to a ledger. + * + * @return + */ + boolean isConnected(); + /** * Initiates a ledger-local transfer to start an ILP transaction. * @@ -92,5 +99,4 @@ public interface LedgerClient { * @param handler An instance of {@link LedgerEventHandler} that will handle events emitted from a {@link Ledger}. */ void registerEventHandler(LedgerEventHandler handler); - } diff --git a/src/main/java/org/interledgerx/ilp/core/DeliveredLedgerTransfer.java b/src/main/java/org/interledgerx/ilp/core/DeliveredLedgerTransfer.java new file mode 100644 index 0000000..e7dadeb --- /dev/null +++ b/src/main/java/org/interledgerx/ilp/core/DeliveredLedgerTransfer.java @@ -0,0 +1,34 @@ +package org.interledgerx.ilp.core; + +import money.fluid.ilp.connector.model.ids.LedgerAccountId; +import money.fluid.ilp.ledger.model.LedgerId; + +/** + * An extension of {@link LedgerTransfer} that represents a transfer where the best-matching routing table entry is a + * local ledger. In other words, a Connector can initiate a transfer of this type if no further ILP Connectors are + * required to fulfill an ILP transaction. + * + * @param The type of object that {@link #getOptData()} should return. + * @param The type of object that the {@link #getOptNoteToSelf()} should return. + * @see "/~https://github.com/interledger/rfcs/issues/77" + */ +public interface DeliveredLedgerTransfer extends LedgerTransfer { + + /** + * Get the identifier of the {@link Ledger} that this transfer will be completed in. + * + * @return + */ + LedgerId getLedgerId(); + + /** + * Get {@link LedgerAccountId} for the local account that funds are being credited to. This is not an instance of + * {@link IlpAddress} because a transfer should operate on only a single Ledger. + * + * @return An {@link LedgerAccountId} for the local destination account. + */ + IlpAddress getLocalDestinationAddress(); + + + +} diff --git a/src/main/java/org/interledgerx/ilp/core/ForwardedLedgerTransfer.java b/src/main/java/org/interledgerx/ilp/core/ForwardedLedgerTransfer.java new file mode 100644 index 0000000..446e060 --- /dev/null +++ b/src/main/java/org/interledgerx/ilp/core/ForwardedLedgerTransfer.java @@ -0,0 +1,23 @@ +package org.interledgerx.ilp.core; + +import money.fluid.ilp.ledger.model.LedgerId; + +/** + * An extension of {@link LedgerTransfer} that represents a transfer where the best matching routing table entry names + * another connector. In other words, a connector will create a transfer of this type if it has no direct access to + * the destination ledger. + * + * @param The type of object that {@link #getOptData()} should return. + * @param The type of object that the {@link #getOptNoteToSelf()} should return. + * @see "/~https://github.com/interledger/rfcs/issues/77" + */ +public interface ForwardedLedgerTransfer extends LedgerTransfer { + + /** + * Get the identifier of the {@link Ledger} that this transfer will be completed in. + * + * @return + */ + LedgerId getLedgerId(); + +} diff --git a/src/main/java/org/interledgerx/ilp/core/LedgerTransfer.java b/src/main/java/org/interledgerx/ilp/core/LedgerTransfer.java index 061f4bc..3bb02d3 100644 --- a/src/main/java/org/interledgerx/ilp/core/LedgerTransfer.java +++ b/src/main/java/org/interledgerx/ilp/core/LedgerTransfer.java @@ -10,20 +10,20 @@ public interface LedgerTransfer { /** - * Get the local account that funds are being debited from, as an ILP {@link IlpAddress}. + * Get the packet header for this transfer, based upon all information contained in the Transfer. * - * @return An {@link IlpAddress} for the local source account. + * @return the Interledger Packet Header */ - IlpAddress getLocalSourceAddress(); + InterledgerPacketHeader getInterledgerPacketHeader(); /** - * Get the local account that funds are being credited to, as an ILP {@link IlpAddress}. This is an optional field - * because is not always known. For example, often it must be computed by the Ledger after a transfer has been - * submitted. + * Get the local account that funds are being debited from, as an ILP {@link IlpAddress}. This field exists because + * the local source account may differ from {@link InterledgerPacketHeader#getSourceAddress()}, for example, an ILP + * transfer that involves multiple connectors and ledgers. * - * @return An {@link IlpAddress} for the local destination account. + * @return An {@link IlpAddress} for the local source account. */ - Optional getOptLocalDestinationAddress(); + IlpAddress getLocalSourceAddress(); /** * Get the transfer amount. @@ -35,7 +35,9 @@ public interface LedgerTransfer { * * @return An instance of {@link MonetaryAmount}. */ - MonetaryAmount getAmount(); +// default MonetaryAmount getAmount() { +// return this.getInterledgerPacketHeader().getAmount(); +// } /** * Get the data to be sent. @@ -57,8 +59,8 @@ public interface LedgerTransfer { * Get the host's internal memo. This can be encoded on the wire in any format chosen by an implementation, while * being treated as a typed object in the JVM. *

- * An optional bytestring containing details the host needs to persist with the transfer in order to be able to - * react to transfer events like condition fulfillment later. + * For example, this could be an optional bytestring containing details the host needs to persist with the transfer + * in order to be able to react to transfer events like condition fulfillment later. *

* Ledger plugins MAY attach the noteToSelf to the transfer and let the ledger store it. Otherwise it MAY use the * store in order to persist this field. Regardless of the implementation, the ledger plugin MUST ensure that all @@ -71,10 +73,5 @@ public interface LedgerTransfer { */ Optional getOptNoteToSelf(); - /** - * Get the packet header for this transfer, based upon all information contained in the Transfer. - * - * @return the Interledger Packet Header - */ - InterledgerPacketHeader getInterledgerPacketHeader(); + } diff --git a/src/main/java/org/interledgerx/ilp/core/events/LedgerEventHandler.java b/src/main/java/org/interledgerx/ilp/core/events/LedgerEventHandler.java index 5bc432f..5908c5c 100644 --- a/src/main/java/org/interledgerx/ilp/core/events/LedgerEventHandler.java +++ b/src/main/java/org/interledgerx/ilp/core/events/LedgerEventHandler.java @@ -2,6 +2,8 @@ import money.fluid.ilp.connector.Connector; import money.fluid.ilp.connector.model.ids.ConnectorId; +import money.fluid.ilp.connector.model.ids.IlpTransactionId; +import money.fluid.ilp.ledger.model.LedgerId; import org.interledgerx.ilp.core.LedgerInfo; /** @@ -30,5 +32,4 @@ public interface LedgerEventHandler { Connector getListeningConnector(); LedgerInfo getSourceLedgerInfo(); - } diff --git a/src/test/java/money/fluid/ilp/connector/IlpInMemoryTestHarness.java b/src/test/java/money/fluid/ilp/connector/IlpInMemoryTestHarness.java index 7d765b5..773b958 100644 --- a/src/test/java/money/fluid/ilp/connector/IlpInMemoryTestHarness.java +++ b/src/test/java/money/fluid/ilp/connector/IlpInMemoryTestHarness.java @@ -1,7 +1,12 @@ package money.fluid.ilp.connector; import com.google.common.collect.ImmutableSet; +import com.google.common.testing.FakeTicker; +import money.fluid.ilp.connector.managers.ledgers.DefaultLedgerManager; +import money.fluid.ilp.connector.managers.ledgers.InMemoryPendingTransferManager; +import money.fluid.ilp.connector.managers.ledgers.LedgerManager; import money.fluid.ilp.connector.model.ids.ConnectorId; +import money.fluid.ilp.connector.model.ids.IlpTransactionId; import money.fluid.ilp.connector.model.ids.LedgerAccountId; import money.fluid.ilp.connector.services.routing.DefaultRoute; import money.fluid.ilp.connector.services.routing.RoutingService; @@ -9,7 +14,7 @@ import money.fluid.ilp.ledger.QuotingService; import money.fluid.ilp.ledger.inmemory.InMemoryLedger; import money.fluid.ilp.ledger.inmemory.model.DefaultLedgerInfo; -import money.fluid.ilp.ledger.inmemory.model.DefaultLedgerTransfer; +import money.fluid.ilp.ledger.inmemory.model.InitialLedgerTransferImpl; import money.fluid.ilp.ledger.model.ConnectionInfo; import money.fluid.ilp.ledger.model.ConnectorInfo; import money.fluid.ilp.ledger.model.LedgerAccount; @@ -33,6 +38,8 @@ import javax.money.MonetaryAmount; import java.math.BigDecimal; import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -68,6 +75,8 @@ */ public class IlpInMemoryTestHarness { + private static final long DEFAULT_LEDGER_TIMEOUT = 5L; + // The currency code for the "Sand" ledger. Denominated in granules of sand. private static final String SND = "SND"; private static final String SAND_CURRENCY_SYMBOL = "(S)"; @@ -142,14 +151,20 @@ public class IlpInMemoryTestHarness { ///////////// // The Fluid Money connector that has accounts on both the Sand and Dirt ledgers. + private InMemoryPendingTransferManager pendingTransferManager1; private Connector fluidConnector1; + + private InMemoryPendingTransferManager pendingTransferManager2; private Connector fluidConnector2; + private FakeTicker fakeTicker; @Before public void setup() { MockitoAnnotations.initMocks(this); + this.fakeTicker = new FakeTicker(); + // For this simulation, everyone uses the same precision and scale. final int precision = 2; final int scale = 10; @@ -169,28 +184,36 @@ public void setup() { final LedgerInfo sandLedger1Info = new DefaultLedgerInfo( precision, scale, SND, SAND_CURRENCY_SYMBOL, SAND_LEDGER1 ); - this.sandLedger1 = new InMemoryLedger("Sand Ledger 1", sandLedger1Info, sandLedger1QuotingServiceMock); + this.sandLedger1 = new InMemoryLedger( + "Sand Ledger 1", sandLedger1Info, sandLedger1QuotingServiceMock, DEFAULT_LEDGER_TIMEOUT, fakeTicker); + //sandLedger1.getEscrowManager().setEscrowExpirationHandler(); this.initializeLedgerAccounts(sandLedger1.getLedgerAccountManager()); // Initialize Sand Ledger2 final LedgerInfo sandLedger2Info = new DefaultLedgerInfo( precision, scale, SND, SAND_CURRENCY_SYMBOL, SAND_LEDGER2 ); - this.sandLedger2 = new InMemoryLedger("Sand Ledger 2", sandLedger2Info, sandLedger2QuotingServiceMock); + this.sandLedger2 = new InMemoryLedger("Sand Ledger 2", sandLedger2Info, sandLedger2QuotingServiceMock, + DEFAULT_LEDGER_TIMEOUT, fakeTicker + ); this.initializeLedgerAccounts(sandLedger2.getLedgerAccountManager()); // Initialize Sand Ledger3 final LedgerInfo sandLedger3Info = new DefaultLedgerInfo( precision, scale, SND, SAND_CURRENCY_SYMBOL, SAND_LEDGER3 ); - this.sandLedger3 = new InMemoryLedger("Sand Ledger 3", sandLedger3Info, sandLedger3QuotingServiceMock); + this.sandLedger3 = new InMemoryLedger("Sand Ledger 3", sandLedger3Info, sandLedger3QuotingServiceMock, + DEFAULT_LEDGER_TIMEOUT, fakeTicker + ); this.initializeLedgerAccounts(sandLedger3.getLedgerAccountManager()); // Initialize Dirt Ledger1 final LedgerInfo dirtLedger1Info = new DefaultLedgerInfo( precision, scale, DRT, DIRT_CURRENCY_SYMBOL, DIRT_LEDGER1 ); - this.dirtLedger1 = new InMemoryLedger("Dirt Ledger 1", dirtLedger1Info, dirtLedger1QuotingServiceMock); + this.dirtLedger1 = new InMemoryLedger("Dirt Ledger 1", dirtLedger1Info, dirtLedger1QuotingServiceMock, + DEFAULT_LEDGER_TIMEOUT, fakeTicker + ); this.initializeLedgerAccounts(dirtLedger1.getLedgerAccountManager()); //###################### @@ -243,8 +266,13 @@ public void setup() { final ImmutableSet ledgerClients = ImmutableSet.of( sandLedger1Client, sandLedger2Client, dirtLedger1Client ); + + + // Used to mock time for the guava cache and to access the Cache via Get. + final LedgerManager ledgerManager = new DefaultLedgerManager( + connectorInfo.getConnectorId(), ledgerClients, new InMemoryPendingTransferManager()); this.fluidConnector1 = new DefaultConnector( - connectorInfo, ledgerClients, this.connector1RoutingServiceMock); + connectorInfo, this.connector1RoutingServiceMock, ledgerManager); } //###################### @@ -284,10 +312,15 @@ public void setup() { .optLedgerAccountId(Optional.empty()) .build(); + // For simulation purposes, this connector has the same account identifier on all in-memory ledgers. final ImmutableSet ledgerClients = ImmutableSet.of(sandLedger2Client, sandLedger3Client); + + // Used to mock time for the guava cache and to access the Cache via Get. + final LedgerManager ledgerManager = new DefaultLedgerManager( + connectorInfo.getConnectorId(), ledgerClients, new InMemoryPendingTransferManager()); this.fluidConnector2 = new DefaultConnector( - connectorInfo, ledgerClients, this.connector2RoutingServiceMock); + connectorInfo, this.connector2RoutingServiceMock, ledgerManager); } this.initializeMockQuotingServices(); @@ -296,8 +329,10 @@ public void setup() { @After public void tearDown() { // Disconnect each LedgerClient... - fluidConnector1.getLedgerClients().stream().forEach(simpleLedger -> simpleLedger.disconnect()); - fluidConnector2.getLedgerClients().stream().forEach(simpleLedger -> simpleLedger.disconnect()); + fluidConnector1.getLedgerManager().getLedgerClients().stream().forEach( + simpleLedger -> simpleLedger.disconnect()); + fluidConnector2.getLedgerManager().getLedgerClients().stream().forEach( + simpleLedger -> simpleLedger.disconnect()); } /** @@ -321,8 +356,10 @@ public void tearDown() { */ @Test public void testScenario1_RecipientAcceptsTransfer() { - final DefaultLedgerTransfer ledgerTransfer = - new DefaultLedgerTransfer( + final IlpTransactionId ilpTransactionId = IlpTransactionId.of(UUID.randomUUID().toString()); + final InitialLedgerTransferImpl ledgerTransfer = + new InitialLedgerTransferImpl( + ilpTransactionId, IlpAddress.of(ALICE, SAND_LEDGER1), IlpAddress.of(BOB, SAND_LEDGER1), Money.of(25, "SND") @@ -359,8 +396,10 @@ public void testScenario1_RecipientAcceptsTransfer() { @Test public void testScenario1_RecipientRejectsTransfer() { - final DefaultLedgerTransfer ledgerTransfer = - new DefaultLedgerTransfer( + final IlpTransactionId ilpTransactionId = IlpTransactionId.of(UUID.randomUUID().toString()); + final LedgerTransfer ledgerTransfer = + new InitialLedgerTransferImpl( + ilpTransactionId, IlpAddress.of(ALICE, SAND_LEDGER1), IlpAddress.of(BOB, SAND_LEDGER1), Money.of(25, "SND") @@ -425,7 +464,9 @@ public void testScenario1_RecipientRejectsTransfer() { public void testScenario2_OneConnector_LedgersWithSameAssetType_SenderAcceptsPayment() { // Step1: Assemble and send an amount of SND to Bob on the Sand2 Ledger. - final LedgerTransfer ledgerTransfer = new DefaultLedgerTransfer( + final IlpTransactionId ilpTransactionId = IlpTransactionId.of(UUID.randomUUID().toString()); + final LedgerTransfer ledgerTransfer = new InitialLedgerTransferImpl( + ilpTransactionId, IlpAddress.of(ALICE, SAND_LEDGER1), IlpAddress.of(BOB, SAND_LEDGER2), Money.of(100, "SND") @@ -503,7 +544,9 @@ public void testScenario2_OneConnector_LedgersWithSameAssetType_SenderAcceptsPay @Test public void testScenario2_OneConnector_LedgersWithSameAssetType_SenderRejectsPayment() { // Step1: Assemble and send an amount of SND to Bob on the Sand2 Ledger. - final LedgerTransfer ledgerTransfer = new DefaultLedgerTransfer( + final IlpTransactionId ilpTransactionId = IlpTransactionId.of(UUID.randomUUID().toString()); + final LedgerTransfer ledgerTransfer = new InitialLedgerTransferImpl( + ilpTransactionId, IlpAddress.of(ALICE, SAND_LEDGER1), IlpAddress.of(BOB, SAND_LEDGER2), Money.of(100, "SND") @@ -587,7 +630,9 @@ public void testScenario2_OneConnector_LedgersWithSameAssetType_SenderRejectsPay public void testScenario2_TwoConnectors_LedgersWithSameAssetType_SenderAcceptsPayment() { // Step1: Assemble and send an amount of SND to Bob on the Sand2 Ledger. - final LedgerTransfer ledgerTransfer = new DefaultLedgerTransfer( + final IlpTransactionId ilpTransactionId = IlpTransactionId.of(UUID.randomUUID().toString()); + final LedgerTransfer ledgerTransfer = new InitialLedgerTransferImpl( + ilpTransactionId, IlpAddress.of(ALICE, SAND_LEDGER1), IlpAddress.of(BOB, SAND_LEDGER3), Money.of(100, "SND") @@ -650,8 +695,9 @@ public void testScenario2_TwoConnectors_LedgersWithSameAssetType_SenderAcceptsPa /** * This test simulates a fixed-source-amount payment from one account to another on different ledgers that share the - * same asset type, connected by a two connectors, where the final recipient rejects the transfer. In this case, - * Alice will send SND 100 from {@code sandLedger1} to Bob's account on {@code sandLedger2} via two Fluid + * same asset type, connected by a two connectors, where the final recipient rejects the transfer. + *

+ * In this case, Alice will send SND 100 from {@code sandLedger1} to Bob's account on {@code sandLedger3} via two * Connectors, FluidConnector1 and FluidConnector2. *

* FluidConnector1 has an accounts on the SandLedger1 and SandLedger2. FluidConnector2 has accounts on the @@ -662,7 +708,9 @@ public void testScenario2_TwoConnectors_LedgersWithSameAssetType_SenderAcceptsPa public void testScenario2_TwoConnectors_LedgersWithSameAssetType_SenderRejectsPayment() { // Step1: Assemble and send an amount of SND to Bob on the Sand2 Ledger. - final LedgerTransfer ledgerTransfer = new DefaultLedgerTransfer( + final IlpTransactionId ilpTransactionId = IlpTransactionId.of(UUID.randomUUID().toString()); + final LedgerTransfer ledgerTransfer = new InitialLedgerTransferImpl( + ilpTransactionId, IlpAddress.of(ALICE, SAND_LEDGER1), IlpAddress.of(BOB, SAND_LEDGER3), Money.of(100, "SND") @@ -726,6 +774,125 @@ public void testScenario2_TwoConnectors_LedgersWithSameAssetType_SenderRejectsPa assertLedgerAccount(sandLedger3, ESCROW, ZERO_AMOUNT); } + /** + * This test simulates a fixed-source-amount payment from one account to another on different ledgers that share the + * same asset type, connected by a two connectors, where the final recipient accepts the transfer, but the second + * connector (Connector2) does not pass a fulfillment back to Connector1 before the transaction expires, because it + * (Connector2) goes offline. + *

+ * In this case, Alice will attempt to send SND 100 from {@code sandLedger1} to Bob's account on {@code sandLedger3} + * via two Fluid Connectors, FluidConnector1 and FluidConnector2. + *

+ * FluidConnector1 has an accounts on the SandLedger1 and SandLedger2. FluidConnector2 has accounts on the + * SandLedger2 and SandLedger3. In order to complete the transaction, ILP activity will need to occur on all three + * ledgers. + */ + @Test + public void testScenario2_TwoConnectors_LedgersWithSameAssetType_Connector2TimeOut() throws InterruptedException { + + // Step1: Assemble and send an amount of SND to Bob on the Sand2 Ledger. + final IlpTransactionId ilpTransactionId = IlpTransactionId.of(UUID.randomUUID().toString()); + final LedgerTransfer ledgerTransfer = new InitialLedgerTransferImpl( + ilpTransactionId, + IlpAddress.of(ALICE, SAND_LEDGER1), + IlpAddress.of(BOB, SAND_LEDGER3), + Money.of(100, "SND") + ); + + sandLedger1.send(ledgerTransfer); + + // Sender has sent money from ledger1, but her accounts on other ledgers should be left untouched. + assertLedgerAccount(sandLedger1, ALICE, "400"); + assertLedgerAccount(sandLedger2, ALICE, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, ALICE, INITIAL_AMOUNT); + + // Receivers accounts should not be affected until he accepts the transfer. + assertLedgerAccount(sandLedger1, BOB, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, BOB, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, BOB, INITIAL_AMOUNT); + + assertLedgerAccount(sandLedger1, CONNECTOR1, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, CONNECTOR1, "400"); + assertLedgerAccount(sandLedger3, CONNECTOR1, INITIAL_AMOUNT); + + assertLedgerAccount(sandLedger1, CONNECTOR2, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, CONNECTOR2, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, CONNECTOR2, "400"); + + // Escrows on all ledgers are funded... + assertLedgerAccount(sandLedger1, ESCROW, "100"); + assertLedgerAccount(sandLedger2, ESCROW, "100"); + assertLedgerAccount(sandLedger3, ESCROW, "100"); + + // Take Connector2 Offline. + fluidConnector2.getLedgerManager().findLedgerClient(SAND_LEDGER3).ifPresent( + (ledgerClient -> ledgerClient.disconnect())); + + // Simulate BOB accepting funds on Ledger 3. + sandLedger3.fulfillCondition(ledgerTransfer.getInterledgerPacketHeader().getIlpTransactionId()); + + // Because Connector2 is not connected, it never gets the LedgerEvent. So, Ledger3 will be in a fulfilled state, + // but the other ledgers and escrows will not. + + // Sender should have money returned to her on Ledger1 + assertLedgerAccount(sandLedger1, ALICE, "400"); + assertLedgerAccount(sandLedger2, ALICE, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, ALICE, INITIAL_AMOUNT); + + // Bob has accepted the funds on SL3 only. + assertLedgerAccount(sandLedger1, BOB, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, BOB, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, BOB, "600"); + + // Connector1 should have all of its funds returned... + assertLedgerAccount(sandLedger1, CONNECTOR1, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, CONNECTOR1, "400"); + assertLedgerAccount(sandLedger3, CONNECTOR1, INITIAL_AMOUNT); + + // Connector2 should have lost money on SL3, since it was paid to Bob. + assertLedgerAccount(sandLedger1, CONNECTOR2, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, CONNECTOR2, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, CONNECTOR2, "400"); + + // Escrows on all ledgers are reversed... + assertLedgerAccount(sandLedger1, ESCROW, "100"); + assertLedgerAccount(sandLedger2, ESCROW, "100"); + assertLedgerAccount(sandLedger3, ESCROW, ZERO_AMOUNT); + + // In this particular case, we simulate that some time goes by because the other ledgers (Ledgers 2 and 1) will + // expire their escrows. + + + fakeTicker.advance(10, TimeUnit.SECONDS); + this.sandLedger2.getEscrowManager().getEscrows().cleanUp(); + this.sandLedger1.getEscrowManager().getEscrows().cleanUp(); + + // Sender should have money returned to her on Ledger1 + assertLedgerAccount(sandLedger1, ALICE, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, ALICE, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, ALICE, INITIAL_AMOUNT); + + // Bob has accepted the funds on SL3 only. + assertLedgerAccount(sandLedger1, BOB, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, BOB, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, BOB, "600"); + + // Connector1 should have all of its funds returned... + assertLedgerAccount(sandLedger1, CONNECTOR1, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, CONNECTOR1, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, CONNECTOR1, INITIAL_AMOUNT); + + // Connector2 should have lost money on SL3, since it was paid to Bob. + assertLedgerAccount(sandLedger1, CONNECTOR2, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger2, CONNECTOR2, INITIAL_AMOUNT); + assertLedgerAccount(sandLedger3, CONNECTOR2, "400"); + + // Escrows on all ledgers are reversed... + assertLedgerAccount(sandLedger1, ESCROW, ZERO_AMOUNT); + assertLedgerAccount(sandLedger2, ESCROW, ZERO_AMOUNT); + assertLedgerAccount(sandLedger3, ESCROW, ZERO_AMOUNT); + } + // testScenario2_OneConnector_LedgersWithSameAssetType_MiddleConnectorFailsFulfilment @@ -769,6 +936,8 @@ private void initializeMockRoutingService() { .destinationAddress(IlpAddress.of(BOB, SAND_LEDGER2)) .optExpiresAt(Optional.empty()) .build(); + when(routingServiceMock.bestHopForDestinationAmount(eq(IlpAddress.of(BOB, SAND_LEDGER2)))) + .thenReturn(Optional.of(defaultRouteToBobAtSand2)); when(routingServiceMock.bestHopForDestinationAmount(eq(IlpAddress.of(BOB, SAND_LEDGER2)), any())) .thenReturn(Optional.of(defaultRouteToBobAtSand2)); @@ -778,6 +947,8 @@ private void initializeMockRoutingService() { .destinationAddress(IlpAddress.of(BOB, SAND_LEDGER3)) .optExpiresAt(Optional.empty()) .build(); + when(routingServiceMock.bestHopForDestinationAmount(eq(IlpAddress.of(BOB, SAND_LEDGER3)))).thenReturn( + Optional.of(defaultRouteToBobAtSand3)); when(routingServiceMock.bestHopForDestinationAmount(eq(IlpAddress.of(BOB, SAND_LEDGER3)), any())) .thenReturn(Optional.of(defaultRouteToBobAtSand3)); } @@ -793,6 +964,8 @@ private void initializeMockRoutingService() { .destinationAddress(IlpAddress.of(BOB, SAND_LEDGER3)) .optExpiresAt(Optional.empty()) .build(); + when(routingServiceMock.bestHopForDestinationAmount(eq(IlpAddress.of(BOB, SAND_LEDGER3)))) + .thenReturn(Optional.of(defaultRouteToBobAtSand3)); when(routingServiceMock.bestHopForDestinationAmount(eq(IlpAddress.of(BOB, SAND_LEDGER3)), any())) .thenReturn(Optional.of(defaultRouteToBobAtSand3)); }