You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by my...@apache.org on 2018/07/18 14:17:32 UTC
[fineract-cn-stellar-bridge] 02/03: Copied in and adjusted stellar
mapping code from https://github.com/openMF/stellar-connector.
This is an automated email from the ASF dual-hosted git repository.
myrle pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/fineract-cn-stellar-bridge.git
commit 7b3437577d246d615b8c1398817fe1b5eee39aa4
Author: Myrle Krantz <my...@apache.org>
AuthorDate: Tue Jul 17 17:03:14 2018 +0200
Copied in and adjusted stellar mapping code from
https://github.com/openMF/stellar-connector.
---
.../api/v1/events/EventConstants.java | 6 +-
.../cn/stellarbridge/SuiteTestEnvironment.java | 2 +-
.../cn/stellarbridge/TestBridgeConfiguration.java | 5 +-
.../listener/BridgeConfigurationEventListener.java | 2 +-
service/build.gradle | 5 +-
.../service/StellarBridgeApplication.java | 1 +
.../internal/accounting/AccountingAdapter.java | 31 +++
.../internal/accounting/AccountingListener.java | 28 ++
.../JournalEntryCreator.java} | 37 +--
.../internal/command/FineractPaymentCommand.java | 52 ++++
.../internal/command/StellarPaymentCommand.java | 28 ++
.../handler/BridgeConfigurationCommandHandler.java | 11 +-
.../handler/FineractPaymentCommandHandler.java | 74 +++++
.../handler/StellarPaymentCommandHandler.java | 46 ++++
.../config}/StellarBridgeConfiguration.java | 15 +-
.../internal/config/StellarBridgeProperties.java | 57 ++++
.../federation/ExternalFederationService.java | 92 +++++++
.../federation/FederationFailedException.java | 37 +++
.../federation/InvalidStellarAddressException.java | 17 ++
.../internal/federation/StellarAccountId.java | 48 ++++
.../internal/federation/StellarAddress.java | 119 ++++++++
.../federation/StellarAddressResolver.java | 21 ++
.../HorizonServerEffectsListener.java | 132 +++++++++
.../HorizonServerPaymentObserver.java | 82 ++++++
.../horizonadapter/HorizonServerUtilities.java | 301 +++++++++++++++++++++
.../InvalidConfigurationException.java | 20 ++
.../horizonadapter/StellarAccountHelpers.java | 184 +++++++++++++
.../StellarPaymentFailedException.java | 16 ++
.../repository/BridgeConfigurationEntity.java | 20 ++
...ory.java => BridgeConfigurationRepository.java} | 4 +-
.../internal/repository/StellarCursorEntity.java | 64 +++++
.../repository/StellarCursorRepository.java | 11 +
.../service/BridgeConfigurationService.java | 10 +-
.../db/migrations/mariadb/V1__initial_setup.sql | 19 +-
shared.gradle | 5 +-
35 files changed, 1557 insertions(+), 45 deletions(-)
diff --git a/api/src/main/java/org/apache/fineract/cn/stellarbridge/api/v1/events/EventConstants.java b/api/src/main/java/org/apache/fineract/cn/stellarbridge/api/v1/events/EventConstants.java
index f3619ba..c644272 100644
--- a/api/src/main/java/org/apache/fineract/cn/stellarbridge/api/v1/events/EventConstants.java
+++ b/api/src/main/java/org/apache/fineract/cn/stellarbridge/api/v1/events/EventConstants.java
@@ -25,6 +25,10 @@ public interface EventConstants {
String SELECTOR_NAME = "action";
String INITIALIZE = "initialize";
String PUT_CONFIG = "put-config";
+ String STELLAR_PAYMENT_PROCESSED = "bridge-stellar-payment";
+ String FINERACT_PAYMENT_PROCESSED = "bridge-fineract-payment";
String SELECTOR_INITIALIZE = SELECTOR_NAME + " = '" + INITIALIZE + "'";
- String SELECTOR_POST_SAMPLE = SELECTOR_NAME + " = '" + PUT_CONFIG + "'";
+ String SELECTOR_PUT_CONFIG = SELECTOR_NAME + " = '" + PUT_CONFIG + "'";
+ String SELECTOR_STELLAR_PAYMENT_PROCESSED = SELECTOR_NAME + " = '" + STELLAR_PAYMENT_PROCESSED + "'";
+ String SELECTOR_FINERACT_PAYMENT_PROCESSED = SELECTOR_NAME + " = '" + FINERACT_PAYMENT_PROCESSED + "'";
}
diff --git a/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/SuiteTestEnvironment.java b/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/SuiteTestEnvironment.java
index 13e360b..9ce6265 100644
--- a/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/SuiteTestEnvironment.java
+++ b/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/SuiteTestEnvironment.java
@@ -34,7 +34,7 @@ import org.junit.rules.TestRule;
*/
public class SuiteTestEnvironment {
static final String APP_VERSION = "1";
- static final String APP_NAME = "stellarbridge-v1" + APP_VERSION;
+ static final String APP_NAME = "stellarbridge-v" + APP_VERSION;
static final TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
static final CassandraInitializer cassandraInitializer = new CassandraInitializer();
diff --git a/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/TestBridgeConfiguration.java b/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/TestBridgeConfiguration.java
index 07b3c67..1a91fde 100644
--- a/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/TestBridgeConfiguration.java
+++ b/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/TestBridgeConfiguration.java
@@ -23,7 +23,7 @@ import org.apache.fineract.cn.api.context.AutoUserContext;
import org.apache.fineract.cn.stellarbridge.api.v1.client.StellarBridgeManager;
import org.apache.fineract.cn.stellarbridge.api.v1.domain.BridgeConfiguration;
import org.apache.fineract.cn.stellarbridge.api.v1.events.EventConstants;
-import org.apache.fineract.cn.stellarbridge.service.StellarBridgeConfiguration;
+import org.apache.fineract.cn.stellarbridge.service.internal.config.StellarBridgeConfiguration;
import org.apache.fineract.cn.test.fixture.TenantDataStoreContextTestRule;
import org.apache.fineract.cn.test.listener.EnableEventRecording;
import org.apache.fineract.cn.test.listener.EventRecorder;
@@ -48,7 +48,8 @@ import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
+ properties = {"stellarBridge.user=homer", "stellarBridge.horizonAddress=https://horizon-testnet.stellar.org"})
public class TestBridgeConfiguration extends SuiteTestEnvironment {
private static final String LOGGER_NAME = "test-logger";
private static final String TEST_USER = "homer";
diff --git a/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/listener/BridgeConfigurationEventListener.java b/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/listener/BridgeConfigurationEventListener.java
index 9500f2c..5e695c4 100644
--- a/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/listener/BridgeConfigurationEventListener.java
+++ b/component-test/src/main/java/org/apache/fineract/cn/stellarbridge/listener/BridgeConfigurationEventListener.java
@@ -41,7 +41,7 @@ public class BridgeConfigurationEventListener {
@JmsListener(
subscription = EventConstants.DESTINATION,
destination = EventConstants.DESTINATION,
- selector = EventConstants.SELECTOR_POST_SAMPLE
+ selector = EventConstants.SELECTOR_PUT_CONFIG
)
public void onChangeConfiguration(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
final String payload) {
diff --git a/service/build.gradle b/service/build.gradle
index 2b0a369..34ec58f 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -48,7 +48,7 @@ dependencies {
[group: 'org.springframework.cloud', name: 'spring-cloud-starter-config'],
[group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka'],
[group: 'org.springframework.boot', name: 'spring-boot-starter-jetty'],
- [group: 'org.apache.fineract.cn.identity', name: 'api', version: versions.identity],
+ [group: 'org.apache.fineract.cn.accounting', name: 'api', version: versions.accounting],
[group: 'org.apache.fineract.cn.stellarbridge', name: 'api', version: project.version],
[group: 'org.apache.fineract.cn.anubis', name: 'library', version: versions.frameworkanubis],
[group: 'com.google.code.gson', name: 'gson'],
@@ -58,7 +58,8 @@ dependencies {
[group: 'org.apache.fineract.cn', name: 'mariadb', version: versions.frameworkmariadb],
[group: 'org.apache.fineract.cn', name: 'command', version: versions.frameworkcommand],
[group: 'org.apache.fineract.cn.permitted-feign-client', name: 'library', version: versions.frameworkpermittedfeignclient],
- [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator]
+ [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
+ [group: 'com.github.stellar', name: 'java-stellar-sdk', version: versions.stellar]
)
}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/StellarBridgeApplication.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/StellarBridgeApplication.java
index ac5f8dd..32410ef 100644
--- a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/StellarBridgeApplication.java
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/StellarBridgeApplication.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.cn.stellarbridge.service;
+import org.apache.fineract.cn.stellarbridge.service.internal.config.StellarBridgeConfiguration;
import org.springframework.boot.SpringApplication;
public class StellarBridgeApplication {
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/AccountingAdapter.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/AccountingAdapter.java
new file mode 100644
index 0000000..6075143
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/AccountingAdapter.java
@@ -0,0 +1,31 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.accounting;
+
+import java.math.BigDecimal;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationEntity;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AccountingAdapter {
+ private final JournalEntryCreator journalEntryCreator;
+
+ @Autowired
+ public AccountingAdapter(
+ final JournalEntryCreator journalEntryCreator) {
+ this.journalEntryCreator = journalEntryCreator;
+ }
+
+ public String adjustFineractBalances(
+ final BridgeConfigurationEntity bridgeConfigurationEntity,
+ final BigDecimal amount,
+ final String assetCode) {
+ //journalEntryCreator.createJournalEntry(journalEntry);
+ return null;
+ }
+
+ public void tellFineractPaymentSucceeded(String fineractStagingAccountIdentifier,
+ String assetCode, BigDecimal amount)
+ {
+
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/AccountingListener.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/AccountingListener.java
new file mode 100644
index 0000000..b4ff467
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/AccountingListener.java
@@ -0,0 +1,28 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.accounting;
+
+import org.apache.fineract.cn.command.gateway.CommandGateway;
+import org.apache.fineract.cn.lang.config.TenantHeaderFilter;
+import org.apache.fineract.cn.stellarbridge.service.internal.command.FineractPaymentCommand;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AccountingListener {
+ private final CommandGateway commandGateway;
+
+ @Autowired
+ public AccountingListener(
+ final CommandGateway commandGateway) {
+ this.commandGateway = commandGateway;
+ }
+
+ public void onFineractPayment(
+ @Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ final FineractPaymentCommand fineractPaymentCommand = new FineractPaymentCommand(tenant,
+ payload, null, null, null, null);
+ commandGateway.process(fineractPaymentCommand);
+ }
+
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/identity/ApplicationPermissionRequestCreator.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/JournalEntryCreator.java
similarity index 54%
rename from service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/identity/ApplicationPermissionRequestCreator.java
rename to service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/JournalEntryCreator.java
index b3c1e7f..5375cda 100644
--- a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/identity/ApplicationPermissionRequestCreator.java
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/accounting/JournalEntryCreator.java
@@ -16,19 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.cn.stellarbridge.service.internal.identity;
+package org.apache.fineract.cn.stellarbridge.service.internal.accounting;
-import javax.validation.Valid;
+import org.apache.fineract.cn.accounting.api.v1.client.JournalEntryAlreadyExistsException;
+import org.apache.fineract.cn.accounting.api.v1.client.JournalEntryValidationException;
+import org.apache.fineract.cn.accounting.api.v1.domain.JournalEntry;
import org.apache.fineract.cn.anubis.annotation.Permittable;
import org.apache.fineract.cn.api.annotation.ThrowsException;
-import org.apache.fineract.cn.identity.api.v1.client.ApplicationPermissionAlreadyExistsException;
-import org.apache.fineract.cn.identity.api.v1.domain.Permission;
+import org.apache.fineract.cn.api.annotation.ThrowsExceptions;
import org.apache.fineract.cn.permittedfeignclient.annotation.EndpointSet;
import org.apache.fineract.cn.permittedfeignclient.annotation.PermittedFeignClientsConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -36,16 +36,19 @@ import org.springframework.web.bind.annotation.RequestMethod;
/**
* @author Myrle Krantz
*/
-@EndpointSet(identifier = "stellarbridge__v1__identity__v1")
-@FeignClient(name="identity-v1", path="/identity/v1", configuration=PermittedFeignClientsConfiguration.class)
-public interface ApplicationPermissionRequestCreator {
-
- @RequestMapping(value = "/applications/{applicationidentifier}/permissions", method = RequestMethod.POST,
- consumes = {MediaType.APPLICATION_JSON_VALUE},
- produces = {MediaType.ALL_VALUE})
- @ThrowsException(status = HttpStatus.CONFLICT, exception = ApplicationPermissionAlreadyExistsException.class)
- @Permittable(groupId = org.apache.fineract.cn.identity.api.v1.PermittableGroupIds.APPLICATION_SELF_MANAGEMENT)
- void createApplicationPermission(
- @PathVariable("applicationidentifier") String applicationIdentifier,
- @RequestBody @Valid Permission permission);
+@EndpointSet(identifier = "stellarbridge__v1__accounting__v1")
+@FeignClient(name="accounting-v1", path="/accounting/v1", configuration=PermittedFeignClientsConfiguration.class)
+public interface JournalEntryCreator {
+ @RequestMapping(
+ value = "/journal",
+ method = RequestMethod.POST,
+ produces = {MediaType.APPLICATION_JSON_VALUE},
+ consumes = {MediaType.APPLICATION_JSON_VALUE}
+ )
+ @ThrowsExceptions({
+ @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = JournalEntryValidationException.class),
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = JournalEntryAlreadyExistsException.class)
+ })
+ @Permittable(groupId = org.apache.fineract.cn.accounting.api.v1.PermittableGroupIds.THOTH_JOURNAL)
+ void createJournalEntry(@RequestBody final JournalEntry journalEntry);
}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/FineractPaymentCommand.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/FineractPaymentCommand.java
new file mode 100644
index 0000000..d830386
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/FineractPaymentCommand.java
@@ -0,0 +1,52 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.command;
+
+import java.math.BigDecimal;
+
+public class FineractPaymentCommand {
+
+ final private String tenantIdentifer;
+ final private String transactionIdentifier;
+ final private String targetAccount;
+ final private String sinkDomain;
+ final private BigDecimal amount;
+ final private String assetCode;
+
+ public FineractPaymentCommand(
+ String tenantIdentifer,
+ String transactionIdentifier,
+ String targetAccount,
+ String sinkDomain,
+ BigDecimal amount,
+ String assetCode) {
+ this.tenantIdentifer = tenantIdentifer;
+ this.transactionIdentifier = transactionIdentifier;
+ this.targetAccount = targetAccount;
+ this.sinkDomain = sinkDomain;
+ this.amount = amount;
+ this.assetCode = assetCode;
+ }
+
+ public String getTenantIdentifier() {
+ return tenantIdentifer;
+ }
+
+ public String getTransactionIdentifier() {
+ return transactionIdentifier;
+ }
+
+ public String getTargetAccount() {
+ return targetAccount;
+ }
+
+ public String getSinkDomain() {
+ return sinkDomain;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public String getAssetCode() {
+ return assetCode;
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/StellarPaymentCommand.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/StellarPaymentCommand.java
new file mode 100644
index 0000000..d1cd85a
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/StellarPaymentCommand.java
@@ -0,0 +1,28 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.command;
+
+import java.math.BigDecimal;
+
+public class StellarPaymentCommand {
+
+ private final String tenantIdentifier;
+ private final String assetCode;
+ private final BigDecimal amount;
+
+ public StellarPaymentCommand(String tenantIdentifier, String assetCode, BigDecimal amount) {
+ this.tenantIdentifier = tenantIdentifier;
+ this.assetCode = assetCode;
+ this.amount = amount;
+ }
+
+ public String getTenantIdentifier() {
+ return tenantIdentifier;
+ }
+
+ public String getAssetCode() {
+ return assetCode;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/BridgeConfigurationCommandHandler.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/BridgeConfigurationCommandHandler.java
index 9522086..973eeb4 100644
--- a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/BridgeConfigurationCommandHandler.java
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/BridgeConfigurationCommandHandler.java
@@ -25,7 +25,7 @@ import org.apache.fineract.cn.stellarbridge.api.v1.events.EventConstants;
import org.apache.fineract.cn.stellarbridge.service.internal.command.ChangeConfigurationCommand;
import org.apache.fineract.cn.stellarbridge.service.internal.mapper.BridgeConfigurationMapper;
import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationEntity;
-import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationEntityRepository;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
@@ -33,16 +33,17 @@ import org.springframework.transaction.annotation.Transactional;
@Aggregate
public class BridgeConfigurationCommandHandler {
- private final BridgeConfigurationEntityRepository bridgeConfigurationEntityRepository;
+ private final BridgeConfigurationRepository bridgeConfigurationRepository;
private final EventHelper eventHelper;
@Autowired
public BridgeConfigurationCommandHandler(
- final BridgeConfigurationEntityRepository bridgeConfigurationEntityRepository,
+ final BridgeConfigurationRepository bridgeConfigurationRepository,
final EventHelper eventHelper) {
- this.bridgeConfigurationEntityRepository = bridgeConfigurationEntityRepository;
+ this.bridgeConfigurationRepository = bridgeConfigurationRepository;
this.eventHelper = eventHelper;
}
+
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@Transactional
public void handle(final ChangeConfigurationCommand changeConfigurationCommand) {
@@ -50,7 +51,7 @@ public class BridgeConfigurationCommandHandler {
final BridgeConfigurationEntity entity = BridgeConfigurationMapper.map(
changeConfigurationCommand.tenantIdentifier(),
changeConfigurationCommand.instance());
- this.bridgeConfigurationEntityRepository.save(entity);
+ this.bridgeConfigurationRepository.save(entity);
eventHelper.sendEvent(EventConstants.PUT_CONFIG, changeConfigurationCommand.tenantIdentifier(), null);
}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/FineractPaymentCommandHandler.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/FineractPaymentCommandHandler.java
new file mode 100644
index 0000000..328a542
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/FineractPaymentCommandHandler.java
@@ -0,0 +1,74 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.command.handler;
+
+import java.util.Optional;
+import org.apache.fineract.cn.command.annotation.Aggregate;
+import org.apache.fineract.cn.command.annotation.CommandHandler;
+import org.apache.fineract.cn.command.annotation.CommandLogLevel;
+import org.apache.fineract.cn.stellarbridge.api.v1.events.EventConstants;
+import org.apache.fineract.cn.stellarbridge.service.internal.accounting.AccountingAdapter;
+import org.apache.fineract.cn.stellarbridge.service.internal.command.FineractPaymentCommand;
+import org.apache.fineract.cn.stellarbridge.service.internal.federation.StellarAccountId;
+import org.apache.fineract.cn.stellarbridge.service.internal.federation.StellarAddress;
+import org.apache.fineract.cn.stellarbridge.service.internal.federation.StellarAddressResolver;
+import org.apache.fineract.cn.stellarbridge.service.internal.horizonadapter.HorizonServerUtilities;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationEntity;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+@Aggregate
+public class FineractPaymentCommandHandler {
+ private final BridgeConfigurationRepository bridgeConfigurationRepository;
+ private final StellarAddressResolver stellarAddressResolver;
+ private final HorizonServerUtilities horizonServerUtilities;
+ private final AccountingAdapter accountingAdapter;
+ private final EventHelper eventHelper;
+
+ @Autowired
+ public FineractPaymentCommandHandler(
+ final BridgeConfigurationRepository bridgeConfigurationRepository,
+ final StellarAddressResolver stellarAddressResolver,
+ final HorizonServerUtilities horizonServerUtilities,
+ AccountingAdapter accountingAdapter,
+ final EventHelper eventHelper) {
+ this.bridgeConfigurationRepository = bridgeConfigurationRepository;
+ this.stellarAddressResolver = stellarAddressResolver;
+ this.horizonServerUtilities = horizonServerUtilities;
+ this.accountingAdapter = accountingAdapter;
+ this.eventHelper = eventHelper;
+ }
+
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @Transactional
+ public void handle(final FineractPaymentCommand command) {
+
+ final Optional<BridgeConfigurationEntity> accountBridge =
+ bridgeConfigurationRepository.findByTenantIdentifier(command.getTenantIdentifier());
+
+ accountBridge.ifPresent(x -> pay(command, x));
+ }
+
+ private void pay(
+ final FineractPaymentCommand command,
+ final BridgeConfigurationEntity bridgeConfigurationEntity)
+ {
+ final StellarAccountId targetAccountId;
+ targetAccountId = stellarAddressResolver.getAccountIdOfStellarAccount(
+ StellarAddress.forTenant(command.getTargetAccount(), command.getSinkDomain()));
+
+ final char[] decodedStellarPrivateKey =
+ bridgeConfigurationEntity.getStellarAccountPrivateKey();
+
+ horizonServerUtilities.findPathPay(
+ targetAccountId,
+ command.getAmount(), command.getAssetCode(),
+ decodedStellarPrivateKey);
+
+ accountingAdapter.tellFineractPaymentSucceeded(
+ bridgeConfigurationEntity.getFineractStagingAccountIdentifier(),
+ command.getAssetCode(),
+ command.getAmount());
+
+ eventHelper.sendEvent(EventConstants.FINERACT_PAYMENT_PROCESSED, command.getTenantIdentifier(), command.getTransactionIdentifier());
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/StellarPaymentCommandHandler.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/StellarPaymentCommandHandler.java
new file mode 100644
index 0000000..00ea780
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/command/handler/StellarPaymentCommandHandler.java
@@ -0,0 +1,46 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.command.handler;
+
+import java.util.Optional;
+import org.apache.fineract.cn.command.annotation.Aggregate;
+import org.apache.fineract.cn.command.annotation.CommandHandler;
+import org.apache.fineract.cn.command.annotation.CommandLogLevel;
+import org.apache.fineract.cn.stellarbridge.api.v1.events.EventConstants;
+import org.apache.fineract.cn.stellarbridge.service.internal.accounting.AccountingAdapter;
+import org.apache.fineract.cn.stellarbridge.service.internal.command.StellarPaymentCommand;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationEntity;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+@Aggregate
+public class StellarPaymentCommandHandler {
+ private final BridgeConfigurationRepository bridgeConfigurationRepository;
+ private final AccountingAdapter accountingAdapter;
+ private final EventHelper eventHelper;
+
+ @Autowired
+ public StellarPaymentCommandHandler(
+ final BridgeConfigurationRepository bridgeConfigurationRepository,
+ final AccountingAdapter accountingAdapter,
+ final EventHelper eventHelper) {
+ this.bridgeConfigurationRepository = bridgeConfigurationRepository;
+ this.accountingAdapter = accountingAdapter;
+ this.eventHelper = eventHelper;
+ }
+
+
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @Transactional
+ public void handle(final StellarPaymentCommand command) {
+
+ final Optional<BridgeConfigurationEntity> accountBridge =
+ bridgeConfigurationRepository.findByTenantIdentifier(command.getTenantIdentifier());
+
+ final Optional<String> transactionIdentifier = accountBridge.map(x -> accountingAdapter.adjustFineractBalances(
+ x, command.getAmount(), command.getAssetCode()));
+
+ transactionIdentifier.ifPresent(x ->
+ eventHelper.sendEvent(EventConstants.STELLAR_PAYMENT_PROCESSED, command.getTenantIdentifier(), x));
+ }
+
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/StellarBridgeConfiguration.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/config/StellarBridgeConfiguration.java
similarity index 82%
rename from service/src/main/java/org/apache/fineract/cn/stellarbridge/service/StellarBridgeConfiguration.java
rename to service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/config/StellarBridgeConfiguration.java
index 74398be..fec205f 100644
--- a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/StellarBridgeConfiguration.java
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/config/StellarBridgeConfiguration.java
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.cn.stellarbridge.service;
+package org.apache.fineract.cn.stellarbridge.service.internal.config;
import org.apache.fineract.cn.anubis.config.EnableAnubis;
import org.apache.fineract.cn.api.config.EnableApiFactory;
@@ -28,7 +28,8 @@ import org.apache.fineract.cn.lang.config.EnableServiceException;
import org.apache.fineract.cn.lang.config.EnableTenantContext;
import org.apache.fineract.cn.mariadb.config.EnableMariaDB;
import org.apache.fineract.cn.permittedfeignclient.config.EnablePermissionRequestingFeignClient;
-import org.apache.fineract.cn.stellarbridge.service.internal.identity.ApplicationPermissionRequestCreator;
+import org.apache.fineract.cn.stellarbridge.service.ServiceConstants;
+import org.apache.fineract.cn.stellarbridge.service.internal.accounting.JournalEntryCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -50,18 +51,22 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
@EnableAsync
@EnableTenantContext
@EnableCassandra
-@EnableMariaDB
+@EnableMariaDB(forTenantContext = false)
@EnableCommandProcessing
@EnableAnubis
@EnableServiceException
-@EnablePermissionRequestingFeignClient(feignClasses = {ApplicationPermissionRequestCreator.class})
+@EnablePermissionRequestingFeignClient(feignClasses = {JournalEntryCreator.class})
@RibbonClient(name = "rhythm-v1")
@EnableApplicationName
-@EnableFeignClients(clients = {ApplicationPermissionRequestCreator.class})
+@EnableFeignClients(clients = {JournalEntryCreator.class})
@ComponentScan({
"org.apache.fineract.cn.stellarbridge.service.rest",
"org.apache.fineract.cn.stellarbridge.service.internal.service",
+ "org.apache.fineract.cn.stellarbridge.service.internal.config",
"org.apache.fineract.cn.stellarbridge.service.internal.repository",
+ "org.apache.fineract.cn.stellarbridge.service.internal.federation",
+ "org.apache.fineract.cn.stellarbridge.service.internal.horizonadapter",
+ "org.apache.fineract.cn.stellarbridge.service.internal.accounting",
"org.apache.fineract.cn.stellarbridge.service.internal.command.handler"
})
@EnableJpaRepositories({
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/config/StellarBridgeProperties.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/config/StellarBridgeProperties.java
new file mode 100644
index 0000000..5452b0c
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/config/StellarBridgeProperties.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cn.stellarbridge.service.internal.config;
+
+import org.apache.fineract.cn.lang.validation.constraints.ValidIdentifier;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * @author Myrle Krantz
+ */
+@Component
+@ConfigurationProperties(prefix="stellarBridge")
+@Validated
+public class StellarBridgeProperties {
+ @ValidIdentifier
+ private String user;
+
+ private String horizonAddress;
+
+
+ public StellarBridgeProperties() {
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public String getHorizonAddress() {
+ return horizonAddress;
+ }
+
+ public void setHorizonAddress(String horizonAddress) {
+ this.horizonAddress = horizonAddress;
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/ExternalFederationService.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/ExternalFederationService.java
new file mode 100644
index 0000000..0fd46b7
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/ExternalFederationService.java
@@ -0,0 +1,92 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.federation;
+
+import java.io.IOException;
+import org.springframework.stereotype.Service;
+import org.stellar.sdk.federation.Federation;
+import org.stellar.sdk.federation.FederationResponse;
+import org.stellar.sdk.federation.MalformedAddressException;
+
+@Service
+public class ExternalFederationService {
+
+ class StellarResolver
+ { //To make a static function mockable.
+ FederationResponse resolve(final String address)
+ throws IOException, MalformedAddressException {
+ return Federation.resolve(address);
+ }
+ }
+
+ private StellarResolver stellarResolver;
+
+ ExternalFederationService()
+ {
+ this.stellarResolver = new StellarResolver();
+ }
+
+ ExternalFederationService(final StellarResolver stellarResolver)
+ {
+ this.stellarResolver = stellarResolver;
+ }
+
+ /**
+ * Based on the stellar address, finds the stellar account id. Resolves the domain, and calls
+ * the federation service to do so. This only returns an account id if the memo type is id or
+ * there is no memo type.
+ *
+ * @param stellarAddress The stellar address for which to return a stellar account id.
+ * @return The corresponding stellar account id.
+ *
+ * @throws FederationFailedException for the following cases:
+ * * domain server not reachable,
+ * * stellar.toml not parseable for federation server,
+ * * federation server not reachable,
+ * * federation server response does not match expected format.
+ * * memo type is not id.
+ */
+ public StellarAccountId getAccountId(final StellarAddress stellarAddress)
+ throws FederationFailedException
+ {
+ final org.stellar.sdk.federation.FederationResponse federationResponse;
+ try {
+ federationResponse = stellarResolver.resolve(stellarAddress.toString());
+ }
+ catch (final MalformedAddressException e)
+ {
+ throw FederationFailedException.malformedAddress(stellarAddress.toString());
+ }
+ catch (final IOException e)
+ {
+ throw FederationFailedException
+ .domainDoesNotReferToValidFederationServer(stellarAddress.getDomain().toString());
+ }
+
+ if (federationResponse == null)
+ {
+ throw FederationFailedException.addressNameNotFound(stellarAddress.toString());
+ }
+ if (federationResponse.getAccountId() == null)
+ {
+ throw FederationFailedException.addressNameNotFound(stellarAddress.toString());
+ }
+
+ return convertFederationResponseToStellarAddress(federationResponse);
+ }
+
+ private StellarAccountId convertFederationResponseToStellarAddress(
+ final org.stellar.sdk.federation.FederationResponse response)
+ {
+ if (response.getMemoType().equalsIgnoreCase("text"))
+ {
+ return StellarAccountId.subAccount(response.getAccountId(), response.getMemo());
+ }
+ else if (response.getMemoType() == null || response.getMemoType().isEmpty())
+ {
+ return StellarAccountId.mainAccount(response.getAccountId());
+ }
+ else
+ {
+ throw FederationFailedException.addressRequiresUnsupportedMemoType(response.getMemoType());
+ }
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/FederationFailedException.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/FederationFailedException.java
new file mode 100644
index 0000000..09effbd
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/FederationFailedException.java
@@ -0,0 +1,37 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.federation;
+
+public class FederationFailedException extends RuntimeException {
+ private FederationFailedException(final String message) { super(message);
+ }
+
+ static FederationFailedException domainDoesNotReferToValidFederationServer
+ (final String domain)
+ {
+ return new FederationFailedException(
+ "The federation server for the given domain could not be reached: " + domain);
+ }
+
+ static FederationFailedException addressRequiresUnsupportedMemoType(final String memoType)
+ {
+ return new FederationFailedException(
+ "The given federation address returned an unsupported memo type: " + memoType);
+ }
+
+ static FederationFailedException wrongDomain(final String domain) {
+ return new FederationFailedException("Wrong domain: " + domain);
+ }
+
+ static FederationFailedException addressNameNotFound(final String address) {
+ return new FederationFailedException("The address name is not found: " + address);
+ }
+
+ static FederationFailedException malformedAddress(final String address) {
+ return new FederationFailedException("The address is not a valid stellar address: " + address);
+ }
+
+ static FederationFailedException needTopLevelStellarAccount
+ (final String address) {
+ return new FederationFailedException(
+ "Need top level Stellar account: " + address);
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/InvalidStellarAddressException.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/InvalidStellarAddressException.java
new file mode 100644
index 0000000..8effca3
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/InvalidStellarAddressException.java
@@ -0,0 +1,17 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.federation;
+
+public class InvalidStellarAddressException extends RuntimeException {
+ private InvalidStellarAddressException(final String message) {
+ super(message);
+ }
+
+ static InvalidStellarAddressException invalidDomainName(final String domainName) {
+ return new InvalidStellarAddressException("Domain name is not valid: " + domainName);
+ }
+
+ static InvalidStellarAddressException nonConformantStellarAddress(final String address) {
+ return new InvalidStellarAddressException(
+ "Non-conformant stellar address: " + address
+ );
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAccountId.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAccountId.java
new file mode 100644
index 0000000..d9eb45a
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAccountId.java
@@ -0,0 +1,48 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.federation;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class StellarAccountId {
+ private final String publicKey;
+ private final Optional<String> subAccount;
+
+ static public StellarAccountId mainAccount(final String publicKey)
+ {
+ return new StellarAccountId(publicKey, Optional.empty());
+ }
+
+ static public StellarAccountId subAccount(final String mainAccountPublicKey,
+ final String subAccountId)
+ {
+ //TODO: Check what the correct form of the memo should be here...
+ return new StellarAccountId(mainAccountPublicKey, Optional.of(subAccountId));
+ }
+
+ private StellarAccountId(final String publicKey, final Optional<String> subAccount)
+ {
+ this.publicKey = publicKey;
+ this.subAccount = subAccount;
+ }
+
+ public String getPublicKey() {
+ return publicKey;
+ }
+
+ public Optional<String> getSubAccount() {
+ return subAccount;
+ }
+
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ StellarAccountId that = (StellarAccountId) o;
+ return Objects.equals(publicKey, that.publicKey) && Objects.equals(subAccount, that.subAccount);
+ }
+
+ @Override public int hashCode() {
+ return Objects.hash(publicKey, subAccount);
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAddress.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAddress.java
new file mode 100644
index 0000000..626f964
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAddress.java
@@ -0,0 +1,119 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.federation;
+
+import com.google.common.net.InternetDomainName;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class StellarAddress {
+
+ private final InternetDomainName domain;
+ private final String tenantName;
+ private final Optional<String> userAccountId;
+ private final boolean isVaultAddress;
+
+ public static StellarAddress forTenant(final String tenantName, final String domain)
+ {
+ return new StellarAddress(InternetDomainName.from(domain), tenantName, Optional.empty());
+ }
+
+ public static StellarAddress parse(final String address)
+ throws InvalidStellarAddressException
+ {
+ //I chose not to use Pattern.UNICODE_CHARACTER_CLASS because of potential performance issues and
+ //no current knowledge of use cases which require unicode addresses. Certainly the domain
+ //name can't contain unicode characters. According to the federation servers specs at
+ //Stellar, the part before the * might contain them. Depending on what use cases we encounter,
+ //we may need to adjust this.
+ final Pattern stellarAddressPattern = Pattern.compile(
+ "(?<name>^[^\\:\\*@\\p{Space}]+)(:(?<subname>[^\\:\\*@\\p{Space}]+))?+\\*(?<domain>[\\p{Alnum}-\\.]+)$");
+
+ final Matcher addressMatcher = stellarAddressPattern.matcher(address);
+ if (!addressMatcher.matches()) {
+ throw InvalidStellarAddressException.nonConformantStellarAddress(address);
+ }
+
+ if (addressMatcher.group("subname") != null)
+ {
+ return new StellarAddress(getInternetDomainName(addressMatcher.group("domain")),
+ addressMatcher.group("name"), Optional.of(addressMatcher.group("subname")));
+ }
+ else
+ {
+ return new StellarAddress(getInternetDomainName(addressMatcher.group("domain")),
+ addressMatcher.group("name"), Optional.empty());
+ }
+ }
+
+ private static InternetDomainName getInternetDomainName(final String domain)
+ throws InvalidStellarAddressException
+ {
+ try {
+ return InternetDomainName.from(domain);
+ }
+ catch (final IllegalArgumentException e)
+ {
+ throw InvalidStellarAddressException.invalidDomainName(domain);
+ }
+ }
+
+ private StellarAddress(
+ final InternetDomainName domain,
+ final String tenantName,
+ final Optional<String> userAccountId)
+ {
+ this.domain = domain;
+ this.tenantName = tenantName;
+ if (userAccountId.orElse("").equals("vault")) {
+ isVaultAddress = true;
+ this.userAccountId = Optional.empty();
+ }
+ else
+ {
+ isVaultAddress = false;
+ this.userAccountId = userAccountId;
+ }
+ }
+
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (!(o instanceof StellarAddress))
+ return false;
+ StellarAddress that = (StellarAddress) o;
+ return isVaultAddress == that.isVaultAddress &&
+ Objects.equals(domain, that.domain) &&
+ Objects.equals(tenantName, that.tenantName) &&
+ Objects.equals(userAccountId, that.userAccountId);
+ }
+
+ @Override public int hashCode() {
+ return Objects.hash(domain, tenantName, userAccountId, isVaultAddress);
+ }
+
+ public String toString() {
+ if (isVaultAddress) {
+ return tenantName + ":" + "vault" + "*" + domain.toString();
+ }
+ else
+ return userAccountId.map(s -> tenantName + ":" + s + "*" + domain.toString())
+ .orElseGet(() -> tenantName + "*" + domain.toString());
+ }
+
+ public InternetDomainName getDomain() {
+ return domain;
+ }
+
+ public String getTenantName() {
+ return tenantName;
+ }
+
+ public Optional<String> getUserAccountId() {
+ return userAccountId;
+ }
+
+ public boolean isVaultAddress() {
+ return isVaultAddress;
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAddressResolver.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAddressResolver.java
new file mode 100644
index 0000000..f8b404f
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/federation/StellarAddressResolver.java
@@ -0,0 +1,21 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.federation;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class StellarAddressResolver {
+ private final ExternalFederationService externalFederationService;
+
+ @Autowired
+ public StellarAddressResolver(
+ final ExternalFederationService externalFederationService) {
+ this.externalFederationService = externalFederationService;
+ }
+
+ public StellarAccountId getAccountIdOfStellarAccount(final StellarAddress stellarAddress)
+ throws FederationFailedException
+ {
+ return externalFederationService.getAccountId(stellarAddress);
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerEffectsListener.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerEffectsListener.java
new file mode 100644
index 0000000..f9726a9
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerEffectsListener.java
@@ -0,0 +1,132 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.horizonadapter;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.Optional;
+import org.apache.fineract.cn.command.gateway.CommandGateway;
+import org.apache.fineract.cn.stellarbridge.service.ServiceConstants;
+import org.apache.fineract.cn.stellarbridge.service.internal.command.StellarPaymentCommand;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationEntity;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationRepository;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.StellarCursorEntity;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.StellarCursorRepository;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+import org.stellar.sdk.Asset;
+import org.stellar.sdk.AssetTypeCreditAlphaNum;
+import org.stellar.sdk.requests.EventListener;
+import org.stellar.sdk.responses.effects.AccountCreditedEffectResponse;
+import org.stellar.sdk.responses.effects.AccountDebitedEffectResponse;
+import org.stellar.sdk.responses.effects.EffectResponse;
+
+@Component
+public class HorizonServerEffectsListener implements EventListener<EffectResponse> {
+
+ private final BridgeConfigurationRepository accountBridgeRepository;
+ private final StellarCursorRepository stellarCursorRepository;
+ private final CommandGateway commandGateway;
+
+ private final Logger logger;
+
+
+ @Autowired
+ HorizonServerEffectsListener(
+ final BridgeConfigurationRepository accountBridgeRepository,
+ final StellarCursorRepository stellarCursorRepository,
+ final CommandGateway commandGateway,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger)
+ {
+ this.accountBridgeRepository = accountBridgeRepository;
+ this.stellarCursorRepository = stellarCursorRepository;
+ this.commandGateway = commandGateway;
+ this.logger = logger;
+ }
+
+ @Override public void onEvent(final EffectResponse operation) {
+ final String pagingToken = operation.getPagingToken();
+
+ //This is important, because an event can be sent twice if we are managing both the sending and
+ //receiving account. We need to be certain we process it only once.
+ final StellarCursorEntity cursorPersistency = markPlace(pagingToken);
+ if (cursorPersistency.getProcessed())
+ return;
+
+ logger.info("Operation with cursor {}", pagingToken);
+
+ handleOperation(operation);
+
+ cursorPersistency.setProcessed(true);
+ stellarCursorRepository.save(cursorPersistency);
+ }
+
+ StellarCursorEntity markPlace(final String pagingToken)
+ {
+ synchronized (stellarCursorRepository) {
+ final Optional<StellarCursorEntity> entry =
+ stellarCursorRepository.findByCursor(pagingToken);
+
+ return entry.orElse(
+ stellarCursorRepository.save(new StellarCursorEntity(pagingToken, new Date())));
+ }
+ }
+
+ private void handleOperation(final EffectResponse effect) {
+
+ if (effect instanceof AccountCreditedEffectResponse)
+ {
+ final AccountCreditedEffectResponse accountCreditedEffect = (AccountCreditedEffectResponse) effect;
+ final BridgeConfigurationEntity toAccount
+ = accountBridgeRepository.findByStellarAccountIdentifier(effect.getAccount().getAccountId());
+ if (toAccount == null)
+ return; //Nothing to do. Not one of ours.
+
+ final BigDecimal amount
+ = StellarAccountHelpers.stellarBalanceToBigDecimal(accountCreditedEffect.getAmount());
+ final Asset asset = accountCreditedEffect.getAsset();
+ final String assetCode = StellarAccountHelpers.getAssetCode(asset);
+ final String issuer = StellarAccountHelpers.getIssuer(asset);
+
+ logger.info("Credit to {} of {}, in currency {}@{}",
+ toAccount.getTenantIdentifier(), amount, assetCode, issuer);
+
+ //TODO: This will prevent lumens from being registered in the mifos account (likewise below in debit)...
+ if (!(asset instanceof AssetTypeCreditAlphaNum))
+ return;
+
+ final StellarPaymentCommand receivePaymentCommand =
+ new StellarPaymentCommand(toAccount.getTenantIdentifier(), assetCode, amount);
+ commandGateway.process(receivePaymentCommand);
+ }
+ else if (effect instanceof AccountDebitedEffectResponse)
+ {
+ final AccountDebitedEffectResponse accountDebitedEffect = (AccountDebitedEffectResponse)effect;
+
+ final BridgeConfigurationEntity toAccount = accountBridgeRepository
+ .findByStellarAccountIdentifier(accountDebitedEffect.getAccount().getAccountId());
+ if (toAccount == null)
+ return; //Nothing to do. Not one of ours.
+
+ final BigDecimal amount
+ = StellarAccountHelpers.stellarBalanceToBigDecimal(accountDebitedEffect.getAmount());
+ final Asset asset = accountDebitedEffect.getAsset();
+ final String assetCode = StellarAccountHelpers.getAssetCode(asset);
+ final String issuer = StellarAccountHelpers.getIssuer(asset);
+
+ logger.info("Debit to {} of {}, in currency {}@{}",
+ toAccount.getTenantIdentifier(), amount, assetCode, issuer);
+
+ if (!(asset instanceof AssetTypeCreditAlphaNum))
+ return;
+
+ final StellarPaymentCommand receivePaymentCommand =
+ new StellarPaymentCommand(toAccount.getTenantIdentifier(), assetCode, amount.negate());
+ commandGateway.process(receivePaymentCommand);
+ }
+ else
+ {
+ logger.info("Effect of type {}", effect.getType());
+ }
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerPaymentObserver.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerPaymentObserver.java
new file mode 100644
index 0000000..53e9acd
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerPaymentObserver.java
@@ -0,0 +1,82 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.horizonadapter;
+
+import java.net.URI;
+import java.util.Optional;
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.NotNull;
+import org.apache.fineract.cn.stellarbridge.service.ServiceConstants;
+import org.apache.fineract.cn.stellarbridge.service.internal.config.StellarBridgeProperties;
+import org.apache.fineract.cn.stellarbridge.service.internal.federation.StellarAccountId;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationRepository;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.StellarCursorEntity;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.StellarCursorRepository;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+import org.stellar.sdk.KeyPair;
+import org.stellar.sdk.requests.EffectsRequestBuilder;
+
+@Component
+public class HorizonServerPaymentObserver {
+ private final StellarBridgeProperties stellarBridgeProperties;
+
+ private final BridgeConfigurationRepository bridgeConfigurationRepository;
+ private final StellarCursorRepository stellarCursorRepository;
+ private final HorizonServerEffectsListener listener;
+ private final Logger logger;
+
+ @PostConstruct
+ void init()
+ {
+ final Optional<String> cursor = getCurrentCursor();
+
+ bridgeConfigurationRepository.findAll()
+ .forEach(config -> setupListeningForAccount(
+ StellarAccountId.mainAccount(config.getStellarAccountIdentifier()), cursor));
+ }
+
+ @Autowired
+ HorizonServerPaymentObserver(
+ final StellarBridgeProperties stellarBridgeProperties,
+ final BridgeConfigurationRepository bridgeConfigurationRepository,
+ final StellarCursorRepository stellarCursorRepository,
+ final HorizonServerEffectsListener listener,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger)
+ {
+ this.stellarBridgeProperties = stellarBridgeProperties;
+ this.bridgeConfigurationRepository = bridgeConfigurationRepository;
+ this.stellarCursorRepository = stellarCursorRepository;
+
+ this.listener = listener;
+
+ this.logger = logger;
+ }
+
+ public void setupListeningForAccount(final StellarAccountId stellarAccountId)
+ {
+ setupListeningForAccount(stellarAccountId, Optional.empty());
+ }
+
+ private Optional<String> getCurrentCursor() {
+ final Optional<StellarCursorEntity> cursorPersistency
+ = stellarCursorRepository.findTopByProcessedTrueOrderByCreatedOnDesc();
+
+ return cursorPersistency.map(StellarCursorEntity::getCursor);
+ }
+
+ private void setupListeningForAccount(
+ @NotNull final StellarAccountId stellarAccountId, @NotNull final Optional<String> cursor)
+ {
+ logger.info("HorizonServerPaymentObserver.setupListeningForAccount {}, cursor {}",
+ stellarAccountId.getPublicKey(), cursor);
+
+ final EffectsRequestBuilder effectsRequestBuilder
+ = new EffectsRequestBuilder(URI.create(stellarBridgeProperties.getHorizonAddress()));
+ effectsRequestBuilder.forAccount(KeyPair.fromAccountId(stellarAccountId.getPublicKey()));
+ cursor.ifPresent(effectsRequestBuilder::cursor);
+
+ effectsRequestBuilder.stream(listener);
+ }
+
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerUtilities.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerUtilities.java
new file mode 100644
index 0000000..cb86bda
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/HorizonServerUtilities.java
@@ -0,0 +1,301 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.horizonadapter;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URISyntaxException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+import javax.annotation.PostConstruct;
+import org.apache.fineract.cn.stellarbridge.service.ServiceConstants;
+import org.apache.fineract.cn.stellarbridge.service.internal.config.StellarBridgeProperties;
+import org.apache.fineract.cn.stellarbridge.service.internal.federation.StellarAccountId;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+import org.stellar.sdk.Account;
+import org.stellar.sdk.Asset;
+import org.stellar.sdk.KeyPair;
+import org.stellar.sdk.Memo;
+import org.stellar.sdk.PathPaymentOperation;
+import org.stellar.sdk.Server;
+import org.stellar.sdk.Transaction;
+import org.stellar.sdk.responses.AccountResponse;
+import org.stellar.sdk.responses.Page;
+import org.stellar.sdk.responses.PathResponse;
+import org.stellar.sdk.responses.SubmitTransactionResponse;
+
+@Component
+public class HorizonServerUtilities {
+ private final StellarBridgeProperties stellarBridgeProperties;
+ private final Logger logger;
+
+ private Server server;
+
+ private final LoadingCache<String, Account> accounts;
+
+ @Autowired
+ HorizonServerUtilities(
+ final StellarBridgeProperties stellarBridgeProperties,
+ @Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger)
+ {
+ this.stellarBridgeProperties = stellarBridgeProperties;
+ this.logger = logger;
+
+ accounts = CacheBuilder.newBuilder().build(
+ new CacheLoader<String, Account>() {
+ public Account load(final String accountId)
+ throws InvalidConfigurationException {
+ final KeyPair accountKeyPair = KeyPair.fromAccountId(accountId);
+ final StellarAccountHelpers accountHelper = getAccount(accountKeyPair);
+ final Long sequenceNumber = accountHelper.get().getSequenceNumber();
+ return new Account(accountKeyPair, sequenceNumber);
+ }
+ });
+ }
+
+ @PostConstruct
+ void init()
+ {
+ server = new Server(stellarBridgeProperties.getHorizonAddress());
+ }
+
+ public void simplePay(
+ final StellarAccountId targetAccountId,
+ final BigDecimal amount,
+ final String assetCode,
+ final StellarAccountId issuingAccountId,
+ final char[] stellarAccountPrivateKey)
+ throws InvalidConfigurationException, StellarPaymentFailedException
+ {
+ logger.info("HorizonServerUtilities.simplePay");
+ final Asset asset = StellarAccountHelpers.getAsset(assetCode, issuingAccountId);
+
+ pay(targetAccountId, amount, asset, asset, stellarAccountPrivateKey);
+ }
+
+ private void pay(
+ final StellarAccountId targetAccountId,
+ final BigDecimal amount,
+ final Asset sendAsset,
+ final Asset receiveAsset,
+ final char[] stellarAccountPrivateKey)
+ throws InvalidConfigurationException, StellarPaymentFailedException
+ {
+ final KeyPair sourceAccountKeyPair = KeyPair.fromSecretSeed(stellarAccountPrivateKey);
+ final KeyPair targetAccountKeyPair = KeyPair.fromAccountId(targetAccountId.getPublicKey());
+
+ final Account sourceAccount = accounts.getUnchecked(sourceAccountKeyPair.getAccountId());
+
+ final Transaction.Builder transferTransactionBuilder
+ = new Transaction.Builder(sourceAccount);
+ final PathPaymentOperation paymentOperation =
+ new PathPaymentOperation.Builder(
+ sendAsset,
+ StellarAccountHelpers.bigDecimalToStellarBalance(amount),
+ targetAccountKeyPair,
+ receiveAsset,
+ StellarAccountHelpers.bigDecimalToStellarBalance(amount))
+ .setSourceAccount(sourceAccountKeyPair).build();
+
+ transferTransactionBuilder.addOperation(paymentOperation);
+
+ if (targetAccountId.getSubAccount().isPresent())
+ {
+ final Memo subAccountMemo = Memo.text(targetAccountId.getSubAccount().get());
+ transferTransactionBuilder.addMemo(subAccountMemo);
+ }
+
+ submitTransaction(sourceAccount, transferTransactionBuilder, sourceAccountKeyPair,
+ StellarPaymentFailedException::transactionFailed);
+ }
+
+ public BigDecimal getBalance(
+ final StellarAccountId stellarAccountId,
+ final String assetCode)
+ {
+ logger.info("HorizonServerUtilities.getBalance");
+ return getAccount(KeyPair.fromAccountId(stellarAccountId.getPublicKey())).getBalance(assetCode);
+ }
+
+ public BigDecimal getBalanceByIssuer(
+ final StellarAccountId stellarAccountId,
+ final String assetCode,
+ final StellarAccountId accountIdOfIssuingStellarAddress)
+ throws InvalidConfigurationException
+ {
+ logger.info("HorizonServerUtilities.getBalanceByIssuer");
+
+ final Asset asset = StellarAccountHelpers.getAsset(assetCode, accountIdOfIssuingStellarAddress);
+
+ return getAccount(KeyPair.fromAccountId(stellarAccountId.getPublicKey()))
+ .getBalanceOfAsset(asset);
+ }
+
+ private StellarAccountHelpers getAccount(final KeyPair installationAccountKeyPair)
+ throws InvalidConfigurationException
+ {
+ final AccountResponse installationAccount;
+ try {
+ installationAccount = server.accounts().account(installationAccountKeyPair);
+ }
+ catch (final IOException e) {
+ throw InvalidConfigurationException.unreachableStellarServerAddress(stellarBridgeProperties.getHorizonAddress());
+ }
+
+ if (installationAccount == null)
+ {
+ throw InvalidConfigurationException.invalidInstallationAccountSecretSeed();
+ }
+
+ return new StellarAccountHelpers(installationAccount);
+ }
+
+
+ public void findPathPay(
+ final StellarAccountId targetAccountId,
+ final BigDecimal amount,
+ final String assetCode,
+ final char[] stellarAccountPrivateKey)
+ throws InvalidConfigurationException, StellarPaymentFailedException
+ {
+ logger.info("HorizonServerUtilities.findPathPay");
+ final KeyPair sourceAccountKeyPair = KeyPair.fromSecretSeed(stellarAccountPrivateKey);
+ final KeyPair targetAccountKeyPair = KeyPair.fromAccountId(targetAccountId.getPublicKey());
+
+ final StellarAccountHelpers sourceAccount = getAccount(sourceAccountKeyPair);
+ final StellarAccountHelpers targetAccount = getAccount(targetAccountKeyPair);
+
+ final Set<Asset> targetAssets = targetAccount.findAssetsWithTrust(amount, assetCode);
+ final Set<Asset> sourceAssets = sourceAccount.findAssetsWithBalance(amount, assetCode);
+
+ final Optional<MatchingAssetPair> assetPair = findAnyMatchingAssetPair(
+ amount, sourceAssets, targetAssets, sourceAccountKeyPair, targetAccountKeyPair);
+ if (!assetPair.isPresent())
+ throw StellarPaymentFailedException.noPathExists(assetCode);
+
+ pay(targetAccountId, amount,
+ assetPair.get().asset1, assetPair.get().asset2,
+ stellarAccountPrivateKey);
+ }
+
+ static class MatchingAssetPair {
+ final Asset asset1;
+ final Asset asset2;
+
+ MatchingAssetPair(Asset asset1, Asset asset2) {
+ this.asset1 = asset1;
+ this.asset2 = asset2;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MatchingAssetPair that = (MatchingAssetPair) o;
+ return Objects.equals(asset1, that.asset1) &&
+ Objects.equals(asset2, that.asset2);
+ }
+
+ @Override
+ public int hashCode() {
+
+ return Objects.hash(asset1, asset2);
+ }
+ }
+
+ private Optional<MatchingAssetPair> findAnyMatchingAssetPair(
+ final BigDecimal amount,
+ final Set<Asset> sourceAssets,
+ final Set<Asset> targetAssets,
+ final KeyPair sourceAccountKeyPair,
+ final KeyPair targetAccountKeyPair) {
+ if (sourceAssets.isEmpty())
+ return Optional.empty();
+
+ for (final Asset targetAsset : targetAssets) {
+ Page<PathResponse> paths;
+ try {
+ paths = server.paths()
+ .sourceAccount(sourceAccountKeyPair)
+ .destinationAccount(targetAccountKeyPair)
+ .destinationAsset(targetAsset)
+ .destinationAmount(StellarAccountHelpers.bigDecimalToStellarBalance(amount))
+ .execute();
+ } catch (final IOException e) {
+ return Optional.empty();
+ }
+
+ while (paths != null && paths.getRecords() != null) {
+ for (final PathResponse path : paths.getRecords())
+ {
+ if (StellarAccountHelpers.stellarBalanceToBigDecimal(path.getSourceAmount()).compareTo(amount) <= 0)
+ {
+ if (sourceAssets.contains(path.getSourceAsset()))
+ {
+ return Optional.of(new MatchingAssetPair(path.getSourceAsset(), targetAsset));
+ }
+ }
+ }
+
+ try {
+ paths = ((paths.getLinks() == null) || (paths.getLinks().getNext() == null)) ?
+ null : paths.getNextPage();
+ } catch (final URISyntaxException | IOException e) {
+ return Optional.empty();
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ private <T extends Exception> void submitTransaction(
+ final Account transactionSubmitter,
+ final Transaction.Builder transactionBuilder,
+ final KeyPair signingKeyPair,
+ final Supplier<T> failureHandler)
+ throws T
+ {
+ try {
+ //final Long sequenceNumberSubmitted = account.getSequenceNumber();
+
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (transactionSubmitter) {
+ final Transaction transaction = transactionBuilder.build();
+ transaction.sign(signingKeyPair);
+ final SubmitTransactionResponse transactionResponse = server.submitTransaction(transaction);
+ if (!transactionResponse.isSuccess()) {
+ if (transactionResponse.getExtras() != null) {
+ logger.info("Stellar transaction failed, request: {}", transactionResponse.getExtras().getEnvelopeXdr());
+ logger.info("Stellar transaction failed, response: {}", transactionResponse.getExtras().getResultXdr());
+ }
+ else
+ {
+ logger.info("Stellar transaction failed. No extra information available.");
+ }
+ //TODO: resend transaction if you get a bad sequence.
+ /*Thread.sleep(6000); //Wait for ledger to close.
+ Long sequenceNumberShouldHaveBeen =
+ server.accounts().account(account.getKeypair()).getSequenceNumber();
+ if (sequenceNumberSubmitted != sequenceNumberShouldHaveBeen) {
+ logger.info("Sequence number submitted: {}, Sequence number should have been: {}",
+ sequenceNumberSubmitted, sequenceNumberShouldHaveBeen);
+ }*/
+ throw failureHandler.get();
+ }
+ }
+ } catch (final IOException e) {
+ throw InvalidConfigurationException.unreachableStellarServerAddress(stellarBridgeProperties.getHorizonAddress());
+ }
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/InvalidConfigurationException.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/InvalidConfigurationException.java
new file mode 100644
index 0000000..f0417cb
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/InvalidConfigurationException.java
@@ -0,0 +1,20 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.horizonadapter;
+
+public class InvalidConfigurationException extends RuntimeException {
+ private InvalidConfigurationException(final String message)
+ {
+ super(message);
+ }
+
+ static InvalidConfigurationException invalidInstallationAccountSecretSeed() {
+ return new InvalidConfigurationException(
+ "Invalid installation account secret seed. Have your admin check configuration.");
+ }
+
+ static InvalidConfigurationException unreachableStellarServerAddress(
+ final String serverAddress) {
+ return new InvalidConfigurationException(
+ "Unreachable stellar server address: " + serverAddress +
+ ". Have your admin check configuration.");
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/StellarAccountHelpers.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/StellarAccountHelpers.java
new file mode 100644
index 0000000..1ad52a0
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/StellarAccountHelpers.java
@@ -0,0 +1,184 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.horizonadapter;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.fineract.cn.stellarbridge.service.internal.federation.StellarAccountId;
+import org.stellar.sdk.Asset;
+import org.stellar.sdk.AssetTypeCreditAlphaNum;
+import org.stellar.sdk.AssetTypeNative;
+import org.stellar.sdk.KeyPair;
+import org.stellar.sdk.responses.AccountResponse;
+import org.stellar.sdk.responses.AccountResponse.Balance;
+
+class StellarAccountHelpers {
+ static String getAssetCode(final Asset asset) {
+ if (asset instanceof AssetTypeCreditAlphaNum)
+ {
+ return ((AssetTypeCreditAlphaNum)asset).getCode();
+ }
+ else
+ {
+ return "XLM";
+ }
+ }
+
+ static String getIssuer(final Asset asset) {
+ if (asset instanceof AssetTypeCreditAlphaNum)
+ {
+ return ((AssetTypeCreditAlphaNum)asset).getIssuer().getAccountId();
+ }
+ else
+ {
+ return "stellar";
+ }
+ }
+
+ static boolean balanceIsInAsset(final AccountResponse.Balance balance, final String assetCode)
+ {
+ if (balance.getAssetType() == null)
+ return false;
+
+ if (balance.getAssetCode() == null) {
+ return assetCode.equals("XLM") && balance.getAssetType().equals("native");
+ }
+
+ return balance.getAssetCode().equals(assetCode);
+ }
+
+ static Asset getAssetOfBalance(final AccountResponse.Balance balance)
+ {
+ if (balance.getAssetCode() == null)
+ return new AssetTypeNative();
+ else
+ return Asset.createNonNativeAsset(balance.getAssetCode(),
+ KeyPair.fromAccountId(balance.getAssetIssuer()));
+ }
+
+ static BigDecimal stellarBalanceToBigDecimal(final String balance)
+ {
+ return BigDecimal.valueOf(Double.parseDouble(balance));
+ }
+
+ static String bigDecimalToStellarBalance(final BigDecimal balance)
+ {
+ return balance.toString();
+ }
+
+
+ static Asset getAsset(final String assetCode, final StellarAccountId targetIssuer) {
+ return Asset.createNonNativeAsset(assetCode, KeyPair.fromAccountId(targetIssuer.getPublicKey()));
+ }
+
+ static BigDecimal remainingTrustInBalance(final AccountResponse.Balance balance)
+ {
+ return stellarBalanceToBigDecimal(balance.getLimit())
+ .subtract(stellarBalanceToBigDecimal(balance.getBalance()));
+ }
+
+ private final AccountResponse account;
+
+ StellarAccountHelpers(final AccountResponse account)
+ {
+ this.account = account;
+ }
+
+ AccountResponse get()
+ {
+ return account;
+ }
+
+ BigDecimal getBalanceOfAsset(final Asset asset)
+ {
+ return getNumericAspectOfAsset(asset,
+ balance -> stellarBalanceToBigDecimal(balance.getBalance()));
+ }
+
+ BigDecimal getNumericAspectOfAsset(
+ final Asset asset,
+ final Function<Balance, BigDecimal> aspect)
+ {
+ final Optional<BigDecimal> balanceOfGivenAsset
+ = Arrays.stream(account.getBalances())
+ .filter(balance -> getAssetOfBalance(balance).equals(asset))
+ .map(aspect)
+ .max(BigDecimal::compareTo);
+
+ //Theoretically there shouldn't be more than one balance, but if this should turn out to be
+ //incorrect, we return the largest one, rather than adding them together.
+
+ return balanceOfGivenAsset.orElse(BigDecimal.ZERO);
+ }
+
+ BigDecimal getBalance(final String assetCode) {
+ final AccountResponse.Balance[] balances = account.getBalances();
+
+ return Arrays.stream(balances)
+ .filter(balance -> balanceIsInAsset(balance, assetCode))
+ .map(balance -> stellarBalanceToBigDecimal(balance.getBalance()))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ Set<Asset> findAssetsWithBalance(
+ final BigDecimal amount,
+ final String assetCode) {
+
+ return findAssetsWithAspect(amount, assetCode,
+ balance -> stellarBalanceToBigDecimal(balance.getBalance()));
+ }
+
+ Set<Asset> findAssetsWithTrust(
+ final BigDecimal amount,
+ final String assetCode) {
+
+ return findAssetsWithAspect(amount, assetCode,
+ StellarAccountHelpers::remainingTrustInBalance);
+ }
+
+ private Set<Asset> findAssetsWithAspect(
+ final BigDecimal amount,
+ final String assetCode,
+ final Function<AccountResponse.Balance, BigDecimal> numericAspect)
+ {
+ return Arrays.stream(account.getBalances())
+ .filter(balance -> balanceIsInAsset(balance, assetCode))
+ .filter(balance -> numericAspect.apply(balance).compareTo(amount) >= 0)
+ .sorted(Comparator.comparing(numericAspect::apply))
+ .map(StellarAccountHelpers::getAssetOfBalance)
+ .collect(Collectors.toSet());
+ }
+
+ Stream<Balance> getAllNonnativeBalancesStream(final String assetCode, final Asset vaultAsset)
+ {
+ return Arrays.stream(account.getBalances())
+ .filter(balance -> balanceIsInAsset(balance, assetCode))
+ .filter(balance -> !getAssetOfBalance(balance).equals(vaultAsset));
+ }
+
+ public BigDecimal getRemainingTrustInAsset(final Asset asset) {
+ return getTrustInAsset(asset).subtract(getBalanceOfAsset(asset));
+ }
+
+ public BigDecimal getTrustInAsset(final Asset asset) {
+ return getNumericAspectOfAsset(asset,
+ balance -> stellarBalanceToBigDecimal(balance.getLimit()));
+ }
+
+ public Stream<AccountResponse.Balance> getVaultBalancesStream(final String stellarVaultAccountId) {
+ return Arrays.stream(account.getBalances())
+ .filter(balance -> balance.getAssetIssuer() != null)
+ .filter(balance -> balance.getAssetIssuer().equals(stellarVaultAccountId))
+ .filter(balance -> stellarBalanceToBigDecimal(balance.getLimit()).compareTo(BigDecimal.ZERO) != 0);
+ }
+
+ public Stream<AccountResponse.Balance> getAllNonnativeBalancesStream() {
+ return Arrays.stream(account.getBalances())
+ .filter(balance -> balance.getAssetIssuer() != null)
+ .filter(balance -> stellarBalanceToBigDecimal(balance.getBalance()).compareTo(BigDecimal.ZERO) != 0);
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/StellarPaymentFailedException.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/StellarPaymentFailedException.java
new file mode 100644
index 0000000..d9f4b3e
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/horizonadapter/StellarPaymentFailedException.java
@@ -0,0 +1,16 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.horizonadapter;
+
+public class StellarPaymentFailedException extends RuntimeException {
+ private StellarPaymentFailedException(final String msg) {
+ super(msg);
+ }
+
+ static StellarPaymentFailedException noPathExists(final String assetCode) {
+ return new StellarPaymentFailedException("No path exists in the given currency: " + assetCode);
+ }
+
+ static StellarPaymentFailedException transactionFailed() {
+ return new StellarPaymentFailedException(
+ "Stellar Horizon server did not accept payment for unknown reason.");
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationEntity.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationEntity.java
index 544d076..9c582eb 100644
--- a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationEntity.java
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationEntity.java
@@ -36,8 +36,12 @@ public class BridgeConfigurationEntity {
private String fineractIncomingAccountIdentifier;
@Column(name = "fineract_outgoing_identifier")
private String fineractOutgoingAccountIdentifier;
+ @Column(name = "fineract_staging_identifier")
+ private String fineractStagingAccountIdentifier;
@Column(name = "stellar_identifier")
private String stellarAccountIdentifier;
+ @Column(name = "stellar_account_private_key")
+ private char[] stellarAccountPrivateKey;
public BridgeConfigurationEntity() {
super();
@@ -75,6 +79,14 @@ public class BridgeConfigurationEntity {
this.fineractOutgoingAccountIdentifier = fineractOutgoingAccountIdentifier;
}
+ public String getFineractStagingAccountIdentifier() {
+ return fineractStagingAccountIdentifier;
+ }
+
+ public void setFineractStagingAccountIdentifier(String fineractStagingAccountIdentifier) {
+ this.fineractStagingAccountIdentifier = fineractStagingAccountIdentifier;
+ }
+
public String getStellarAccountIdentifier() {
return stellarAccountIdentifier;
}
@@ -83,6 +95,14 @@ public class BridgeConfigurationEntity {
this.stellarAccountIdentifier = stellarAccountIdentifier;
}
+ public char[] getStellarAccountPrivateKey() {
+ return stellarAccountPrivateKey;
+ }
+
+ public void setStellarAccountPrivateKey(char[] stellarAccountPrivateKey) {
+ this.stellarAccountPrivateKey = stellarAccountPrivateKey;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationEntityRepository.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationRepository.java
similarity index 85%
rename from service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationEntityRepository.java
rename to service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationRepository.java
index 28839ad..358d064 100644
--- a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationEntityRepository.java
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/BridgeConfigurationRepository.java
@@ -23,6 +23,8 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
-public interface BridgeConfigurationEntityRepository extends JpaRepository<BridgeConfigurationEntity, Long> {
+public interface BridgeConfigurationRepository extends JpaRepository<BridgeConfigurationEntity, Long> {
Optional<BridgeConfigurationEntity> findByTenantIdentifier(String identifier);
+
+ BridgeConfigurationEntity findByStellarAccountIdentifier(String accountId);
}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/StellarCursorEntity.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/StellarCursorEntity.java
new file mode 100644
index 0000000..c69bc17
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/StellarCursorEntity.java
@@ -0,0 +1,64 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.repository;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import java.util.Date;
+
+@SuppressWarnings("unused")
+@Entity
+@Table(name = "nenet_stellar_cursor")
+public class StellarCursorEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "cursor")
+ private String cursor;
+
+ @SuppressWarnings("unused")
+ @Column(name = "processed")
+ private Boolean processed;
+
+ @Column(name = "created_on")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date createdOn;
+
+ @SuppressWarnings("unused")
+ public StellarCursorEntity() { }
+
+ public StellarCursorEntity(final String cursor, final Date createdOn) {
+ this.cursor = cursor;
+ this.processed = false;
+ this.createdOn = createdOn;
+ }
+
+ public String getCursor() {
+ return cursor;
+ }
+
+ public void setProcessed(Boolean processed) {
+ this.processed = processed;
+ }
+
+ public Boolean getProcessed() {
+ return processed;
+ }
+
+ @SuppressWarnings("unused")
+ public Date getCreatedOn() {
+ return createdOn;
+ }
+
+ @SuppressWarnings("unused")
+ public void setCreatedOn(final Date createdOn) {
+ this.createdOn = createdOn;
+ }
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/StellarCursorRepository.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/StellarCursorRepository.java
new file mode 100644
index 0000000..3ead558
--- /dev/null
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/repository/StellarCursorRepository.java
@@ -0,0 +1,11 @@
+package org.apache.fineract.cn.stellarbridge.service.internal.repository;
+
+import java.util.Optional;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface StellarCursorRepository extends CrudRepository<StellarCursorEntity, Long> {
+ Optional<StellarCursorEntity> findTopByProcessedTrueOrderByCreatedOnDesc();
+ Optional<StellarCursorEntity> findByCursor(String cursor);
+}
diff --git a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/service/BridgeConfigurationService.java b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/service/BridgeConfigurationService.java
index b6ccc5f..4eb621e 100644
--- a/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/service/BridgeConfigurationService.java
+++ b/service/src/main/java/org/apache/fineract/cn/stellarbridge/service/internal/service/BridgeConfigurationService.java
@@ -21,22 +21,22 @@ package org.apache.fineract.cn.stellarbridge.service.internal.service;
import java.util.Optional;
import org.apache.fineract.cn.stellarbridge.api.v1.domain.BridgeConfiguration;
import org.apache.fineract.cn.stellarbridge.service.internal.mapper.BridgeConfigurationMapper;
-import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationEntityRepository;
+import org.apache.fineract.cn.stellarbridge.service.internal.repository.BridgeConfigurationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BridgeConfigurationService {
- private final BridgeConfigurationEntityRepository bridgeConfigurationEntityRepository;
+ private final BridgeConfigurationRepository bridgeConfigurationRepository;
@Autowired
- public BridgeConfigurationService(final BridgeConfigurationEntityRepository bridgeConfigurationEntityRepository) {
+ public BridgeConfigurationService(final BridgeConfigurationRepository bridgeConfigurationRepository) {
super();
- this.bridgeConfigurationEntityRepository = bridgeConfigurationEntityRepository;
+ this.bridgeConfigurationRepository = bridgeConfigurationRepository;
}
public Optional<BridgeConfiguration> findByTenantIdentifier(final String tenantIdentifier) {
- return this.bridgeConfigurationEntityRepository.findByTenantIdentifier(tenantIdentifier).map(BridgeConfigurationMapper::map);
+ return this.bridgeConfigurationRepository.findByTenantIdentifier(tenantIdentifier).map(BridgeConfigurationMapper::map);
}
}
diff --git a/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql b/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
index d1614c6..d5d568e 100644
--- a/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
+++ b/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
@@ -20,9 +20,20 @@
CREATE TABLE nenet_configuration (
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_identifier VARCHAR(32) NOT NULL,
- fineract_incoming_identifier VARCHAR(512) NOT NULL,
- fineract_outgoing_identifier VARCHAR(512) NOT NULL,
+ fineract_incoming_ledger VARCHAR(512) NOT NULL,
+ fineract_outgoing_ledger VARCHAR(512) NOT NULL,
+ fineract_stellar_ledger VARCHAR(512) NOT NULL,
stellar_identifier VARCHAR(512) NULL,
- CONSTRAINT nenet_uq UNIQUE (tenant_identifier),
- CONSTRAINT stellar_identifier PRIMARY KEY (id)
+ stellar_private_key VARCHAR(512) NULL,
+ CONSTRAINT nenet_configuration_uq UNIQUE (tenant_identifier),
+ CONSTRAINT nenet_configuration_pk PRIMARY KEY (id)
);
+
+CREATE TABLE nenet_stellar_cursor (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ xcursor VARCHAR(50) NOT NULL,
+ processed BOOLEAN NOT NULL,
+ created_on TIMESTAMP NOT NULL,
+ CONSTRAINT nenet_stellar_cursor_uq UNIQUE (xcursor),
+ CONSTRAINT nenet_stellar_cursor_pk PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/shared.gradle b/shared.gradle
index 778c355..7d08b55 100644
--- a/shared.gradle
+++ b/shared.gradle
@@ -28,7 +28,8 @@ ext.versions = [
frameworktest: '0.1.0-BUILD-SNAPSHOT',
frameworkanubis: '0.1.0-BUILD-SNAPSHOT',
frameworkpermittedfeignclient: '0.1.0-BUILD-SNAPSHOT',
- identity: '0.1.0-BUILD-SNAPSHOT',
+ accounting : '0.1.0-BUILD-SNAPSHOT',
+ stellar : '0.1.6',
validator : '5.3.0.Final'
]
@@ -45,6 +46,8 @@ tasks.withType(JavaCompile) {
repositories {
jcenter()
mavenLocal()
+ mavenCentral()
+ maven { url "https://jitpack.io" }
}
dependencyManagement {