- * 1. To hold accounts on various ledgers so that it can provide liquidity across ledgers.
- * 3. To provide route information and routing updates to other connectors.
- * 3. To provide quotes for a given ledger-to-ledger transfer.
- *
- *
- *
Interledger Payments moves asset representations (e.g., currency, stock, IOUs, gold, etc) from one party to
- * another by utilizing one or more ledger transfers, potentially across multiple ledgers.
- *
- *
When a sender prepares a transfer on a Ledger to start a payment, the sender attaches an ILP Payment to the
- * ledger transfer, in the memo field if possible. If a ledger does not support attaching the entire ILP Payment to a
- * transfer as a memo, users of that ledger can transmit the ILP Payment using another authenticated messaging channel,
- * but MUST be able to correlate transfers and ILP Payments.
- *
- *
When a connector sees an incoming prepared transfer with an ILP Payment, it reads the ILP Payment information to
- * confirm the details of the packet. For example, the connector reads the InterledgerAddress of the payment's receiver,
- * and if the connector has a route to the receiver's account, the connector prepares a transfer to continue the payment
- * chain by attaching the same ILP Payment to the new transfer.
- *
- *
At the end of the payment chain, the final receiver (or, more likely, that ledger acting on behalf of the final
- * receiver) confirms that the amount in the ILP Payment Packet matches the amount actually delivered by the transfer.
- * Finally, the last-hop ledger decodes the data portion of the Payment and matches the condition to the payment. The
- * final Interledger node MUST confirm the integrity of the ILP Payment, for example with a hash-based message
- * authentication code (HMAC). If the receiver finds the transfer acceptable, the receiver releases the fulfillment for
- * the transfer, which can be used to execute all prepared transfers that were established prior to the receiver
- * accepting the payment.
- */
-public interface Connector {
-
- /**
- * Accessor for the {@link LedgerPluginManager} that is used to centralize all interactions with ledger plugins for a
- * given Connector.
- */
- LedgerPluginManager getLedgerPluginManager();
-
- T getConnectorConfigurationService();
-
- // TODO: Router, Quoter
-
+package org.interledger.connector;
+
+
+import org.interledger.connector.config.ConnectorConfigurationService;
+import org.interledger.connector.services.LedgerPluginManager;
+
+/**
+ * This interface defines an Interledger Connector, which orchestrates any two ledgers to facilitate an Interledger
+ * payment.
+ *
+ *
The Connector serves three primary purposes:
+ *
+ *
+ * 1. To hold accounts on various ledgers so that it can provide liquidity across ledgers.
+ * 3. To provide route information and routing updates to other connectors.
+ * 3. To provide quotes for a given ledger-to-ledger transfer.
+ *
+ *
+ *
Interledger Payments moves asset representations (e.g., currency, stock, IOUs, gold, etc) from one party to
+ * another by utilizing one or more ledger transfers, potentially across multiple ledgers.
+ *
+ *
When a sender prepares a transfer on a Ledger to start a payment, the sender attaches an ILP Payment to the
+ * ledger transfer, in the memo field if possible. If a ledger does not support attaching the entire ILP Payment to a
+ * transfer as a memo, users of that ledger can transmit the ILP Payment using another authenticated messaging channel,
+ * but MUST be able to correlate transfers and ILP Payments.
+ *
+ *
When a connector sees an incoming prepared transfer with an ILP Payment, it reads the ILP Payment information to
+ * confirm the details of the packet. For example, the connector reads the InterledgerAddress of the payment's receiver,
+ * and if the connector has a route to the receiver's account, the connector prepares a transfer to continue the payment
+ * chain by attaching the same ILP Payment to the new transfer.
+ *
+ *
At the end of the payment chain, the final receiver (or, more likely, that ledger acting on behalf of the final
+ * receiver) confirms that the amount in the ILP Payment Packet matches the amount actually delivered by the transfer.
+ * Finally, the last-hop ledger decodes the data portion of the Payment and matches the condition to the payment. The
+ * final Interledger node MUST confirm the integrity of the ILP Payment, for example with a hash-based message
+ * authentication code (HMAC). If the receiver finds the transfer acceptable, the receiver releases the fulfillment for
+ * the transfer, which can be used to execute all prepared transfers that were established prior to the receiver
+ * accepting the payment.
+ */
+public interface Connector {
+
+ /**
+ * Accessor for the {@link LedgerPluginManager} that is used to centralize all interactions with ledger plugins for a
+ * given Connector.
+ * @return {@link LedgerPluginManager}
+ */
+ LedgerPluginManager getLedgerPluginManager();
+
+ T getConnectorConfigurationService();
+
+ // TODO: Router, Quoter
+
}
\ No newline at end of file
diff --git a/src/main/java/org/interledger/connector/ConnectorUtils.java b/src/main/java/org/interledger/connector/ConnectorUtils.java
index 3284693..790b984 100644
--- a/src/main/java/org/interledger/connector/ConnectorUtils.java
+++ b/src/main/java/org/interledger/connector/ConnectorUtils.java
@@ -1,64 +1,64 @@
-package org.interledger.connector;
-
-import org.interledger.InterledgerAddress;
-import org.interledger.plugin.lpi.TransferId;
-
-import com.google.common.io.BaseEncoding;
-
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Objects;
-import java.util.UUID;
-
-/**
- * Various utilities that might be useful to consumers of this library.
- */
-public class ConnectorUtils {
-
- /**
- * Using HMAC-SHA-256, deterministically generate a UUID from a secret and a public input, which in this case is a
- * ledger-prefix and a transferId.
- *
- * This method can be used to generate a deterministic identifier for the "next" transfer that a Connector might make,
- * so that the connector doesn't send duplicate outgoing transfers if it receives duplicate notifications. In the case
- * of a Connector's next-hop transfer identifier, the deterministic generation should ideally be impossible for a
- * third party to predict. Otherwise an attacker might be able to squat on a predicted ID in order to interfere with a
- * payment or make a connector look unreliable. In order to assure this, the connector may use a secret that seeds the
- * deterministic ID generation.
- *
- * @param secret A {@link String} containing secret information known only to the creator of this transfer id.
- * @param ledgerPrefix A {@link InterledgerAddress} containing a ledger prefix.
- * @param transferId A {@link TransferId} that uniquely identifies the transfer.
- *
- * @returns A deterministically generated {@link UUID}.
- **/
- public static TransferId generateTransferId(
- final String secret, final InterledgerAddress ledgerPrefix, final TransferId transferId
- ) {
- Objects.requireNonNull(secret);
- Objects.requireNonNull(ledgerPrefix);
- InterledgerAddress.requireLedgerPrefix(ledgerPrefix);
- Objects.requireNonNull(transferId);
-
- final String publicInput = String.format("%s/%s", ledgerPrefix, transferId);
-
- try {
- final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
- messageDigest.update(secret.getBytes());
- byte[] digest = messageDigest.digest(publicInput.getBytes());
-
- final String hash = BaseEncoding.base16().encode(digest).substring(0, 36);
- final char[] hashCharArray = hash.toCharArray();
- hashCharArray[8] = '-';
- hashCharArray[13] = '-';
- hashCharArray[14] = '4';
- hashCharArray[18] = '-';
- hashCharArray[19] = '8';
- hashCharArray[23] = '-';
- return TransferId.of(UUID.fromString(new String(hashCharArray)));
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
-
- }
-}
+package org.interledger.connector;
+
+import org.interledger.InterledgerAddress;
+import org.interledger.plugin.lpi.TransferId;
+
+import com.google.common.io.BaseEncoding;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Various utilities that might be useful to consumers of this library.
+ */
+public class ConnectorUtils {
+
+ /**
+ * Using HMAC-SHA-256, deterministically generate a UUID from a secret and a public input, which in this case is a
+ * ledger-prefix and a transferId.
+ *
+ * This method can be used to generate a deterministic identifier for the "next" transfer that a Connector might make,
+ * so that the connector doesn't send duplicate outgoing transfers if it receives duplicate notifications. In the case
+ * of a Connector's next-hop transfer identifier, the deterministic generation should ideally be impossible for a
+ * third party to predict. Otherwise an attacker might be able to squat on a predicted ID in order to interfere with a
+ * payment or make a connector look unreliable. In order to assure this, the connector may use a secret that seeds the
+ * deterministic ID generation.
+ *
+ * @param secret A {@link String} containing secret information known only to the creator of this transfer id.
+ * @param ledgerPrefix A {@link InterledgerAddress} containing a ledger prefix.
+ * @param transferId A {@link TransferId} that uniquely identifies the transfer.
+ *
+ * @return A deterministically generated {@link UUID}.
+ **/
+ public static TransferId generateTransferId(
+ final String secret, final InterledgerAddress ledgerPrefix, final TransferId transferId
+ ) {
+ Objects.requireNonNull(secret);
+ Objects.requireNonNull(ledgerPrefix);
+ InterledgerAddress.requireLedgerPrefix(ledgerPrefix);
+ Objects.requireNonNull(transferId);
+
+ final String publicInput = String.format("%s/%s", ledgerPrefix, transferId);
+
+ try {
+ final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ messageDigest.update(secret.getBytes());
+ byte[] digest = messageDigest.digest(publicInput.getBytes());
+
+ final String hash = BaseEncoding.base16().encode(digest).substring(0, 36);
+ final char[] hashCharArray = hash.toCharArray();
+ hashCharArray[8] = '-';
+ hashCharArray[13] = '-';
+ hashCharArray[14] = '4';
+ hashCharArray[18] = '-';
+ hashCharArray[19] = '8';
+ hashCharArray[23] = '-';
+ return TransferId.of(UUID.fromString(new String(hashCharArray)));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+}
diff --git a/src/main/java/org/interledger/connector/config/ConnectorConfig.java b/src/main/java/org/interledger/connector/config/ConnectorConfig.java
index 5a650dc..4345f8b 100644
--- a/src/main/java/org/interledger/connector/config/ConnectorConfig.java
+++ b/src/main/java/org/interledger/connector/config/ConnectorConfig.java
@@ -1,19 +1,20 @@
-package org.interledger.connector.config;
-
-/**
- * Defines Connector-wide configuration properties that affect the connector as a whole.
- */
-public interface ConnectorConfig {
-
- /**
- * The minimum time, in seconds, that the connector wants to budget for getting a message to the ledgers its trading
- * on.
- *
- * Defaults to 1 second.
- */
- default Integer getMinimumMessageWindow() {
- return 1;
- }
-
-
-}
+package org.interledger.connector.config;
+
+/**
+ * Defines Connector-wide configuration properties that affect the connector as a whole.
+ */
+public interface ConnectorConfig {
+
+ /**
+ * The minimum time, in seconds, that the connector wants to budget for getting a message to the ledgers its trading
+ * on.
+ *
+ * Defaults to 1 second.
+ * @return Integer
+ */
+ default Integer getMinimumMessageWindow() {
+ return 1;
+ }
+
+
+}
diff --git a/src/main/java/org/interledger/connector/config/ConnectorConfigurationService.java b/src/main/java/org/interledger/connector/config/ConnectorConfigurationService.java
index 68bdf4f..6939143 100644
--- a/src/main/java/org/interledger/connector/config/ConnectorConfigurationService.java
+++ b/src/main/java/org/interledger/connector/config/ConnectorConfigurationService.java
@@ -1,51 +1,52 @@
-package org.interledger.connector.config;
-
-import org.interledger.InterledgerAddress;
-import org.interledger.connector.services.LedgerPluginManager;
-import org.interledger.plugin.lpi.LedgerPluginConfig;
-
-import java.util.Collection;
-
-/**
- * A configuration service that provides typed (and runtime-reloadable) access to important configuration properties for
- * _this_ connector.
- */
-public interface ConnectorConfigurationService {
-
- /**
- * Accessor for Connector-wide configuration values.
- */
- T getConnectorConfig();
-
- /**
- * Accessor for all currently configured ledger plugins.
- *
- * Note that this list does not necessarily reflect the current status of a connection to any underlying ledger, but
- * instead merely represents information about the current configuration (whereas the current state of the connector,
- * such as the availability of a given ledger plugin due to downtime) is managed by a separate service.
- *
- * @return A {@link Collection} of type {@link LedgerPluginConfig} for all configured ledger plugins.
- */
- Collection extends LedgerPluginConfig> getLedgerPluginConfigurations();
-
- /**
- * Return configuration info for the ledger plugin indicated by the supplied {@code ledgerPrefix}. This interface
- * returns only some typed configuration properties, with any other properties contained in {@link
- * LedgerPluginConfig#getOptions()}. Implementations can write adaptor classes to access these properties in a typed
- * fashion.
- *
- * Note that this list does not necessarily reflect the current status of a connection to any underlying ledger, but
- * instead merely represents information about the current configuration (whereas the current state of the connector,
- * such as the availability of a given ledger plugin due to downtime) is managed by the {@link LedgerPluginManager}
- * and the plugin itself.
- *
- * @param ledgerPrefix An {@link InterledgerAddress} that is a ledger-prefix for a ledger that this connector has an
- * account on.
- *
- * @return A {@link LedgerPluginConfig} for the specified ledger prefix.
- *
- * @throws RuntimeException if ledger-plugin configuration cannot be found or otherwise assembled.
- */
- LedgerPluginConfig getLedgerPluginConfiguration(InterledgerAddress ledgerPrefix);
-
-}
+package org.interledger.connector.config;
+
+import org.interledger.InterledgerAddress;
+import org.interledger.connector.services.LedgerPluginManager;
+import org.interledger.plugin.lpi.LedgerPluginConfig;
+
+import java.util.Collection;
+
+/**
+ * A configuration service that provides typed (and runtime-reloadable) access to important configuration properties for
+ * _this_ connector.
+ */
+public interface ConnectorConfigurationService {
+
+ /**
+ * Accessor for Connector-wide configuration values.
+ * @return Generic Type T of {@link ConnectorConfig}
+ */
+ T getConnectorConfig();
+
+ /**
+ * Accessor for all currently configured ledger plugins.
+ *
+ * Note that this list does not necessarily reflect the current status of a connection to any underlying ledger, but
+ * instead merely represents information about the current configuration (whereas the current state of the connector,
+ * such as the availability of a given ledger plugin due to downtime) is managed by a separate service.
+ *
+ * @return A {@link Collection} of type {@link LedgerPluginConfig} for all configured ledger plugins.
+ */
+ Collection extends LedgerPluginConfig> getLedgerPluginConfigurations();
+
+ /**
+ * Return configuration info for the ledger plugin indicated by the supplied {@code ledgerPrefix}. This interface
+ * returns only some typed configuration properties, with any other properties contained in {@link
+ * LedgerPluginConfig#getOptions()}. Implementations can write adaptor classes to access these properties in a typed
+ * fashion.
+ *
+ * Note that this list does not necessarily reflect the current status of a connection to any underlying ledger, but
+ * instead merely represents information about the current configuration (whereas the current state of the connector,
+ * such as the availability of a given ledger plugin due to downtime) is managed by the {@link LedgerPluginManager}
+ * and the plugin itself.
+ *
+ * @param ledgerPrefix An {@link InterledgerAddress} that is a ledger-prefix for a ledger that this connector has an
+ * account on.
+ *
+ * @return A {@link LedgerPluginConfig} for the specified ledger prefix.
+ *
+ * @throws RuntimeException if ledger-plugin configuration cannot be found or otherwise assembled.
+ */
+ LedgerPluginConfig getLedgerPluginConfiguration(InterledgerAddress ledgerPrefix);
+
+}
diff --git a/src/main/java/org/interledger/connector/lpi/AbstractLedgerPluginEventHandler.java b/src/main/java/org/interledger/connector/lpi/AbstractLedgerPluginEventHandler.java
index 0138612..97326c4 100644
--- a/src/main/java/org/interledger/connector/lpi/AbstractLedgerPluginEventHandler.java
+++ b/src/main/java/org/interledger/connector/lpi/AbstractLedgerPluginEventHandler.java
@@ -1,514 +1,526 @@
-package org.interledger.connector.lpi;
-
-import org.interledger.InterledgerAddress;
-import org.interledger.connector.ConnectorUtils;
-import org.interledger.connector.repository.ImmutableTransferCorrelation;
-import org.interledger.connector.repository.TransferCorrelation;
-import org.interledger.connector.routing.InterledgerHop;
-import org.interledger.connector.routing.PaymentRouter;
-import org.interledger.connector.services.LedgerPluginManager;
-import org.interledger.ilp.InterledgerPayment;
-import org.interledger.ilp.InterledgerProtocolError;
-import org.interledger.ilp.InterledgerProtocolError.Builder;
-import org.interledger.ilp.InterledgerProtocolError.ErrorCode;
-import org.interledger.plugin.lpi.ImmutableTransfer;
-import org.interledger.plugin.lpi.LedgerPlugin;
-import org.interledger.plugin.lpi.Transfer;
-import org.interledger.plugin.lpi.TransferId;
-import org.interledger.plugin.lpi.events.IncomingTransferCancelledEvent;
-import org.interledger.plugin.lpi.events.IncomingTransferFulfilledEvent;
-import org.interledger.plugin.lpi.events.IncomingTransferPreparedEvent;
-import org.interledger.plugin.lpi.events.IncomingTransferRejectedEvent;
-import org.interledger.plugin.lpi.events.LedgerPluginErrorEvent;
-import org.interledger.plugin.lpi.events.OutgoingTransferCancelledEvent;
-import org.interledger.plugin.lpi.events.OutgoingTransferPreparedEvent;
-import org.interledger.plugin.lpi.events.OutgoingTransferRejectedEvent;
-import org.interledger.plugin.lpi.exceptions.AccountNotFoundException;
-import org.interledger.plugin.lpi.exceptions.DuplicateTransferIdentifier;
-import org.interledger.plugin.lpi.exceptions.InsufficientBalanceException;
-import org.interledger.plugin.lpi.exceptions.InvalidTransferException;
-import org.interledger.plugin.lpi.exceptions.LedgerPluginException;
-import org.interledger.plugin.lpi.handlers.LedgerPluginEventHandler;
-
-import com.google.common.annotations.VisibleForTesting;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.math.BigInteger;
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * An abstract implementation of {@link LedgerPluginEventHandler} that handles events from Ledger plugins running in a
- * Connector.
- */
-public abstract class AbstractLedgerPluginEventHandler implements LedgerPluginEventHandler {
-
- private final Logger logger = LoggerFactory.getLogger(this.getClass());
-
- // Provided by a Connector or higher level system to seed a deterministic identifier generating
- // function.
- private final String deterministicIdSecret;
-
- private final LedgerPluginManager ledgerPluginManager;
- private final PaymentRouter paymentRouter;
-
- public AbstractLedgerPluginEventHandler(
- final String deterministicIdSecret, final LedgerPluginManager ledgerPluginManager,
- final PaymentRouter paymentRouter) {
- this.ledgerPluginManager = Objects.requireNonNull(ledgerPluginManager);
- this.deterministicIdSecret = Objects.requireNonNull(deterministicIdSecret);
- this.paymentRouter = Objects.requireNonNull(paymentRouter);
- }
-
- @Override
- public void onError(LedgerPluginErrorEvent event) {
- logger.error(
- "LedgerPlugin will disconnect and be removed from the LedgrePluginManager after encountering an unrecoverable Error: {}",
- event.getLedgerPrefix(), event.getError());
-
- // Remove ourselves from the active plugins, because something went horribly wrong...
- this.getLedgerPluginManager().removeLedgerPlugin(event.getLedgerPrefix());
- }
-
- @Override
- public void onTransferPrepared(IncomingTransferPreparedEvent event) {
- logger.info("onTransferPrepared: {}", event);
-
- // This method works by attempting to process the incoming transfer while if at any point a non-retryable error
- // is encountered, it is thrown as an InvalidTransferException, which is caught and then used to reject the
- // payment on the source ledger. Otherwise, non-InvalidTransferExceptions are simply thrown and logged by the
- // system, with implementations potentially choosing to implement queued notification handling that might
- // allow for retries after something like a bug or other temporary condition is fixed.
-
- final Transfer sourceTransfer = event.getTransfer();
- try {
- //this.validateIncomingPreparedTransfer(sourceTransfer);
-
- // The address of the ultimate receiver of this ILP Payment....
- final InterledgerPayment ilpPaymentPacket = sourceTransfer.getInterlederPaymentPacket();
-
- // The address of the connector account on the underlying source ledger...
- final InterledgerAddress myAddress = getLedgerPluginManager().getLedgerPluginSafe(
- sourceTransfer.getTransferId(), sourceTransfer.getLedgerPrefix())
- .getConnectorAccount();
-
- // Don't do anything with incoming ILP payments where this connector is the final receiver, because there is
- // no "next-hop" transfer to be made.
- if (ilpPaymentPacket.getDestinationAccount().startsWith(myAddress)) {
- logger.warn(
- "Ignoring Transfer to destination which starts with this plugin's address: "
- + "thisPlugin: \"{}\" ilpPayment Destination: \"{}\"",
- myAddress, ilpPaymentPacket.getDestinationAccount());
- return;
- }
-
- // Determine the nextHop for this payment....
- final InterledgerHop nextHop = this.getPaymentRouter().determineNexHop(
- sourceTransfer.getLedgerPrefix(),
- sourceTransfer.getInterlederPaymentPacket(),
- sourceTransfer.getAmount()
- )
- // If no hop can be determined, we immediately reject the source transfer.
- .orElseThrow(() -> new InvalidTransferException(
- String.format("No route found from \"%s\" to \"%s\"",
- myAddress,
- ilpPaymentPacket.getDestinationAccount()
- ),
- sourceTransfer.getSourceAccount(),
- sourceTransfer.getTransferId(),
- InterledgerProtocolError.builder()
- .errorCode(ErrorCode.F02_UNREACHABLE)
- .triggeredAt(Instant.now())
- .triggeredByAddress(myAddress)
- .build()
- ));
-
- final Transfer destinationTransfer = this.buildNextHopTransfer(sourceTransfer, nextHop);
-
- // Specifies which source_transfer to utilize when handling future reject/fulfill events on the
- // source and destination ledgers. This operation should be done before preparing the transfer
- // on the destination ledger. If that prepare fails, it will likely be retried, in which case
- // this call will merely overwrite itself, which is benign.
- final TransferCorrelation transferCorrelation = ImmutableTransferCorrelation.builder()
- .sourceTransfer(sourceTransfer)
- .destinationTransfer(destinationTransfer)
- .build();
- this.getLedgerPluginManager().getTransferCorrelationRepository()
- .save(transferCorrelation);
-
- // Prepare the transfer on the destination ledger...
- this.prepareDestinationTransfer(sourceTransfer, destinationTransfer);
-
- } catch (InvalidTransferException e) {
- // The transfer was invalid for whatever reason, so we should immediately reject it.
- logger.error("Rejecting Incoming Transfer: {}", e.getMessage(), e);
- this.getLedgerPluginManager()
- .getLedgerPluginSafe(sourceTransfer.getTransferId(),
- sourceTransfer.getLedgerPrefix())
- .rejectIncomingTransfer(sourceTransfer.getTransferId(), e.getRejectionReason());
- return;
- }
- }
-
- /**
- * Given a source transfer and information about the "next hop" in an Interledger payment chain, construct a new
- * {@link Transfer} that can be used to complete this Interledger payment.
- */
- protected Transfer buildNextHopTransfer(
- final Transfer sourceTransfer, final InterledgerHop nextHop
- ) {
- Objects.requireNonNull(sourceTransfer);
- Objects.requireNonNull(nextHop);
-
- // No need to verify connectivity to the destination ledger here, because this will either succeed or
- // fail in the prepareDestinationTransfer call...
-
- // The address of the connector account on the underlying source ledger...
- final InterledgerAddress myAddress = getLedgerPluginManager().getLedgerPluginSafe(
- sourceTransfer.getTransferId(), sourceTransfer.getLedgerPrefix())
- .getConnectorAccount();
-
- final InterledgerPayment ilpPaymentPacket = sourceTransfer.getInterlederPaymentPacket();
-
- // Check if this connector can authorize the final sourceTransfer.
- final InterledgerAddress nextHopCreditAccount;
- final BigInteger nextHopAmount;
- if (nextHop.isFinal()) {
- // TODO: Account for slippage?
- // Verify expectedFinalAmount ≤ actualFinalAmount
- // As long as the fxSpread > slippage, the connector won't lose money.
- final BigInteger slippage = BigInteger.ZERO;
- final BigInteger expectedFinalAmount = ilpPaymentPacket.getDestinationAmount()
- .multiply(BigInteger.ONE.subtract(slippage));
- // If the expectedFinalAmount is greater than the actual final amount, then this sourceTransfer doesn't have
- // enough funds in it.
- if (expectedFinalAmount.compareTo(nextHop.getFinalAmount()) > 0) {
- new InvalidTransferException(
- "Payment rate does not match the rate currently offered",
- sourceTransfer.getLedgerPrefix(),
- sourceTransfer.getTransferId(),
- InterledgerProtocolError.builder()
- .errorCode(ErrorCode.R01_INSUFFICIENT_SOURCE_AMOUNT)
- .triggeredAt(Instant.now())
- .triggeredByAddress(myAddress)
- .build()
- );
- }
-
- // Since this is the final hop, we can merely payout to the destination account in the ILP packet.
- nextHopCreditAccount = ilpPaymentPacket.getDestinationAccount();
- nextHopAmount = ilpPaymentPacket.getDestinationAmount();
- } else {
- // This is not the final "hop" for this payment, so the address/amount should come from the routing table.
- nextHopCreditAccount = nextHop.getDestinationLedgerCreditAccount();
- nextHopAmount = nextHop.getDestinationAmount();
- }
-
- // The ID for the next sourceTransfer should be deterministically generated, so that the connector doesn't send
- // duplicate outgoing transfers if it receives duplicate notifications. The deterministic generation should
- // ideally be impossible for a third party to predict. Otherwise an attacker might be able to squat on a
- // predicted ID in order to interfere with a payment or make a connector look unreliable. In order to assure
- // this, the connector may use a secret that seeds the deterministic ID generation.
- final TransferId destinationTransferId = ConnectorUtils.generateTransferId(
- deterministicIdSecret, sourceTransfer.getLedgerPrefix(), sourceTransfer.getTransferId()
- );
-
- // The "next-hop" sourceTransfer
- final Transfer destinationTransfer = ImmutableTransfer.builder()
- .transferId(destinationTransferId)
- .ledgerPrefix(nextHop.getDestinationLedgerPrefix())
- // The "source" account for this transfer should be this connector's account on the destination leger.
- .sourceAccount(
- // If the source plugin is not connected at this point, then something went wrong, and an exception
- // should be thrown, but ultimately this operation should simply be retried (assuming this event is
- // queued).
- this.getLedgerPluginManager()
- .getLedgerPluginSafe(destinationTransferId,
- nextHop.getDestinationLedgerPrefix())
- .getConnectorAccount()
- )
- .amount(nextHopAmount)
- .destinationAccount(nextHopCreditAccount)
- .interlederPaymentPacket(sourceTransfer.getInterlederPaymentPacket())
- .executionCondition(sourceTransfer.getExecutionCondition())
- .cancellationCondition(sourceTransfer.getCancellationCondition())
- .expiresAt(sourceTransfer.getExpiresAt())
- // TODO: Atomic-mode "cases"?
- .build();
-
- return destinationTransfer;
- }
-
- /**
- * Called when an incoming transfer has been fulfilled on the underlying ledger. For standard behavior, this is a
- * no-op because in general, this connector was the one that would have passed the fulfillment to that ledger plugin.
- *
- * @param event A {@link IncomingTransferFulfilledEvent}.
- */
- @Override
- public void onTransferFulfilled(IncomingTransferFulfilledEvent event) {
- // No-Op.
- if (logger.isDebugEnabled()) {
- logger
- .debug(
- "Incoming Transfer intended for this Connector successfully fulfilled by this Connector: {}",
- event);
- }
- }
-
- /**
- * Called when an incoming transfer has expired on the underlying ledger. This event may be emitted by the
- * ledger-plugin, but it might also be emitted by an Atomic-mode validator.
- *
- * For this implementation, this is currently a no-op. However, if this is occurring in a Universal-Mode Connector, it
- * may be desirable to track this, because it _may_ have occurred whilst an outgoing transfer was waiting to be
- * prepared, and/or might have been fulfilled. In that instance, this connector likely would lose money.
- *
- * In Atomic-Mode usage, this callback merely indicates that the source-transfer ledger cancelled the transaction.
- * However, this is likely still just a no-op because either no destination transfer has yet been prepared, in which
- * case nothing need happen. Or, the destination ledger will consult the same Atomic-Mode notary and likewise reject
- * the destination transfer, meaning no action is necessary here.
- *
- * One optimization that could be made is for the Connector to respond to this method by checking to see if any
- * incoming_transfer events have yet to be processed, and preemptively _not_ prepare on the destination ledger, but
- * this is likely an optimization that would slow-down the happy path due to checking in the prepare handler, so the
- * optimization may not be worth it.
- *
- * @param event A {@link IncomingTransferCancelledEvent}.
- */
- @Override
- public void onTransferCancelled(IncomingTransferCancelledEvent event) {
- // No-Op.
- if (logger.isDebugEnabled()) {
- logger
- .debug("Incoming Transfer intended for this Connector expired: {}", event);
- }
- }
-
- /**
- * Called when an incoming transfer that was rejected by this connector has completed its rejection.
- *
- * @param event A {@link IncomingTransferRejectedEvent}.
- */
- @Override
- public void onTransferRejected(IncomingTransferRejectedEvent event) {
- // No-Op.
- if (logger.isDebugEnabled()) {
- logger
- .debug(
- "Incoming Transfer intended for this Connector successfully rejected by this Connector: {}",
- event);
- }
- }
-
- /**
- * Called when an outgoing transfer has been prepared on the underlying, destination ledger. For standard behavior,
- * this is a no-op because in general, this connector was the one that would have prepared the transfer in the first
- * place, so other than logging the notification, there's nothing more to be done here.
- *
- * @param event A {@link OutgoingTransferPreparedEvent}.
- */
- @Override
- public void onTransferPrepared(final OutgoingTransferPreparedEvent event) {
- Objects.requireNonNull(event);
- // No-Op.
- if (logger.isDebugEnabled()) {
- logger
- .debug("Outgoing Transfer from this Connector successfully prepared: {}", event);
- }
- }
-
- /**
- * This is event is emitted if an outgoing transfer prepared by this connector is cancelled by the destination ledger
- * (i.e., it timed out).
- *
- * @param event A {@link OutgoingTransferCancelledEvent}.
- */
- @Override
- public void onTransferCancelled(OutgoingTransferCancelledEvent event) {
- this.rejectSourceTransferForDestination(event.getTransfer(), event.getCancellationReason());
- }
-
- /**
- * This is event is emitted if an outgoing transfer prepared by this connector is rejected by the destination ledger.
- *
- * @param event A {@link OutgoingTransferCancelledEvent}.
- */
- @Override
- public void onTransferRejected(OutgoingTransferRejectedEvent event) {
- Objects.requireNonNull(event);
- this.rejectSourceTransferForDestination(event.getTransfer(), event.getRejectionReason());
- }
-
- ////////////////////
- // Helper methods...
- ////////////////////
-
-// @VisibleForTesting
-// protected void validateIncomingPreparedTransfer(final Transfer transfer) {
-// Objects.requireNonNull(transfer);
-//
-// // The expected ledger prefix for the incoming transfer.
-// final InterledgerAddress expectedLedgerPrefix = this.ledgerPluginConfig.getLedgerPrefix();
-//
-// // Ensure that the transfer's ledger-prefix matches the ledger prefix of this plugin.
-// if (!expectedLedgerPrefix.equals(transfer.getLedgerPrefix())) {
-// throw new InvalidTransferException(
-// String.format("Unsupported Transfer Ledger Prefix \"%s\" for LedgerPlugin prefix \"%s\"!",
-// transfer.getLedgerPrefix(), expectedLedgerPrefix),
-// expectedLedgerPrefix,
-// transfer.getTransferId(),
-// InterledgerProtocolError.builder()
-// .errorCode(ErrorCode.F00_BAD_REQUEST)
-// // TODO: https://github.com/interledger/java-ilp-core/issues/82
-// .triggeredAt(Instant.now())
-// .triggeredByAddress(expectedLedgerPrefix)
-// .build()
-// );
-// }
-//
-// // Ensure that the destination account is this plugin's connector account.
-// if (!transfer.getDestinationAccount().startsWith(expectedLedgerPrefix)) {
-// throw new InvalidTransferException(
-// String.format("Invalid _destination_ account: \"%s\" for LedgerPlugin: \"%s\"!",
-// transfer.getSourceAccount(), expectedLedgerPrefix),
-// expectedLedgerPrefix,
-// transfer.getTransferId(),
-// InterledgerProtocolError.builder()
-// .errorCode(ErrorCode.F00_BAD_REQUEST)
-// // TODO: https://github.com/interledger/java-ilp-core/issues/82
-// .triggeredAt(Instant.now())
-// .triggeredByAddress(expectedLedgerPrefix)
-// .build()
-// );
-// }
-//
-// // Ensure that the source account is correct for the ledger prefix.
-// if (!transfer.getSourceAccount().startsWith(expectedLedgerPrefix)) {
-// throw new InvalidTransferException(
-// String.format("Invalid _source_ account: \"%s\" for LedgerPlugin: \"%s\"!",
-// transfer.getSourceAccount(), expectedLedgerPrefix),
-// expectedLedgerPrefix,
-// transfer.getTransferId(), InterledgerProtocolError.builder()
-// .errorCode(ErrorCode.F00_BAD_REQUEST)
-// // TODO: https://github.com/interledger/java-ilp-core/issues/82
-// .triggeredAt(Instant.now())
-// .triggeredByAddress(expectedLedgerPrefix)
-// .build()
-// );
-// }
-// }
-
- /**
- * Given a prepared source transfer, and a ready to transmit outgoing transfer, prepare the destination transfer on
- * the appropriate ledger plugin.
- *
- * If the destination transfer cannot be prepared, for whatever reason, then reject the incoming source transfer with
- * an appropriate ILP error code.
- */
- @VisibleForTesting
- protected void prepareDestinationTransfer(final Transfer sourceTransfer,
- final Transfer destinationTransfer) {
- if (logger.isDebugEnabled()) {
- logger.debug("About to settle payment. Source: {}; Destination Transfer: {}",
- sourceTransfer,
- destinationTransfer);
- }
-
- // Before trying to settle, we should ensure that the connector is connected to both the source and destination
- // via the correct ledger plugins. If resolving these fails for any reason, then this is a runtime error, and
- // should not trigger any responses to the source ledger (in other words, this is like a precondition).
-
- final LedgerPlugin destinationLedgerPlugin = this.getLedgerPluginManager()
- .getLedgerPluginSafe(destinationTransfer.getTransferId(),
- sourceTransfer.getLedgerPrefix());
-
- try {
- destinationLedgerPlugin.sendTransfer(destinationTransfer);
- } catch (LedgerPluginException lpe) {
- // Map the LedgerPluginException to a proper RejectionMessage that can be sent back to the source ledger plugin.
- final InterledgerProtocolError rejectionReason = this
- .fromLedgerPluginException(destinationTransfer.getLedgerPrefix(), lpe);
-
- // If the source ledger plugin cannot be located, this is definitely a runtime exception, which can simply
- // be emitted and handled by the caller of this method. However, no exception is expected, so we reject the
- // source transfer on the located ledger plugin.
- this.getLedgerPluginManager()
- .getLedgerPluginSafe(sourceTransfer.getTransferId(),
- sourceTransfer.getLedgerPrefix())
- .rejectIncomingTransfer(sourceTransfer.getTransferId(), rejectionReason);
- }
- }
-
- /**
- * Map an instance of {@link LedgerPluginException} to a corresponding {@link InterledgerProtocolError} for sending
- * back to a source ledger.
- */
- @VisibleForTesting
- protected InterledgerProtocolError fromLedgerPluginException(
- final InterledgerAddress triggeringLedgerPrefix, final LedgerPluginException lpe
- ) {
- Objects.requireNonNull(triggeringLedgerPrefix);
-
- return Optional.of(lpe)
- .map(exception -> {
- final Builder builder = InterledgerProtocolError.builder()
- .triggeredAt(Instant.now())
- .triggeredByAddress(triggeringLedgerPrefix);
- if (exception instanceof DuplicateTransferIdentifier
- || exception instanceof InvalidTransferException) {
- builder.errorCode(ErrorCode.F00_BAD_REQUEST);
- } else if (exception instanceof InsufficientBalanceException) {
- builder.errorCode(ErrorCode.T04_INSUFFICIENT_LIQUIDITY);
- } else if (exception instanceof AccountNotFoundException) {
- builder.errorCode(ErrorCode.F02_UNREACHABLE);
- } else {
- builder.errorCode(ErrorCode.T01_LEDGER_UNREACHABLE);
- }
- return builder.build();
- }).get();
- }
-
- /**
- * This method rejects a source transfer where there is a rejected destination transfer.
- *
- * @param rejectedDestinationTransfer A destination {@link Transfer}
- * @param rejectionReason A {@link InterledgerProtocolError} containing information from the destination
- * ledger about why that destination transfer was rejected.
- */
- @VisibleForTesting
- protected void rejectSourceTransferForDestination(
- final Transfer rejectedDestinationTransfer, final InterledgerProtocolError rejectionReason
- ) {
- final TransferCorrelation transferCorrelation = this.getLedgerPluginManager()
- .getTransferCorrelationRepository()
- .findByDestinationTransferId(rejectedDestinationTransfer.getTransferId())
- .orElseThrow(() -> new RuntimeException(String.format(
- "Unable to reject source transfer for supplied destination transfer due to missing "
- + "TransferCorrelation info! "
- + "DestinationTransfer: %s; rejectionReason: %s",
- rejectedDestinationTransfer, rejectionReason))
- );
-
- final TransferId sourceTransferId = transferCorrelation.getSourceTransfer().getTransferId();
- final InterledgerAddress sourceLedgerPrefix = transferCorrelation.getSourceTransfer()
- .getLedgerPrefix();
-
- final LedgerPlugin sourceLedgerPlugin = this.getLedgerPluginManager()
- .getLedgerPluginSafe(sourceTransferId, sourceLedgerPrefix);
-
- final InterledgerProtocolError forwardedRejectionReason = InterledgerProtocolError
- .withForwardedAddress(rejectionReason, sourceLedgerPlugin.getConnectorAccount());
- sourceLedgerPlugin.rejectIncomingTransfer(sourceTransferId, forwardedRejectionReason);
- }
-
- public LedgerPluginManager getLedgerPluginManager() {
- return this.ledgerPluginManager;
- }
-
- public PaymentRouter getPaymentRouter() {
- return paymentRouter;
- }
-}
+package org.interledger.connector.lpi;
+
+import org.interledger.InterledgerAddress;
+import org.interledger.connector.ConnectorUtils;
+import org.interledger.connector.repository.ImmutableTransferCorrelation;
+import org.interledger.connector.repository.TransferCorrelation;
+import org.interledger.connector.routing.InterledgerHop;
+import org.interledger.connector.routing.PaymentRouter;
+import org.interledger.connector.services.LedgerPluginManager;
+import org.interledger.ilp.InterledgerPayment;
+import org.interledger.ilp.InterledgerProtocolError;
+import org.interledger.ilp.InterledgerProtocolError.Builder;
+import org.interledger.ilp.InterledgerProtocolError.ErrorCode;
+import org.interledger.plugin.lpi.ImmutableTransfer;
+import org.interledger.plugin.lpi.LedgerPlugin;
+import org.interledger.plugin.lpi.Transfer;
+import org.interledger.plugin.lpi.TransferId;
+import org.interledger.plugin.lpi.events.IncomingTransferCancelledEvent;
+import org.interledger.plugin.lpi.events.IncomingTransferFulfilledEvent;
+import org.interledger.plugin.lpi.events.IncomingTransferPreparedEvent;
+import org.interledger.plugin.lpi.events.IncomingTransferRejectedEvent;
+import org.interledger.plugin.lpi.events.LedgerPluginErrorEvent;
+import org.interledger.plugin.lpi.events.OutgoingTransferCancelledEvent;
+import org.interledger.plugin.lpi.events.OutgoingTransferPreparedEvent;
+import org.interledger.plugin.lpi.events.OutgoingTransferRejectedEvent;
+import org.interledger.plugin.lpi.exceptions.AccountNotFoundException;
+import org.interledger.plugin.lpi.exceptions.DuplicateTransferIdentifier;
+import org.interledger.plugin.lpi.exceptions.InsufficientBalanceException;
+import org.interledger.plugin.lpi.exceptions.InvalidTransferException;
+import org.interledger.plugin.lpi.exceptions.LedgerPluginException;
+import org.interledger.plugin.lpi.handlers.LedgerPluginEventHandler;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.math.BigInteger;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * An abstract implementation of {@link LedgerPluginEventHandler} that handles events from Ledger plugins running in a
+ * Connector.
+ */
+public abstract class AbstractLedgerPluginEventHandler implements LedgerPluginEventHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ // Provided by a Connector or higher level system to seed a deterministic identifier generating
+ // function.
+ private final String deterministicIdSecret;
+
+ private final LedgerPluginManager ledgerPluginManager;
+ private final PaymentRouter paymentRouter;
+
+ public AbstractLedgerPluginEventHandler(
+ final String deterministicIdSecret, final LedgerPluginManager ledgerPluginManager,
+ final PaymentRouter paymentRouter) {
+ this.ledgerPluginManager = Objects.requireNonNull(ledgerPluginManager);
+ this.deterministicIdSecret = Objects.requireNonNull(deterministicIdSecret);
+ this.paymentRouter = Objects.requireNonNull(paymentRouter);
+ }
+
+ @Override
+ public void onError(LedgerPluginErrorEvent event) {
+ logger.error(
+ "LedgerPlugin will disconnect and be removed from the LedgrePluginManager after encountering an unrecoverable Error: {}",
+ event.getLedgerPrefix(), event.getError());
+
+ // Remove ourselves from the active plugins, because something went horribly wrong...
+ this.getLedgerPluginManager().removeLedgerPlugin(event.getLedgerPrefix());
+ }
+
+ @Override
+ public void onTransferPrepared(IncomingTransferPreparedEvent event) {
+ logger.info("onTransferPrepared: {}", event);
+
+ // This method works by attempting to process the incoming transfer while if at any point a non-retryable error
+ // is encountered, it is thrown as an InvalidTransferException, which is caught and then used to reject the
+ // payment on the source ledger. Otherwise, non-InvalidTransferExceptions are simply thrown and logged by the
+ // system, with implementations potentially choosing to implement queued notification handling that might
+ // allow for retries after something like a bug or other temporary condition is fixed.
+
+ final Transfer sourceTransfer = event.getTransfer();
+ try {
+ //this.validateIncomingPreparedTransfer(sourceTransfer);
+
+ // The address of the ultimate receiver of this ILP Payment....
+ final InterledgerPayment ilpPaymentPacket = sourceTransfer.getInterlederPaymentPacket();
+
+ // The address of the connector account on the underlying source ledger...
+ final InterledgerAddress myAddress = getLedgerPluginManager().getLedgerPluginSafe(
+ sourceTransfer.getTransferId(), sourceTransfer.getLedgerPrefix())
+ .getConnectorAccount();
+
+ // Don't do anything with incoming ILP payments where this connector is the final receiver, because there is
+ // no "next-hop" transfer to be made.
+ if (ilpPaymentPacket.getDestinationAccount().startsWith(myAddress)) {
+ logger.warn(
+ "Ignoring Transfer to destination which starts with this plugin's address: "
+ + "thisPlugin: \"{}\" ilpPayment Destination: \"{}\"",
+ myAddress, ilpPaymentPacket.getDestinationAccount());
+ return;
+ }
+
+ // Determine the nextHop for this payment....
+ final InterledgerHop nextHop = this.getPaymentRouter().determineNexHop(
+ sourceTransfer.getLedgerPrefix(),
+ sourceTransfer.getInterlederPaymentPacket(),
+ sourceTransfer.getAmount()
+ )
+ // If no hop can be determined, we immediately reject the source transfer.
+ .orElseThrow(() -> new InvalidTransferException(
+ String.format("No route found from \"%s\" to \"%s\"",
+ myAddress,
+ ilpPaymentPacket.getDestinationAccount()
+ ),
+ sourceTransfer.getSourceAccount(),
+ sourceTransfer.getTransferId(),
+ InterledgerProtocolError.builder()
+ .errorCode(ErrorCode.F02_UNREACHABLE)
+ .triggeredAt(Instant.now())
+ .triggeredByAddress(myAddress)
+ .build()
+ ));
+
+ final Transfer destinationTransfer = this.buildNextHopTransfer(sourceTransfer, nextHop);
+
+ // Specifies which source_transfer to utilize when handling future reject/fulfill events on the
+ // source and destination ledgers. This operation should be done before preparing the transfer
+ // on the destination ledger. If that prepare fails, it will likely be retried, in which case
+ // this call will merely overwrite itself, which is benign.
+ final TransferCorrelation transferCorrelation = ImmutableTransferCorrelation.builder()
+ .sourceTransfer(sourceTransfer)
+ .destinationTransfer(destinationTransfer)
+ .build();
+ this.getLedgerPluginManager().getTransferCorrelationRepository()
+ .save(transferCorrelation);
+
+ // Prepare the transfer on the destination ledger...
+ this.prepareDestinationTransfer(sourceTransfer, destinationTransfer);
+
+ } catch (InvalidTransferException e) {
+ // The transfer was invalid for whatever reason, so we should immediately reject it.
+ logger.error("Rejecting Incoming Transfer: {}", e.getMessage(), e);
+ this.getLedgerPluginManager()
+ .getLedgerPluginSafe(sourceTransfer.getTransferId(),
+ sourceTransfer.getLedgerPrefix())
+ .rejectIncomingTransfer(sourceTransfer.getTransferId(), e.getRejectionReason());
+ return;
+ }
+ }
+
+ /**
+ * Given a source transfer and information about the "next hop" in an Interledger payment chain, construct a new
+ * {@link Transfer} that can be used to complete this Interledger payment.
+ * @param sourceTransfer - a {@link Transfer}
+ * @param nextHop - an {@link InterledgerHop}
+ * @return {@link Transfer}
+ */
+ protected Transfer buildNextHopTransfer(
+ final Transfer sourceTransfer, final InterledgerHop nextHop
+ ) {
+ Objects.requireNonNull(sourceTransfer);
+ Objects.requireNonNull(nextHop);
+
+ // No need to verify connectivity to the destination ledger here, because this will either succeed or
+ // fail in the prepareDestinationTransfer call...
+
+ // The address of the connector account on the underlying source ledger...
+ final InterledgerAddress myAddress = getLedgerPluginManager().getLedgerPluginSafe(
+ sourceTransfer.getTransferId(), sourceTransfer.getLedgerPrefix())
+ .getConnectorAccount();
+
+ final InterledgerPayment ilpPaymentPacket = sourceTransfer.getInterlederPaymentPacket();
+
+ // Check if this connector can authorize the final sourceTransfer.
+ final InterledgerAddress nextHopCreditAccount;
+ final BigInteger nextHopAmount;
+ if (nextHop.isFinal()) {
+ // TODO: Account for slippage?
+ // Verify expectedFinalAmount ≤ actualFinalAmount
+ // As long as the fxSpread > slippage, the connector won't lose money.
+ final BigInteger slippage = BigInteger.ZERO;
+ final BigInteger expectedFinalAmount = ilpPaymentPacket.getDestinationAmount()
+ .multiply(BigInteger.ONE.subtract(slippage));
+ // If the expectedFinalAmount is greater than the actual final amount, then this sourceTransfer doesn't have
+ // enough funds in it.
+ if (expectedFinalAmount.compareTo(nextHop.getFinalAmount()) > 0) {
+ new InvalidTransferException(
+ "Payment rate does not match the rate currently offered",
+ sourceTransfer.getLedgerPrefix(),
+ sourceTransfer.getTransferId(),
+ InterledgerProtocolError.builder()
+ .errorCode(ErrorCode.R01_INSUFFICIENT_SOURCE_AMOUNT)
+ .triggeredAt(Instant.now())
+ .triggeredByAddress(myAddress)
+ .build()
+ );
+ }
+
+ // Since this is the final hop, we can merely payout to the destination account in the ILP packet.
+ nextHopCreditAccount = ilpPaymentPacket.getDestinationAccount();
+ nextHopAmount = ilpPaymentPacket.getDestinationAmount();
+ } else {
+ // This is not the final "hop" for this payment, so the address/amount should come from the routing table.
+ nextHopCreditAccount = nextHop.getDestinationLedgerCreditAccount();
+ nextHopAmount = nextHop.getDestinationAmount();
+ }
+
+ // The ID for the next sourceTransfer should be deterministically generated, so that the connector doesn't send
+ // duplicate outgoing transfers if it receives duplicate notifications. The deterministic generation should
+ // ideally be impossible for a third party to predict. Otherwise an attacker might be able to squat on a
+ // predicted ID in order to interfere with a payment or make a connector look unreliable. In order to assure
+ // this, the connector may use a secret that seeds the deterministic ID generation.
+ final TransferId destinationTransferId = ConnectorUtils.generateTransferId(
+ deterministicIdSecret, sourceTransfer.getLedgerPrefix(), sourceTransfer.getTransferId()
+ );
+
+ // The "next-hop" sourceTransfer
+ final Transfer destinationTransfer = ImmutableTransfer.builder()
+ .transferId(destinationTransferId)
+ .ledgerPrefix(nextHop.getDestinationLedgerPrefix())
+ // The "source" account for this transfer should be this connector's account on the destination leger.
+ .sourceAccount(
+ // If the source plugin is not connected at this point, then something went wrong, and an exception
+ // should be thrown, but ultimately this operation should simply be retried (assuming this event is
+ // queued).
+ this.getLedgerPluginManager()
+ .getLedgerPluginSafe(destinationTransferId,
+ nextHop.getDestinationLedgerPrefix())
+ .getConnectorAccount()
+ )
+ .amount(nextHopAmount)
+ .destinationAccount(nextHopCreditAccount)
+ .interlederPaymentPacket(sourceTransfer.getInterlederPaymentPacket())
+ .executionCondition(sourceTransfer.getExecutionCondition())
+ .cancellationCondition(sourceTransfer.getCancellationCondition())
+ .expiresAt(sourceTransfer.getExpiresAt())
+ // TODO: Atomic-mode "cases"?
+ .build();
+
+ return destinationTransfer;
+ }
+
+ /**
+ * Called when an incoming transfer has been fulfilled on the underlying ledger. For standard behavior, this is a
+ * no-op because in general, this connector was the one that would have passed the fulfillment to that ledger plugin.
+ *
+ * @param event A {@link IncomingTransferFulfilledEvent}.
+ */
+ @Override
+ public void onTransferFulfilled(IncomingTransferFulfilledEvent event) {
+ // No-Op.
+ if (logger.isDebugEnabled()) {
+ logger
+ .debug(
+ "Incoming Transfer intended for this Connector successfully fulfilled by this Connector: {}",
+ event);
+ }
+ }
+
+ /**
+ * Called when an incoming transfer has expired on the underlying ledger. This event may be emitted by the
+ * ledger-plugin, but it might also be emitted by an Atomic-mode validator.
+ *
+ * For this implementation, this is currently a no-op. However, if this is occurring in a Universal-Mode Connector, it
+ * may be desirable to track this, because it _may_ have occurred whilst an outgoing transfer was waiting to be
+ * prepared, and/or might have been fulfilled. In that instance, this connector likely would lose money.
+ *
+ * In Atomic-Mode usage, this callback merely indicates that the source-transfer ledger cancelled the transaction.
+ * However, this is likely still just a no-op because either no destination transfer has yet been prepared, in which
+ * case nothing need happen. Or, the destination ledger will consult the same Atomic-Mode notary and likewise reject
+ * the destination transfer, meaning no action is necessary here.
+ *
+ * One optimization that could be made is for the Connector to respond to this method by checking to see if any
+ * incoming_transfer events have yet to be processed, and preemptively _not_ prepare on the destination ledger, but
+ * this is likely an optimization that would slow-down the happy path due to checking in the prepare handler, so the
+ * optimization may not be worth it.
+ *
+ * @param event A {@link IncomingTransferCancelledEvent}.
+ */
+ @Override
+ public void onTransferCancelled(IncomingTransferCancelledEvent event) {
+ // No-Op.
+ if (logger.isDebugEnabled()) {
+ logger
+ .debug("Incoming Transfer intended for this Connector expired: {}", event);
+ }
+ }
+
+ /**
+ * Called when an incoming transfer that was rejected by this connector has completed its rejection.
+ *
+ * @param event A {@link IncomingTransferRejectedEvent}.
+ */
+ @Override
+ public void onTransferRejected(IncomingTransferRejectedEvent event) {
+ // No-Op.
+ if (logger.isDebugEnabled()) {
+ logger
+ .debug(
+ "Incoming Transfer intended for this Connector successfully rejected by this Connector: {}",
+ event);
+ }
+ }
+
+ /**
+ * Called when an outgoing transfer has been prepared on the underlying, destination ledger. For standard behavior,
+ * this is a no-op because in general, this connector was the one that would have prepared the transfer in the first
+ * place, so other than logging the notification, there's nothing more to be done here.
+ *
+ * @param event A {@link OutgoingTransferPreparedEvent}.
+ */
+ @Override
+ public void onTransferPrepared(final OutgoingTransferPreparedEvent event) {
+ Objects.requireNonNull(event);
+ // No-Op.
+ if (logger.isDebugEnabled()) {
+ logger
+ .debug("Outgoing Transfer from this Connector successfully prepared: {}", event);
+ }
+ }
+
+ /**
+ * This is event is emitted if an outgoing transfer prepared by this connector is cancelled by the destination ledger
+ * (i.e., it timed out).
+ *
+ * @param event A {@link OutgoingTransferCancelledEvent}.
+ */
+ @Override
+ public void onTransferCancelled(OutgoingTransferCancelledEvent event) {
+ this.rejectSourceTransferForDestination(event.getTransfer(), event.getCancellationReason());
+ }
+
+ /**
+ * This is event is emitted if an outgoing transfer prepared by this connector is rejected by the destination ledger.
+ *
+ * @param event A {@link OutgoingTransferCancelledEvent}.
+ */
+ @Override
+ public void onTransferRejected(OutgoingTransferRejectedEvent event) {
+ Objects.requireNonNull(event);
+ this.rejectSourceTransferForDestination(event.getTransfer(), event.getRejectionReason());
+ }
+
+ ////////////////////
+ // Helper methods...
+ ////////////////////
+
+// @VisibleForTesting
+// protected void validateIncomingPreparedTransfer(final Transfer transfer) {
+// Objects.requireNonNull(transfer);
+//
+// // The expected ledger prefix for the incoming transfer.
+// final InterledgerAddress expectedLedgerPrefix = this.ledgerPluginConfig.getLedgerPrefix();
+//
+// // Ensure that the transfer's ledger-prefix matches the ledger prefix of this plugin.
+// if (!expectedLedgerPrefix.equals(transfer.getLedgerPrefix())) {
+// throw new InvalidTransferException(
+// String.format("Unsupported Transfer Ledger Prefix \"%s\" for LedgerPlugin prefix \"%s\"!",
+// transfer.getLedgerPrefix(), expectedLedgerPrefix),
+// expectedLedgerPrefix,
+// transfer.getTransferId(),
+// InterledgerProtocolError.builder()
+// .errorCode(ErrorCode.F00_BAD_REQUEST)
+// // TODO: https://github.com/interledger/java-ilp-core/issues/82
+// .triggeredAt(Instant.now())
+// .triggeredByAddress(expectedLedgerPrefix)
+// .build()
+// );
+// }
+//
+// // Ensure that the destination account is this plugin's connector account.
+// if (!transfer.getDestinationAccount().startsWith(expectedLedgerPrefix)) {
+// throw new InvalidTransferException(
+// String.format("Invalid _destination_ account: \"%s\" for LedgerPlugin: \"%s\"!",
+// transfer.getSourceAccount(), expectedLedgerPrefix),
+// expectedLedgerPrefix,
+// transfer.getTransferId(),
+// InterledgerProtocolError.builder()
+// .errorCode(ErrorCode.F00_BAD_REQUEST)
+// // TODO: https://github.com/interledger/java-ilp-core/issues/82
+// .triggeredAt(Instant.now())
+// .triggeredByAddress(expectedLedgerPrefix)
+// .build()
+// );
+// }
+//
+// // Ensure that the source account is correct for the ledger prefix.
+// if (!transfer.getSourceAccount().startsWith(expectedLedgerPrefix)) {
+// throw new InvalidTransferException(
+// String.format("Invalid _source_ account: \"%s\" for LedgerPlugin: \"%s\"!",
+// transfer.getSourceAccount(), expectedLedgerPrefix),
+// expectedLedgerPrefix,
+// transfer.getTransferId(), InterledgerProtocolError.builder()
+// .errorCode(ErrorCode.F00_BAD_REQUEST)
+// // TODO: https://github.com/interledger/java-ilp-core/issues/82
+// .triggeredAt(Instant.now())
+// .triggeredByAddress(expectedLedgerPrefix)
+// .build()
+// );
+// }
+// }
+
+ /**
+ * Given a prepared source transfer, and a ready to transmit outgoing transfer, prepare the destination transfer on
+ * the appropriate ledger plugin.
+ *
+ * If the destination transfer cannot be prepared, for whatever reason, then reject the incoming source transfer with
+ * an appropriate ILP error code.
+ *
+ * @param sourceTransfer - a source {@link Transfer}
+ * @param destinationTransfer - a destination {@link Transfer}
+ *
+ */
+ @VisibleForTesting
+ protected void prepareDestinationTransfer(final Transfer sourceTransfer,
+ final Transfer destinationTransfer) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("About to settle payment. Source: {}; Destination Transfer: {}",
+ sourceTransfer,
+ destinationTransfer);
+ }
+
+ // Before trying to settle, we should ensure that the connector is connected to both the source and destination
+ // via the correct ledger plugins. If resolving these fails for any reason, then this is a runtime error, and
+ // should not trigger any responses to the source ledger (in other words, this is like a precondition).
+
+ final LedgerPlugin destinationLedgerPlugin = this.getLedgerPluginManager()
+ .getLedgerPluginSafe(destinationTransfer.getTransferId(),
+ sourceTransfer.getLedgerPrefix());
+
+ try {
+ destinationLedgerPlugin.sendTransfer(destinationTransfer);
+ } catch (LedgerPluginException lpe) {
+ // Map the LedgerPluginException to a proper RejectionMessage that can be sent back to the source ledger plugin.
+ final InterledgerProtocolError rejectionReason = this
+ .fromLedgerPluginException(destinationTransfer.getLedgerPrefix(), lpe);
+
+ // If the source ledger plugin cannot be located, this is definitely a runtime exception, which can simply
+ // be emitted and handled by the caller of this method. However, no exception is expected, so we reject the
+ // source transfer on the located ledger plugin.
+ this.getLedgerPluginManager()
+ .getLedgerPluginSafe(sourceTransfer.getTransferId(),
+ sourceTransfer.getLedgerPrefix())
+ .rejectIncomingTransfer(sourceTransfer.getTransferId(), rejectionReason);
+ }
+ }
+
+ /**
+ * Map an instance of {@link LedgerPluginException} to a corresponding {@link InterledgerProtocolError} for sending
+ * back to a source ledger.
+ *
+ * @param triggeringLedgerPrefix - a {@link InterledgerAddress}
+ * @param lpe - a {@link LedgerPluginException}
+ *
+ * @return {@link InterledgerProtocolError}
+ */
+ @VisibleForTesting
+ protected InterledgerProtocolError fromLedgerPluginException(
+ final InterledgerAddress triggeringLedgerPrefix, final LedgerPluginException lpe
+ ) {
+ Objects.requireNonNull(triggeringLedgerPrefix);
+
+ return Optional.of(lpe)
+ .map(exception -> {
+ final Builder builder = InterledgerProtocolError.builder()
+ .triggeredAt(Instant.now())
+ .triggeredByAddress(triggeringLedgerPrefix);
+ if (exception instanceof DuplicateTransferIdentifier
+ || exception instanceof InvalidTransferException) {
+ builder.errorCode(ErrorCode.F00_BAD_REQUEST);
+ } else if (exception instanceof InsufficientBalanceException) {
+ builder.errorCode(ErrorCode.T04_INSUFFICIENT_LIQUIDITY);
+ } else if (exception instanceof AccountNotFoundException) {
+ builder.errorCode(ErrorCode.F02_UNREACHABLE);
+ } else {
+ builder.errorCode(ErrorCode.T01_LEDGER_UNREACHABLE);
+ }
+ return builder.build();
+ }).get();
+ }
+
+ /**
+ * This method rejects a source transfer where there is a rejected destination transfer.
+ *
+ * @param rejectedDestinationTransfer A destination {@link Transfer}
+ * @param rejectionReason A {@link InterledgerProtocolError} containing information from the destination
+ * ledger about why that destination transfer was rejected.
+ */
+ @VisibleForTesting
+ protected void rejectSourceTransferForDestination(
+ final Transfer rejectedDestinationTransfer, final InterledgerProtocolError rejectionReason
+ ) {
+ final TransferCorrelation transferCorrelation = this.getLedgerPluginManager()
+ .getTransferCorrelationRepository()
+ .findByDestinationTransferId(rejectedDestinationTransfer.getTransferId())
+ .orElseThrow(() -> new RuntimeException(String.format(
+ "Unable to reject source transfer for supplied destination transfer due to missing "
+ + "TransferCorrelation info! "
+ + "DestinationTransfer: %s; rejectionReason: %s",
+ rejectedDestinationTransfer, rejectionReason))
+ );
+
+ final TransferId sourceTransferId = transferCorrelation.getSourceTransfer().getTransferId();
+ final InterledgerAddress sourceLedgerPrefix = transferCorrelation.getSourceTransfer()
+ .getLedgerPrefix();
+
+ final LedgerPlugin sourceLedgerPlugin = this.getLedgerPluginManager()
+ .getLedgerPluginSafe(sourceTransferId, sourceLedgerPrefix);
+
+ final InterledgerProtocolError forwardedRejectionReason = InterledgerProtocolError
+ .withForwardedAddress(rejectionReason, sourceLedgerPlugin.getConnectorAccount());
+ sourceLedgerPlugin.rejectIncomingTransfer(sourceTransferId, forwardedRejectionReason);
+ }
+
+ public LedgerPluginManager getLedgerPluginManager() {
+ return this.ledgerPluginManager;
+ }
+
+ public PaymentRouter getPaymentRouter() {
+ return paymentRouter;
+ }
+}
diff --git a/src/main/java/org/interledger/connector/quoting/Quote.java b/src/main/java/org/interledger/connector/quoting/Quote.java
index 4d087e2..bc6a21c 100644
--- a/src/main/java/org/interledger/connector/quoting/Quote.java
+++ b/src/main/java/org/interledger/connector/quoting/Quote.java
@@ -1,59 +1,69 @@
-package org.interledger.connector.quoting;
-
-import org.interledger.connector.RouteId;
-import org.interledger.connector.routing.InterledgerHop;
-
-import java.math.BigInteger;
-import java.time.Instant;
-
-import javax.money.convert.ExchangeRate;
-
-/**
- * Defines an Interledger payment quote that can be used to assemble a "next-hop" transfer on a destination ledger, in
- * response to an incoming transfer on a different ledger.
- */
-public interface Quote {
-
- /**
- * The unique identifier of the liquidity path this quote is valid for.
- */
- RouteId getRouteId();
-
- /**
- * The next "hop" that this quote exists for.
- */
- InterledgerHop getNextHop();
-
- /**
- * The exchange-rate applied to this quote.
- */
- ExchangeRate getExchangeRate();
-
- /**
- * The amount to be delivered to the next-hop ledger, in local-ledger units of that destination ledger.
- *
- * @deprecated // TODO: This interface assumes a fixed quote amount regardless of transfer amount.
- */
- BigInteger getAmount();
-
- /**
- * The date/time when this quote expires.
- */
- Instant getExpiresAt();
-
-// route: fullRoute,
-// hop: fullRoute.isLocal ? null : connector,
-// liquidityCurve: fullRoute.curve.shiftX(shiftBy),
-// sourceHoldDuration: request.destinationHoldDuration + fullRoute.minMessageWindow * 1000,
-// expiresAt: Math.min(quoteExpiresAt, fullRoute.curveExpiresAt || Infinity)
-//
-
-// appliesToPrefix = "usd-ledger."
-// expiresAt = 1434412845000
-// hop = null
-// liquidityCurve = LiquidityCurve
-// route = Route
-// sourceHoldDuration = 11001
-
-
-}
+package org.interledger.connector.quoting;
+
+import org.interledger.connector.RouteId;
+import org.interledger.connector.routing.InterledgerHop;
+
+import java.math.BigInteger;
+import java.time.Instant;
+
+import javax.money.convert.ExchangeRate;
+
+/**
+ * Defines an Interledger payment quote that can be used to assemble a "next-hop" transfer on a destination ledger, in
+ * response to an incoming transfer on a different ledger.
+ */
+public interface Quote {
+
+ /**
+ * The unique identifier of the liquidity path this quote is valid for.
+ *
+ * @return {@link RouteId}
+ */
+ RouteId getRouteId();
+
+ /**
+ * The next "hop" that this quote exists for.
+ *
+ * @return {@link InterledgerHop}
+ */
+ InterledgerHop getNextHop();
+
+ /**
+ * The exchange-rate applied to this quote.
+ *
+ * @return {@link ExchangeRate}
+ */
+ ExchangeRate getExchangeRate();
+
+ /**
+ * The amount to be delivered to the next-hop ledger, in local-ledger units of that destination ledger.
+ *
+ * @deprecated // TODO: This interface assumes a fixed quote amount regardless of transfer amount.
+ *
+ * @return BigInteger
+ */
+ BigInteger getAmount();
+
+ /**
+ * The date/time when this quote expires.
+ *
+ * @return Instant
+ */
+ Instant getExpiresAt();
+
+// route: fullRoute,
+// hop: fullRoute.isLocal ? null : connector,
+// liquidityCurve: fullRoute.curve.shiftX(shiftBy),
+// sourceHoldDuration: request.destinationHoldDuration + fullRoute.minMessageWindow * 1000,
+// expiresAt: Math.min(quoteExpiresAt, fullRoute.curveExpiresAt || Infinity)
+//
+
+// appliesToPrefix = "usd-ledger."
+// expiresAt = 1434412845000
+// hop = null
+// liquidityCurve = LiquidityCurve
+// route = Route
+// sourceHoldDuration = 11001
+
+
+}
diff --git a/src/main/java/org/interledger/connector/quoting/QuotingService.java b/src/main/java/org/interledger/connector/quoting/QuotingService.java
index f316320..e84f769 100644
--- a/src/main/java/org/interledger/connector/quoting/QuotingService.java
+++ b/src/main/java/org/interledger/connector/quoting/QuotingService.java
@@ -1,47 +1,51 @@
-package org.interledger.connector.quoting;
-
-import org.interledger.InterledgerAddress;
-import org.interledger.connector.routing.InterledgerHop;
-
-import java.math.BigInteger;
-
-/**
- * A service for determining FX quotes for particular liquidity paths.
- *
- * A quote contains information relating to the
- */
-public interface QuotingService {
-
- /**
- * Gets a quote to deliver the specified {@code sourceAmount} to a destination ledger via Interledger.
- *
- * A
- */
- Quote getQuoteBySourceAmount(InterledgerAddress sourceLedgerPrefix, BigInteger sourceAmount);
-
-
-
- /**
- * Gets a quote to deliver the specified {@code destinationAmount} to a destination ledger via Interledger
- *
- * @param {InterledgerAddress} sourceLedgerPrefix
- * @param {InterledgerAddress} destinationAmount
- * @return {Quote}
- */
- Quote getQuoteByDestinationAmount(InterledgerAddress sourceLedgerPrefix, BigInteger destinationAmount);
-
- //findBestPathForSourceAmount(IlpAddress sourceLedger, IlpAddress destination, BigInteger sourceAmount);
-
-
-
- //findBestPathForSourceAmount(sourceLedger, ilpPacket.account, sourceTransfer.amount)
-
-
-
- /**
- *
- * @return
- */
- InterledgerHop findNextHop();
-
-}
+package org.interledger.connector.quoting;
+
+import org.interledger.InterledgerAddress;
+import org.interledger.connector.routing.InterledgerHop;
+
+import java.math.BigInteger;
+
+/**
+ * A service for determining FX quotes for particular liquidity paths.
+ *
+ * A quote contains information relating to the
+ */
+public interface QuotingService {
+
+ /**
+ * Gets a quote to deliver the specified {@code sourceAmount} to a destination ledger via Interledger.
+ *
+ * A
+ *
+ * @param sourceLedgerPrefix - an {@link InterledgerAddress}
+ * @param sourceAmount - a BigInteger
+ * @return {@link Quote}
+ */
+ Quote getQuoteBySourceAmount(InterledgerAddress sourceLedgerPrefix, BigInteger sourceAmount);
+
+
+
+ /**
+ * Gets a quote to deliver the specified {@code destinationAmount} to a destination ledger via Interledger
+ *
+ * @param sourceLedgerPrefix - a source {@link InterledgerAddress}
+ * @param destinationAmount - BigInteger
+ * @return {@link Quote}
+ */
+ Quote getQuoteByDestinationAmount(InterledgerAddress sourceLedgerPrefix, BigInteger destinationAmount);
+
+ //findBestPathForSourceAmount(IlpAddress sourceLedger, IlpAddress destination, BigInteger sourceAmount);
+
+
+
+ //findBestPathForSourceAmount(sourceLedger, ilpPacket.account, sourceTransfer.amount)
+
+
+
+ /**
+ *
+ * @return {@link InterledgerHop}
+ */
+ InterledgerHop findNextHop();
+
+}
diff --git a/src/main/java/org/interledger/connector/routing/InterledgerHop.java b/src/main/java/org/interledger/connector/routing/InterledgerHop.java
index b7c04ca..7c9b5e9 100644
--- a/src/main/java/org/interledger/connector/routing/InterledgerHop.java
+++ b/src/main/java/org/interledger/connector/routing/InterledgerHop.java
@@ -1,51 +1,61 @@
-package org.interledger.connector.routing;
-
-import org.interledger.InterledgerAddress;
-
-import org.immutables.value.Value;
-
-import java.math.BigInteger;
-
-/**
- * Represents a "next hop" for purposes of Interledger Routing.
- */
-@Value.Immutable
-public interface InterledgerHop {
-
- /**
- * The ledger prefix of the destination ledger to make the "next" local-ledger transfer in.
- */
- InterledgerAddress getDestinationLedgerPrefix();
-
- /**
- * The ILP address of the account to credit funds to as part of the "next" local-ledger transfer (the source-account
- * will always be the account of the connector at the destination ledger).
- */
- InterledgerAddress getDestinationLedgerCreditAccount();
-
- /**
- * The amount of the next-hop transfer.
- *
- * headCurve.amountAt(sourceAmount).toString(),
- */
- BigInteger getDestinationAmount();
-
- /**
- * The actual final amount of this transfer, once slippage is considered.
- *
- * quote.liquidityCurve.amountAt(sourceAmount).toString()
- */
- BigInteger getFinalAmount();
-
- /**
- * Determines if this hop is servicable by a ledger that is locally-connected to the Connector wanting to know about a
- * next-hop. If a hop is "final", it means that this connector is delivering this payment (as opposed to forwarding
- * it). *
- *
- * @return {@code true} if this hop is the final hop; {@code false} if this hop is an intermediate hop.
- *
- * @see "https://github.com/interledger/rfcs/issues/77"
- */
- boolean isFinal();
-
-}
+package org.interledger.connector.routing;
+
+import org.interledger.InterledgerAddress;
+
+import org.immutables.value.Value;
+
+import java.math.BigInteger;
+
+/**
+ * Represents a "next hop" for purposes of Interledger Routing.
+ */
+@Value.Immutable
+public interface InterledgerHop {
+
+ /**
+ * The ledger prefix of the destination ledger to make the "next" local-ledger transfer in.
+ *
+ * @return {InterledgerAddress}
+ */
+ InterledgerAddress getDestinationLedgerPrefix();
+
+ /**
+ * The ILP address of the account to credit funds to as part of the "next" local-ledger transfer (the source-account
+ * will always be the account of the connector at the destination ledger).
+ *
+ * @return {@link InterledgerAddress}
+ */
+ InterledgerAddress getDestinationLedgerCreditAccount();
+
+ /**
+ * The amount of the next-hop transfer.
+ *
+ * headCurve.amountAt(sourceAmount).toString(),
+ *
+ * @return BigInteger
+ */
+ BigInteger getDestinationAmount();
+
+ /**
+ * The actual final amount of this transfer, once slippage is considered.
+ *
+ * quote.liquidityCurve.amountAt(sourceAmount).toString()
+ *
+ * @return BigInteger
+ */
+ BigInteger getFinalAmount();
+
+ /**
+ * Determines if this hop is servicable by a ledger that is locally-connected to the Connector wanting to know about a
+ * next-hop. If a hop is "final", it means that this connector is delivering this payment (as opposed to forwarding
+ * it). *
+ *
+ * @return {@code true} if this hop is the final hop; {@code false} if this hop is an intermediate hop.
+ *
+ * @see "https://github.com/interledger/rfcs/issues/77"
+ *
+ * @return boolean
+ */
+ boolean isFinal();
+
+}
diff --git a/src/main/java/org/interledger/connector/routing/InterledgerRoute.java b/src/main/java/org/interledger/connector/routing/InterledgerRoute.java
index 2c1ea5f..c5ef532 100644
--- a/src/main/java/org/interledger/connector/routing/InterledgerRoute.java
+++ b/src/main/java/org/interledger/connector/routing/InterledgerRoute.java
@@ -1,93 +1,101 @@
-package org.interledger.connector.routing;
-
-import org.interledger.InterledgerAddress;
-import org.interledger.connector.RouteId;
-
-import org.immutables.value.Value;
-import org.immutables.value.Value.Default;
-
-/**
- * A "route" for purposes of Interledger payments. This design is very simple for current functionality, and currently
- * models a table that knows about all and looks like this:
- *
- *
- *
- * Eventually, this class needs to be refined so that it can properly model an appropriate route, likely taking into
- * account things like LiquidityCurve and Fees (i.e., a cost function).
- *
- * @see "https://github.com/interledgerjs/five-bells-shared/blob/v22.0.1/schemas/Routes.json"
- * @deprecated This interface will likely be replaced with a variant from java-ilp-core.
- */
-@Deprecated
-@Value.Immutable
-public interface InterledgerRoute {
-
- RouteId getRouteId();
- // TODO
-
-// addedDuringEpoch = 0
-// additionalInfo = undefined
-// curve = LiquidityCurve
-// destinationAccount = "usd-ledger.mark"
-// destinationLedger = "usd-ledger."
-// expiresAt = undefined
-// isLocal = true
-// minMessageWindow = 1
-// nextLedger = "usd-ledger."
-// paths = Array[1]
-// sourceAccount = "eur-ledger.mark"
-// sourceLedger = "eur-ledger."
-// targetPrefix = "usd-ledger."
-
-
- /**
- * The ledger-prefix of the destination ledger for this route.
- */
- InterledgerAddress getDestinationLedgerPrefix();
-
- /**
- * The ledger-prefix of the next-hop ledger that a payment should be forwarded to in order to complete an Interledger
- * payment.
- */
- InterledgerAddress getNextHopLedgerPrefix();
-
- /**
- * The number of hops that this route will require in order to reach the destination.
- **/
- Integer getNumHops();
-
- /**
- * The target of this route. Defines the address or prefix that a particular message/packet should be routed
- * towards.
- */
- //InterledgerAddress getTargetInterledgerAddress();
-
- /**
- * The address-prefix of the ledger that will satisfy this route (in other words, a "ledger prefix").
- */
- //InterledgerAddress getLedgerPrefix();
-
- /**
- * The Interledger address of this connector's account on the above ledger.
- */
- //InterledgerAddress getConnectorAddress();
-
- /**
- * Determines if this route is "local", meaning the route's target is connected to this connector (i.e., the connector
- * holding the routing table).
- */
- @Default
- default boolean isLocal() {
- return false;
- }
-}
+package org.interledger.connector.routing;
+
+import org.interledger.InterledgerAddress;
+import org.interledger.connector.RouteId;
+
+import org.immutables.value.Value;
+import org.immutables.value.Value.Default;
+
+/**
+ * A "route" for purposes of Interledger payments. This design is very simple for current functionality, and currently
+ * models a table that knows about all and looks like this:
+ *
+ *
+ *
+ * Eventually, this class needs to be refined so that it can properly model an appropriate route, likely taking into
+ * account things like LiquidityCurve and Fees (i.e., a cost function).
+ *
+ * @see "https://github.com/interledgerjs/five-bells-shared/blob/v22.0.1/schemas/Routes.json"
+ * @deprecated This interface will likely be replaced with a variant from java-ilp-core.
+ */
+@Deprecated
+@Value.Immutable
+public interface InterledgerRoute {
+
+ RouteId getRouteId();
+ // TODO
+
+// addedDuringEpoch = 0
+// additionalInfo = undefined
+// curve = LiquidityCurve
+// destinationAccount = "usd-ledger.mark"
+// destinationLedger = "usd-ledger."
+// expiresAt = undefined
+// isLocal = true
+// minMessageWindow = 1
+// nextLedger = "usd-ledger."
+// paths = Array[1]
+// sourceAccount = "eur-ledger.mark"
+// sourceLedger = "eur-ledger."
+// targetPrefix = "usd-ledger."
+
+
+ /**
+ * The ledger-prefix of the destination ledger for this route.
+ *
+ * @return {@link InterledgerAddress}
+ */
+ InterledgerAddress getDestinationLedgerPrefix();
+
+ /**
+ * The ledger-prefix of the next-hop ledger that a payment should be forwarded to in order to complete an Interledger
+ * payment.
+ *
+ * @return {@link InterledgerAddress}
+ */
+ InterledgerAddress getNextHopLedgerPrefix();
+
+ /**
+ * The number of hops that this route will require in order to reach the destination.
+ *
+ * @return Integer
+ **/
+ Integer getNumHops();
+
+ /**
+ * The target of this route. Defines the address or prefix that a particular message/packet should be routed
+ * towards.
+ */
+ //InterledgerAddress getTargetInterledgerAddress();
+
+ /**
+ * The address-prefix of the ledger that will satisfy this route (in other words, a "ledger prefix").
+ */
+ //InterledgerAddress getLedgerPrefix();
+
+ /**
+ * The Interledger address of this connector's account on the above ledger.
+ */
+ //InterledgerAddress getConnectorAddress();
+
+ /**
+ * Determines if this route is "local", meaning the route's target is connected to this connector (i.e., the connector
+ * holding the routing table).
+ *
+ * @return boolean
+ */
+ @Default
+ default boolean isLocal() {
+ return false;
+ }
+}
diff --git a/src/main/java/org/interledger/connector/routing/PaymentRouter.java b/src/main/java/org/interledger/connector/routing/PaymentRouter.java
index ec1d524..5689da5 100644
--- a/src/main/java/org/interledger/connector/routing/PaymentRouter.java
+++ b/src/main/java/org/interledger/connector/routing/PaymentRouter.java
@@ -1,39 +1,41 @@
-package org.interledger.connector.routing;
-
-import org.interledger.InterledgerAddress;
-import org.interledger.ilp.InterledgerPayment;
-import org.interledger.plugin.lpi.Transfer;
-
-import java.math.BigInteger;
-import java.util.Optional;
-
-/**
- * An interface that determines which payment paths a particular Interledger payment should be routed along using
- * hop-by-hop transfers.
- */
-public interface PaymentRouter {
-
- /**
- * Given a source {@link Transfer} with an embedded Interledger Payment (i.e., a transfer made to this connector on an
- * underlying ledger, also known as an "incoming transfer), determine the "next hop" for an ILP payment according to
- * the routing and quoting requirements of a particular connector implementation.
- *
- * At a general level, this method works as follows:
- *
- * Given an ILP Payment from A→C, find the next hop B on the payment path from A to C. If the next hop is the final
- * one (B == C), return a hop with {@link InterledgerHop#isFinal()} set to {@code true} and information that will
- * direct a connector event-handler to deliver construct a transfer on the final ledger (B/C in this case). Otherwise,
- * return a transfer at B, with an embedded {@link InterledgerPayment} destined for ledger C.
- *
- * @param sourceLedgerPrefix The {@link InterledgerAddress} prefix of the source ledger that an incoming transfer
- * (for this Connector) was received on.
- * @param interlederPaymentPacket An {@link InterledgerPayment} containing information about the overall ILP payment.
- * @param sourceTransferAmount The amount of the incoming source transfer.
- */
- default Optional determineNexHop(
- InterledgerAddress sourceLedgerPrefix, InterledgerPayment interlederPaymentPacket,
- BigInteger sourceTransferAmount
- ) {
- return Optional.empty();
- }
-}
+package org.interledger.connector.routing;
+
+import org.interledger.InterledgerAddress;
+import org.interledger.ilp.InterledgerPayment;
+import org.interledger.plugin.lpi.Transfer;
+
+import java.math.BigInteger;
+import java.util.Optional;
+
+/**
+ * An interface that determines which payment paths a particular Interledger payment should be routed along using
+ * hop-by-hop transfers.
+ */
+public interface PaymentRouter {
+
+ /**
+ * Given a source {@link Transfer} with an embedded Interledger Payment (i.e., a transfer made to this connector on an
+ * underlying ledger, also known as an "incoming transfer), determine the "next hop" for an ILP payment according to
+ * the routing and quoting requirements of a particular connector implementation.
+ *
+ * At a general level, this method works as follows:
+ *
+ * Given an ILP Payment from A→C, find the next hop B on the payment path from A to C. If the next hop is the final
+ * one (B == C), return a hop with {@link InterledgerHop#isFinal()} set to {@code true} and information that will
+ * direct a connector event-handler to deliver construct a transfer on the final ledger (B/C in this case). Otherwise,
+ * return a transfer at B, with an embedded {@link InterledgerPayment} destined for ledger C.
+ *
+ * @param sourceLedgerPrefix The {@link InterledgerAddress} prefix of the source ledger that an incoming transfer
+ * (for this Connector) was received on.
+ * @param interlederPaymentPacket An {@link InterledgerPayment} containing information about the overall ILP payment.
+ * @param sourceTransferAmount The amount of the incoming source transfer.
+ *
+ * @return {@link Optional} of type {@link InterledgerHop}
+ */
+ default Optional determineNexHop(
+ InterledgerAddress sourceLedgerPrefix, InterledgerPayment interlederPaymentPacket,
+ BigInteger sourceTransferAmount
+ ) {
+ return Optional.empty();
+ }
+}