diff --git a/.github/workflows/check-pr-title.yaml b/.github/workflows/check-pr-title.yaml deleted file mode 100644 index 66f4ddd..0000000 --- a/.github/workflows/check-pr-title.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: 'Check PR Title' -on: - pull_request: - types: [opened, edited, reopened] - -jobs: - check-pr-title: - runs-on: ubuntu-latest - steps: - - uses: morrisoncole/pr-lint-action@v1.6.1 - with: - title-regex: '^([A-Z]{2,}-\d+)(.*)' - on-failed-regex-comment: "PR title failed to match regex -> `%regex%`" - repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/java-samples/README.md b/java-samples/README.md index a198dde..a46fc5d 100644 --- a/java-samples/README.md +++ b/java-samples/README.md @@ -11,6 +11,7 @@ This is the Java Samples folder. ``` . ├── README.md -└── corda5-obligation-cordapp +├── corda5-obligation-cordapp +└── mgm-dynamic-network ``` diff --git a/java-samples/mgm-dynamic-network/README.md b/java-samples/mgm-dynamic-network/README.md new file mode 100644 index 0000000..dbe6a87 --- /dev/null +++ b/java-samples/mgm-dynamic-network/README.md @@ -0,0 +1,53 @@ +# MGM-Dynamic-Network + +This demo CorDapp is to show the use of the MGM tool on setting up a dynamic Corda5 application network. This app will use the Corda 5 combined worker as the foundation. We will then use the MGM tool to deploy the dynamic Corda5 application network on it. All the instructions are written into 4 shell scripts, which you can simply run them. + +## Before running the app +* You need to prepare a self-generate CA for create certificates for keys generated within Corda. This CA will be external to Corda. +* You would need to reset the path for both `WORK_DIR` and `RUNTIME_OS` variables in the script. + +[R3 INTERAL NOTES]: If you are an R3 employee, you can use the mock ca tool in Corda-runtime-os. Get the `release/os/5.0-Beta2` branch of the Corda-runtime-os. + + +## Running the demo +Find the gradle task `startCorda` and wait for the combined worker to be fully started. Once the swagger page is loaded, run the shell script one by one, and follow the prompts. +``` +. +├── Step1-mgm-deploy.sh +├── Step2-notary-onboard.sh +├── Step3-first-member-onboard.sh +└── Step4-more-member-onboard.sh +``` + +## Test your deployment +#### Step 1: Create Chat Entry +Pick a VNode identity to initiate the chat, and get its short hash. (Let's pick Alice. Don't pick Bob because Bob is the person who we will have the chat with). + +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +``` +{ + "clientRequestId": "create-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.CreateNewChatFlow", + "requestBody": { + "chatName":"Chat with Bob", + "otherMember":"THE-NODE-YOU-CREATED", + "message": "Hello Bob" + } +} +``` + +After trigger the create-chat flow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and clientrequestid to view the flow result + +#### Step 2: List the chat +In order to continue the chat, we would need the chat ID. This step will bring out all the chat entries this entity (Alice) has. +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +``` +{ + "clientRequestId": "list-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.ListChatsFlow", + "requestBody": {} +} +``` +After trigger the list-chats flow, again, we need to hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and check the result. As the screenshot shows, in the response body, +we will see a list of chat entries, but it currently only has one entry. And we can see the id of the chat entry. Let's record that id. + diff --git a/java-samples/mgm-dynamic-network/Step1-mgm-deploy.sh b/java-samples/mgm-dynamic-network/Step1-mgm-deploy.sh new file mode 100644 index 0000000..d7249d2 --- /dev/null +++ b/java-samples/mgm-dynamic-network/Step1-mgm-deploy.sh @@ -0,0 +1,150 @@ +#!/bin/sh + +echo "---Set Env---" +RPC_HOST=localhost +RPC_PORT=8888 +P2P_GATEWAY_HOST=localhost +P2P_GATEWAY_PORT=8080 +API_URL="https://$RPC_HOST:$RPC_PORT/api/v1" +WORK_DIR=~/Corda/corda5/corda5-samples/java-samples/mgm-dynamic-network/register-mgm +mkdir -p "$WORK_DIR" +RUNTIME_OS=~/Corda/corda5/corda-runtime-os + +echo "\n---Create a mock CA and signing keys---" +cd "$WORK_DIR" +#default signing key +echo '-----BEGIN CERTIFICATE----- +MIIB7zCCAZOgAwIBAgIEFyV7dzAMBggqhkjOPQQDAgUAMFsxCzAJBgNVBAYTAkdC +MQ8wDQYDVQQHDAZMb25kb24xDjAMBgNVBAoMBUNvcmRhMQswCQYDVQQLDAJSMzEe +MBwGA1UEAwwVQ29yZGEgRGV2IENvZGUgU2lnbmVyMB4XDTIwMDYyNTE4NTI1NFoX +DTMwMDYyMzE4NTI1NFowWzELMAkGA1UEBhMCR0IxDzANBgNVBAcTBkxvbmRvbjEO +MAwGA1UEChMFQ29yZGExCzAJBgNVBAsTAlIzMR4wHAYDVQQDExVDb3JkYSBEZXYg +Q29kZSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQDjSJtzQ+ldDFt +pHiqdSJebOGPZcvZbmC/PIJRsZZUF1bl3PfMqyG3EmAe0CeFAfLzPQtf2qTAnmJj +lGTkkQhxo0MwQTATBgNVHSUEDDAKBggrBgEFBQcDAzALBgNVHQ8EBAMCB4AwHQYD +VR0OBBYEFLMkL2nlYRLvgZZq7GIIqbe4df4pMAwGCCqGSM49BAMCBQADSAAwRQIh +ALB0ipx6EplT1fbUKqgc7rjH+pV1RQ4oKF+TkfjPdxnAAiArBdAI15uI70wf+xlL +zU+Rc5yMtcOY4/moZUq36r0Ilg== +-----END CERTIFICATE-----' > ./gradle-plugin-default-key.pem + +#Generate a signing key: +keytool -genkeypair -alias "signing key 1" -keystore signingkeys.pfx -storepass "keystore password" -dname "cn=CPI Plugin Example - Signing Key 1, o=R3, L=London, c=GB" -keyalg RSA -storetype pkcs12 -validity 4000 +#Import gradle-plugin-default-key.pem into the keystore +keytool -importcert -keystore signingkeys.pfx -storepass "keystore password" -noprompt -alias gradle-plugin-default-key -file gradle-plugin-default-key.pem +#cd "$RUNTIME_OS" +#./gradlew :applications:tools:p2p-test:fake-ca:clean :applications:tools:p2p-test:fake-ca:appJar +#java -jar ./applications/tools/p2p-test/fake-ca/build/bin/corda-fake-ca-5.0.0.0-Gecko-SNAPSHOT.jar -m /tmp/ca -a RSA -s 3072 ca +cd "$WORK_DIR" + + +echo "\n---Build mgm CPB---" +#cd "$RUNTIME_OS" +#./gradlew testing:cpbs:mgm:build +#cp testing/cpbs/mgm/build/libs/mgm-5.0.0.0-Gecko-SNAPSHOT-package.cpb "$WORK_DIR" +echo '{ + "fileFormatVersion" : 1, + "groupId" : "CREATE_ID", + "registrationProtocol" :"net.corda.membership.impl.registration.dynamic.mgm.MGMRegistrationService", + "synchronisationProtocol": "net.corda.membership.impl.synchronisation.MgmSynchronisationServiceImpl" +}' > "$WORK_DIR"/MgmGroupPolicy.json +cd "$WORK_DIR" +mv ./mgm-5.0.0.0-Gecko-SNAPSHOT-package.cpb mgm.cpb + + +echo "\n---Build and upload MGM CPI---" +cd "$WORK_DIR" +#Run this command to turn a CPB into a CPI +sh ~/.corda/cli/corda-cli.sh package create-cpi --cpb mgm.cpb --group-policy MgmGroupPolicy.json --cpi-name "mgm cpi" --cpi-version "1.0.0.0-SNAPSHOT" --file mgm.cpi --keystore signingkeys.pfx --storepass "keystore password" --key "signing key 1" +#Import the gradle plugin default key into Corda +curl --insecure -u admin:admin -X PUT -F alias="gradle-plugin-default-key" -F certificate=@gradle-plugin-default-key.pem https://localhost:8888/api/v1/certificates/cluster/code-signer +#Export the signing key certificate from the key store +keytool -exportcert -rfc -alias "signing key 1" -keystore signingkeys.pfx -storepass "keystore password" -file signingkey1.pem +#Import the signing key into Corda +curl --insecure -u admin:admin -X PUT -F alias="signingkey1-2022" -F certificate=@signingkey1.pem https://localhost:8888/api/v1/certificates/cluster/code-signer +CPI_PATH=./mgm.cpi +curl --insecure -u admin:admin -F upload=@$CPI_PATH $API_URL/cpi/ +echo "\n" +read -p "Enter the CPI_ID from the returned body:" CPI_ID +echo "CPI_ID:" $CPI_ID + + +echo "---Create MGM VNode---" +curl --insecure -u admin:admin $API_URL/cpi/status/$CPI_ID +echo "\n" +read -p "Enter the CPI_CHECKSUM from the returned body:" CPI_CHECKSUM +curl --insecure -u admin:admin -d '{ "request": {"cpiFileChecksum": "'$CPI_CHECKSUM'", "x500Name": "C=GB, L=London, O=MGM"}}' $API_URL/virtualnode +echo "\n" +read -p "Enter the MGM_HOLDING_ID from the returned body:" MGM_HOLDING_ID +echo "MGM_HOLDING_ID:" $MGM_HOLDING_ID + + +echo "---Assign soft HSM---" +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$MGM_HOLDING_ID/SESSION_INIT +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$MGM_HOLDING_ID/alias/$MGM_HOLDING_ID-session/category/SESSION_INIT/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the SESSION_KEY_ID from the returned body:" SESSION_KEY_ID +echo "SESSION_KEY_ID:" $SESSION_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$MGM_HOLDING_ID/PRE_AUTH +echo "\nECDH_KEY_ID: " +curl --insecure -u admin:admin -X POST $API_URL/keys/$MGM_HOLDING_ID/alias/$MGM_HOLDING_ID-auth/category/PRE_AUTH/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the ECDH_KEY_ID from the returned body:" ECDH_KEY_ID +echo "ECDH_KEY_ID:" $ECDH_KEY_ID + + +echo "\n--Set up the TLS key pair and certificate---" +curl -k -u admin:admin -X POST -H "Content-Type: application/json" $API_URL/keys/p2p/alias/p2p-TLS/category/TLS/scheme/CORDA.RSA +echo "\n" +read -p "Enter the TLS_KEY_ID from the returned body:" TLS_KEY_ID +echo "TLS_KEY_ID:" $TLS_KEY_ID +curl -k -u admin:admin -X POST -H "Content-Type: application/json" -d '{"x500Name": "CN=CordaOperator, C=GB, L=London, O=Org", "subjectAlternativeNames": ["'$P2P_GATEWAY_HOST'"]}' $API_URL"/certificates/p2p/"$TLS_KEY_ID > "$WORK_DIR"/request1.csr +read -p "Wait for download to be finished, Then press any key to continue..." ANY +cd "$RUNTIME_OS" +java -jar ./applications/tools/p2p-test/fake-ca/build/bin/corda-fake-ca-5.0.0.0-Gecko-SNAPSHOT.jar -m /tmp/ca csr "$WORK_DIR"/request1.csr +cd "$WORK_DIR" +curl -k -u admin:admin -X PUT -F certificate=@/tmp/ca/request1/certificate.pem -F alias=p2p-tls-cert $API_URL/certificates/cluster/p2p-tls + + +echo "---Disable revocation checks---" +curl --insecure -u admin:admin -X GET $API_URL/config/corda.p2p.gateway +echo "\n" +read -p "Enter the CONFIG_VERSION from the returned body:" CONFIG_VERSION +echo "CONFIG_VERSION:" $CONFIG_VERSION +curl -k -u admin:admin -X PUT -d '{"section":"corda.p2p.gateway", "version":"'$CONFIG_VERSION'", "config":"{ \"sslConfig\": { \"revocationCheck\": { \"mode\": \"OFF\" } } }", "schemaVersion": {"major": 1, "minor": 0}}' $API_URL"/config" + + +echo "\n---Register MGM---" +TLS_CA_CERT=$(cat /tmp/ca/ca/root-certificate.pem | awk '{printf "%s\\n", $0}') +REGISTRATION_CONTEXT='{ + "corda.session.key.id": "'$SESSION_KEY_ID'", + "corda.ecdh.key.id": "'$ECDH_KEY_ID'", + "corda.group.protocol.registration": "net.corda.membership.impl.registration.dynamic.member.DynamicMemberRegistrationService", + "corda.group.protocol.synchronisation": "net.corda.membership.impl.synchronisation.MemberSynchronisationServiceImpl", + "corda.group.protocol.p2p.mode": "Authenticated_Encryption", + "corda.group.key.session.policy": "Combined", + "corda.group.pki.session": "NoPKI", + "corda.group.pki.tls": "Standard", + "corda.group.tls.version": "1.3", + "corda.endpoints.0.connectionURL": "https://'$P2P_GATEWAY_HOST':'$P2P_GATEWAY_PORT'", + "corda.endpoints.0.protocolVersion": "1", + "corda.group.truststore.tls.0" : "'$TLS_CA_CERT'" +}' +REGISTRATION_REQUEST='{"memberRegistrationRequest":{"action": "requestJoin", "context": '$REGISTRATION_CONTEXT'}}' +curl --insecure -u admin:admin -d "$REGISTRATION_REQUEST" $API_URL/membership/$MGM_HOLDING_ID +echo "\n" +read -p "Enter the REGISTRATION_ID from the returned body:" REGISTRATION_ID +echo "REGISTRATION_ID:" $REGISTRATION_ID +curl --insecure -u admin:admin -X GET $API_URL/membership/$MGM_HOLDING_ID/$REGISTRATION_ID +echo "\n" + + +echo "---Configure virtual node as network participant---" +curl -k -u admin:admin -X PUT -d '{"p2pTlsCertificateChainAlias": "p2p-tls-cert", "useClusterLevelTlsCertificateAndKey": true, "sessionKeyId": "'$SESSION_KEY_ID'"}' $API_URL/network/setup/$MGM_HOLDING_ID +echo "\n" + + +echo "---Export group policy for group---" +cd "$WORK_DIR" +mkdir -p "./register-member" +curl --insecure -u admin:admin -X GET $API_URL/mgm/$MGM_HOLDING_ID/info > "$WORK_DIR/register-member/GroupPolicy.json" diff --git a/java-samples/mgm-dynamic-network/Step2-notary-onboard.sh b/java-samples/mgm-dynamic-network/Step2-notary-onboard.sh new file mode 100644 index 0000000..a1d774b --- /dev/null +++ b/java-samples/mgm-dynamic-network/Step2-notary-onboard.sh @@ -0,0 +1,92 @@ +#!/bin/sh + +echo "---Set Env---" +RPC_HOST=localhost +RPC_PORT=8888 +P2P_GATEWAY_HOST=localhost +P2P_GATEWAY_PORT=8080 +API_URL="https://$RPC_HOST:$RPC_PORT/api/v1" +WORK_DIR=~/Corda/corda5/corda5-samples/java-samples/mgm-dynamic-network/register-mgm +RUNTIME_OS=~/Corda/corda5/corda-runtime-os + + +echo "\n---Build and upload Notary CPI---" +cd $WORK_DIR +cp ./notary.cpb ./register-member +cd "$WORK_DIR/register-member/" +##Run this command to turn a CPB into a CPI +sh ~/.corda/cli/corda-cli.sh package create-cpi --cpb notary.cpb --group-policy GroupPolicy.json --cpi-name "notary cpi" --cpi-version "1.0.0.0-SNAPSHOT" --file notary.cpi --keystore ../signingkeys.pfx --storepass "keystore password" --key "signing key 1" +CPI_PATH="$WORK_DIR/register-member/notary.cpi" +curl --insecure -u admin:admin -F upload=@$CPI_PATH $API_URL/cpi/ +echo "\n" +read -p "Enter the Notary CPI_ID from the returned body:" CPI_ID +echo "CPI_ID:" $CPI_ID +curl --insecure -u admin:admin $API_URL/cpi/status/$CPI_ID +echo "\n" +read -p "Enter the CPI_CHECKSUM from the returned body:" CPI_CHECKSUM + + +echo "\n---Create a Member virtual node---" +echo "\n" +read -p "Enter the X500_NAME from the returned body (Formatt: C=GB,L=London,O=NotaryRep1):" X500_NAME +curl --insecure -u admin:admin -d '{"request": {"cpiFileChecksum": "'$CPI_CHECKSUM'", "x500Name": "'$X500_NAME'"}}' $API_URL/virtualnode +echo "\n" +read -p "Enter the HOLDING_ID from the returned body:" HOLDING_ID +echo "HOLDING_ID:" $HOLDING_ID +echo "\n" + + +echo "---Assign soft HSM---" +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/SESSION_INIT +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL'/keys/'$HOLDING_ID'/alias/'$HOLDING_ID'-session/category/SESSION_INIT/scheme/CORDA.ECDSA.SECP256R1' +echo "\n" +read -p "Enter the SESSION_KEY_ID from the returned body:" SESSION_KEY_ID +echo "SESSION_KEY_ID:" $SESSION_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/LEDGER +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$HOLDING_ID/alias/$HOLDING_ID-ledger/category/LEDGER/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the LEDGER_KEY_ID from the returned body:" LEDGER_KEY_ID +echo "LEDGER_KEY_ID:" $LEDGER_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/NOTARY +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$HOLDING_ID/alias/$HOLDING_ID-notary/category/NOTARY/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the NOTARY_KEY_ID from the returned body:" NOTARY_KEY_ID +echo "NOTARY_KEY_ID:" $NOTARY_KEY_ID + + +echo "\n---Configure virtual node as network participant---" +curl -k -u admin:admin -X PUT -d '{"p2pTlsCertificateChainAlias": "p2p-tls-cert", "useClusterLevelTlsCertificateAndKey": true, "sessionKeyId": "'$SESSION_KEY_ID'"}' $API_URL/network/setup/$HOLDING_ID +echo "\n" + + +echo "\n---Build Notary registration context---" +echo "\n" +read -p "Enter the NOTARY_SERVICE_NAME (Formatt: C=GB,L=London,O=NotaryServiceA):" NOTARY_SERVICE_NAME +echo "NOTARY_SERVICE_NAME:" $NOTARY_SERVICE_NAME +REGISTRATION_CONTEXT='{ + "corda.session.key.id": "'$SESSION_KEY_ID'", + "corda.session.key.signature.spec": "SHA256withECDSA", + "corda.ledger.keys.0.id": "'$LEDGER_KEY_ID'", + "corda.ledger.keys.0.signature.spec": "SHA256withECDSA", + "corda.notary.keys.0.id": "'$NOTARY_KEY_ID'", + "corda.notary.keys.0.signature.spec": "SHA256withECDSA", + "corda.endpoints.0.connectionURL": "https://'$P2P_GATEWAY_HOST':'$P2P_GATEWAY_PORT'", + "corda.endpoints.0.protocolVersion": "1", + "corda.roles.0" : "notary", + "corda.notary.service.name" : "'$NOTARY_SERVICE_NAME'", + "corda.notary.service.plugin" : "net.corda.notary.NonValidatingNotary" +}' +REGISTRATION_REQUEST='{"memberRegistrationRequest":{"action": "requestJoin", "context": '$REGISTRATION_CONTEXT'}}' + + +echo "\n---Register Notary VNode---" +curl --insecure -u admin:admin -d "$REGISTRATION_REQUEST" $API_URL/membership/$HOLDING_ID +echo "\n" +read -p "Enter the REGISTRATION_ID from the returned body:" REGISTRATION_ID +echo "REGISTRATION_ID:" $REGISTRATION_ID +curl --insecure -u admin:admin -X GET $API_URL/membership/$HOLDING_ID/$REGISTRATION_ID +echo "\n" +curl --insecure -u admin:admin -X GET $API_URL/members/$HOLDING_ID diff --git a/java-samples/mgm-dynamic-network/Step3-first-member-onboard.sh b/java-samples/mgm-dynamic-network/Step3-first-member-onboard.sh new file mode 100644 index 0000000..20817ca --- /dev/null +++ b/java-samples/mgm-dynamic-network/Step3-first-member-onboard.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +echo "---Set Env---" +RPC_HOST=localhost +RPC_PORT=8888 +P2P_GATEWAY_HOST=localhost +P2P_GATEWAY_PORT=8080 +API_URL="https://$RPC_HOST:$RPC_PORT/api/v1" +WORK_DIR=~/Corda/corda5/corda5-samples/java-samples/mgm-dynamic-network/register-mgm +RUNTIME_OS=~/Corda/corda5/corda-runtime-os + + +echo "\n---Build and upload chat CPI---" +./gradlew jar +./gradlew cpb +cd ./workflows/build/libs +mv ./workflows-1.0-SNAPSHOT-package.cpb chat.cpb +cd $WORK_DIR/.. +cp ./workflows/build/libs/chat.cpb ./register-mgm/register-member +##Run this command to turn a CPB into a CPI +cd "$WORK_DIR/register-member/" +sh ~/.corda/cli/corda-cli.sh package create-cpi --cpb chat.cpb --group-policy GroupPolicy.json --cpi-name "chat cpi" --cpi-version "1.0.0.0-SNAPSHOT" --file chat.cpi --keystore ../signingkeys.pfx --storepass "keystore password" --key "signing key 1" +CPI_PATH="$WORK_DIR/register-member/chat.cpi" +curl --insecure -u admin:admin -F upload=@$CPI_PATH $API_URL/cpi/ +echo "\n" +read -p "Enter the chat CPI_ID from the returned body:" CPI_ID +echo "CPI_ID:" $CPI_ID +curl --insecure -u admin:admin $API_URL/cpi/status/$CPI_ID +echo "\n" +read -p "Enter the CPI_CHECKSUM from the returned body:" CPI_CHECKSUM + + +echo "\n---Create a Member virtual node---" +echo "\n" +read -p "Enter the X500_NAME from the returned body (Formatt: C=GB,L=London,O=Alice):" X500_NAME +curl --insecure -u admin:admin -d '{"request": {"cpiFileChecksum": "'$CPI_CHECKSUM'", "x500Name": "'$X500_NAME'"}}' $API_URL/virtualnode +echo "\n" +read -p "Enter the HOLDING_ID from the returned body:" HOLDING_ID +echo "HOLDING_ID:" $HOLDING_ID + + +echo "---Assign soft HSM---" +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/SESSION_INIT +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL'/keys/'$HOLDING_ID'/alias/'$HOLDING_ID'-session/category/SESSION_INIT/scheme/CORDA.ECDSA.SECP256R1' +echo "\n" +read -p "Enter the SESSION_KEY_ID from the returned body:" SESSION_KEY_ID +echo "SESSION_KEY_ID:" $SESSION_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/LEDGER +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$HOLDING_ID/alias/$HOLDING_ID-ledger/category/LEDGER/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the LEDGER_KEY_ID from the returned body:" LEDGER_KEY_ID +echo "LEDGER_KEY_ID:" $LEDGER_KEY_ID + + +echo "\n---Configure virtual node as network participant---" +curl -k -u admin:admin -X PUT -d '{"p2pTlsCertificateChainAlias": "p2p-tls-cert", "useClusterLevelTlsCertificateAndKey": true, "sessionKeyId": "'$SESSION_KEY_ID'"}' $API_URL/network/setup/$HOLDING_ID + + +echo "\n---Build registration context---" +REGISTRATION_CONTEXT='{ + "corda.session.key.id": "'$SESSION_KEY_ID'", + "corda.session.key.signature.spec": "SHA256withECDSA", + "corda.ledger.keys.0.id": "'$LEDGER_KEY_ID'", + "corda.ledger.keys.0.signature.spec": "SHA256withECDSA", + "corda.endpoints.0.connectionURL": "https://'$P2P_GATEWAY_HOST':'$P2P_GATEWAY_PORT'", + "corda.endpoints.0.protocolVersion": "1" +}' +REGISTRATION_REQUEST='{"memberRegistrationRequest":{"action": "requestJoin", "context": '$REGISTRATION_CONTEXT'}}' + + +echo "\n---Register Member VNode---" +curl --insecure -u admin:admin -d "$REGISTRATION_REQUEST" $API_URL/membership/$HOLDING_ID +echo "\n" +read -p "Enter the REGISTRATION_ID from the returned body:" REGISTRATION_ID +echo "REGISTRATION_ID:" $REGISTRATION_ID +curl --insecure -u admin:admin -X GET $API_URL/membership/$HOLDING_ID/$REGISTRATION_ID +echo "\n" +curl --insecure -u admin:admin -X GET $API_URL/members/$HOLDING_ID +echo "\n" +echo "\n---The Chat app CPI_CHECKSUM is : " +echo $CPI_CHECKSUM diff --git a/java-samples/mgm-dynamic-network/Step4-more-member-onboard.sh b/java-samples/mgm-dynamic-network/Step4-more-member-onboard.sh new file mode 100644 index 0000000..c89c19d --- /dev/null +++ b/java-samples/mgm-dynamic-network/Step4-more-member-onboard.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +echo "---Set Env---" +RPC_HOST=localhost +RPC_PORT=8888 +P2P_GATEWAY_HOST=localhost +P2P_GATEWAY_PORT=8080 +API_URL="https://$RPC_HOST:$RPC_PORT/api/v1" +WORK_DIR=~/Corda/corda5/corda5-samples/java-samples/mgm-dynamic-network/register-mgm +RUNTIME_OS=~/Corda/corda5/corda-runtime-os + + +echo "\n---Create a Member virtual node---" +echo "\n" +read -p "Enter the CPI_CHECKSUM from the returned body:" CPI_CHECKSUM +echo "\n" +read -p "Enter the X500_NAME from the returned body (Formatt: C=GB,L=London,O=Alice):" X500_NAME +curl --insecure -u admin:admin -d '{"request": {"cpiFileChecksum": "'$CPI_CHECKSUM'", "x500Name": "'$X500_NAME'"}}' $API_URL/virtualnode +echo "\n" +read -p "Enter the HOLDING_ID from the returned body:" HOLDING_ID +echo "HOLDING_ID:" $HOLDING_ID + + +echo "---Assign soft HSM---" +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/SESSION_INIT +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL'/keys/'$HOLDING_ID'/alias/'$HOLDING_ID'-session/category/SESSION_INIT/scheme/CORDA.ECDSA.SECP256R1' +echo "\n" +read -p "Enter the SESSION_KEY_ID from the returned body:" SESSION_KEY_ID +echo "SESSION_KEY_ID:" $SESSION_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/LEDGER +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$HOLDING_ID/alias/$HOLDING_ID-ledger/category/LEDGER/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the LEDGER_KEY_ID from the returned body:" LEDGER_KEY_ID +echo "LEDGER_KEY_ID:" $LEDGER_KEY_ID + + +echo "\n---Configure virtual node as network participant---" +curl -k -u admin:admin -X PUT -d '{"p2pTlsCertificateChainAlias": "p2p-tls-cert", "useClusterLevelTlsCertificateAndKey": true, "sessionKeyId": "'$SESSION_KEY_ID'"}' $API_URL/network/setup/$HOLDING_ID + + +echo "\n---Build registration context---" +REGISTRATION_CONTEXT='{ + "corda.session.key.id": "'$SESSION_KEY_ID'", + "corda.session.key.signature.spec": "SHA256withECDSA", + "corda.ledger.keys.0.id": "'$LEDGER_KEY_ID'", + "corda.ledger.keys.0.signature.spec": "SHA256withECDSA", + "corda.endpoints.0.connectionURL": "https://'$P2P_GATEWAY_HOST':'$P2P_GATEWAY_PORT'", + "corda.endpoints.0.protocolVersion": "1" +}' +REGISTRATION_REQUEST='{"memberRegistrationRequest":{"action": "requestJoin", "context": '$REGISTRATION_CONTEXT'}}' + + +echo "\n---Register Member VNode---" +curl --insecure -u admin:admin -d "$REGISTRATION_REQUEST" $API_URL/membership/$HOLDING_ID +echo "\n" +read -p "Enter the REGISTRATION_ID from the returned body:" REGISTRATION_ID +echo "REGISTRATION_ID:" $REGISTRATION_ID +curl --insecure -u admin:admin -X GET $API_URL/membership/$HOLDING_ID/$REGISTRATION_ID +echo "\n" +curl --insecure -u admin:admin -X GET $API_URL/members/$HOLDING_ID + diff --git a/java-samples/mgm-dynamic-network/build.gradle b/java-samples/mgm-dynamic-network/build.gradle new file mode 100644 index 0000000..5f374e9 --- /dev/null +++ b/java-samples/mgm-dynamic-network/build.gradle @@ -0,0 +1,47 @@ +import static org.gradle.api.JavaVersion.VERSION_11 + +plugins { + id 'org.jetbrains.kotlin.jvm' + id 'net.corda.cordapp.cordapp-configuration' + id 'org.jetbrains.kotlin.plugin.jpa' + id 'java' + id 'maven-publish' + id 'csde' +} + +allprojects { + group 'com.r3.developers.csdetemplate' + version '1.0-SNAPSHOT' + + def javaVersion = VERSION_11 + + // Declare the set of Java compiler options we need to build a CorDapp. + tasks.withType(JavaCompile) { + // -parameters - Needed for reflection and serialization to work correctly. + options.compilerArgs += [ + "-parameters" + ] + } + + repositories { + // All dependencies are held in Maven Central + mavenCentral() + } + + tasks.withType(Test).configureEach { + useJUnitPlatform() + } + +} + +publishing { + publications { + maven(MavenPublication) { + artifactId "corda-mgm" + groupId project.group + artifact jar + } + + } +} + diff --git a/java-samples/mgm-dynamic-network/buildSrc/build.gradle b/java-samples/mgm-dynamic-network/buildSrc/build.gradle new file mode 100644 index 0000000..d21dbee --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/build.gradle @@ -0,0 +1,22 @@ + +plugins { + id 'groovy-gradle-plugin' + id 'java' +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + + implementation "com.konghq:unirest-java:$unirestVersion" + implementation "com.konghq:unirest-objectmapper-jackson:$unirestVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion" + + implementation "net.corda:corda-base:$cordaApiVersion" +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/gradle.properties b/java-samples/mgm-dynamic-network/buildSrc/gradle.properties new file mode 100644 index 0000000..7ab643a --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/gradle.properties @@ -0,0 +1,4 @@ +jacksonVersion = 2.13.4 +unirestVersion=3.13.10 + +cordaApiVersion=5.0.0.665-Gecko1.0 diff --git a/java-samples/mgm-dynamic-network/buildSrc/settings.gradle b/java-samples/mgm-dynamic-network/buildSrc/settings.gradle new file mode 100644 index 0000000..86ac012 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/settings.gradle @@ -0,0 +1 @@ +// File intentionally left blank diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/groovy/csde.gradle b/java-samples/mgm-dynamic-network/buildSrc/src/main/groovy/csde.gradle new file mode 100644 index 0000000..a94ffa9 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/groovy/csde.gradle @@ -0,0 +1,248 @@ +// Note, IntelliJ does not recognise the imported Java Classes, hence they are +// highlighted in Red. However, they are recognised in the gradle compilation. + + +// todo: look at the declaration of the script variables, can they be combined with the declaration of the Project Context +// todo: investigate adding corda-cli to the class path then executing it directly - might not work as gradle has to set up the jar file, so its not their when you start. +// Todo: write a test flow runner helper function?? +// todo: rename deployCPIsHelper +// todo: add proper logging, rather than reading Stdout +// todo: add test corda running/live task +// todo: add a test to check docker is running and display error if not + halt start corda +// todo: add a clean corda task. +// todo: fix logging level and make it configurable. + + +import com.r3.csde.CordaLifeCycleHelper +import com.r3.csde.ProjectContext +import com.r3.csde.DeployCPIsHelper +import com.r3.csde.BuildCPIsHelper +import com.r3.csde.ProjectUtils +import com.r3.csde.CordaStatusQueries +import com.r3.csde.VNodesHelper +import com.r3.csde.NetworkConfig + +plugins { + id 'java-library' + id 'groovy' + id 'java' +} + + +configurations { + combinedWorker{ + canBeConsumed = false + canBeResolved= true + } + + myPostgresJDBC { + canBeConsumed = false + canBeResolved = true + } + + notaryServerCPB { + canBeConsumed = false + canBeResolved = true + } +} + +// Dependencies for supporting tools +dependencies { + combinedWorker "net.corda:corda-combined-worker:$combinedWorkerVersion" + myPostgresJDBC "org.postgresql:postgresql:$postgresqlVersion" + notaryServerCPB("com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-server:$cordaNotaryPluginsVersion") { + artifact { + classifier = 'package' + extension = 'cpb' + } + } + + implementation "org.codehaus.groovy:groovy-json:3.0.9" +} + +// task groupings +def cordaGroup = 'csde-corda' // corda lifecycle tasks +def cordappGroup = 'csde-cordapp' // tasks to build and deploy corDapps +def queriesGroup = 'csde-queries' // tasks which query corda status +def supportingGroup = 'supporting' // tasks which should be hidden from the csde user + + +def cordaBinDir = System.getenv("CSDE_CORDA_BIN") ?: System.getProperty('user.home') + "/.corda/corda5" +def cordaCliBinDir = System.getenv("CSDE_CORDA_CLI") ?:System.getProperty('user.home') + "/.corda/cli" +def cordaJDBCDir = cordaBinDir + "/jdbcDrivers" +def cordaNotaryServerDir = cordaBinDir + "/notaryserver" +def signingCertAlias="gradle-plugin-default-key" +// Get error if this is not a autotyped object +// def signingCertFName = "$rootDir/config/gradle-plugin-default-key.pem" +def signingCertFName = rootDir.toString() + "/config/gradle-plugin-default-key.pem" +def keystoreAlias = "my-signing-key" +def keystoreFName = devEnvWorkspace + "/signingkeys.pfx" +def keystoreCertFName = devEnvWorkspace + "/signingkey1.pem" +def combiWorkerPidCacheFile = devEnvWorkspace + "/CordaPID.dat" +// todo: can we rely on the build directory always being /workflow/build? aslo, is the +// workflow directory the correct place to build the cpb to. shoudl it be the main build directory. +def workflowBuildDir = rootDir.toString() + "/workflows/build" + + +// todo: Need to read things from cordapp plugin - the cordapp names will be changed by the user +def appCpiName = 'cpi name' +def notaryCpiName = 'CSDE Notary Server CPI' + + +// todo: there should be a better way to set up these project context variables. +def projectContext = new ProjectContext(project, + cordaClusterURL.toString(), + cordaRpcUser, + cordaRpcPasswd, + devEnvWorkspace, + // todo: why is this not obtained in the groovy def's abouve - its inconsistent. + new String("${System.getProperty("java.home")}/bin"), + dbContainerName, + cordaJDBCDir, + combiWorkerPidCacheFile, + signingCertAlias, + signingCertFName, + keystoreAlias, + keystoreFName, + keystoreCertFName, + appCpiName, + notaryCpiName, + devEnvWorkspace, + cordaCliBinDir, + cordaNotaryServerDir, + workflowBuildDir, + cordaNotaryPluginsVersion +) + +def networkConfig = new NetworkConfig("config/static-network-config.json") + +def utils = new ProjectUtils() + +// Initiate workspace folder + +tasks.register('projInit') { + group = supportingGroup + doLast { + mkdir devEnvWorkspace + } +} + + +// CordaLifeCycle tasks + +def cordaLifeCycle = new CordaLifeCycleHelper(projectContext) + +tasks.register("startCorda") { + group = cordaGroup + dependsOn('getDevCordaLite', 'getPostgresJDBC') + doLast { + mkdir devEnvWorkspace + cordaLifeCycle.startCorda() + } +} + +tasks.register("stopCorda") { + group = cordaGroup + doLast { + cordaLifeCycle.stopCorda() + } +} + +tasks.register("getPostgresJDBC") { + group = supportingGroup + doLast { + copy { + from configurations.myPostgresJDBC + into "$cordaJDBCDir" + } + } +} + +tasks.register("getDevCordaLite", Copy) { + group = supportingGroup + from configurations.combinedWorker + into cordaBinDir +} + + +// Corda status queries + +def cordaStatusQueries = new CordaStatusQueries(projectContext) + + +tasks.register('listVNodes') { + group = queriesGroup + doLast { + cordaStatusQueries.listVNodes() + } +} + +tasks.register('listCPIs') { + group = queriesGroup + doLast { + cordaStatusQueries.listCPIs() + } +} + +// Build CPI tasks + +def buildCPIsHelper = new BuildCPIsHelper(projectContext, networkConfig) + +tasks.register("1-createGroupPolicy") { + group = cordappGroup + dependsOn('projInit') + doLast { + buildCPIsHelper.createGroupPolicy() + } +} + +tasks.register("getNotaryServerCPB", Copy) { + group = supportingGroup + from configurations.notaryServerCPB + into cordaNotaryServerDir +} + +tasks.register('2-createKeystore') { + group = cordappGroup + dependsOn('projInit') + doLast { + buildCPIsHelper.createKeyStore() + } +} + +tasks.register('3-buildCPIs') { + group = cordappGroup + def dependsOnTasks = subprojects.collect {it.tasks.findByName("build") } + dependsOnTasks.add('1-createGroupPolicy') + dependsOnTasks.add('2-createKeystore') + dependsOnTasks.add('getNotaryServerCPB') + dependsOn dependsOnTasks + doLast{ + buildCPIsHelper.buildCPIs() + } +} + + +// deploy CPI tasks + +def deployCPIsHelper = new DeployCPIsHelper(projectContext) + +tasks.register('4-deployCPIs') { + group = cordappGroup + dependsOn('3-buildCPIs') + doLast { + deployCPIsHelper.deployCPIs() + } +} + +// Setup VNodes tasks + +def vNodesHelper = new VNodesHelper(projectContext, networkConfig ) + +tasks.register('5-vNodeSetup') { + group = cordappGroup + dependsOn('4-deployCPIs') + doLast { + vNodesHelper.vNodesSetup() + } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java new file mode 100644 index 0000000..96140ef --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java @@ -0,0 +1,278 @@ +package com.r3.csde; + +import java.io.*; +import java.util.LinkedList; +import java.util.List; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class BuildCPIsHelper { + + public ProjectContext pc; + public ProjectUtils utils; + + public NetworkConfig config; + public BuildCPIsHelper(ProjectContext _pc, NetworkConfig _config) { + pc = _pc; + utils = new ProjectUtils(pc); + config = _config; + } + + public void createGroupPolicy() throws IOException { + + File groupPolicyFile = new File(String.format("%s/GroupPolicy.json", pc.devEnvWorkspace)); + File devnetFile = new File(pc.project.getRootDir() + "/" + config.getConfigFilePath()); + + + if (!groupPolicyFile.exists() || groupPolicyFile.lastModified() < devnetFile.lastModified()) { + + pc.out.println("createGroupPolicy: Creating a GroupPolicy"); + + List configX500Ids = config.getX500Names(); + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("mgm"); + commandList.add("groupPolicy"); + for (String id : configX500Ids) { + commandList.add("--name"); + commandList.add(id); + } + commandList.add("--endpoint-protocol=1"); + commandList.add("--endpoint=http://localhost:1080"); + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); + + // todo add exception catching + FileWriter fileWriter = new FileWriter(groupPolicyFile); + String line; + while (( line = reader.readLine()) != null){ + fileWriter.write(line + "\n"); + } + fileWriter.close(); + + } else { + pc.out.println("createPolicyTask: everything up to date; nothing to do."); + } + + } + + public void createKeyStore() throws IOException, InterruptedException { + + File keystoreFile = new File(pc.keystoreFName); + if(!keystoreFile.exists()) { + pc.out.println("createKeystore: Create a keystore"); + + generateKeyPair(); + addDefaultSigningKey(); + exportCert(); + + } else { + pc.out.println("createKeystore: keystore already created; nothing to do."); + } + + } + + private void generateKeyPair() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-genkeypair"); + cmdArray.add("-alias"); + cmdArray.add(pc.keystoreAlias); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-dname"); + cmdArray.add("CN=CPI Example - My Signing Key, O=CorpOrgCorp, L=London, C=GB"); + cmdArray.add("-keyalg"); + cmdArray.add("RSA"); + cmdArray.add("-storetype"); + cmdArray.add("pkcs12"); + cmdArray.add("-validity"); + cmdArray.add("4000"); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + private void addDefaultSigningKey() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-importcert"); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-noprompt"); + cmdArray.add("-alias"); + cmdArray.add(pc.signingCertAlias); + cmdArray.add("-file"); + cmdArray.add(pc.signingCertFName); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + } + + private void exportCert() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-exportcert"); + cmdArray.add("-rfc"); + cmdArray.add("-alias"); + cmdArray.add(pc.keystoreAlias); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-file"); + cmdArray.add(pc.keystoreCertFName); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + public void buildCPIs() throws IOException, InterruptedException, CsdeException { + createCorDappCPI(); + createNotaryCPI(); + } + + private void createCorDappCPI() throws IOException, InterruptedException, CsdeException { + + String appCPIFilePath = pc.workflowBuildDir + "/" + + pc.project.getRootProject().getName() + "-" + + pc.project.getVersion() + ".cpi"; + + File appCPIFile = new File(appCPIFilePath); + appCPIFile.delete(); + + File srcDir = new File(pc.workflowBuildDir + "/libs"); + File[] appCPBs = srcDir.listFiles(( x , name ) -> name.endsWith(".cpb")); + if (appCPBs == null) throw new CsdeException("Expecting exactly one CPB but no CPB found."); + if (appCPBs.length != 1) throw new CsdeException("Expecting exactly one CPB but more than one found."); + + pc.out.println("appCpbs:"); + pc.out.println(appCPBs[0].getAbsolutePath()); + + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("package"); + commandList.add("create-cpi"); + commandList.add("--cpb"); + commandList.add(appCPBs[0].getAbsolutePath()); + commandList.add("--group-policy"); + commandList.add(pc.devEnvWorkspace + "/GroupPolicy.json"); + commandList.add("--cpi-name"); + commandList.add(pc.appCPIName); + commandList.add("--cpi-version"); + commandList.add(pc.project.getVersion().toString()); + commandList.add("--file"); + commandList.add(appCPIFilePath); + commandList.add("--keystore"); + commandList.add(pc.devEnvWorkspace + "/signingkeys.pfx"); + commandList.add("--storepass"); + commandList.add("keystore password"); + commandList.add("--key"); + commandList.add("my-signing-key"); // todo: should be passed as context property + + + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + +// todo: work out how to capture error code better than the following code + +// BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); +// File tempOutputFile = new File(String.format("%s/tempOutput.txt", pc.devEnvWorkspace)); +// tempOutputFile.delete(); +// FileWriter fileWriter = new FileWriter(tempOutputFile); +// String line; +// while (( line = reader.readLine()) != null){ +// fileWriter.write(line + "\n"); +// } +// fileWriter.close(); + + } + + private void createNotaryCPI() throws CsdeException, IOException, InterruptedException { + + String notaryCPIFilePath = pc.workflowBuildDir + "/" + + pc.notaryCPIName.replace(' ', '-').toLowerCase() + "-" + + pc.project.getVersion() + ".cpi"; + + File notaryCPIFile = new File(notaryCPIFilePath); + notaryCPIFile.delete(); + + File srcDir = new File(pc.cordaNotaryServiceDir); + File[] notaryCPBs = srcDir.listFiles(( x , name ) -> name.endsWith(".cpb") && name.contains(pc.cordaNotaryPluginsVersion)); + if (notaryCPBs == null) throw new CsdeException("Expecting exactly one notary CPB but no CPB found."); + if (notaryCPBs.length != 1) throw new CsdeException("Expecting exactly one notary CPB but more than one found."); + + pc.out.println("notaryCpbs:"); + pc.out.println(notaryCPBs[0]); + + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("package"); + commandList.add("create-cpi"); + commandList.add("--cpb"); + commandList.add(notaryCPBs[0].getAbsolutePath()); + commandList.add("--group-policy"); + commandList.add(pc.devEnvWorkspace + "/GroupPolicy.json"); + commandList.add("--cpi-name"); + commandList.add(pc.notaryCPIName); + commandList.add("--cpi-version"); + commandList.add(pc.project.getVersion().toString()); + commandList.add("--file"); + commandList.add(notaryCPIFilePath); + commandList.add("--keystore"); + commandList.add(pc.devEnvWorkspace + "/signingkeys.pfx"); + commandList.add("--storepass"); + commandList.add("keystore password"); + commandList.add("--key"); + commandList.add("my-signing-key"); + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + // todo: this might be needed for improved logging + private void printCmdArray(LinkedList cmdArray) { + for (int i = 0; i < cmdArray.size(); i++) { + pc.out.print(cmdArray.get(i) + " "); + } + } + +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java new file mode 100644 index 0000000..daa78a6 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java @@ -0,0 +1,93 @@ +package com.r3.csde; + +import kong.unirest.Unirest; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Scanner; + +/** + * Manages Bringing corda up, testing for liveness and taking corda down + */ +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class CordaLifeCycleHelper { + + ProjectContext pc; + ProjectUtils utils; + + public CordaLifeCycleHelper(ProjectContext _pc) { + pc = _pc; + utils = new ProjectUtils(pc); + Unirest.config().verifySsl(false); + } + + public void startCorda() throws IOException { + PrintStream pidStore = new PrintStream(new FileOutputStream(pc.cordaPidCache)); + File combinedWorkerJar = pc.project.getConfigurations().getByName("combinedWorker").getSingleFile(); + + // Manual version of the command to start postgres (for reference): + // docker run -d --rm -p5432:5432 --name CSDEpostgresql -e POSTGRES_DB=cordacluster -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password postgres:latest + + new ProcessBuilder( + "docker", + "run", "-d", "--rm", + "-p", "5432:5432", + "--name", pc.dbContainerName, + "-e", "POSTGRES_DB=cordacluster", + "-e", "POSTGRES_USER=postgres", + "-e", "POSTGRES_PASSWORD=password", + "postgres:latest").start(); + + // todo: we should poll for readiness not wait 10 seconds, see https://r3-cev.atlassian.net/browse/CORE-11626 + utils.rpcWait(10000); + + ProcessBuilder procBuild = new ProcessBuilder(pc.javaBinDir + "/java", + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", + "-DsecurityMangerEnabled=false", + "-Dlog4j.configurationFile=" + pc.project.getRootDir() + "/config/log4j2.xml", + "-Dco.paralleluniverse.fibers.verifyInstrumentation=true", + "-jar", + combinedWorkerJar.toString(), + "--instance-id=0", + "-mbus.busType=DATABASE", + "-spassphrase=password", + "-ssalt=salt", + "-ddatabase.user=user", + "-ddatabase.pass=password", + "-ddatabase.jdbc.url=jdbc:postgresql://localhost:5432/cordacluster", + "-ddatabase.jdbc.directory="+pc.JDBCDir); + + procBuild.redirectErrorStream(true); + Process proc = procBuild.start(); + pidStore.print(proc.pid()); + pc.out.println("Corda Process-id="+proc.pid()); + proc.getInputStream().transferTo(pc.out); + + // todo: we should poll for readiness before completing the startCorda task, see https://r3-cev.atlassian.net/browse/CORE-11625 + } + + + public void stopCorda() throws IOException, CsdeException { + File cordaPIDFile = new File(pc.cordaPidCache); + if(cordaPIDFile.exists()) { + Scanner sc = new Scanner(cordaPIDFile); + long pid = sc.nextLong(); + pc.out.println("pid to kill=" + pid); + + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + new ProcessBuilder("Powershell", "-Command", "Stop-Process", "-Id", Long.toString(pid), "-PassThru").start(); + } else { + new ProcessBuilder("kill", "-9", Long.toString(pid)).start(); + } + + Process proc = new ProcessBuilder("docker", "stop", pc.dbContainerName).start(); + + cordaPIDFile.delete(); + } + else { + throw new CsdeException("Cannot stop the Combined worker\nCached process ID file " + pc.cordaPidCache + " missing.\nWas the combined worker not started?"); + } + } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java new file mode 100644 index 0000000..95072ba --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java @@ -0,0 +1,64 @@ +package com.r3.csde; + +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import kong.unirest.HttpResponse; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class CordaStatusQueries { + + ProjectContext pc; + public CordaStatusQueries(ProjectContext _pc){ pc = _pc; } + + + public HttpResponse getVNodeInfo() { + Unirest.config().verifySsl(false); + return Unirest.get(pc.baseURL + "/api/v1/virtualnode/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + } + public void listVNodesVerbose() { + HttpResponse vnodeResponse = getVNodeInfo(); + pc.out.println("VNodes:\n" + vnodeResponse.getBody().toPrettyString()); + } + + // X500Name, shorthash, cpiname + public void listVNodes() { + HttpResponse vnodeResponse = getVNodeInfo(); + + JSONArray virtualNodesJson = (JSONArray) vnodeResponse.getBody().getObject().get("virtualNodes"); + pc.out.println("X500 Name\tHolding identity short hash\tCPI Name"); + for(Object o: virtualNodesJson){ + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("holdingIdentity"); + JSONObject cpiObj = ((JSONObject) o).getJSONObject("cpiIdentifier"); + pc.out.print("\"" + idObj.get("x500Name") + "\""); + pc.out.print("\t\"" + idObj.get("shortHash") + "\""); + pc.out.println("\t\"" + cpiObj.get("cpiName") + "\""); + } + } + } + + public HttpResponse getCpiInfo() { + Unirest.config().verifySsl(false); + return Unirest.get(pc.baseURL + "/api/v1/cpi/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + } + + public void listCPIs() { + HttpResponse cpiResponse = getCpiInfo(); + JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); + + for(Object o: jArray){ + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("id"); + pc.out.print("cpiName=" + idObj.get("cpiName")); + pc.out.println(", cpiVersion=" + idObj.get("cpiVersion")); + } + } + } + +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CsdeException.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CsdeException.java new file mode 100644 index 0000000..72f8fea --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CsdeException.java @@ -0,0 +1,10 @@ +package com.r3.csde; + +public class CsdeException extends Exception { + public CsdeException(String message, Throwable cause) { + super(message, cause); + } + public CsdeException(String message){ + super(message); + } +} \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java new file mode 100644 index 0000000..fe1362b --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java @@ -0,0 +1,187 @@ +package com.r3.csde; + +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import kong.unirest.HttpResponse; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintStream; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_OK; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class DeployCPIsHelper { + + public DeployCPIsHelper() { + } + ProjectContext pc; + CordaStatusQueries queries; + ProjectUtils utils; + + public DeployCPIsHelper(ProjectContext _pc) { + pc = _pc; + queries = new CordaStatusQueries(pc); + utils = new ProjectUtils(pc); + } + + public void deployCPIs() throws FileNotFoundException, CsdeException{ + + uploadCertificate(pc.signingCertAlias, pc.signingCertFName); + uploadCertificate(pc.keystoreAlias, pc.keystoreCertFName); + + // todo: make consistent with other string building code - remove String.format + String appCPILocation = String.format("%s/%s-%s.cpi", + pc.workflowBuildDir, + pc.project.getName(), + pc.project.getVersion()); + deployCPI(appCPILocation, pc.appCPIName,pc.project.getVersion().toString()); + + String notaryCPILocation = String.format("%s/%s-%s.cpi", + pc.workflowBuildDir, + pc.notaryCPIName.replace(' ','-').toLowerCase(), + pc.project.getVersion()); + deployCPI(notaryCPILocation, + pc.notaryCPIName, + pc.project.getVersion().toString(), + "-NotaryServer" ); + + } + + public void uploadCertificate(String certAlias, String certFName) { + Unirest.config().verifySsl(false); + HttpResponse uploadResponse = Unirest.put(pc.baseURL + "/api/v1/certificates/cluster/code-signer") + .field("alias", certAlias) + .field("certificate", new File(certFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + pc.out.println("Certificate/key upload, alias "+certAlias+" certificate/key file "+certFName); + pc.out.println(uploadResponse.getBody().toPrettyString()); + } + + public void forceuploadCPI(String cpiFName) throws FileNotFoundException, CsdeException { + forceuploadCPI(cpiFName, ""); + } + + public void forceuploadCPI(String cpiFName, String uploadStatusQualifier) throws FileNotFoundException, CsdeException { + Unirest.config().verifySsl(false); + HttpResponse jsonResponse = Unirest.post(pc.baseURL + "/api/v1/maintenance/virtualnode/forcecpiupload/") + .field("upload", new File(cpiFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if(jsonResponse.getStatus() == HTTP_OK) { + String id = (String) jsonResponse.getBody().getObject().get("id"); + pc.out.println("get id:\n" +id); + HttpResponse statusResponse = uploadStatus(id); + + if (statusResponse.getStatus() == HTTP_OK) { + PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream( + pc.CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json" ))); + cpiUploadStatus.print(statusResponse.getBody()); + pc.out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); + } else { + utils.reportError(statusResponse); + } + } + else { + utils.reportError(jsonResponse); + } + } + + private boolean uploadStatusRetry(HttpResponse response) { + int status = response.getStatus(); + JsonNode body = response.getBody(); + // Do not retry on success // todo: need to think through the possible outcomes here - what if the bodyTitle is null, it won't retry + if(status == HTTP_OK) { + // Keep retrying until we get "OK" may move through "Validating upload", "Persisting CPI" + return !(body.getObject().get("status").equals("OK")); + } + else if (status == HTTP_BAD_REQUEST){ + String bodyTitle = response.getBody().getObject().getString("title"); + return bodyTitle != null && bodyTitle.matches("No such requestId=[-0-9a-f]+"); + } + return false; + } + + public HttpResponse uploadStatus(String requestId) { + HttpResponse statusResponse = null; + do { + utils.rpcWait(1000); + statusResponse = Unirest + .get(pc.baseURL + "/api/v1/cpi/status/" + requestId + "/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + pc.out.println("Upload status="+statusResponse.getStatus()+", status query response:\n"+statusResponse.getBody().toPrettyString()); + } + while(uploadStatusRetry(statusResponse)); + + return statusResponse; + } + + public void deployCPI(String cpiFName, String cpiName, String cpiVersion) throws FileNotFoundException, CsdeException { + deployCPI(cpiFName, cpiName, cpiVersion, ""); + } + + public void deployCPI(String cpiFName, + String cpiName, + String cpiVersion, + String uploadStatusQualifier) throws FileNotFoundException, CsdeException { + // todo: where is the primary instance declared? + Unirest.config().verifySsl(false); + + HttpResponse cpiResponse = queries.getCpiInfo(); + JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); + + int matches = 0; + for(Object o: jArray.toList() ) { + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("id"); + if((idObj.get("cpiName").toString().equals(cpiName) + && idObj.get("cpiVersion").toString().equals(cpiVersion))) { + matches++; + } + } + } + pc.out.println("Matching CPIS="+matches); + + if(matches == 0) { + HttpResponse uploadResponse = Unirest.post(pc.baseURL + "/api/v1/cpi/") + .field("upload", new File(cpiFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + JsonNode body = uploadResponse.getBody(); + + int status = uploadResponse.getStatus(); + + pc.out.println("Upload Status:" + status); + pc.out.println("Pretty print the body\n" + body.toPrettyString()); + + // We expect the id field to be a string. + if (status == HTTP_OK) { + String id = (String) body.getObject().get("id"); + pc.out.println("get id:\n" + id); + + HttpResponse statusResponse = uploadStatus(id); + if (statusResponse.getStatus() == HTTP_OK) { + PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream( + pc.CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json" ))); + cpiUploadStatus.print(statusResponse.getBody()); + pc.out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); + } else { + utils.reportError(statusResponse); + } + } else { + utils.reportError(uploadResponse); + } + } + else { + pc.out.println("CPI already uploaded doing a 'force' upload."); + forceuploadCPI(cpiFName, uploadStatusQualifier); + } + } + +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java new file mode 100644 index 0000000..9b3f0ca --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java @@ -0,0 +1,40 @@ +package com.r3.csde; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.FileInputStream; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * This class reads the network config from the json file and makes it available as a list of VNodes. + */ +public class NetworkConfig { + + private List vNodes; + private String configFilePath; + + public NetworkConfig(String _configFilePath) throws CsdeException { + configFilePath = _configFilePath; + + ObjectMapper mapper = new ObjectMapper(); + try { + FileInputStream in = new FileInputStream(configFilePath); + vNodes = mapper.readValue(in, new TypeReference>() { + }); + } catch (Exception e) { + throw new CsdeException("Failed to read static network configuration file, with exception: " + e); + } + } + + String getConfigFilePath() { return configFilePath; } + + List getVNodes() { return vNodes; } + + List getX500Names() { + return vNodes.stream().map(vn -> vn.getX500Name()).collect(Collectors.toList()); + } + +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectContext.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectContext.java new file mode 100644 index 0000000..0e84567 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectContext.java @@ -0,0 +1,84 @@ +package com.r3.csde; + +import org.gradle.api.Project; +import java.io.PrintStream; +import java.util.Map; + +public class ProjectContext { + Project project; + String baseURL = "https://localhost:8888"; + String rpcUser = "admin"; + String rpcPasswd = "admin"; + String workspaceDir = "workspace"; + int retryWaitMs = 1000; + PrintStream out = System.out; + String CPIUploadStatusBaseName = "CPIFileStatus.json"; + String NotaryCPIUploadBaseName = "CPIFileStatus-NotaryServer.json"; + String CPIUploadStatusFName; + String NotaryCPIUploadStatusFName; + String javaBinDir; + String cordaPidCache = "CordaPIDCache.dat"; + String dbContainerName; + String JDBCDir; + String combinedWorkerBinRe; + Map notaryRepresentatives = null; + String signingCertAlias; + String signingCertFName; + String keystoreAlias; + String keystoreFName; + String keystoreCertFName; + String appCPIName; + String notaryCPIName; + String devEnvWorkspace; + String cordaCliBinDir; + String cordaNotaryServiceDir; + String workflowBuildDir; + String cordaNotaryPluginsVersion; + + public ProjectContext (Project inProject, + String inBaseUrl, + String inRpcUser, + String inRpcPasswd, + String inWorkspaceDir, + String inJavaBinDir, + String inDbContainerName, + String inJDBCDir, + String inCordaPidCache, + String inSigningCertAlias, + String inSigningCertFName, + String inKeystoreAlias, + String inKeystoreFName, + String inKeystoreCertFName, + String inAppCPIName, + String inNotaryCPIName, + String inDevEnvWorkspace, + String inCordaCLiBinDir, + String inCordaNotaryServiceDir, + String inWorkflowBuildDir, + String inCordaNotaryPluginsVersion + ) { + project = inProject; + baseURL = inBaseUrl; + rpcUser = inRpcUser; + rpcPasswd = inRpcPasswd; + workspaceDir = inWorkspaceDir; + javaBinDir = inJavaBinDir; + cordaPidCache = inCordaPidCache; + dbContainerName = inDbContainerName; + JDBCDir = inJDBCDir; + CPIUploadStatusFName = workspaceDir + "/" + CPIUploadStatusBaseName; + NotaryCPIUploadStatusFName = workspaceDir + "/" + NotaryCPIUploadBaseName; + signingCertAlias = inSigningCertAlias; + signingCertFName = inSigningCertFName; + keystoreAlias = inKeystoreAlias; + keystoreFName = inKeystoreFName; + keystoreCertFName = inKeystoreCertFName; + appCPIName = inAppCPIName; + notaryCPIName = inNotaryCPIName; + devEnvWorkspace = inDevEnvWorkspace; + cordaCliBinDir = inCordaCLiBinDir; + cordaNotaryServiceDir = inCordaNotaryServiceDir; + workflowBuildDir = inWorkflowBuildDir; + cordaNotaryPluginsVersion = inCordaNotaryPluginsVersion; + } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java new file mode 100644 index 0000000..dbc3dd8 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java @@ -0,0 +1,36 @@ +package com.r3.csde; + +import kong.unirest.HttpResponse; +import kong.unirest.JsonNode; + +import static java.lang.Thread.sleep; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class ProjectUtils { + + ProjectContext pc; + + ProjectUtils(ProjectContext _pc) { + pc = _pc; + } + + void rpcWait(int millis) { + try { + sleep(millis); + } + catch(InterruptedException e) { + throw new UnsupportedOperationException("Interrupts not supported.", e); + } + } + + public void reportError(HttpResponse response) throws CsdeException { + + pc.out.println("*** *** ***"); + pc.out.println("Unexpected response from Corda"); + pc.out.println("Status="+ response.getStatus()); + pc.out.println("*** Headers ***\n"+ response.getHeaders()); + pc.out.println("*** Body ***\n"+ response.getBody()); + pc.out.println("*** *** ***"); + throw new CsdeException("Error: unexpected response from Corda."); + } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNode.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNode.java new file mode 100644 index 0000000..d77d85c --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNode.java @@ -0,0 +1,25 @@ +package com.r3.csde; + + +/** + * This class is a representation of a Vnode used to express the vNodes required on the network. + */ + +public class VNode { + private String x500Name; + private String cpi; + + private String serviceX500Name; + + public VNode() { } + + public String getX500Name(){ return x500Name; } + public void setX500Name(String _x500Name) { x500Name = _x500Name; } + + public String getCpi() { return cpi; } + public void setCpi(String _cpi) { cpi = _cpi; } + + public String getServiceX500Name() { return serviceX500Name; } + public void setServiceX500Name(String _name) { serviceX500Name = _name; } + +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java new file mode 100644 index 0000000..bafdc75 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java @@ -0,0 +1,288 @@ +package com.r3.csde; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.r3.csde.dtos.*; +import kong.unirest.HttpResponse; +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import java.io.FileInputStream; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import static java.net.HttpURLConnection.HTTP_OK; + +/** + * The VNodesHelper class is used to create and register the Vnodes specified in the static-network-config.json file. + */ + +public class VNodesHelper { + private ProjectContext pc; + private ProjectUtils utils; + private NetworkConfig config; + private ObjectMapper mapper; + + public VNodesHelper(ProjectContext _pc, NetworkConfig _config) { + pc = _pc; + utils = new ProjectUtils(pc); + config = _config; + Unirest.config().verifySsl(false); + mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + /** + * Entry point for setting up vnodes, called from the 5-vNodeSetup task in csde.gradle + */ + public void vNodesSetup() throws CsdeException { + List requiredNodes = config.getVNodes(); + createVNodes(requiredNodes); + registerVNodes(requiredNodes); + } + + /** + * Creates vnodes specified in the config if they don't already exist. + * @param requiredNodes represents the list of VNodes as specified in the network Config json file (static-network-config.json) + */ + private void createVNodes(List requiredNodes) throws CsdeException { + + // Get existing Nodes. + List existingVNodes = getExistingNodes(); + + // Check if each required vnode already exist, if not create it. + for (VNode vn : requiredNodes) { + // Match on x500 and cpi name + List matches = existingVNodes + .stream() + .filter(existing -> + existing.getHoldingIdentity().getX500Name().equals( vn.getX500Name()) && + existing.getCpiIdentifier().getCpiName().equals(vn.getCpi())) + .collect(Collectors.toList()); + + if (matches.size() == 0) { + createVNode(vn); + } + } + } + + /** + * Gets a list of the virtual nodes which have already been created. + * @return a list of the virtual nodes which have already been created. + */ + private List getExistingNodes () throws CsdeException { + + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/virtualnode") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Failed to get Existing vNodes, response status: "+ response.getStatus()); + } + + try { + return mapper.readValue(response.getBody().toString(), VirtualNodesDTO.class).getVirtualNodes(); + } catch (Exception e){ + throw new CsdeException("Failed to get Existing vNodes with exception: " + e); + } + } + + /** + * Creates a vnode on the Corda cluster from the VNode info. + * @param vNode represents a virtual node using VNode Class. + */ + private void createVNode(VNode vNode) throws CsdeException { + + pc.out.println("Creating virtual node for "+ vNode.getX500Name()); + + // Reads the current CPIFileChecksum value and checks it has been uploaded. + String cpiCheckSum = getCpiCheckSum(vNode); + if (!checkCpiUploaded(cpiCheckSum)) throw new CsdeException("Cpi " + cpiCheckSum + " not uploaded."); + + // Creates the vnode on Cluster + HttpResponse response = Unirest.post(pc.baseURL + "/api/v1/virtualnode") + .body("{ \"request\" : { \"cpiFileChecksum\": \"" + cpiCheckSum + "\", \"x500Name\": \"" + vNode.getX500Name() + "\" } }") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Creation of virtual node failed with response status: " + response.getStatus()); + } + } + + /** + * Reads the latest CPI checksums from file. + * @param vNode represents a virtual node using VNode Class. + */ + private String getCpiCheckSum(VNode vNode) throws CsdeException { + + try { + String file = (vNode.getServiceX500Name() == null) ? pc.CPIUploadStatusFName : pc.NotaryCPIUploadStatusFName; + FileInputStream in = new FileInputStream(file); + CPIFileStatusDTO statusDTO = mapper.readValue(in, CPIFileStatusDTO.class); + return statusDTO.getCpiFileChecksum().toString(); + } catch (Exception e) { + throw new CsdeException("Failed to read CPI checksum from file, with error: " + e); + } + } + + private boolean checkCpiUploaded(String cpiCheckSum) throws CsdeException { + + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/cpi") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Failed to check cpis, response status: " + response.getStatus()); + } + + try { + GetCPIsResponseDTO cpisResponse = mapper.readValue( + response.getBody().toString(), GetCPIsResponseDTO.class); + + for (CpiMetadataDTO cpi: cpisResponse.getCpis()) { + if (Objects.equals(cpi.getCpiFileChecksum(), cpiCheckSum)) { + return true; + } + } + // Returns false if no cpis were returned or the cpiCheckSum didnt' match ay results. + return false; + } catch (Exception e) { + throw new CsdeException("Failed to check cpis with exception: " + e); + } + } + + /** + * Checks if the required virtual nodes have been registered and if not registers them. + * @param requiredNodes represents the list of VNodes as specified in the network Config json file (static-network-config.json) + */ + private void registerVNodes(List requiredNodes) throws CsdeException { + + // There appears to be a delay between the successful post /virtualnodes synchronous call and the + // vnodes being returned in the GET /virtualnodes call. Putting a thread wait here as a quick fix + // as this will move to async mechanism post beta2. + utils.rpcWait(2000); + List existingVNodes = getExistingNodes(); + + for (VNode vn: requiredNodes) { + // Match on x500 and cpi name + List matches = existingVNodes + .stream() + .filter(existing -> + existing.getHoldingIdentity().getX500Name().equals( vn.getX500Name()) && + existing.getCpiIdentifier().getCpiName().equals(vn.getCpi())) + .collect(Collectors.toList()); + + if (matches.size() == 0) { + throw new CsdeException("Registration failed because virtual node for '" + vn.getX500Name() + "' not found."); + } else if (matches.size() >1 ) { + throw new CsdeException(("Registration failed because more than virtual node for '" + vn.getX500Name() + "'")); + } + + String shortHash = matches.get(0).getHoldingIdentity().getShortHash(); + + if (!checkVNodeIsRegistered(shortHash)) { + registerVnode(vn, shortHash); + } + } + } + + /** + * Registers a virtual node. + * @param vnode represents the vnode to be registered. + * @param shortHash the shortHash of the virtual node to register. + */ + private void registerVnode(VNode vnode, String shortHash) throws CsdeException { + + pc.out.println("Registering vNode: " + vnode.getX500Name() + " with shortHash: " + shortHash); + + // Configure the registration body (notary vs non notary) + String registrationBody; + if (vnode.getServiceX500Name() == null) { + registrationBody = + "{ \"action\" : \"requestJoin\"," + + " \"context\" : {" + + " \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\" } }"; + } else { + registrationBody = + "{ \"action\" : \"requestJoin\"," + + " \"context\" : {" + + " \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\", " + + " \"corda.roles.0\" : \"notary\", " + + " \"corda.notary.service.name\" : \"" + vnode.getServiceX500Name() + "\", " + + " \"corda.notary.service.plugin\" : \"net.corda.notary.NonValidatingNotary\" } }"; + } + + HttpResponse response = Unirest.post(pc.baseURL + "/api/v1/membership/" + shortHash) + .body(registrationBody) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() == HTTP_OK) { + pc.out.println("Membership requested for node " + shortHash); + } else { + throw new CsdeException("Failed to register virtual node " + shortHash + ", response status: " + response.getStatus() ); + } + + // wait until Vnode registered + pollForRegistration(shortHash, 30000, 1000); + + } + + /** + * Checks if a virtual node with given shortHash has been registered + * @param shortHash shortHash of the node which is being checked. + * @return returns true if the vnode is registered + */ + private boolean checkVNodeIsRegistered(String shortHash) throws CsdeException { + + // Queries registration status for vnode. + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/membership/" + shortHash) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) + throw new CsdeException("Failed to check registration status for virtual node '" + shortHash + + "' response status: " + response.getStatus()); + + try { + // If the response body is not empty check all previous requests for an "APPROVED" + if (!response.getBody().getArray().isEmpty()) { + List requests = mapper.readValue( + response.getBody().toString(), new TypeReference<>() { + }); + for (RegistrationRequestProgressDTO request : requests) { + if (Objects.equals(request.getRegistrationStatus(), "APPROVED")) { + return true; + } + } + } + // Returns false if array was empty or "APPROVED" wasn't found + return false; + + } catch (Exception e) { + throw new CsdeException("Failed to check registration status for " + shortHash + + " with exception " + e); + } + } + + /** + * Polls for registration of a vnode + * @param shortHash short hash of the node which is being poled for + * @param duration the number of milliseconds before the pollign times out + * @param cooldown the number of milliseconds in between poll attempts. + */ + private void pollForRegistration(String shortHash, int duration, int cooldown) throws CsdeException { + + int timer = 0; + while (timer < duration ) { + if (checkVNodeIsRegistered(shortHash)){ + pc.out.println("Vnode " + shortHash +" registered."); + return; + } + utils.rpcWait(cooldown); + timer += cooldown; + } + throw new CsdeException("Vnode " + shortHash + " failed to register in " + duration + " milliseconds"); + } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java new file mode 100644 index 0000000..1c6e118 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java @@ -0,0 +1,16 @@ +package com.r3.csde.dtos; + +public class CPIFileStatusDTO { + private String status; + private String cpiFileChecksum; + + public CPIFileStatusDTO() {} + + public String getStatus() { return status; } + + public void setStatus(String status) { this.status = status; } + + public String getCpiFileChecksum() { return cpiFileChecksum; } + + public void setCpiFileChecksum(String cpiFileChecksum) { this.cpiFileChecksum = cpiFileChecksum; } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java new file mode 100644 index 0000000..e5d5dde --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java @@ -0,0 +1,23 @@ +package com.r3.csde.dtos; + +public class CpiIdentifierDTO { + + // Note, these DTOs don't cover all returned values, just the ones required for CSDE + private String cpiName; + private String cpiVersion; + private String signerSummaryHash; + + public CpiIdentifierDTO() { } + + public String getCpiName() { return cpiName; } + + public void setCpiName(String cpiName) { this.cpiName = cpiName; } + + public String getCpiVersion() { return cpiVersion; } + + public void setCpiVersion(String cpiVersion) { this.cpiVersion = cpiVersion; } + + public String getSignerSummaryHash() { return signerSummaryHash; } + + public void setSignerSummaryHash(String signerSummaryHash) { this.signerSummaryHash = signerSummaryHash; } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java new file mode 100644 index 0000000..ef89c7d --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class CpiMetadataDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String cpiFileChecksum; + private CpiIdentifierDTO id; + + public CpiMetadataDTO() {} + + public String getCpiFileChecksum() { return cpiFileChecksum; } + + public void setCpiFileChecksum(String cpiFileChecksum) { this.cpiFileChecksum = cpiFileChecksum; } + + public CpiIdentifierDTO getId() { return id; } + + public void setId(CpiIdentifierDTO id) { this.id = id; } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java new file mode 100644 index 0000000..a16e9a1 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java @@ -0,0 +1,14 @@ +package com.r3.csde.dtos; + +import java.util.List; + +public class GetCPIsResponseDTO { + + private List cpis; + + public GetCPIsResponseDTO() {} + + public List getCpis() { return cpis; } + + public void setCpis(List cpis) { this.cpis = cpis; } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java new file mode 100644 index 0000000..48c12c0 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java @@ -0,0 +1,27 @@ +package com.r3.csde.dtos; + +public class HoldingIdentityDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String fullHash; + private String groupId; + private String shortHash; + private String x500Name; + + public HoldingIdentityDTO() {} + + public String getFullHash() { return fullHash; } + + public void setFullHash(String fullHash) { this.fullHash = fullHash; } + + public String getGroupId() { return groupId; } + + public void setGroupID(String groupID) { this.groupId = groupId; } + + public String getShortHash() { return shortHash; } + + public void setShortHash(String shortHash) { this.shortHash = shortHash; } + + public String getX500Name() { return x500Name; } + + public void setX500Name(String x500Name) { this.x500Name = x500Name; } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java new file mode 100644 index 0000000..b3e63b0 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class RegistrationRequestProgressDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String registrationStatus; + private String reason; + + public RegistrationRequestProgressDTO() {} + + public String getRegistrationStatus() { return registrationStatus; } + + public void setRegistrationStatus(String registrationStatus) { this.registrationStatus = registrationStatus; } + + public String getReason() { return reason; } + + public void setReason(String reason) { this.reason = reason; } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java new file mode 100644 index 0000000..152cf3e --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class VirtualNodeInfoDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private HoldingIdentityDTO holdingIdentity; + private CpiIdentifierDTO cpiIdentifier; + + public VirtualNodeInfoDTO() {} + + public HoldingIdentityDTO getHoldingIdentity() { return holdingIdentity; } + + public void setHoldingIdentity(HoldingIdentityDTO holdingIdentity) { this.holdingIdentity = holdingIdentity; } + + public CpiIdentifierDTO getCpiIdentifier() { return cpiIdentifier; } + + public void setCpiIdentifier(CpiIdentifierDTO cpiIdentifier) { this.cpiIdentifier = cpiIdentifier; } +} diff --git a/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java new file mode 100644 index 0000000..86966b7 --- /dev/null +++ b/java-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java @@ -0,0 +1,14 @@ +package com.r3.csde.dtos; + +import java.util.List; + +public class VirtualNodesDTO { + + private List virtualNodes; + + public VirtualNodesDTO() {} + + public List getVirtualNodes() { return virtualNodes; } + + public void setVirtualNodes(List virtualNodes) { this.virtualNodes = virtualNodes; } +} diff --git a/java-samples/mgm-dynamic-network/clean.sh b/java-samples/mgm-dynamic-network/clean.sh new file mode 100644 index 0000000..f6f6851 --- /dev/null +++ b/java-samples/mgm-dynamic-network/clean.sh @@ -0,0 +1,15 @@ +#!/bin/sh +rm -rf ./workspace +rm -rf ./logs + +echo "---Clean Up Env---" +WORK_DIR=./register-mgm +cd $WORK_DIR + +rm signingkey1.pem +rm signingkeys.pfx +rm mgm.cpi +rm request1.csr +rm gradle-plugin-default-key.pem + +rm -rf register-member \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/config/gradle-plugin-default-key.pem b/java-samples/mgm-dynamic-network/config/gradle-plugin-default-key.pem new file mode 100644 index 0000000..5294bbd --- /dev/null +++ b/java-samples/mgm-dynamic-network/config/gradle-plugin-default-key.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7zCCAZOgAwIBAgIEFyV7dzAMBggqhkjOPQQDAgUAMFsxCzAJBgNVBAYTAkdC +MQ8wDQYDVQQHDAZMb25kb24xDjAMBgNVBAoMBUNvcmRhMQswCQYDVQQLDAJSMzEe +MBwGA1UEAwwVQ29yZGEgRGV2IENvZGUgU2lnbmVyMB4XDTIwMDYyNTE4NTI1NFoX +DTMwMDYyMzE4NTI1NFowWzELMAkGA1UEBhMCR0IxDzANBgNVBAcTBkxvbmRvbjEO +MAwGA1UEChMFQ29yZGExCzAJBgNVBAsTAlIzMR4wHAYDVQQDExVDb3JkYSBEZXYg +Q29kZSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQDjSJtzQ+ldDFt +pHiqdSJebOGPZcvZbmC/PIJRsZZUF1bl3PfMqyG3EmAe0CeFAfLzPQtf2qTAnmJj +lGTkkQhxo0MwQTATBgNVHSUEDDAKBggrBgEFBQcDAzALBgNVHQ8EBAMCB4AwHQYD +VR0OBBYEFLMkL2nlYRLvgZZq7GIIqbe4df4pMAwGCCqGSM49BAMCBQADSAAwRQIh +ALB0ipx6EplT1fbUKqgc7rjH+pV1RQ4oKF+TkfjPdxnAAiArBdAI15uI70wf+xlL +zU+Rc5yMtcOY4/moZUq36r0Ilg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/config/log4j2.xml b/java-samples/mgm-dynamic-network/config/log4j2.xml new file mode 100644 index 0000000..909222c --- /dev/null +++ b/java-samples/mgm-dynamic-network/config/log4j2.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/config/static-network-config.json b/java-samples/mgm-dynamic-network/config/static-network-config.json new file mode 100644 index 0000000..9adde9b --- /dev/null +++ b/java-samples/mgm-dynamic-network/config/static-network-config.json @@ -0,0 +1,23 @@ +[ + { + "x500Name" : "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Dave, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "CSDE Notary Server CPI", + "serviceX500Name": "CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB" + } +] diff --git a/java-samples/mgm-dynamic-network/contracts/build.gradle b/java-samples/mgm-dynamic-network/contracts/build.gradle new file mode 100644 index 0000000..52ca6f0 --- /dev/null +++ b/java-samples/mgm-dynamic-network/contracts/build.gradle @@ -0,0 +1,88 @@ + +plugins { + // Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well. + // These extend existing build environment so that CPB and CPK files can be built. + // This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp + // required by Corda. + id 'net.corda.plugins.cordapp-cpb2' + id 'org.jetbrains.kotlin.jvm' + id 'maven-publish' +} + +// Declare dependencies for the modules we will use. +// A cordaProvided declaration is required for anything that we use that the Corda API provides. +// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on. +dependencies { + + cordaProvided 'org.jetbrains.kotlin:kotlin-osgi-bundle' + + // Declare a "platform" so that we use the correct set of dependency versions for the version of the + // Corda API specified. + cordaProvided platform("net.corda:corda-api:$cordaApiVersion") + + // If using transistive dependencies this will provide most of Corda-API: + // cordaProvided 'net.corda:corda-application' + + // Alternatively we can explicitly specify all our Corda-API dependencies: + cordaProvided 'net.corda:corda-base' + cordaProvided 'net.corda:corda-application' + cordaProvided 'net.corda:corda-crypto' + cordaProvided 'net.corda:corda-membership' + // cordaProvided 'net.corda:corda-persistence' + cordaProvided 'net.corda:corda-serialization' + cordaProvided 'net.corda:corda-ledger-utxo' + cordaProvided 'net.corda:corda-ledger-consensual' + + // CorDapps that use the UTXO ledger must include at least one notary client plugin + cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion" + + // The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration. + cordaProvided 'org.slf4j:slf4j-api' + + // This are shared so should be here. + // Dependencies Required By Test Tooling + // Todo: these are commented out as the simulator UTXO work has not been merged into Gecko yet. +// testImplementation "net.corda:corda-simulator-api:$simulatorVersion" +// testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion" + + // 3rd party libraries + // Required + testImplementation "org.slf4j:slf4j-simple:2.0.0" + testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Optional but used by exmaple tests. + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" +} + +// The CordApp section. +// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp. +// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s +// subproject. +// This is required by the corda plugins to build the CorDapp. +cordapp { + // "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred + // and earliest versions of the Corda platform that the CorDapp will run on respectively. + // Enforced versioning has not implemented yet so we need to pass in a dummy value for now. + // The platform version will correspond to and be roughly equivalent to the Corda API version. + targetPlatformVersion = platformVersion.toInteger() + minimumPlatformVersion = platformVersion.toInteger() + + // The cordapp section contains either a workflow or contract subsection depending on the type of component. + // Declares the type and metadata of the CPK (this CPB has one CPK). + contract { + name "ContractsModuleNameHere" + versionId 1 + vendor "VendorNameHere" + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} diff --git a/java-samples/mgm-dynamic-network/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java b/java-samples/mgm-dynamic-network/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java new file mode 100644 index 0000000..04a289e --- /dev/null +++ b/java-samples/mgm-dynamic-network/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java @@ -0,0 +1,55 @@ +package com.r3.developers.csdetemplate.utxoexample.contracts; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.ledger.utxo.Command; +import net.corda.v5.ledger.utxo.Contract; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class ChatContract implements Contract { + + private final static Logger log = LoggerFactory.getLogger(ChatContract.class); + + public static class Create implements Command { } + public static class Update implements Command { } + + @Override + public void verify(UtxoLedgerTransaction transaction) { + + requireThat( transaction.getCommands().size() == 1, "Require a single command."); + Command command = transaction.getCommands().get(0); + + ChatState output = transaction.getOutputStates(ChatState.class).get(0); + + requireThat(output.getParticipants().size() == 2, "The output state should have two and only two participants."); + + if(command.getClass() == Create.class) { + requireThat(transaction.getInputContractStates().isEmpty(), "When command is Create there should be no input states."); + requireThat(transaction.getOutputContractStates().size() == 1, "When command is Create there should be one and only one output state."); + } + else if(command.getClass() == Update.class) { + requireThat(transaction.getInputContractStates().size() == 1, "When command is Update there should be one and only one input state."); + requireThat(transaction.getOutputContractStates().size() == 1, "When command is Update there should be one and only one output state."); + + ChatState input = transaction.getInputStates(ChatState.class).get(0); + requireThat(input.getId().equals(output.getId()), "When command is Update id must not change."); + requireThat(input.getChatName().equals(output.getChatName()), "When command is Update chatName must not change."); + requireThat( + input.getParticipants().containsAll(output.getParticipants()) && + output.getParticipants().containsAll(input.getParticipants()), + "When command is Update participants must not change."); + } + else { + throw new CordaRuntimeException("Unsupported command"); + } + } + + private void requireThat(boolean asserted, String errorMessage) { + if(!asserted) { + throw new CordaRuntimeException("Failed requirement: " + errorMessage); + } + } +} diff --git a/java-samples/mgm-dynamic-network/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java b/java-samples/mgm-dynamic-network/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java new file mode 100644 index 0000000..bc85e7f --- /dev/null +++ b/java-samples/mgm-dynamic-network/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java @@ -0,0 +1,55 @@ +package com.r3.developers.csdetemplate.utxoexample.states; + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract; +import net.corda.v5.base.annotations.ConstructorForDeserialization; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.ledger.utxo.BelongsToContract; +import net.corda.v5.ledger.utxo.ContractState; + +import java.security.PublicKey; +import java.util.*; + +@BelongsToContract(ChatContract.class) +public class ChatState implements ContractState { + + private UUID id; + private String chatName; + private MemberX500Name messageFrom; + private String message; + public List participants; + + // Allows serialisation and to use a specified UUID. + @ConstructorForDeserialization + public ChatState(UUID id, + String chatName, + MemberX500Name messageFrom, + String message, + List participants) { + this.id = id; + this.chatName = chatName; + this.messageFrom = messageFrom; + this.message = message; + this.participants = participants; + } + + public UUID getId() { + return id; + } + public String getChatName() { + return chatName; + } + public MemberX500Name getMessageFrom() { + return messageFrom; + } + public String getMessage() { + return message; + } + + public List getParticipants() { + return participants; + } + + public ChatState updateMessage(MemberX500Name name, String message) { + return new ChatState(id, chatName, name, message, participants); + } +} \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/gradle.properties b/java-samples/mgm-dynamic-network/gradle.properties new file mode 100644 index 0000000..e2ceeaa --- /dev/null +++ b/java-samples/mgm-dynamic-network/gradle.properties @@ -0,0 +1,40 @@ +kotlin.code.style=official + +# Specify the version of the Corda-API to use. +# This needs to match the version supported by the Corda Cluster the CorDapp will run on. +cordaApiVersion=5.0.0.665-Gecko1.0 + +# Settings For Development Utilities +combinedWorkerVersion=5.0.0.0-Gecko1.0 +simulatorVersion=5.0.0.0-Gecko1.0 + +# Specify the version of the notary plugins to use. +# Currently packaged as part of corda-runtime-os, so should be set to a corda-runtime-os version. +cordaNotaryPluginsVersion=5.0.0.0-Gecko1.0 + +# Specify the version of the cordapp-cpb and cordapp-cpk plugins +cordaPluginsVersion=7.0.1 + +# For the time being this just needs to be set to a dummy value. +platformVersion = 999 + +# Version of Kotlin to use. +# We recommend using a version close to that used by Corda-API. +kotlinVersion = 1.7.21 + +# Do not use default dependencies. +kotlin.stdlib.default.dependency=false + +# Test Tooling Dependency Versions +junitVersion = 5.8.2 +mockitoKotlinVersion=4.0.0 +mockitoVersion=4.6.1 +hamcrestVersion=2.2 + +postgresqlVersion=42.4.3 + +cordaClusterURL=https://localhost:8888 +cordaRpcUser=admin +cordaRpcPasswd=admin +devEnvWorkspace=workspace +dbContainerName=CSDEpostgresql \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.jar b/java-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/java-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.jar differ diff --git a/java-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.properties b/java-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5ec4b8e --- /dev/null +++ b/java-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java-samples/mgm-dynamic-network/gradlew b/java-samples/mgm-dynamic-network/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/java-samples/mgm-dynamic-network/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/java-samples/mgm-dynamic-network/gradlew.bat b/java-samples/mgm-dynamic-network/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/java-samples/mgm-dynamic-network/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-samples/mgm-dynamic-network/register-mgm/notary.cpb b/java-samples/mgm-dynamic-network/register-mgm/notary.cpb new file mode 100644 index 0000000..4ee9209 Binary files /dev/null and b/java-samples/mgm-dynamic-network/register-mgm/notary.cpb differ diff --git a/java-samples/mgm-dynamic-network/settings.gradle b/java-samples/mgm-dynamic-network/settings.gradle new file mode 100644 index 0000000..0440221 --- /dev/null +++ b/java-samples/mgm-dynamic-network/settings.gradle @@ -0,0 +1,24 @@ +pluginManagement { + // Declare the repositories where plugins are stored. + repositories { + gradlePluginPortal() + mavenCentral() + } + + // The plugin dependencies with versions of the plugins congruent with the specified CorDapp plugin version, + // Corda API version, and Kotlin version. + plugins { + id 'net.corda.plugins.cordapp-cpk2' version cordaPluginsVersion + id 'net.corda.plugins.cordapp-cpb2' version cordaPluginsVersion + id 'net.corda.cordapp.cordapp-configuration' version cordaApiVersion + id 'org.jetbrains.kotlin.jvm' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.jpa' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.allopen' version kotlinVersion + } +} + +// Root project name, used in naming the project as a whole and used in naming objects built by the project. +rootProject.name = 'mgm-dynamic-network-java' +include ':workflows' +include ':contracts' + diff --git a/java-samples/mgm-dynamic-network/workflows/build.gradle b/java-samples/mgm-dynamic-network/workflows/build.gradle new file mode 100644 index 0000000..d3d21e5 --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/build.gradle @@ -0,0 +1,90 @@ +plugins { + // Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well. + // These extend existing build environment so that CPB and CPK files can be built. + // This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp + // required by Corda. + id 'net.corda.plugins.cordapp-cpb2' + id 'org.jetbrains.kotlin.jvm' + id 'maven-publish' +} + +// Declare dependencies for the modules we will use. +// A cordaProvided declaration is required for anything that we use that the Corda API provides. +// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on. +dependencies { + // From other subprojects: + cordapp project(':contracts') + + cordaProvided 'org.jetbrains.kotlin:kotlin-osgi-bundle' + + // Declare a "platform" so that we use the correct set of dependency versions for the version of the + // Corda API specified. + cordaProvided platform("net.corda:corda-api:$cordaApiVersion") + + // If using transistive dependencies this will provide most of Corda-API: + // cordaProvided 'net.corda:corda-application' + + // Alternatively we can explicitly specify all our Corda-API dependencies: + cordaProvided 'net.corda:corda-base' + cordaProvided 'net.corda:corda-application' + cordaProvided 'net.corda:corda-crypto' + cordaProvided 'net.corda:corda-membership' + // cordaProvided 'net.corda:corda-persistence' + cordaProvided 'net.corda:corda-serialization' + cordaProvided 'net.corda:corda-ledger-utxo' + cordaProvided 'net.corda:corda-ledger-consensual' + + // CorDapps that use the UTXO ledger must include at least one notary client plugin + cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion" + + // The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration. + cordaProvided 'org.slf4j:slf4j-api' + + // This are shared so should be here. + // Dependencies Required By Test Tooling + // Todo: these are commented out as the simulator UTXO work has not been merged into Gecko yet. +// testImplementation "net.corda:corda-simulator-api:$simulatorVersion" +// testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion" + + // 3rd party libraries + // Required + testImplementation "org.slf4j:slf4j-simple:2.0.0" + testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Optional but used by exmaple tests. + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" + +} + +// The CordApp section. +// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp. +// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s +// subproject. +// This is required by the corda plugins to build the CorDapp. +cordapp { + // "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred + // and earliest versions of the Corda platform that the CorDapp will run on respectively. + // Enforced versioning has not implemented yet so we need to pass in a dummy value for now. + // The platform version will correspond to and be roughly equivalent to the Corda API version. + targetPlatformVersion = platformVersion.toInteger() + minimumPlatformVersion = platformVersion.toInteger() + + // The cordapp section contains either a workflow or contract subsection depending on the type of component. + // Declares the type and metadata of the CPK (this CPB has one CPK). + workflow { + name "WorkflowsModuleNameHere" + versionId 1 + vendor "VendorNameHere" + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java new file mode 100644 index 0000000..5c3f37f --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java @@ -0,0 +1,41 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +// Class to hold the ListChatFlow results. +// The ChatState(s) cannot be returned directly as the JsonMarshallingService can only serialize simple classes +// that the underlying Jackson serializer recognises, hence creating a DTO style object which consists only of Strings +// and a UUID. It is possible to create custom serializers for the JsonMarshallingService, but this beyond the scope +// of this simple example. +public class ChatStateResults { + + private UUID id; + private String chatName; + private String messageFromName; + private String message; + + public ChatStateResults() {} + + public ChatStateResults(UUID id, String chatName, String messageFromName, String message) { + this.id = id; + this.chatName = chatName; + this.messageFromName = messageFromName; + this.message = message; + } + + public UUID getId() { + return id; + } + + public String getChatName() { + return chatName; + } + + public String getMessageFromName() { + return messageFromName; + } + + public String getMessage() { + return message; + } +} diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java new file mode 100644 index 0000000..9a8e11c --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java @@ -0,0 +1,133 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract; +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.*; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.ledger.common.NotaryLookup; +import net.corda.v5.ledger.common.Party; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; +import net.corda.v5.membership.MemberInfo; +import net.corda.v5.membership.NotaryInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.PublicKey; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; + +import static java.util.Objects.*; + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. +public class CreateNewChatFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(CreateNewChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public NotaryLookup notaryLookup; + + // FlowEngine service is required to run SubFlows. + @CordaInject + public FlowEngine flowEngine; + + + @Suspendable + @Override + public String call( ClientRequestBody requestBody) { + + log.info("CreateNewChatFlow.call() called"); + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + CreateNewChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, CreateNewChatFlowArgs.class); + + // Get MemberInfos for the Vnode running the flow and the otherMember. + MemberInfo myInfo = memberLookup.myInfo(); + MemberInfo otherMember = requireNonNull( + memberLookup.lookup(MemberX500Name.parse(flowArgs.getOtherMember())), + "MemberLookup can't find otherMember specified in flow arguments." + ); + + // Create the ChatState from the input arguments and member information. + ChatState chatState = new ChatState( + UUID.randomUUID(), + flowArgs.getChatName(), + myInfo.getName(), + flowArgs.getMessage(), + Arrays.asList(myInfo.getLedgerKeys().get(0), otherMember.getLedgerKeys().get(0)) + ); + + // Obtain the Notary name and public key. + NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); + PublicKey notaryKey = null; + for(MemberInfo memberInfo: memberLookup.lookup()){ + if(Objects.equals( + memberInfo.getMemberProvidedContext().get("corda.notary.service.name"), + notary.getName().toString())) { + notaryKey = memberInfo.getLedgerKeys().get(0); + break; + } + } + // Note, in Java CorDapps only unchecked RuntimeExceptions can be thrown not + // declared checked exceptions as this changes the method signature and breaks override. + if(notaryKey == null) { + throw new CordaRuntimeException("No notary PublicKey found"); + } + + // Use UTXOTransactionBuilder to build up the draft transaction. + UtxoTransactionBuilder txBuilder = ledgerService.getTransactionBuilder() + .setNotary(new Party(notary.getName(), notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addCommand(new ChatContract.Create()) + .addSignatories(chatState.getParticipants()); + + // Convert the transaction builder to a UTXOSignedTransaction. Verifies the content of the + // UtxoTransactionBuilder and signs the transaction with any required signatories that belong to + // the current node. + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); + + // Call FinalizeChatSubFlow which will finalise the transaction. + // If successful the flow will return a String of the created transaction id, + // if not successful it will return an error message. + return flowEngine.subFlow(new FinalizeChatSubFlow(signedTransaction, otherMember.getName())); + } + // Catch any exceptions, log them and rethrow the exception. + catch (Exception e) { + log.warn("Failed to process utxo flow for request body " + requestBody + " because: " + e.getMessage()); + throw new CordaRuntimeException(e.getMessage()); + } + } +} + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "create-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.CreateNewChatFlow", + "requestBody": { + "chatName":"Chat with Bob", + "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "message": "Hello Bob" + } +} + */ diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java new file mode 100644 index 0000000..c2b815e --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java @@ -0,0 +1,30 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +// A class to hold the deserialized arguments required to start the flow. +public class CreateNewChatFlowArgs{ + + // Serialisation service requires a default constructor + public CreateNewChatFlowArgs() {} + + private String chatName; + private String message; + private String otherMember; + + public CreateNewChatFlowArgs(String chatName, String message, String otherMember) { + this.chatName = chatName; + this.message = message; + this.otherMember = otherMember; + } + + public String getChatName() { + return chatName; + } + + public String getMessage() { + return message; + } + + public String getOtherMember() { + return otherMember; + } +} \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java new file mode 100644 index 0000000..01f4b31 --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java @@ -0,0 +1,75 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.InitiatedBy; +import net.corda.v5.application.flows.ResponderFlow; +import net.corda.v5.application.messaging.FlowSession; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; + + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. + +//@InitiatingBy declares the protocol which will be used to link the initiator to the responder. +@InitiatedBy(protocol = "finalize-chat-protocol") +public class FinalizeChatResponderFlow implements ResponderFlow { + private final static Logger log = LoggerFactory.getLogger(FinalizeChatResponderFlow.class); + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @Suspendable + @Override + public void call(FlowSession session) { + + log.info("FinalizeChatResponderFlow.call() called"); + + try { + // Defines the lambda validator used in receiveFinality below. + UtxoTransactionValidator txValidator = ledgerTransaction -> { + ChatState state = (ChatState) ledgerTransaction.getOutputContractStates().get(0); + // Uses checkForBannedWords() and checkMessageFromMatchesCounterparty() functions + // to check whether to sign the transaction. + if (checkForBannedWords(state.getMessage()) || !checkMessageFromMatchesCounterparty(state, session.getCounterparty())) { + throw new CordaRuntimeException("Failed verification"); + } + log.info("Verified the transaction - " + ledgerTransaction.getId()); + }; + + // Calls receiveFinality() function which provides the responder to the finalise() function + // in the Initiating Flow. Accepts a lambda validator containing the business logic to decide whether + // responder should sign the Transaction. + UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService.receiveFinality(session, txValidator); + log.info("Finished responder flow - " + finalizedSignedTransaction.getId()); + } + // Soft fails the flow and log the exception. + catch(Exception e) + { + log.warn("Exceptionally finished responder flow", e); + } + } + + + @Suspendable + Boolean checkForBannedWords(String str) { + List bannedWords = Arrays.asList("banana", "apple", "pear"); + return bannedWords.stream().anyMatch(str::contains); + } + + @Suspendable + Boolean checkMessageFromMatchesCounterparty(ChatState state, MemberX500Name otherMember) { + return state.getMessageFrom().equals(otherMember); + } + +} diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java new file mode 100644 index 0000000..12f8e98 --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java @@ -0,0 +1,71 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import net.corda.v5.application.flows.*; +import net.corda.v5.application.messaging.FlowMessaging; +import net.corda.v5.application.messaging.FlowSession; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. + +// @InitiatingFlow declares the protocol which will be used to link the initiator to the responder. +@InitiatingFlow(protocol = "finalize-chat-protocol") +public class FinalizeChatSubFlow implements SubFlow { + + private final static Logger log = LoggerFactory.getLogger(FinalizeChatSubFlow.class); + private final UtxoSignedTransaction signedTransaction; + private final MemberX500Name otherMember; + + public FinalizeChatSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name otherMember) { + this.signedTransaction = signedTransaction; + this.otherMember = otherMember; + } + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public FlowMessaging flowMessaging; + + @Override + @Suspendable + public String call() { + + log.info("FinalizeChatFlow.call() called"); + + // Initiates a session with the other Member. + FlowSession session = flowMessaging.initiateFlow(otherMember); + + // Calls the Corda provided finalise() function which gather signatures from the counterparty, + // notarises the transaction and persists the transaction to each party's vault. + // On success returns the id of the transaction created. (This is different to the ChatState id) + String result; + try { + List sessionsList = Arrays.asList(session); + + UtxoSignedTransaction finalizedSignedTransaction = ledgerService.finalize( + signedTransaction, + sessionsList + ); + + result = finalizedSignedTransaction.getId().toString(); + log.info("Success! Response: " + result); + + } + // Soft fails the flow and returns the error message without throwing a flow exception. + catch (Exception e) { + log.warn("Finality failed", e); + result = "Finality failed, " + e.getMessage(); + } + // Returns the transaction id converted as a string + return result; + } +} diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java new file mode 100644 index 0000000..6f192ea --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java @@ -0,0 +1,119 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.ClientRequestBody; +import net.corda.v5.application.flows.ClientStartableFlow; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.crypto.SecureHash; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import static java.util.Objects.*; +import static java.util.stream.Collectors.toList; + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. +public class GetChatFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(GetChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + public UtxoLedgerService ledgerService; + + @Override + @Suspendable + public String call(ClientRequestBody requestBody) { + + // Obtain the deserialized input arguments to the flow from the requestBody. + GetChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, GetChatFlowArgs.class); + + // Look up the latest unconsumed ChatState with the given id. + // Note, this code brings all unconsumed states back, then filters them. + // This is an inefficient way to perform this operation when there are a large number of chats. + // Note, you will get this error if you input an id which has no corresponding ChatState (common error). + List> chatStateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class); + List> chatStateAndRefsWithId = chatStateAndRefs.stream() + .filter(sar -> sar.getState().getContractState().getId().equals(flowArgs.getId())).collect(toList()); + if (chatStateAndRefsWithId.size() != 1) throw new CordaRuntimeException("Multiple or zero Chat states with id " + flowArgs.getId() + " found"); + StateAndRef chatStateAndRef = chatStateAndRefsWithId.get(0); + + // Calls resolveMessagesFromBackchain() which retrieves the chat history from the backchain. + return jsonMarshallingService.format(resolveMessagesFromBackchain(chatStateAndRef, flowArgs.getNumberOfRecords() )); + } + + // resoveMessageFromBackchain() starts at the stateAndRef provided, which represents the unconsumed head of the + // backchain for this particular chat, then walks the chain backwards for the number of transaction specified in + // the numberOfRecords argument. For each transaction it adds the MessageAndSender representing the + // message and who sent it to a list which is then returned. + @Suspendable + private List resolveMessagesFromBackchain(StateAndRef stateAndRef, int numberOfRecords) { + + // Set up a Mutable List to collect the MessageAndSender(s) + List messages = new LinkedList<>(); + + // Set up initial conditions for walking the backchain. + StateAndRef currentStateAndRef = stateAndRef; + int recordsToFetch = numberOfRecords; + boolean moreBackchain = true; + + // Continue to loop until the start of the backchain or enough records have been retrieved. + while (moreBackchain) { + + // Obtain the transaction id from the current StateAndRef and fetch the transaction from the vault. + SecureHash transactionId = currentStateAndRef.getRef().getTransactionId(); + UtxoLedgerTransaction transaction = requireNonNull( + ledgerService.findLedgerTransaction(transactionId), + "Transaction " + transactionId + " not found." + ); + + // Get the output state from the transaction and use it to create a MessageAndSender Object which + // is appended to the mutable list. + List chatStates = transaction.getOutputStates(ChatState.class); + if (chatStates.size() != 1) throw new CordaRuntimeException( + "Expecting one and only one ChatState output for transaction " + transactionId + "."); + ChatState output = chatStates.get(0); + + messages.add(new MessageAndSender(output.getMessageFrom().toString(), output.getMessage())); + // Decrement the number of records to fetch. + recordsToFetch--; + + // Get the reference to the input states. + List> inputStateAndRefs = transaction.getInputStateAndRefs(); + + // Check if there are no more input states (start of chain) or we have retrieved enough records. + // Check the transaction is not malformed by having too many input states. + // Set the currentStateAndRef to the input StateAndRef, then repeat the loop. + if (inputStateAndRefs.isEmpty() || recordsToFetch == 0) { + moreBackchain = false; + } else if (inputStateAndRefs.size() > 1) { + throw new CordaRuntimeException("More than one input state found for transaction " + transactionId + "."); + } else { + currentStateAndRef = inputStateAndRefs.get(0); + } + } + return messages; + } +} + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "get-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.GetChatFlow", + "requestBody": { + "id":"** fill in id **", + "numberOfRecords":"4" + } +} + */ + diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java new file mode 100644 index 0000000..d1bac81 --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java @@ -0,0 +1,24 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +// A class to hold the deserialized arguments required to start the flow. +public class GetChatFlowArgs { + + private UUID id; + private int numberOfRecords; + public GetChatFlowArgs() {} + + public GetChatFlowArgs(UUID id, int numberOfRecords ) { + this.id = id; + this.numberOfRecords = numberOfRecords; + } + + public UUID getId() { + return id; + } + + public int getNumberOfRecords() { + return numberOfRecords; + } +} diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java new file mode 100644 index 0000000..1416b7c --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java @@ -0,0 +1,58 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.ClientRequestBody; +import net.corda.v5.application.flows.ClientStartableFlow; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. +public class ListChatsFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(ListChatsFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + + log.info("ListChatsFlow.call() called"); + + // Queries the VNode's vault for unconsumed states and converts the result to a serializable DTO. + List> states = utxoLedgerService.findUnconsumedStatesByType(ChatState.class); + List results = states.stream().map( stateAndRef -> + new ChatStateResults( + stateAndRef.getState().getContractState().getId(), + stateAndRef.getState().getContractState().getChatName(), + stateAndRef.getState().getContractState().getMessageFrom().toString(), + stateAndRef.getState().getContractState().getMessage() + ) + ).collect(Collectors.toList()); + + // Uses the JsonMarshallingService's format() function to serialize the DTO to Json. + return jsonMarshallingService.format(results); + } +} + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "list-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.ListChatsFlow", + "requestBody": {} +} +*/ \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/MessageAndSender.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/MessageAndSender.java new file mode 100644 index 0000000..7be54fd --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/MessageAndSender.java @@ -0,0 +1,22 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +// A class to pair the messageFrom and message together. +public class MessageAndSender { + + private String messageFrom; + private String message; + public MessageAndSender() {} + + public MessageAndSender(String messageFrom, String message) { + this.messageFrom = messageFrom; + this.message = message; + } + + public String getMessageFrom() { + return messageFrom; + } + + public String getMessage() { + return message; + } +} diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java new file mode 100644 index 0000000..79d9609 --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java @@ -0,0 +1,120 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract; +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.ClientRequestBody; +import net.corda.v5.application.flows.ClientStartableFlow; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.FlowEngine; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; +import net.corda.v5.membership.MemberInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +import static java.util.Objects.*; +import static java.util.stream.Collectors.toList; + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. +public class UpdateChatFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(UpdateChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + public UtxoLedgerService ledgerService; + + // FlowEngine service is required to run SubFlows. + @CordaInject + public FlowEngine flowEngine; + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + + log.info("UpdateNewChatFlow.call() called"); + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + UpdateChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, UpdateChatFlowArgs.class); + + // Look up the latest unconsumed ChatState with the given id. + // Note, this code brings all unconsumed states back, then filters them. + // This is an inefficient way to perform this operation when there are a large number of chats. + // Note, you will get this error if you input an id which has no corresponding ChatState (common error). + List> chatStateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class); + List> chatStateAndRefsWithId = chatStateAndRefs.stream() + .filter(sar -> sar.getState().getContractState().getId().equals(flowArgs.getId())).collect(toList()); + if (chatStateAndRefsWithId.size() != 1) throw new CordaRuntimeException("Multiple or zero Chat states with id " + flowArgs.getId() + " found"); + StateAndRef chatStateAndRef = chatStateAndRefsWithId.get(0); + + // Get MemberInfos for the Vnode running the flow and the otherMember. + MemberInfo myInfo = memberLookup.myInfo(); + ChatState state = chatStateAndRef.getState().getContractState(); + + List members = state.getParticipants().stream().map( + it -> requireNonNull(memberLookup.lookup(it), "Member not found from public Key "+ it + ".") + ).collect(toList()); + members.remove(myInfo); + if(members.size() != 1) throw new RuntimeException("Should be only one participant other than the initiator"); + MemberInfo otherMember = members.get(0); + + // Create a new ChatState using the updateMessage helper function. + ChatState newChatState = state.updateMessage(myInfo.getName(), flowArgs.getMessage()); + + // Use UTXOTransactionBuilder to build up the draft transaction. + UtxoTransactionBuilder txBuilder = ledgerService.getTransactionBuilder() + .setNotary(chatStateAndRef.getState().getNotary()) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(newChatState) + .addInputState(chatStateAndRef.getRef()) + .addCommand(new ChatContract.Update()) + .addSignatories(newChatState.getParticipants()); + + // Convert the transaction builder to a UTXOSignedTransaction. Verifies the content of the + // UtxoTransactionBuilder and signs the transaction with any required signatories that belong to + // the current node. + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); + + // Call FinalizeChatSubFlow which will finalise the transaction. + // If successful the flow will return a String of the created transaction id, + // if not successful it will return an error message. + return flowEngine.subFlow(new FinalizeChatSubFlow(signedTransaction, otherMember.getName())); + + + } + // Catch any exceptions, log them and rethrow the exception. + catch (Exception e) { + log.warn("Failed to process utxo flow for request body " + requestBody + " because: " + e.getMessage()); + throw e; + } + } +} + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "update-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.UpdateChatFlow", + "requestBody": { + "id":" ** fill in id **", + "message": "How are you today?" + } +} + */ \ No newline at end of file diff --git a/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java new file mode 100644 index 0000000..8ac7009 --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java @@ -0,0 +1,24 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +// A class to hold the deserialized arguments required to start the flow. +public class UpdateChatFlowArgs { + public UpdateChatFlowArgs() {} + + private UUID id; + private String message; + + public UpdateChatFlowArgs(UUID id, String message) { + this.id = id; + this.message = message; + } + + public UUID getId() { + return id; + } + + public String getMessage() { + return message; + } +} diff --git a/java-samples/mgm-dynamic-network/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java b/java-samples/mgm-dynamic-network/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java new file mode 100644 index 0000000..9dea76c --- /dev/null +++ b/java-samples/mgm-dynamic-network/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java @@ -0,0 +1,41 @@ +//package com.r3.developers.csdetemplate.flowexample.workflows; +// +//import net.corda.simulator.RequestData; +//import net.corda.simulator.SimulatedVirtualNode; +//import net.corda.simulator.Simulator; +//import net.corda.v5.base.types.MemberX500Name; +//import org.junit.jupiter.api.Test; +// +//class MyFirstFlowTest { +// // Names picked to match the corda network in config/dev-net.json +// private MemberX500Name aliceX500 = MemberX500Name.parse("CN=Alice, OU=Test Dept, O=R3, L=London, C=GB"); +// private MemberX500Name bobX500 = MemberX500Name.parse("CN=Bob, OU=Test Dept, O=R3, L=London, C=GB"); +// +// @Test +// @SuppressWarnings("unchecked") +// public void test_that_MyFirstFLow_returns_correct_message() { +// // Instantiate an instance of the simulator. +// Simulator simulator = new Simulator(); +// +// // Create Alice's and Bob's virtual nodes, including the classes of the flows which will be registered on each node. +// // Don't assign Bob's virtual node to a value. You don't need it for this particular test. +// SimulatedVirtualNode aliceVN = simulator.createVirtualNode(aliceX500, MyFirstFlow.class); +// simulator.createVirtualNode(bobX500, MyFirstFlowResponder.class); +// +// // Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow. +// MyFirstFlowStartArgs myFirstFlowStartArgs = new MyFirstFlowStartArgs(bobX500); +// +// // Create a requestData object. +// RequestData requestData = RequestData.Companion.create( +// "request no 1", // A unique reference for the instance of the flow request. +// MyFirstFlow.class, // The name of the flow class which is to be started. +// myFirstFlowStartArgs // The object which contains the start arguments of the flow. +// ); +// +// // Call the flow on Alice's virtual node and capture the response. +// String flowResponse = aliceVN.callFlow(requestData); +// +// // Check that the flow has returned the expected string. +// assert(flowResponse.equals("Hello Alice, best wishes from Bob")); +// } +//} diff --git a/kotlin-samples/README.md b/kotlin-samples/README.md index 917688d..dfe2550 100644 --- a/kotlin-samples/README.md +++ b/kotlin-samples/README.md @@ -11,6 +11,7 @@ This is the Kotlin Samples folder. ``` . ├── README.md -└── corda5-obligation-cordapp +├── corda5-obligation-cordapp +└── mgm-dynamic-network ``` diff --git a/kotlin-samples/mgm-dynamic-network/.ci/Jenkinsfile b/kotlin-samples/mgm-dynamic-network/.ci/Jenkinsfile new file mode 100644 index 0000000..bbb01e4 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/.ci/Jenkinsfile @@ -0,0 +1,11 @@ +@Library('corda-shared-build-pipeline-steps@5.0') _ + +cordaPipeline( + nexusAppId: 'com.corda.CSDE-kotlin.5.0', + publishRepoPrefix: '', + slimBuild: true, + runUnitTests: false, + dedicatedJobForSnykDelta: false, + slackChannel: '#corda-corda5-dev-ex-build-notifications', + gitHubComments: false + ) diff --git a/kotlin-samples/mgm-dynamic-network/.ci/nightly/JenkinsfileSnykScan b/kotlin-samples/mgm-dynamic-network/.ci/nightly/JenkinsfileSnykScan new file mode 100644 index 0000000..c07efb7 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/.ci/nightly/JenkinsfileSnykScan @@ -0,0 +1,6 @@ +@Library('corda-shared-build-pipeline-steps@5.0') _ + +cordaSnykScanPipeline ( + snykTokenId: 'r3-snyk-corda5', + snykAdditionalCommands: "--all-sub-projects -d" +) diff --git a/kotlin-samples/mgm-dynamic-network/.gitignore b/kotlin-samples/mgm-dynamic-network/.gitignore new file mode 100644 index 0000000..65a33ef --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/.gitignore @@ -0,0 +1,83 @@ + +# Eclipse, ctags, Mac metadata, log files +.classpath +.project +.settings +tags +.DS_Store +*.log +*.orig + +# Created by .ignore support plugin (hsz.mobi) + +.gradle +local.properties +.gradletasknamecache + +# General build files +**/build/* + +lib/quasar.jar + +**/logs/* + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/*.xml +.idea/.name +.idea/copyright +.idea/inspectionProfiles +.idea/libraries +.idea/shelf +.idea/dataSources +.idea/markdown-navigator +.idea/runConfigurations +.idea/dictionaries + + +# Include the -parameters compiler option by default in IntelliJ required for serialization. +!.idea/codeStyleSettings.xml + + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +**/out/ +/classes/ + + + +# vim +*.swp +*.swn +*.swo + + + +# Directory generated during Resolve and TestOSGi gradle tasks +bnd/ + +# Ignore Gradle build output directory +build +/.idea/codeStyles/codeStyleConfig.xml +/.idea/codeStyles/Project.xml + + + +# Ignore Visual studio directory +bin/ + + + +*.cpi +*.cpb +*.cpk +workspace/** \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/.run/runConfigurations/DebugCorDapp.run.xml b/kotlin-samples/mgm-dynamic-network/.run/runConfigurations/DebugCorDapp.run.xml new file mode 100644 index 0000000..1d8da82 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/.run/runConfigurations/DebugCorDapp.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/.snyk b/kotlin-samples/mgm-dynamic-network/.snyk new file mode 100644 index 0000000..8c3989f --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/.snyk @@ -0,0 +1,22 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.25.0 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744: + - '*': + reason: >- + This vulnerability relates to information exposure via creation of + temporary files (via Kotlin functions) with insecure permissions. + Corda does not use any of the vulnerable functions so it is not + susceptible to this vulnerability + expires: 2023-06-19T17:08:41.029Z + created: 2023-02-02T17:08:41.032Z + SNYK-JAVA-ORGJETBRAINSKOTLIN-2628385: + - '*': + reason: >- + corda-simulator-runtime is a testRuntimeOnly dependency, as such this + dependency will not be included in any cordaApp produced by the CSDE + project Template. + expires: 2023-06-19T17:09:11.451Z + created: 2023-02-02T17:09:11.455Z +patch: {} diff --git a/kotlin-samples/mgm-dynamic-network/README.md b/kotlin-samples/mgm-dynamic-network/README.md new file mode 100644 index 0000000..d30e3ee --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/README.md @@ -0,0 +1,53 @@ +# MGM-Dynamic-Network + +This demo CorDapp is to show the use of the MGM tool on setting up a dynamic Corda5 application network. This app will use the Corda 5 combined worker as the foundation. We will then use the MGM tool to deploy the dynamic Corda5 application network on it. All the instructions are written into 4 shell scripts, which you can simply run them. + +## Before running the app +* You need to prepare a self-generate CA for create certificates for keys generated within Corda. This CA will be external to Corda. +* You would need to reset the path for both `WORK_DIR` and `RUNTIME_OS` variables in the script. + +[R3 INTERAL NOTES]: If you are an R3 employee, you can use the mock ca tool in Corda-runtime-os. Get the `release/os/5.0-Beta2` branch of the Corda-runtime-os. + + +## Running the demo +Find the gradle task `startCorda` and wait for the combined worker to be fully started. Once the swagger page is loaded, run the shell script one by one, and follow the prompts. +``` +. +├── Step1-mgm-deploy.sh +├── Step2-notary-onboard.sh +├── Step3-first-member-onboard.sh +└── Step4-more-member-onboard.sh +``` + +## Test your deployment +#### Step 1: Create Chat Entry +Pick a VNode identity to initiate the chat, and get its short hash. (Let's pick Alice. Don't pick Bob because Bob is the person who we will have the chat with). + +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +``` +{ + "clientRequestId": "create-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.CreateNewChatFlow", + "requestBody": { + "chatName":"Chat with Bob", + "otherMember":"THE-NODE-YOU-CREATED", + "message": "Hello Bob" + } +} +``` + +After trigger the create-chat flow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and clientrequestid to view the flow result + +#### Step 2: List the chat +In order to continue the chat, we would need the chat ID. This step will bring out all the chat entries this entity (Alice) has. +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +``` +{ + "clientRequestId": "list-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.ListChatsFlow", + "requestBody": {} +} +``` +After trigger the list-chats flow, again, we need to hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and check the result. As the screenshot shows, in the response body, +we will see a list of chat entries, but it currently only has one entry. And we can see the id of the chat entry. Let's record that id. + diff --git a/kotlin-samples/mgm-dynamic-network/Step1-mgm-deploy.sh b/kotlin-samples/mgm-dynamic-network/Step1-mgm-deploy.sh new file mode 100644 index 0000000..b334c02 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/Step1-mgm-deploy.sh @@ -0,0 +1,149 @@ +#!/bin/sh + +echo "---Set Env---" +RPC_HOST=localhost +RPC_PORT=8888 +P2P_GATEWAY_HOST=localhost +P2P_GATEWAY_PORT=8080 +API_URL="https://$RPC_HOST:$RPC_PORT/api/v1" +WORK_DIR=~/Corda/corda5/corda5-samples/kotlin-samples/mgm-dynamic-network/register-mgm +mkdir -p "$WORK_DIR" +RUNTIME_OS=~/Corda/corda5/corda-runtime-os + +echo "\n---Create a mock CA and signing keys---" +cd "$WORK_DIR" +#default signing key +echo '-----BEGIN CERTIFICATE----- +MIIB7zCCAZOgAwIBAgIEFyV7dzAMBggqhkjOPQQDAgUAMFsxCzAJBgNVBAYTAkdC +MQ8wDQYDVQQHDAZMb25kb24xDjAMBgNVBAoMBUNvcmRhMQswCQYDVQQLDAJSMzEe +MBwGA1UEAwwVQ29yZGEgRGV2IENvZGUgU2lnbmVyMB4XDTIwMDYyNTE4NTI1NFoX +DTMwMDYyMzE4NTI1NFowWzELMAkGA1UEBhMCR0IxDzANBgNVBAcTBkxvbmRvbjEO +MAwGA1UEChMFQ29yZGExCzAJBgNVBAsTAlIzMR4wHAYDVQQDExVDb3JkYSBEZXYg +Q29kZSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQDjSJtzQ+ldDFt +pHiqdSJebOGPZcvZbmC/PIJRsZZUF1bl3PfMqyG3EmAe0CeFAfLzPQtf2qTAnmJj +lGTkkQhxo0MwQTATBgNVHSUEDDAKBggrBgEFBQcDAzALBgNVHQ8EBAMCB4AwHQYD +VR0OBBYEFLMkL2nlYRLvgZZq7GIIqbe4df4pMAwGCCqGSM49BAMCBQADSAAwRQIh +ALB0ipx6EplT1fbUKqgc7rjH+pV1RQ4oKF+TkfjPdxnAAiArBdAI15uI70wf+xlL +zU+Rc5yMtcOY4/moZUq36r0Ilg== +-----END CERTIFICATE-----' > ./gradle-plugin-default-key.pem +#Generate a signing key: +keytool -genkeypair -alias "signing key 1" -keystore signingkeys.pfx -storepass "keystore password" -dname "cn=CPI Plugin Example - Signing Key 1, o=R3, L=London, c=GB" -keyalg RSA -storetype pkcs12 -validity 4000 +#Import gradle-plugin-default-key.pem into the keystore +keytool -importcert -keystore signingkeys.pfx -storepass "keystore password" -noprompt -alias gradle-plugin-default-key -file gradle-plugin-default-key.pem +cd "$RUNTIME_OS" +./gradlew :applications:tools:p2p-test:fake-ca:clean :applications:tools:p2p-test:fake-ca:appJar +java -jar ./applications/tools/p2p-test/fake-ca/build/bin/corda-fake-ca-5.0.0.0-Gecko-SNAPSHOT.jar -m /tmp/ca -a RSA -s 3072 ca +cd "$WORK_DIR" + + +echo "\n---Build mgm CPB---" +cd "$RUNTIME_OS" +./gradlew testing:cpbs:mgm:build +cp testing/cpbs/mgm/build/libs/mgm-5.0.0.0-Gecko-SNAPSHOT-package.cpb "$WORK_DIR" +echo '{ + "fileFormatVersion" : 1, + "groupId" : "CREATE_ID", + "registrationProtocol" :"net.corda.membership.impl.registration.dynamic.mgm.MGMRegistrationService", + "synchronisationProtocol": "net.corda.membership.impl.synchronisation.MgmSynchronisationServiceImpl" +}' > "$WORK_DIR"/MgmGroupPolicy.json +cd "$WORK_DIR" +mv ./mgm-5.0.0.0-Gecko-SNAPSHOT-package.cpb mgm.cpb + + +echo "\n---Build and upload MGM CPI---" +cd "$WORK_DIR" +#Run this command to turn a CPB into a CPI +sh ~/.corda/cli/corda-cli.sh package create-cpi --cpb mgm.cpb --group-policy MgmGroupPolicy.json --cpi-name "mgm cpi" --cpi-version "1.0.0.0-SNAPSHOT" --file mgm.cpi --keystore signingkeys.pfx --storepass "keystore password" --key "signing key 1" +#Import the gradle plugin default key into Corda +curl --insecure -u admin:admin -X PUT -F alias="gradle-plugin-default-key" -F certificate=@gradle-plugin-default-key.pem https://localhost:8888/api/v1/certificates/cluster/code-signer +#Export the signing key certificate from the key store +keytool -exportcert -rfc -alias "signing key 1" -keystore signingkeys.pfx -storepass "keystore password" -file signingkey1.pem +#Import the signing key into Corda +curl --insecure -u admin:admin -X PUT -F alias="signingkey1-2022" -F certificate=@signingkey1.pem https://localhost:8888/api/v1/certificates/cluster/code-signer +CPI_PATH=./mgm.cpi +curl --insecure -u admin:admin -F upload=@$CPI_PATH $API_URL/cpi/ +echo "\n" +read -p "Enter the CPI_ID from the returned body:" CPI_ID +echo "CPI_ID:" $CPI_ID + + +echo "---Create MGM VNode---" +curl --insecure -u admin:admin $API_URL/cpi/status/$CPI_ID +echo "\n" +read -p "Enter the CPI_CHECKSUM from the returned body:" CPI_CHECKSUM +curl --insecure -u admin:admin -d '{ "request": {"cpiFileChecksum": "'$CPI_CHECKSUM'", "x500Name": "C=GB, L=London, O=MGM"}}' $API_URL/virtualnode +echo "\n" +read -p "Enter the MGM_HOLDING_ID from the returned body:" MGM_HOLDING_ID +echo "MGM_HOLDING_ID:" $MGM_HOLDING_ID + + +echo "---Assign soft HSM---" +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$MGM_HOLDING_ID/SESSION_INIT +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$MGM_HOLDING_ID/alias/$MGM_HOLDING_ID-session/category/SESSION_INIT/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the SESSION_KEY_ID from the returned body:" SESSION_KEY_ID +echo "SESSION_KEY_ID:" $SESSION_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$MGM_HOLDING_ID/PRE_AUTH +echo "\nECDH_KEY_ID: " +curl --insecure -u admin:admin -X POST $API_URL/keys/$MGM_HOLDING_ID/alias/$MGM_HOLDING_ID-auth/category/PRE_AUTH/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the ECDH_KEY_ID from the returned body:" ECDH_KEY_ID +echo "ECDH_KEY_ID:" $ECDH_KEY_ID + + +echo "\n--Set up the TLS key pair and certificate---" +curl -k -u admin:admin -X POST -H "Content-Type: application/json" $API_URL/keys/p2p/alias/p2p-TLS/category/TLS/scheme/CORDA.RSA +echo "\n" +read -p "Enter the TLS_KEY_ID from the returned body:" TLS_KEY_ID +echo "TLS_KEY_ID:" $TLS_KEY_ID +curl -k -u admin:admin -X POST -H "Content-Type: application/json" -d '{"x500Name": "CN=CordaOperator, C=GB, L=London, O=Org", "subjectAlternativeNames": ["'$P2P_GATEWAY_HOST'"]}' $API_URL"/certificates/p2p/"$TLS_KEY_ID > "$WORK_DIR"/request1.csr +read -p "Wait for download to be finished, Then press any key to continue..." ANY +cd "$RUNTIME_OS" +java -jar ./applications/tools/p2p-test/fake-ca/build/bin/corda-fake-ca-5.0.0.0-Gecko-SNAPSHOT.jar -m /tmp/ca csr "$WORK_DIR"/request1.csr +cd "$WORK_DIR" +curl -k -u admin:admin -X PUT -F certificate=@/tmp/ca/request1/certificate.pem -F alias=p2p-tls-cert $API_URL/certificates/cluster/p2p-tls + + +echo "---Disable revocation checks---" +curl --insecure -u admin:admin -X GET $API_URL/config/corda.p2p.gateway +echo "\n" +read -p "Enter the CONFIG_VERSION from the returned body:" CONFIG_VERSION +echo "CONFIG_VERSION:" $CONFIG_VERSION +curl -k -u admin:admin -X PUT -d '{"section":"corda.p2p.gateway", "version":"'$CONFIG_VERSION'", "config":"{ \"sslConfig\": { \"revocationCheck\": { \"mode\": \"OFF\" } } }", "schemaVersion": {"major": 1, "minor": 0}}' $API_URL"/config" + + +echo "\n---Register MGM---" +TLS_CA_CERT=$(cat /tmp/ca/ca/root-certificate.pem | awk '{printf "%s\\n", $0}') +REGISTRATION_CONTEXT='{ + "corda.session.key.id": "'$SESSION_KEY_ID'", + "corda.ecdh.key.id": "'$ECDH_KEY_ID'", + "corda.group.protocol.registration": "net.corda.membership.impl.registration.dynamic.member.DynamicMemberRegistrationService", + "corda.group.protocol.synchronisation": "net.corda.membership.impl.synchronisation.MemberSynchronisationServiceImpl", + "corda.group.protocol.p2p.mode": "Authenticated_Encryption", + "corda.group.key.session.policy": "Combined", + "corda.group.pki.session": "NoPKI", + "corda.group.pki.tls": "Standard", + "corda.group.tls.version": "1.3", + "corda.endpoints.0.connectionURL": "https://'$P2P_GATEWAY_HOST':'$P2P_GATEWAY_PORT'", + "corda.endpoints.0.protocolVersion": "1", + "corda.group.truststore.tls.0" : "'$TLS_CA_CERT'" +}' +REGISTRATION_REQUEST='{"memberRegistrationRequest":{"action": "requestJoin", "context": '$REGISTRATION_CONTEXT'}}' +curl --insecure -u admin:admin -d "$REGISTRATION_REQUEST" $API_URL/membership/$MGM_HOLDING_ID +echo "\n" +read -p "Enter the REGISTRATION_ID from the returned body:" REGISTRATION_ID +echo "REGISTRATION_ID:" $REGISTRATION_ID +curl --insecure -u admin:admin -X GET $API_URL/membership/$MGM_HOLDING_ID/$REGISTRATION_ID +echo "\n" + + +echo "---Configure virtual node as network participant---" +curl -k -u admin:admin -X PUT -d '{"p2pTlsCertificateChainAlias": "p2p-tls-cert", "useClusterLevelTlsCertificateAndKey": true, "sessionKeyId": "'$SESSION_KEY_ID'"}' $API_URL/network/setup/$MGM_HOLDING_ID +echo "\n" + + +echo "---Export group policy for group---" +cd "$WORK_DIR" +mkdir -p "./register-member" +curl --insecure -u admin:admin -X GET $API_URL/mgm/$MGM_HOLDING_ID/info > "$WORK_DIR/register-member/GroupPolicy.json" diff --git a/kotlin-samples/mgm-dynamic-network/Step2-notary-onboard.sh b/kotlin-samples/mgm-dynamic-network/Step2-notary-onboard.sh new file mode 100644 index 0000000..5e0d864 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/Step2-notary-onboard.sh @@ -0,0 +1,92 @@ +#!/bin/sh + +echo "---Set Env---" +RPC_HOST=localhost +RPC_PORT=8888 +P2P_GATEWAY_HOST=localhost +P2P_GATEWAY_PORT=8080 +API_URL="https://$RPC_HOST:$RPC_PORT/api/v1" +WORK_DIR=~/Corda/corda5/corda5-samples/kotlin-samples/mgm-dynamic-network/register-mgm +RUNTIME_OS=~/Corda/corda5/corda-runtime-os + + +echo "\n---Build and upload Notary CPI---" +cd $WORK_DIR +cp ./notary.cpb ./register-member +cd "$WORK_DIR/register-member/" +##Run this command to turn a CPB into a CPI +sh ~/.corda/cli/corda-cli.sh package create-cpi --cpb notary.cpb --group-policy GroupPolicy.json --cpi-name "notary cpi" --cpi-version "1.0.0.0-SNAPSHOT" --file notary.cpi --keystore ../signingkeys.pfx --storepass "keystore password" --key "signing key 1" +CPI_PATH="$WORK_DIR/register-member/notary.cpi" +curl --insecure -u admin:admin -F upload=@$CPI_PATH $API_URL/cpi/ +echo "\n" +read -p "Enter the Notary CPI_ID from the returned body:" CPI_ID +echo "CPI_ID:" $CPI_ID +curl --insecure -u admin:admin $API_URL/cpi/status/$CPI_ID +echo "\n" +read -p "Enter the CPI_CHECKSUM from the returned body:" CPI_CHECKSUM + + +echo "\n---Create a Member virtual node---" +echo "\n" +read -p "Enter the X500_NAME from the returned body (Formatt: C=GB,L=London,O=NotaryRep1):" X500_NAME +curl --insecure -u admin:admin -d '{"request": {"cpiFileChecksum": "'$CPI_CHECKSUM'", "x500Name": "'$X500_NAME'"}}' $API_URL/virtualnode +echo "\n" +read -p "Enter the HOLDING_ID from the returned body:" HOLDING_ID +echo "HOLDING_ID:" $HOLDING_ID +echo "\n" + + +echo "---Assign soft HSM---" +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/SESSION_INIT +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL'/keys/'$HOLDING_ID'/alias/'$HOLDING_ID'-session/category/SESSION_INIT/scheme/CORDA.ECDSA.SECP256R1' +echo "\n" +read -p "Enter the SESSION_KEY_ID from the returned body:" SESSION_KEY_ID +echo "SESSION_KEY_ID:" $SESSION_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/LEDGER +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$HOLDING_ID/alias/$HOLDING_ID-ledger/category/LEDGER/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the LEDGER_KEY_ID from the returned body:" LEDGER_KEY_ID +echo "LEDGER_KEY_ID:" $LEDGER_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/NOTARY +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$HOLDING_ID/alias/$HOLDING_ID-notary/category/NOTARY/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the NOTARY_KEY_ID from the returned body:" NOTARY_KEY_ID +echo "NOTARY_KEY_ID:" $NOTARY_KEY_ID + + +echo "\n---Configure virtual node as network participant---" +curl -k -u admin:admin -X PUT -d '{"p2pTlsCertificateChainAlias": "p2p-tls-cert", "useClusterLevelTlsCertificateAndKey": true, "sessionKeyId": "'$SESSION_KEY_ID'"}' $API_URL/network/setup/$HOLDING_ID +echo "\n" + + +echo "\n---Build Notary registration context---" +echo "\n" +read -p "Enter the NOTARY_SERVICE_NAME (Formatt: C=GB,L=London,O=NotaryServiceA):" NOTARY_SERVICE_NAME +echo "NOTARY_SERVICE_NAME:" $NOTARY_SERVICE_NAME +REGISTRATION_CONTEXT='{ + "corda.session.key.id": "'$SESSION_KEY_ID'", + "corda.session.key.signature.spec": "SHA256withECDSA", + "corda.ledger.keys.0.id": "'$LEDGER_KEY_ID'", + "corda.ledger.keys.0.signature.spec": "SHA256withECDSA", + "corda.notary.keys.0.id": "'$NOTARY_KEY_ID'", + "corda.notary.keys.0.signature.spec": "SHA256withECDSA", + "corda.endpoints.0.connectionURL": "https://'$P2P_GATEWAY_HOST':'$P2P_GATEWAY_PORT'", + "corda.endpoints.0.protocolVersion": "1", + "corda.roles.0" : "notary", + "corda.notary.service.name" : "'$NOTARY_SERVICE_NAME'", + "corda.notary.service.plugin" : "net.corda.notary.NonValidatingNotary" +}' +REGISTRATION_REQUEST='{"memberRegistrationRequest":{"action": "requestJoin", "context": '$REGISTRATION_CONTEXT'}}' + + +echo "\n---Register Notary VNode---" +curl --insecure -u admin:admin -d "$REGISTRATION_REQUEST" $API_URL/membership/$HOLDING_ID +echo "\n" +read -p "Enter the REGISTRATION_ID from the returned body:" REGISTRATION_ID +echo "REGISTRATION_ID:" $REGISTRATION_ID +curl --insecure -u admin:admin -X GET $API_URL/membership/$HOLDING_ID/$REGISTRATION_ID +echo "\n" +curl --insecure -u admin:admin -X GET $API_URL/members/$HOLDING_ID diff --git a/kotlin-samples/mgm-dynamic-network/Step3-first-member-onboard.sh b/kotlin-samples/mgm-dynamic-network/Step3-first-member-onboard.sh new file mode 100644 index 0000000..98a2aaa --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/Step3-first-member-onboard.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +echo "---Set Env---" +RPC_HOST=localhost +RPC_PORT=8888 +P2P_GATEWAY_HOST=localhost +P2P_GATEWAY_PORT=8080 +API_URL="https://$RPC_HOST:$RPC_PORT/api/v1" +WORK_DIR=~/Corda/corda5/corda5-samples/kotlin-samples/mgm-dynamic-network/register-mgm +RUNTIME_OS=~/Corda/corda5/corda-runtime-os + + +echo "\n---Build and upload chat CPI---" +./gradlew jar +./gradlew cpb +cd ./workflows/build/libs +mv ./workflows-1.0-SNAPSHOT-package.cpb chat.cpb +cd $WORK_DIR/.. +cp ./workflows/build/libs/chat.cpb ./register-mgm/register-member +##Run this command to turn a CPB into a CPI +cd "$WORK_DIR/register-member/" +sh ~/.corda/cli/corda-cli.sh package create-cpi --cpb chat.cpb --group-policy GroupPolicy.json --cpi-name "chat cpi" --cpi-version "1.0.0.0-SNAPSHOT" --file chat.cpi --keystore ../signingkeys.pfx --storepass "keystore password" --key "signing key 1" +CPI_PATH="$WORK_DIR/register-member/chat.cpi" +curl --insecure -u admin:admin -F upload=@$CPI_PATH $API_URL/cpi/ +echo "\n" +read -p "Enter the chat CPI_ID from the returned body:" CPI_ID +echo "CPI_ID:" $CPI_ID +curl --insecure -u admin:admin $API_URL/cpi/status/$CPI_ID +echo "\n" +read -p "Enter the CPI_CHECKSUM from the returned body:" CPI_CHECKSUM + + +echo "\n---Create a Member virtual node---" +echo "\n" +read -p "Enter the X500_NAME from the returned body (Formatt: C=GB,L=London,O=Alice):" X500_NAME +curl --insecure -u admin:admin -d '{"request": {"cpiFileChecksum": "'$CPI_CHECKSUM'", "x500Name": "'$X500_NAME'"}}' $API_URL/virtualnode +echo "\n" +read -p "Enter the HOLDING_ID from the returned body:" HOLDING_ID +echo "HOLDING_ID:" $HOLDING_ID + + +echo "---Assign soft HSM---" +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/SESSION_INIT +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL'/keys/'$HOLDING_ID'/alias/'$HOLDING_ID'-session/category/SESSION_INIT/scheme/CORDA.ECDSA.SECP256R1' +echo "\n" +read -p "Enter the SESSION_KEY_ID from the returned body:" SESSION_KEY_ID +echo "SESSION_KEY_ID:" $SESSION_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/LEDGER +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$HOLDING_ID/alias/$HOLDING_ID-ledger/category/LEDGER/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the LEDGER_KEY_ID from the returned body:" LEDGER_KEY_ID +echo "LEDGER_KEY_ID:" $LEDGER_KEY_ID + + +echo "\n---Configure virtual node as network participant---" +curl -k -u admin:admin -X PUT -d '{"p2pTlsCertificateChainAlias": "p2p-tls-cert", "useClusterLevelTlsCertificateAndKey": true, "sessionKeyId": "'$SESSION_KEY_ID'"}' $API_URL/network/setup/$HOLDING_ID + + +echo "\n---Build registration context---" +REGISTRATION_CONTEXT='{ + "corda.session.key.id": "'$SESSION_KEY_ID'", + "corda.session.key.signature.spec": "SHA256withECDSA", + "corda.ledger.keys.0.id": "'$LEDGER_KEY_ID'", + "corda.ledger.keys.0.signature.spec": "SHA256withECDSA", + "corda.endpoints.0.connectionURL": "https://'$P2P_GATEWAY_HOST':'$P2P_GATEWAY_PORT'", + "corda.endpoints.0.protocolVersion": "1" +}' +REGISTRATION_REQUEST='{"memberRegistrationRequest":{"action": "requestJoin", "context": '$REGISTRATION_CONTEXT'}}' + + +echo "\n---Register Member VNode---" +curl --insecure -u admin:admin -d "$REGISTRATION_REQUEST" $API_URL/membership/$HOLDING_ID +echo "\n" +read -p "Enter the REGISTRATION_ID from the returned body:" REGISTRATION_ID +echo "REGISTRATION_ID:" $REGISTRATION_ID +curl --insecure -u admin:admin -X GET $API_URL/membership/$HOLDING_ID/$REGISTRATION_ID +echo "\n" +curl --insecure -u admin:admin -X GET $API_URL/members/$HOLDING_ID +echo "\n" +echo "\n---The Chat app CPI_CHECKSUM is : " +echo $CPI_CHECKSUM diff --git a/kotlin-samples/mgm-dynamic-network/Step4-more-member-onboard.sh b/kotlin-samples/mgm-dynamic-network/Step4-more-member-onboard.sh new file mode 100644 index 0000000..e531cf7 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/Step4-more-member-onboard.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +echo "---Set Env---" +RPC_HOST=localhost +RPC_PORT=8888 +P2P_GATEWAY_HOST=localhost +P2P_GATEWAY_PORT=8080 +API_URL="https://$RPC_HOST:$RPC_PORT/api/v1" +WORK_DIR=~/Corda/corda5/corda5-samples/kotlin-samples/mgm-dynamic-network/register-mgm +RUNTIME_OS=~/Corda/corda5/corda-runtime-os + + +echo "\n---Create a Member virtual node---" +echo "\n" +read -p "Enter the CPI_CHECKSUM from the returned body:" CPI_CHECKSUM +echo "\n" +read -p "Enter the X500_NAME from the returned body (Formatt: C=GB,L=London,O=Alice):" X500_NAME +curl --insecure -u admin:admin -d '{"request": {"cpiFileChecksum": "'$CPI_CHECKSUM'", "x500Name": "'$X500_NAME'"}}' $API_URL/virtualnode +echo "\n" +read -p "Enter the HOLDING_ID from the returned body:" HOLDING_ID +echo "HOLDING_ID:" $HOLDING_ID + + +echo "---Assign soft HSM---" +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/SESSION_INIT +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL'/keys/'$HOLDING_ID'/alias/'$HOLDING_ID'-session/category/SESSION_INIT/scheme/CORDA.ECDSA.SECP256R1' +echo "\n" +read -p "Enter the SESSION_KEY_ID from the returned body:" SESSION_KEY_ID +echo "SESSION_KEY_ID:" $SESSION_KEY_ID +curl --insecure -u admin:admin -X POST $API_URL/hsm/soft/$HOLDING_ID/LEDGER +echo "\n" +curl --insecure -u admin:admin -X POST $API_URL/keys/$HOLDING_ID/alias/$HOLDING_ID-ledger/category/LEDGER/scheme/CORDA.ECDSA.SECP256R1 +echo "\n" +read -p "Enter the LEDGER_KEY_ID from the returned body:" LEDGER_KEY_ID +echo "LEDGER_KEY_ID:" $LEDGER_KEY_ID + + +echo "\n---Configure virtual node as network participant---" +curl -k -u admin:admin -X PUT -d '{"p2pTlsCertificateChainAlias": "p2p-tls-cert", "useClusterLevelTlsCertificateAndKey": true, "sessionKeyId": "'$SESSION_KEY_ID'"}' $API_URL/network/setup/$HOLDING_ID + + +echo "\n---Build registration context---" +REGISTRATION_CONTEXT='{ + "corda.session.key.id": "'$SESSION_KEY_ID'", + "corda.session.key.signature.spec": "SHA256withECDSA", + "corda.ledger.keys.0.id": "'$LEDGER_KEY_ID'", + "corda.ledger.keys.0.signature.spec": "SHA256withECDSA", + "corda.endpoints.0.connectionURL": "https://'$P2P_GATEWAY_HOST':'$P2P_GATEWAY_PORT'", + "corda.endpoints.0.protocolVersion": "1" +}' +REGISTRATION_REQUEST='{"memberRegistrationRequest":{"action": "requestJoin", "context": '$REGISTRATION_CONTEXT'}}' + + +echo "\n---Register Member VNode---" +curl --insecure -u admin:admin -d "$REGISTRATION_REQUEST" $API_URL/membership/$HOLDING_ID +echo "\n" +read -p "Enter the REGISTRATION_ID from the returned body:" REGISTRATION_ID +echo "REGISTRATION_ID:" $REGISTRATION_ID +curl --insecure -u admin:admin -X GET $API_URL/membership/$HOLDING_ID/$REGISTRATION_ID +echo "\n" +curl --insecure -u admin:admin -X GET $API_URL/members/$HOLDING_ID + diff --git a/kotlin-samples/mgm-dynamic-network/build.gradle b/kotlin-samples/mgm-dynamic-network/build.gradle new file mode 100644 index 0000000..9708874 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/build.gradle @@ -0,0 +1,65 @@ +import static org.gradle.api.JavaVersion.VERSION_11 + +plugins { + id 'org.jetbrains.kotlin.jvm' + id 'net.corda.cordapp.cordapp-configuration' + id 'org.jetbrains.kotlin.plugin.jpa' + id 'java' + id 'maven-publish' + id 'csde' +} + +allprojects { + group 'com.r3.developers.csdetemplate' + version '1.0-SNAPSHOT' + + def javaVersion = VERSION_11 + + // Declare the set of Kotlin compiler options we need to build a CorDapp. + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + allWarningsAsErrors = false + + // Specify the version of Kotlin that we are that we will be developing. + languageVersion = '1.7' + // Specify the Kotlin libraries that code is compatible with + apiVersion = '1.7' + // Note that we Need to use a version of Kotlin that will be compatible with the Corda API. + // Currently that is developed in Kotlin 1.7 so picking the same version ensures compatibility with that. + + // Specify the version of Java to target. + jvmTarget = javaVersion + + // Needed for reflection to work correctly. + javaParameters = true + + // -Xjvm-default determines how Kotlin supports default methods. + // JetBrains currently recommends developers use -Xjvm-default=all + // https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-default/ + freeCompilerArgs += [ + "-Xjvm-default=all" + ] + } + } + + repositories { + // All dependencies are held in Maven Central + mavenCentral() + mavenLocal() + } + + tasks.withType(Test).configureEach { + useJUnitPlatform() + } + +} + +publishing { + publications { + maven(MavenPublication) { + artifactId "corda-mgm" + groupId project.group + artifact jar + } + } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/build.gradle b/kotlin-samples/mgm-dynamic-network/buildSrc/build.gradle new file mode 100644 index 0000000..d21dbee --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/build.gradle @@ -0,0 +1,22 @@ + +plugins { + id 'groovy-gradle-plugin' + id 'java' +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + + implementation "com.konghq:unirest-java:$unirestVersion" + implementation "com.konghq:unirest-objectmapper-jackson:$unirestVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion" + + implementation "net.corda:corda-base:$cordaApiVersion" +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/gradle.properties b/kotlin-samples/mgm-dynamic-network/buildSrc/gradle.properties new file mode 100644 index 0000000..1d2a0ce --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/gradle.properties @@ -0,0 +1,6 @@ +jacksonVersion = 2.13.4 +unirestVersion=3.13.10 + +# todo: why is this set in two places. +cordaApiVersion=5.0.0.665-Gecko1.0 + diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/settings.gradle b/kotlin-samples/mgm-dynamic-network/buildSrc/settings.gradle new file mode 100644 index 0000000..86ac012 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/settings.gradle @@ -0,0 +1 @@ +// File intentionally left blank diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/groovy/csde.gradle b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/groovy/csde.gradle new file mode 100644 index 0000000..a94ffa9 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/groovy/csde.gradle @@ -0,0 +1,248 @@ +// Note, IntelliJ does not recognise the imported Java Classes, hence they are +// highlighted in Red. However, they are recognised in the gradle compilation. + + +// todo: look at the declaration of the script variables, can they be combined with the declaration of the Project Context +// todo: investigate adding corda-cli to the class path then executing it directly - might not work as gradle has to set up the jar file, so its not their when you start. +// Todo: write a test flow runner helper function?? +// todo: rename deployCPIsHelper +// todo: add proper logging, rather than reading Stdout +// todo: add test corda running/live task +// todo: add a test to check docker is running and display error if not + halt start corda +// todo: add a clean corda task. +// todo: fix logging level and make it configurable. + + +import com.r3.csde.CordaLifeCycleHelper +import com.r3.csde.ProjectContext +import com.r3.csde.DeployCPIsHelper +import com.r3.csde.BuildCPIsHelper +import com.r3.csde.ProjectUtils +import com.r3.csde.CordaStatusQueries +import com.r3.csde.VNodesHelper +import com.r3.csde.NetworkConfig + +plugins { + id 'java-library' + id 'groovy' + id 'java' +} + + +configurations { + combinedWorker{ + canBeConsumed = false + canBeResolved= true + } + + myPostgresJDBC { + canBeConsumed = false + canBeResolved = true + } + + notaryServerCPB { + canBeConsumed = false + canBeResolved = true + } +} + +// Dependencies for supporting tools +dependencies { + combinedWorker "net.corda:corda-combined-worker:$combinedWorkerVersion" + myPostgresJDBC "org.postgresql:postgresql:$postgresqlVersion" + notaryServerCPB("com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-server:$cordaNotaryPluginsVersion") { + artifact { + classifier = 'package' + extension = 'cpb' + } + } + + implementation "org.codehaus.groovy:groovy-json:3.0.9" +} + +// task groupings +def cordaGroup = 'csde-corda' // corda lifecycle tasks +def cordappGroup = 'csde-cordapp' // tasks to build and deploy corDapps +def queriesGroup = 'csde-queries' // tasks which query corda status +def supportingGroup = 'supporting' // tasks which should be hidden from the csde user + + +def cordaBinDir = System.getenv("CSDE_CORDA_BIN") ?: System.getProperty('user.home') + "/.corda/corda5" +def cordaCliBinDir = System.getenv("CSDE_CORDA_CLI") ?:System.getProperty('user.home') + "/.corda/cli" +def cordaJDBCDir = cordaBinDir + "/jdbcDrivers" +def cordaNotaryServerDir = cordaBinDir + "/notaryserver" +def signingCertAlias="gradle-plugin-default-key" +// Get error if this is not a autotyped object +// def signingCertFName = "$rootDir/config/gradle-plugin-default-key.pem" +def signingCertFName = rootDir.toString() + "/config/gradle-plugin-default-key.pem" +def keystoreAlias = "my-signing-key" +def keystoreFName = devEnvWorkspace + "/signingkeys.pfx" +def keystoreCertFName = devEnvWorkspace + "/signingkey1.pem" +def combiWorkerPidCacheFile = devEnvWorkspace + "/CordaPID.dat" +// todo: can we rely on the build directory always being /workflow/build? aslo, is the +// workflow directory the correct place to build the cpb to. shoudl it be the main build directory. +def workflowBuildDir = rootDir.toString() + "/workflows/build" + + +// todo: Need to read things from cordapp plugin - the cordapp names will be changed by the user +def appCpiName = 'cpi name' +def notaryCpiName = 'CSDE Notary Server CPI' + + +// todo: there should be a better way to set up these project context variables. +def projectContext = new ProjectContext(project, + cordaClusterURL.toString(), + cordaRpcUser, + cordaRpcPasswd, + devEnvWorkspace, + // todo: why is this not obtained in the groovy def's abouve - its inconsistent. + new String("${System.getProperty("java.home")}/bin"), + dbContainerName, + cordaJDBCDir, + combiWorkerPidCacheFile, + signingCertAlias, + signingCertFName, + keystoreAlias, + keystoreFName, + keystoreCertFName, + appCpiName, + notaryCpiName, + devEnvWorkspace, + cordaCliBinDir, + cordaNotaryServerDir, + workflowBuildDir, + cordaNotaryPluginsVersion +) + +def networkConfig = new NetworkConfig("config/static-network-config.json") + +def utils = new ProjectUtils() + +// Initiate workspace folder + +tasks.register('projInit') { + group = supportingGroup + doLast { + mkdir devEnvWorkspace + } +} + + +// CordaLifeCycle tasks + +def cordaLifeCycle = new CordaLifeCycleHelper(projectContext) + +tasks.register("startCorda") { + group = cordaGroup + dependsOn('getDevCordaLite', 'getPostgresJDBC') + doLast { + mkdir devEnvWorkspace + cordaLifeCycle.startCorda() + } +} + +tasks.register("stopCorda") { + group = cordaGroup + doLast { + cordaLifeCycle.stopCorda() + } +} + +tasks.register("getPostgresJDBC") { + group = supportingGroup + doLast { + copy { + from configurations.myPostgresJDBC + into "$cordaJDBCDir" + } + } +} + +tasks.register("getDevCordaLite", Copy) { + group = supportingGroup + from configurations.combinedWorker + into cordaBinDir +} + + +// Corda status queries + +def cordaStatusQueries = new CordaStatusQueries(projectContext) + + +tasks.register('listVNodes') { + group = queriesGroup + doLast { + cordaStatusQueries.listVNodes() + } +} + +tasks.register('listCPIs') { + group = queriesGroup + doLast { + cordaStatusQueries.listCPIs() + } +} + +// Build CPI tasks + +def buildCPIsHelper = new BuildCPIsHelper(projectContext, networkConfig) + +tasks.register("1-createGroupPolicy") { + group = cordappGroup + dependsOn('projInit') + doLast { + buildCPIsHelper.createGroupPolicy() + } +} + +tasks.register("getNotaryServerCPB", Copy) { + group = supportingGroup + from configurations.notaryServerCPB + into cordaNotaryServerDir +} + +tasks.register('2-createKeystore') { + group = cordappGroup + dependsOn('projInit') + doLast { + buildCPIsHelper.createKeyStore() + } +} + +tasks.register('3-buildCPIs') { + group = cordappGroup + def dependsOnTasks = subprojects.collect {it.tasks.findByName("build") } + dependsOnTasks.add('1-createGroupPolicy') + dependsOnTasks.add('2-createKeystore') + dependsOnTasks.add('getNotaryServerCPB') + dependsOn dependsOnTasks + doLast{ + buildCPIsHelper.buildCPIs() + } +} + + +// deploy CPI tasks + +def deployCPIsHelper = new DeployCPIsHelper(projectContext) + +tasks.register('4-deployCPIs') { + group = cordappGroup + dependsOn('3-buildCPIs') + doLast { + deployCPIsHelper.deployCPIs() + } +} + +// Setup VNodes tasks + +def vNodesHelper = new VNodesHelper(projectContext, networkConfig ) + +tasks.register('5-vNodeSetup') { + group = cordappGroup + dependsOn('4-deployCPIs') + doLast { + vNodesHelper.vNodesSetup() + } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java new file mode 100644 index 0000000..6675087 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/BuildCPIsHelper.java @@ -0,0 +1,276 @@ +package com.r3.csde; + +import java.io.*; +import java.util.LinkedList; +import java.util.List; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class BuildCPIsHelper { + + public ProjectContext pc; + public ProjectUtils utils; + + public NetworkConfig config; + public BuildCPIsHelper(ProjectContext _pc, NetworkConfig _config) { + pc = _pc; + utils = new ProjectUtils(pc); + config = _config; + } + + public void createGroupPolicy() throws IOException { + + File groupPolicyFile = new File(String.format("%s/GroupPolicy.json", pc.devEnvWorkspace)); + File devnetFile = new File(pc.project.getRootDir() + "/" + config.getConfigFilePath()); + + + if (!groupPolicyFile.exists() || groupPolicyFile.lastModified() < devnetFile.lastModified()) { + + pc.out.println("createGroupPolicy: Creating a GroupPolicy"); + + List configX500Ids = config.getX500Names(); + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("mgm"); + commandList.add("groupPolicy"); + for (String id : configX500Ids) { + commandList.add("--name"); + commandList.add(id); + } + commandList.add("--endpoint-protocol=1"); + commandList.add("--endpoint=http://localhost:1080"); + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); + + // todo add exception catching + FileWriter fileWriter = new FileWriter(groupPolicyFile); + String line; + while (( line = reader.readLine()) != null){ + fileWriter.write(line + "\n"); + } + fileWriter.close(); + + } else { + pc.out.println("createPolicyTask: everything up to date; nothing to do."); + } + + } + + public void createKeyStore() throws IOException, InterruptedException { + + File keystoreFile = new File(pc.keystoreFName); + if(!keystoreFile.exists()) { + pc.out.println("createKeystore: Create a keystore"); + + generateKeyPair(); + addDefaultSigningKey(); + exportCert(); + + } else { + pc.out.println("createKeystore: keystore already created; nothing to do."); + } + + } + + private void generateKeyPair() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-genkeypair"); + cmdArray.add("-alias"); + cmdArray.add(pc.keystoreAlias); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-dname"); + cmdArray.add("CN=CPI Example - My Signing Key, O=CorpOrgCorp, L=London, C=GB"); + cmdArray.add("-keyalg"); + cmdArray.add("RSA"); + cmdArray.add("-storetype"); + cmdArray.add("pkcs12"); + cmdArray.add("-validity"); + cmdArray.add("4000"); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + private void addDefaultSigningKey() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-importcert"); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-noprompt"); + cmdArray.add("-alias"); + cmdArray.add(pc.signingCertAlias); + cmdArray.add("-file"); + cmdArray.add(pc.signingCertFName); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + } + + private void exportCert() throws IOException, InterruptedException { + + LinkedList cmdArray = new LinkedList<>(); + + cmdArray.add(pc.javaBinDir + "/keytool"); + cmdArray.add("-exportcert"); + cmdArray.add("-rfc"); + cmdArray.add("-alias"); + cmdArray.add(pc.keystoreAlias); + cmdArray.add("-keystore"); + cmdArray.add(pc.keystoreFName); + cmdArray.add("-storepass"); + cmdArray.add("keystore password"); + cmdArray.add("-file"); + cmdArray.add(pc.keystoreCertFName); + + ProcessBuilder pb = new ProcessBuilder(cmdArray); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + public void buildCPIs() throws IOException, InterruptedException, CsdeException { + createCorDappCPI(); + createNotaryCPI(); + } + + private void createCorDappCPI() throws IOException, InterruptedException, CsdeException { + + String appCPIFilePath = pc.workflowBuildDir + "/" + + pc.project.getRootProject().getName() + "-" + + pc.project.getVersion() + ".cpi"; + + File appCPIFile = new File(appCPIFilePath); + appCPIFile.delete(); + + File srcDir = new File(pc.workflowBuildDir + "/libs"); + File[] appCPBs = srcDir.listFiles(( x , name ) -> name.endsWith(".cpb")); + if (appCPBs == null) throw new CsdeException("Expecting exactly one CPB but no CPB found."); + if (appCPBs.length != 1) throw new CsdeException("Expecting exactly one CPB but more than one found."); + + pc.out.println("appCpbs:"); + pc.out.println(appCPBs[0].getAbsolutePath()); + + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("package"); + commandList.add("create-cpi"); + commandList.add("--cpb"); + commandList.add(appCPBs[0].getAbsolutePath()); + commandList.add("--group-policy"); + commandList.add(pc.devEnvWorkspace + "/GroupPolicy.json"); + commandList.add("--cpi-name"); + commandList.add(pc.appCPIName); + commandList.add("--cpi-version"); + commandList.add(pc.project.getVersion().toString()); + commandList.add("--file"); + commandList.add(appCPIFilePath); + commandList.add("--keystore"); + commandList.add(pc.devEnvWorkspace + "/signingkeys.pfx"); + commandList.add("--storepass"); + commandList.add("keystore password"); + commandList.add("--key"); + commandList.add("my-signing-key"); // todo: should be passed as context property + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + +// todo: work out how to capture error code better than the following code + +// BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); +// File tempOutputFile = new File(String.format("%s/tempOutput.txt", pc.devEnvWorkspace)); +// tempOutputFile.delete(); +// FileWriter fileWriter = new FileWriter(tempOutputFile); +// String line; +// while (( line = reader.readLine()) != null){ +// fileWriter.write(line + "\n"); +// } +// fileWriter.close(); + + } + + private void createNotaryCPI() throws CsdeException, IOException, InterruptedException { + + String notaryCPIFilePath = pc.workflowBuildDir + "/" + + pc.notaryCPIName.replace(' ', '-').toLowerCase() + "-" + + pc.project.getVersion() + ".cpi"; + + File notaryCPIFile = new File(notaryCPIFilePath); + notaryCPIFile.delete(); + + File srcDir = new File(pc.cordaNotaryServiceDir); + File[] notaryCPBs = srcDir.listFiles(( x , name ) -> name.endsWith(".cpb") && name.contains(pc.cordaNotaryPluginsVersion)); + if (notaryCPBs == null) throw new CsdeException("Expecting exactly one notary CPB but no CPB found."); + if (notaryCPBs.length != 1) throw new CsdeException("Expecting exactly one notary CPB but more than one found."); + + pc.out.println("notaryCpbs:"); + pc.out.println(notaryCPBs[0]); + + LinkedList commandList = new LinkedList<>(); + + commandList.add(String.format("%s/java", pc.javaBinDir)); + commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir)); + commandList.add("-jar"); + commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir)); + commandList.add("package"); + commandList.add("create-cpi"); + commandList.add("--cpb"); + commandList.add(notaryCPBs[0].getAbsolutePath()); + commandList.add("--group-policy"); + commandList.add(pc.devEnvWorkspace + "/GroupPolicy.json"); + commandList.add("--cpi-name"); + commandList.add(pc.notaryCPIName); + commandList.add("--cpi-version"); + commandList.add(pc.project.getVersion().toString()); + commandList.add("--file"); + commandList.add(notaryCPIFilePath); + commandList.add("--keystore"); + commandList.add(pc.devEnvWorkspace + "/signingkeys.pfx"); + commandList.add("--storepass"); + commandList.add("keystore password"); + commandList.add("--key"); + commandList.add("my-signing-key"); + + ProcessBuilder pb = new ProcessBuilder(commandList); + pb.redirectErrorStream(true); + Process proc = pb.start(); + proc.waitFor(); + + } + + // todo: this might be needed for improved logging + private void printCmdArray(LinkedList cmdArray) { + for (int i = 0; i < cmdArray.size(); i++) { + pc.out.print(cmdArray.get(i) + " "); + } + } + +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java new file mode 100644 index 0000000..daa78a6 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaLifeCycleHelper.java @@ -0,0 +1,93 @@ +package com.r3.csde; + +import kong.unirest.Unirest; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Scanner; + +/** + * Manages Bringing corda up, testing for liveness and taking corda down + */ +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class CordaLifeCycleHelper { + + ProjectContext pc; + ProjectUtils utils; + + public CordaLifeCycleHelper(ProjectContext _pc) { + pc = _pc; + utils = new ProjectUtils(pc); + Unirest.config().verifySsl(false); + } + + public void startCorda() throws IOException { + PrintStream pidStore = new PrintStream(new FileOutputStream(pc.cordaPidCache)); + File combinedWorkerJar = pc.project.getConfigurations().getByName("combinedWorker").getSingleFile(); + + // Manual version of the command to start postgres (for reference): + // docker run -d --rm -p5432:5432 --name CSDEpostgresql -e POSTGRES_DB=cordacluster -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password postgres:latest + + new ProcessBuilder( + "docker", + "run", "-d", "--rm", + "-p", "5432:5432", + "--name", pc.dbContainerName, + "-e", "POSTGRES_DB=cordacluster", + "-e", "POSTGRES_USER=postgres", + "-e", "POSTGRES_PASSWORD=password", + "postgres:latest").start(); + + // todo: we should poll for readiness not wait 10 seconds, see https://r3-cev.atlassian.net/browse/CORE-11626 + utils.rpcWait(10000); + + ProcessBuilder procBuild = new ProcessBuilder(pc.javaBinDir + "/java", + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", + "-DsecurityMangerEnabled=false", + "-Dlog4j.configurationFile=" + pc.project.getRootDir() + "/config/log4j2.xml", + "-Dco.paralleluniverse.fibers.verifyInstrumentation=true", + "-jar", + combinedWorkerJar.toString(), + "--instance-id=0", + "-mbus.busType=DATABASE", + "-spassphrase=password", + "-ssalt=salt", + "-ddatabase.user=user", + "-ddatabase.pass=password", + "-ddatabase.jdbc.url=jdbc:postgresql://localhost:5432/cordacluster", + "-ddatabase.jdbc.directory="+pc.JDBCDir); + + procBuild.redirectErrorStream(true); + Process proc = procBuild.start(); + pidStore.print(proc.pid()); + pc.out.println("Corda Process-id="+proc.pid()); + proc.getInputStream().transferTo(pc.out); + + // todo: we should poll for readiness before completing the startCorda task, see https://r3-cev.atlassian.net/browse/CORE-11625 + } + + + public void stopCorda() throws IOException, CsdeException { + File cordaPIDFile = new File(pc.cordaPidCache); + if(cordaPIDFile.exists()) { + Scanner sc = new Scanner(cordaPIDFile); + long pid = sc.nextLong(); + pc.out.println("pid to kill=" + pid); + + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + new ProcessBuilder("Powershell", "-Command", "Stop-Process", "-Id", Long.toString(pid), "-PassThru").start(); + } else { + new ProcessBuilder("kill", "-9", Long.toString(pid)).start(); + } + + Process proc = new ProcessBuilder("docker", "stop", pc.dbContainerName).start(); + + cordaPIDFile.delete(); + } + else { + throw new CsdeException("Cannot stop the Combined worker\nCached process ID file " + pc.cordaPidCache + " missing.\nWas the combined worker not started?"); + } + } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java new file mode 100644 index 0000000..95072ba --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CordaStatusQueries.java @@ -0,0 +1,64 @@ +package com.r3.csde; + +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import kong.unirest.HttpResponse; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class CordaStatusQueries { + + ProjectContext pc; + public CordaStatusQueries(ProjectContext _pc){ pc = _pc; } + + + public HttpResponse getVNodeInfo() { + Unirest.config().verifySsl(false); + return Unirest.get(pc.baseURL + "/api/v1/virtualnode/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + } + public void listVNodesVerbose() { + HttpResponse vnodeResponse = getVNodeInfo(); + pc.out.println("VNodes:\n" + vnodeResponse.getBody().toPrettyString()); + } + + // X500Name, shorthash, cpiname + public void listVNodes() { + HttpResponse vnodeResponse = getVNodeInfo(); + + JSONArray virtualNodesJson = (JSONArray) vnodeResponse.getBody().getObject().get("virtualNodes"); + pc.out.println("X500 Name\tHolding identity short hash\tCPI Name"); + for(Object o: virtualNodesJson){ + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("holdingIdentity"); + JSONObject cpiObj = ((JSONObject) o).getJSONObject("cpiIdentifier"); + pc.out.print("\"" + idObj.get("x500Name") + "\""); + pc.out.print("\t\"" + idObj.get("shortHash") + "\""); + pc.out.println("\t\"" + cpiObj.get("cpiName") + "\""); + } + } + } + + public HttpResponse getCpiInfo() { + Unirest.config().verifySsl(false); + return Unirest.get(pc.baseURL + "/api/v1/cpi/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + } + + public void listCPIs() { + HttpResponse cpiResponse = getCpiInfo(); + JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); + + for(Object o: jArray){ + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("id"); + pc.out.print("cpiName=" + idObj.get("cpiName")); + pc.out.println(", cpiVersion=" + idObj.get("cpiVersion")); + } + } + } + +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CsdeException.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CsdeException.java new file mode 100644 index 0000000..72f8fea --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/CsdeException.java @@ -0,0 +1,10 @@ +package com.r3.csde; + +public class CsdeException extends Exception { + public CsdeException(String message, Throwable cause) { + super(message, cause); + } + public CsdeException(String message){ + super(message); + } +} \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java new file mode 100644 index 0000000..fe1362b --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/DeployCPIsHelper.java @@ -0,0 +1,187 @@ +package com.r3.csde; + +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import kong.unirest.HttpResponse; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintStream; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_OK; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class DeployCPIsHelper { + + public DeployCPIsHelper() { + } + ProjectContext pc; + CordaStatusQueries queries; + ProjectUtils utils; + + public DeployCPIsHelper(ProjectContext _pc) { + pc = _pc; + queries = new CordaStatusQueries(pc); + utils = new ProjectUtils(pc); + } + + public void deployCPIs() throws FileNotFoundException, CsdeException{ + + uploadCertificate(pc.signingCertAlias, pc.signingCertFName); + uploadCertificate(pc.keystoreAlias, pc.keystoreCertFName); + + // todo: make consistent with other string building code - remove String.format + String appCPILocation = String.format("%s/%s-%s.cpi", + pc.workflowBuildDir, + pc.project.getName(), + pc.project.getVersion()); + deployCPI(appCPILocation, pc.appCPIName,pc.project.getVersion().toString()); + + String notaryCPILocation = String.format("%s/%s-%s.cpi", + pc.workflowBuildDir, + pc.notaryCPIName.replace(' ','-').toLowerCase(), + pc.project.getVersion()); + deployCPI(notaryCPILocation, + pc.notaryCPIName, + pc.project.getVersion().toString(), + "-NotaryServer" ); + + } + + public void uploadCertificate(String certAlias, String certFName) { + Unirest.config().verifySsl(false); + HttpResponse uploadResponse = Unirest.put(pc.baseURL + "/api/v1/certificates/cluster/code-signer") + .field("alias", certAlias) + .field("certificate", new File(certFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + pc.out.println("Certificate/key upload, alias "+certAlias+" certificate/key file "+certFName); + pc.out.println(uploadResponse.getBody().toPrettyString()); + } + + public void forceuploadCPI(String cpiFName) throws FileNotFoundException, CsdeException { + forceuploadCPI(cpiFName, ""); + } + + public void forceuploadCPI(String cpiFName, String uploadStatusQualifier) throws FileNotFoundException, CsdeException { + Unirest.config().verifySsl(false); + HttpResponse jsonResponse = Unirest.post(pc.baseURL + "/api/v1/maintenance/virtualnode/forcecpiupload/") + .field("upload", new File(cpiFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if(jsonResponse.getStatus() == HTTP_OK) { + String id = (String) jsonResponse.getBody().getObject().get("id"); + pc.out.println("get id:\n" +id); + HttpResponse statusResponse = uploadStatus(id); + + if (statusResponse.getStatus() == HTTP_OK) { + PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream( + pc.CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json" ))); + cpiUploadStatus.print(statusResponse.getBody()); + pc.out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); + } else { + utils.reportError(statusResponse); + } + } + else { + utils.reportError(jsonResponse); + } + } + + private boolean uploadStatusRetry(HttpResponse response) { + int status = response.getStatus(); + JsonNode body = response.getBody(); + // Do not retry on success // todo: need to think through the possible outcomes here - what if the bodyTitle is null, it won't retry + if(status == HTTP_OK) { + // Keep retrying until we get "OK" may move through "Validating upload", "Persisting CPI" + return !(body.getObject().get("status").equals("OK")); + } + else if (status == HTTP_BAD_REQUEST){ + String bodyTitle = response.getBody().getObject().getString("title"); + return bodyTitle != null && bodyTitle.matches("No such requestId=[-0-9a-f]+"); + } + return false; + } + + public HttpResponse uploadStatus(String requestId) { + HttpResponse statusResponse = null; + do { + utils.rpcWait(1000); + statusResponse = Unirest + .get(pc.baseURL + "/api/v1/cpi/status/" + requestId + "/") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + pc.out.println("Upload status="+statusResponse.getStatus()+", status query response:\n"+statusResponse.getBody().toPrettyString()); + } + while(uploadStatusRetry(statusResponse)); + + return statusResponse; + } + + public void deployCPI(String cpiFName, String cpiName, String cpiVersion) throws FileNotFoundException, CsdeException { + deployCPI(cpiFName, cpiName, cpiVersion, ""); + } + + public void deployCPI(String cpiFName, + String cpiName, + String cpiVersion, + String uploadStatusQualifier) throws FileNotFoundException, CsdeException { + // todo: where is the primary instance declared? + Unirest.config().verifySsl(false); + + HttpResponse cpiResponse = queries.getCpiInfo(); + JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); + + int matches = 0; + for(Object o: jArray.toList() ) { + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("id"); + if((idObj.get("cpiName").toString().equals(cpiName) + && idObj.get("cpiVersion").toString().equals(cpiVersion))) { + matches++; + } + } + } + pc.out.println("Matching CPIS="+matches); + + if(matches == 0) { + HttpResponse uploadResponse = Unirest.post(pc.baseURL + "/api/v1/cpi/") + .field("upload", new File(cpiFName)) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + JsonNode body = uploadResponse.getBody(); + + int status = uploadResponse.getStatus(); + + pc.out.println("Upload Status:" + status); + pc.out.println("Pretty print the body\n" + body.toPrettyString()); + + // We expect the id field to be a string. + if (status == HTTP_OK) { + String id = (String) body.getObject().get("id"); + pc.out.println("get id:\n" + id); + + HttpResponse statusResponse = uploadStatus(id); + if (statusResponse.getStatus() == HTTP_OK) { + PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream( + pc.CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json" ))); + cpiUploadStatus.print(statusResponse.getBody()); + pc.out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); + } else { + utils.reportError(statusResponse); + } + } else { + utils.reportError(uploadResponse); + } + } + else { + pc.out.println("CPI already uploaded doing a 'force' upload."); + forceuploadCPI(cpiFName, uploadStatusQualifier); + } + } + +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java new file mode 100644 index 0000000..e2aa2fe --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/NetworkConfig.java @@ -0,0 +1,40 @@ +package com.r3.csde; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * This class reads the network config from the json file and makes it available as a list of VNodes. + */ +public class NetworkConfig { + + private List vNodes; + private String configFilePath; + + public NetworkConfig(String _configFilePath) throws CsdeException { + configFilePath = _configFilePath; + + ObjectMapper mapper = new ObjectMapper(); + try { + FileInputStream in = new FileInputStream(configFilePath); + vNodes = mapper.readValue(in, new TypeReference>() { + }); + } catch (Exception e) { + throw new CsdeException("Failed to read static network configuration file, with exception: " + e); + } + } + + String getConfigFilePath() { return configFilePath; } + + List getVNodes() { return vNodes; } + + List getX500Names() { + return vNodes.stream().map(vn -> vn.getX500Name()).collect(Collectors.toList()); + } + +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectContext.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectContext.java new file mode 100644 index 0000000..6610697 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectContext.java @@ -0,0 +1,84 @@ +package com.r3.csde; + +import org.gradle.api.Project; +import java.io.PrintStream; +import java.util.Map; + +public class ProjectContext { + Project project; + String baseURL = "https://localhost:8888"; + String rpcUser = "admin"; + String rpcPasswd = "admin"; + String workspaceDir = "workspace"; + int retryWaitMs = 1000; + PrintStream out = System.out; + String CPIUploadStatusBaseName = "CPIFileStatus.json"; + String NotaryCPIUploadBaseName = "CPIFileStatus-NotaryServer.json"; + String CPIUploadStatusFName; + String NotaryCPIUploadStatusFName; + String javaBinDir; + String cordaPidCache = "CordaPIDCache.dat"; + String dbContainerName; + String JDBCDir; + String combinedWorkerBinRe; + Map notaryRepresentatives = null; + String signingCertAlias; + String signingCertFName; + String keystoreAlias; + String keystoreFName; + String keystoreCertFName; + String appCPIName; + String notaryCPIName; + String devEnvWorkspace; + String cordaCliBinDir; + String cordaNotaryServiceDir; + String workflowBuildDir; + String cordaNotaryPluginsVersion; + + public ProjectContext (Project inProject, + String inBaseUrl, + String inRpcUser, + String inRpcPasswd, + String inWorkspaceDir, + String inJavaBinDir, + String inDbContainerName, + String inJDBCDir, + String inCordaPidCache, + String inSigningCertAlias, + String inSigningCertFName, + String inKeystoreAlias, + String inKeystoreFName, + String inKeystoreCertFName, + String inAppCPIName, + String inNotaryCPIName, + String inDevEnvWorkspace, + String inCordaCLiBinDir, + String inCordaNotaryServiceDir, + String inWorkflowBuildDir, + String inCordaNotaryPluginsVersion + ) { + project = inProject; + baseURL = inBaseUrl; + rpcUser = inRpcUser; + rpcPasswd = inRpcPasswd; + workspaceDir = inWorkspaceDir; + javaBinDir = inJavaBinDir; + cordaPidCache = inCordaPidCache; + dbContainerName = inDbContainerName; + JDBCDir = inJDBCDir; + CPIUploadStatusFName = workspaceDir + "/" + CPIUploadStatusBaseName; + NotaryCPIUploadStatusFName = workspaceDir + "/" + NotaryCPIUploadBaseName; + signingCertAlias = inSigningCertAlias; + signingCertFName = inSigningCertFName; + keystoreAlias = inKeystoreAlias; + keystoreFName = inKeystoreFName; + keystoreCertFName = inKeystoreCertFName; + appCPIName = inAppCPIName; + notaryCPIName = inNotaryCPIName; + devEnvWorkspace = inDevEnvWorkspace; + cordaCliBinDir = inCordaCLiBinDir; + cordaNotaryServiceDir = inCordaNotaryServiceDir; + workflowBuildDir = inWorkflowBuildDir; + cordaNotaryPluginsVersion = inCordaNotaryPluginsVersion; + } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java new file mode 100644 index 0000000..dbc3dd8 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/ProjectUtils.java @@ -0,0 +1,36 @@ +package com.r3.csde; + +import kong.unirest.HttpResponse; +import kong.unirest.JsonNode; + +import static java.lang.Thread.sleep; + +// todo: This class needs refactoring, see https://r3-cev.atlassian.net/browse/CORE-11624 +public class ProjectUtils { + + ProjectContext pc; + + ProjectUtils(ProjectContext _pc) { + pc = _pc; + } + + void rpcWait(int millis) { + try { + sleep(millis); + } + catch(InterruptedException e) { + throw new UnsupportedOperationException("Interrupts not supported.", e); + } + } + + public void reportError(HttpResponse response) throws CsdeException { + + pc.out.println("*** *** ***"); + pc.out.println("Unexpected response from Corda"); + pc.out.println("Status="+ response.getStatus()); + pc.out.println("*** Headers ***\n"+ response.getHeaders()); + pc.out.println("*** Body ***\n"+ response.getBody()); + pc.out.println("*** *** ***"); + throw new CsdeException("Error: unexpected response from Corda."); + } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNode.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNode.java new file mode 100644 index 0000000..d77d85c --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNode.java @@ -0,0 +1,25 @@ +package com.r3.csde; + + +/** + * This class is a representation of a Vnode used to express the vNodes required on the network. + */ + +public class VNode { + private String x500Name; + private String cpi; + + private String serviceX500Name; + + public VNode() { } + + public String getX500Name(){ return x500Name; } + public void setX500Name(String _x500Name) { x500Name = _x500Name; } + + public String getCpi() { return cpi; } + public void setCpi(String _cpi) { cpi = _cpi; } + + public String getServiceX500Name() { return serviceX500Name; } + public void setServiceX500Name(String _name) { serviceX500Name = _name; } + +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java new file mode 100644 index 0000000..4656ce9 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/VNodesHelper.java @@ -0,0 +1,288 @@ +package com.r3.csde; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.r3.csde.dtos.*; +import kong.unirest.HttpResponse; +import kong.unirest.JsonNode; +import kong.unirest.Unirest; +import java.io.FileInputStream; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import static java.net.HttpURLConnection.HTTP_OK; + +/** + * The VNodesHelper class is used to create and register the Vnodes specified in the static-network-config.json file. + */ + +public class VNodesHelper { + private ProjectContext pc; + private ProjectUtils utils; + private NetworkConfig config; + private ObjectMapper mapper; + + public VNodesHelper(ProjectContext _pc, NetworkConfig _config) { + pc = _pc; + utils = new ProjectUtils(pc); + config = _config; + Unirest.config().verifySsl(false); + mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + /** + * Entry point for setting up vnodes, called from the 5-vNodeSetup task in csde.gradle + */ + public void vNodesSetup() throws CsdeException { + List requiredNodes = config.getVNodes(); + createVNodes(requiredNodes); + registerVNodes(requiredNodes); + } + + /** + * Creates vnodes specified in the config if they don't already exist. + * @param requiredNodes represents the list of VNodes as specified in the network Config json file (static-network-config.json) + */ + private void createVNodes(List requiredNodes) throws CsdeException { + + // Get existing Nodes. + List existingVNodes = getExistingNodes(); + + // Check if each required vnode already exist, if not create it. + for (VNode vn : requiredNodes) { + // Match on x500 and + List matches = existingVNodes + .stream() + .filter(existing -> + existing.getHoldingIdentity().getX500Name().equals( vn.getX500Name()) && + existing.getCpiIdentifier().getCpiName().equals(vn.getCpi())) + .collect(Collectors.toList()); + + if (matches.size() == 0) { + createVNode(vn); + } + } + } + + /** + * Gets a list of the virtual nodes which have already been created. + * @return a list of the virtual nodes which have already been created. + */ + private List getExistingNodes () throws CsdeException { + + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/virtualnode") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Failed to get Existing vNodes, response status: "+ response.getStatus()); + } + + try { + return mapper.readValue(response.getBody().toString(), VirtualNodesDTO.class).getVirtualNodes(); + } catch (Exception e){ + throw new CsdeException("Failed to get Existing vNodes with exception: " + e); + } + } + + /** + * Creates a vnode on the Corda cluster from the VNode info. + * @param vNode represents a virtual node using VNode Class. + */ + private void createVNode(VNode vNode) throws CsdeException { + + pc.out.println("Creating virtual node for "+ vNode.getX500Name()); + + // Reads the current CPIFileChecksum value and checks it has been uploaded. + String cpiCheckSum = getCpiCheckSum(vNode); + if (!checkCpiUploaded(cpiCheckSum)) throw new CsdeException("Cpi " + cpiCheckSum + " not uploaded."); + + // Creates the vnode on Cluster + HttpResponse response = Unirest.post(pc.baseURL + "/api/v1/virtualnode") + .body("{ \"request\" : { \"cpiFileChecksum\": \"" + cpiCheckSum + "\", \"x500Name\": \"" + vNode.getX500Name() + "\" } }") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Creation of virtual node failed with response status: " + response.getStatus()); + } + } + + /** + * Reads the latest CPI checksums from file. + * @param vNode represents a virtual node using VNode Class. + */ + private String getCpiCheckSum(VNode vNode) throws CsdeException { + + try { + String file = (vNode.getServiceX500Name() == null) ? pc.CPIUploadStatusFName : pc.NotaryCPIUploadStatusFName; + FileInputStream in = new FileInputStream(file); + CPIFileStatusDTO statusDTO = mapper.readValue(in, CPIFileStatusDTO.class); + return statusDTO.getCpiFileChecksum().toString(); + } catch (Exception e) { + throw new CsdeException("Failed to read CPI checksum from file, with error: " + e); + } + } + + private boolean checkCpiUploaded(String cpiCheckSum) throws CsdeException { + + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/cpi") + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) { + throw new CsdeException("Failed to check cpis, response status: " + response.getStatus()); + } + + try { + GetCPIsResponseDTO cpisResponse = mapper.readValue( + response.getBody().toString(), GetCPIsResponseDTO.class); + + for (CpiMetadataDTO cpi: cpisResponse.getCpis()) { + if (Objects.equals(cpi.getCpiFileChecksum(), cpiCheckSum)) { + return true; + } + } + // Returns false if no cpis were returned or the cpiCheckSum didnt' match ay results. + return false; + } catch (Exception e) { + throw new CsdeException("Failed to check cpis with exception: " + e); + } + } + + /** + * Checks if the required virtual nodes have been registered and if not registers them. + * @param requiredNodes represents the list of VNodes as specified in the network Config json file (static-network-config.json) + */ + private void registerVNodes(List requiredNodes) throws CsdeException { + + // There appears to be a delay between the successful post /virtualnodes synchronous call and the + // vnodes being returned in the GET /virtualnodes call. Putting a thread wait here as a quick fix + // as this will move to async mechanism post beta2. + utils.rpcWait(2000); + List existingVNodes = getExistingNodes(); + + for (VNode vn: requiredNodes) { + // Match on x500 and cpi name + List matches = existingVNodes + .stream() + .filter(existing -> + existing.getHoldingIdentity().getX500Name().equals( vn.getX500Name()) && + existing.getCpiIdentifier().getCpiName().equals(vn.getCpi())) + .collect(Collectors.toList()); + + if (matches.size() == 0) { + throw new CsdeException("Registration failed because virtual node for '" + vn.getX500Name() + "' not found."); + } else if (matches.size() >1 ) { + throw new CsdeException(("Registration failed because more than virtual node for '" + vn.getX500Name() + "'")); + } + + String shortHash = matches.get(0).getHoldingIdentity().getShortHash(); + + if (!checkVNodeIsRegistered(shortHash)) { + registerVnode(vn, shortHash); + } + } + } + + /** + * Registers a virtual node. + * @param vnode represents the vnode to be registered. + * @param shortHash the shortHash of the virtual node to register. + */ + private void registerVnode(VNode vnode, String shortHash) throws CsdeException { + + pc.out.println("Registering vNode: " + vnode.getX500Name() + " with shortHash: " + shortHash); + + // Configure the registration body (notary vs non notary) + String registrationBody; + if (vnode.getServiceX500Name() == null) { + registrationBody = + "{ \"action\" : \"requestJoin\"," + + " \"context\" : {" + + " \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\" } }"; + } else { + registrationBody = + "{ \"action\" : \"requestJoin\"," + + " \"context\" : {" + + " \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\", " + + " \"corda.roles.0\" : \"notary\", " + + " \"corda.notary.service.name\" : \"" + vnode.getServiceX500Name() + "\", " + + " \"corda.notary.service.plugin\" : \"net.corda.notary.NonValidatingNotary\" } }"; + } + + HttpResponse response = Unirest.post(pc.baseURL + "/api/v1/membership/" + shortHash) + .body(registrationBody) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() == HTTP_OK) { + pc.out.println("Membership requested for node " + shortHash); + } else { + throw new CsdeException("Failed to register virtual node " + shortHash + ", response status: " + response.getStatus() ); + } + + // wait until Vnode registered + pollForRegistration(shortHash, 30000, 1000); + + } + + /** + * Checks if a virtual node with given shortHash has been registered + * @param shortHash shortHash of the node which is being checked. + * @return returns true if the vnode is registered + */ + private boolean checkVNodeIsRegistered(String shortHash) throws CsdeException { + + // Queries registration status for vnode. + HttpResponse response = Unirest.get(pc.baseURL + "/api/v1/membership/" + shortHash) + .basicAuth(pc.rpcUser, pc.rpcPasswd) + .asJson(); + + if (response.getStatus() != HTTP_OK) + throw new CsdeException("Failed to check registration status for virtual node '" + shortHash + + "' response status: " + response.getStatus()); + + try { + // If the response body is not empty check all previous requests for an "APPROVED" + if (!response.getBody().getArray().isEmpty()) { + List requests = mapper.readValue( + response.getBody().toString(), new TypeReference<>() { + }); + for (RegistrationRequestProgressDTO request : requests) { + if (Objects.equals(request.getRegistrationStatus(), "APPROVED")) { + return true; + } + } + } + // Returns false if array was empty or "APPROVED" wasn't found + return false; + + } catch (Exception e) { + throw new CsdeException("Failed to check registration status for " + shortHash + + " with exception " + e); + } + } + + /** + * Polls for registration of a vnode + * @param shortHash short hash of the node which is being poled for + * @param duration the number of milliseconds before the pollign times out + * @param cooldown the number of milliseconds in between poll attempts. + */ + private void pollForRegistration(String shortHash, int duration, int cooldown) throws CsdeException { + + int timer = 0; + while (timer < duration ) { + if (checkVNodeIsRegistered(shortHash)){ + pc.out.println("Vnode " + shortHash +" registered."); + return; + } + utils.rpcWait(cooldown); + timer += cooldown; + } + throw new CsdeException("Vnode " + shortHash + " failed to register in " + duration + " milliseconds"); + } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java new file mode 100644 index 0000000..1c6e118 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CPIFileStatusDTO.java @@ -0,0 +1,16 @@ +package com.r3.csde.dtos; + +public class CPIFileStatusDTO { + private String status; + private String cpiFileChecksum; + + public CPIFileStatusDTO() {} + + public String getStatus() { return status; } + + public void setStatus(String status) { this.status = status; } + + public String getCpiFileChecksum() { return cpiFileChecksum; } + + public void setCpiFileChecksum(String cpiFileChecksum) { this.cpiFileChecksum = cpiFileChecksum; } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java new file mode 100644 index 0000000..e5d5dde --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiIdentifierDTO.java @@ -0,0 +1,23 @@ +package com.r3.csde.dtos; + +public class CpiIdentifierDTO { + + // Note, these DTOs don't cover all returned values, just the ones required for CSDE + private String cpiName; + private String cpiVersion; + private String signerSummaryHash; + + public CpiIdentifierDTO() { } + + public String getCpiName() { return cpiName; } + + public void setCpiName(String cpiName) { this.cpiName = cpiName; } + + public String getCpiVersion() { return cpiVersion; } + + public void setCpiVersion(String cpiVersion) { this.cpiVersion = cpiVersion; } + + public String getSignerSummaryHash() { return signerSummaryHash; } + + public void setSignerSummaryHash(String signerSummaryHash) { this.signerSummaryHash = signerSummaryHash; } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java new file mode 100644 index 0000000..ef89c7d --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/CpiMetadataDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class CpiMetadataDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String cpiFileChecksum; + private CpiIdentifierDTO id; + + public CpiMetadataDTO() {} + + public String getCpiFileChecksum() { return cpiFileChecksum; } + + public void setCpiFileChecksum(String cpiFileChecksum) { this.cpiFileChecksum = cpiFileChecksum; } + + public CpiIdentifierDTO getId() { return id; } + + public void setId(CpiIdentifierDTO id) { this.id = id; } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java new file mode 100644 index 0000000..a16e9a1 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/GetCPIsResponseDTO.java @@ -0,0 +1,14 @@ +package com.r3.csde.dtos; + +import java.util.List; + +public class GetCPIsResponseDTO { + + private List cpis; + + public GetCPIsResponseDTO() {} + + public List getCpis() { return cpis; } + + public void setCpis(List cpis) { this.cpis = cpis; } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java new file mode 100644 index 0000000..48c12c0 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/HoldingIdentityDTO.java @@ -0,0 +1,27 @@ +package com.r3.csde.dtos; + +public class HoldingIdentityDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String fullHash; + private String groupId; + private String shortHash; + private String x500Name; + + public HoldingIdentityDTO() {} + + public String getFullHash() { return fullHash; } + + public void setFullHash(String fullHash) { this.fullHash = fullHash; } + + public String getGroupId() { return groupId; } + + public void setGroupID(String groupID) { this.groupId = groupId; } + + public String getShortHash() { return shortHash; } + + public void setShortHash(String shortHash) { this.shortHash = shortHash; } + + public String getX500Name() { return x500Name; } + + public void setX500Name(String x500Name) { this.x500Name = x500Name; } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java new file mode 100644 index 0000000..b3e63b0 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/RegistrationRequestProgressDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class RegistrationRequestProgressDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private String registrationStatus; + private String reason; + + public RegistrationRequestProgressDTO() {} + + public String getRegistrationStatus() { return registrationStatus; } + + public void setRegistrationStatus(String registrationStatus) { this.registrationStatus = registrationStatus; } + + public String getReason() { return reason; } + + public void setReason(String reason) { this.reason = reason; } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java new file mode 100644 index 0000000..152cf3e --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodeInfoDTO.java @@ -0,0 +1,17 @@ +package com.r3.csde.dtos; + +public class VirtualNodeInfoDTO { + // Note, these DTOs don't cover all returned values, just the ones required for CSDE. + private HoldingIdentityDTO holdingIdentity; + private CpiIdentifierDTO cpiIdentifier; + + public VirtualNodeInfoDTO() {} + + public HoldingIdentityDTO getHoldingIdentity() { return holdingIdentity; } + + public void setHoldingIdentity(HoldingIdentityDTO holdingIdentity) { this.holdingIdentity = holdingIdentity; } + + public CpiIdentifierDTO getCpiIdentifier() { return cpiIdentifier; } + + public void setCpiIdentifier(CpiIdentifierDTO cpiIdentifier) { this.cpiIdentifier = cpiIdentifier; } +} diff --git a/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java new file mode 100644 index 0000000..52c00fe --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/buildSrc/src/main/java/com/r3/csde/dtos/VirtualNodesDTO.java @@ -0,0 +1,16 @@ +package com.r3.csde.dtos; + +import com.r3.csde.dtos.VirtualNodeInfoDTO; + +import java.util.List; + +public class VirtualNodesDTO { + + private List virtualNodes; + + public VirtualNodesDTO() {} + + public List getVirtualNodes() { return virtualNodes; } + + public void setVirtualNodes(List virtualNodes) { this.virtualNodes = virtualNodes; } +} diff --git a/kotlin-samples/mgm-dynamic-network/clean.sh b/kotlin-samples/mgm-dynamic-network/clean.sh new file mode 100644 index 0000000..f6f6851 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/clean.sh @@ -0,0 +1,15 @@ +#!/bin/sh +rm -rf ./workspace +rm -rf ./logs + +echo "---Clean Up Env---" +WORK_DIR=./register-mgm +cd $WORK_DIR + +rm signingkey1.pem +rm signingkeys.pfx +rm mgm.cpi +rm request1.csr +rm gradle-plugin-default-key.pem + +rm -rf register-member \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/config/gradle-plugin-default-key.pem b/kotlin-samples/mgm-dynamic-network/config/gradle-plugin-default-key.pem new file mode 100644 index 0000000..5294bbd --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/config/gradle-plugin-default-key.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7zCCAZOgAwIBAgIEFyV7dzAMBggqhkjOPQQDAgUAMFsxCzAJBgNVBAYTAkdC +MQ8wDQYDVQQHDAZMb25kb24xDjAMBgNVBAoMBUNvcmRhMQswCQYDVQQLDAJSMzEe +MBwGA1UEAwwVQ29yZGEgRGV2IENvZGUgU2lnbmVyMB4XDTIwMDYyNTE4NTI1NFoX +DTMwMDYyMzE4NTI1NFowWzELMAkGA1UEBhMCR0IxDzANBgNVBAcTBkxvbmRvbjEO +MAwGA1UEChMFQ29yZGExCzAJBgNVBAsTAlIzMR4wHAYDVQQDExVDb3JkYSBEZXYg +Q29kZSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQDjSJtzQ+ldDFt +pHiqdSJebOGPZcvZbmC/PIJRsZZUF1bl3PfMqyG3EmAe0CeFAfLzPQtf2qTAnmJj +lGTkkQhxo0MwQTATBgNVHSUEDDAKBggrBgEFBQcDAzALBgNVHQ8EBAMCB4AwHQYD +VR0OBBYEFLMkL2nlYRLvgZZq7GIIqbe4df4pMAwGCCqGSM49BAMCBQADSAAwRQIh +ALB0ipx6EplT1fbUKqgc7rjH+pV1RQ4oKF+TkfjPdxnAAiArBdAI15uI70wf+xlL +zU+Rc5yMtcOY4/moZUq36r0Ilg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/config/log4j2.xml b/kotlin-samples/mgm-dynamic-network/config/log4j2.xml new file mode 100644 index 0000000..909222c --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/config/log4j2.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/config/static-network-config.json b/kotlin-samples/mgm-dynamic-network/config/static-network-config.json new file mode 100644 index 0000000..9adde9b --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/config/static-network-config.json @@ -0,0 +1,23 @@ +[ + { + "x500Name" : "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=Dave, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "cpi name" + }, + { + "x500Name" : "CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB", + "cpi" : "CSDE Notary Server CPI", + "serviceX500Name": "CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB" + } +] diff --git a/kotlin-samples/mgm-dynamic-network/contracts/build.gradle b/kotlin-samples/mgm-dynamic-network/contracts/build.gradle new file mode 100644 index 0000000..52ca6f0 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/contracts/build.gradle @@ -0,0 +1,88 @@ + +plugins { + // Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well. + // These extend existing build environment so that CPB and CPK files can be built. + // This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp + // required by Corda. + id 'net.corda.plugins.cordapp-cpb2' + id 'org.jetbrains.kotlin.jvm' + id 'maven-publish' +} + +// Declare dependencies for the modules we will use. +// A cordaProvided declaration is required for anything that we use that the Corda API provides. +// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on. +dependencies { + + cordaProvided 'org.jetbrains.kotlin:kotlin-osgi-bundle' + + // Declare a "platform" so that we use the correct set of dependency versions for the version of the + // Corda API specified. + cordaProvided platform("net.corda:corda-api:$cordaApiVersion") + + // If using transistive dependencies this will provide most of Corda-API: + // cordaProvided 'net.corda:corda-application' + + // Alternatively we can explicitly specify all our Corda-API dependencies: + cordaProvided 'net.corda:corda-base' + cordaProvided 'net.corda:corda-application' + cordaProvided 'net.corda:corda-crypto' + cordaProvided 'net.corda:corda-membership' + // cordaProvided 'net.corda:corda-persistence' + cordaProvided 'net.corda:corda-serialization' + cordaProvided 'net.corda:corda-ledger-utxo' + cordaProvided 'net.corda:corda-ledger-consensual' + + // CorDapps that use the UTXO ledger must include at least one notary client plugin + cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion" + + // The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration. + cordaProvided 'org.slf4j:slf4j-api' + + // This are shared so should be here. + // Dependencies Required By Test Tooling + // Todo: these are commented out as the simulator UTXO work has not been merged into Gecko yet. +// testImplementation "net.corda:corda-simulator-api:$simulatorVersion" +// testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion" + + // 3rd party libraries + // Required + testImplementation "org.slf4j:slf4j-simple:2.0.0" + testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Optional but used by exmaple tests. + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" +} + +// The CordApp section. +// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp. +// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s +// subproject. +// This is required by the corda plugins to build the CorDapp. +cordapp { + // "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred + // and earliest versions of the Corda platform that the CorDapp will run on respectively. + // Enforced versioning has not implemented yet so we need to pass in a dummy value for now. + // The platform version will correspond to and be roughly equivalent to the Corda API version. + targetPlatformVersion = platformVersion.toInteger() + minimumPlatformVersion = platformVersion.toInteger() + + // The cordapp section contains either a workflow or contract subsection depending on the type of component. + // Declares the type and metadata of the CPK (this CPB has one CPK). + contract { + name "ContractsModuleNameHere" + versionId 1 + vendor "VendorNameHere" + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} diff --git a/kotlin-samples/mgm-dynamic-network/contracts/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.kt b/kotlin-samples/mgm-dynamic-network/contracts/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.kt new file mode 100644 index 0000000..285a43b --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/contracts/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.kt @@ -0,0 +1,62 @@ +package com.r3.developers.csdetemplate.utxoexample.contracts + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState +import net.corda.v5.base.exceptions.CordaRuntimeException +import net.corda.v5.ledger.utxo.Command +import net.corda.v5.ledger.utxo.Contract +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction + +class ChatContract: Contract { + + // Command Class used to indicate that the transaction should start a new chat. + class Create: Command + // Command Class used to indicate that the transaction should append a new ChatState to an existing chat. + class Update: Command + + // verify() function is used to apply contract rules to the transaction. + override fun verify(transaction: UtxoLedgerTransaction) { + + // Ensures that there is only one command in the transaction + val command = transaction.commands.singleOrNull() ?: throw CordaRuntimeException("Requires a single command.") + + // Applies a universal constraint (applies to all transactions irrespective of command) + "The output state should have two and only two participants." using { + val output = transaction.outputContractStates.first() as ChatState + output.participants.size== 2 + } + // Switches case based on the command + when(command) { + // Rules applied only to transactions with the Create Command. + is Create -> { + "When command is Create there should be no input states." using (transaction.inputContractStates.isEmpty()) + "When command is Create there should be one and only one output state." using (transaction.outputContractStates.size == 1) + } + // Rules applied only to transactions with the Update Command. + is Update -> { + "When command is Update there should be one and only one input state." using (transaction.inputContractStates.size == 1) + "When command is Update there should be one and only one output state." using (transaction.outputContractStates.size == 1) + + val input = transaction.inputContractStates.single() as ChatState + val output = transaction.outputContractStates.single() as ChatState + "When command is Update id must not change." using (input.id == output.id) + "When command is Update chatName must not change." using (input.chatName == output.chatName) + "When command is Update participants must not change." using ( + input.participants.toSet().intersect(output.participants.toSet()).size == 2) + } + else -> { + throw CordaRuntimeException("Command not allowed.") + } + } + } + + // Helper function to allow writing constraints in the Corda 4 '"text" using (boolean)' style + private infix fun String.using(expr: Boolean) { + if (!expr) throw CordaRuntimeException("Failed requirement: $this") + } + + // Helper function to allow writing constraints in '"text" using {lambda}' style where the last expression + // in the lambda is a boolean. + private infix fun String.using(expr: () -> Boolean) { + if (!expr.invoke()) throw CordaRuntimeException("Failed requirement: $this") + } +} \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/contracts/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/states/ChatState.kt b/kotlin-samples/mgm-dynamic-network/contracts/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/states/ChatState.kt new file mode 100644 index 0000000..2fc17c9 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/contracts/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/states/ChatState.kt @@ -0,0 +1,37 @@ +package com.r3.developers.csdetemplate.utxoexample.states + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract +import net.corda.v5.base.types.MemberX500Name +import net.corda.v5.ledger.utxo.BelongsToContract +import net.corda.v5.ledger.utxo.ContractState +import java.security.PublicKey +import java.util.* + + +// The ChatState represents data stored on ledger. A chat consists of a linear series of messages between two +// participants and is represented by a UUID. Any given pair of participants can have multiple chats +// Each ChatState stores one message between the two participants in the chat. The backchain of ChatStates +// represents the history of the chat. + +@BelongsToContract(ChatContract::class) +data class ChatState( + // Unique identifier for the chat. + val id : UUID = UUID.randomUUID(), + // Non-unique name for the chat. + val chatName: String, + // The MemberX500Name of the participant who sent the message. + val messageFrom: MemberX500Name, + // The message + val message: String, + // The participants to the chat, represented by their public key. + private val participants: List) : ContractState { + + override fun getParticipants(): List { + return participants + } + + // Helper function to create a new ChatState from the previous (input) ChatState. + fun updateMessage(messageFrom: MemberX500Name, message: String) = + copy(messageFrom = messageFrom, message = message) +} + diff --git a/kotlin-samples/mgm-dynamic-network/gradle.properties b/kotlin-samples/mgm-dynamic-network/gradle.properties new file mode 100644 index 0000000..4bd8d38 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/gradle.properties @@ -0,0 +1,43 @@ +kotlin.code.style=official + +# Specify the version of the Corda-API to use. +# This needs to match the version supported by the Corda Cluster the CorDapp will run on. + +cordaApiVersion=5.0.0.665-Gecko1.0 + +# Settings For Development Utilities +combinedWorkerVersion=5.0.0.0-Gecko1.0 + +# Specify the version of the notary plugins to use. +# Currently packaged as part of corda-runtime-os, so should be set to a corda-runtime-os version. +cordaNotaryPluginsVersion=5.0.0.0-Gecko1.0 + + + +# Specify the version of the cordapp-cpb and cordapp-cpk plugins +cordaPluginsVersion=7.0.1 + +# For the time being this just needs to be set to a dummy value. +platformVersion = 999 + +# Version of Kotlin to use. +# We recommend using a version close to that used by Corda-API. +kotlinVersion = 1.7.21 + +# Do not use default dependencies. +kotlin.stdlib.default.dependency=false + +# Test Tooling Dependency Versions +junitVersion = 5.8.2 +mockitoKotlinVersion=4.0.0 +mockitoVersion=4.6.1 +hamcrestVersion=2.2 + +postgresqlVersion=42.4.3 + +cordaClusterURL=https://localhost:8888 +cordaRpcUser=admin +cordaRpcPasswd=admin +devEnvWorkspace=workspace +dbContainerName=CSDEpostgresql + diff --git a/kotlin-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.jar b/kotlin-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/kotlin-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kotlin-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.properties b/kotlin-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5ec4b8e --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin-samples/mgm-dynamic-network/gradlew b/kotlin-samples/mgm-dynamic-network/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/kotlin-samples/mgm-dynamic-network/gradlew.bat b/kotlin-samples/mgm-dynamic-network/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-samples/mgm-dynamic-network/settings.gradle b/kotlin-samples/mgm-dynamic-network/settings.gradle new file mode 100644 index 0000000..57ff1e7 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + // Declare the repositories where plugins are stored. + repositories { + gradlePluginPortal() + mavenCentral() + mavenLocal() + } + + // The plugin dependencies with versions of the plugins congruent with the specified CorDapp plugin version, + // Corda API version, and Kotlin version. + plugins { + id 'net.corda.plugins.cordapp-cpk2' version cordaPluginsVersion + id 'net.corda.plugins.cordapp-cpb2' version cordaPluginsVersion + id 'net.corda.cordapp.cordapp-configuration' version cordaApiVersion + id 'org.jetbrains.kotlin.jvm' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.jpa' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.allopen' version kotlinVersion + } +} + +// Root project name, used in naming the project as a whole and used in naming objects built by the project. +rootProject.name = 'mgm-dynamic-network-kotlin' +include ':workflows' +include ':contracts' + diff --git a/kotlin-samples/mgm-dynamic-network/workflows/build.gradle b/kotlin-samples/mgm-dynamic-network/workflows/build.gradle new file mode 100644 index 0000000..4196b47 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/build.gradle @@ -0,0 +1,89 @@ +plugins { + // Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well. + // These extend existing build environment so that CPB and CPK files can be built. + // This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp + // required by Corda. + id 'net.corda.plugins.cordapp-cpb2' + id 'org.jetbrains.kotlin.jvm' + id 'maven-publish' +} + +// Declare dependencies for the modules we will use. +// A cordaProvided declaration is required for anything that we use that the Corda API provides. +// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on. +dependencies { + // From other subprojects: + cordapp project(':contracts') + + cordaProvided 'org.jetbrains.kotlin:kotlin-osgi-bundle' + + // Declare a "platform" so that we use the correct set of dependency versions for the version of the + // Corda API specified. + cordaProvided platform("net.corda:corda-api:$cordaApiVersion") + + // If using transistive dependencies this will provide most of Corda-API: + // cordaProvided 'net.corda:corda-application' + + // Alternatively we can explicitly specify all our Corda-API dependencies: + cordaProvided 'net.corda:corda-base' + cordaProvided 'net.corda:corda-application' + cordaProvided 'net.corda:corda-crypto' + cordaProvided 'net.corda:corda-membership' + // cordaProvided 'net.corda:corda-persistence' + cordaProvided 'net.corda:corda-serialization' + cordaProvided 'net.corda:corda-ledger-utxo' + cordaProvided 'net.corda:corda-ledger-consensual' + + // CorDapps that use the UTXO ledger must include at least one notary client plugin + cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion" + + // The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration. + cordaProvided 'org.slf4j:slf4j-api' + + // Dependencies Required By Simulator Tests + // Todo: these are commented out as the simulator UTXO work has not been merged into Gecko yet. +// testImplementation "net.corda:corda-simulator-api:$simulatorVersion" +// testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion" + + // 3rd party libraries + // Required + testImplementation "org.slf4j:slf4j-simple:2.0.0" + testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Optional but used by exmaple tests. + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" + +} + +// The CordApp section. +// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp. +// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s +// subproject. +// This is required by the corda plugins to build the CorDapp. +cordapp { + // "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred + // and earliest versions of the Corda platform that the CorDapp will run on respectively. + // Enforced versioning has not implemented yet so we need to pass in a dummy value for now. + // The platform version will correspond to and be roughly equivalent to the Corda API version. + targetPlatformVersion = platformVersion.toInteger() + minimumPlatformVersion = platformVersion.toInteger() + + // The cordapp section contains either a workflow or contract subsection depending on the type of component. + // Declares the type and metadata of the CPK (this CPB has one CPK). + workflow { + name "WorkflowsModuleNameHere" + versionId 1 + vendor "VendorNameHere" + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} diff --git a/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.kt b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.kt new file mode 100644 index 0000000..6819342 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.kt @@ -0,0 +1,113 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract +import com.r3.developers.csdetemplate.utxoexample.states.ChatState +import net.corda.v5.application.flows.* +import net.corda.v5.application.marshalling.JsonMarshallingService +import net.corda.v5.application.membership.MemberLookup +import net.corda.v5.base.annotations.Suspendable +import net.corda.v5.base.exceptions.CordaRuntimeException +import net.corda.v5.base.types.MemberX500Name +import net.corda.v5.ledger.common.NotaryLookup +import net.corda.v5.ledger.common.Party +import net.corda.v5.ledger.utxo.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.time.Duration +import java.time.Instant + +// A class to hold the deserialized arguments required to start the flow. +data class CreateNewChatFlowArgs(val chatName: String, val message: String, val otherMember: String) + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. +class CreateNewChatFlow: ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + @CordaInject + lateinit var memberLookup: MemberLookup + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @CordaInject + lateinit var notaryLookup: NotaryLookup + + // FlowEngine service is required to run SubFlows. + @CordaInject + lateinit var flowEngine: FlowEngine + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + + log.info("CreateNewChatFlow.call() called") + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + val flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, CreateNewChatFlowArgs::class.java) + + // Get MemberInfos for the Vnode running the flow and the otherMember. + // Good practice in Kotlin CorDapps is to only throw RuntimeException. + // Note, in Java CorDapps only unchecked RuntimeExceptions can be thrown not + // declared checked exceptions as this changes the method signature and breaks override. + val myInfo = memberLookup.myInfo() + val otherMember = memberLookup.lookup(MemberX500Name.parse(flowArgs.otherMember)) ?: + throw CordaRuntimeException("MemberLookup can't find otherMember specified in flow arguments.") + + // Create the ChatState from the input arguments and member information. + val chatState = ChatState( + chatName = flowArgs.chatName, + messageFrom = myInfo.name, + message = flowArgs.message, + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + // Obtain the notary. + val notary = notaryLookup.notaryServices.single() + + // Use UTXOTransactionBuilder to build up the draft transaction. + val txBuilder= ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notary.publicKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addCommand(ChatContract.Create()) + .addSignatories(chatState.participants) + + // Convert the transaction builder to a UTXOSignedTransaction. Verifies the content of the + // UtxoTransactionBuilder and signs the transaction with any required signatories that belong to + // the current node. + val signedTransaction = txBuilder.toSignedTransaction() + + // Call FinalizeChatSubFlow which will finalise the transaction. + // If successful the flow will return a String of the created transaction id, + // if not successful it will return an error message. + return flowEngine.subFlow(FinalizeChatSubFlow(signedTransaction, otherMember.name)) + + + } + // Catch any exceptions, log them and rethrow the exception. + catch (e: Exception) { + log.warn("Failed to process utxo flow for request body '$requestBody' because:'${e.message}'") + throw e + } + } +} + + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "create-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.CreateNewChatFlow", + "requestBody": { + "chatName":"Chat with Bob", + "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "message": "Hello Bob" + } +} + */ \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.kt b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.kt new file mode 100644 index 0000000..d5d91f2 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.kt @@ -0,0 +1,103 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState +import net.corda.v5.application.flows.* +import net.corda.v5.application.messaging.FlowMessaging +import net.corda.v5.application.messaging.FlowSession +import net.corda.v5.base.annotations.Suspendable +import net.corda.v5.base.exceptions.CordaRuntimeException +import net.corda.v5.base.types.MemberX500Name +import net.corda.v5.ledger.utxo.UtxoLedgerService +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction +import org.slf4j.LoggerFactory + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. + +// @InitiatingFlow declares the protocol which will be used to link the initiator to the responder. +@InitiatingFlow(protocol = "finalize-chat-protocol") +class FinalizeChatSubFlow(private val signedTransaction: UtxoSignedTransaction, private val otherMember: MemberX500Name): SubFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @CordaInject + lateinit var flowMessaging: FlowMessaging + + @Suspendable + override fun call(): String { + + log.info("FinalizeChatFlow.call() called") + + // Initiates a session with the other Member. + val session = flowMessaging.initiateFlow(otherMember) + + return try { + // Calls the Corda provided finalise() function which gather signatures from the counterparty, + // notarises the transaction and persists the transaction to each party's vault. + // On success returns the id of the transaction created. (This is different to the ChatState id) + val finalizedSignedTransaction = ledgerService.finalize( + signedTransaction, + listOf(session) + ) + // Returns the transaction id converted to a string. + finalizedSignedTransaction.id.toString().also { + log.info("Success! Response: $it") + } + } + // Soft fails the flow and returns the error message without throwing a flow exception. + catch (e: Exception) { + log.warn("Finality failed", e) + "Finality failed, ${e.message}" + } + } +} + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. + +//@InitiatingBy declares the protocol which will be used to link the initiator to the responder. +@InitiatedBy(protocol = "finalize-chat-protocol") +class FinalizeChatResponderFlow: ResponderFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @Suspendable + override fun call(session: FlowSession) { + + log.info("FinalizeChatResponderFlow.call() called") + + try { + // Calls receiveFinality() function which provides the responder to the finalise() function + // in the Initiating Flow. Accepts a lambda validator containing the business logic to decide whether + // responder should sign the Transaction. + val finalizedSignedTransaction = ledgerService.receiveFinality(session) { ledgerTransaction -> + + // Note, this exception will only be shown in the logs if Corda Logging is set to debug. + val state = ledgerTransaction.getOutputStates(ChatState::class.java).singleOrNull() ?: + throw CordaRuntimeException("Failed verification - transaction did not have exactly one output ChatState.") + + // Uses checkForBannedWords() and checkMessageFromMatchesCounterparty() functions + // to check whether to sign the transaction. + checkForBannedWords(state.message) + checkMessageFromMatchesCounterparty(state, session.counterparty) + + log.info("Verified the transaction- ${ledgerTransaction.id}") + } + log.info("Finished responder flow - ${finalizedSignedTransaction.id}") + } + // Soft fails the flow and log the exception. + catch (e: Exception) { + log.warn("Exceptionally finished responder flow", e) + } + } +} \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.kt b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.kt new file mode 100644 index 0000000..d79e351 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.kt @@ -0,0 +1,116 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState +import net.corda.v5.application.flows.ClientRequestBody +import net.corda.v5.application.flows.ClientStartableFlow +import net.corda.v5.application.flows.CordaInject +import net.corda.v5.application.marshalling.JsonMarshallingService +import net.corda.v5.base.annotations.Suspendable +import net.corda.v5.base.exceptions.CordaRuntimeException +import net.corda.v5.ledger.utxo.StateAndRef +import net.corda.v5.ledger.utxo.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.util.* + +// A class to hold the deserialized arguments required to start the flow. +data class GetChatFlowArgs(val id: UUID, val numberOfRecords: Int) + +// A class to pair the messageFrom and message together. +data class MessageAndSender(val messageFrom: String, val message: String) + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. +class GetChatFlow: ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + + log.info("GetChatFlow.call() called") + + // Obtain the deserialized input arguments to the flow from the requestBody. + val flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, GetChatFlowArgs::class.java) + + // Look up the latest unconsumed ChatState with the given id. + // Note, this code brings all unconsumed states back, then filters them. + // This is an inefficient way to perform this operation when there are a large number of chats. + // Note, you will get this error if you input an id which has no corresponding ChatState (common error). + val states = ledgerService.findUnconsumedStatesByType(ChatState::class.java) + val state = states.singleOrNull {it.state.contractState.id == flowArgs.id} + ?: throw CordaRuntimeException("Did not find an unique unconsumed ChatState with id ${flowArgs.id}") + + // Calls resolveMessagesFromBackchain() which retrieves the chat history from the backchain. + return jsonMarshallingService.format(resolveMessagesFromBackchain(state, flowArgs.numberOfRecords )) + } + + // resoveMessageFromBackchain() starts at the stateAndRef provided, which represents the unconsumed head of the + // backchain for this particular chat, then walks the chain backwards for the number of transaction specified in + // the numberOfRecords argument. For each transaction it adds the MessageAndSender representing the + // message and who sent it to a list which is then returned. + @Suspendable + private fun resolveMessagesFromBackchain(stateAndRef: StateAndRef, numberOfRecords: Int): List{ + + // Set up a MutableList to collect the MessageAndSender(s) + val messages = mutableListOf() + + // Set up initial conditions for walking the backchain. + var currentStateAndRef = stateAndRef + var recordsToFetch = numberOfRecords + var moreBackchain = true + + // Continue to loop until the start of the backchain or enough records have been retrieved. + while (moreBackchain) { + + // Obtain the transaction id from the current StateAndRef and fetch the transaction from the vault. + val transactionId = currentStateAndRef.ref.transactionId + val transaction = ledgerService.findLedgerTransaction(transactionId) + ?: throw CordaRuntimeException("Transaction $transactionId not found.") + + // Get the output state from the transaction and use it to create a MessageAndSender Object which + // is appended to the mutable list. + val output = transaction.getOutputStates(ChatState::class.java).singleOrNull() + ?: throw CordaRuntimeException("Expecting one and only one ChatState output for transaction $transactionId.") + messages.add(MessageAndSender(output.messageFrom.toString(), output.message)) + // Decrement the number of records to fetch. + recordsToFetch-- + + // Get the reference to the input states. + val inputStateAndRefs = transaction.inputStateAndRefs + + // Check if there are no more input states (start of chain) or we have retrieved enough records. + // Check the transaction is not malformed by having too many input states. + // Set the currentStateAndRef to the input StateAndRef, then repeat the loop. + if (inputStateAndRefs.isEmpty() || recordsToFetch == 0) { + moreBackchain = false + } else if (inputStateAndRefs.size > 1) { + throw CordaRuntimeException("More than one input state found for transaction $transactionId.") + } else { + @Suppress("UNCHECKED_CAST") + currentStateAndRef = inputStateAndRefs.single() as StateAndRef + } + } + // Convert to an immutable List. + return messages.toList() + } +} + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "get-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.GetChatFlow", + "requestBody": { + "id":"** fill in id **", + "numberOfRecords":"4" + } +} + */ \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.kt b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.kt new file mode 100644 index 0000000..6a211b4 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.kt @@ -0,0 +1,61 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState +import net.corda.v5.application.flows.ClientRequestBody +import net.corda.v5.application.flows.ClientStartableFlow +import net.corda.v5.application.flows.CordaInject +import net.corda.v5.application.marshalling.JsonMarshallingService +import net.corda.v5.base.annotations.Suspendable +import net.corda.v5.ledger.utxo.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.util.* + + +// Data class to hold the Flow results. +// The ChatState(s) cannot be returned directly as the JsonMarshallingService can only serialize simple classes +// that the underlying Jackson serializer recognises, hence creating a DTO style object which consists only of Strings +// and a UUID. It is possible to create custom serializers for the JsonMarshallingService, but this beyond the scope +// of this simple example. +data class ChatStateResults(val id: UUID, val chatName: String,val messageFromName: String, val message: String) + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. +class ListChatsFlow : ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + + log.info("ListChatsFlow.call() called") + + // Queries the VNode's vault for unconsumed states and converts the result to a serializable DTO. + val states = ledgerService.findUnconsumedStatesByType(ChatState::class.java) + val results = states.map { + ChatStateResults( + it.state.contractState.id, + it.state.contractState.chatName, + it.state.contractState.messageFrom.toString(), + it.state.contractState.message) } + + // Uses the JsonMarshallingService's format() function to serialize the DTO to Json. + return jsonMarshallingService.format(results) + } +} + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "list-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.ListChatsFlow", + "requestBody": {} +} +*/ diff --git a/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/ResponderValidations.kt b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/ResponderValidations.kt new file mode 100644 index 0000000..b784788 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/ResponderValidations.kt @@ -0,0 +1,24 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState +import net.corda.v5.base.annotations.Suspendable +import net.corda.v5.base.exceptions.CordaRuntimeException +import net.corda.v5.base.types.MemberX500Name + +// Note, these exceptions will only be visible in the logs if Corda logging is set to debug. + +// Checks that the message does not contain banned words and throws and exception if it does. +@Suspendable +fun checkForBannedWords(str: String) { + val bannedWords = listOf("banana", "apple", "pear") + if (bannedWords.any { str.contains(it) }) + throw CordaRuntimeException("Failed verification - message contains banned words") +} + +// Checks that the messageFrom field in the ChatState matches the initiators (otherMember) +// memberX500Name, if not it throws an exception. +@Suspendable +fun checkMessageFromMatchesCounterparty(state: ChatState, otherMember: MemberX500Name) { + if( state.messageFrom != otherMember) + throw CordaRuntimeException("Failed verification - messageFrom does not equal flow initiator memberX500Name") +} \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/TestContractFlow.kt b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/TestContractFlow.kt new file mode 100644 index 0000000..dad0fe0 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/TestContractFlow.kt @@ -0,0 +1,497 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract +import com.r3.developers.csdetemplate.utxoexample.states.ChatState +import net.corda.v5.application.flows.* +import net.corda.v5.application.marshalling.JsonMarshallingService +import net.corda.v5.application.membership.MemberLookup +import net.corda.v5.base.annotations.Suspendable +import net.corda.v5.base.exceptions.CordaRuntimeException +import net.corda.v5.base.types.MemberX500Name +import net.corda.v5.ledger.common.NotaryLookup +import net.corda.v5.ledger.common.Party +import net.corda.v5.ledger.utxo.Command +import net.corda.v5.ledger.utxo.StateRef +import net.corda.v5.ledger.utxo.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.time.Duration +import java.time.Instant +import java.util.* + + +data class TestContractFlowArgs(val otherMember: String) + +class TestContractFlow: ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + @CordaInject + lateinit var memberLookup: MemberLookup + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @CordaInject + lateinit var notaryLookup: NotaryLookup + + @CordaInject + lateinit var flowEngine: FlowEngine + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + + + val results = mutableMapOf() + + log.info("TestContractFlow.call() called") + + class FakeCommand : Command + + try { + val flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, TestContractFlowArgs::class.java) + + val myInfo = memberLookup.myInfo() + + val otherMember = memberLookup.lookup(MemberX500Name.parse(flowArgs.otherMember)) ?: + throw CordaRuntimeException("MemberLookup can't find otherMember specified in flow arguments.") + + // Obtain the Notary name and public key. + val notary = notaryLookup.notaryServices.first() + val notaryKey = memberLookup.lookup().first { + it.memberProvidedContext["corda.notary.service.name"] == notary.name.toString() + }.ledgerKeys.first() + + + // Create a well formed transaction with an output State which can be referenced + // as an input StateRef in the tests + lateinit var inputStateRef: StateRef + lateinit var chatId: UUID + + try { + val chatState = ChatState( + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + chatId = chatState.id + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addCommand(ChatContract.Create()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + inputStateRef = StateRef(signedTransaction.id, 0) + flowEngine.subFlow(FinalizeChatSubFlow(signedTransaction, otherMember.name)) + + } catch (e:Exception) { + throw CordaRuntimeException("Set up transaction could not be created because of exception: ${e.message}") + } + + + + + // ************* START TESTS **************** + + // Multiple Commands not permitted + results["Multiple Commands not permitted"] = try { + val chatState = ChatState( + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addCommand(ChatContract.Create()) + .addCommand(FakeCommand()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("Requires a single command.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // ChatState with 3 Participants not permitted + results["ChatState with 3 Participants not permitted"] = try { + + val chatState = ChatState( + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addCommand(ChatContract.Create()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("The output state should have two and only two participants.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // Input State on Create not permitted + results["Input State on Create not permitted"] = try { + val chatState = ChatState( + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(inputStateRef) + .addOutputState(chatState) + .addCommand(ChatContract.Create()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("When command is Create there should be no input states.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + // Zero output States on Create not permitted + + // Test omitted as it would fail on + // "The output state should have two and only two participants." first + + + // Two output States on Create not permitted + results["Two output States on Create not permitted"] = try { + val chatState = ChatState( + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addOutputState(chatState) + .addCommand(ChatContract.Create()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("When command is Create there should be one and only one output state.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // Zero input State on Update not permitted + results["Zero input State on Update not permitted"] = try { + + val chatState = ChatState( + id = chatId, + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addCommand(ChatContract.Update()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("When command is Update there should be one and only one input state.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // Two Input State on Update not permitted + results["Two Input State on Update not permitted"] = try { + + log.info("MB: test change") + val chatState = ChatState( + id = chatId, + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(inputStateRef) + .addInputState(inputStateRef) + .addOutputState(chatState) + .addCommand(ChatContract.Update()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("When command is Update there should be one and only one input state.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // Zero output States on Update not permitted + + // Test omitted as it would fail on + // "The output state should have two and only two participants." first + + + // Two output States on Update not permitted + results["Two output States on Update not permitted"] = try { + val chatState = ChatState( + id = chatId, + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(inputStateRef) + .addOutputState(chatState) + .addOutputState(chatState) + .addCommand(ChatContract.Update()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("When command is Update there should be one and only one output state.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // On Update id must not change + results["On Update id must not change"] = try { + val chatState = ChatState( + id = UUID.randomUUID(), + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(inputStateRef) + .addOutputState(chatState) + .addCommand(ChatContract.Update()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("When command is Update id must not change")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // On Update chatName must not change + results["On Update chatName must not change"] = try { + val chatState = ChatState( + id = chatId, + chatName = "DummyChat Name has changed", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(inputStateRef) + .addOutputState(chatState) + .addCommand(ChatContract.Update()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("When command is Update chatName must not change.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // On Update participants must not change + results["On Update participants must not change"] = try { + val chatState = ChatState( + id = chatId, + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), myInfo.ledgerKeys.first()) + ) + + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(inputStateRef) + .addOutputState(chatState) + .addCommand(ChatContract.Update()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("When command is Update participants must not change.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + // FakeCommand not permitted + results["FakeCommand not permitted"] = try { + val chatState = ChatState( + chatName = "DummyChat", + messageFrom = myInfo.name, + message = "Dummy Message", + participants = listOf(myInfo.ledgerKeys.first(), otherMember.ledgerKeys.first()) + ) + + + // Use UTXOTransactionBuilder to build up the draft transaction. + val txBuilder = ledgerService.getTransactionBuilder() + .setNotary(Party(notary.name, notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addCommand(FakeCommand()) + .addSignatories(chatState.participants) + + @Suppress("DEPRECATION", "UNUSED_VARIABLE") + val signedTransaction = txBuilder.toSignedTransaction() + + "Fail" + + } catch (e:Exception) { + val exceptionMessage = e.message ?: "No exception message" + if (exceptionMessage.contains("Command not allowed.")) { + "Pass" } + else { + "Contract failed but with a different Exception: ${e.message}" + } + } + + + + return results.toString() + + // Catch any exceptions, log them and rethrow the exception. + } catch (e: Exception) { + log.warn("Failed to process utxo flow for request body '$requestBody' because:'${e.message}'") + throw e + } + } + +} +/* +{ + "clientRequestId": "dummy-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.TestContractFlow", + "requestBody": { + "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" + } +} + + */ \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.kt b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.kt new file mode 100644 index 0000000..ab67032 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/src/main/kotlin/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.kt @@ -0,0 +1,109 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract +import com.r3.developers.csdetemplate.utxoexample.states.ChatState +import net.corda.v5.application.flows.* +import net.corda.v5.application.marshalling.JsonMarshallingService +import net.corda.v5.application.membership.MemberLookup +import net.corda.v5.base.annotations.Suspendable +import net.corda.v5.base.exceptions.CordaRuntimeException +import net.corda.v5.ledger.utxo.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.time.Duration +import java.time.Instant +import java.util.* + +// A class to hold the deserialized arguments required to start the flow. +data class UpdateChatFlowArgs(val id: UUID, val message: String) + + +// See Chat CorDapp Design section of the getting started docs for a description of this flow. +class UpdateChatFlow: ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + @CordaInject + lateinit var memberLookup: MemberLookup + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + // FlowEngine service is required to run SubFlows. + @CordaInject + lateinit var flowEngine: FlowEngine + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + + log.info("UpdateNewChatFlow.call() called") + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + val flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, UpdateChatFlowArgs::class.java) + + // Look up the latest unconsumed ChatState with the given id. + // Note, this code brings all unconsumed states back, then filters them. + // This is an inefficient way to perform this operation when there are a large number of chats. + // Note, you will get this error if you input an id which has no corresponding ChatState (common error). + val stateAndRef = ledgerService.findUnconsumedStatesByType(ChatState::class.java).singleOrNull { + it.state.contractState.id == flowArgs.id + } ?: throw CordaRuntimeException("Multiple or zero Chat states with id ${flowArgs.id} found.") + + // Get MemberInfos for the Vnode running the flow and the otherMember. + val myInfo = memberLookup.myInfo() + val state = stateAndRef.state.contractState + + val members = state.participants.map { + memberLookup.lookup(it) ?: throw CordaRuntimeException("Member not found from public key $it.")} + val otherMember = (members - myInfo).singleOrNull() + ?: throw CordaRuntimeException("Should be only one participant other than the initiator.") + + // Create a new ChatState using the updateMessage helper function. + val newChatState = state.updateMessage(myInfo.name, flowArgs.message) + + // Use UTXOTransactionBuilder to build up the draft transaction. + val txBuilder= ledgerService.getTransactionBuilder() + .setNotary(stateAndRef.state.notary) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(newChatState) + .addInputState(stateAndRef.ref) + .addCommand(ChatContract.Update()) + .addSignatories(newChatState.participants) + + // Convert the transaction builder to a UTXOSignedTransaction. Verifies the content of the + // UtxoTransactionBuilder and signs the transaction with any required signatories that belong to + // the current node. + val signedTransaction = txBuilder.toSignedTransaction() + + // Call FinalizeChatSubFlow which will finalise the transaction. + // If successful the flow will return a String of the created transaction id, + // if not successful it will return an error message. + return flowEngine.subFlow(FinalizeChatSubFlow(signedTransaction, otherMember.name)) + + + } + // Catch any exceptions, log them and rethrow the exception. + catch (e: Exception) { + log.warn("Failed to process utxo flow for request body '$requestBody' because:'${e.message}'") + throw e + } + } +} + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "update-2", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.UpdateChatFlow", + "requestBody": { + "id":"** fill in id **", + "message": "How are you today?" + } +} + */ \ No newline at end of file diff --git a/kotlin-samples/mgm-dynamic-network/workflows/src/test/kotlin/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.kt b/kotlin-samples/mgm-dynamic-network/workflows/src/test/kotlin/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.kt new file mode 100644 index 0000000..4cbc637 --- /dev/null +++ b/kotlin-samples/mgm-dynamic-network/workflows/src/test/kotlin/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.kt @@ -0,0 +1,48 @@ +package com.r3.developers.csdetemplate.flowexample.workflows + +//import com.r3.developers.csdetemplate.flowexample.workflows.MyFirstFlow +//import com.r3.developers.csdetemplate.flowexample.workflows.MyFirstFlowResponder +//import com.r3.developers.csdetemplate.flowexample.workflows.MyFirstFlowStartArgs +//import net.corda.simulator.RequestData +//import net.corda.simulator.Simulator +//import net.corda.v5.base.types.MemberX500Name +//import org.junit.jupiter.api.Test + +// Note: this simulator test has been commented out pending the merging of the UTXO code into the Gecko Branch. + + + +//class MyFirstFlowTest { +// +// // Names picked to match the corda network in config/static-network-config.json +// private val aliceX500 = MemberX500Name.parse("CN=Alice, OU=Test Dept, O=R3, L=London, C=GB") +// private val bobX500 = MemberX500Name.parse("CN=Bob, OU=Test Dept, O=R3, L=London, C=GB") +// +// @Test +// fun `test that MyFirstFLow returns correct message`() { +// +// // Instantiate an instance of the Simulator +// val simulator = Simulator() +// +// // Create Alice and Bob's virtual nodes, including the Class's of the flows which will be registered on each node. +// // We don't assign Bob's virtual node to a val because we don't need it for this particular test. +// val aliceVN = simulator.createVirtualNode(aliceX500, MyFirstFlow::class.java) +// simulator.createVirtualNode(bobX500, MyFirstFlowResponder::class.java) +// +// // Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow +// val myFirstFlowStartArgs = MyFirstFlowStartArgs(bobX500) +// +// // Create a requestData object +// val requestData = RequestData.create( +// "request no 1", // A unique reference for the instance of the flow request +// MyFirstFlow::class.java, // The name of the flow class which is to be started +// myFirstFlowStartArgs // The object which contains the start arguments of the flow +// ) +// +// // Call the Flow on Alice's virtual node and capture the response from the flow +// val flowResponse = aliceVN.callFlow(requestData) +// +// // Check that the flow has returned the expected string +// assert(flowResponse == "Hello Alice, best wishes from Bob") +// } +//}