Giter Club home page Giter Club logo

fabric-chaincode-java's Introduction

Java Chaincode 开发教程

本教程将讲解如何编写基于JavaHyperledger Fabric链代码。有关链码的一般说明,如何编写和操作,请访问Chaincode教程

必要工具

  • JDK8+
  • Gradle 4.8+
  • Hyperledger Fabric 1.3+

环境配置

这里主要是配置 Gradle 的环境变量。

export path=path:/opt/gradle-4.8

简单的Chaincode开发示例

编写自己的链代码需要了解Fabric平台,JavaGradle。应用程序是一个基本的示例链代码,用于在分类帐上创建资产(键值对)。

下载代码

$ git clone https://github.com/hyperledger/fabric-chaincode-java.git

在开发工具eclipse中导入工程代码。

文件夹结构

  • fabric-chaincode-protos 文件夹包含Java shim用于与Fabric对等方通信的protobuf定义文件。

  • fabric-chaincode-shim 文件夹包含定义Java链代码APIjava shim类以及与Fabric对等方通信的方式。

  • fabric-chaincode-docker 文件夹包含构建docker镜像的说明 hyperledger/fabric-javaenv

  • fabric-chaincode-example-gradle 包含一个示例java chaincode gradle项目,其中包含示例链代码和基本gradle构建指令。

创建Gradle项目

可以使用fabric-chaincode-example-gradle作为起始点。或者在fabric-chaincode-example-gradle的同级新建一个gradle项目fabric-chaincode-asset-gradle。确保项目构建创建一个可运行的jar,其中包含名为chaincode.jar的所有依赖项。

plugins {
    id 'com.github.johnrengelman.shadow' version '2.0.3'
    id 'java'
}

group 'org.hyperledger.fabric-chaincode-java'
version '1.3.1-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    compile group: 'org.hyperledger.fabric-chaincode-java', name: 'fabric-chaincode-shim', version: '1.3.+'
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

shadowJar {
    baseName = 'chaincode'
    version = null
    classifier = null

    manifest {
        attributes 'Main-Class': 'com.github.hooj0.chaincode.SimpleAssetChaincode'
    }
}

新建完成后,可以在项目上右键,选择Gradle -> Refresh Gradle Project加载项目依赖的Jar

创建 Maven 项目

可以在fabric-chaincode-example-gradle的同级新建一个maven项目fabric-chaincode-asset-maven。确保项目构建创建一个可运行的jar,其中包含名为chaincode.jar的所有依赖项。在maven的配置文件pom.xml中添加如下配置:

<properties>
    <!-- Generic properties -->
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

    <!-- fabric-chaincode-java dev -->
    <fabric-chaincode-java.version>1.3.1-SNAPSHOT</fabric-chaincode-java.version>
    <!-- prod env
  <fabric-chaincode-java.version>1.3.0</fabric-chaincode-java.version>
  -->

    <!-- Logging -->
    <logback.version>1.0.13</logback.version>
    <slf4j.version>1.7.5</slf4j.version>

    <!-- Test -->
    <junit.version>4.11</junit.version>
</properties>

<dependencies>

    <!-- fabric-chaincode-java -->
    <dependency>
        <groupId>org.hyperledger.fabric-chaincode-java</groupId>
        <artifactId>fabric-chaincode-shim</artifactId>
        <version>${fabric-chaincode-java.version}</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>org.hyperledger.fabric-chaincode-java</groupId>
        <artifactId>fabric-chaincode-protos</artifactId>
        <version>${fabric-chaincode-java.version}</version>
        <scope>compile</scope>
    </dependency>


    <!-- fabric-sdk-java -->

    <!-- Logging with SLF4J & LogBack -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
        <scope>runtime</scope>
    </dependency>

    <!-- Test Artifacts -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>

</dependencies>

<build>
    <sourceDirectory>src/main/java</sourceDirectory>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
            </configuration>
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.1.0</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <finalName>chaincode</finalName>
                        <transformers>
                            <transformer
                                         implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>com.github.hooj0.chaincode.SimpleAssetChaincode</mainClass>
                            </transformer>
                        </transformers>
                        <filters>
                            <filter>
                                <!-- filter out signature files from signed dependencies, else repackaging fails with security ex -->
                                <artifact>*:*</artifact>
                                <excludes>
                                    <exclude>META-INF/*.SF</exclude>
                                    <exclude>META-INF/*.DSA</exclude>
                                    <exclude>META-INF/*.RSA</exclude>
                                </excludes>
                            </filter>
                        </filters>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

最终目录结构如下:

$ ll
total 65

-rw-r--r-- 1 Administrator 197121   687 十二  3 16:30 build.gradle
drwxr-xr-x 1 Administrator 197121     0 十二  3 17:04 fabric-chaincode-asset-gradle/
drwxr-xr-x 1 Administrator 197121     0 十二  3 16:39 fabric-chaincode-asset-maven/
drwxr-xr-x 1 Administrator 197121     0 十一 30 16:55 fabric-chaincode-docker/
drwxr-xr-x 1 Administrator 197121     0 十一 30 16:55 fabric-chaincode-protos/
drwxr-xr-x 1 Administrator 197121     0 十一 30 16:56 fabric-chaincode-shim/
-rwxr-xr-x 1 Administrator 197121  5296 十一  1 17:31 gradlew*
-rw-r--r-- 1 Administrator 197121  2260 十一  1 17:31 gradlew.bat
-rw-r--r-- 1 Administrator 197121  1062 十二  3 15:54 LICENSE
-rw-r--r-- 1 Administrator 197121 22743 十二  3 17:04 README.md
-rw-r--r-- 1 Administrator 197121   146 十二  3 16:30 settings.gradle

创建链码类

使用Java版的Simple Asset Chaincode作为示例。这个链代码是Simple Asset ChaincodeGo to Java翻译,将对此进行解释。

ChaincodeBase类是一个抽象类,它继承了Chaincode形式,它包含用于启动chaincodestart方法。因此,将通过扩展ChaincodeBase而不是实现Chaincode来创建我们的链代码。

首先,从一些基本的开始,创建一个class文件SimpleAssetChaincode。与每个链代码一样,它继承了ChaincodeBase抽象类,特别是实现了initinvoke函数

package com.github.hooj0.chaincode;

import org.hyperledger.fabric.shim.ChaincodeBase;
import org.hyperledger.fabric.shim.ChaincodeStub;

/**
 * simple asset chaincode
 * @author hoojo
 * @createDate 2018年11月30日 下午4:13:27
 * @file SimpleAssetChaincode.java
 * @package com.github.hooj0.chaincode
 * @project fabric-chaincode-asset-gradle
 * @blog http://hoojo.cnblogs.com
 * @email [email protected]
 * @version 1.0
 */
public class SimpleAssetChaincode extends ChaincodeBase {

}

初始化Chaincode

接下来,将实现init函数

    /**
     * Init is called during chaincode instantiation to initialize any
     * data. Note that chaincode upgrade also calls this function to reset
     * or to migrate data.
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @return response
     */
    @Override
    public Response init(ChaincodeStub stub) {       
    }

注意:链码升级也会调用此函数。在编写将升级现有链代码的链代码时,请确保正确修改Init函数。特别是,如果没有“迁移”或者在升级过程中没有任何内容要初始化,请提供一个空的Init方法

接下来,将使用ChaincodeStub -> stub.getStringArgs函数检索Init调用的参数并检查其有效性。在例子中,将使用一个键值对。Chaincode初始化在Response init(ChaincodeStub stub)方法内完成。首先,使用ChaincodeStub.getStringArgs()方法获取参数。

    /**
     * Init is called during chaincode instantiation to initialize any
     * data. Note that chaincode upgrade also calls this function to reset
     * or to migrate data.
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @return response
     */
    @Override
    public Response init(ChaincodeStub stub) {
        // Get the args from the transaction proposal
        List<String> args = stub.getStringArgs();
        if (args.size() != 2) {
            newErrorResponse("Incorrect arguments. Expecting a key and a value");
        }
        return newSuccessResponse();
    }

接下来,既然已经确定调用有效,将把初始状态存储在分类帐中。为此,将调用stub.putStringState作为参数传入的键和值。假设一切顺利,返回一个Response对象,表明初始化成功。

    /**
     * Init is called during chaincode instantiation to initialize any
     * data. Note that chaincode upgrade also calls this function to reset
     * or to migrate data.
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @return response
     */
    @Override
    public Response init(ChaincodeStub stub) {
        try {
            // Get the args from the transaction proposal
            List<String> args = stub.getStringArgs();
            if (args.size() != 2) {
                newErrorResponse("Incorrect arguments. Expecting a key and a value");
            }
            // Set up any variables or assets here by calling stub.putState()
            // We store the key and the value on the ledger
            stub.putStringState(args.get(0), args.get(1));
            return newSuccessResponse();
        } catch (Throwable e) {
            return newErrorResponse("Failed to create asset");
        }
    }

调用Chaincode

首先,添加invoke函数的签名Chaincode调用在Response invoke(ChaincodeStub stub)方法内完成。

    /**
     * Invoke is called per transaction on the chaincode. Each transaction is
     * either a 'get' or a 'set' on the asset created by Init function. The Set
     * method may create a new asset by specifying a new key-value pair.
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @return response
     */
    @Override
    public Response invoke(ChaincodeStub stub) {
        return newSuccessResponse();
    }

与上面的init函数一样,需要ChaincodeStub中提取参数invoke函数的参数将是要调用的链代码应用程序函数的名称。在例子中,应用程序将只有两个函数:setget,它们允许设置资产的值或检索其当前状态。使用ChaincodeStub.getFunction()ChaincodeStub.getParameters()方法提取函数名称和参数验证函数名称调用相应的链代码方法链代码方法接收的值应作为成功响应有效负载返回。如果出现异常或不正确的函数值,则返回错误响应

    public Response invoke(ChaincodeStub stub) {
        try {
            // Extract the function and args from the transaction proposal
            String func = stub.getFunction();
            List<String> params = stub.getParameters();            
        }
    }

接下来,将函数名称验证为setget,并调用这些链代码应用程序函数,通过调用父类的newSuccessResponsenewErrorResponse函数返回适当的响应,这些函数将响应序列化为gRPC protobuf消息

	public Response invoke(ChaincodeStub stub) {
        try {
            // Extract the function and args from the transaction proposal
            String func = stub.getFunction();
            List<String> params = stub.getParameters();
            if (func.equals("set")) {
                // Return result as success payload
                return newSuccessResponse(set(stub, params));
            } else if (func.equals("get")) {
                // Return result as success payload
                return newSuccessResponse(get(stub, params));
            }
            return newErrorResponse("Invalid invoke function name. Expecting one of: [\"set\", \"get\"");
        } catch (Throwable e) {
            return newErrorResponse(e.getMessage());
        }
    }

实现Chaincode应用程序

如上所述,链码应用程序实现了两个可以通过invoke函数调用的函数,现在实现这些功能。请注意,正如上面提到的,为了访问分类帐的状态,使用ChaincodeStub.putStringState(key,value)ChaincodeStub.getStringState(key)实现方法set()get()

	/**
     * get returns the value of the specified asset key
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @param args key
     * @return value
     */
    private String get(ChaincodeStub stub, List<String> args) {
        if (args.size() != 1) {
            throw new RuntimeException("Incorrect arguments. Expecting a key");
        }

        String value = stub.getStringState(args.get(0));
        if (value == null || value.isEmpty()) {
            throw new RuntimeException("Asset not found: " + args.get(0));
        }
        return value;
    }

    /**
     * set stores the asset (both key and value) on the ledger. If the key exists,
     * it will override the value with the new one
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @param args key and value
     * @return value
     */
    private String set(ChaincodeStub stub, List<String> args) {
        if (args.size() != 2) {
            throw new RuntimeException("Incorrect arguments. Expecting a key and a value");
        }
        stub.putStringState(args.get(0), args.get(1));
        return args.get(1);
    }

示例完整代码

最后,需要添加main函数,它将调用shim.Start函数。这是整个链码程序的完整源代码文件。

package com.github.hooj0.chaincode;

import java.util.List;

import org.hyperledger.fabric.shim.ChaincodeBase;
import org.hyperledger.fabric.shim.ChaincodeStub;

/**
 * simple asset chaincode
 * @author hoojo
 * @createDate 2018年11月30日 下午4:13:27
 * @file SimpleAssetChaincode.java
 * @package com.github.hooj0.chaincode
 * @project fabric-chaincode-asset-gradle
 * @blog http://hoojo.cnblogs.com
 * @email [email protected]
 * @version 1.0
 */
public class SimpleAssetChaincode extends ChaincodeBase {

	/**
     * Init is called during chaincode instantiation to initialize any
     * data. Note that chaincode upgrade also calls this function to reset
     * or to migrate data.
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @return response
     */
    @Override
    public Response init(ChaincodeStub stub) {
        try {
            // Get the args from the transaction proposal
            List<String> args = stub.getStringArgs();
            if (args.size() != 2) {
                newErrorResponse("Incorrect arguments. Expecting a key and a value");
            }
            // Set up any variables or assets here by calling stub.putState()
            // We store the key and the value on the ledger
            stub.putStringState(args.get(0), args.get(1));
            return newSuccessResponse();
        } catch (Throwable e) {
            return newErrorResponse("Failed to create asset");
        }
    }

    /**
     * Invoke is called per transaction on the chaincode. Each transaction is
     * either a 'get' or a 'set' on the asset created by Init function. The Set
     * method may create a new asset by specifying a new key-value pair.
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @return response
     */
    @Override
    public Response invoke(ChaincodeStub stub) {
        try {
            // Extract the function and args from the transaction proposal
            String func = stub.getFunction();
            List<String> params = stub.getParameters();
            if (func.equals("set")) {
                // Return result as success payload
                return newSuccessResponse(set(stub, params));
            } else if (func.equals("get")) {
                // Return result as success payload
                return newSuccessResponse(get(stub, params));
            }
            return newErrorResponse("Invalid invoke function name. Expecting one of: [\"set\", \"get\"");
        } catch (Throwable e) {
            return newErrorResponse(e.getMessage());
        }
    }

    /**
     * get returns the value of the specified asset key
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @param args key
     * @return value
     */
    private String get(ChaincodeStub stub, List<String> args) {
        if (args.size() != 1) {
            throw new RuntimeException("Incorrect arguments. Expecting a key");
        }

        String value = stub.getStringState(args.get(0));
        if (value == null || value.isEmpty()) {
            throw new RuntimeException("Asset not found: " + args.get(0));
        }
        return value;
    }

    /**
     * set stores the asset (both key and value) on the ledger. If the key exists,
     * it will override the value with the new one
     *
     * @param stub {@link ChaincodeStub} to operate proposal and ledger
     * @param args key and value
     * @return value
     */
    private String set(ChaincodeStub stub, List<String> args) {
        if (args.size() != 2) {
            throw new RuntimeException("Incorrect arguments. Expecting a key and a value");
        }
        stub.putStringState(args.get(0), args.get(1));
        return args.get(1);
    }

    public static void main(String[] args) {
        new SimpleAssetChaincode().start(args);
    }
}

构建Chaincode

现在编译构建链码。

$ cd fabric-chaincode-asset/fabric-chaincode-asset-gradle

$ gradle clean build shadowJar

# 如果是Maven项目
$ mvn clean install

假设没有错误,会生成jar文件,现在可以继续下一步,测试链代码。

$ ll build/libs/
total 16584
-rw-r--r-- 1 Administrator 197121 16974993 十一 30 17:56 chaincode.jar
-rw-r--r-- 1 Administrator 197121     2371 十一 30 17:56 fabric-chaincode-asset-gradle-1.3.1-SNAPSHOT.jar

使用开发模式测试

通常,链码由对等体启动和维护。然而,在“开发模式”中,链码由用户构建和启动。在链码开发阶段,此模式非常有用,可用于快速代码/构建/运行/调试周期周转。

通过为示例开发网络,利用预先生成的定序者和通道工件来启动“开发模式(dev mode)”。这样,用户可以立即进入编译链码和操作调用的过程。

如果还不会开发模式,可以参考开发模式使用方式文档

准备Gradle示例和链码

如果还没有这样做,请安装样本,二进制文件和Docker镜像

进入到示例目录下的 chaincode位置。

$ cd fabric-samples/chaincode

创建链码文件夹

$ mkdir -p asset/java

将项目的**源代码和gradle**的配置文件,拷贝到上面的文件夹中

$ cp xxx/xxx/src/*.java asset/java/src

$ cp xxx/xxx/*.gradle asset/java/

准备Maven示例和链码

如果还没有这样做,请安装样本,二进制文件和Docker镜像

进入到示例目录下的 chaincode位置。

$ cd fabric-samples/chaincode

创建链码文件夹

$ mkdir -p asset/java

将项目的**源代码和maven**的配置文件,拷贝到上面的文件夹中

$ cp -r xxx/xxx/src asset/java/

$ cp xxx/xxx/*.pom asset/java/

测试和运行链码

进入到fabric-samples项目的chaincode-docker-devmode目录:

$ cd chaincode-docker-devmode

现在打开三个终端并导航到每个终端中的chaincode-docker-devmode目录。

终端1 - 启动网络

$ docker-compose -f docker-compose-simple.yaml up

以上内容使用SingleSampleMSPSolo定序者配置文件启动网络,并以“开发模式”启动对等体。它还启动了两个额外的容器,一个用于链码环境,另一个用于与链代码交互。创建和加入通道的命令嵌入在CLI容器中,因此可以立即跳转到链代码调用。

终端2 - 使用链码

即使处于--peer-chaincodedev模式,仍然必须安装链代码,以便生命周期系统链码可以正常进行检查。在--pere-chaincodedev模式下,将来可能会删除此要求。

$ docker exec -it cli bash

cli 容器中运行:

$ CORE_LOGGING_PEER=info CORE_CHAINCODE_LOGGING_SHIM=info CORE_CHAINCODE_LOGGING_LEVEL=info peer chaincode install -n asset -v v0 -l java -p /opt/gopath/src/chaincodedev/chaincode/asset/java/

$ CORE_LOGGING_PEER=info CORE_CHAINCODE_LOGGING_SHIM=info CORE_CHAINCODE_LOGGING_LEVEL=info peer chaincode instantiate -n asset -v v0 -c '{"Args":["a", "5"]}' -C myc -l java

现在发出一个invoke调用,将a的值更改为“20”。

$ peer chaincode invoke -n asset -c '{"Args":["set", "a", "20"]}' -C myc

最后,查询a。应该看到20的值。

$ peer chaincode query -n asset -c '{"Args":["get","a"]}' -C myc

测试新的链码

默认情况下,只挂载sacc。但是,可以通过将不同的链码添加到chaincode子目录重新启动网络来轻松地测试它们。此时,可以在chaincode容器中访问它们。

链码日志

链码容器的日志查看,需要在链码安装并实例化后才能通过 docker 容器的方式进行查看容器日志。一般链码容器是以dev-peer-xxx-xxx格式的名称的容器。

查看链码日志

查看日志具体操作如下:

# 找到链码容器
$ docker ps

CONTAINER ID        IMAGE                                                                                COMMAND                  CREATED             STATUS              PORTS                                            NAMES
32ef0a73a344        dev-peer-asset-v0-ab37288c7dfae60b51cf93c7fade76b6d55b2225c1d00a81a627037628408dc7   "/root/chaincode-jav…"   15 minutes ago      Up 15 minutes                                                        dev-peer-asset-v0
299103c48df3        hyperledger/fabric-ccenv:latest                                                      "/bin/bash -c 'sleep…"   17 minutes ago      Up 17 minutes                                                        chaincode
0e81e8e846d2        hyperledger/fabric-tools:latest                                                      "/bin/bash -c ./scri…"   17 minutes ago      Up 17 minutes                                                        cli
2ef83f03cce6        hyperledger/fabric-peer:latest                                                       "peer node start --p…"   17 minutes ago      Up 17 minutes       0.0.0.0:7051->7051/tcp, 0.0.0.0:7053->7053/tcp   peer
296f09b716af        hyperledger/fabric-orderer:latest                                                    "orderer"                17 minutes ago      Up 17 minutes       0.0.0.0:7050->7050/tcp                           orderer

# 通过名称查看容器日志
$ docker logs dev-peer-asset-v0

# 通过id查看容器日志
$ docker logs -f 32ef0a73a344

设置链码日志的级别

设置环境变量覆盖掉core.yaml中的配置,在执行命令的时候进行环境变量设置如下:

CORE_LOGGING_PEER=info CORE_CHAINCODE_LOGGING_SHIM=info CORE_CHAINCODE_LOGGING_LEVEL=info peer chaincode install -n asset -v v0 -l java -p xxxx

Java 链码日志

链码日志需要使用官方指定的日志记录器,目前官方支持的链码容器日志输出是 apache.commons.logging。如果采用其他的日志记录器,可能存在安装实例化链码的时候找不到相关JAR文件;而且日志的输出级别不受环境变量控制,尝试使用logback输出日志,但无法通过配置文件或环境变量设置日志格式和级别。

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

private static final Log log = LogFactory.getLog(Xxx.class);

fabric-chaincode-java's People

Contributors

hooj0 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

fabric-chaincode-java's Issues

链码提交问题

你好,你文中说打开3个终端操作,但是你列出了两个终端的操作,是遗漏了吗?还是就两个终端操作就可以了?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.