├── test_flows.txt ├── lib ├── quasar.jar └── README.txt ├── design └── images │ ├── spv.png │ ├── mapping.png │ ├── issuance.png │ ├── architecture.png │ ├── central-bank.png │ ├── deployment.png │ ├── redemption.png │ ├── issuance-state.png │ ├── create-issuance.png │ ├── bank-account-state.png │ ├── compatibility-zone.png │ ├── nostro-transaction-state.png │ └── create-nostro-transactions.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── settings.gradle ├── TRADEMARK ├── daemon ├── src │ └── main │ │ ├── resources │ │ ├── starling.conf │ │ ├── monzo.conf │ │ └── log4j2.xml │ │ └── kotlin │ │ └── com │ │ └── r3 │ │ └── corda │ │ └── finance │ │ └── cash │ │ └── issuer │ │ └── daemon │ │ ├── OpenBankingApi.kt │ │ ├── Utilities.kt │ │ ├── OpenBankingApiClient.kt │ │ ├── MockDaemon.kt │ │ ├── ArgsParser.kt │ │ ├── OpenBankingApiFactory.kt │ │ ├── Daemon.kt │ │ ├── mock │ │ ├── MockMonzo.kt │ │ └── MockUtilities.kt │ │ ├── Main.kt │ │ ├── clients │ │ ├── Monzo.kt │ │ └── Starling.kt │ │ └── AbstractDaemon.kt ├── config │ └── dev │ │ └── log4j2.xml └── build.gradle ├── common ├── contracts │ ├── src │ │ └── main │ │ │ └── kotlin │ │ │ └── com │ │ │ └── r3 │ │ │ └── corda │ │ │ └── sdk │ │ │ └── issuer │ │ │ └── common │ │ │ └── contracts │ │ │ ├── types │ │ │ ├── NodeTransactionStatus.kt │ │ │ ├── NodeTransactionType.kt │ │ │ ├── NostroTransactionStatus.kt │ │ │ ├── NostroTransactionType.kt │ │ │ ├── BankAccount.kt │ │ │ ├── BankAccountType.kt │ │ │ ├── NostroTransaction.kt │ │ │ └── AccountNumber.kt │ │ │ ├── serializers │ │ │ └── TokenTypeSerializer.kt │ │ │ ├── BankAccountContract.kt │ │ │ ├── NodeTransactionContract.kt │ │ │ ├── NostroTransactionContract.kt │ │ │ ├── schemas │ │ │ ├── NodeTransactionStateSchema.kt │ │ │ ├── BankAccountStateSchema.kt │ │ │ └── NostroTransactionStateSchema.kt │ │ │ └── states │ │ │ ├── NodeTransactionState.kt │ │ │ ├── BankAccountState.kt │ │ │ └── NostroTransactionState.kt │ └── build.gradle └── workflows │ ├── src │ └── main │ │ └── kotlin │ │ └── com │ │ └── r3 │ │ └── corda │ │ └── sdk │ │ └── issuer │ │ └── common │ │ └── workflows │ │ ├── flows │ │ ├── AbstractNotifyNostroTransaction.kt │ │ ├── AbstractRedeemCash.kt │ │ ├── AbstractVerifyBankAccount.kt │ │ ├── AbstractIssueCash.kt │ │ ├── MoveCashHandler.kt │ │ ├── VerifyBankAccountHandler.kt │ │ ├── AddBankAccountHandler.kt │ │ ├── MoveCash.kt │ │ └── AddBankAccount.kt │ │ └── utilities │ │ ├── MockDataUtilities.kt │ │ └── QueryUtils.kt │ └── build.gradle ├── service ├── src │ └── main │ │ └── kotlin │ │ └── com │ │ └── r3 │ │ └── corda │ │ └── finance │ │ └── cash │ │ └── issuer │ │ └── service │ │ ├── flows │ │ ├── GetNostroAccountBalances.kt │ │ ├── GetLastUpdatesByAccountId.kt │ │ ├── NotifyNostroTransaction.kt │ │ ├── RedeemCashHandler.kt │ │ ├── ReProcessNostroTransaction.kt │ │ ├── VerifyBankAccount.kt │ │ ├── ProcessRedemptionPayment.kt │ │ ├── ProcessRedemption.kt │ │ ├── IssueCash.kt │ │ ├── AddNostroTransactions.kt │ │ └── ProcessNostroTransaction.kt │ │ └── services │ │ └── UpdateObserverService.kt └── build.gradle ├── LICENCE ├── .idea └── runConfigurations │ ├── Debug_CorDapp.xml │ └── Unit_tests.xml ├── config ├── test │ └── log4j2.xml └── dev │ └── log4j2.xml ├── client ├── src │ └── main │ │ └── kotlin │ │ └── com │ │ └── r3 │ │ └── corda │ │ └── finance │ │ └── cash │ │ └── issuer │ │ └── client │ │ └── flows │ │ ├── NotifyNostroTransactionHandler.kt │ │ ├── ReceiveIssuedCash.kt │ │ └── RedeemCash.kt └── build.gradle ├── service-ui └── build.gradle ├── .gitignore ├── integration-test ├── build.gradle └── src │ └── test │ └── kotlin │ └── test │ └── IntegrationTest.kt ├── gradlew.bat ├── gradlew └── README.md /test_flows.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/quasar.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/lib/quasar.jar -------------------------------------------------------------------------------- /design/images/spv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/spv.png -------------------------------------------------------------------------------- /design/images/mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/mapping.png -------------------------------------------------------------------------------- /design/images/issuance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/issuance.png -------------------------------------------------------------------------------- /design/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/architecture.png -------------------------------------------------------------------------------- /design/images/central-bank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/central-bank.png -------------------------------------------------------------------------------- /design/images/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/deployment.png -------------------------------------------------------------------------------- /design/images/redemption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/redemption.png -------------------------------------------------------------------------------- /design/images/issuance-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/issuance-state.png -------------------------------------------------------------------------------- /design/images/create-issuance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/create-issuance.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /design/images/bank-account-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/bank-account-state.png -------------------------------------------------------------------------------- /design/images/compatibility-zone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/compatibility-zone.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | name=Corda Finance Module 2 | group=com.r3.corda.finance.issuer 3 | version=0.1 4 | kotlin.incremental=false -------------------------------------------------------------------------------- /design/images/nostro-transaction-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/nostro-transaction-state.png -------------------------------------------------------------------------------- /design/images/create-nostro-transactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corda/cash-issuer/HEAD/design/images/create-nostro-transactions.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'service' 2 | include 'daemon' 3 | include 'client' 4 | include 'service-ui' 5 | include 'common:contracts' 6 | include 'common:workflows' 7 | include 'integration-test' 8 | 9 | -------------------------------------------------------------------------------- /TRADEMARK: -------------------------------------------------------------------------------- 1 | Corda and the Corda logo are trademarks of R3CEV LLC and its affiliates. All rights reserved. 2 | 3 | For R3CEV LLC's trademark and logo usage information, please consult our Trademark Usage Policy at 4 | https://www.r3.com/trademark-policy/. 5 | -------------------------------------------------------------------------------- /daemon/src/main/resources/starling.conf: -------------------------------------------------------------------------------- 1 | apiBaseUrl="https://api.starlingbank.com/" 2 | apiVersion="api/v1/" 3 | apiAccessToken="ENTER TOKEN HERE" 4 | accounts=[ 5 | { 6 | number="12345612345678" 7 | type="collateral" 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Aug 25 12:50:39 BST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/types/NodeTransactionStatus.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.types 2 | 3 | import net.corda.core.serialization.CordaSerializable 4 | 5 | @CordaSerializable 6 | enum class NodeTransactionStatus { 7 | PENDING, 8 | COMPLETE 9 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/types/NodeTransactionType.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.types 2 | 3 | import net.corda.core.serialization.CordaSerializable 4 | 5 | @CordaSerializable 6 | enum class NodeTransactionType { 7 | ISSUANCE, 8 | REDEMPTION 9 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/AbstractNotifyNostroTransaction.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import net.corda.core.flows.FlowLogic 4 | import net.corda.core.flows.InitiatingFlow 5 | 6 | @InitiatingFlow 7 | abstract class AbstractNotifyNostroTransaction : FlowLogic() -------------------------------------------------------------------------------- /lib/README.txt: -------------------------------------------------------------------------------- 1 | The Quasar.jar in this directory is for runtime instrumentation of classes by Quasar. 2 | 3 | When running corda outside of the given gradle building you must add the following flag with the 4 | correct path to your call to Java: 5 | 6 | java -javaagent:path-to-quasar-jar.jar ... 7 | 8 | See the Quasar docs for more information: http://docs.paralleluniverse.co/quasar/ -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/types/NostroTransactionStatus.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.types 2 | 3 | import net.corda.core.serialization.CordaSerializable 4 | 5 | @CordaSerializable 6 | enum class NostroTransactionStatus { 7 | UNMATCHED, 8 | MATCHED_ISSUER, 9 | MATCHED_COUNTERPARTY, 10 | MATCHED 11 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/AbstractRedeemCash.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import net.corda.core.flows.FlowLogic 4 | import net.corda.core.flows.InitiatingFlow 5 | import net.corda.core.transactions.SignedTransaction 6 | 7 | @InitiatingFlow 8 | abstract class AbstractRedeemCash : FlowLogic() -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/types/NostroTransactionType.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.types 2 | 3 | import net.corda.core.serialization.CordaSerializable 4 | 5 | @CordaSerializable 6 | enum class NostroTransactionType { 7 | UNKNOWN, 8 | ISSUANCE, 9 | REDEMPTION, 10 | COLLATERAL_TRANSFER, 11 | ISSUER_INCOME 12 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/AbstractVerifyBankAccount.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import net.corda.core.flows.FlowLogic 4 | import net.corda.core.flows.InitiatingFlow 5 | import net.corda.core.transactions.SignedTransaction 6 | 7 | @InitiatingFlow 8 | abstract class AbstractVerifyBankAccount : FlowLogic() -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/AbstractIssueCash.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import net.corda.core.flows.FlowLogic 4 | import net.corda.core.flows.InitiatingFlow 5 | import net.corda.core.utilities.ProgressTracker 6 | 7 | @InitiatingFlow 8 | abstract class AbstractIssueCash(override val progressTracker: ProgressTracker) : FlowLogic() -------------------------------------------------------------------------------- /daemon/src/main/resources/monzo.conf: -------------------------------------------------------------------------------- 1 | apiBaseUrl="https://api.monzo.com/" 2 | apiVersion="" 3 | apiAccessToken="eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYiI6ImIwUXdhcnY2Ym1GZE1IejdGamQzIiwianRpIjoiYWNjdG9rXzAwMDA5ank2TFNHd0V2bG9FdWV2dUQiLCJ0eXAiOiJhdCIsInYiOiI1In0.fRXS-te2YKKE2NMOGWQgZ8D7EpHSmwboKWTpuaPT-LWgucWh_nq-DJbD8th2EOXHLVR0qV8niW1svP08j0_YtA" 4 | accounts=[ 5 | { 6 | number="12345612345678" 7 | type="collateral" 8 | } 9 | { 10 | number="34238" 11 | type="other" 12 | } 13 | ] 14 | 15 | -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/GetNostroAccountBalances.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getNostroAccountBalances 5 | import net.corda.core.flows.FlowLogic 6 | import net.corda.core.flows.StartableByRPC 7 | 8 | @StartableByRPC 9 | class GetNostroAccountBalances : FlowLogic>() { 10 | @Suspendable 11 | override fun call() = getNostroAccountBalances(serviceHub) 12 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/GetLastUpdatesByAccountId.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getLatestNostroTransactionStatesGroupedByAccount 5 | import net.corda.core.flows.FlowLogic 6 | import net.corda.core.flows.StartableByRPC 7 | 8 | @StartableByRPC 9 | class GetLastUpdatesByAccountId : FlowLogic>() { 10 | @Suspendable 11 | override fun call() = getLatestNostroTransactionStatesGroupedByAccount(serviceHub) 12 | } -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2018, R3 Limited. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/serializers/TokenTypeSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.serializers 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator 4 | import com.fasterxml.jackson.databind.JsonSerializer 5 | import com.fasterxml.jackson.databind.SerializerProvider 6 | import com.r3.corda.lib.tokens.contracts.types.TokenType 7 | 8 | class TokenTypeSerializer : JsonSerializer() { 9 | override fun serialize(value: TokenType, gen: JsonGenerator, provider: SerializerProvider) { 10 | gen.writeString(value.tokenIdentifier) 11 | } 12 | } -------------------------------------------------------------------------------- /.idea/runConfigurations/Debug_CorDapp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/NotifyNostroTransaction.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 5 | import com.r3.corda.sdk.issuer.common.workflows.flows.AbstractNotifyNostroTransaction 6 | import net.corda.core.identity.Party 7 | 8 | class NotifyNostroTransaction( 9 | val nostroTransaction: NostroTransaction, 10 | val counterparty: Party 11 | ) : AbstractNotifyNostroTransaction() { 12 | @Suspendable 13 | override fun call() = initiateFlow(counterparty).send(nostroTransaction) 14 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/MoveCashHandler.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.lib.tokens.workflows.flows.move.ConfidentialMoveTokensFlowHandler 5 | import net.corda.core.flows.FlowLogic 6 | import net.corda.core.flows.FlowSession 7 | import net.corda.core.flows.InitiatedBy 8 | 9 | /** 10 | * Simple move cash flow for demos. 11 | */ 12 | @InitiatedBy(MoveCash::class) 13 | class MoveCashHandler(val otherSession: FlowSession) : FlowLogic() { 14 | @Suspendable 15 | override fun call() { 16 | subFlow(ConfidentialMoveTokensFlowHandler(otherSession)) 17 | } 18 | } -------------------------------------------------------------------------------- /config/test/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | [%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n 8 | > 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/BankAccountContract.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts 2 | 3 | import net.corda.core.contracts.CommandData 4 | import net.corda.core.contracts.Contract 5 | import net.corda.core.transactions.LedgerTransaction 6 | 7 | class BankAccountContract : Contract { 8 | 9 | companion object { 10 | @JvmStatic 11 | val CONTRACT_ID = "com.r3.corda.sdk.issuer.common.contracts.BankAccountContract" 12 | } 13 | 14 | interface Commands : CommandData 15 | class Add : Commands 16 | class Update : Commands 17 | 18 | // TODO: Contract code not implemented for demo. 19 | override fun verify(tx: LedgerTransaction) = Unit 20 | 21 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/NodeTransactionContract.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts 2 | 3 | import net.corda.core.contracts.CommandData 4 | import net.corda.core.contracts.Contract 5 | import net.corda.core.transactions.LedgerTransaction 6 | 7 | class NodeTransactionContract : Contract { 8 | 9 | companion object { 10 | @JvmStatic 11 | val CONTRACT_ID = "com.r3.corda.sdk.issuer.common.contracts.NodeTransactionContract" 12 | } 13 | 14 | interface Commands : CommandData 15 | class Create : Commands 16 | class Update : Commands 17 | 18 | // TODO: Contract code not implemented for demo. 19 | override fun verify(tx: LedgerTransaction) = Unit 20 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/NostroTransactionContract.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts 2 | 3 | import net.corda.core.contracts.CommandData 4 | import net.corda.core.contracts.Contract 5 | import net.corda.core.transactions.LedgerTransaction 6 | 7 | class NostroTransactionContract : Contract { 8 | 9 | companion object { 10 | @JvmStatic 11 | val CONTRACT_ID = "com.r3.corda.sdk.issuer.common.contracts.NostroTransactionContract" 12 | } 13 | 14 | interface Commands : CommandData 15 | class Add : Commands 16 | class Match : Commands 17 | 18 | // TODO: Contract code not implemented for demo. 19 | override fun verify(tx: LedgerTransaction) = Unit 20 | 21 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/VerifyBankAccountHandler.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import net.corda.core.flows.FlowLogic 5 | import net.corda.core.flows.FlowSession 6 | import net.corda.core.flows.InitiatedBy 7 | import net.corda.core.flows.ReceiveFinalityFlow 8 | 9 | @InitiatedBy(AbstractVerifyBankAccount::class) 10 | class VerifyBankAccountHandler(val otherSession: FlowSession) : FlowLogic() { 11 | @Suspendable 12 | override fun call() { 13 | if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) { 14 | subFlow(ReceiveFinalityFlow(otherSession)) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.idea/runConfigurations/Unit_tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /client/src/main/kotlin/com/r3/corda/finance/cash/issuer/client/flows/NotifyNostroTransactionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.client.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 5 | import com.r3.corda.sdk.issuer.common.workflows.flows.AbstractNotifyNostroTransaction 6 | import net.corda.core.flows.FlowLogic 7 | import net.corda.core.flows.FlowSession 8 | import net.corda.core.flows.InitiatedBy 9 | import net.corda.core.utilities.unwrap 10 | 11 | @InitiatedBy(AbstractNotifyNostroTransaction::class) 12 | class NotifyNostroTransactionHandler(val otherSession: FlowSession) : FlowLogic() { 13 | @Suspendable 14 | override fun call(): NostroTransaction { 15 | return otherSession.receive().unwrap { it } 16 | } 17 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/types/BankAccount.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.types 2 | 3 | import com.r3.corda.lib.tokens.contracts.types.TokenType 4 | import com.r3.corda.sdk.issuer.common.contracts.states.BankAccountState 5 | import net.corda.core.identity.Party 6 | import net.corda.core.serialization.CordaSerializable 7 | 8 | @CordaSerializable 9 | data class BankAccount( 10 | val accountId: String, 11 | val accountName: String, 12 | val accountNumber: AccountNumber, 13 | val currency: TokenType, 14 | val type: BankAccountType = BankAccountType.COLLATERAL // Defaulted to collateral for now. 15 | ) 16 | 17 | fun BankAccount.toState(owner: Party, verifier: Party): BankAccountState { 18 | return BankAccountState(owner, verifier, accountId, accountName, accountNumber, currency, type) 19 | } -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/OpenBankingApi.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import com.r3.corda.lib.tokens.contracts.types.TokenType 4 | import com.r3.corda.sdk.issuer.common.contracts.types.BankAccount 5 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 6 | import net.corda.core.contracts.Amount 7 | import rx.Observable 8 | import java.time.Instant 9 | 10 | abstract class OpenBankingApi { 11 | abstract val accounts: List 12 | abstract fun balance(accountId: BankAccountId? = null): Amount 13 | abstract fun transactionsFeed(): Observable> 14 | val lastTransactions = mutableMapOf() 15 | fun updateLastTransactionTimestamps(accountId: BankAccountId, timestamp: Long) { 16 | lastTransactions[accountId] = Instant.ofEpochMilli(timestamp) 17 | } 18 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/AddBankAccountHandler.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import net.corda.core.flows.FlowLogic 5 | import net.corda.core.flows.FlowSession 6 | import net.corda.core.flows.InitiatedBy 7 | import net.corda.core.flows.ReceiveFinalityFlow 8 | import net.corda.core.node.StatesToRecord 9 | 10 | @InitiatedBy(AddBankAccount::class) 11 | class AddBankAccountHandler(val otherSession: FlowSession) : FlowLogic() { 12 | @Suspendable 13 | override fun call() { 14 | if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) { 15 | // The verifier should store the transaction using ALL VISIBLE as they are not a participant in the state. 16 | subFlow(ReceiveFinalityFlow(otherSession, statesToRecord = StatesToRecord.ALL_VISIBLE)) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /client/src/main/kotlin/com/r3/corda/finance/cash/issuer/client/flows/ReceiveIssuedCash.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.client.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.workflows.flows.AbstractIssueCash 5 | import net.corda.core.flows.FlowLogic 6 | import net.corda.core.flows.FlowSession 7 | import net.corda.core.flows.InitiatedBy 8 | import net.corda.core.flows.ReceiveFinalityFlow 9 | 10 | @InitiatedBy(AbstractIssueCash::class) 11 | class ReceiveIssuedCash(val otherSession: FlowSession) : FlowLogic() { 12 | @Suspendable 13 | override fun call() { 14 | logger.info("Starting ReceiveIssuedCash flow...") 15 | //return subFlow(ReceiveTransactionFlow(otherSession, true, StatesToRecord.ALL_VISIBLE)) 16 | if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) { 17 | subFlow(ReceiveFinalityFlow(otherSession)) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/RedeemCashHandler.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.lib.tokens.workflows.flows.redeem.ConfidentialRedeemFungibleTokensFlowHandler 5 | import com.r3.corda.sdk.issuer.common.workflows.flows.AbstractRedeemCash 6 | import net.corda.core.flows.FlowLogic 7 | import net.corda.core.flows.FlowSession 8 | import net.corda.core.flows.InitiatedBy 9 | 10 | // TODO: Need to refactor this using Kasia's updated Redeem flow. 11 | 12 | @InitiatedBy(AbstractRedeemCash::class) 13 | class RedeemCashHandler(val otherSession: FlowSession) : FlowLogic() { 14 | @Suspendable 15 | override fun call() { 16 | logger.info("Starting redeem handler flow.") 17 | // We probably shouldn't have a type parameter on the responder. 18 | subFlow(ConfidentialRedeemFungibleTokensFlowHandler(otherSession)) 19 | } 20 | } -------------------------------------------------------------------------------- /client/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | cordapp { 4 | targetPlatformVersion 4 5 | minimumPlatformVersion 4 6 | contract { 7 | name "Template Contract" 8 | vendor "Corda Open Source" 9 | licence "A liberal, open source licence" 10 | versionId 1 11 | } 12 | signing { 13 | enabled true 14 | } 15 | } 16 | 17 | sourceSets { 18 | main { 19 | resources { 20 | srcDir rootProject.file("config/dev") 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | // Kotlin. 27 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 28 | 29 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 30 | testCompile "junit:junit:$junit_version" 31 | 32 | // Corda dependencies. 33 | compile "$corda_release_group:corda-core:$corda_release_version" 34 | compile "$corda_release_group:corda-jackson:$corda_release_version" 35 | testCompile "$corda_release_group:corda-node-driver:$corda_release_version" 36 | compile project(":common:workflows") 37 | } -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/Utilities.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import net.corda.core.toFuture 4 | import net.corda.core.utilities.getOrThrow 5 | import retrofit2.HttpException 6 | import rx.Observable 7 | import rx.schedulers.Schedulers 8 | 9 | // TODO: Make this fault tolerant. 10 | // TODO: Deal with HTTP error codes 11 | // Retry with exponential back-off. 12 | // Don't bail out on error. 13 | // Helper to convert an observable that emits one event to a future, with the work being performed on an IO thread. 14 | fun Observable.getOrThrow() = observeOn(Schedulers.io()) 15 | .toFuture() 16 | .getOrThrow() 17 | 18 | fun BankAccountId.truncate() = this.take(10) 19 | 20 | fun wrapWithTry(block: () -> T): T { 21 | return try { 22 | block() 23 | } catch (e: HttpException) { 24 | throw RuntimeException("Creating open banking API client failed. The most likely reason is bad credentials. " + 25 | "Check your API key. If you don't have a monzo or starling account, then run the daemon in --mock-mode") 26 | } 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /service/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | apply plugin: 'net.corda.plugins.cordapp' 3 | 4 | cordapp { 5 | targetPlatformVersion 4 6 | minimumPlatformVersion 4 7 | contract { 8 | name "Template Contract" 9 | vendor "Corda Open Source" 10 | licence "A liberal, open source licence" 11 | versionId 1 12 | } 13 | signing { 14 | enabled true 15 | } 16 | } 17 | 18 | sourceSets { 19 | main { 20 | resources { 21 | srcDir rootProject.file("config/dev") 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | // Kotlin. 28 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 29 | 30 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 31 | testCompile "junit:junit:$junit_version" 32 | 33 | // Corda dependencies. 34 | cordaCompile "$corda_release_group:corda-core:$corda_release_version" 35 | cordaRuntime "$corda_release_group:corda:$corda_release_version" 36 | testCompile "$corda_release_group:corda-node-driver:$corda_release_version" 37 | 38 | // Project dependencies. 39 | cordaCompile project(":common:workflows") 40 | } -------------------------------------------------------------------------------- /service-ui/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | apply plugin: 'net.corda.plugins.cordapp' 3 | 4 | sourceSets { 5 | main { 6 | resources { 7 | srcDir rootProject.file("config/dev") 8 | } 9 | } 10 | } 11 | 12 | dependencies { 13 | // Kotlin. 14 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 15 | 16 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 17 | testCompile "junit:junit:$junit_version" 18 | 19 | // Corda dependencies. 20 | cordaCompile "$corda_release_group:corda-core:$corda_release_version" 21 | cordaRuntime "$corda_release_group:corda:$corda_release_version" 22 | cordaCompile "$corda_release_group:corda-jfx:$corda_release_version" 23 | cordaCompile "$corda_release_group:corda-node-api:$corda_release_version" 24 | testCompile "$corda_release_group:corda-node-driver:$corda_release_version" 25 | 26 | // Project dependencies. 27 | cordaCompile project(":common:contracts") 28 | cordaCompile project(":service") 29 | 30 | // Token SDK. 31 | cordaCompile "$tokens_release_group:tokens-workflows:$tokens_release_version" 32 | 33 | // Tornado Fx. 34 | compile 'no.tornado:tornadofx:1.7.15' 35 | } -------------------------------------------------------------------------------- /common/workflows/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | apply plugin: 'net.corda.plugins.cordapp' 3 | 4 | cordapp { 5 | targetPlatformVersion 4 6 | minimumPlatformVersion 4 7 | contract { 8 | name "Template Contract" 9 | vendor "Corda Open Source" 10 | licence "A liberal, open source licence" 11 | versionId 1 12 | } 13 | signing { 14 | enabled true 15 | } 16 | } 17 | 18 | sourceSets { 19 | main { 20 | resources { 21 | srcDir rootProject.file("config/dev") 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | // Kotlin. 28 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 29 | 30 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 31 | testCompile "junit:junit:$junit_version" 32 | 33 | // Corda dependencies. 34 | cordaCompile "$corda_release_group:corda-core:$corda_release_version" 35 | cordaRuntime "$corda_release_group:corda:$corda_release_version" 36 | testCompile "$corda_release_group:corda-node-driver:$corda_release_version" 37 | 38 | // Project dependencies. 39 | cordaCompile project(":common:contracts") 40 | 41 | // Token SDK. 42 | cordaCompile "$tokens_release_group:tokens-workflows:$tokens_release_version" 43 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/types/BankAccountType.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.types 2 | 3 | import net.corda.core.serialization.CordaSerializable 4 | 5 | /** 6 | * The issuer operates a multitude of bank accounts, most of them are collateral accounts. However, not all of them 7 | * are. As per electronic money regulations in the EU, the issuer is entitled to overnight interest on relevant funds 8 | * balances. Therefore, the issuer will need to sweep interest income into a separate account. The issuer, when 9 | * permitted, may from time to time, sweep cash in respect of fees from any of the collateral accounts into its 10 | * operational accounts. Bank account type must be taken into account when inspecting transactions between accounts 11 | * operated by the issuer. For example: an outflow from a collateral account and a corresponding in flow into an 12 | * issuer account is likely because some fees were due from a customer. On the other hand, an out flow from a collateral 13 | * account with a corresponding in flow for the same amount into another collateral account will be due to liquidity 14 | * management between the various nostro accounts which the issuer operates. 15 | */ 16 | @CordaSerializable 17 | enum class BankAccountType { 18 | COLLATERAL, 19 | ISSUER 20 | } -------------------------------------------------------------------------------- /common/contracts/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | apply plugin: 'net.corda.plugins.cordapp' 3 | 4 | cordapp { 5 | targetPlatformVersion 4 6 | minimumPlatformVersion 4 7 | contract { 8 | name "Template Contract" 9 | vendor "Corda Open Source" 10 | licence "A liberal, open source licence" 11 | versionId 1 12 | } 13 | signing { 14 | enabled true 15 | } 16 | } 17 | 18 | sourceSets { 19 | main { 20 | resources { 21 | srcDir rootProject.file("config/dev") 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | // Kotlin. 28 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 29 | 30 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 31 | testCompile "junit:junit:$junit_version" 32 | 33 | // Corda dependencies. 34 | cordaCompile "$corda_release_group:corda-core:$corda_release_version" 35 | cordaRuntime "$corda_release_group:corda:$corda_release_version" 36 | cordaCompile "$corda_release_group:corda-jackson:$corda_release_version" 37 | testCompile "$corda_release_group:corda-node-driver:$corda_release_version" 38 | 39 | // Token SDK. 40 | cordaCompile "$tokens_release_group:tokens-contracts:$tokens_release_version" 41 | cordaCompile "$tokens_release_group:tokens-money:$tokens_release_version" 42 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/types/NostroTransaction.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.types 2 | 3 | import com.r3.corda.lib.tokens.contracts.types.TokenType 4 | import com.r3.corda.sdk.issuer.common.contracts.states.NostroTransactionState 5 | import net.corda.core.contracts.AmountTransfer 6 | import net.corda.core.identity.Party 7 | import net.corda.core.serialization.CordaSerializable 8 | import java.time.Instant 9 | 10 | @CordaSerializable 11 | data class NostroTransaction( 12 | val transactionId: String, 13 | val accountId: String, 14 | val amount: Long, 15 | val currency: TokenType, 16 | val type: String, 17 | val description: String, 18 | val createdAt: Instant, 19 | val source: AccountNumber, 20 | val destination: AccountNumber 21 | ) 22 | 23 | fun NostroTransaction.toState(issuer: Party): NostroTransactionState { 24 | return NostroTransactionState( 25 | accountId = accountId, 26 | issuer = issuer, 27 | transactionId = transactionId, 28 | amountTransfer = AmountTransfer( 29 | quantityDelta = amount, 30 | token = currency, 31 | source = source, 32 | destination = destination 33 | ), 34 | description = description, 35 | createdAt = createdAt 36 | ) 37 | } -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/OpenBankingApiClient.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import com.typesafe.config.Config 4 | import com.typesafe.config.ConfigFactory 5 | import net.corda.core.utilities.contextLogger 6 | 7 | typealias BankAccountId = String 8 | 9 | data class ApiConfig(val apiBaseUrl: String, val apiAccessToken: String, val accounts: List>) 10 | 11 | // TODO: Add a feature to this class that allows sub-classes to filter accounts based on the whitelist (in the config file). 12 | abstract class OpenBankingApiClient(val configName: String) : OpenBankingApi() { 13 | companion object { 14 | val logger = contextLogger() 15 | } 16 | 17 | abstract val api: Any 18 | 19 | protected val apiConfig: ApiConfig by lazy { 20 | val config: Config = ConfigFactory.parseResources("$configName.conf") 21 | val apiBaseUrl: String = config.getString("apiBaseUrl") + config.getString("apiVersion") 22 | val apiAccessToken: String = config.getString("apiAccessToken") 23 | val accounts = config.getConfigList("accounts").map { Pair(it.getString("number"), it.getString("type")) } 24 | if (accounts.isEmpty()) { 25 | // TODO: Currently one must specify accounts but they are not yet used. 26 | logger.warn("No bank accounts have been specified for $configName.") 27 | } 28 | ApiConfig(apiBaseUrl, apiAccessToken, accounts) 29 | } 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse, ctags, Mac metadata, log files 2 | .classpath 3 | .project 4 | .settings 5 | tags 6 | .DS_Store 7 | *.log 8 | *.log.gz 9 | *.orig 10 | 11 | .gradle 12 | 13 | # General build files 14 | **/build/* 15 | !docs/build/* 16 | 17 | lib/dokka.jar 18 | 19 | ### JetBrains template 20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 21 | 22 | *.iml 23 | 24 | ## Directory-based project format: 25 | #.idea 26 | 27 | # if you remove the above rule, at least ignore the following: 28 | 29 | # Specific files to avoid churn 30 | .idea/*.xml 31 | .idea/copyright 32 | .idea/jsLibraryMappings.xml 33 | 34 | # User-specific stuff: 35 | .idea/tasks.xml 36 | .idea/dictionaries 37 | 38 | # Sensitive or high-churn files: 39 | .idea/dataSources.ids 40 | .idea/dataSources.xml 41 | .idea/sqlDataSources.xml 42 | .idea/dynamic.xml 43 | .idea/uiDesigner.xml 44 | 45 | # Gradle: 46 | .idea/libraries 47 | 48 | # Mongo Explorer plugin: 49 | .idea/mongoSettings.xml 50 | 51 | ## File-based project format: 52 | *.ipr 53 | *.iws 54 | 55 | ## Plugin-specific files: 56 | 57 | # IntelliJ 58 | /out/ 59 | **/out/* 60 | /issuer/out/ 61 | /mock-bank/out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | 74 | # docs related 75 | docs/virtualenv/ 76 | /cash-issuer/daemon/src/main/resources/starling.conf 77 | /cash-issuer/daemon/src/main/resources/monzo.conf 78 | -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/MockDaemon.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import com.r3.corda.finance.cash.issuer.daemon.mock.MockClient 4 | import io.github.classgraph.ClassGraph 5 | import net.corda.core.messaging.CordaRPCOps 6 | 7 | private const val clientsPackage = "com.r3.corda.finance.cash.issuer.daemon.mock" 8 | 9 | class MockDaemon(services: CordaRPCOps, options: CommandLineOptions) : AbstractDaemon(services, options) { 10 | override fun scanForOpenBankingApiClients(): List { 11 | println("Scanning the 'clients' package for Open Banking API clients...\n") 12 | 13 | val list = mutableListOf() 14 | ClassGraph().enableAllInfo().whitelistPackages(clientsPackage).scan().use { scanResult -> 15 | val sub = scanResult.getSubclasses(OpenBankingApi::class.java.name) 16 | sub.map { 17 | val apiName = it.simpleName 18 | val apiClient = it.loadClass().getDeclaredConstructor().newInstance() 19 | println("\t* Loaded $apiName API interface and client.") 20 | list.add(apiClient as OpenBankingApi) 21 | } 22 | } 23 | 24 | return list.toList() 25 | } 26 | 27 | override fun start() { 28 | openBankingApiClients.forEach { 29 | it as MockClient 30 | it.startGeneratingTransactions(0) 31 | } 32 | super.start() 33 | } 34 | 35 | override fun stop() { 36 | openBankingApiClients.forEach { 37 | it as MockClient 38 | it.stopGeneratingTransactions() 39 | } 40 | super.stop() 41 | } 42 | } -------------------------------------------------------------------------------- /client/src/main/kotlin/com/r3/corda/finance/cash/issuer/client/flows/RedeemCash.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.client.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.lib.tokens.contracts.types.TokenType 5 | import com.r3.corda.lib.tokens.contracts.utilities.of 6 | import com.r3.corda.lib.tokens.money.FiatCurrency 7 | import com.r3.corda.lib.tokens.workflows.flows.redeem.ConfidentialRedeemFungibleTokensFlow 8 | import com.r3.corda.sdk.issuer.common.workflows.flows.AbstractRedeemCash 9 | import net.corda.core.contracts.Amount 10 | import net.corda.core.flows.FlowLogic 11 | import net.corda.core.flows.StartableByRPC 12 | import net.corda.core.identity.Party 13 | import net.corda.core.transactions.SignedTransaction 14 | import net.corda.core.utilities.ProgressTracker 15 | 16 | class RedeemCash(val amount: Amount, val issuer: Party) : AbstractRedeemCash() { 17 | 18 | companion object { 19 | object REDEEMING : ProgressTracker.Step("Redeeming cash.") 20 | @JvmStatic 21 | fun tracker() = ProgressTracker(REDEEMING) 22 | } 23 | 24 | override val progressTracker: ProgressTracker = tracker() 25 | 26 | @Suspendable 27 | override fun call(): SignedTransaction { 28 | val issuerSession = initiateFlow(issuer) 29 | return subFlow(ConfidentialRedeemFungibleTokensFlow(amount, issuerSession)) 30 | } 31 | } 32 | 33 | @StartableByRPC 34 | class RedeemCashShell(val amount: Long, val currency: String, val issuer: Party) : FlowLogic() { 35 | 36 | override val progressTracker: ProgressTracker = ProgressTracker() 37 | 38 | @Suspendable 39 | override fun call(): SignedTransaction { 40 | val fiatCurrency = FiatCurrency.getInstance(currency) 41 | return subFlow(RedeemCash(amount of fiatCurrency, issuer)) 42 | } 43 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/schemas/NodeTransactionStateSchema.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.schemas 2 | 3 | import net.corda.core.schemas.MappedSchema 4 | import net.corda.core.schemas.PersistentState 5 | import javax.persistence.Column 6 | import javax.persistence.Entity 7 | import javax.persistence.Table 8 | 9 | object NodeTransactionStateSchema 10 | 11 | object NodeTransactionStateSchemaV1 : MappedSchema( 12 | schemaFamily = NodeTransactionStateSchema.javaClass, 13 | version = 1, 14 | mappedTypes = listOf(PersistentNodeTransactionState::class.java) 15 | ) { 16 | 17 | @Entity 18 | @Table(name = "node_transaction_states") 19 | class PersistentNodeTransactionState( 20 | @Column(name = "issuer") 21 | var issuer: String, 22 | @Column(name = "counterparty") 23 | var counterparty: String, 24 | @Column(name = "notes") 25 | var notes: String, 26 | @Column(name = "amount") 27 | var amount: Long, 28 | @Column(name = "currency") 29 | var currency: String, 30 | @Column(name = "created_at") 31 | var createdAt: Long, 32 | @Column(name = "type") 33 | var type: String, 34 | @Column(name = "status") 35 | var status: String, 36 | @Column(name = "linear_id") 37 | var linearId: String 38 | ) : PersistentState() { 39 | // Default constructor required by hibernate. 40 | constructor() : this( 41 | issuer = "", 42 | counterparty = "", 43 | notes = "", 44 | amount = 0L, 45 | currency = "", 46 | createdAt = 0L, 47 | type = "", 48 | status = "", 49 | linearId = "" 50 | ) 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/ArgsParser.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import joptsimple.OptionParser 4 | import java.io.PrintStream 5 | import java.time.Instant 6 | 7 | class ArgsParser { 8 | private val optionParser = OptionParser() 9 | 10 | private val rpcHostPortArg = optionParser.accepts("host-port", "The hostname and port of the Issuer node.").withRequiredArg() 11 | private val rpcUserArg = optionParser.accepts("rpcUser", "Corda RPC username.").withRequiredArg().defaultsTo("user1") 12 | private val rpcPassArg = optionParser.accepts("rpcPass", "CordaRPC password.").withRequiredArg().defaultsTo("test") 13 | private val mockModeArg = optionParser.accepts("mock-mode", "Run the daemon in mockMode mode.") 14 | private val autoModeArg = optionParser.accepts("auto-mode", "Run the daemon in automatic mode.") 15 | private val startFromArg = optionParser.accepts("start-from", "Run the daemon in automatic mode.").withOptionalArg().defaultsTo(Instant.now().toEpochMilli().toString()) 16 | 17 | fun parse(vararg args: String): CommandLineOptions { 18 | val optionSet = optionParser.parse(*args) 19 | return CommandLineOptions( 20 | rpcHostAndPort = optionSet.valueOf(rpcHostPortArg), 21 | rpcUser = optionSet.valueOf(rpcUserArg), 22 | rpcPass = optionSet.valueOf(rpcPassArg), 23 | mockMode = optionSet.has(mockModeArg), 24 | autoMode = optionSet.has(autoModeArg), 25 | startFrom = Instant.ofEpochMilli(optionSet.valueOf(startFromArg).toLong()) 26 | ) 27 | } 28 | 29 | fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) 30 | } 31 | 32 | data class CommandLineOptions( 33 | val rpcHostAndPort: String, 34 | val rpcUser: String, 35 | val rpcPass: String, 36 | val mockMode: Boolean, 37 | val autoMode: Boolean, 38 | val startFrom: Instant? 39 | ) -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/schemas/BankAccountStateSchema.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.schemas 2 | 3 | import net.corda.core.schemas.MappedSchema 4 | import net.corda.core.schemas.PersistentState 5 | import javax.persistence.Column 6 | import javax.persistence.Entity 7 | import javax.persistence.Table 8 | 9 | object BankAccountStateSchema 10 | 11 | object BankAccountStateSchemaV1 : MappedSchema( 12 | schemaFamily = BankAccountStateSchema.javaClass, 13 | version = 1, 14 | mappedTypes = listOf(PersistentBankAccountState::class.java) 15 | ) { 16 | 17 | @Entity 18 | @Table(name = "bank_account_states") 19 | class PersistentBankAccountState( 20 | @Column(name = "owner") 21 | var owner: String, 22 | @Column(name = "verifier") 23 | var verifier: String, 24 | @Column(name = "accountName") 25 | var accountName: String, 26 | @Column(name = "accountNumber") 27 | var accountNumber: String, 28 | @Column(name = "currency") 29 | var currency: String, 30 | @Column(name = "type") 31 | var type: String, 32 | @Column(name = "verified") 33 | var verified: Boolean, 34 | @Column(name = "last_updated") 35 | var lastUpdated: Long, 36 | @Column(name = "linear_id") 37 | var linearId: String, 38 | @Column(name = "external_id") 39 | var externalId: String 40 | ) : PersistentState() { 41 | @Suppress("UNUSED") 42 | constructor() : this( 43 | owner = "", 44 | verifier = "", 45 | accountName = "", 46 | accountNumber = "", 47 | currency = "", 48 | type = "", 49 | verified = false, 50 | lastUpdated = 0L, 51 | linearId = "", 52 | externalId = "" 53 | ) 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/ReProcessNostroTransaction.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.finance.cash.issuer.service.services.UpdateObserverService 5 | import com.r3.corda.sdk.issuer.common.contracts.states.BankAccountState 6 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getNostroTransactionsByAccountNumber 7 | import net.corda.core.flows.FlowLogic 8 | import net.corda.core.flows.InitiatingFlow 9 | import net.corda.core.flows.StartableByService 10 | 11 | /** 12 | * A flow to decide which nostro transactions to re-process when a new bank account state is received. This workflow 13 | * cannot be performed in the [UpdateObserverService] which seems to be the natural place for it... This is because 14 | * the [UpdateObserverService] observes updates from the vault on the [Schedulers.io] thread pool which doesn't have 15 | * access to ThreadLocal. 16 | */ 17 | @StartableByService 18 | @InitiatingFlow 19 | class ReProcessNostroTransaction(val bankAccountState: BankAccountState) : FlowLogic() { 20 | 21 | @Suspendable 22 | override fun call() { 23 | // TODO: this seems to have a bug. New bank account states are added to all nostro txs with missing accounts. 24 | // The assumption is that we only pull out transactions which have been partially matched. I.e. matched to the 25 | // issuer's nostro account only. 26 | val matchedTransactions = getNostroTransactionsByAccountNumber(bankAccountState.accountNumber, serviceHub) 27 | 28 | if (matchedTransactions.isEmpty()) { 29 | UpdateObserverService.logger.info("No transactions with this newly added account have been seen before.") 30 | return 31 | } 32 | 33 | UpdateObserverService.logger.info("There are ${matchedTransactions.size} which need re-processing.") 34 | 35 | // Process, again, all the nostro transactions with this bank account. 36 | matchedTransactions.forEach { subFlow(ProcessNostroTransaction(it)) } 37 | UpdateObserverService.logger.info("Reprocessed ${matchedTransactions.size} nostro transactions.") 38 | } 39 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/schemas/NostroTransactionStateSchema.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.schemas 2 | 3 | import net.corda.core.schemas.MappedSchema 4 | import net.corda.core.schemas.PersistentState 5 | import javax.persistence.Column 6 | import javax.persistence.Entity 7 | import javax.persistence.Table 8 | 9 | object NostroTransactionStateSchema 10 | 11 | object NostroTransactionStateSchemaV1 : MappedSchema( 12 | schemaFamily = NostroTransactionStateSchema.javaClass, 13 | version = 1, 14 | mappedTypes = listOf(PersistentNostroTransactionState::class.java) 15 | ) { 16 | 17 | @Entity 18 | @Table(name = "nostro_transaction_States") 19 | class PersistentNostroTransactionState( 20 | @Column(name = "id") 21 | var transactionId: String, 22 | @Column(name = "account_id") 23 | var accountId: String, 24 | @Column(name = "amount") 25 | var amount: Long, 26 | @Column(name = "currency") 27 | var currency: String, 28 | @Column(name = "source_account_number") 29 | var sourceAccountNumber: String, 30 | @Column(name = "destination_account_number") 31 | var destinationAccountNumber: String, 32 | @Column(name = "created_at") 33 | var createdAt: Long, 34 | @Column(name = "last_updated") 35 | var lastUpdated: Long, 36 | @Column(name = "type") 37 | var type: String, 38 | @Column(name = "status") 39 | var status: String, 40 | @Column(name = "linear_id") 41 | var linearId: String 42 | ) : PersistentState() { 43 | // Default constructor required by hibernate. 44 | constructor() : this( 45 | accountId = "", 46 | transactionId = "", 47 | amount = 0L, 48 | currency = "", 49 | sourceAccountNumber = "", 50 | destinationAccountNumber = "", 51 | createdAt = 0L, 52 | lastUpdated = 0L, 53 | type = "", 54 | status = "", 55 | linearId = "" 56 | ) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/types/AccountNumber.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.types 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonSubTypes 5 | import com.fasterxml.jackson.annotation.JsonTypeInfo 6 | import net.corda.core.serialization.CordaSerializable 7 | 8 | /** 9 | * Marker interface for bank account numbers. 10 | */ 11 | // TODO: Remove this hack at some point. 12 | @JsonTypeInfo( 13 | use = JsonTypeInfo.Id.NAME, 14 | include = JsonTypeInfo.As.PROPERTY, 15 | property = "type" 16 | ) 17 | @JsonSubTypes( 18 | JsonSubTypes.Type(value = UKAccountNumber::class, name = "uk"), 19 | JsonSubTypes.Type(value = NoAccountNumber::class, name = "none") 20 | ) 21 | @CordaSerializable 22 | interface AccountNumber { 23 | val digits: String 24 | } 25 | 26 | /** 27 | * UK bank account numbers come in sort code and account number pairs. 28 | */ 29 | @CordaSerializable 30 | data class UKAccountNumber(override val digits: String) : AccountNumber { 31 | 32 | @JsonCreator 33 | constructor(sortCode: String, accountNumber: String) : this("$sortCode$accountNumber") 34 | 35 | val sortCode get() = digits.subSequence(0, 6) 36 | val accountNumber get() = digits.subSequence(6, 14) 37 | 38 | init { 39 | // Account number validation. 40 | require(accountNumber.length == 8) { "A UK bank account accountNumber must be eight digits long." } 41 | require(accountNumber.matches(Regex("[0-9]+"))) { 42 | "An account accountNumber must only contain the numbers zero to nine." 43 | } 44 | 45 | // Sort code validation. 46 | require(sortCode.length == 6) { "A UK bank sort code must be 6 digits long." } 47 | require(sortCode.matches(Regex("[0-9]+"))) { 48 | "An account accountNumber must only contain the numbers zero to nine." 49 | } 50 | } 51 | 52 | override fun toString() = "Sort Code: $sortCode Account Number: $accountNumber" 53 | 54 | } 55 | 56 | /** 57 | * Sometimes we don't have a bank account number. 58 | */ 59 | @CordaSerializable 60 | data class NoAccountNumber(override val digits: String = "No bank account number available.") : AccountNumber -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/MoveCash.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.lib.tokens.contracts.types.TokenType 5 | import com.r3.corda.lib.tokens.contracts.utilities.of 6 | import com.r3.corda.lib.tokens.money.FiatCurrency 7 | import com.r3.corda.lib.tokens.workflows.flows.move.ConfidentialMoveFungibleTokensFlow 8 | import com.r3.corda.lib.tokens.workflows.types.PartyAndAmount 9 | import net.corda.core.contracts.Amount 10 | import net.corda.core.flows.FlowLogic 11 | import net.corda.core.flows.InitiatingFlow 12 | import net.corda.core.flows.StartableByRPC 13 | import net.corda.core.identity.Party 14 | import net.corda.core.transactions.SignedTransaction 15 | import net.corda.core.utilities.ProgressTracker 16 | 17 | /** 18 | * Simple move cash flow for demos. 19 | */ 20 | @InitiatingFlow 21 | class MoveCash(val recipient: Party, val amount: Amount) : FlowLogic() { 22 | 23 | override val progressTracker: ProgressTracker = ProgressTracker() 24 | 25 | @Suspendable 26 | override fun call(): SignedTransaction { 27 | val recipientSession = initiateFlow(recipient) 28 | val changeKey = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false).party.anonymise() 29 | return subFlow(ConfidentialMoveFungibleTokensFlow( 30 | partiesAndAmounts = listOf(PartyAndAmount(recipient, amount)), 31 | participantSessions = listOf(recipientSession), 32 | observerSessions = emptyList(), 33 | queryCriteria = null, 34 | changeHolder = changeKey 35 | )) 36 | } 37 | } 38 | 39 | @StartableByRPC 40 | class MoveCashShell(val recipient: Party, val amount: Long, val currency: String) : FlowLogic() { 41 | 42 | companion object { 43 | object MOVING : ProgressTracker.Step("Moving cash.") 44 | 45 | @JvmStatic 46 | fun tracker() = ProgressTracker(MOVING) 47 | } 48 | 49 | override val progressTracker: ProgressTracker = tracker() 50 | 51 | @Suspendable 52 | override fun call(): SignedTransaction { 53 | val fiatCurrency = FiatCurrency.getInstance(currency) 54 | return subFlow(MoveCash(recipient, amount of fiatCurrency)) 55 | } 56 | } -------------------------------------------------------------------------------- /integration-test/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript{ 2 | dependencies{ 3 | classpath "net.corda.plugins:quasar-utils:$corda_gradle_plugins_version" 4 | } 5 | } 6 | 7 | plugins { 8 | id 'java' 9 | } 10 | 11 | apply plugin: 'net.corda.plugins.quasar-utils' 12 | 13 | group 'com.r3.corda.finance.issuer' 14 | version '0.1' 15 | 16 | sourceCompatibility = 1.8 17 | 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | dependencies { 23 | testCompile group: 'junit', name: 'junit', version: '4.12' 24 | testCompile "$corda_release_group:corda-core:$corda_release_version" 25 | testCompile "$corda_release_group:corda-jackson:$corda_release_version" 26 | testCompile "$corda_release_group:corda-rpc:$corda_release_version" 27 | testCompile "$corda_release_group:corda-node-api:$corda_release_version" 28 | testCompile "$corda_release_group:corda:$corda_release_version" 29 | 30 | // For testing. 31 | testCompile "$corda_release_group:corda-node-driver:$corda_release_version" 32 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 33 | testCompile "junit:junit:$junit_version" 34 | 35 | 36 | testCompile project(":common:contracts") 37 | testCompile project(":common:workflows") 38 | testCompile project(":service") 39 | testCompile project(":client") 40 | testCompile "$tokens_release_group:tokens-contracts:$tokens_release_version" 41 | testCompile "$tokens_release_group:tokens-workflows:$tokens_release_version" 42 | testCompile "$tokens_release_group:tokens-money:$tokens_release_version" 43 | } 44 | 45 | sourceSets { 46 | main { 47 | resources { 48 | srcDir "config/dev" 49 | } 50 | } 51 | test { 52 | resources { 53 | srcDir "config/test" 54 | } 55 | } 56 | integrationTest { 57 | kotlin { 58 | compileClasspath += main.output + test.output 59 | runtimeClasspath += main.output + test.output 60 | srcDir file('src/integrationTest/kotlin') 61 | } 62 | } 63 | } 64 | 65 | configurations { 66 | integrationTestCompile.extendsFrom testCompile 67 | integrationTestRuntime.extendsFrom testRuntime 68 | } 69 | 70 | task integrationTest(type: Test, dependsOn: []) { 71 | testClassesDirs = sourceSets.integrationTest.output.classesDirs 72 | classpath = sourceSets.integrationTest.runtimeClasspath 73 | } 74 | -------------------------------------------------------------------------------- /config/dev/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logs 6 | node-${hostName} 7 | ${log-path}/archive 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | %highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink} 17 | > 18 | 19 | 20 | 21 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /daemon/config/dev/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logs 6 | node-${hostName} 7 | ${log-path}/archive 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | %highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink} 17 | > 18 | 19 | 20 | 21 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /daemon/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logs 6 | node-${hostName} 7 | ${log-path}/archive 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | %highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red 17 | blink} 18 | > 19 | 20 | 21 | 22 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/OpenBankingApiFactory.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import net.corda.client.jackson.JacksonSupport 4 | import okhttp3.OkHttpClient 5 | import okhttp3.logging.HttpLoggingInterceptor 6 | import org.slf4j.Logger 7 | import retrofit2.Retrofit 8 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory 9 | import retrofit2.converter.jackson.JacksonConverterFactory 10 | import java.util.concurrent.TimeUnit 11 | 12 | 13 | class OpenBankingApiFactory(val service: Class, val config: ApiConfig, val logger: Logger) { 14 | 15 | private val additionalHeaders: MutableMap = mutableMapOf() 16 | 17 | private fun createOkHttpClient(headers: Map): OkHttpClient { 18 | return OkHttpClient().newBuilder().apply { 19 | // TODO: Add the capability to customise an http request. 20 | readTimeout(10, TimeUnit.SECONDS) 21 | connectTimeout(5, TimeUnit.SECONDS) 22 | 23 | // Add logging. 24 | val logger = HttpLoggingInterceptor.Logger { s -> logger.debug(s) } 25 | val loggingInterceptor = HttpLoggingInterceptor(logger) 26 | loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY 27 | addInterceptor(loggingInterceptor) 28 | 29 | // Add default header. 30 | addInterceptor { chain -> 31 | val request = chain.request().newBuilder().apply { 32 | headers.forEach { key, value -> addHeader(key, value) } 33 | } 34 | chain.proceed(request.build()) 35 | } 36 | 37 | addInterceptor(loggingInterceptor) 38 | 39 | 40 | }.build() 41 | } 42 | 43 | fun withAdditionalHeaders(headers: Map): OpenBankingApiFactory { 44 | additionalHeaders.putAll(headers) 45 | return this 46 | } 47 | 48 | fun build(): T { 49 | val headers = mapOf("Authorization" to "Bearer ${config.apiAccessToken}") + additionalHeaders 50 | val okHttpClient = createOkHttpClient(headers) 51 | val retrofit = Retrofit.Builder() 52 | .baseUrl(config.apiBaseUrl) 53 | .client(okHttpClient) 54 | .addConverterFactory(JacksonConverterFactory.create(JacksonSupport.createNonRpcMapper())) 55 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 56 | .build() 57 | 58 | return retrofit.create(service) 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/states/NodeTransactionState.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.states 2 | 3 | import com.r3.corda.lib.tokens.contracts.types.TokenType 4 | import com.r3.corda.sdk.issuer.common.contracts.NodeTransactionContract 5 | import com.r3.corda.sdk.issuer.common.contracts.schemas.NodeTransactionStateSchemaV1 6 | import com.r3.corda.sdk.issuer.common.contracts.types.NodeTransactionStatus 7 | import com.r3.corda.sdk.issuer.common.contracts.types.NodeTransactionType 8 | import net.corda.core.contracts.AmountTransfer 9 | import net.corda.core.contracts.BelongsToContract 10 | import net.corda.core.contracts.LinearState 11 | import net.corda.core.contracts.UniqueIdentifier 12 | import net.corda.core.identity.AbstractParty 13 | import net.corda.core.identity.Party 14 | import net.corda.core.schemas.MappedSchema 15 | import net.corda.core.schemas.PersistentState 16 | import net.corda.core.schemas.QueryableState 17 | import java.time.Instant 18 | 19 | @BelongsToContract(NodeTransactionContract::class) 20 | data class NodeTransactionState( 21 | val amountTransfer: AmountTransfer, 22 | val createdAt: Instant, 23 | override val participants: List, 24 | val notes: String, 25 | val type: NodeTransactionType, 26 | val status: NodeTransactionStatus = NodeTransactionStatus.PENDING, 27 | override val linearId: UniqueIdentifier = UniqueIdentifier() 28 | ) : LinearState, QueryableState { 29 | 30 | override fun generateMappedObject(schema: MappedSchema): PersistentState { 31 | return when (schema) { 32 | is NodeTransactionStateSchemaV1 -> { 33 | NodeTransactionStateSchemaV1.PersistentNodeTransactionState( 34 | issuer = amountTransfer.source.name.toString(), 35 | counterparty = amountTransfer.destination.name.toString(), 36 | notes = notes, 37 | amount = amountTransfer.quantityDelta, 38 | currency = amountTransfer.token.tokenIdentifier, 39 | type = type.name, 40 | createdAt = createdAt.toEpochMilli(), 41 | status = status.name, 42 | linearId = linearId.id.toString() 43 | ) 44 | } 45 | else -> throw IllegalArgumentException("Unrecognised schema $schema") 46 | } 47 | } 48 | 49 | override fun supportedSchemas(): Iterable = listOf(NodeTransactionStateSchemaV1) 50 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/VerifyBankAccount.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.contracts.BankAccountContract 5 | import com.r3.corda.sdk.issuer.common.workflows.flows.AbstractVerifyBankAccount 6 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getBankAccountStateByLinearId 7 | import net.corda.core.contracts.Command 8 | import net.corda.core.contracts.UniqueIdentifier 9 | import net.corda.core.flows.FinalityFlow 10 | import net.corda.core.flows.FlowException 11 | import net.corda.core.flows.StartableByRPC 12 | import net.corda.core.flows.StartableByService 13 | import net.corda.core.transactions.SignedTransaction 14 | import net.corda.core.transactions.TransactionBuilder 15 | 16 | /** 17 | * For now accounts must be looked-up via account number. 18 | * TODO: Add constructors for other lookup options. 19 | */ 20 | @StartableByService 21 | @StartableByRPC 22 | class VerifyBankAccount( 23 | val linearId: UniqueIdentifier 24 | ) : AbstractVerifyBankAccount() { 25 | @Suspendable 26 | override fun call(): SignedTransaction { 27 | logger.info("Starting VerifyBankAccount flow.") 28 | val notary = serviceHub.networkMapCache.notaryIdentities.first() 29 | val bankAccountStateAndRef = getBankAccountStateByLinearId(linearId, serviceHub) 30 | ?: throw FlowException("Bank account with linearId $linearId not found.") 31 | 32 | val bankAccountState = bankAccountStateAndRef.state.data 33 | if (bankAccountState.verified) { 34 | throw FlowException("Bank account ${bankAccountState.accountNumber} is already verified.") 35 | } 36 | 37 | val ownerSsession = initiateFlow(bankAccountState.owner) 38 | 39 | logger.info("Updating verified flag for ${bankAccountState.accountNumber}.") 40 | val updatedBankAccountState = bankAccountState.copy(verified = true) 41 | val command = Command(BankAccountContract.Update(), listOf(ourIdentity.owningKey)) 42 | val utx = TransactionBuilder(notary = notary).apply { 43 | addInputState(bankAccountStateAndRef) 44 | addCommand(command) 45 | addOutputState(updatedBankAccountState) 46 | } 47 | 48 | // Sign with legal identity key. 49 | val stx = serviceHub.signInitialTransaction(utx) 50 | val sessions = if (serviceHub.myInfo.isLegalIdentity(bankAccountState.owner)) emptyList() else listOf(ownerSsession) 51 | return subFlow(FinalityFlow(stx, sessions)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/Daemon.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import com.r3.corda.lib.tokens.contracts.types.TokenType 4 | import io.github.classgraph.ClassGraph 5 | import net.corda.core.contracts.Amount 6 | import net.corda.core.messaging.CordaRPCOps 7 | import java.lang.reflect.InvocationTargetException 8 | 9 | 10 | private val clientsPackage = "com.r3.corda.finance.cash.issuer.daemon.clients" 11 | 12 | data class Balance(val accountId: BankAccountId, val nodeBalance: Amount, val bankBalance: Amount) 13 | 14 | class Daemon(services: CordaRPCOps, options: CommandLineOptions) : AbstractDaemon(services, options) { 15 | override fun scanForOpenBankingApiClients(): List { 16 | println("Scanning the 'clients' package for Open Banking API clients...\n") 17 | 18 | val list = mutableListOf() 19 | ClassGraph().enableAllInfo().whitelistPackages(clientsPackage).scan().use { scanResult -> 20 | scanResult.getSubclasses(OpenBankingApiClient::class.java.name).map { 21 | if (!it.simpleName.endsWith("Client")) { 22 | throw IllegalStateException("Your bank API client's name must be suffixed with \"Client\".") 23 | } 24 | val apiName = it.simpleName.removeSuffix("Client") 25 | // Check to see that an API interface definition has been provided for Retrofit. It is expected that 26 | // the API interface has the same name as the client minus the "Client" suffix. 27 | try { 28 | Class.forName("$clientsPackage.$apiName") 29 | } catch (e: ClassNotFoundException) { 30 | throw IllegalStateException("For each bank API you must implement an API interface for Retrofit " + 31 | "and an associated client. E.g. \"Monzo\" for the interface and \"MonzoClient\" for the " + 32 | "client. In addition an associated config file is required for each bank API. E.g. For " + 33 | "Monzo a config file called \"monzo.conf\" is required.") 34 | } 35 | 36 | println("\t* Loaded $apiName API interface and client.") 37 | val apiClient = try { 38 | it.loadClass().getDeclaredConstructor(String::class.java).newInstance(apiName.toLowerCase()) 39 | } catch (e: InvocationTargetException) { 40 | throw RuntimeException("Creating open banking API client failed. The most likely reason is bad credentials. " + 41 | "Check your API key. If you don't have a monzo or starling account, then run the daemon in --mock-mode") 42 | } 43 | list.add(apiClient as OpenBankingApi) 44 | } 45 | } 46 | return list.toList() 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/mock/MockMonzo.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon.mock 2 | 3 | import com.r3.corda.finance.cash.issuer.daemon.OpenBankingApi 4 | import com.r3.corda.lib.tokens.contracts.types.TokenType 5 | import com.r3.corda.sdk.issuer.common.contracts.types.BankAccount 6 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 7 | import com.r3.corda.sdk.issuer.common.workflows.utilities.MockContact 8 | import com.r3.corda.sdk.issuer.common.workflows.utilities.MockTransactionGenerator 9 | import net.corda.core.contracts.Amount 10 | import rx.Observable 11 | import java.util.* 12 | 13 | /** 14 | * A mock version of the Monzo API that is not yet that extensible. Only really used for demos. Only supports one 15 | * account for now. Will abstract into a mock bank later on. 16 | * 17 | * If the list of transactions is empty, then we'll generate transactions. Otherwise, they will be taken from the 18 | * list provided. 19 | */ 20 | @Suppress("Unused") 21 | class MockMonzo( 22 | override val accounts: List = listOf(mockMonzoAccount), 23 | override val transactions: MutableList = Collections.synchronizedList(mutableListOf()), 24 | override val contacts: List = mockMonzoCounterparties, 25 | fastForward: Boolean = false 26 | ) : OpenBankingApi(), MockClient { 27 | 28 | // Only one account per mock bank for now. 29 | override val account: BankAccount get() = accounts.single() 30 | override val transactionGenerator: MockTransactionGenerator = MockTransactionGenerator(mockTransactionGenerator(this, true), fastForward) 31 | 32 | override fun transactionsFeed(): Observable> { 33 | val accountId = account.accountId 34 | val lastTransactionTimestamp = lastTransactions[accountId]?.plusMillis(1L) 35 | val transactions = transactions.takeLastWhile { it.createdAt >= lastTransactionTimestamp } 36 | return Observable.from(listOf(transactions)) 37 | } 38 | 39 | /** 40 | * We need to keep a record of which contacts have depositied what balances, so that we don't withdraw more than 41 | * they have deposited. If they could, then the mock transactions wouldn't really make sense for testing purposes. 42 | */ 43 | override val contactBalances: MutableMap = Collections.synchronizedMap(hashMapOf()) 44 | 45 | override fun balance(accountId: String?): Amount { 46 | if (accountId == null) throw IllegalArgumentException("You must specify an accountId.") 47 | val balance = transactions.map(NostroTransaction::amount).sum() 48 | return Amount(balance, accounts.single().currency) 49 | } 50 | 51 | override fun startGeneratingTransactions(numberToGenerate: Int) { 52 | transactionGenerator.start(numberToGenerate) { transactions.add(it) } 53 | } 54 | 55 | override fun stopGeneratingTransactions() { 56 | transactionGenerator.stop() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/ProcessRedemptionPayment.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.contracts.NodeTransactionContract 5 | import com.r3.corda.sdk.issuer.common.contracts.states.BankAccountState 6 | import com.r3.corda.sdk.issuer.common.contracts.states.NostroTransactionState 7 | import com.r3.corda.sdk.issuer.common.contracts.types.NodeTransactionStatus 8 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getPendingRedemptionByNotes 9 | import net.corda.core.flows.FinalityFlow 10 | import net.corda.core.flows.FlowLogic 11 | import net.corda.core.flows.FlowSession 12 | import net.corda.core.flows.StartableByService 13 | import net.corda.core.transactions.SignedTransaction 14 | import net.corda.core.transactions.TransactionBuilder 15 | 16 | @StartableByService 17 | class ProcessRedemptionPayment(val signedTransaction: SignedTransaction) : FlowLogic() { 18 | @Suspendable 19 | override fun call() { 20 | val counterparty = signedTransaction.tx.toLedgerTransaction(serviceHub).referenceInputRefsOfType().single { 21 | it.state.data.owner != ourIdentity 22 | }.state.data.owner 23 | logger.info("Counterparty to redeem to is $counterparty") 24 | val notes = signedTransaction.tx.outputsOfType().single().description 25 | val pendingRedemption = try { 26 | getPendingRedemptionByNotes(notes, serviceHub)!! 27 | } catch (e: Throwable) { 28 | throw IllegalStateException("ERROR!!! Oh no!!! The issuer has made an erroneous redemption payment!") 29 | } 30 | val totalRedemptionAmountPending = pendingRedemption.state.data.amountTransfer.quantityDelta 31 | val transactionAmount = signedTransaction.tx.outputsOfType().single().amountTransfer.quantityDelta 32 | logger.info("Total redemption amount pending is $totalRedemptionAmountPending. Tx amount is $transactionAmount") 33 | logger.info("Total redemption payments") 34 | require(totalRedemptionAmountPending == transactionAmount) { "The payment must equal the redemption amount requested." } 35 | val notary = serviceHub.networkMapCache.notaryIdentities.first() 36 | val transactionBuilder = TransactionBuilder(notary = notary).apply { 37 | addInputState(pendingRedemption) 38 | addOutputState( 39 | pendingRedemption.state.data.copy(status = NodeTransactionStatus.COMPLETE), 40 | NodeTransactionContract.CONTRACT_ID 41 | ) 42 | addReferenceState(signedTransaction.tx.outRefsOfType().single().referenced()) 43 | addCommand(NodeTransactionContract.Update(), listOf(ourIdentity.owningKey)) 44 | } 45 | val stx = serviceHub.signInitialTransaction(transactionBuilder) 46 | subFlow(FinalityFlow(stx, emptySet())) 47 | logger.info(stx.tx.toString()) 48 | } 49 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/states/BankAccountState.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.states 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonSerialize 4 | import com.r3.corda.lib.tokens.contracts.types.TokenType 5 | import com.r3.corda.sdk.issuer.common.contracts.BankAccountContract 6 | import com.r3.corda.sdk.issuer.common.contracts.schemas.BankAccountStateSchemaV1 7 | import com.r3.corda.sdk.issuer.common.contracts.serializers.TokenTypeSerializer 8 | import com.r3.corda.sdk.issuer.common.contracts.types.AccountNumber 9 | import com.r3.corda.sdk.issuer.common.contracts.types.BankAccountType 10 | import net.corda.core.contracts.BelongsToContract 11 | import net.corda.core.contracts.LinearState 12 | import net.corda.core.contracts.UniqueIdentifier 13 | import net.corda.core.identity.AbstractParty 14 | import net.corda.core.identity.Party 15 | import net.corda.core.schemas.MappedSchema 16 | import net.corda.core.schemas.PersistentState 17 | import net.corda.core.schemas.QueryableState 18 | import java.time.Instant 19 | 20 | 21 | // The same principle applies to all accounts, customers and issuers. 22 | // Only match transactions once the accounts have been verified/whitelisted. 23 | @BelongsToContract(BankAccountContract::class) 24 | data class BankAccountState( 25 | val owner: Party, 26 | val verifier: Party, 27 | val accountName: String, 28 | val accountNumber: AccountNumber, 29 | @JsonSerialize(using = TokenTypeSerializer::class) val currency: TokenType, 30 | val type: BankAccountType, 31 | val verified: Boolean, 32 | override val linearId: UniqueIdentifier, 33 | val lastUpdated: Instant = Instant.now() 34 | ) : LinearState, QueryableState { 35 | 36 | constructor( 37 | owner: Party, 38 | verifier: Party, 39 | accountId: String, 40 | accountName: String, 41 | accountNumber: AccountNumber, 42 | currency: TokenType, 43 | type: BankAccountType 44 | ) : this(owner, verifier, accountName, accountNumber, currency, type, false, UniqueIdentifier(accountId)) 45 | 46 | override val participants: List get() = listOf(owner) 47 | 48 | override fun generateMappedObject(schema: MappedSchema): PersistentState { 49 | return when (schema) { 50 | is BankAccountStateSchemaV1 -> BankAccountStateSchemaV1.PersistentBankAccountState( 51 | owner = owner.name.toString(), 52 | verifier = verifier.name.toString(), 53 | accountName = accountName, 54 | accountNumber = accountNumber.digits, 55 | currency = currency.tokenIdentifier, 56 | type = type.name, 57 | verified = verified, 58 | lastUpdated = lastUpdated.toEpochMilli(), 59 | linearId = linearId.id.toString(), 60 | externalId = linearId.externalId.toString() 61 | ) 62 | else -> throw IllegalArgumentException("Unrecognised schema $schema") 63 | } 64 | } 65 | 66 | override fun supportedSchemas(): Iterable = listOf(BankAccountStateSchemaV1) 67 | } 68 | -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/ProcessRedemption.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.lib.tokens.contracts.states.FungibleToken 5 | import com.r3.corda.lib.tokens.contracts.utilities.sumTokenStatesOrThrow 6 | import com.r3.corda.lib.tokens.contracts.utilities.sumTokenStatesOrZero 7 | import com.r3.corda.lib.tokens.workflows.utilities.toParty 8 | import com.r3.corda.sdk.issuer.common.contracts.NodeTransactionContract 9 | import com.r3.corda.sdk.issuer.common.contracts.states.NodeTransactionState 10 | import com.r3.corda.sdk.issuer.common.contracts.types.NodeTransactionType 11 | import com.r3.corda.sdk.issuer.common.workflows.utilities.GenerationScheme 12 | import com.r3.corda.sdk.issuer.common.workflows.utilities.generateRandomString 13 | import net.corda.core.contracts.AmountTransfer 14 | import net.corda.core.flows.FinalityFlow 15 | import net.corda.core.flows.FlowLogic 16 | import net.corda.core.flows.StartableByService 17 | import net.corda.core.transactions.SignedTransaction 18 | import net.corda.core.transactions.TransactionBuilder 19 | import java.time.Instant 20 | 21 | @StartableByService 22 | class ProcessRedemption(val signedTransaction: SignedTransaction) : FlowLogic() { 23 | @Suspendable 24 | override fun call(): SignedTransaction { 25 | // Calculate the redemption amount. 26 | val ledgerTx = signedTransaction.tx.toLedgerTransaction(serviceHub) 27 | val inputAmount = ledgerTx.inputsOfType().sumTokenStatesOrThrow() 28 | val inputIssuedTokenType = inputAmount.token 29 | val outputAmount = ledgerTx.outputsOfType().sumTokenStatesOrZero(inputIssuedTokenType) 30 | val redemptionAmount = inputAmount - outputAmount 31 | val redeemingParty = ledgerTx.inputsOfType() 32 | .map { it.holder.toParty(serviceHub) } 33 | .toSet() 34 | .single() 35 | // Create the internal record. This is only used by the issuer. 36 | val nodeTransactionState = NodeTransactionState( 37 | amountTransfer = AmountTransfer( 38 | quantityDelta = -redemptionAmount.quantity / 100, // Hack. Fix this properly. 39 | token = inputIssuedTokenType.tokenType, 40 | source = ourIdentity, 41 | destination = redeemingParty 42 | ), 43 | notes = generateRandomString(10, GenerationScheme.NUMBERS), 44 | createdAt = Instant.now(), 45 | participants = listOf(ourIdentity), 46 | type = NodeTransactionType.REDEMPTION 47 | ) 48 | val notary = serviceHub.networkMapCache.notaryIdentities.first() 49 | val tx = TransactionBuilder(notary = notary).apply { 50 | addOutputState(nodeTransactionState, NodeTransactionContract.CONTRACT_ID) 51 | addCommand(NodeTransactionContract.Create(), listOf(ourIdentity.owningKey)) 52 | } 53 | val partiallySignedTransaction = serviceHub.signInitialTransaction(tx) 54 | return subFlow(FinalityFlow(partiallySignedTransaction, emptyList())) 55 | } 56 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/flows/AddBankAccount.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.contracts.BankAccountContract 5 | import com.r3.corda.sdk.issuer.common.contracts.types.BankAccount 6 | import com.r3.corda.sdk.issuer.common.contracts.types.toState 7 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getBankAccountStateByAccountNumber 8 | import net.corda.core.contracts.Command 9 | import net.corda.core.flows.* 10 | import net.corda.core.identity.Party 11 | import net.corda.core.transactions.SignedTransaction 12 | import net.corda.core.transactions.TransactionBuilder 13 | import net.corda.core.utilities.ProgressTracker 14 | 15 | /** 16 | * Adds a new [BankAccount] state to the ledger. 17 | * This assumes that the [BankAccount] will only ever need to be shared with one verifier/issuer. There are no flows to 18 | * share an existing [BankAccount] with another issuer (they were deleted in a previous PR). However it is likely that 19 | * the same bank account state might need to be shared with more than one issuer. 20 | */ 21 | @StartableByRPC 22 | @StartableByService 23 | @InitiatingFlow 24 | class AddBankAccount(val bankAccount: BankAccount, val verifier: Party) : FlowLogic() { 25 | 26 | // TODO: Add the rest of the progress tracker. 27 | companion object { 28 | object FINALISING : ProgressTracker.Step("Finalising transaction.") 29 | 30 | fun tracker() = ProgressTracker(FINALISING) 31 | } 32 | 33 | override val progressTracker: ProgressTracker = tracker() 34 | 35 | @Suspendable 36 | override fun call(): SignedTransaction { 37 | logger.info("Starting AddBankAccount flow...") 38 | val accountNumber = bankAccount.accountNumber 39 | 40 | logger.info("Checking for existence of state for $bankAccount.") 41 | val result = getBankAccountStateByAccountNumber(accountNumber, serviceHub) 42 | 43 | if (result != null) { 44 | val linearId = result.state.data.linearId 45 | throw IllegalArgumentException("Bank account $accountNumber already exists with linearId ($linearId).") 46 | } 47 | 48 | logger.info("No state for $bankAccount. Adding it.") 49 | val bankAccountState = bankAccount.toState(ourIdentity, verifier) 50 | val notary = serviceHub.networkMapCache.notaryIdentities.first() 51 | 52 | // The node running this flow is always the only signer. 53 | val command = Command(BankAccountContract.Add(), listOf(ourIdentity.owningKey)) 54 | val unsignedTransaction = TransactionBuilder(notary = notary).apply { 55 | addOutputState(bankAccountState) 56 | addCommand(command) 57 | } 58 | 59 | // Sign the transaction with legal identity key. 60 | val signedTransaction = serviceHub.signInitialTransaction(unsignedTransaction) 61 | 62 | progressTracker.currentStep = FINALISING 63 | // If the verifier IS the node running this flow then don't initiate a session for FinalityFlow. 64 | val verifierSession = initiateFlow(bankAccountState.verifier) 65 | val sessionsForFinality = if (serviceHub.myInfo.isLegalIdentity(verifier)) emptyList() else listOf(verifierSession) 66 | return subFlow(FinalityFlow(signedTransaction, sessionsForFinality)) 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /common/contracts/src/main/kotlin/com/r3/corda/sdk/issuer/common/contracts/states/NostroTransactionState.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.contracts.states 2 | 3 | import com.r3.corda.lib.tokens.contracts.types.TokenType 4 | import com.r3.corda.sdk.issuer.common.contracts.NostroTransactionContract 5 | import com.r3.corda.sdk.issuer.common.contracts.schemas.NostroTransactionStateSchemaV1 6 | import com.r3.corda.sdk.issuer.common.contracts.types.AccountNumber 7 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransactionStatus 8 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransactionType 9 | import net.corda.core.contracts.AmountTransfer 10 | import net.corda.core.contracts.BelongsToContract 11 | import net.corda.core.contracts.LinearState 12 | import net.corda.core.contracts.UniqueIdentifier 13 | import net.corda.core.identity.AbstractParty 14 | import net.corda.core.identity.Party 15 | import net.corda.core.schemas.MappedSchema 16 | import net.corda.core.schemas.PersistentState 17 | import net.corda.core.schemas.QueryableState 18 | import java.time.Instant 19 | 20 | @BelongsToContract(NostroTransactionContract::class) 21 | data class NostroTransactionState( 22 | val accountId: String, 23 | val amountTransfer: AmountTransfer, 24 | val description: String, 25 | val createdAt: Instant, 26 | override val participants: List, 27 | override val linearId: UniqueIdentifier, 28 | val lastUpdated: Instant = createdAt, 29 | val status: NostroTransactionStatus = NostroTransactionStatus.UNMATCHED, 30 | val type: NostroTransactionType = NostroTransactionType.UNKNOWN 31 | ) : LinearState, QueryableState { 32 | 33 | constructor( 34 | accountId: String, 35 | issuer: Party, 36 | transactionId: String, 37 | amountTransfer: AmountTransfer, 38 | description: String, 39 | createdAt: Instant 40 | ) : this(accountId, amountTransfer, description, createdAt, listOf(issuer), UniqueIdentifier(transactionId)) 41 | 42 | fun updateStatus(newStatus: NostroTransactionStatus): NostroTransactionState = copy(status = newStatus) 43 | fun updateType(newType: NostroTransactionType): NostroTransactionState = copy(type = newType) 44 | 45 | override fun generateMappedObject(schema: MappedSchema): PersistentState { 46 | return when (schema) { 47 | is NostroTransactionStateSchemaV1 -> NostroTransactionStateSchemaV1.PersistentNostroTransactionState( 48 | accountId = accountId, 49 | transactionId = linearId.externalId ?: throw IllegalStateException("This should never be null."), 50 | amount = amountTransfer.quantityDelta, 51 | currency = amountTransfer.token.tokenIdentifier, 52 | sourceAccountNumber = amountTransfer.source.digits, 53 | destinationAccountNumber = amountTransfer.destination.digits, 54 | lastUpdated = lastUpdated.toEpochMilli(), 55 | status = status.name, 56 | type = type.name, 57 | createdAt = createdAt.toEpochMilli(), 58 | linearId = linearId.id.toString() 59 | ) 60 | else -> throw IllegalArgumentException("Unrecognised schema $schema") 61 | } 62 | } 63 | 64 | override fun supportedSchemas(): Iterable = listOf(NostroTransactionStateSchemaV1) 65 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/IssueCash.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.lib.tokens.contracts.utilities.heldBy 5 | import com.r3.corda.lib.tokens.contracts.utilities.issuedBy 6 | import com.r3.corda.lib.tokens.contracts.utilities.of 7 | import com.r3.corda.lib.tokens.workflows.flows.rpc.ConfidentialIssueTokens 8 | import com.r3.corda.sdk.issuer.common.contracts.NodeTransactionContract 9 | import com.r3.corda.sdk.issuer.common.contracts.states.NodeTransactionState 10 | import com.r3.corda.sdk.issuer.common.contracts.types.NodeTransactionStatus 11 | import net.corda.core.flows.* 12 | import net.corda.core.transactions.SignedTransaction 13 | import net.corda.core.transactions.TransactionBuilder 14 | import net.corda.core.utilities.ProgressTracker 15 | 16 | // TODO: Denominations? E.g. 100 split between 10 issuances of 10. 17 | // TODO: Ask the counterparty for a random key and issue to that key. 18 | // TODO: Move all tx generation stuff to the common library. 19 | // TODO: Add the option for issuing to anonymous public keys. 20 | 21 | @InitiatingFlow 22 | @StartableByService 23 | class IssueCash(val stx: SignedTransaction) : FlowLogic>() { 24 | 25 | companion object { 26 | object GENERATING_TX : ProgressTracker.Step("Generating node transaction") 27 | object SIGNING_TX : ProgressTracker.Step("Signing node transaction") 28 | object FINALISING_TX : ProgressTracker.Step("Obtaining notary signature and recording node transaction") { 29 | override fun childProgressTracker() = FinalityFlow.tracker() 30 | } 31 | 32 | @JvmStatic 33 | fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) 34 | } 35 | 36 | override val progressTracker = tracker() 37 | @Suspendable 38 | override fun call(): Pair { 39 | // Our chosen notary. 40 | val notary = serviceHub.networkMapCache.notaryIdentities.first() 41 | val nodeTransactionStateAndRef = stx.tx.outRefsOfType().single() 42 | val nodeTransactionState = nodeTransactionStateAndRef.state.data 43 | 44 | progressTracker.currentStep = GENERATING_TX 45 | // TODO: Check that the bank account states are verified. 46 | val internalBuilder = TransactionBuilder(notary = notary).apply { 47 | addCommand(NodeTransactionContract.Update(), listOf(ourIdentity.owningKey)) 48 | addInputState(nodeTransactionStateAndRef) 49 | addOutputState(nodeTransactionState.copy(status = NodeTransactionStatus.COMPLETE), NodeTransactionContract.CONTRACT_ID) 50 | } 51 | 52 | progressTracker.currentStep = SIGNING_TX 53 | val signedTransaction = serviceHub.signInitialTransaction(internalBuilder) 54 | 55 | progressTracker.currentStep = FINALISING_TX 56 | val internalFtx = subFlow(FinalityFlow(signedTransaction, emptySet(), FINALISING_TX.childProgressTracker())) 57 | 58 | /** Commit the cash issuance transaction. */ 59 | val recipient = nodeTransactionState.amountTransfer.destination 60 | val quantity = nodeTransactionState.amountTransfer.quantityDelta 61 | val tokenType = nodeTransactionState.amountTransfer.token 62 | val tokenToIssue = quantity of tokenType issuedBy ourIdentity heldBy recipient 63 | val issueTx = subFlow(ConfidentialIssueTokens(tokensToIssue = listOf(tokenToIssue), observers = emptyList())) 64 | // Return the internal tx and the external tx. 65 | return Pair(internalFtx, issueTx) 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /daemon/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.github.jengelman.gradle.plugins:shadow:4.0.1' 9 | } 10 | } 11 | 12 | apply plugin: 'kotlin' 13 | apply plugin: 'java' 14 | apply plugin: 'com.github.johnrengelman.shadow' 15 | apply plugin: 'project-report' 16 | apply plugin: 'application' 17 | 18 | sourceSets { 19 | main { 20 | resources { 21 | srcDir "config/dev" 22 | } 23 | } 24 | test { 25 | resources { 26 | srcDir "config/test" 27 | } 28 | } 29 | integrationTest { 30 | kotlin { 31 | compileClasspath += main.output + test.output 32 | runtimeClasspath += main.output + test.output 33 | srcDir file('src/integrationTest/kotlin') 34 | } 35 | } 36 | } 37 | 38 | configurations { 39 | integrationTestCompile.extendsFrom testCompile 40 | integrationTestRuntime.extendsFrom testRuntime 41 | } 42 | 43 | dependencies { 44 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 45 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 46 | testCompile "junit:junit:$junit_version" 47 | 48 | // Corda integration dependencies 49 | compile "$corda_release_group:corda-rpc:$corda_release_version" 50 | 51 | // Class path scanning. 52 | compile "io.github.classgraph:classgraph:4.1.12" 53 | 54 | // JOpt: for command line flags. 55 | compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version" 56 | 57 | // OKHttp for HTTP library. 58 | compile "com.squareup.okhttp3:logging-interceptor:3.9.1" 59 | 60 | // Retrofit for REST API client. 61 | compile "com.squareup.retrofit2:retrofit:$retrofit_version" 62 | compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" 63 | compile "com.squareup.retrofit2:converter-jackson:$retrofit_version" 64 | 65 | // Logging. 66 | compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" 67 | compile "org.apache.logging.log4j:log4j-web:$log4j_version" 68 | compile "org.slf4j:jul-to-slf4j:$slf4j_version" 69 | 70 | // Project dependencies. 71 | compile project(":common:contracts") 72 | compile project(":service") 73 | 74 | // Token SDK. 75 | compile "$tokens_release_group:tokens-workflows:$tokens_release_version" 76 | compile "$tokens_release_group:tokens-money:$tokens_release_version" 77 | 78 | } 79 | 80 | task integrationTest(type: Test, dependsOn: []) { 81 | testClassesDirs = sourceSets.integrationTest.output.classesDirs 82 | classpath = sourceSets.integrationTest.runtimeClasspath 83 | } 84 | 85 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 86 | kotlinOptions { 87 | languageVersion = "1.2" 88 | apiVersion = "1.2" 89 | jvmTarget = "1.8" 90 | javaParameters = true // Useful for reflection. 91 | } 92 | } 93 | 94 | shadowJar { 95 | baseName = 'daemon' 96 | classifier = 'fat' 97 | version = null 98 | zip64 true 99 | } 100 | 101 | mainClassName = 'com.r3.corda.finance.cash.issuer.daemon.MainKt' 102 | 103 | jar { 104 | manifest { 105 | attributes 'Main-Class': 'com.r3.corda.finance.cash.issuer.daemon.MainKt' 106 | } 107 | baseName = project.name 108 | from { 109 | configurations.compile.collect { 110 | it.isDirectory() ? it : zipTree(it).matching { 111 | exclude 'META-INF/**.RSA' 112 | exclude 'META-INF/MANIFEST.MF' 113 | exclude 'META-INF/log4j-provider.properties' 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/AddNostroTransactions.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.contracts.NostroTransactionContract 5 | import com.r3.corda.sdk.issuer.common.contracts.states.NostroTransactionState 6 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 7 | import com.r3.corda.sdk.issuer.common.contracts.types.toState 8 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getNostroTransactionStateByTransactionId 9 | import net.corda.core.contracts.Command 10 | import net.corda.core.flows.* 11 | import net.corda.core.transactions.TransactionBuilder 12 | import net.corda.core.utilities.ProgressTracker 13 | import java.time.Instant 14 | 15 | // TODO: Change this to add one transaction at a time. 16 | // Don't pass back the timestamp of the last added transaction. as this flow could be used to add old transactions. 17 | 18 | /** 19 | * Records a list of nostro transaction objects. We can batch up issuance of these as nostro transaction states always 20 | * remain private to the issuer node. We want this flow to finish as soon as possible, so we can return back to the 21 | * daemon process with the transactions which have been committed up to this point. 22 | */ 23 | @StartableByRPC 24 | @InitiatingFlow 25 | class AddNostroTransactions(val newNostroTransactions: List) : FlowLogic>() { 26 | 27 | companion object { 28 | // TODO: Add the rest of the progress tracker. 29 | object FINALISING : ProgressTracker.Step("Finalising transaction.") { 30 | override fun childProgressTracker() = FinalityFlow.tracker() 31 | } 32 | 33 | @JvmStatic 34 | fun tracker() = ProgressTracker(FINALISING) 35 | } 36 | 37 | override val progressTracker: ProgressTracker = tracker() 38 | 39 | @Suspendable 40 | override fun call(): Map { 41 | // As we are polling for new transactions, there might not be any new transactions to add. 42 | // This should really be checked on the RPC client side but just double checking it here 43 | // as well, otherwise we'll end up trying to commit transactions with no output states! 44 | logger.info("Starting AddNostroTransaction flow...") 45 | 46 | newNostroTransactions.forEach { logger.info(it.toString()) } 47 | 48 | if (newNostroTransactions.isEmpty()) { 49 | return emptyMap() 50 | } 51 | 52 | // Filter out transactions which have been added before. 53 | val transactionsToRecord = newNostroTransactions.filter { (transactionId) -> 54 | getNostroTransactionStateByTransactionId(transactionId, serviceHub) == null 55 | } 56 | 57 | if (transactionsToRecord.isEmpty()) { 58 | return emptyMap() 59 | } 60 | 61 | // Convert into an unmapped nostro transaction state. 62 | val nostroTransactionStates = transactionsToRecord.map { it.toState(ourIdentity) } 63 | 64 | // Now, commit the nostro transaction records to the ledger. It's only the issuer that sees this though. 65 | val notary = serviceHub.networkMapCache.notaryIdentities.first() 66 | // It's always the issuer that signs. 67 | val command = Command(NostroTransactionContract.Add(), listOf(ourIdentity.owningKey)) 68 | 69 | // Create a transaction builder, then add all the nostro transaction states and command. 70 | val unsignedTransaction = TransactionBuilder(notary = notary).addCommand(command) 71 | nostroTransactionStates.forEach { 72 | unsignedTransaction.addOutputState(it, NostroTransactionContract.CONTRACT_ID) 73 | } 74 | 75 | val signedTransaction = serviceHub.signInitialTransaction(unsignedTransaction) 76 | progressTracker.currentStep = FINALISING 77 | val finalisedTransaction = subFlow(FinalityFlow(signedTransaction, emptySet(), FINALISING.childProgressTracker())) 78 | 79 | // The flow returns the IDs and timestamps of the last updates for each nostro account so 80 | // the daemon knows what has been recorded to date. 81 | val outputs = finalisedTransaction.tx.outRefsOfType().map { it.state.data } 82 | val outputsByAccountId = outputs.groupBy({ it.accountId }, { it.createdAt }) 83 | return outputsByAccountId.mapValues { it.value.max()!! } // Map values will never be empty lists. 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/mock/MockUtilities.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon.mock 2 | 3 | import com.r3.corda.lib.tokens.money.GBP 4 | import com.r3.corda.sdk.issuer.common.contracts.types.BankAccount 5 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 6 | import com.r3.corda.sdk.issuer.common.contracts.types.UKAccountNumber 7 | import com.r3.corda.sdk.issuer.common.workflows.utilities.* 8 | import net.corda.core.internal.randomOrNull 9 | import java.time.Instant 10 | 11 | interface MockClient { 12 | val transactions: MutableList 13 | val contacts: List 14 | val transactionGenerator: MockTransactionGenerator 15 | val contactBalances: MutableMap 16 | val account: BankAccount 17 | fun startGeneratingTransactions(numberToGenerate: Int) 18 | fun stopGeneratingTransactions() 19 | } 20 | 21 | val mockMonzoAccount = BankAccount( 22 | accountId = "acc_00009RE1DzwEupfetgm84f", 23 | accountName = "R3 Ltd", 24 | accountNumber = UKAccountNumber("97784499", "040004"), 25 | currency = GBP 26 | ) 27 | 28 | val mockMonzoCounterparties = listOf( 29 | MockContact("anonuser_e4d0fc5b4693fc16219ef7", "Roger Willis", UKAccountNumber("442200", "13371337")), 30 | MockContact("anonuser_qb9hcjpaem61mocujv3zh4", "David Nicol", UKAccountNumber("873456", "12345678")), 31 | MockContact("anonuser_x636uuqj1b913bd1mflm61", "Richard Brown", UKAccountNumber("059015", "73510753")), 32 | MockContact("anonuser_keu8gr5fs4qw6kj4nufy91", "Todd McDonald", UKAccountNumber("022346", "34782115")), 33 | MockContact("anonuser_z1oxucxi9ooep90oteb4qw", "Mike Hearn", UKAccountNumber("040040", "90143578")) 34 | ) 35 | 36 | /** 37 | * This is quite a naive generator but it's useful for testing. The generation logic is as follows: 38 | * 39 | * 1. If no contacts have made a deposit then a random one is picked to make a deposit. 40 | * 2. If one or more contacts have made deposits, then: 41 | * - if allowWithdrawals is set then there is a 40% chance that one of the depositors will withdraw an amount 42 | * from the account and this amount cannot be greater than the amount they have deposited. Else, there is a 60% 43 | * chance that a randomly chosen contact from the list will deposit more in the account. 44 | * - if allowWithdrawals is set to false, then a random contact is chosen to make a deposit. 45 | * 3. Source and destination accounts are always filled in and obtained from the static data provided to this class 46 | * constructor. 47 | * 4. All the other stuff is made up. 48 | * 49 | * It is currently tailored for monzo... 50 | */ 51 | fun mockTransactionGenerator( 52 | mockApi: MockClient, 53 | allowWithdrawals: Boolean 54 | ): () -> NostroTransaction { 55 | 56 | // A hacky function that either generates semi-realistic transaction amounts for randomly selected contacts. 57 | fun nextContactAndAmount(): Pair { 58 | return if (mockApi.contactBalances.isEmpty() || !allowWithdrawals) { 59 | Pair(mockApi.contacts.randomOrNull()!!, randomAmountGenerator()) 60 | } else { 61 | when (rng.nextFloat()) { 62 | in 0.0f..0.6f -> Pair(mockApi.contacts.randomOrNull()!!, randomAmountGenerator()) 63 | in 0.6f..1.0f -> { 64 | val contact = mockApi.contactBalances.keys.toList().randomOrNull()!! 65 | val maxAmount = mockApi.contactBalances[contact]!! 66 | val amount = -randomAmountGenerator(maxAmount) 67 | Pair(contact, amount) 68 | } 69 | else -> throw IllegalStateException("This shouldn't happen!!") 70 | } 71 | } 72 | } 73 | 74 | return { 75 | val (contact, amount) = nextContactAndAmount() 76 | 77 | // Tx data. 78 | val nextId = "tx_00009${generateRandomString(16)}" 79 | val type = "payport_faster_payments" 80 | val description = "dummy description for now..." 81 | val now = Instant.now() 82 | 83 | val nostroTransaction = if (amount > 0L) { 84 | NostroTransaction(nextId, mockApi.account.accountId, amount, GBP, type, description, now, 85 | source = contact.accountNumber, 86 | destination = mockApi.account.accountNumber 87 | ) 88 | } else { 89 | NostroTransaction(nextId, mockApi.account.accountId, amount, GBP, type, description, now, 90 | source = mockApi.account.accountNumber, 91 | destination = contact.accountNumber 92 | ) 93 | } 94 | 95 | // Add the balance to the map. 96 | val contactBalance = mockApi.contactBalances[contact] ?: 0L 97 | mockApi.contactBalances.put(contact, contactBalance + amount) 98 | 99 | nostroTransaction 100 | } 101 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/utilities/MockDataUtilities.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.utilities 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.contracts.types.AccountNumber 5 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 6 | import rx.Observable 7 | import rx.Subscription 8 | import java.util.* 9 | import java.util.concurrent.TimeUnit 10 | import kotlin.streams.toList 11 | 12 | /** 13 | * For generating id numbers, etc. 14 | */ 15 | enum class GenerationScheme { LETTERS, NUMBERS, LETTERS_AND_NUMBERS } 16 | 17 | private const val capitals = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 18 | private const val lowercase = "abcdefghijklmnopqrstuvwqyz" 19 | private const val numbers = "0123456789" 20 | 21 | val rng = Random() 22 | 23 | // Generates random strings for transaction IDs and account IDs. Can use letters or numbers, or both. 24 | @Suspendable 25 | fun generateRandomString(length: Long, scheme: GenerationScheme = GenerationScheme.LETTERS_AND_NUMBERS): String { 26 | val source = when (scheme) { 27 | GenerationScheme.LETTERS -> capitals + lowercase 28 | GenerationScheme.NUMBERS -> numbers 29 | else -> capitals + lowercase + numbers 30 | } 31 | return rng.ints(length, 0, source.length).toList().map(source::get).joinToString("") 32 | } 33 | 34 | 35 | /** 36 | * For generating random numbers. 37 | * A ceiling can be provided to ensure we generate numbers < ceiling. 38 | */ 39 | // Simple function to create realistic'ish transaction amounts. For now it's OK. 40 | @Suspendable 41 | fun randomAmountGenerator(ceiling: Long? = null): Long { 42 | fun generate(): Long { 43 | val selector = rng.nextInt(11) 44 | return when (selector) { 45 | 0 -> rng.nextInt(1000) * 10000L // Millions. 46 | in 1..4 -> rng.nextInt(1000) * 1000L // Hundred thousands. 47 | in 5..7 -> rng.nextInt(1000) * 100L // Ten thousands. 48 | in 8..9 -> rng.nextInt(1000) * 10L // Thousands. 49 | 10 -> rng.nextInt(1000).toLong() // Hundreds. 50 | else -> throw IllegalStateException("Something bad happened!") 51 | } 52 | } 53 | 54 | // This is very hacky but does the job for now. 55 | return if (ceiling == null) { 56 | generate() 57 | } else { 58 | var amount = generate() 59 | while (amount > ceiling) { 60 | amount = generate() 61 | } 62 | amount 63 | } 64 | } 65 | 66 | /** 67 | * For generating random delays. 68 | */ 69 | // We want the delays to be mostly long but we also want a fair few short ones too. 70 | // We can "fast forward" the generation if we can't be bothered to sit around. This caps the maximum interval at 3 71 | // seconds and produces more "zero" delays. 72 | @Suspendable 73 | fun randomDelayGenerator(fastForward: Boolean): () -> Long { 74 | return { 75 | val random = rng.nextInt(11) 76 | val selector = if (fastForward) random % 3 else random 77 | val interval = when (selector) { 78 | 0 -> rng.nextInt(1) // Up to 1 second delay. (1/10) 79 | 1 -> rng.nextInt(3) // Up to 3 second delay. (1/10) 80 | in 2..3 -> rng.nextInt(5) // Up to 5 second delay. (2/10) 81 | in 4..9 -> rng.nextInt(10 + 1 - 5) + 5 // Between 5 to 10 second delay. (6/10) 82 | 10 -> rng.nextInt(20 + 1 - 10) + 10 // Between 10 to 20 second delay. (1/10) 83 | else -> throw IllegalStateException("Something bad happened!") 84 | } 85 | interval.toLong() 86 | } 87 | } 88 | 89 | /** 90 | * A mock contact for testing purposes. 91 | */ 92 | data class MockContact(val id: String, val name: String, val accountNumber: AccountNumber) 93 | 94 | /** 95 | * Generates mock transaction data at random intervals, forever. 96 | * TODO: Mess around with publishOn() / subscribeOn() to see which schedulers perform the best. 97 | */ 98 | class MockTransactionGenerator( 99 | txGenerator: () -> NostroTransaction, 100 | fastForward: Boolean, 101 | gapGenerator: () -> Long = randomDelayGenerator(fastForward) 102 | ) { 103 | private var generatorSubscription: Subscription? = null 104 | 105 | // This calls the provided function at random intervals. 106 | private val transactionStream = Observable 107 | .fromCallable { txGenerator() } 108 | .delay { Observable.timer(gapGenerator(), TimeUnit.SECONDS) } 109 | .repeat() 110 | 111 | fun start(numberToGenerate: Int = 0, block: (NostroTransaction) -> Unit) { 112 | val observable = if (numberToGenerate == 0) transactionStream else transactionStream.take(numberToGenerate) 113 | generatorSubscription = observable.subscribe { block(it) } 114 | } 115 | 116 | fun stop() { 117 | generatorSubscription?.unsubscribe() 118 | } 119 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/Main.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import com.r3.corda.finance.cash.issuer.daemon.mock.MockMonzo 4 | import com.r3.corda.lib.tokens.money.GBP 5 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 6 | import com.r3.corda.sdk.issuer.common.contracts.types.UKAccountNumber 7 | import com.r3.corda.sdk.issuer.common.workflows.utilities.generateRandomString 8 | import joptsimple.OptionException 9 | import net.corda.client.rpc.CordaRPCClient 10 | import net.corda.client.rpc.RPCException 11 | import net.corda.core.messaging.CordaRPCOps 12 | import net.corda.core.utilities.NetworkHostAndPort.Companion.parse 13 | import java.time.Instant 14 | import java.util.* 15 | import kotlin.system.exitProcess 16 | 17 | private fun parseArguments(vararg args: String): CommandLineOptions { 18 | val argsParser = ArgsParser() 19 | return try { 20 | argsParser.parse(*args) 21 | } catch (ex: OptionException) { 22 | println("Invalid command line arguments: ${ex.message}") 23 | argsParser.printHelp(System.out) 24 | exitProcess(1) 25 | } 26 | } 27 | 28 | // Connects to a Corda node specified by a hostname and port using the provided user name and pawssword. 29 | private fun connectToCordaRpc(hostAndPort: String, username: String, password: String): CordaRPCOps { 30 | println("Connecting to Issuer node $hostAndPort.") 31 | val nodeAddress = parse(hostAndPort) 32 | val client = CordaRPCClient(nodeAddress) 33 | val cordaRpcOps = client.start(username, password).proxy 34 | println("Connected!") 35 | return cordaRpcOps 36 | } 37 | 38 | private fun welcome() { 39 | println() 40 | println("Welcome to the Corda cash issuer daemon.") 41 | println() 42 | println(" .-\"\"-. "); 43 | println(" /" + "[O]" + " __\\ ") 44 | println(" _|__" + "o" + " LI|_ -------- I'm R3D2, the Corda Cash Issuer Daemon! ") 45 | println(" / | " + "====" + " | \\ ") 46 | println(" |_| " + "====" + " |_| ") 47 | println(" " + "|" + "|\" || |" + "|" + " ") 48 | println(" " + "|" + "|LI " + "o" + " |" + "|" + " ") 49 | println(" " + "|" + "|'----'|" + "|" + " ") 50 | println(" /__| |__\\ ") 51 | println() 52 | } 53 | 54 | private fun prompt() = print("> ") 55 | 56 | private fun manual(daemon: AbstractDaemon, cmdLineOptions: CommandLineOptions, scanner: Scanner) { 57 | if (!cmdLineOptions.mockMode) { 58 | println("Manually adding transaction can only be done in mock mode.") 59 | return 60 | } 61 | 62 | if (daemon.started) { 63 | println("Stopping auto generation of transactions.") 64 | daemon.stop() 65 | } 66 | 67 | println("Manual transaction entry starting...") 68 | 69 | val txId = "tx_00009${generateRandomString(16)}" 70 | val accountId = "acc_00009RE1DzwEupfetgm84f" 71 | val type = "payport_faster_payments" 72 | val now = Instant.now() 73 | 74 | println("Enter in description (This is the redemption code!)") 75 | prompt() 76 | val description = scanner.nextLine() 77 | 78 | println("Enter the source account number") 79 | prompt() 80 | val source = UKAccountNumber(scanner.nextLine()) 81 | 82 | println("Enter the destination account number") 83 | prompt() 84 | val destination = UKAccountNumber(scanner.nextLine()) 85 | 86 | require(source != destination) { "Source and destination account cannot be the same." } 87 | 88 | println("Enter the amount") 89 | prompt() 90 | val amount = scanner.nextLine().toLong() 91 | 92 | val tx = NostroTransaction(txId, accountId, amount, GBP, type, description, now, source, destination) 93 | 94 | val monzo = daemon.openBankingApiClients.single { it is MockMonzo } as MockMonzo 95 | monzo.transactions.add(tx) 96 | // Grab the new transaction from the list. 97 | daemon.start() 98 | daemon.stop() 99 | } 100 | 101 | private fun repl(daemon: AbstractDaemon, cmdLineOptions: CommandLineOptions) { 102 | val scanner = Scanner(System.`in`) 103 | if (cmdLineOptions.autoMode) { 104 | println("\nAuto-mode set to TRUE. ") 105 | println("Polling for transactions from all registered APIs at FIVE second intervals...") 106 | } else { 107 | print("\nEnter a command ") 108 | prompt() 109 | } 110 | 111 | while (true) { 112 | val command = scanner.nextLine() 113 | when (command) { 114 | "start" -> { 115 | println("Polling for transactions from all registered APIs at FIVE second intervals...") 116 | daemon.start() 117 | } 118 | "stop" -> { 119 | daemon.stop() 120 | prompt() 121 | } 122 | "manual" -> { 123 | manual(daemon, cmdLineOptions, scanner) 124 | prompt() 125 | } 126 | "quit" -> { 127 | println("Bye bye!") 128 | exitProcess(0) 129 | } 130 | "exit" -> { 131 | println("Bye bye!") 132 | exitProcess(0) 133 | } 134 | "help" -> { 135 | println("start - start polling for transactions.") 136 | println("manual for manually adding a transaction - mock mode only") 137 | println("stop - stop polling for transactions.") 138 | println("exit/quit - Exit process.") 139 | prompt() 140 | } 141 | else -> { 142 | println("What you say?! Type \"help\" for help.") 143 | prompt() 144 | } 145 | } 146 | } 147 | } 148 | 149 | fun main(args: Array) { 150 | val cmdLineOptions = parseArguments(*args) 151 | val services = try { 152 | connectToCordaRpc(cmdLineOptions.rpcHostAndPort, cmdLineOptions.rpcUser, cmdLineOptions.rpcPass) 153 | } catch (e: RPCException) { 154 | throw RuntimeException("Could not connect to RPC client on host and post: ${cmdLineOptions.rpcHostAndPort}.") 155 | } 156 | welcome() 157 | if (cmdLineOptions.mockMode) { 158 | repl(MockDaemon(services, cmdLineOptions), cmdLineOptions) 159 | } else { 160 | repl(Daemon(services, cmdLineOptions), cmdLineOptions) 161 | } 162 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/services/UpdateObserverService.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.services 2 | 3 | import com.r3.corda.finance.cash.issuer.service.flows.* 4 | import com.r3.corda.lib.tokens.contracts.commands.RedeemTokenCommand 5 | import com.r3.corda.sdk.issuer.common.contracts.BankAccountContract 6 | import com.r3.corda.sdk.issuer.common.contracts.NostroTransactionContract 7 | import com.r3.corda.sdk.issuer.common.contracts.states.BankAccountState 8 | import com.r3.corda.sdk.issuer.common.contracts.states.NostroTransactionState 9 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransactionStatus 10 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransactionType 11 | import net.corda.core.contracts.CommandData 12 | import net.corda.core.node.AppServiceHub 13 | import net.corda.core.node.services.CordaService 14 | import net.corda.core.serialization.SingletonSerializeAsToken 15 | import net.corda.core.transactions.SignedTransaction 16 | import net.corda.core.utilities.loggerFor 17 | import rx.schedulers.Schedulers 18 | 19 | /** 20 | * This service listens for transaction updates and decides what to do based upon the data in the transaction. 21 | * It starts flows on new threads. 22 | */ 23 | @CordaService 24 | class UpdateObserverService(val services: AppServiceHub) : SingletonSerializeAsToken() { 25 | 26 | companion object { 27 | val logger = loggerFor() 28 | } 29 | 30 | init { 31 | // All this processing is punted off to a thread pool. 32 | // Always watch for new transactions and updates. Caution, if the node dies between an 33 | // event being emitted and the flow starting, then the flows will have to be started manually. 34 | // We can do this by pulling out all the nostro transaction states and running the process flow over 35 | // all UNMATCHED states. 36 | 37 | // Shouldn't really use this it will soon be deprecated. 38 | // TODO: For Corda core: We need to add commands to vault update events. 39 | services.validatedTransactions.updates.observeOn(Schedulers.io()).subscribe({ signedTransaction -> 40 | 41 | val isAddBankAccount = checkCommand(signedTransaction) 42 | val isUpdateBankAccount = checkCommand(signedTransaction) 43 | val isAddNostroTransaction = checkCommand(signedTransaction) 44 | val isMatchNostroTransaction = checkCommand(signedTransaction) 45 | val isRedeem = checkCommand(signedTransaction) 46 | 47 | logger.info("isAddBankAccount=$isAddBankAccount,isAddNostroTx=$isAddNostroTransaction,isMatchNostroTx=" + 48 | "$isMatchNostroTransaction,isUpdateBankAccount=$isUpdateBankAccount") 49 | 50 | when { 51 | isAddBankAccount -> { 52 | logger.info("New bank account added.") 53 | addBankAccountAction(signedTransaction) 54 | } 55 | isUpdateBankAccount -> { 56 | logger.info("Just updated a BankAccountState. Don't need to do anything.") 57 | } 58 | isAddNostroTransaction -> { 59 | logger.info("Processing a newly added nostro transaction...") 60 | addNostroTransactionAction(signedTransaction) 61 | } 62 | isMatchNostroTransaction -> { 63 | logger.info("A full or partial nostro transaction match has occurred...") 64 | matchNostroTransactionAction(signedTransaction) 65 | } 66 | isRedeem -> { 67 | logger.info("A redemption has occured.") 68 | processRedemption(signedTransaction) 69 | } 70 | else -> { 71 | logger.info("Transaction type not recognised.") 72 | println(signedTransaction.tx) 73 | } 74 | } 75 | }, { throwable -> logger.info(throwable.message) }) 76 | } 77 | 78 | inline fun checkCommand(stx: SignedTransaction): Boolean { 79 | return stx.tx.commands.singleOrNull { it.value is T } != null 80 | } 81 | 82 | private fun processRedemption(signedTransaction: SignedTransaction) { 83 | services.startFlow(ProcessRedemption(signedTransaction)) 84 | } 85 | 86 | /** 87 | * We are the issuer, all our bank accounts should be verified - they are ours! 88 | */ 89 | private fun addBankAccountAction(signedTransaction: SignedTransaction) { 90 | val bankAccountState = signedTransaction.tx.outputStates.single() as BankAccountState 91 | if (bankAccountState.owner == services.myInfo.legalIdentities.single()) { 92 | logger.info("The issuer has just added an account. It can be immediately verified.") 93 | val transaction = signedTransaction.tx 94 | val linearId = transaction.outRefsOfType().single().state.data.linearId 95 | services.startFlow(VerifyBankAccount(linearId)) 96 | // TODO We probably need to reprocess here as well... 97 | } else { 98 | logger.info("We've received an account from another node.") 99 | //TODO probably reprocess after verification... 100 | services.startFlow(ReProcessNostroTransaction(bankAccountState)) 101 | } 102 | } 103 | 104 | private fun addNostroTransactionAction(signedTransaction: SignedTransaction) { 105 | val transaction = signedTransaction.tx 106 | // Process all nostro transactions which have been added. 107 | // We can add more than one at a time. 108 | transaction.outRefsOfType().forEach { 109 | services.startFlow(ProcessNostroTransaction(it)) 110 | } 111 | } 112 | 113 | private fun matchNostroTransactionAction(signedTransaction: SignedTransaction) { 114 | val transaction = signedTransaction.tx 115 | // Get the nostro transaction and check if it has been matched. 116 | val nostroTransactionState = transaction.outputsOfType().single() 117 | // Check whether the conditions for issuance are satisfied. 118 | val isMatched = nostroTransactionState.status == NostroTransactionStatus.MATCHED 119 | val isIssuance = nostroTransactionState.type == NostroTransactionType.ISSUANCE 120 | val isRedemption = nostroTransactionState.type == NostroTransactionType.REDEMPTION 121 | logger.info("isMatched=$isMatched,isIssuance=$isIssuance,isRedemption=$isRedemption") 122 | // Start the issue cash flow. 123 | when { 124 | isMatched && isIssuance -> { 125 | logger.info("Issuing cash!") 126 | services.startTrackedFlow(IssueCash(signedTransaction)) 127 | } 128 | isMatched && isRedemption -> { 129 | logger.info("Processing redemption payment...") 130 | services.startTrackedFlow(ProcessRedemptionPayment(signedTransaction)) 131 | } 132 | } 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /common/workflows/src/main/kotlin/com/r3/corda/sdk/issuer/common/workflows/utilities/QueryUtils.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.sdk.issuer.common.workflows.utilities 2 | 3 | import com.r3.corda.sdk.issuer.common.contracts.schemas.BankAccountStateSchemaV1 4 | import com.r3.corda.sdk.issuer.common.contracts.schemas.NodeTransactionStateSchemaV1 5 | import com.r3.corda.sdk.issuer.common.contracts.schemas.NostroTransactionStateSchemaV1 6 | import com.r3.corda.sdk.issuer.common.contracts.states.BankAccountState 7 | import com.r3.corda.sdk.issuer.common.contracts.states.NodeTransactionState 8 | import com.r3.corda.sdk.issuer.common.contracts.states.NostroTransactionState 9 | import com.r3.corda.sdk.issuer.common.contracts.types.AccountNumber 10 | import com.r3.corda.sdk.issuer.common.contracts.types.NodeTransactionStatus 11 | import com.r3.corda.sdk.issuer.common.contracts.types.NodeTransactionType 12 | import net.corda.core.contracts.ContractState 13 | import net.corda.core.contracts.StateAndRef 14 | import net.corda.core.contracts.UniqueIdentifier 15 | import net.corda.core.node.ServiceHub 16 | import net.corda.core.node.services.Vault 17 | import net.corda.core.node.services.queryBy 18 | import net.corda.core.node.services.vault.Builder.equal 19 | import net.corda.core.node.services.vault.QueryCriteria 20 | import net.corda.core.node.services.vault.builder 21 | 22 | /** Bunch of helpers for querying the vault. */ 23 | 24 | fun getBankAccountStateByAccountNumber(accountNumber: AccountNumber, services: ServiceHub): StateAndRef? { 25 | val states = getState(services) { generalCriteria -> 26 | val additionalCriteria = QueryCriteria.VaultCustomQueryCriteria(BankAccountStateSchemaV1.PersistentBankAccountState::accountNumber.equal(accountNumber.digits)) 27 | generalCriteria.and(additionalCriteria) 28 | } 29 | return states.singleOrNull() 30 | } 31 | 32 | fun getBankAccountStateByLinearId(linearId: UniqueIdentifier, services: ServiceHub): StateAndRef? { 33 | val states = getState(services) { generalCriteria -> 34 | val additionalCriteria = QueryCriteria.VaultCustomQueryCriteria(BankAccountStateSchemaV1.PersistentBankAccountState::linearId.equal(linearId.id.toString())) 35 | generalCriteria.and(additionalCriteria) 36 | } 37 | return states.singleOrNull() 38 | } 39 | 40 | fun getNostroTransactionStateByTransactionId(transactionId: String, services: ServiceHub): StateAndRef? { 41 | val states = getState(services) { generalCriteria -> 42 | val additionalCriteria = QueryCriteria.VaultCustomQueryCriteria(NostroTransactionStateSchemaV1.PersistentNostroTransactionState::transactionId.equal(transactionId)) 43 | generalCriteria.and(additionalCriteria) 44 | } 45 | return states.singleOrNull() 46 | } 47 | 48 | fun getPendingRedemptionsByCounterparty(counterparty: String, services: ServiceHub): List>? { 49 | return getState(services) { generalCriteria -> 50 | val additionalCriteria = QueryCriteria.VaultCustomQueryCriteria(NodeTransactionStateSchemaV1.PersistentNodeTransactionState::status.equal(NodeTransactionStatus.PENDING.name)) 51 | val additionalCriteriaTwo = QueryCriteria.VaultCustomQueryCriteria(NodeTransactionStateSchemaV1.PersistentNodeTransactionState::type.equal(NodeTransactionType.REDEMPTION.name)) 52 | val additionalCriteriaThree = QueryCriteria.VaultCustomQueryCriteria(NodeTransactionStateSchemaV1.PersistentNodeTransactionState::counterparty.equal(counterparty)) 53 | generalCriteria.and(additionalCriteria.and(additionalCriteriaTwo.and(additionalCriteriaThree))) 54 | } 55 | } 56 | 57 | fun getPendingRedemptionByNotes(notes: String, services: ServiceHub): StateAndRef? { 58 | val states = getState(services) { generalCriteria -> 59 | val additionalCriteria = QueryCriteria.VaultCustomQueryCriteria(NodeTransactionStateSchemaV1.PersistentNodeTransactionState::notes.equal(notes)) 60 | generalCriteria.and(additionalCriteria) 61 | } 62 | return states.singleOrNull() 63 | } 64 | 65 | private inline fun getState( 66 | services: ServiceHub, 67 | block: (generalCriteria: QueryCriteria.VaultQueryCriteria) -> QueryCriteria 68 | ): List> { 69 | val query = builder { 70 | val generalCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED) 71 | block(generalCriteria) 72 | } 73 | val result = services.vaultService.queryBy(query) 74 | return result.states 75 | } 76 | 77 | fun getLatestNostroTransactionStatesGroupedByAccount(services: ServiceHub): Map { 78 | val query = builder { 79 | val generalCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED) 80 | val nostroTransactionCriteria = QueryCriteria.VaultCustomQueryCriteria( 81 | // Return transactions with the highest timestamp grouped by "accountId". 82 | NostroTransactionStateSchemaV1.PersistentNostroTransactionState::createdAt.max( 83 | groupByColumns = listOf(NostroTransactionStateSchemaV1.PersistentNostroTransactionState::accountId) 84 | ) 85 | ) 86 | generalCriteria.and(nostroTransactionCriteria) 87 | } 88 | 89 | return services.vaultService.queryBy(query).otherResults.chunked(2).associate { 90 | it[1] as String to it[0] as Long 91 | } 92 | } 93 | 94 | fun getNostroAccountBalances(services: ServiceHub): Map { 95 | val query = builder { 96 | val generalCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED) 97 | val nostroTransactionCriteria = QueryCriteria.VaultCustomQueryCriteria( 98 | // Return transactions with the highest timestamp grouped by "accountId". 99 | NostroTransactionStateSchemaV1.PersistentNostroTransactionState::amount.sum( 100 | groupByColumns = listOf(NostroTransactionStateSchemaV1.PersistentNostroTransactionState::accountId) 101 | ) 102 | ) 103 | generalCriteria.and(nostroTransactionCriteria) 104 | } 105 | 106 | return services.vaultService.queryBy(query).otherResults.chunked(2).associate { 107 | it[1] as String to it[0] as Long 108 | } 109 | } 110 | 111 | // TODO: Refactor this to use the above private function. 112 | fun getNostroTransactionsByAccountNumber(accountNumber: AccountNumber, services: ServiceHub): List> { 113 | val query = builder { 114 | val generalCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED) 115 | val sourceAccountNumber = QueryCriteria.VaultCustomQueryCriteria( 116 | NostroTransactionStateSchemaV1.PersistentNostroTransactionState::sourceAccountNumber.equal(accountNumber.digits) 117 | ) 118 | val destinationAccountNumber = QueryCriteria.VaultCustomQueryCriteria( 119 | NostroTransactionStateSchemaV1.PersistentNostroTransactionState::destinationAccountNumber.equal(accountNumber.digits) 120 | ) 121 | generalCriteria.and(sourceAccountNumber.or(destinationAccountNumber)) 122 | } 123 | 124 | val result = services.vaultService.queryBy(query) 125 | return result.states 126 | } 127 | 128 | -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/clients/Monzo.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon.clients 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import com.r3.corda.finance.cash.issuer.daemon.* 5 | import com.r3.corda.lib.tokens.contracts.types.TokenType 6 | import com.r3.corda.sdk.issuer.common.contracts.types.* 7 | import net.corda.core.contracts.Amount 8 | import retrofit2.http.GET 9 | import retrofit2.http.Query 10 | import rx.Observable 11 | import rx.schedulers.Schedulers 12 | import java.time.Instant 13 | 14 | interface Monzo { 15 | @GET("/accounts") 16 | fun accounts(): Observable 17 | 18 | @GET("/balance") 19 | fun balance(@Query("account_id") accountId: String): Observable 20 | 21 | @GET("/transactions") 22 | fun transactions( 23 | @Query("account_id") accountId: String, 24 | @Query("limit") limit: Int?, 25 | @Query("since") since: String?, 26 | @Query("before") before: String? 27 | ): Observable 28 | 29 | @GET("/transactions") 30 | fun transaction(@Query("transaction_id") transactionId: String): Observable 31 | } 32 | 33 | @Suppress("UNUSED") 34 | class MonzoClient(configName: String) : OpenBankingApiClient(configName) { 35 | override val api: Monzo = OpenBankingApiFactory(Monzo::class.java, apiConfig, logger).build() 36 | 37 | private val _accounts: Map by lazy { 38 | accounts().map { it.accountId to it }.toMap() 39 | } 40 | 41 | override val accounts: List = _accounts.values.toList() 42 | 43 | private fun accounts(): List { 44 | // TODO: Filter accounts based upon those whitelisted in the config file. 45 | val accounts = wrapWithTry { api.accounts().getOrThrow().accounts.filter { !it.closed } } 46 | // Monzo doesn't provide the currency of its accounts. 47 | // For now they are all GBP but that might change... 48 | val currencies = wrapWithTry { accounts.map { balance(it.id).token } } 49 | require(currencies.size == accounts.size) { "Couldn't obtain currency information for all accounts." } 50 | // Join the currencies to the bank account data. 51 | val accountsWithCurrencies = accounts.zip(currencies) 52 | return accountsWithCurrencies.map { (account, currency) -> 53 | // TODO: Add the bank account type from the config file. 54 | account.toBankAccount(currency) 55 | } 56 | } 57 | 58 | override fun balance(accountId: BankAccountId?): Amount { 59 | if (accountId == null) throw IllegalArgumentException("AccountId is required for Monzo::balance.") 60 | val balance = wrapWithTry { api.balance(accountId).getOrThrow() } 61 | return Amount(balance.balance, balance.currency) 62 | } 63 | 64 | override fun transactionsFeed(): Observable> { 65 | val transactions = accounts.map { account -> 66 | val accountId = account.accountId 67 | // For monzo, if we provide the last timestamp the API always returns the last transaction. So 68 | // here the timestamp in incremented by 1 millisecond. 69 | // TODO: Remove this hack and use the transaction ID instead. 70 | val lastTransactionTimestamp = lastTransactions[accountId]?.plusMillis(1L) 71 | wrapWithTry { 72 | api.transactions(account.accountId, null, lastTransactionTimestamp?.toString(), null).observeOn(Schedulers.io()).map { 73 | it.transactions.map { transaction -> transaction.toNostroTransaction(ourAccount = account.accountNumber) } 74 | } 75 | } 76 | } 77 | 78 | // Merge and then flatten the feeds for all accounts. 79 | return Observable.merge(transactions) 80 | } 81 | } 82 | 83 | /** DATA TYPES */ 84 | 85 | data class MonzoAccounts(val accounts: List) 86 | 87 | @JsonIgnoreProperties(ignoreUnknown = true) 88 | data class MonzoAccount( 89 | val id: String, 90 | val closed: Boolean, 91 | val description: String, 92 | val account_number: String?, 93 | val created: Instant, 94 | val sort_code: String? 95 | ) 96 | 97 | fun MonzoAccount.toBankAccount(currency: TokenType): BankAccount { 98 | val accountNumber = if (account_number == null || sort_code == null) NoAccountNumber() else UKAccountNumber(sort_code, account_number) 99 | return BankAccount(id, description, accountNumber, currency) 100 | } 101 | 102 | @JsonIgnoreProperties(ignoreUnknown = true) 103 | data class MonzoBalance(val balance: Long, val currency: TokenType) 104 | 105 | @JsonIgnoreProperties(ignoreUnknown = true) 106 | data class MonzoCounterparty( 107 | val sort_code: String?, 108 | val account_number: String?, 109 | val name: String?, 110 | val user_id: String? 111 | ) 112 | 113 | @JsonIgnoreProperties(ignoreUnknown = true) 114 | data class MonzoTransaction( 115 | val account_id: String, 116 | val amount: Long, 117 | val created: Instant, 118 | val currency: TokenType, 119 | val description: String, 120 | val id: String, 121 | val notes: String, 122 | val scheme: String, 123 | val settled: Instant, 124 | val updated: Instant, 125 | val counterparty: MonzoCounterparty? 126 | ) 127 | 128 | @JsonIgnoreProperties(ignoreUnknown = true) 129 | data class MonzoTransactions(val transactions: List) 130 | 131 | // TODO: Refactor this. 132 | fun MonzoTransaction.toNostroTransaction(ourAccount: AccountNumber): NostroTransaction { 133 | // Either we have an account number, or we don't. 134 | val theirAccount = counterparty?.let { 135 | if (it.sort_code == null || it.account_number == null) NoAccountNumber() 136 | else UKAccountNumber(it.sort_code, it.account_number) 137 | } ?: NoAccountNumber() 138 | 139 | // Monzo uses positive amounts for deposits and negative amounts for 140 | // withdrawals. Source and destination accounts are set accordingly. 141 | return if (amount > 0L) { 142 | NostroTransaction(id, account_id, amount, currency, scheme, description, created, theirAccount, ourAccount) 143 | } else { 144 | NostroTransaction(id, account_id, amount, currency, scheme, description, created, ourAccount, theirAccount) 145 | } 146 | } 147 | 148 | /** 149 | override var lastUpdates = mapOf() 150 | override val accounts: List get() = _accounts.values.toList() 151 | 152 | 153 | 154 | override fun transactions(accountId: String?, limit: Int?, since: String?, before: String?): List { 155 | if (accountId == null) throw IllegalArgumentException("AccountId is required for Monzo::transactions.") 156 | return monzo.transactions(accountId, limit, since, before).getOrThrow().transactions.map { 157 | require(accountId == it.account_id) { throw IllegalStateException("Account IDs should match.") } 158 | // If HTTP request was successful, then 'accountId' should be a valid key in the _accounts map. 159 | it.toNostroTransaction(_accounts[accountId]!!.accountNumber) 160 | } 161 | } 162 | 163 | override fun transactionsFeed(): Observable> { 164 | // TODO: Filter out any closed accounts from 'since'. 165 | val transactions = accounts.map { (accountId) -> 166 | // There may or may not be a last update. If there is not, the 167 | // lookup returns null and all transactions are polled for. 168 | val lastUpdate = lastUpdates[accountId] 169 | monzo.transactions(accountId, null, lastUpdate.toString(), null).observeOn(Schedulers.io()).map { 170 | // _accounts will always be populated before this line executes. 171 | it.transactions.map { it.toNostroTransaction(ourAccount = _accounts[accountId]!!.accountNumber) } 172 | } 173 | } 174 | 175 | // Merge and then flatten the feeds for all accounts. 176 | return Observable.merge(transactions) 177 | } 178 | */ -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/AbstractDaemon.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.daemon 2 | 3 | import com.r3.corda.finance.cash.issuer.service.flows.AddNostroTransactions 4 | import com.r3.corda.finance.cash.issuer.service.flows.GetLastUpdatesByAccountId 5 | import com.r3.corda.finance.cash.issuer.service.flows.GetNostroAccountBalances 6 | import com.r3.corda.sdk.issuer.common.contracts.types.UKAccountNumber 7 | import com.r3.corda.sdk.issuer.common.workflows.flows.AddBankAccount 8 | import net.corda.core.CordaRuntimeException 9 | import net.corda.core.contracts.Amount 10 | import net.corda.core.messaging.CordaRPCOps 11 | import net.corda.core.utilities.getOrThrow 12 | import net.corda.core.utilities.loggerFor 13 | import rx.Observable 14 | import rx.Subscription 15 | import rx.schedulers.Schedulers 16 | import java.util.concurrent.TimeUnit 17 | 18 | abstract class AbstractDaemon(val services: CordaRPCOps, val cmdLineOptions: CommandLineOptions) { 19 | 20 | var started: Boolean = false 21 | 22 | protected val autoStart: Boolean = cmdLineOptions.autoMode 23 | 24 | val openBankingApiClients: List by lazy { 25 | try { 26 | scanForOpenBankingApiClients() 27 | } catch (e: RuntimeException) { 28 | throw e 29 | } 30 | } 31 | 32 | // Maps bank account IDs to the open banking api client which provides access. 33 | protected val accountsToBank: Map by lazy { 34 | mutableMapOf().apply { 35 | openBankingApiClients.flatMap { client -> 36 | client.accounts.map { account -> 37 | put(account.accountId, client) 38 | } 39 | } 40 | } 41 | } 42 | 43 | private var subscriber: Subscription? = null 44 | private val transactionsFeed = Observable 45 | .interval(5, TimeUnit.SECONDS, Schedulers.io()) 46 | .startWith(1) 47 | .flatMap { Observable.merge(openBankingApiClients.map(OpenBankingApi::transactionsFeed)) } 48 | .doOnError { println(it.message) } 49 | 50 | init { 51 | // 1. Add all bank accounts for all API clients. 52 | addAllBankAccounts() 53 | // 2. Query nostro balances on Corda and via the APIs. 54 | val balances = getAllBalances() 55 | printBalances(balances) 56 | // 3. Get the last recorded transactions on the node for each account. It might be the case that the node has 57 | // the last transaction for an account but is missing one inbetween. In this case, a reconciliation of 58 | // transaction IDs must be performed. 59 | getLastRecordedNostroTransactions() 60 | // 4. Start the polling if auto-mode is enabled. 61 | if (cmdLineOptions.autoMode) { 62 | start() 63 | } 64 | } 65 | 66 | open fun start() { 67 | println("Starting...") 68 | subscriber = transactionsFeed.subscribe { 69 | if (it.isNotEmpty()) { 70 | println("Adding ${it.size} nostro transactions to the issuer node.") 71 | val addedTransactions = services.startFlowDynamic(AddNostroTransactions::class.java, it).returnValue.getOrThrow() 72 | addedTransactions.forEach { accountId, timestamp -> 73 | // Update the last stored transaction timestamp. 74 | accountsToBank[accountId]?.updateLastTransactionTimestamps(accountId, timestamp.toEpochMilli()) 75 | val bankApiName = accountsToBank[accountId]!!::class.java.simpleName 76 | println("Updated $accountId for $bankApiName with the last seen timestamp $timestamp.") 77 | } 78 | } else { 79 | logger.info("Grabbed no transactions.") 80 | } 81 | } 82 | started = true 83 | } 84 | 85 | open fun stop() { 86 | subscriber?.unsubscribe() 87 | started = false 88 | } 89 | 90 | companion object { 91 | val logger = loggerFor() 92 | } 93 | 94 | abstract fun scanForOpenBankingApiClients(): List 95 | 96 | private fun addAllBankAccounts() { 97 | val allAccounts = openBankingApiClients.flatMap { client -> client.accounts } 98 | println("\nAttempting to add bank account data to the issuer node...\n") 99 | allAccounts.forEach { 100 | val accountNumber = it.accountNumber as UKAccountNumber 101 | try { 102 | services.startFlowDynamic(AddBankAccount::class.java, it, services.nodeInfo().legalIdentities.first()).returnValue.getOrThrow() 103 | println("\t* Added bank account with $accountNumber.") 104 | } catch (e: CordaRuntimeException) { 105 | println("\t* Bank account with $accountNumber has already been added.") 106 | } 107 | } 108 | } 109 | 110 | private fun getAllBalances(): List { 111 | val nodeNostroBalances = services.startFlowDynamic(GetNostroAccountBalances::class.java).returnValue.getOrThrow() 112 | val bankNostroBalances = openBankingApiClients.flatMap { client -> 113 | client.accounts.map { account -> 114 | Pair(account.accountId, client.balance(account.accountId)) 115 | } 116 | }.toMap() 117 | return bankNostroBalances.keys.map { 118 | val nodeBalance = nodeNostroBalances.getOrDefault(it, 0L) 119 | val bankBalance = bankNostroBalances[it]!! 120 | Balance(it, Amount(nodeBalance, bankBalance.token), bankBalance) 121 | } 122 | } 123 | 124 | private fun printBalances(balances: List) { 125 | println("\nChecking bank balances for differences...\n") 126 | println("\tAccount ID\t\tNode Balance\t\tBank Balance\t\tDifference") 127 | println("\t----------\t\t------------\t\t------------\t\t----------") 128 | var totalDifference = 0L 129 | balances.forEach { (accountId, node, bank) -> 130 | val id = accountId.truncate() 131 | val difference = node.quantity - bank.quantity 132 | totalDifference += difference 133 | println("\t$id\t\t$node\t\t\t$bank\t\t\t$difference") 134 | } 135 | println() 136 | println("\t\t\t\t\t\t\tTotal difference: \t\t$totalDifference") 137 | } 138 | 139 | private fun getLastRecordedNostroTransactions() { 140 | // The daemon doesn't persist any data across restarts, so it must query the node to ascertain 141 | // the timestamps of the last recorded nostro transaction information. This is so the daemon 142 | // doesn't miss any transactions when it starts up after some downtime. 143 | if (cmdLineOptions.startFrom != null) { 144 | println("\nWill use \"start-from\" time if it is greater than the node's last timestamps...\n") 145 | } else { 146 | println("\nQuerying Issuer node for last recorded transactions per nostro account...\n") 147 | } 148 | val lastUpdatesByAccountId = services.startFlowDynamic(GetLastUpdatesByAccountId::class.java).returnValue.getOrThrow() 149 | if (lastUpdatesByAccountId.isNotEmpty()) { 150 | println("\tAccount ID\t\tTimestamp") 151 | println("\t----------\t\t---------") 152 | lastUpdatesByAccountId.forEach { (accountId, timestamp) -> 153 | // Use the start from timestamp if it was specified in the options and greater than the timestamps 154 | // of the last stored transactions in the node. 155 | val lastUpdate = cmdLineOptions.startFrom?.let { 156 | if (it.toEpochMilli() > timestamp) it.toEpochMilli() else timestamp 157 | } ?: timestamp 158 | println("\t${accountId.truncate()}\t\t$lastUpdate") 159 | accountsToBank[accountId]?.updateLastTransactionTimestamps(accountId, lastUpdate) 160 | ?: throw IllegalStateException("Issuer node has a last recorded transaction for $accountId. " + 161 | "However, there is no corresponding bank API client!") 162 | } 163 | } else { 164 | println("\t* The node has no nostro transactions stored.") 165 | if (cmdLineOptions.startFrom != null) { 166 | println("\t* ... but we will use the \"start-from\" timestamp.") 167 | accountsToBank.forEach { t, _ -> 168 | accountsToBank[t]!!.updateLastTransactionTimestamps(t, cmdLineOptions.startFrom.toEpochMilli()) 169 | } 170 | } 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/clients/Starling.kt: -------------------------------------------------------------------------------- 1 | //package com.r3.corda.finance.cash.issuer.daemon.clients 2 | // 3 | //import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | //import com.r3.corda.finance.cash.issuer.daemon.BankAccountId 5 | //import com.r3.corda.finance.cash.issuer.daemon.OpenBankingApiClient 6 | //import com.r3.corda.finance.cash.issuer.daemon.OpenBankingApiFactory 7 | //import com.r3.corda.finance.cash.issuer.daemon.getOrThrow 8 | //import com.r3.corda.lib.tokens.money.FiatCurrency 9 | //import com.r3.corda.sdk.issuer.common.contracts.types.* 10 | //import net.corda.core.contracts.Amount 11 | //import retrofit2.http.GET 12 | //import retrofit2.http.Path 13 | //import retrofit2.http.Query 14 | //import rx.Observable 15 | //import rx.schedulers.Schedulers 16 | //import java.math.BigDecimal 17 | //import java.time.Instant 18 | //import java.time.LocalDateTime 19 | //import java.time.ZoneOffset 20 | //import java.time.format.DateTimeFormatter 21 | // 22 | //interface Starling { 23 | // @GET("accounts") 24 | // fun accounts(): Observable 25 | // 26 | // @GET("accounts/balance") 27 | // fun balance(): Observable 28 | // 29 | // @GET("transactions") 30 | // fun transactions(@Query("from") from: String?, @Query("to") to: String?): Observable 31 | // 32 | // @GET("transactions/{transactionId}") 33 | // fun transaction(@Path("id") transactionId: String): Observable 34 | // 35 | // @GET("contacts/{contactId}/accounts/{accountId}") 36 | // fun contactAccount(@Path("contactId") contactId: String, @Path("accountId") accountId: String): Observable 37 | // 38 | // @GET("transactions/fps/out/{transactionId}") 39 | // fun fpsOut(@Path("transactionId") transactionId: String): Observable 40 | // 41 | // @GET("transactions/fps/in/{transactionId}") 42 | // fun fpsIn(@Path("transactionId") contactId: String): Observable 43 | //} 44 | // 45 | //@Suppress("UNUSED") 46 | //class StarlingClient(configName: String) : OpenBankingApiClient(configName) { 47 | // override val api = OpenBankingApiFactory(Starling::class.java, apiConfig, logger) 48 | // .withAdditionalHeaders(mapOf("User-Agent" to "R3 Issuer Ltd")) 49 | // .build() 50 | // 51 | // private val _accounts: Map by lazy { 52 | // accounts().map { it.accountId to it }.toMap() 53 | // } 54 | // 55 | // override val accounts: List = _accounts.values.toList() 56 | // 57 | // fun accounts(): List { 58 | // val account = api.accounts().getOrThrow() 59 | // return listOf(account.toBankAccount()) 60 | // } 61 | // 62 | // override fun balance(accountId: BankAccountId?): Amount { 63 | // val balance = api.balance().getOrThrow() 64 | // // Amounts require cent/pence values. 65 | // return Amount((balance.amount * 100.toBigDecimal()).longValueExact(), balance.currency) 66 | // } 67 | // 68 | // private fun getDateStringFromInstant(instant: Instant): String { 69 | // val date = LocalDateTime.ofInstant(instant, ZoneOffset.UTC) 70 | // val formattedDate = DateTimeFormatter.ofPattern("yyyy-MM-dd") 71 | // return formattedDate.format(date) 72 | // } 73 | // 74 | // override fun transactionsFeed(): Observable> { 75 | // // There's only one account with Starling for the time being. 76 | // val lastTransaction = lastTransactions.values.singleOrNull() 77 | // val from = if (lastTransaction != null) getDateStringFromInstant(lastTransaction) else null 78 | // return api.transactions(from, null).observeOn(Schedulers.io()).map { 79 | // it._embedded.transactions.map { 80 | // toNostroTransaction(it) 81 | // }.filter { 82 | // // Starling only allows us to specify from which DAY we wish 83 | // // to query for transactions. As we have the last transaction 84 | // // timestamp we can filter out all the transactions we've 85 | // // already seen. 86 | // it.createdAt > (lastTransaction ?: Instant.EPOCH) 87 | // } 88 | // } 89 | // } 90 | // 91 | // private fun toNostroTransaction(tx: StarlingTransaction): NostroTransaction { 92 | // // We must multiply the amount by 100 as Starling uses decimals. 93 | // val amount = tx.amount.toLong() * 100 94 | // val account = accounts.single() 95 | // 96 | // // Function to get the account sort code and number as Starling doesn't provide it in the transaction data. 97 | // // It might be the case that no account number is available for some transactions. 98 | // fun getContactAccount(block: () -> StarlingFpsTransaction): AccountNumber { 99 | // val details = block() 100 | // return if (details.sendingContactId == null || details.sendingContactAccountId == null) { 101 | // NoAccountNumber() 102 | // } else { 103 | // val contactAccount = api.contactAccount(details.sendingContactId, details.sendingContactAccountId).getOrThrow() 104 | // UKAccountNumber(contactAccount.sortCode, contactAccount.accountNumber) 105 | // } 106 | // } 107 | // 108 | // val (source, destination) = when (tx.direction) { 109 | // "INBOUND" -> { 110 | // // Get the account info. 111 | // val contactAccount = getContactAccount { api.fpsIn(tx.id).getOrThrow() } 112 | // Pair(contactAccount, account.accountNumber) 113 | // } 114 | // "OUTBOUND" -> { 115 | // val contactAccount = getContactAccount { api.fpsOut(tx.id).getOrThrow() } 116 | // Pair(account.accountNumber, contactAccount) 117 | // } 118 | // else -> throw IllegalStateException("This shouldn't happen.") 119 | // } 120 | // 121 | // return NostroTransaction(tx.id, account.accountId, amount, tx.currency, tx.source, tx.narrative, tx.created, source, destination) 122 | // } 123 | //} 124 | // 125 | ///** 126 | // * -------------------------------- 127 | // *** STARLING DATA MODELS BELOW *** 128 | // * -------------------------------- 129 | // * 130 | // * WARNING! THE DATA MODELS ARE NOT DOCUMENTED AND ARE NOT BACKWARDS COMPATIBLE SO THEY ARE EXPECTED TO BREAK SOMETIMES. 131 | // * IF THEY DO THEN IT's LIKELY THAT SOMETHING UNDER HERE WILL NEED CHANGING. MOAN AT THEM ON SLACK IF YOU 132 | // * NEED HELP: https://starlingdevs.slack.com/. 133 | // */ 134 | // 135 | ///** 136 | // * The model Starling returns for an account. Lots of information but most other APIs are not as generous, so we 137 | // * discard most of this in order to achieve a common set of data for all APIs. 138 | // */ 139 | //@JsonIgnoreProperties(ignoreUnknown = true) 140 | //data class StarlingAccount( 141 | // val bic: String, // Not yet used. 142 | // val createdAt: Instant, 143 | // val currency: FiatCurrency, 144 | // val iban: String, // Not yet used. 145 | // val id: String, 146 | // val name: String, 147 | // val number: String, // Not yet used. 148 | // val accountNumber: String, 149 | // val sortCode: String 150 | //) 151 | // 152 | //fun StarlingAccount.toBankAccount(): BankAccount { 153 | // return BankAccount(id, name, UKAccountNumber(sortCode, accountNumber), currency) 154 | //} 155 | // 156 | ///** 157 | // * Again, lots of information but we discard most of it apart from amount and currency. 158 | // */ 159 | //@JsonIgnoreProperties(ignoreUnknown = true) 160 | //data class StarlingBalance( 161 | // val amount: BigDecimal, 162 | // val clearedBalance: BigDecimal, 163 | // val currency: FiatCurrency, 164 | // val effectiveBalance: BigDecimal, 165 | // val pendingTransactions: BigDecimal 166 | //) 167 | // 168 | ///** 169 | // * Starling don't include contact information in their transaction models. You have to look it up separately. This is 170 | // * the model representing a counterparty/contact account. 171 | // */ 172 | //@JsonIgnoreProperties(ignoreUnknown = true) 173 | //data class StarlingContactAccount( 174 | // val id: String, 175 | // val name: String, 176 | // val accountNumber: String, 177 | // val sortCode: String 178 | //) 179 | // 180 | ///** Starling embed their transaction data inside a nested mess of Json. */ 181 | //@JsonIgnoreProperties(ignoreUnknown = true) 182 | //data class StarlingTransactionBase(val _links: Any, val _embedded: StarlingTransactions) 183 | // 184 | //data class StarlingTransactions(val transactions: List) 185 | // 186 | ///** 187 | // * The actual transaction model. Not useful as it doesn't tell us where the bloody money came from. To get that we need 188 | // * to query the FPS endpoints. 189 | // */ 190 | //@JsonIgnoreProperties(ignoreUnknown = true) 191 | //data class StarlingTransaction( 192 | // val id: String, 193 | // val currency: FiatCurrency, 194 | // val amount: BigDecimal, 195 | // val direction: String, 196 | // val created: Instant, 197 | // val narrative: String, 198 | // val source: String 199 | //) 200 | // 201 | ///** 202 | // * For getting the contact ID for counterparties/contacts. 203 | // */ 204 | //@JsonIgnoreProperties(ignoreUnknown = true) 205 | //data class StarlingFpsTransaction(val sendingContactId: String?, val sendingContactAccountId: String?) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) 2 | 3 | # Corda Cash Issuer 4 | 5 | ** WARNING: This code is not intended for production but serves as a good example 6 | of how to integrate the Tokens SDK with real bank accounts for projects that 7 | require "cash on ledger". 8 | 9 | This code works with Monzo and Starling bank accounts. For those that do not have 10 | Monzo or Starling bank accounts you can use the `MockMonzo` client to generate 11 | fake transaction data. 12 | 13 | This repo contains an example of how to implement a cash issuer/cash tokenizer as 14 | described in the [accompanying design document](design/design.md). 15 | 16 | The code is more instructive than anything else. If you do want to use this code 17 | and get stuck then e-mail `roger.willis@r3.com`. 18 | 19 | The repo is split into a number of modules: 20 | 21 | 1. **client** - code which should be run by participants in a cash business 22 | network. 23 | 2. **common** - code which is shared by the cash issuer and users of cash states 24 | issued by the cash issuer. E.g. abstract flow initiator definitions 25 | and types. 26 | 3. **daemon** - a process which polls bank APIs for new transactions, 27 | transforms the data into a common format and sends it to the issuer 28 | node for processing 29 | 4. **service** - the cash issuer node. Contains folows for processing data 30 | provided by the daemon as well as flows for issuing and redeeming cash 31 | states 32 | 5. **service-ui** - a basic JavaFx app that provides a view on the cash issuer 33 | node. 34 | 35 | ## Requirements 36 | 37 | 1. Three bank accounts. One for the Issuer, one for PartyA and one for 38 | partyB. 39 | 2. The bank holding the Issuer's bank account needs to offer a 40 | public API which allows clients to get account information, balance 41 | information and transaction information in real time. 42 | 3. You will need a working API key for the bank's API. If you don't have this 43 | then you must start ed `daemon` in `mock-mode`. 44 | 45 | ## How to use this code 46 | 47 | Add your own bank API clients: 48 | 49 | 1. The daemon is extensible. Support for any bank HTTP API can be added by 50 | sub-classing `OpenBankingApiClient` and providing an interface definition 51 | for the API that can be used by Retrofit. Look at the [Monzo](cash-issuer/daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/clients/Monzo.kt) 52 | and [Starling](cash-issuer/daemon/src/main/kotlin/com/r3/corda/finance/cash/issuer/daemon/clients/Starling.kt) 53 | implementations as examples. 54 | 2. You will notice that the Starling and Monzo implementations differ a 55 | little, this is due to the different API interfaces offered. 56 | 3. You will need to add a config file to the resources folder, For, 57 | example to add support for "Foo Bank", add a "FooBankClient" class that 58 | sub-classes `OpenBankingApiClient` and add a `foobank.conf` config file 59 | (omit 'Client' from the config file name). Config files contain three 60 | key/value pairs: 61 | 62 | ``` 63 | apiBaseUrl="[URL HERE]" 64 | apiVersion="" 65 | apiAccessToken="[ADD ACCESS TOKEN HERE]" 66 | ``` 67 | 68 | If you have your own Starling or Monzo account you'd like to use, then 69 | no additional work is required. 70 | 71 | Starling and Monzo sometimes change their API, if they do then this code will break. 72 | 73 | Using the Mock Monzo bank account: 74 | 75 | * Start the Daemon with the option `-mock-mode`. 76 | * This way you can experiment with the functionality of the cash issuer 77 | without having to use a real bank account. 78 | * The MockMonzo bank will create realistic-ish transactions at random 79 | intervals. 80 | * The transactions created by the MockMonzo bank come from five pre-defined bank 81 | account numbers, which are: 82 | 83 | Account number: 13371337, sort code: 442200 84 | Account number: 12345678, sort code: 873456 85 | Account number: 73510753, sort code: 059015 86 | Account number: 34782115, sort code: 022346 87 | Account number: 90143578, sort code: 040040 88 | 89 | When following the instructions below, add one of these banks accounts to 90 | NodeA. 91 | 92 | ## Getting started 93 | 94 | Start the corda nodes and issuer daemon: 95 | 96 | 1. Assuming all the API clients you need are implemented and a working 97 | config is present in the `resources` directory, then you are good to 98 | go! The repo comes with a config file for Monzo and Starling. You just 99 | need to add your API key which has permission to view accounts, view transactions 100 | and check balances. If you don't have a Starling or Monzo account then use 101 | the daemon in `--mock-mode` 102 | 2. From the root of this repo run `./gradlew clean deployNodes`. The 103 | deployNodes script will build `Notary` `Issuer`, `PartyA` and `PartyB` 104 | nodes. 105 | 3. Navigate to the node directories `cd build/nodes`. 106 | 4. Run the nodes `./runnodes`. 107 | 5. Wait for all the nodes to start up. 108 | 6. Build the issuer daemon jar with `./gradlew :daemon:jar` the jar will be 109 | output to `daemon/build/libs/daemon-0.1.jar` 110 | 7. Start the issuer daemon (See "Starting the issuer daemon" below). 111 | 8. Start the issuer `service-ui` via IntelliJ. Run via the Green Arrow next to the 112 | `main` function in `com/r3/corda/finance/cash/issuer/Main.kt`. The app 113 | is defaulted to connect to the Issuer node on port 10006. This can be 114 | changed in `Main.kt` if required. 115 | 116 | At this point all the required processes are up and running. Next, you can 117 | perform a demo run of an issuance: 118 | 119 | 1. From `PartyA` add a new bank account via the node shell: `flow start AddBankAccount bankAccount: { accountId: 12345, accountName: Rogers Account, accountNumber: { sortCode: XXXXXX, accountNumber: XXXXXXXX, type: uk }, currency: { tokenIdentifier: GBP} }, verifier: Issuer` 120 | replacing `XXXXXX` and `YYYYYYYY` with your sort code and account number. 121 | This is the bank account that you will make a payment from, to the issuer's 122 | account. 123 | 2. Next, we need to send the bank account you have just added, to the 124 | issuer node. First, we need to know the linear ID of the bank account 125 | state which has just been added: `run vaultQuery contractStateType: com.r3.corda.finance.cash.issuer.common.states.BankAccountState`. 126 | You should see the linear ID in the data structure which is output to the shell. 127 | Send the account to the issuer with `start Send issuer: Issuer, linearId: LINEAR_ID`. 128 | 3. You should see the issuer's UI update with new bank account information. 129 | Note: the issuer's account should already be added. 130 | 4. From the issuer daemon shell type `start`. The daemon should start 131 | polling for new transactions. 132 | 5. Make a payment (for a small amount!!) from `PartyA`s bank account to 133 | the `Issuer`s bank account. Soon after the payment has been made, the 134 | daemon should pick up the transaction information and the Issuer UI 135 | should update in the "nostro transactions" pane and the "node transactions" 136 | pane. 137 | 6. Assuming the correct details for the bank account used by PartyA were 138 | added and successfully sent to the issuer, then the issuance record in 139 | the node transaction tab should be marked as complete. 140 | 7. Run `run vaultQuery contractStateType: net.corda.finance.contracts.asset.Cash$State` 141 | from PartyA to inspect the amount of cash issued. It should be for 142 | the same amount of the payment sent to the issuer's account. 143 | 144 | Next things to do are to transfer the cash from A to B, then send the cash from B 145 | to the Issuer node for redemption. 146 | 147 | ## Starting the issuer daemon 148 | 149 | 1. Start the daemon either via the main method in `Main.kt` from IntelliJ or 150 | from the JAR created above with `java -jar daemon-0.1.jar`. The daemon should 151 | start and present you with a simple command line interface. The daemon 152 | requires a number of command line parameters. The main ones to know are: 153 | ``` 154 | host-port the host name and port of the code node to connect to 155 | rpcUser the RPC username for the corda node 156 | rpcPass the RPC password for the corda node 157 | ``` 158 | All three of the above arguments are required. As such, note that if 159 | no corda node is available to connect to on the specified hostname and 160 | port, then the daemon will not start successfully. 161 | 162 | There are three other parameters to note: 163 | ``` 164 | mock-mode - use this if you don't want to use a real bank account. 165 | auto-mode - Use this to start polling the bank accounts for new transactions as soon as the daemon startes. 166 | start-from - Use this flag to ignore all the past transactions in the bank account. This is useful if you want to perform a demo and need to re-use the same account multiple times but give the impression that the demo is from "scratch". 167 | ``` 168 | 2. When the daemon starts up, it requests bank account information for all 169 | the supplied API interfaces. It then uploads the account information to 170 | the issuer node, via RPC. Note: if the daemon is connected to a corda 171 | node which does not have the required flows, then the daemon and the corda 172 | node in question will throw an exception. Make sure that the daemon 173 | only connects to issuers nodes as defined in the `service` module! Once 174 | account information has been added. It requests the balance information 175 | for each of the added accounts. Lastly, it presents a basic shell. 176 | Current commands are: 177 | ``` 178 | start starts polling the apis for new transactions 179 | with a 5 second interval 180 | stop stop polling 181 | help show help 182 | quit exit the daemon 183 | ``` 184 | 185 | -------------------------------------------------------------------------------- /service/src/main/kotlin/com/r3/corda/finance/cash/issuer/service/flows/ProcessNostroTransaction.kt: -------------------------------------------------------------------------------- 1 | package com.r3.corda.finance.cash.issuer.service.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import com.r3.corda.sdk.issuer.common.contracts.NodeTransactionContract 5 | import com.r3.corda.sdk.issuer.common.contracts.NostroTransactionContract 6 | import com.r3.corda.sdk.issuer.common.contracts.states.BankAccountState 7 | import com.r3.corda.sdk.issuer.common.contracts.states.NodeTransactionState 8 | import com.r3.corda.sdk.issuer.common.contracts.states.NostroTransactionState 9 | import com.r3.corda.sdk.issuer.common.contracts.types.NoAccountNumber 10 | import com.r3.corda.sdk.issuer.common.contracts.types.NodeTransactionType 11 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransactionStatus 12 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransactionType 13 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getBankAccountStateByAccountNumber 14 | import com.r3.corda.sdk.issuer.common.workflows.utilities.getPendingRedemptionByNotes 15 | import net.corda.core.contracts.AmountTransfer 16 | import net.corda.core.contracts.Command 17 | import net.corda.core.contracts.StateAndRef 18 | import net.corda.core.flows.* 19 | import net.corda.core.transactions.SignedTransaction 20 | import net.corda.core.transactions.TransactionBuilder 21 | import java.time.Instant 22 | 23 | // TODO: What about segregation of duties? Typically, something like an issuance event would require multiple sign-offs. 24 | // Perhaps this can be done for transactions over a certain value. 25 | 26 | /** 27 | * This takes a nostro transaction states and attempts to match it to two bank account state. 28 | * If we can get a match then, in theory, we know what to do in respect of this transaction hitting the nostro account. 29 | * If we can't get a match we need to triage the nostro transaction and figure out what to do with it. 30 | * TODO This flow should probably be called MatchNostroTransactionFlow 31 | */ 32 | @StartableByService 33 | @InitiatingFlow 34 | class ProcessNostroTransaction(val stateAndRef: StateAndRef) : FlowLogic() { 35 | 36 | /** 37 | * Updates the status of the nostro transaction state. Doesn't add anything else. No return type as the builder is 38 | * mutable. 39 | */ 40 | @Suspendable 41 | private fun createBaseTransaction(builder: TransactionBuilder, newType: NostroTransactionType, newStatus: NostroTransactionStatus) { 42 | val command = Command(NostroTransactionContract.Match(), listOf(ourIdentity.owningKey)) 43 | val nostroTransactionOutput = stateAndRef.state.data.copy(type = newType, status = newStatus) 44 | builder 45 | .addInputState(stateAndRef) 46 | .addCommand(command) 47 | .addOutputState(nostroTransactionOutput, NostroTransactionContract.CONTRACT_ID) 48 | } 49 | 50 | @Suspendable 51 | private fun addNodeTransactionState( 52 | builder: TransactionBuilder, 53 | bankAccountStates: List>, 54 | nostroTransactionState: NostroTransactionState, 55 | isRedemption: Boolean = false 56 | ) { 57 | // The original issuance details. 58 | val counterparty = bankAccountStates.single { it.state.data.owner != ourIdentity }.state.data.owner 59 | val issuanceAmount = nostroTransactionState.amountTransfer 60 | // A record of the issuance for the issuer. We store this separately to the nostro transaction states as these 61 | // records pertain to issuance and redemption of cash states as opposed to payments in and out of the nostro 62 | // accounts. Currently this state is committed to the ledger separately to the cash issuance. Ideally we want to 63 | // commit them atomically. 64 | // TODO: This is a hack which needs removing. 65 | // Node transaction states for redemptions are only added by the redeem cash handler. So.. if we remove some 66 | // data from the node (accidentally perhaps) then when the node processes the nostro transactions 67 | val nodeTransactionState = NodeTransactionState( 68 | amountTransfer = AmountTransfer( 69 | quantityDelta = issuanceAmount.quantityDelta, 70 | token = issuanceAmount.token, 71 | source = if (isRedemption) counterparty else ourIdentity, 72 | destination = if (isRedemption) ourIdentity else counterparty 73 | ), 74 | notes = nostroTransactionState.description, 75 | createdAt = Instant.now(), 76 | participants = listOf(ourIdentity), 77 | type = if (isRedemption) NodeTransactionType.REDEMPTION else NodeTransactionType.ISSUANCE 78 | ) 79 | 80 | // TODO: Add node transaction contract code to check info. 81 | builder.addOutputState(nodeTransactionState, NodeTransactionContract.CONTRACT_ID) 82 | .addCommand(NodeTransactionContract.Create(), listOf(ourIdentity.owningKey)) 83 | } 84 | 85 | @Suspendable 86 | override fun call(): SignedTransaction { 87 | logger.info("Starting ProcessNostroTransaction flow.") 88 | // For brevity. 89 | val nostroTransaction = stateAndRef.state.data 90 | val amountTransfer = nostroTransaction.amountTransfer 91 | 92 | // Get StateAndRefs for the bank account data. We discard the nulls. This will contain either 0, 1 or 2 bank 93 | // account state refs. If there's three or more then we have a dupe and this should never happen. 94 | val bankAccountStateRefs = listOf(amountTransfer.source, amountTransfer.destination).map { accountNumber -> 95 | if (accountNumber !is NoAccountNumber) { 96 | getBankAccountStateByAccountNumber(accountNumber, serviceHub) 97 | } else null 98 | }.filterNotNull() 99 | 100 | // Set up our transaction builder. 101 | val notary = serviceHub.networkMapCache.notaryIdentities.first() 102 | val builder = TransactionBuilder(notary = notary) 103 | 104 | // It's easier to work with ContractStates. 105 | val bankAccountStates = bankAccountStateRefs.map { it.state.data } 106 | 107 | /** ONLY ONE OF THESE CONDITIONS SHOULD EVER BE TRUE FOR EACH NOSTRO TRANSACTION! */ 108 | 109 | // If there are no matches then something has gone quite wrong. 110 | // We always should have, at least, the bank account details for the issuer. 111 | val areNoMatches = bankAccountStates.isEmpty() 112 | 113 | // We can only match one of the bank accounts. 114 | // If there is only one account matched then it should always be the issuer's account. 115 | val isSingleMatch = bankAccountStates.size == 1 116 | val issuerMatchOnly = bankAccountStates.singleOrNull { it.owner == ourIdentity } != null && isSingleMatch 117 | 118 | // If all of the bank accounts are the issuer's then this transaction must be a 119 | // transfer between nostro accounts. We should see an equal and opposite transfer 120 | // on another account. 121 | val isDoubleMatch = bankAccountStates.size == 2 122 | val isDoubleMatchInternalTransfer = bankAccountStates.all { it.owner == ourIdentity } && isDoubleMatch 123 | 124 | // Check to see if one of the accounts is ours and the other, a counterparty's. 125 | val singleIssuerBankAccount = bankAccountStates.singleOrNull { it.owner == ourIdentity } 126 | val singleCounterpartyBankAccount = bankAccountStates.toSet().minus(singleIssuerBankAccount).singleOrNull() 127 | val isDoubleMatchExternalTransfer = singleIssuerBankAccount != null && singleCounterpartyBankAccount != null 128 | 129 | // If the amount transfer is greater than zero, then the assumption is that if it isn't an 130 | // internal transfer, it MUST be a deposit from a counterparty's account. Therefore, an issuance. 131 | val isIssuance = amountTransfer.quantityDelta > 0L && (isDoubleMatchExternalTransfer || issuerMatchOnly) 132 | val isRedemption = amountTransfer.quantityDelta < 0L && (isDoubleMatchExternalTransfer || issuerMatchOnly) 133 | 134 | // Add whatever nostro account states we have in the list. 135 | bankAccountStateRefs.forEach { builder.addReferenceState(it.referenced()) } 136 | 137 | when { 138 | areNoMatches -> throw FlowException("We should always, at least, have our bank account data recorded.") 139 | isDoubleMatchInternalTransfer -> { 140 | logger.info("The nostro transaction is an internal transfer.") 141 | createBaseTransaction(builder, NostroTransactionType.COLLATERAL_TRANSFER, NostroTransactionStatus.MATCHED) 142 | } 143 | issuerMatchOnly -> { 144 | logger.info("We don't have the counterparty's bank account details.") 145 | logger.info("We'll have to keep this cash safe until we figure out who sent it to us.") 146 | val type = if (isIssuance) NostroTransactionType.ISSUANCE else NostroTransactionType.REDEMPTION 147 | createBaseTransaction(builder, type, NostroTransactionStatus.MATCHED_ISSUER) 148 | } 149 | isIssuance -> { 150 | createBaseTransaction(builder, NostroTransactionType.ISSUANCE, NostroTransactionStatus.MATCHED) 151 | addNodeTransactionState(builder, bankAccountStateRefs, nostroTransaction, isRedemption) 152 | // TODO: Check that accounts are verified. 153 | logger.info("This is an issuance!") 154 | } 155 | isRedemption -> { 156 | createBaseTransaction(builder, NostroTransactionType.REDEMPTION, NostroTransactionStatus.MATCHED) 157 | // TODO: Hack alert!!! (Need to do some more thinking around this re: "start from date"). 158 | // Need to work out how we deal with backup restores or processing nostro transactinos which have 159 | // already been processed after a database backup restore. Currently, I'm just re-creating the pending 160 | // node transaction state here as the assumption is that node transaction states always precede 161 | // nostro transaction states for redemptions. 162 | if (getPendingRedemptionByNotes(nostroTransaction.description, serviceHub) == null) { 163 | addNodeTransactionState(builder, bankAccountStateRefs, nostroTransaction, isRedemption) 164 | } 165 | logger.info("This is an redemption!") 166 | } 167 | else -> throw FlowException("Something went wrong. Someone is going to be in trouble...!") 168 | } 169 | 170 | val stx = serviceHub.signInitialTransaction(builder) 171 | return subFlow(FinalityFlow(stx, emptySet())) 172 | } 173 | 174 | } -------------------------------------------------------------------------------- /integration-test/src/test/kotlin/test/IntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import com.r3.corda.finance.cash.issuer.client.flows.RedeemCashShell 4 | import com.r3.corda.finance.cash.issuer.service.flows.AddNostroTransactions 5 | import com.r3.corda.lib.tokens.contracts.states.FungibleToken 6 | import com.r3.corda.lib.tokens.money.GBP 7 | import com.r3.corda.sdk.issuer.common.contracts.states.BankAccountState 8 | import com.r3.corda.sdk.issuer.common.contracts.states.NodeTransactionState 9 | import com.r3.corda.sdk.issuer.common.contracts.states.NostroTransactionState 10 | import com.r3.corda.sdk.issuer.common.contracts.types.BankAccount 11 | import com.r3.corda.sdk.issuer.common.contracts.types.BankAccountType 12 | import com.r3.corda.sdk.issuer.common.contracts.types.NostroTransaction 13 | import com.r3.corda.sdk.issuer.common.contracts.types.UKAccountNumber 14 | import com.r3.corda.sdk.issuer.common.workflows.flows.AddBankAccount 15 | import com.r3.corda.sdk.issuer.common.workflows.flows.MoveCashShell 16 | import net.corda.core.contracts.ContractState 17 | import net.corda.core.identity.CordaX500Name 18 | import net.corda.core.messaging.startFlow 19 | import net.corda.core.toFuture 20 | import net.corda.core.utilities.contextLogger 21 | import net.corda.core.utilities.getOrThrow 22 | import net.corda.testing.common.internal.testNetworkParameters 23 | import net.corda.testing.driver.DriverParameters 24 | import net.corda.testing.driver.NodeParameters 25 | import net.corda.testing.driver.driver 26 | import net.corda.testing.node.TestCordapp 27 | import org.junit.Test 28 | import rx.Observable 29 | import rx.schedulers.Schedulers 30 | import java.time.Instant 31 | import java.util.concurrent.CompletableFuture 32 | import kotlin.test.assertEquals 33 | 34 | class IntegrationTest { 35 | 36 | companion object { 37 | private val log = contextLogger() 38 | } 39 | 40 | private val partyA = NodeParameters( 41 | providedName = CordaX500Name("PartyA", "London", "GB"), 42 | additionalCordapps = listOf(TestCordapp.findCordapp("com.r3.corda.finance.cash.issuer.client")) 43 | ) 44 | 45 | private val partyB = NodeParameters( 46 | providedName = CordaX500Name("PartyB", "London", "GB"), 47 | additionalCordapps = listOf(TestCordapp.findCordapp("com.r3.corda.finance.cash.issuer.client")) 48 | ) 49 | 50 | private val issuer = NodeParameters( 51 | providedName = CordaX500Name("Issuer", "London", "GB"), 52 | additionalCordapps = listOf(TestCordapp.findCordapp("com.r3.corda.finance.cash.issuer.service")) 53 | ) 54 | 55 | private val defaultCorDapps = listOf( 56 | TestCordapp.findCordapp("com.r3.corda.sdk.issuer.common.contracts"), 57 | TestCordapp.findCordapp("com.r3.corda.sdk.issuer.common.workflows"), 58 | TestCordapp.findCordapp("com.r3.corda.lib.tokens.workflows"), 59 | TestCordapp.findCordapp("com.r3.corda.lib.tokens.contracts"), 60 | TestCordapp.findCordapp("com.r3.corda.lib.tokens.money") 61 | ) 62 | 63 | private val driverParameters = DriverParameters( 64 | startNodesInProcess = false, 65 | cordappsForAllNodes = defaultCorDapps, 66 | networkParameters = testNetworkParameters(notaries = emptyList(), minimumPlatformVersion = 4), 67 | isDebug = false 68 | ) 69 | 70 | @Test 71 | fun `node test`() { 72 | driver(driverParameters) { 73 | val A = startNode(partyA).getOrThrow() 74 | val B = startNode(partyB).getOrThrow() 75 | val I = startNode(issuer).getOrThrow() 76 | 77 | log.info("All nodes started up.") 78 | 79 | val issuerBankAccount = BankAccount( 80 | accountId = "1", 81 | accountName = "Issuer Collateral Account", 82 | accountNumber = UKAccountNumber(sortCode = "224466", accountNumber = "11228899"), 83 | currency = GBP, 84 | type = BankAccountType.COLLATERAL 85 | ) 86 | 87 | val partyABankAccount = BankAccount( 88 | accountId = "2", 89 | accountName = "Party A Bank Account", 90 | accountNumber = UKAccountNumber(sortCode = "112233", accountNumber = "44557788"), 91 | currency = GBP, 92 | type = BankAccountType.COLLATERAL 93 | ) 94 | 95 | val partyBBankAccount = BankAccount( 96 | accountId = "3", 97 | accountName = "Party B Bank Account", 98 | accountNumber = UKAccountNumber(sortCode = "996633", accountNumber = "11663300"), 99 | currency = GBP, 100 | type = BankAccountType.COLLATERAL 101 | ) 102 | 103 | val paymentToIssuerNostro = NostroTransaction( 104 | transactionId = "1", 105 | accountId = "1", 106 | amount = 1000L, 107 | currency = GBP, 108 | type = "", 109 | description = "", 110 | createdAt = Instant.now(), 111 | source = UKAccountNumber(sortCode = "112233", accountNumber = "44557788"), // Party A. 112 | destination = UKAccountNumber(sortCode = "224466", accountNumber = "11228899") 113 | ) 114 | 115 | fun generatePaymentFromIssuerNostro(secretCode: String): NostroTransaction { 116 | return NostroTransaction( 117 | transactionId = "2", 118 | accountId = "1", 119 | amount = -200L, 120 | currency = GBP, 121 | type = "", 122 | description = secretCode, 123 | createdAt = Instant.now(), 124 | source = UKAccountNumber(sortCode = "224466", accountNumber = "11228899"), 125 | destination = UKAccountNumber(sortCode = "996633", accountNumber = "11663300") // Party B. 126 | ) 127 | } 128 | 129 | val issuerParty = I.nodeInfo.legalIdentities.first() 130 | val aParty = A.nodeInfo.legalIdentities.first() 131 | val bParty = B.nodeInfo.legalIdentities.first() 132 | 133 | // ----------------------------- 134 | // Stage 1 - Add issuer account. 135 | // ----------------------------- 136 | 137 | // Start add bank account flow. 138 | val addIssuerBankAccountFlow = I.rpc.startFlow(::AddBankAccount, issuerBankAccount, issuerParty).returnValue.toCompletableFuture() 139 | // Confirm state is stored in the vault of the issuer. 140 | val stageOneIssuerVaultUpdate = I.rpc.vaultTrack(BankAccountState::class.java).updates.toFuture().toCompletableFuture() 141 | // Wait for all futures to complete. 142 | CompletableFuture.allOf(addIssuerBankAccountFlow, stageOneIssuerVaultUpdate) 143 | // Print transaction. 144 | println("Issuer bank account state added.") 145 | println(addIssuerBankAccountFlow.get().tx) 146 | // TODO: Need to manually verify bank account. 147 | 148 | // ------------------------------ 149 | // Stage 2 - Add Party A account. 150 | // ------------------------------ 151 | 152 | // Add partyA account and check it was added. 153 | val addPartyABankAccountFlow = A.rpc.startFlow(::AddBankAccount, partyABankAccount, issuerParty).returnValue.toCompletableFuture() 154 | // Confirm state is stored in the vault of the issuer and party A. 155 | val stageTwoIssuerVaultUpdate = I.rpc.vaultTrack(BankAccountState::class.java).updates.toFuture().toCompletableFuture() 156 | val stageTwoPartyAVaultUpdate = A.rpc.vaultTrack(BankAccountState::class.java).updates.toFuture().toCompletableFuture() 157 | // Wait for all futures to complete. 158 | CompletableFuture.allOf(stageTwoIssuerVaultUpdate, stageTwoPartyAVaultUpdate, addPartyABankAccountFlow) 159 | // Check the issuer and party A have the same state. 160 | assertEquals(stageTwoIssuerVaultUpdate.get().produced.single(), stageTwoPartyAVaultUpdate.get().produced.single()) 161 | // Print transaction. 162 | println("Party A bank account state added.") 163 | // Confirm that the bank account verification was done. 164 | val stageTwoIssuerVerifyBankAccountStateUpdate = I.rpc.vaultTrack(BankAccountState::class.java).updates.toFuture().toCompletableFuture() 165 | println(addPartyABankAccountFlow.get().tx) 166 | println("Party A bank account verified.") 167 | val verifyBankAccountStateVaultUpdate = stageTwoIssuerVerifyBankAccountStateUpdate.getOrThrow() 168 | //assertEquals(verifyBankAccountStateVaultUpdate.consumed.single(), stageTwoIssuerVaultUpdate.get().produced.single()) 169 | println(verifyBankAccountStateVaultUpdate) 170 | 171 | // --------------------------------- 172 | // Stage 3 - Add nostro transaction. 173 | // --------------------------------- 174 | 175 | // The updates the internal state of the issuer and issues the same amount of currency (as tokens) that was 176 | // "paid" into the issuer's bank account. 177 | 178 | println("Add nostro transaction (issue cash)") 179 | val addNostroTransactionFlow = I.rpc.startFlow(::AddNostroTransactions, listOf(paymentToIssuerNostro)).returnValue.toCompletableFuture() 180 | // Get the first nostro transaction state added to the database. 181 | val newNostroTransactionState = I.rpc.vaultTrack(NostroTransactionState::class.java).updates.getOrThrow() 182 | println(newNostroTransactionState) 183 | // Get the updated nostro transaction state (MATCHED) and node transaction state. 184 | val nostroTransactionStateUpdate = I.rpc.vaultTrack(ContractState::class.java).updates.getOrThrow() 185 | println(nostroTransactionStateUpdate) 186 | // Get the updated node transaction state (COMPLETE). 187 | val nostroTransactionStateUpdateTwo = I.rpc.vaultTrack(NodeTransactionState::class.java).updates.getOrThrow() 188 | println(nostroTransactionStateUpdateTwo) 189 | // Get the cash issuance. 190 | val newCashOnPartyA = A.rpc.vaultTrack(FungibleToken::class.java).updates.getOrThrow() 191 | println(newCashOnPartyA) 192 | addNostroTransactionFlow.getOrThrow() 193 | 194 | // ---------------------- 195 | // Stage 4 - Cash payment. 196 | // ---------------------- 197 | 198 | println("Cash payment.") 199 | val moveCashFlow = A.rpc.startFlowDynamic(MoveCashShell::class.java, bParty, 500L, "GBP").returnValue.toCompletableFuture() 200 | val newTokenMoveA = A.rpc.vaultTrack(FungibleToken::class.java).updates.toFuture().toCompletableFuture() 201 | val newTokenMoveB = B.rpc.vaultTrack(FungibleToken::class.java).updates.toFuture().toCompletableFuture() 202 | CompletableFuture.allOf(moveCashFlow, newTokenMoveA, newTokenMoveB) 203 | println(moveCashFlow.getOrThrow().tx) 204 | 205 | // ------------------------------------- 206 | // Stage 5 - Add Party B's bank account. 207 | // ------------------------------------- 208 | 209 | // Start add bank account flow. 210 | val addPartyBBankAccountFlow = B.rpc.startFlow(::AddBankAccount, partyBBankAccount, issuerParty).returnValue.toCompletableFuture() 211 | // Confirm state is stored in the vault of the issuer. 212 | val stageFiveIssuerVaultUpdate = I.rpc.vaultTrack(BankAccountState::class.java).updates.toFuture().toCompletableFuture() 213 | // Wait for all futures to complete. 214 | CompletableFuture.allOf(addPartyBBankAccountFlow, stageFiveIssuerVaultUpdate) 215 | // Print transaction. 216 | println("Party B bank account state added.") 217 | println(addPartyBBankAccountFlow.get().tx) 218 | 219 | // ------------------------------------ 220 | // Stage 6 - Redeem tokens with change. 221 | // ------------------------------------ 222 | 223 | val redeemTx = B.rpc.startFlowDynamic(RedeemCashShell::class.java, 200L, "GBP", issuerParty).returnValue.toCompletableFuture() 224 | val partyBchange = B.rpc.vaultTrack(FungibleToken::class.java).updates.toFuture().toCompletableFuture() 225 | CompletableFuture.allOf(redeemTx, partyBchange) 226 | println("Party B change:") 227 | println(partyBchange.getOrThrow()) 228 | println(redeemTx.getOrThrow().tx) 229 | val nodeTxStateTracker = I.rpc.vaultTrack(NodeTransactionState::class.java).updates.toFuture().toCompletableFuture() 230 | // Generate the payment from the issuer to the redeeming party's bank account. 231 | val nodeTxState = nodeTxStateTracker.getOrThrow() 232 | val secretCode = nodeTxState.produced.single().state.data.notes 233 | val nostroTx = generatePaymentFromIssuerNostro(secretCode) 234 | val redeemNostroTxFlow = I.rpc.startFlow(::AddNostroTransactions, listOf(nostroTx)).returnValue.toCompletableFuture() 235 | // Get the first nostro transaction state added to the database. 236 | val redeemNostroTxState = I.rpc.vaultTrack(NostroTransactionState::class.java).updates.getOrThrow() 237 | println(redeemNostroTxState) 238 | val updateNostroTxState = I.rpc.vaultTrack(NostroTransactionState::class.java).updates.getOrThrow() 239 | println(updateNostroTxState) 240 | val nodeTxStateTrackerTwo = I.rpc.vaultTrack(NodeTransactionState::class.java).updates.getOrThrow() 241 | println(nodeTxStateTrackerTwo) 242 | redeemNostroTxFlow.getOrThrow() 243 | } 244 | 245 | } 246 | } 247 | 248 | fun Observable.getOrThrow() = observeOn(Schedulers.io()) 249 | .toFuture() 250 | .getOrThrow() --------------------------------------------------------------------------------