当前位置: 首页 > 工具软件 > AElf > 使用案例 >

智能合约_AElf智能合约开发-第一个AElf智能合约

郑茂材
2023-12-01

第一个AElf智能合约

如果AElf脚手架项目能够正常启动并使用单节点产生区块,现在就可以着手做第一个智能合约了:Hello World

开发AElf智能合约只需要简单的C#语法,并且这些语法都可以通过教程中展示的Demo清晰展示,具备任何编程经验的读者应当都可以在了解基本开发流程之后上手开发智能合约。

现在我们从创建智能合约项目和对应的测试项目开始吧。

假设现在我们手上有一个还没有创建任何智能合约项目的AElf.Boilerplate解决方案,用VS或者Rider等IDE打开AElf.Boilerplate.sln文件以后,目录结构如下(省略了项目中的文件):

AElf.Boilerplate
├── contract
│   └── Directory.Build.props
├── src
│   │── AElf.Boilerplate.Launcher
│   │── AElf.Boilerplate.MainChain
│   │── AElf.Boilerplate.Tester
│   └── AElf.Contracts.Deployer
└── test

contract文件夹下的Directory.Build.props作用是帮contract目录下的所有项目引用同一版本的AElf.Sdk.CSharp,这样就创建智能合约的项目后就无须手动添加对后者的引用了;另外,该props文件导入了AElf.Contract.Tools.targets,后者通过MSBUILD定义了一些Protobuf中定义的方法和结构的代码生成脚本。

src文件夹包含脚手架启动、系统合约部署和模拟交易测试等项目,会在另一部分加以说明。

1. 定义智能合约的服务和结构(Proto文件)

开发智能合约的第一步,即定义一些该智能合约所能够对外提供的接口(对应GRpc中的service)和数据结构(对应GRpc中的message)。

在使用AElf的脚手架开发AElf智能合约时,我们推荐将定义合约服务和数据结构的proto文件放在AElf.Biolerplate项目的protobuf文件夹下:

aelf-boilerplate
├── chain
│   │── AElf.Boilerplate.sln
│   │── AElf.Contract.Tools.targets
│   │── contract
│   │── protobuf(就是这里)
│   │     │── acs0.proto
│   │     │── acs1.proto
│   │     │── ...
│   │     │── acs7.proto
│   │     │── aedpos_contract_impl.proto
│   │     │── aedpos_contract.proto
│   │     │── aelf
│   │     │   │── core.proto
│   │     │   └── options.proto
│   │     └── ...
│   │── scripts
│   │── src
│   └── test
├── README.md
└── web

可以看到protobuf文件夹中已经存在了相当数量的proto文件,其中包括AElf系统合约的proto文件和一系列AElf合约标准(acs*.proto),而aelf文件夹下的core.proto和options.proto是AElf为了方便合约开发定义的一些公共类型和扩展,开发AElf智能合约不可避免地需要导入这两个文件(以及其他一些由google定义的proto文件)。

无论用什么方法,在protobuf文件夹中创建名为hello_world_contract.proto文件(我们推荐合约相关的proto文件命名为”合约名_contract.proto“,尽管这并不必要),然后就可以开始定义该合约的服务和数据结构了:

syntax = "proto3";

import "aelf/options.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

option csharp_namespace = "AElf.Contracts.HelloWorld";

service HelloWorldContract {
    option (aelf.csharp_state) = "AElf.Contracts.HelloWorld.HelloWorldContractState";

    // Actions
    rpc Greet (google.protobuf.Empty) returns (google.protobuf.StringValue) { }
    rpc GreetTo (google.protobuf.StringValue) returns (GreetToOutput) { }
    
    // Views
    rpc GetGreetedList (google.protobuf.Empty) returns (GreetedList) {
        option (aelf.is_view) = true;
    }
}

message GreetToOutput {
    string name = 1;
    google.protobuf.Timestamp greet_time = 2;
}

message GreetedList {
    repeated string value = 1;
}

其中csharp_namespace = "AElf.Contracts.HelloWorld"意味着对应的C#代码应该位于AElf.Contracts.HelloWorld命名空间下。

option (aelf.csharp_state) = "AElf.Contracts.HelloWorld.HelloWorldContractState"意味着合约的状态应该定义在AElf.Contracts.HelloWorld命名空间下的HelloWorldContractState类中。

hello_world_contract.proto中,定义了两个Action服务(即该服务被调用后可能会修改区块链状态,可以理解为调用后全局账本会发生变化):Greet和GreetTo。其中Greet要求输入为google.protobuf.Empty类型(可以理解为空输入的占位),输出为google.protobuf.StringValue(传统意义上的字符串string);GreetTo要求输入为google.protobuf.StringValue,输出为该proto文件中自定义的GreetToOutput类型。

另外定义了一个View服务(仅作为查询当前区块链状态使用的方法):GetGreetedList,要求输入为google.protobuf.Empty类型,输出为自定义的GreetedList类型,可以看到GreetedList本质上就是一个字符串列表(Proto中使用repeated定义列表)。

TIPs:
  • 使用google.protobuf.Empty的前提是导入google/protobuf/empty.proto;
  • 使用google.protobuf.Timestamp的前提是导入google/protobuf/timestamp.proto;
  • 使用google.protobuf.StringValue的前提是导入google/protobuf/wrappers.proto;
  • 使用option (aelf.is_view) = true;声明该服务不会修改区块链状态,这个操作需要导入aelf/options.proto。(使用aelf.csharp_state前也需要导入aelf/options.proto)。

Proto文件创建完毕后,我们就可以开始动手实现其中定义的三个服务了。

2. 创建智能合约项目

在contract文件夹中创建一个名为”AElf.Contracts.HelloWorld“的netstandard2.0即可,然后修改csproj文件如下:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <RootNamespace>AElf.Contracts.HelloWorld</RootNamespace>
        <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
        <IsContract>true</IsContract>
    </PropertyGroup>

    <ItemGroup>
        <ContractCode Include="....protobufhello_world_contract.proto">
            <Link>ProtobufProtohello_world_contract.proto</Link>
        </ContractCode>
    </ItemGroup>

</Project>
如果当前项目要 实现某个proto文件中定义的服务,就使用 ContractCode标签引用该proto文件。

在编译AElf.Contracts.HelloWorld项目之前,先在该项目中创建一个名为HelloWorldContractState的C#代码文件(或者把Class1改名为HelloWorldContractState),并让HelloWorldContractState继承自ContractState,否则会编译失败并报错:

...The type or namespace name 'HelloWorldContractState' does not exist in the namespace 'AElf.Contracts.HelloWorld' (are you missing an assembly reference?)

编译成功后,解决方案管理器中该项目的目录结构应该是:

AElf.Contracts.HelloWorld
├── Protobuf
│   ├── Generated
│   │   │── HelloWorldContract.c.cs
│   │   └── HelloWorldContract.g.cs
│   └── Proto
│       ├── aelf
│       │   ├── core.proto
│       │   └── options.proto
│       └── hello_world_contract.proto
└── HelloWorldContractState.cs

HelloWorldContractState.cs是用来定义合约状态的地方,目前代码为:

using AElf.Sdk.CSharp.State;

namespace AElf.Contracts.HelloWorld
{
    public class HelloWorldContractState : ContractState
    {
        
    }
}

最后,在该项目中创建用来提供服务实现的C#代码文件HelloWorldContract.cs,并让其中的class继承自HelloWorldContractContainer.HelloWorldContractBase,就可以正式使用C#的override机制实现服务了。

在实现HelloWorldContract的代码之前,在这里我们分析一下三个服务的功能。

Greet服务比较简单,就是调用后返回一个”Hello World!“作为交易执行结果。

GreetTo类似于Greet,只不过返回的执行结果中包括了交易发送者指定的字符串。 、 GetGreetedList用来查询过往GreetTo交易的交易参数的记录。可以先不考虑清理数据等问题。但是过往的记录一定要作为状态去存储的。因此,需要在HelloWorldContractState中通过属性的方式,定义一个GreetedList的SingletonState类型:

using AElf.Sdk.CSharp.State;
 
 namespace AElf.Contracts.HelloWorld
 {
     public class HelloWorldContractState : ContractState
     {
         public SingletonState<GreetedList> GreetedList { get; set; }
     }
 }

对上面定义的GreetedList,可以在HelloWorldContract中直接使用State.GreetedList访问,不妨认为State.GreetedList是访问数据库的入口,使用State.GreetedList.Value可以从数据库中得到这个SingletonState<GreetedList>类型当前的值(通过访问其SingletonState的Value属性,AElf合约开发SDK会在背后完成组装key和依次读取缓存、数据库的操作)。

接下来我们看一下如何实现三个服务。

using Google.Protobuf.WellKnownTypes;

namespace AElf.Contracts.HelloWorld
{
    public class HelloWorldContract : HelloWorldContractContainer.HelloWorldContractBase
    {
        public override StringValue Greet(Empty input)
        {
            Context.LogDebug(() => "Hello World!");
            return new StringValue {Value = "Hello World!"};
        }

        public override GreetToOutput GreetTo(StringValue input)
        {
            // Should not greet to empty string or white space.
            Assert(!string.IsNullOrWhiteSpace(input.Value), "Invalid name.");

            // State.GreetedList.Value is null if not initialized.
            var greetList = State.GreetedList.Value ?? new GreetedList();

            // Add input.Value to State.GreetedList.Value if it's new to this list.
            if (!greetList.Value.Contains(input.Value))
            {
                greetList.Value.Add(input.Value);
            }

            // Update State.GreetedList.Value by setting it's value directly.
            State.GreetedList.Value = greetList;

            Context.LogDebug(() => $"Hello {input.Value}!");

            return new GreetToOutput
            {
                GreetTime = Context.CurrentBlockTime,
                Name = input.Value
            };
        }

        public override GreetedList GetGreetedList(Empty input)
        {
            return State.GreetedList.Value ?? new GreetedList();
        }
    }
}

3. 创建智能合约测试项目

使用TestKit

AElf Contract TestKit是专门用来测试AElf智能合约的测试框架,在这个框架中,可以通过构建某个合约的Stub并使用Stub实例所提供的方法,去模拟交易的执行(一般对应合约的Action方法)和查询(一般对应合约的View方法),再配合在测试用例中查询交易执行结果,便可以完成对合约方法的测试任务。

在test文件夹下,创建一个xUnit项目作为AElf智能合约测试项目,或者修改csproj文件为:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
        <RootNamespace>AElf.Contracts.HelloWorld</RootNamespace>
        <IsPackable>false</IsPackable>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="coverlet.msbuild" Version="2.5.1" />
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
        <PackageReference Include="Shouldly" Version="3.0.2" />
        <PackageReference Include="xunit" Version="2.4.1" />
        <PackageReference Include="xunit.runner.console" Version="2.4.1" />
        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="AElf.Contracts.TestKit" Version="0.8.3" />
    </ItemGroup>

    <ItemGroup>
        <ContractStub Include="....protobufacs0.proto">
            <Link>ProtobufProtoacs0.proto</Link>
        </ContractStub>
        <ContractStub Include="....protobufhello_world_contract.proto">
            <Link>ProtobufProtohello_world_contract.proto</Link>
        </ContractStub>
    </ItemGroup>

    <ItemGroup>
        <ProjectReference Include="....contractAElf.Contracts.HelloWorldAElf.Contracts.HelloWorld.csproj" />
    </ItemGroup>

</Project>
如果当前项目需要使用某个合约的Stub来模拟发送交易或查询交易,就使用 ContractStub标签引用该proto文件。
TIPs
  • RootNamespace为显式指定一个该项目下默认的命名空间,此处将默认命名空间改成与合约的代码一致,实际上此举并无必要。
  • 可以根据个人喜好决定是否添加第三方类库Shouldly的引用。
  • 需要添加对主链的AElf.Contracts.TestKit的引用,在写本文档时,AElf最近release的版本为0.8.3。
  • 因为该项目目的是测试刚写的HelloWorld合约,因此需要添加对合约项目的引用。
  • 在初始化测试环境时,需要用零合约部署HelloWorld合约,意味着也需要使用ContractStub标签引用零合约的Stub。

Test Module

XXModule是abp框架对代码进行模块化管理的单位,对于合约测试用例项目而言,只需要依赖于ContractTestModule即可。只不过由于AElf默认关闭了随意部署合约的权限,在准备测试环境时,需要手动把部署合约的权限打开。

using AElf.Contracts.TestKit;
using AElf.Kernel.SmartContract;
using Volo.Abp.Modularity;

namespace AElf.Contracts.HelloWorld
{
    [DependsOn(typeof(ContractTestModule))]
    public class HelloWorldContractTestModule : ContractTestModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            Configure<ContractOptions>(o => o.ContractDeploymentAuthorityRequired = false);
        }
    }
}

Test Base

Test Base作为初始化测试用例中需要使用的变量(如合约Stub和合约地址等)和部署所需测试的合约之用。

在HelloWorldContractTestBase中,我们通过调用零合约的DeploySystemSmartContract方法,部署了HelloWorld合约,并初始化了HelloWorldContractStub和HelloWorldContractAddress这两个在合约测试用例中重要的两个变量。

using System.IO;
using System.Linq;
using Acs0;
using AElf.Contracts.TestKit;
using AElf.Cryptography.ECDSA;
using AElf.Kernel;
using AElf.Types;
using Google.Protobuf;
using Volo.Abp.Threading;

namespace AElf.Contracts.HelloWorld
{
    public class HelloWorldContractTestBase : ContractTestBase<HelloWorldContractTestModule>
    {
        private Address HelloWorldContractAddress { get; set; }

        private ACS0Container.ACS0Stub ZeroContractStub { get; set; }

        internal HelloWorldContractContainer.HelloWorldContractStub HelloWorldContractStub { get; set; }

        protected HelloWorldContractTestBase()
        {
            InitializeContracts();
        }

        private void InitializeContracts()
        {
            ZeroContractStub = GetZeroContractStub(SampleECKeyPairs.KeyPairs.First());

            HelloWorldContractAddress = AsyncHelper.RunSync(() =>
                ZeroContractStub.DeploySystemSmartContract.SendAsync(
                    new SystemContractDeploymentInput
                    {
                        Category = KernelConstants.CodeCoverageRunnerCategory,
                        Code = ByteString.CopyFrom(File.ReadAllBytes(typeof(HelloWorldContract).Assembly.Location)),
                        Name = ProfitSmartContractAddressNameProvider.Name,
                        TransactionMethodCallList = new SystemContractDeploymentInput.Types.SystemTransactionMethodCallList()
                    })).Output;
            HelloWorldContractStub = GetHelloWorldContractStub(SampleECKeyPairs.KeyPairs.First());
        }

        private ACS0Container.ACS0Stub GetZeroContractStub(ECKeyPair keyPair)
        {
            return GetTester<ACS0Container.ACS0Stub>(ContractZeroAddress, keyPair);
        }

        private HelloWorldContractContainer.HelloWorldContractStub GetHelloWorldContractStub(ECKeyPair keyPair)
        {
            return GetTester<HelloWorldContractContainer.HelloWorldContractStub>(HelloWorldContractAddress, keyPair);
        }
    }
}

Test Cases

当Test Base准备充分之后,编写基本测试用例就十分简单了。

想要在测试用例中模拟发交易的过程,比如希望发送一个HelloWorld合约中的Greet交易,直接使用Test Base中初始化好的HelloWorldContractStub,调用await HelloWorldContractStub.Greet.SendAsync(new Empty())就好了。调用结束后,利用一个TransactionResult类型的变量接收返回值,进而对这个交易的执行结果进行检验。

下面是Greet、GreetTo和GetGreetedList三个方法的最基本的测试用例:

        [Fact]
        public async Task GreetTest()
        {
            var txResult = await HelloWorldContractStub.Greet.SendAsync(new Empty());
            txResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined);
            var text = new StringValue();
            text.MergeFrom(txResult.TransactionResult.ReturnValue);
            text.Value.ShouldBe("Hello World!");
        }

        [Theory]
        [InlineData("Ean")]
        [InlineData("Sam")]
        public async Task GreetToTests(string name)
        {
            var txResult = await HelloWorldContractStub.GreetTo.SendAsync(new StringValue {Value = name});
            txResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined);
            var output = new GreetToOutput();
            output.MergeFrom(txResult.TransactionResult.ReturnValue);
            output.Name.ShouldBe(name);
            output.GreetTime.ShouldNotBeNull();
        }

        [Fact]
        public async Task GetGreetedListTest()
        {
            await GreetToTests("Ean");
            await GreetToTests("Sam");

            var greetedList = await HelloWorldContractStub.GetGreetedList.CallAsync(new Empty());
            greetedList.Value.Count.ShouldBe(2);
            greetedList.Value.ShouldContain("Ean");
            greetedList.Value.ShouldContain("Sam");
        }

        [Fact]
        public async Task GreetToWithDuplicatedNameTest()
        {
            const string name = "Ean";
            await GreetToTests(name);
            // Dup the name
            await GreetToTests(name);

            var greetedList = await HelloWorldContractStub.GetGreetedList.CallAsync(new Empty());
            greetedList.Value.Count.ShouldBe(1);
            greetedList.Value.ShouldContain("Ean");
        }

但是要注意,使用SendAsync的前提是编写测试用例时假定对应的交易一定执行成功。如果本身就是想测试交易执行失败的异常情况,需要用另一个方法:SendWithExceptionAsync

        [Theory]
        [InlineData("")]
        [InlineData(" ")]
        public async Task GreetToWithEmptyStringOrWhiteSpace(string name)
        {
            var txResult = await HelloWorldContractStub.GreetTo.SendWithExceptionAsync(new StringValue {Value = name});
            txResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Failed);
            txResult.TransactionResult.Error.ShouldContain("Invalid name.");
        }

4. 部署智能合约

添加引用

让AElf.Boilerplate.Mainchain引用智能合约项目和对应的Proto文件,即在csproj文件中添加:

    <ItemGroup>
        <ProjectReference Include="....contractAElf.Contracts.HelloWorldAElf.Contracts.HelloWorld.csproj">
            <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
            <OutputItemType>Contract</OutputItemType>
            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </ProjectReference>

        <ContractStub Include="....protobufhello_world_contract.proto">
            <Link>ProtobufProtohello_world_contract.proto</Link>
        </ContractStub>
    </ItemGroup>

生成Dto

先简单介绍一下AElf区块链创世区块的生成过程。

同其他区块链系统一样,AElf在链刚刚启动的时候,每个节点都会独立地生成一个具有同样区块哈希的创始区块(如果某个节点生成的创世区块哈希和其他AElf主链节点不一样,那就等于说它启动了一条不同于AElf主链的另一条区块链)。

在创始区块中,会通过固定的代码逻辑和配置项去部署一系列的系统合约,并对这些合约进行初始化。

在使用脚手架测试合约时,可以把自己写的合约部署为创世区块中的系统合约,只需要提供对应的Dto即可。

提供Dto的位置在src/AElf.Boilerplate.Mainchain/GenesisSmartContractDtoProvider.cs的GetGenesisSmartContractDtos方法中,该方法已经包含了脚手架用来部署和初始化其他系统合约Dto,只需要在这里加入HelloWorld合约的Dto即可。

创建一个名为GenesisSmartContractDtoProvider_HelloWorld.cs的C#代码文件,初始化一个GenesisSmartContractDto列表,传入相关信息就好了。

using System.Collections.Generic;
using System.Linq;
using Acs0;
using AElf.OS.Node.Application;
using AElf.Types;

namespace AElf.Blockchains.MainChain
{
    public partial class GenesisSmartContractDtoProvider
    {
        public IEnumerable<GenesisSmartContractDto> GetGenesisSmartContractDtosForHelloWorld()
        {
            var dto = new List<GenesisSmartContractDto>();
            dto.AddGenesisSmartContract(
                _codes.Single(kv=>kv.Key.Contains("HelloWorld")).Value,
                Hash.FromString("AElf.ContractNames.HelloWorld"), new SystemContractDeploymentInput.Types.SystemTransactionMethodCallList());
            return dto;
        }
    }
}

在AElf系统中,每个合约都有一个类型为Hash的唯一标识,称为系统合约名称(System Contract Name),以上代码中的Hash.FromString("AElf.ContractNames.HelloWorld")就是HelloWorld合约的系统合约名称,即唯一标识这一个HelloWorld合约,其用处在下一节中就可以看到。

最后,在GenesisSmartContractDtoProvider的GetGenesisSmartContractDtos方法中加入GetGenesisSmartContractDtosForHelloWorld就好了:

        public IEnumerable<GenesisSmartContractDto> GetGenesisSmartContractDtos(Address zeroContractAddress)
        {
            // The order matters !!!
            return new[]
            {
                GetGenesisSmartContractDtosForVote(zeroContractAddress),
                GetGenesisSmartContractDtosForProfit(zeroContractAddress),
                GetGenesisSmartContractDtosForElection(zeroContractAddress),
                GetGenesisSmartContractDtosForToken(zeroContractAddress),
                GetGenesisSmartContractDtosForConsensus(zeroContractAddress),
                GetGenesisSmartContractDtosForHelloWorld()
            }.SelectMany(x => x);
        }

5. 自动发交易测试智能合约

首先介绍一个AElf主链代码中的一个接口:ISystemTransactionGenerator

该接口生效于打包区块的过程中,通过遍历该接口的所有实现,产生一系列系统交易,这些系统交易会先于从交易池中获取的从网络接收过来的普通交易执行,即在普通交易执行之前率先修改区块链状态。与普通交易一样,系统交易也会被打包进区块中,区别在于,系统交易是通过主链的代码生成的,其Sender为打包区块的节点自身。

因此在脚手架中测试刚刚开发的合约,使用ISystemTransactionGenerator接口”自定义“系统交易不失为一种很好的方法,只需要在实现中制定好发交易的规则即可。

ISystemTransactionGenerator接口只包含一个方法:GenerateTransactions。其签名为:

void GenerateTransactions(Address @from, long preBlockHeight, Hash preBlockHash,
            ref List<Transaction> generatedTransactions)

看一个例子。在AElf区块链中,共识交易是系统交易之一,相关的实现为:

using System.Collections.Generic;
using AElf.Kernel.Miner.Application;
using AElf.Types;
using Volo.Abp.Threading;

namespace AElf.Kernel.Consensus.Application
{
    public class ConsensusTransactionGenerator : ISystemTransactionGenerator
    {
        private readonly IConsensusService _consensusService;

        public ConsensusTransactionGenerator(IConsensusService consensusService)
        {
            _consensusService = consensusService;
        }
        
        public void GenerateTransactions(Address from, long preBlockHeight, Hash previousBlockHash,
            ref List<Transaction> generatedTransactions)
        {
            generatedTransactions.AddRange(
                AsyncHelper.RunSync(() =>
                    _consensusService.GenerateConsensusTransactionsAsync(new ChainContext
                        {BlockHash = previousBlockHash, BlockHeight = preBlockHeight})));
        }
    }
}

本质上,就是调用ConsensusService的GenerateConsensusTransactionsAsync方法,生成交易,再把生成的交易添加进标记了ref关键字的generatedTransactions变量。最后把这个实现的class在组合根中(XXModule的ConfigureServices方法)添加好依赖关系就好了。

    context.Services.AddTransient<ISystemTransactionGenerator, ConsensusTransactionGenerator>();

依此,我们可以实现一个自动发送Greet、GreetTo、GetGreetedList三种交易的ISystemTransactionGenerator。

在src/AElf.Boilerplate.Tester/TestTransactionGenerator文件夹中,创建C#代码文件HelloWorldTransactionGenerator,令它实现ISystemTransactionGenerator。

在正式实现之前,还需要介绍一个AElf主链代码中提供的服务:TransactionResultService,该服务可以通过提供交易ID来查询交易的执行结果。可以直接用构造器注入得到一个ITransactionResultService的实例。

在脚手架中还提供了一个生成交易的服务:TransactionGeneratingService。它的实现并不复杂,就在AElf.Boilerplate.Tester项目的根目录中,它的GenerateTransactionAsync方法通过主链提供的一些其他服务组装出一个交易,然后将该交易返回。

于是,HelloWorldTransactionGenerator可以实现为这样:

using System;
using System.Collections.Generic;
using AElf.Kernel.Blockchain.Application;
using AElf.Kernel.Miner.Application;
using AElf.Types;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Threading;

namespace AElf.Boilerplate.Tester.TestTransactionGenerator
{
    public class HelloWorldTransactionGenerator : ISystemTransactionGenerator
    {
        private readonly ITransactionGeneratingService _transactionGeneratingService;
        private readonly ITransactionResultService _transactionResultService;

        private Hash _lastGetGreetedListTxId = Hash.Empty;

        public ILogger<HelloWorldTransactionGenerator> Logger { get; set; }

        public HelloWorldTransactionGenerator(ITransactionGeneratingService transactionGeneratingService,
            ITransactionResultService transactionResultService)
        {
            _transactionGeneratingService = transactionGeneratingService;
            _transactionResultService = transactionResultService;

            Logger = NullLogger<HelloWorldTransactionGenerator>.Instance;
        }

        public void GenerateTransactions(Address @from, long preBlockHeight, Hash preBlockHash,
            ref List<Transaction> generatedTransactions)
        {
            var empty = new Empty().ToByteString();
            var greetTx = AsyncHelper.RunSync(() => _transactionGeneratingService.GenerateTransactionAsync(
                Hash.FromString("AElf.ContractNames.HelloWorld"), "Greet", empty));

            var randomName = new StringValue {Value = Guid.NewGuid().ToString().Substring(3)}.ToByteString();
            var greetToTx = AsyncHelper.RunSync(() => _transactionGeneratingService.GenerateTransactionAsync(
                Hash.FromString("AElf.ContractNames.HelloWorld"), "GreetTo", randomName));

            var getGreetedListTx = AsyncHelper.RunSync(() => _transactionGeneratingService.GenerateTransactionAsync(
                Hash.FromString("AElf.ContractNames.HelloWorld"), "GetGreetedList", empty));

            var transactions = new List<Transaction>
            {
                greetTx,
                greetToTx,
                getGreetedListTx
            };

            if (_lastGetGreetedListTxId != Hash.Empty)
            {
                var greeted = AsyncHelper.RunSync(() =>
                    _transactionResultService.GetTransactionResultAsync(_lastGetGreetedListTxId)).ReadableReturnValue;
                Logger.LogDebug($"Greeted List: {greeted}");
            }

            _lastGetGreetedListTxId = getGreetedListTx.GetHash();

            generatedTransactions.AddRange(transactions);
        }
    }
}

对于每一个区块,HelloWorldTransactionGenerator都会产生MethodName分别为Greet、GreetTo、GetGreetedList,目标合约系统名称为Hash.FromString("AElf.ContractNames.HelloWorld")的三笔交易。当上一个区块中包含GreetTo交易时(if (_lastGetGreetedListTxId != Hash.Empty)),就使用TransactionResultService查询当前已经”问候过“的人的列表,并打印到日志里。

实现完HelloWorldTransactionGenerator,别忘了在src/AElf.Boilerplate.Tester/TesterModule.cs的ConfigureServices方法中添加一下依赖关系:

    public class TesterModule : AElfModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            context.Services.AddSingleton<ISystemTransactionGenerator, HelloWorldTransactionGenerator>();
        }
    }

这样,重新启动脚手架的单节点,就可以看到控制台上打印的交易执行信息(因为在实现合约的时候,通过Context.LogDebug方法打印了一些日志),还有名单越来越长的Greeted List。

(本文中的代码可以从https://github.com/AElfProject/aelf-boilerplate的tutorial分支获取)

 类似资料: