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 <[email protected]> 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 {
