如何开发一个CorDapp

环境准备

  • JDK(8u131以上)

corda训练营

代码地址:

https://github.com/corda/bootcamp-cordapp

个人gitee项目:https://gitee.com/zheshiyigegexingwangzhan/bootcamp-cordapp.git

添加国内镜像

当前项目修改

下载代码之后为了更快的下载依赖,添加国内的镜像:

1
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/'}

直接修改全局的gradle配置

在**~/.gradle目录下新建init.gradle**文件,写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
allprojects{
repositories {
def ALIYUN_REPOSITORY_URL = 'http://maven.aliyun.com/nexus/content/groups/public'
def ALIYUN_JCENTER_URL = 'http://maven.aliyun.com/nexus/content/repositories/jcenter'
all { ArtifactRepository repo ->
if(repo instanceof MavenArtifactRepository){
def url = repo.url.toString()
if (url.startsWith('https://repo1.maven.org/maven2')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $ALIYUN_REPOSITORY_URL."
remove repo
}
if (url.startsWith('https://jcenter.bintray.com/')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $ALIYUN_JCENTER_URL."
remove repo
}
}
}
maven {
url ALIYUN_REPOSITORY_URL
url ALIYUN_JCENTER_URL
}
}
}

测试代码

运行ProjectImportedOKTest单测,如果通过说明环境没有问题

我的运行结果如下:

1
2
3
4
5
6
7
8
9
> Configure project :
Repository https://jcenter.bintray.com/ replaced by http://maven.aliyun.com/nexus/content/repositories/jcenter.
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test

代码开发

State开发

一个state需要实现ContractState,ContractState中有一个方法getParticipants(),返回的是List<AbstractParty>,表示在这个state发生了交易时需要通知谁,让谁知道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package bootcamp;

import com.google.common.collect.ImmutableList;
import net.corda.core.contracts.ContractState;
import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.Party;
import org.jetbrains.annotations.NotNull;

import java.util.List;

public class IouState implements ContractState {
/**
* 发行人
*/
private Party issuer;
/**
* 拥有者
*/
private Party owner;
/**
* 金额
*/
private int amount;

public IouState(Party issuer, Party owner, int amount) {
this.issuer = issuer;
this.owner = owner;
this.amount = amount;
}

@NotNull
@Override
public List<AbstractParty> getParticipants() {
return ImmutableList.of(issuer, owner);
}

public Party getIssuer() {
return issuer;
}

public Party getOwner() {
return owner;
}

public int getAmount() {
return amount;
}
}

Contract开发

一个contract简单的理解就是一些校验的规则,需要实现Contract类,Contract类如下,只有一个verify方法,验证LedgerTransaction是否正确,如果不正确就抛IllegalArgumentException异常。

1
2
3
4
5
6
7
8
9
10
interface Contract {
/**
* Takes an object that represents a state transition, and ensures the inputs/outputs/commands make sense.
* Must throw an exception if there's a problem that should prevent state transition. Takes a single object
* rather than an argument so that additional data can be added without breaking binary compatibility with
* existing contract code.
*/
@Throws(IllegalArgumentException::class)
fun verify(tx: LedgerTransaction)
}

在开发一个contract时,Corda提议的三个验证的类型:

  • 输入与输出个数的校验(Shape Constraint,No. input states, No. output states, command)
  • 输入与输出的内容的校验(Context Constraint),业务校验
  • 需要的签名的校验(Required Singer Constraint)

image-20201202115747238

按照上图的规则IouContract的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package bootcamp;

import net.corda.core.contracts.Command;
import net.corda.core.contracts.CommandData;
import net.corda.core.contracts.Contract;
import net.corda.core.contracts.ContractState;
import net.corda.core.transactions.LedgerTransaction;
import org.apache.commons.collections4.CollectionUtils;

public class IouContract implements Contract {
public static String ID = "bootcamp.IouContract";

@Override
public void verify(LedgerTransaction tx) {
// 1、Shape Constraint,No. input states, No. output states, command
if (tx.getCommands().size() != 1) {
throw new IllegalArgumentException("command size must be one");
}
Command<CommandData> command = tx.getCommand(0);
if (!(command.getValue() instanceof Commands.Issue)) {
throw new IllegalArgumentException("command must be Issue");
}
if (CollectionUtils.isNotEmpty(tx.getInputs())) {
throw new IllegalArgumentException("Issue must be not inputs");
}
if (tx.getOutputs().size() != 1) {
throw new IllegalArgumentException("Issue outputs must be one");
}

ContractState output = tx.getOutput(0);
// 2、Context Constraint
if (!(output instanceof IouState)) {
throw new IllegalArgumentException("state must be IouState");
}
IouState iouState = (IouState) output;
if (iouState.getAmount() <= 0) {
throw new IllegalArgumentException("issue amount must big than zero");
}

// 3、Required Singer Constraint
if (!command.getSigners().contains(iouState.getIssuer().getOwningKey())) {
throw new IllegalArgumentException("issue business must be sing by issuer");
}
}

public interface Commands extends CommandData {
class Issue implements Commands {
}
}
}

Flow开发

flow有两种

  • 可以在本地主动启动的flow
  • 只能通过其他的flow启动的flow

发起一个交易的flow都是可以在本地主动启动的flow,有以下特点

  • 需要添加注解@InitiatingFlow来表示他是一个可以初始化的flow
  • 需要添加注@StartableByRPC或者@StartableByService来说明启动的方式
  • flow需要继承自FlowLogic,业务逻辑在call方法中实现
  • call方法需要添加@Suspendable注解
  • 指定notary,校验是否双花
  • 创建交易,交易中必须包含command,如果有output必须指定contract来进行验证;可以没有input
  • 然后就是通用的流程,验证交易、收集签名、交易入库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package bootcamp;

import co.paralleluniverse.fibers.Suspendable;
import com.google.common.collect.ImmutableList;
import net.corda.core.contracts.StateAndRef;
import net.corda.core.flows.*;
import net.corda.core.identity.Party;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.transactions.TransactionBuilder;
import net.corda.core.utilities.ProgressTracker;

import static java.util.Collections.singletonList;

/**
*
* @author nicai
*/
@InitiatingFlow
@StartableByRPC
public class IouIssueFlowInitiator extends FlowLogic<SignedTransaction> {
private final Party owner;
private final int amount;

public IouIssueFlowInitiator(Party owner, int amount) {
this.owner = owner;
this.amount = amount;
}

private final ProgressTracker progressTracker = new ProgressTracker();

@Override
public ProgressTracker getProgressTracker() {
return progressTracker;
}

@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// We choose our transaction's notary (the notary prevents double-spends).
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
// We get a reference to our own identity.
Party issuer = getOurIdentity();

// We create our new IouState.
IouState iouState = new IouState(issuer, owner, amount);

// We build our transaction.
TransactionBuilder transactionBuilder = new TransactionBuilder(notary)
// .addInputState()
.addOutputState(iouState, IouContract.ID)
.addCommand(new IouContract.Commands.Issue(), ImmutableList.of(issuer.getOwningKey(), owner.getOwningKey()));

// We check our transaction is valid based on its contracts.
transactionBuilder.verify(getServiceHub());

FlowSession session = initiateFlow(owner);

// We sign the transaction with our private key, making it immutable.
SignedTransaction signedTransaction = getServiceHub().signInitialTransaction(transactionBuilder);

// The counterparty signs the transaction
SignedTransaction fullySignedTransaction = subFlow(new CollectSignaturesFlow(signedTransaction, singletonList(session)));

// We get the transaction notarised and recorded automatically by the platform.
return subFlow(new FinalityFlow(fullySignedTransaction, singletonList(session)));
}
}

被动启动的flow有以下特点:

  • 需要注解@InitiatedBy(IouIssueFlowInitiator.class)指定谁能启动这个flow
  • flow需要继承自FlowLogic,业务逻辑在call方法中实现
  • call方法需要添加@Suspendable注解
  • 需要有实例变量FlowSession,保存调用者的FlowSession
  • call方法需要验证交易,然后执行接收交易的标准流程ReceiveFinalityFlow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package bootcamp;

import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.flows.*;
import net.corda.core.transactions.SignedTransaction;

@InitiatedBy(IouIssueFlowInitiator.class)
public class IouIssueFlowResponder extends FlowLogic<Void> {

private final FlowSession otherSide;

public IouIssueFlowResponder(FlowSession otherSide) {
this.otherSide = otherSide;
}

@Override
@Suspendable
public Void call() throws FlowException {
SignedTransaction signedTransaction = subFlow(new SignTransactionFlow(otherSide) {
@Suspendable
@Override
protected void checkTransaction(SignedTransaction stx) throws FlowException {
// Implement responder flow transaction checks here
}
});
subFlow(new ReceiveFinalityFlow(otherSide, signedTransaction.getId()));
return null;
}
}

运行

打包

1
./gradlew deployNodes

运行所有的节点

1
sudo ./build/nodes/runnodes

启动一个流程

1
flow start IouIssueFlow owner: PartyB, amount: 99

我本地日志如下:

1
2
3
4
5
6
7
 ✅   Starting
Requesting signature by notary service
Requesting signature by Notary service
Validating response from Notary service
✅ Broadcasting transaction to participants
➡️ Done
Flow completed with result: SignedTransaction(id=14D268667D208D26BF92ADC1F58003DFC9EAF7E036ACB2C2CABC153E627500C0)

查询生成的数据

1
run vaultQuery contractStateType: bootcamp.IouState

我本地的结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
states:
- state:
data: !<bootcamp.IouState>
issuer: "O=PartyA, L=London, C=GB"
owner: "O=PartyB, L=New York, C=US"
amount: 99
contract: "bootcamp.IouContract"
notary: "O=Notary, L=London, C=GB"
encumbrance: null
constraint: !<net.corda.core.contracts.SignatureAttachmentConstraint>
key: "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTEw3G5d2maAq8vtLE4kZHgCs5jcB1N31cx1hpsLeqG2ngSysVHqcXhbNts6SkRWDaV7xNcr6MtcbufGUchxredBb6"
ref:
txhash: "14D268667D208D26BF92ADC1F58003DFC9EAF7E036ACB2C2CABC153E627500C0"
index: 0
statesMetadata:
- ref:
txhash: "14D268667D208D26BF92ADC1F58003DFC9EAF7E036ACB2C2CABC153E627500C0"
index: 0
contractStateClassName: "bootcamp.IouState"
recordedTime: "2020-12-03T09:49:48.373Z"
consumedTime: null
status: "UNCONSUMED"
notary: "O=Notary, L=London, C=GB"
lockId: null
lockUpdateTime: null
relevancyStatus: "RELEVANT"
constraintInfo:
constraint:
key: "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTEw3G5d2maAq8vtLE4kZHgCs5jcB1N31cx1hpsLeqG2ngSysVHqcXhbNts6SkRWDaV7xNcr6MtcbufGUchxredBb6"
totalStatesAvailable: -1
stateTypes: "UNCONSUMED"
otherResults: []

节点可视化工具

参考网站:https://docs.corda.net/docs/corda-os/4.6/node-explorer.html

可以下载node-explorer来查看节点信息。

第一次打开界面

login

  • Node Hostname:localhost
  • Node Port:RPC connection address可以在启动的窗口查看,或者配置文件查看
  • RPC Username:在配置文件
  • RPC Password:在配置文件查看

RPC connection address

image-20201207165759379

使用spring开发corda:

https://manosbatsis.github.io/corbeans/