mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-25 19:15:10 +00:00
WIP: asset-transfer-basic app-java
Signed-off-by: r2roC <arturo@IBM.com>
This commit is contained in:
parent
6de6c77f8d
commit
1604f33ec1
6 changed files with 492 additions and 0 deletions
7
asset-transfer-basic/application-java/.gitignore
vendored
Executable file
7
asset-transfer-basic/application-java/.gitignore
vendored
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/bin/
|
||||||
|
/target/
|
||||||
|
.settings/
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
wallet
|
||||||
|
!wallet/.gitkeep
|
||||||
85
asset-transfer-basic/application-java/pom.xml
Normal file
85
asset-transfer-basic/application-java/pom.xml
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>asset-transfer-basic</groupId>
|
||||||
|
<artifactId>application-java</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
<executions>
|
||||||
|
<!-- Attach the shade goal into the package phase -->
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>oss-sonatype</id>
|
||||||
|
<name>OSS Sonatype</name>
|
||||||
|
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hyperledger.fabric</groupId>
|
||||||
|
<artifactId>fabric-gateway-java</artifactId>
|
||||||
|
<version>2.1.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.platform</groupId>
|
||||||
|
<artifactId>junit-platform-launcher</artifactId>
|
||||||
|
<version>1.4.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>5.4.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.vintage</groupId>
|
||||||
|
<artifactId>junit-vintage-engine</artifactId>
|
||||||
|
<version>5.4.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.12.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
177
asset-transfer-basic/application-java/src/main/java/org/example/ClientApp.java
Executable file
177
asset-transfer-basic/application-java/src/main/java/org/example/ClientApp.java
Executable file
|
|
@ -0,0 +1,177 @@
|
||||||
|
/*
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.example;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import org.hyperledger.fabric.gateway.Contract;
|
||||||
|
import org.hyperledger.fabric.gateway.Gateway;
|
||||||
|
import org.hyperledger.fabric.gateway.Network;
|
||||||
|
import org.hyperledger.fabric.gateway.Wallet;
|
||||||
|
import org.hyperledger.fabric.gateway.Wallets;
|
||||||
|
|
||||||
|
public class ClientApp {
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function for getting connected to the gateway
|
||||||
|
public static Gateway Connect() throws Exception{
|
||||||
|
// Load a file system based wallet for managing identities.
|
||||||
|
Path walletPath = Paths.get("wallet");
|
||||||
|
Wallet wallet = Wallets.newFileSystemWallet(walletPath);
|
||||||
|
// load a CCP
|
||||||
|
Path networkConfigPath = Paths.get("..", "..", "test-network", "organizations", "peerOrganizations", "org1.example.com", "connection-org1.yaml");
|
||||||
|
|
||||||
|
Gateway.Builder builder = Gateway.createBuilder();
|
||||||
|
builder.identity(wallet, "appUser").networkConfig(networkConfigPath).discovery(true);
|
||||||
|
return builder.connect();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to the chaincode and calls InitLedger()
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
|
||||||
|
// create a gateway connection
|
||||||
|
try (Gateway gateway = Connect()) {
|
||||||
|
|
||||||
|
// get the network and contract
|
||||||
|
Network network = gateway.getNetwork("mychannel");
|
||||||
|
Contract contract = network.getContract("basic");
|
||||||
|
|
||||||
|
byte[] result;
|
||||||
|
|
||||||
|
contract.submitTransaction("InitLedger");
|
||||||
|
System.out.println("Ledger has been initialized with 6 assets");
|
||||||
|
gateway.close();
|
||||||
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
System.out.println("Error connecting to gateway: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to the chaincode and calls CreateAsset()
|
||||||
|
public static void CreateAsset(final String assetID, final String color, final String size,
|
||||||
|
final String owner, final String appraisedValue) throws Exception {
|
||||||
|
|
||||||
|
// create a gateway connection
|
||||||
|
try (Gateway gateway = Connect()) {
|
||||||
|
|
||||||
|
// get the network and contract
|
||||||
|
Network network = gateway.getNetwork("mychannel");
|
||||||
|
Contract contract = network.getContract("basic");
|
||||||
|
|
||||||
|
contract.submitTransaction("CreateAsset", assetID, color, size, owner, appraisedValue);
|
||||||
|
System.out.println("Created asset: [assetID=" + assetID + ", color="+ color + ", size=" + size
|
||||||
|
+ ", owner=" + owner + ", appraisedValue=" + appraisedValue +"]");
|
||||||
|
gateway.close();
|
||||||
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
System.out.println("Error connecting to gateway: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ReadAsset(final String assetID) throws Exception {
|
||||||
|
// create a gateway connection
|
||||||
|
try (Gateway gateway = Connect()) {
|
||||||
|
|
||||||
|
// get the network and contract
|
||||||
|
Network network = gateway.getNetwork("mychannel");
|
||||||
|
Contract contract = network.getContract("basic");
|
||||||
|
|
||||||
|
byte[] result;
|
||||||
|
|
||||||
|
result = contract.evaluateTransaction("ReadAsset", assetID);
|
||||||
|
System.out.println(new String(result));
|
||||||
|
gateway.close();
|
||||||
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
System.out.println("Error connecting to gateway: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to the chaincode and calls CreateAsset()
|
||||||
|
public static void UpdateAsset(final String assetID, final String color, final String size,
|
||||||
|
final String owner, final String appraisedValue) throws Exception {
|
||||||
|
|
||||||
|
// create a gateway connection
|
||||||
|
try (Gateway gateway = Connect()) {
|
||||||
|
|
||||||
|
// get the network and contract
|
||||||
|
Network network = gateway.getNetwork("mychannel");
|
||||||
|
Contract contract = network.getContract("basic");
|
||||||
|
|
||||||
|
contract.submitTransaction("UpdateAsset", assetID, color, size, owner, appraisedValue);
|
||||||
|
System.out.println("Updated asset: [assetID=" + assetID + ", color="+ color + ", size=" + size
|
||||||
|
+ ", owner=" + owner + ", appraisedValue=" + appraisedValue +"]");
|
||||||
|
gateway.close();
|
||||||
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
System.out.println("Error connecting to gateway: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to the chaincode and calls CreateAsset()
|
||||||
|
public static void DeleteAsset(final String assetID) throws Exception {
|
||||||
|
|
||||||
|
// create a gateway connection
|
||||||
|
try (Gateway gateway = Connect()) {
|
||||||
|
|
||||||
|
// get the network and contract
|
||||||
|
Network network = gateway.getNetwork("mychannel");
|
||||||
|
Contract contract = network.getContract("basic");
|
||||||
|
|
||||||
|
contract.submitTransaction("DeleteAsset", assetID);
|
||||||
|
System.out.println("Deleted: " + assetID);
|
||||||
|
gateway.close();
|
||||||
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
System.out.println("Error connecting to gateway: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to the chaincode and calls CreateAsset()
|
||||||
|
public static void TransferAsset(final String assetID, final String newOwner) throws Exception {
|
||||||
|
|
||||||
|
// create a gateway connection
|
||||||
|
try (Gateway gateway = Connect()) {
|
||||||
|
|
||||||
|
// get the network and contract
|
||||||
|
Network network = gateway.getNetwork("mychannel");
|
||||||
|
Contract contract = network.getContract("basic");
|
||||||
|
|
||||||
|
byte[] result;
|
||||||
|
|
||||||
|
result = contract.submitTransaction("TransferAsset", assetID, newOwner);
|
||||||
|
System.out.println(new String(result));
|
||||||
|
System.out.println("Transferred " + assetID + " to " + newOwner);
|
||||||
|
gateway.close();
|
||||||
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
System.out.println("Error connecting to gateway: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void GetAllAssets() throws Exception {
|
||||||
|
// create a gateway connection
|
||||||
|
try (Gateway gateway = Connect()) {
|
||||||
|
|
||||||
|
// get the network and contract
|
||||||
|
Network network = gateway.getNetwork("mychannel");
|
||||||
|
Contract contract = network.getContract("basic");
|
||||||
|
|
||||||
|
byte[] result;
|
||||||
|
|
||||||
|
result = contract.evaluateTransaction("GetAllAssets");
|
||||||
|
System.out.println(new String(result));
|
||||||
|
gateway.close();
|
||||||
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
System.out.println("Error connecting to gateway: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.example;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import org.hyperledger.fabric.gateway.Wallet;
|
||||||
|
import org.hyperledger.fabric.gateway.Wallets;
|
||||||
|
import org.hyperledger.fabric.gateway.Identities;
|
||||||
|
import org.hyperledger.fabric.gateway.Identity;
|
||||||
|
import org.hyperledger.fabric.sdk.Enrollment;
|
||||||
|
import org.hyperledger.fabric.sdk.security.CryptoSuite;
|
||||||
|
import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory;
|
||||||
|
import org.hyperledger.fabric_ca.sdk.EnrollmentRequest;
|
||||||
|
import org.hyperledger.fabric_ca.sdk.HFCAClient;
|
||||||
|
|
||||||
|
public class EnrollAdmin {
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
|
||||||
|
// Create a CA client for interacting with the CA.
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.put("pemFile",
|
||||||
|
"../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem");
|
||||||
|
props.put("allowAllHostNames", "true");
|
||||||
|
HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props);
|
||||||
|
CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite();
|
||||||
|
caClient.setCryptoSuite(cryptoSuite);
|
||||||
|
|
||||||
|
// Create a wallet for managing identities
|
||||||
|
Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet"));
|
||||||
|
|
||||||
|
// Check to see if we've already enrolled the admin user.
|
||||||
|
if (wallet.get("admin") != null) {
|
||||||
|
System.out.println("An identity for the admin user \"admin\" already exists in the wallet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enroll the admin user, and import the new identity into the wallet.
|
||||||
|
final EnrollmentRequest enrollmentRequestTLS = new EnrollmentRequest();
|
||||||
|
enrollmentRequestTLS.addHost("localhost");
|
||||||
|
enrollmentRequestTLS.setProfile("tls");
|
||||||
|
Enrollment enrollment = caClient.enroll("admin", "adminpw", enrollmentRequestTLS);
|
||||||
|
Identity user = Identities.newX509Identity("Org1MSP", enrollment);
|
||||||
|
wallet.put("admin", user);
|
||||||
|
System.out.println("Successfully enrolled user \"admin\" and imported it into the wallet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.example;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.hyperledger.fabric.gateway.Wallet;
|
||||||
|
import org.hyperledger.fabric.gateway.Wallets;
|
||||||
|
import org.hyperledger.fabric.gateway.Identities;
|
||||||
|
import org.hyperledger.fabric.gateway.Identity;
|
||||||
|
import org.hyperledger.fabric.gateway.X509Identity;
|
||||||
|
import org.hyperledger.fabric.sdk.Enrollment;
|
||||||
|
import org.hyperledger.fabric.sdk.User;
|
||||||
|
import org.hyperledger.fabric.sdk.security.CryptoSuite;
|
||||||
|
import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory;
|
||||||
|
import org.hyperledger.fabric_ca.sdk.HFCAClient;
|
||||||
|
import org.hyperledger.fabric_ca.sdk.RegistrationRequest;
|
||||||
|
|
||||||
|
public class RegisterUser {
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
|
||||||
|
// Create a CA client for interacting with the CA.
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.put("pemFile",
|
||||||
|
"../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem");
|
||||||
|
props.put("allowAllHostNames", "true");
|
||||||
|
HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props);
|
||||||
|
CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite();
|
||||||
|
caClient.setCryptoSuite(cryptoSuite);
|
||||||
|
|
||||||
|
// Create a wallet for managing identities
|
||||||
|
Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet"));
|
||||||
|
|
||||||
|
// Check to see if we've already enrolled the user.
|
||||||
|
if (wallet.get("appUser") != null) {
|
||||||
|
System.out.println("An identity for the user \"appUser\" already exists in the wallet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
X509Identity adminIdentity = (X509Identity)wallet.get("admin");
|
||||||
|
if (adminIdentity == null) {
|
||||||
|
System.out.println("\"admin\" needs to be enrolled and added to the wallet first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
User admin = new User() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getRoles() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAccount() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffiliation() {
|
||||||
|
return "org1.department1";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enrollment getEnrollment() {
|
||||||
|
return new Enrollment() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrivateKey getKey() {
|
||||||
|
return adminIdentity.getPrivateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCert() {
|
||||||
|
return Identities.toPemString(adminIdentity.getCertificate());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMspId() {
|
||||||
|
return "Org1MSP";
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the user, enroll the user, and import the new identity into the wallet.
|
||||||
|
RegistrationRequest registrationRequest = new RegistrationRequest("appUser");
|
||||||
|
registrationRequest.setAffiliation("org1.department1");
|
||||||
|
registrationRequest.setEnrollmentID("appUser");
|
||||||
|
String enrollmentSecret = caClient.register(registrationRequest, admin);
|
||||||
|
Enrollment enrollment = caClient.enroll("appUser", enrollmentSecret);
|
||||||
|
Identity user = Identities.newX509Identity("Org1MSP", adminIdentity.getCertificate(), adminIdentity.getPrivateKey());
|
||||||
|
wallet.put("appUser", user);
|
||||||
|
System.out.println("Successfully enrolled user \"appUser\" and imported it into the wallet");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.example;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class TestApp {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enrolls an admin
|
||||||
|
* Registers a user
|
||||||
|
* Initializes the ledger with some assets
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void start() throws Exception {
|
||||||
|
EnrollAdmin.main(null);
|
||||||
|
RegisterUser.main(null);
|
||||||
|
ClientApp.main(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates asset7, modify the parameters to create other assets
|
||||||
|
@Test
|
||||||
|
public void CreateAsset() throws Exception {
|
||||||
|
ClientApp.CreateAsset("asset7", "magenta", "20", "kenysha", "800");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads asset1, modify the parameters to read other assets
|
||||||
|
@Test
|
||||||
|
public void ReadAsset() throws Exception {
|
||||||
|
ClientApp.ReadAsset("asset7");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates asset3, modify the parameters to read other assets
|
||||||
|
@Test
|
||||||
|
public void UpdateAsset() throws Exception {
|
||||||
|
ClientApp.UpdateAsset("asset3", "blue", "20", "Jin Soo", "600");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes asset2, modify the parameters to delete other assets
|
||||||
|
@Test
|
||||||
|
public void DeleteAsset() throws Exception {
|
||||||
|
ClientApp.DeleteAsset("asset2");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfers asset1 to arturo, modify the parameters to transfer other assets
|
||||||
|
@Test
|
||||||
|
public void TransferAsset() throws Exception {
|
||||||
|
ClientApp.TransferAsset("asset1", "arturo");
|
||||||
|
}
|
||||||
|
// Outputs all the assets
|
||||||
|
@Test
|
||||||
|
public void GetAllAssets() throws Exception {
|
||||||
|
ClientApp.GetAllAssets();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue