用Corda开发

一篇文章中,我概述了Corda想要实现的目标以及为此所做的设计决策。 知道这些信息很高兴,因此我们可以对平台有所了解,但这不会帮助我们编写可以利用Corda的系统。 为此,我们需要了解Corda的各个组件如何工作并配合在一起,然后才可以开始编写一个真正可以完成工作并从Corda角度正确工作的应用程序。 我认为这与学习任何其他框架一样,但是我们需要时时回味以提醒我们,我们实际上是在分布式账本技术平台上设计的,因此我们可以确保我们所使用的应用程序创建是经过适当设计的。

在本文中,我们将着眼于编写一个非常简单的Corda应用程序。

Corda与任何JVM语言兼容。 我知道您的想法,但是不,它不是用Java编写的。 它实际上是用Kotlin编写的。 如果您通常使用Java(就像我一样),这可能需要一点点额外的学习来掌握Kotlin,但是花很长时间您就可以适应它。 因此,我个人建议使用Kotlin编写自己的代码,以使整个堆栈使用相同的语言,因此,当您需要深入研究Corda自己的代码时,与您刚刚编写的代码相比,它看起来并不陌生。 显然,这只是我的建议,您可以改用Clojure,但是如果不先将其转换为等效的Clojure,就会发现很难掌握所有现有示例。

在不首先了解Corda关键概念的情况下,直接进入代码有点困难。 我个人认为,Corda提供的关于该主题的文档非常有用,并且可以帮助您轻松地完成这些工作,而不是亲自研究这些内容。

出于本文的目的,我认为最好省去实际设置Corda节点所需的配置,相反,我们应该完全专注于编写Corda应用程序。 在以后的文章中,我将介绍设置节点所需的配置,以便它们可以彼此交互。 现在,您只需要相信我这个东西就可以了。

应用概述

在开始编写任何代码之前,我们需要了解应用程序正在尝试实现的目标。 我不会提出自己的示例,而是依靠r3的培训材料,因为该示例相当容易理解。 这个示例是“ IOU”流程(我欠您),有人要求提供一些款项,这些款项将在以后偿还。 在本文中,我们将仅关注发行IOU。

用Corda开发

借条交易的简化图

下面是一个序列图,其中包含在两个人之间发出IOU所涉及的非常简化的步骤:

如您所见,借款人要一些钱,如果贷方同意,那么他们都试图记住借款人欠贷方一些钱。 这个过程很简单,但是即使这样,它也已经简化了一些。 在此过程中,我们实际上并未提及何时以及如何在他们之间转移资金。 为了本教程的目的,我们将不进行此转移,而只专注于保存它们之间借入和借出资金的意图。

那么,这种流程如何在Corda中建模? 再说一次,在继续之前,我想提一下Corda的关键概念,因为它们对于理解正确的情况非常重要。

科尔达

借条交易图

为了在Corda中建模流程,我们需要替换一些步骤。 确定借多少钱就成为包含此信息的交易的创建。 现在,对提议的内容感到满意的一方可以通过用其密钥签名交易来表示。 借方与贷方之间的通信被替换为在它们之间发送交易。 最后,记住谁欠谁,现在与双方保存下来的交易就结为一体。 通过使用此新信息更改简单图,将创建新版本:

还有更多步骤,但是过程仍然非常简单。 该图确实说明了一切,我认为我没有什么可以补充的了。 我要说的是,通过删除公证人,我仍进一步简化了该图,但仅着眼于我们尝试建模的过程。 稍后,我们将简要介绍一下公证人,但我将再次建议Corda编写有关该主题的文档 (稍后再做同样的事情)。

这些图应该为我们提供足够的指导,以将我们的Corda应用程序放在一起,以便在两方之间发出IOU。

状态

在编码部分,让我们从状态开始。 关于状态的信息可以发现这里从琴弦文档。 状态在节点之间的网络中传递,最终发现自己存储在分类帐中。 就事务而言,状态可以是输入或输出,并且其中许多可以存在于单个事务中。 它们也是不可变的,这是建立事务中使用的状态链所必需的。

如前所述,我依靠r3培训材料 。 以下是将与应用程序一起传递的IOUState

data class IOUState(val amount: Amount<Currency>,
                    val lender: Party,
                    val borrower: Party,
                    val paid: Amount<Currency> = Amount(0, amount.token),
                    override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState {    
                    override val participants: List<Party> get() = listOf(lender, borrower)
                    }

由于“ IOU”的概念,几乎所有领域都是有意义的,无需过多解释。 amount是借出的金额, lender出借的钱等等。 唯一需要说明的属性是linearId ,它的类型为UniqueIdentifier ,该类基本上是一个UUID,实际上它的internalId是从UUID类生成的。

该状态扩展了LinearState ,它是Corda使用的一种通用状态,另一种是FungibleState 。 这两个都是ContractState实现。 LinearState用来表示引用其文档“通过取代自身而演变”的状态。 这样,在更新状态时,应将其作为事务的输入包括在内,并输出更新的版本。 现在,将旧状态保存到Vault中时,将从UNCONSUMED标记为CONSUMED (Corda在数据库上的抽象化)。

ContractState需要一个返回州参与者的属性。

参与者是国家非常重要的一部分。 此列表中定义的参与方确定谁将状态保存到其自己的保管库/数据库。 如果您发现自己的状态没有保存到应该知道的状态,则很可能是问题所在。 我亲自遇到了这个错误并感到痛苦。

在上面的代码中,参与者没有包含在构造函数中,而是依赖于定义可用于检索他们的get函数。 在这里,它返回出lenderborrower因为它们是交易中仅有的两个参与方。 如果愿意,可以将participants添加到构造函数中,如下所示:

data class IOUState(val amount: Amount<Currency>,
                    val lender: Party,
                    val borrower: Party,
                    val paid: Amount<Currency> = Amount(0, amount.token),
                    override val linearId: UniqueIdentifier = UniqueIdentifier(),
                    override val participants: List<Party> = listOf(lender, borrower)) : LinearState

这使您可以定义可能不包含在状态中的参与者。 您采用哪种方法取决于您的用例,对于本教程而言,这两种方法都可以胜任。

合约

接下来是合同。 合同用于验证交易中所有参与方针对给定命令的输入和输出状态,例如,该命令可以是发布状态或还清欠款。 我们将在本节稍后部分介绍命令,但现在我们应该可以复习它们。

由于具有表达力,因此合同写起来非常好。 我们能够在合同中写出交易有效所必须满足的条件。 如果未满足任何条件,则将引发异常,通常会终止当前事务,因为涉及方发现该事务无效。

这些条件使用Corda定义的requireThat DSL来指定条件以及详细描述事务错误的错误消息。 这使签订合同和了解合同的执行情况变得轻松而轻松,因为代码条件得到了英语消息(或您想用哪种语言编写)的很好补充。

以下是用于验证IOUState定义的IOUState的合同示例,同样,该合同摘自r3的培训材料

class IOUContract : Contract {
    companion object {
        @JvmStatic
        val IOU_CONTRACT_ID = "net.corda.contracts.IOUContract"
    }

    interface Commands : CommandData {
        class Issue : TypeOnlyCommandData(), Commands
        class Transfer : TypeOnlyCommandData(), Commands
        class Settle : TypeOnlyCommandData(), Commands
    }

    override fun verify(tx: LedgerTransaction) {
        val command = tx.commands.requireSingleCommand<Commands>()
        when (command.value) {
            is Commands.Issue -> requireThat {
                "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
                "Only one output state should be created when issuing an IOU." using (tx.outputs.size == 1)
                val iou = tx.outputStates.single() as IOUState
                "A newly issued IOU must have a positive amount." using (iou.amount.quantity > 0)
                "The lender and borrower cannot have the same identity." using (iou.borrower != iou.lender)
                "Both lender and borrower together only may sign IOU issue transaction." using
                        (command.signers.toSet() == iou.participants.map { it.owningKey }.toSet())
            }
            is Commands.Transfer -> requireThat {
                // more conditions
            }
            is Commands.Settle -> {
               // more conditions
            }
        }
    }
}

就本文而言,我已经简化了合同,因为我们将仅专注于实现一种命令类型。 让我们从顶部开始,一直往下走。

IOUContract实现了Contract要求它现在具有一个verify函数,该函数将被调用以验证(因此得名)交易。

合同类别名称

合同的类别名称已包含在此处:

companion object {
    @JvmStatic
    val IOU_CONTRACT_ID = "net.corda.contracts.IOUContract"
}

需要反射时,可用于Corda的其他部分。 r3的培训材料就是这样做的,但我个人认为这有点时髦,应该以不同的方式进行。

companion object {
    @JvmStatic
    val IOU_CONTRACT_ID = IOUContract::class.qualifiedName!!
}

我认为这要好一些。 如果该类已移动或重命名,则此解决方案无需更改字符串的值。 话虽这么说,Corda遵循在其代码中使用字符串的约定,所以如果您需要使用内置合同,则可以期望它们遵循这种格式。 我将由您决定要选择哪一个。

指令

现在,我们可以讨论本节前面简要提到的命令。 我再次将它们放在下面,因此您无需再次滚动:

interface Commands : CommandData {
    class Issue : TypeOnlyCommandData(), Commands
    class Transfer : TypeOnlyCommandData(), Commands
    class Settle : TypeOnlyCommandData(), Commands
}

这些命令用于指定事务的意图。 由于已与合同建立联系,因此已将其放置在此处,因为他们确定必须验证哪些条件。 话虽如此,如果需要,您可以将这些命令放在其他位置,例如在其自己的文件中或合同类之外,但在同一文件中。 只要您的解决方案能最好地描述您的意图,那么您就可能朝着正确的方向前进。

由于这些命令很简单,仅用于指定意图, TypeOnlyCommandData扩展了TypeOnlyCommandData 。 还可以使用其他抽象类来指定我们可能要使用的命令,例如MoveCommand

我们将在下一节中看到如何使用命令。

实施验证

这是大多数魔术发生的地方,代码已复制并粘贴到下面:

override fun verify(tx: LedgerTransaction) {
    val command = tx.commands.requireSingleCommand()
    when (command.value) {
        is Commands.Issue -> requireThat {
            "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
            "Only one output state should be created when issuing an IOU." using (tx.outputs.size == 1)
            val iou = tx.outputStates.single() as IOUState
            "A newly issued IOU must have a positive amount." using (iou.amount.quantity > 0)
            "The lender and borrower cannot have the same identity." using (iou.borrower != iou.lender)
            "Both lender and borrower together only may sign IOU issue transaction." using
                    (command.signers.toSet() == iou.participants.map { it.owningKey }.toSet())
        }
        is Commands.Transfer -> requireThat {
            // more conditions
        }
        is Commands.Settle -> {
            // more conditions
        }
    }
}

验证功能检查提议的交易是否有效。 如果是这样,则该交易将继续进行,最有可能被签名并提交到分类帐,但是如果不满足任何条件,则将抛出IllegalArgumentException ,从而有可能导致提议的交易终止。

通常,Corda如何处理未满足的需求就是例外。 当引发异常时,假设没有任何尝试捕获异常,则执行将终止并向上传播,直到被捕获或最终在控制台输出中为止。 使用此方法,它提供了一种控制事务流程的简单方法,因为它可以在执行过程中的任何位置(甚至在交易对手的节点上)停止,因为异常将被传递回发起者。

进入验证码本身。 检索事务在其状态下执行的命令,并根据类型进行不同的检查以检查事务的有效性。 Corda提供的requireThat DSL允许您编写一个必须为true的条件,以继续处理条件为false时输出的错误消息。

让我们更仔细地看一下其中的requireThat语句:

val iou = tx.outputStates.single() as IOUState
"A newly issued IOU must have a positive amount." using (iou.amount.quantity > 0)

这里没有太多解释。 DSL负责声明的意图。 我要指出的是语法:

<string message of what condition should be met> using <condition it must pass>

非常简单。 愚蠢地抓住了我一点,如果条件中包含空格,则必须将其包含在方括号中。 最后,DSL可以包含不在条件表达式中的代码,从而允许您初始化变量,然后可以在实际条件中使用这些变量。

现在合同就足够了。 当我们将IOUContract付诸实施时,它们将在下一部分再次弹出。

流量

在Corda中,流程是我们连接前面所有部分的中心点。 州,合同和命令都齐心协力编写将提议新交易的代码,如果每个人都满意,则将其发送给所有交易对手进行签名并将其提交到分类帐。 您可以在流中执行更复杂的操作,但是对于本教程,我们将坚持基础知识。

在前面几节中的示例之后,我们现在将实现IOUIssueFlow 。 同样,这取自r3的培训材料 。 下面是一个整体代码,然后我们将进行拆分和检查:

@InitiatingFlow
@StartableByRPC
class IOUIssueFlow(private val state: IOUState) : FlowLogic() {

    @Suspendable
    override fun call(): SignedTransaction {
        val notary = serviceHub.networkMapCache.notaryIdentities.first()
        val issueCommand = Command(IOUContract.Commands.Issue(), state.participants.map { it.owningKey })
        val transaction = TransactionBuilder(notary = notary)
        transaction.addOutputState(state, IOUContract.IOU_CONTRACT_ID)
        transaction.addCommand(issueCommand)
        transaction.verify(serviceHub)
        val singleSignedTransaction = serviceHub.signInitialTransaction(transaction)
        val sessions = (state.participants - ourIdentity).map { initiateFlow(it) }.toSet()
        val allSignedTransaction = subFlow(CollectSignaturesFlow(singleSignedTransaction, sessions))
        subFlow(FinalityFlow(allSignedTransaction))
        return allSignedTransaction
    }
}

@InitiatedBy(IOUIssueFlow::class)
class IOUIssueFlowResponder(private val flowSession: FlowSession) : FlowLogic() {
    @Suspendable
    override fun call() {
        val signedTransactionFlow = object : SignTransactionFlow(flowSession) {
            override fun checkTransaction(stx: SignedTransaction) {
                requireThat {
                    val output = stx.tx.outputs.single().data
                    "This must be an IOU transaction" using (output is IOUState)
                }
            }
        }
        subFlow(signedTransactionFlow)
    }
}

这段代码的流程(是双关语)是相当简单的,并且可能是您在自己的应用程序中编写的典型流程之一。

它所做的就是:

  • 建立状态
  • 将状态添加到新事务
  • 通过合同验证交易
  • 签署交易
  • 要求交易对手签名
  • 为所有参与者保存交易

既然我们知道了该流程中执行的步骤,我们就可以遍历并解释在代码中完成的步骤。

发起流程

首先,使用@InitiatingFlow注释流类,并扩展FlowLogic 。 请求与交易对手进行通信的任何流程都需要这种组合。 FlowLogic包含一个需要由流实现的抽象函数call 。 这是所有魔术发生的地方。 当流程被触发时,我们将在稍后进行介绍, call被执行,并且我们放入函数内部的任何逻辑显然都会运行。 FlowLogic是通用的( FlowLogic<T> ),其中T确定call的返回类型。 在上面的示例中,返回了SignedTransaction ,但是如果您不希望将任何内容返回给流的调用者,则使用FlowLogic<Unit>是完全可行的。

接下来是@StartableByRPC批注。 这允许从RPC连接调用该流,该RPC连接是Corda节点的外部与其内部之间的接口。 当我们考虑触发流程时,我们将对此进行更多讨论。

弹出另一个注释。 @Suspendable实际上起源于quasar-core而不是Corda自己的库之一。 此批注很重要,如果您忘记添加它,可能会遇到错误,不一定表明问题出在哪里。 与交易对手通信的所有功能都需要使用此功能。 如名称“ suspendable”所暗示的那样,注释允许该功能在交易对手处理其交易方时被暂停。 这里发生了很大的魔力,它在关于流Corda文档中简要地涉及到。

现在,我们已经完成了注释,可以查看call的内容。 我再次将其粘贴到下面,以节省滚动时的精力:

@Suspendable
    override fun call(): SignedTransaction {
    val notary = serviceHub.networkMapCache.notaryIdentities.first()
    val issueCommand = Command(IOUContract.Commands.Issue(), state.participants.map { it.owningKey })
    val transaction = TransactionBuilder(notary = notary)
    transaction.addOutputState(state, IOUContract.IOU_CONTRACT_ID)
    transaction.addCommand(issueCommand)
    transaction.verify(serviceHub)
    val singleSignedTransaction = serviceHub.signInitialTransaction(transaction)
    val sessions = (state.participants - ourIdentity).map { initiateFlow(it) }.toSet()
    val allSignedTransaction = subFlow(CollectSignaturesFlow(singleSignedTransaction, sessions))
    subFlow(FinalityFlow(allSignedTransaction))
    return allSignedTransaction
}

在本例中,我将所有内容都放入一个函数中,因此我们可以逐步了解正在发生的事情。 稍后,我将展示此函数的另一个版本,将其拆分为较小的函数,我个人认为该函数很好地传达了流程的目的。

创建交易

首先,我们将考虑构建建议的交易,相关代码已提取如下:

val notary = serviceHub.networkMapCache.notaryIdentities.first()
val issueCommand = Command(IOUContract.Commands.Issue(), state.participants.map { it.owningKey })
val transaction = TransactionBuilder(notary = notary)
transaction.addOutputState(state, IOUContract.IOU_CONTRACT_ID.toString())
transaction.addCommand(issueCommand)

出于这篇文章的目的,我们将假设只有一个公证人可以让我们变得懒惰,并且只从列表中检索第一个公证人。 如果您不知道什么是公证人,则建议像以前一样, 仔细阅读Corda关键概念以获取对该主题的很好解释。 现在,我将为您提供最基本的条件。 公证人是一个节点,其唯一目的是验证发送给它的交易中是否没有发生重复消费,并且如果设置为这样做,还可以运行额外的验证。

serviceHub自带提供,因为我们扩大FlowLogic ; 然后,函数networkMapCache将为我们提供网络上各方的身份,而notaryIdentities将其范围notaryIdentities缩小。 正如我之前提到的,我们将变得很懒,只是从该列表中检索第一个。 您希望在交易中使用的公证人的取回方式可能会根据您的要求而改变。

然后,我们创建一个表示事务意图的命令。 在这种情况下,我们使用前面定义的IOUContract.Commands.Issue 。 在创建命令时,我们还需要提供签署交易所需的各方公共密钥。 it是一个PartyowningKey代表其公共密钥。 此事务中的唯一签名者包含在状态participants属性内,但可以传递独立的列表。

现在我们已经检索或创建了交易所需的所有组件。 现在我们需要真正开始将它们放在一起。 TransactionBuilder就是这样做的。 我们检索到的公证人只能通过TransactionBuilder构造函数传递,而其他公证人则具有各种add方法,并且都包含在构造函数中。 addOutputState接受传递到流中的state以及将对其进行验证的合同名称。 记得我提到过两种获取此名称的方法; 通过对象内的公共属性,Corda通常如何执行此操作,或者通过手动添加类名自己,最终目的是相同的。 我们添加到该事务中的最后一个组件是我们创建的命令。

验证并签署交易

下一个代码块着重于验证和签名交易,相关代码再次粘贴在下面:

transaction.verify(serviceHub)
val singleSignedTransaction = serviceHub.signInitialTransaction(transaction)

一旦我们满意要包含在交易中的所有内容,我们就需要进行验证。 只需调用TransactionBuilder提供的verify函数。 此功能导致针对交易运行合同内部的验证。 如合同部分前面所述,如果合同中的任何条件失败,则会引发异常。 因为在此代码中没有尝试捕获异常,所以当异常在堆栈上传播时,流将失败。

交易通过验证后,就我们(发起方)而言,该交易已准备好与其他方共享。 为此,将调用serviceHub.signInitialTransaction 。 这给我们留下了一个新的SignedTransaction ,当前仅由我们签名。 稍后,当公证人检查交易是否已由所有相关方签署时,签署此交易将变得很重要。

收集交易对手的签名

交易现在已由发起方验证并签名。 下一步是请求交易中涉及的交易对手的签名。 一旦完成,交易就可以坚持到每个人的保险库,因为他们都同意交易是正确的并满足他们的需求。

以下是负责收集签名的代码:

val sessions = (state.participants - ourIdentity).map { initiateFlow(it) }.toSet()
val allSignedTransaction = subFlow(CollectSignaturesFlow(singleSignedTransaction, sessions))

此交易的对手方由participants列表中的各方定义。 如果我们还记得状态中participants字段的构造方式,那么其中仅包含两个当事方,因此只有两个当事方需要签署交易。 尽管该声明是正确的,但交易已由发起方签名,因此现在仅单个交易对手( lender )需要对其进行签名。

要将交易发送给交易对手,我们首先需要与他们建立会话。 initiateFlow就是这样做的。 它接受一个Party并返回一个FlowSession以用于通信。 如前所述,发起者不需要通过此构造再次签名交易,因此在代码中,它们已从为其创建通信会话的各方中删除。 由于我们知道该交易涉及谁,因此可以改写以下内容:

val session = initiateFlow(state.lender)
val allSignedTransaction = subFlow(CollectSignaturesFlow(singleSignedTransaction, listOf(session)))

与其依赖参与者列表,我们不如为lender创建一个会话,因为我们对状态的了解表明他们是唯一的交易对手。

FlowSession需要是二手内部CollectSignaturesFlow与沿SignedTransaction这仍然是由发起者在这一点上只签。 这是我们第一次遇到subFlow 。 这些流程与我们在本文中看到的流程类似,是从另一个流程中调用的。 CollectSignaturesFlow本身无法触发,因为它没有使用@InitiatingFlow注释,因此只能在subFlow内部使用。 开箱即用提供的大多数流量都属于同一类别。

所有subFlow所做的工作都是通过调用流的call函数并返回流通常返回的内容来运行传递给它的流。 一个流不需要将任何特殊的东西传递给subFlow ,如果我们需要的话,可以从另一个流将IOUIssueFlow传递给它。

Corda提供了许多典型操作的流程,这些流程需要在我们自己的流程中重复进行。 这些通过subFlow ,包括(以及更多): CollectSignaturesFlowSignTransactionFlowSendTransactionFlowReceiveTransactionFlow

无论如何,回到手头的流! CollectSignaturesFlow发送SignedTransaction给对方,并等待他们的回应。 在下一部分中,我们将研究如何将响应发送回去。 一旦返回, SignedTransaction现在已经完成,因为它已经被所有人签名,并且现在可以保存。

坚持已签署的交易

这是我们在细分过程中看到的最小片段,整行:

subFlow(FinalityFlow(allSignedTransaction))

尽管对于一个内衬而言,这段代码相当麻烦。 FinalityFlow很可能总是在流的末尾被调用,至少对于更简单的流而言。

调用FinalityFlow将:

  • 将交易发送给公证人(如果需要)
  • 将交易保存到发起方的保管库
  • 广播给交易参与者以将其保存到他们的保管库中

最后两个步骤取决于公证人发现交易有效。 如果不是这样,则像往常一样,将引发异常,从而导致流程退出。 最后(是的,另一个双关语),您不需要编写任何代码来保存交易对手的交易,因为这一切都在幕后进行。

响应流

到目前为止,我们所研究的流程中的所有内容都在流程的发起方方面。 在该示例中,有几次将交易发送到交易对手,并发生了一些“东西”。 在此简短的部分中,我们将检查交易对手将运行的代码:

@InitiatedBy(IOUIssueFlow::class)
class IOUIssueFlowResponder(private val flowSession: FlowSession) : FlowLogic() {
    @Suspendable
    override fun call() {
        val signedTransactionFlow = object : SignTransactionFlow(flowSession) {
            override fun checkTransaction(stx: SignedTransaction) {
                requireThat {
                    val output = stx.tx.outputs.single().data
                    "This must be an IOU transaction" using (output is IOUState)
                }
            }
        }
        subFlow(signedTransactionFlow)
    }
}

和以前一样,该代码已包含在帖子的前面。

此类中最重要的一行是@InitiatedBy批注,该批注指定它接受来自哪个流并对其进行响应的流,在此示例中,这是我们已经经历过的IOUIssueFlow

由于IOUIssueFlowResponder也是一个流,因此它扩展了FlowLogic并将需要实现其自己的call版本。 构造函数中的FlowSession是发起方用来与此流进行通信的会话。 @Suspendable也可以在call使用,就像在启动流程中一样。

SignTransactionFlow是启动器中调用的CollectSignaturesFlow的另一半。 这是一个抽象类,需要实现checkTransaction 。 这包含交易对手可能希望针对交易运行的任何额外验证。 SignTransactioncall函数仍将根据合同验证交易,因此这是其他事情的机会。 确保交易符合交易对手的标准。 也就是说, checkTransaction还可以包含所需的checkTransaction代码,如果合同验证足够,它甚至可以为空。 除了让您展示外观之外,我还可以让您用生动的想象力想象一个空的功能……

最后,在subFlow的实现上SignTransactionFlow导致其得以执行。 运行合同中的验证,然后执行checkTransaction的内容,如果所有的支票都恢复正常,则对交易进行签名并发送回其来源。

此类中的代码可以根据需要简单或复杂。 对于本教程中使用的示例,简单就足够了。 这将根据您的要求以及发起者及其响应者之间必须传达的内容而改变。

我将如何构造启动流程

这小节仅供我参考,以建议您如何构造示例中使用的启动流程。 我认为这样比较好,但是您可能对此主题有不同的看法:

@InitiatingFlow
@StartableByRPC
class IOUIssueFlow(private val state: IOUState) : FlowLogic() {
    @@Suspendable
    override fun call(): SignedTransaction {
        val stx =  collectSignatures(verifyAndSign(transaction()))
        return subFlow(FinalityFlow(stx))
    }

    @Suspendable
    private fun collectSignatures(transaction: SignedTransaction): SignedTransaction {
        val sessions = (state.participants - ourIdentity).map { initiateFlow(it) }.toSet()
        return subFlow(CollectSignaturesFlow(transaction, sessions))
    }

    private fun verifyAndSign(transaction: TransactionBuilder): SignedTransaction {
        transaction.verify(serviceHub)
        return serviceHub.signInitialTransaction(transaction)
    }

    private fun transaction() = TransactionBuilder(notary()).apply {
        addOutputState(state, IOUContract.IOU_CONTRACT_ID)
        addCommand(Command(IOUContract.Commands.Issue(), state.participants.map { it.owningKey }))
    }

    private fun notary() = serviceHub.networkMapCache.notaryIdentities.first()
}

我认为我在这里没有做任何特别的事情,但是我认为这种分离使每一步都更加清晰。 call减少为两行(如果您确实需要,则减少为一行),我什至不用费心解释每种方法的作用,因为我们已经遍历了代码,并且函数名如此准确地描述了它们在做什么。 无论如何,如果您喜欢这样写,那就太好了。 如果您不这样做,请按照自己的意愿去做。 我保证,我不会难过……

开始流

在最后一部分中,我们将研究如何从Corda节点外部调用流。

有几种方法可以执行此操作,每种方法都略有不同。 但是,让我们保持简短和甜美,只看一下沼泽标准的startFlow函数:

proxy.startFlow(::IOUIssueFlow, state)

而已。 正如我所说,简短而甜美。 proxy的类型为CordaRPCOps ,其中包含大量功能,这些功能围绕通过RPC与Corda节点进行交互而产生。 startFlow是这些功能之一。 它使用流类的名称以及作为流的构造函数一部分的所有参数。 因此,在此示例中,将使用传入的IOUState来调用IOUIssueFlowcall函数,以在流中使用它。

返回FlowHandle<T> ,其中T是与调用流相同的通用类型,在这种情况下为SignedTransaction 。 然后可以调用returnValue来检索CordaFuture ,从而在结果可用时立即对其进行检索。 CordaFuture是标准Future的子类型,提供了一些额外的方法,其中之一是toCompletableFuture ,它可能对您有用或无用(无论如何,这对我很有用)。

包起来

我们到了,最后终于到了。

希望这篇文章可以对您有所帮助,帮助您了解如何使用Corda进行开发。 还有很多要学习的内容,因为我仅介绍了这篇文章的基础知识(我还需要自己先学习更多知识)。 在本文中,我们在检查所需的组件的同时实现了IOU的过程。 状态是网络上各方之间共享的事实,合同用于验证交易,流程包含提议新交易的逻辑。 有了这些信息,您就可以开始编写自己的流程了。 您可以使用本博文中未涉及的流程做更多的工作,但是这些基础知识应该可以通过您尝试编写的所有流程为您提供良好的服务。

我计划撰写更多关于Corda开发的文章,重点放在更复杂的功能上,并更深入地探究幕后发生的事情。

如果您发现这篇文章很有帮助,并希望在我撰写本文时跟上我的文章,那么可以在Twitter上通过@LankyDanDev关注我。

翻译自: https://www.javacodegeeks.com/2018/06/developing-with-corda.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值