A Crowdfunding Example Dapp

In this tutorial, we will use the dddappp low-code tool to develop a crowdfunding decentralized application and publish it to the Movement M2 devnet network.

Requirements

Below are the project requirements.

Overview: We want to create a Dapp for crowdfunding projects. Users can list their projects with a specific funding target and deadline to achieve it.

For example, Dan wants to raise 5,000 Mango tokens for his Mango Orchard by November 10th. The project will be listed and other users can fund it by paying Mango tokens. If the target of 5,000 Mango tokens is not reached by November 10th, the funding amount will be returned to the supporters. If the target is met, the amount will be sent to Dan.

So far, we've only needed the smart contract, without a frontend.

  • The smart contract should enable users to create project listings, receive funds from supporters, and distribute funds to the project creator if the funding goals are met.
  • Funds will be locked in the smart contract until a specified funding goal, set by the project creator, is reached. If not met, funds will be returned to the supporters.
  • Each project must have a 15-day time limit from the date it starts, which will be managed by the smart contract.

Below, let's introduce the development process of this sample application using dddappp low-code tool.

Prerequisites

Currently, the dddappp low-code tool is published as a Docker image for developers to experience.

So before getting started, you need to:

  • Install Sui.
  • Install Docker.
  • Configure the Sui CLI tool. This way, if you are a Sui developer, you can deploy to Movement networks with some minor changes to your workflow.
  • (Optional) Install MySQL database server if you want to test the off-chain service. The off-chain service generated by the tool currently use MySQL by default.
  • (Optional) Install JDK and Maven if you want to test the off-chain service. The off-chain services generated by the tool currently use Java.

If you have already installed Docker, you can use Docker to run a MySQL database service. For example:

sudo docker run -p 3306:3306 --name mysql \
-v ~/docker/mysql/conf:/etc/mysql \
-v ~/docker/mysql/logs:/var/log/mysql \
-v ~/docker/mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7

Confirm your Sui CLI client environment

View the currently active environment of the Sui CLI client:

sui client envs

The output should be similar to the following:

╭───────────┬────────────────────────────────────────────┬────────╮
│ alias     │ url                                        │ active │
├───────────┼────────────────────────────────────────────┼────────┤
│ devnet    │ https://fullnode.devnet.sui.io:443         │        │
│ testnet   │ https://fullnode.testnet.sui.io:443        │        │
│ mainnet   │ https://fullnode.mainnet.sui.io:443        │        │
│ m2-devnet │ https://sui.devnet.m2.movementlabs.xyz:443 │ *      │
╰───────────┴────────────────────────────────────────────┴────────╯

View currently active Sui CLI wallet address:

sui client active-address

The output should be similar to the following:

0xfc50aa2363f3b3c5d80631cae512ec51a8ba94080500a981f4ae1a2ce4d201c2

See what coins are currently in your Sui CLI wallet:

sui client gas

If you don't have any coins, you can get some from the Movement M2 devnet faucet: https://faucet.movementlabs.xyz/?network=devnet

Prepare yourself two Coin objects

First, see what coins are currently in your CLI wallet:

sui client gas

If you only have a coin, then the output is similar to the following:

╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x1ed9b740efd757ed9135b4e1d53ea8974ee4fa7dda566ae9b9cce32c4f56dba4 │ 1481526838         │ 1.48             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

You need to split this coin into two. This is because you need to use one coin to donate to the project, and the other coin to pay for the gas.

Use the following command to transfer 0.1 coin to this address:

sui client pay-sui --amounts 100000000 \
--recipients 0xfc50aa2363f3b3c5d80631cae512ec51a8ba94080500a981f4ae1a2ce4d201c2 \
--input-coins 0x1ed9b740efd757ed9135b4e1d53ea8974ee4fa7dda566ae9b9cce32c4f56dba4 \
--gas-budget 100000000

Which will split the original one Coin into two.

Then see what SUI coins are currently in your Sui CLI wallet:

sui client gas

The output is similar to the following:

╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x1ed9b740efd757ed9135b4e1d53ea8974ee4fa7dda566ae9b9cce32c4f56dba4 │ 1379528958         │ 1.37             │
│ 0xed4fc28231a73c83b8568d2d53bb2c9a2cdc5dedfe9ca81056f1bbb760bceae2 │ 100000000          │ 0.10             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

Programming

Write DDDML Model File

In the dddml directory in the root of the repository, create a DDDML file like this.

Tip

About DDDML, here is an introductory article: "Introducing DDDML: The Key to Low-Code Development for Decentralized Applications".

Run dddappp Project Creation Tool

Update dddappp Docker Image

Since the dddappp v0.0.1 image is updated frequently, you may be required to manually delete the image and pull it again before docker run.

# If you have already run it, you may need to Clean Up Exited Docker Containers first
docker rm $(docker ps -aq --filter "ancestor=wubuku/dddappp:0.0.1")
# remove the image
docker image rm wubuku/dddappp:0.0.1
# pull the image
docker pull wubuku/dddappp:0.0.1

In repository root directory, run:

docker run \
-v .:/myapp \
wubuku/dddappp:0.0.1 \
--dddmlDirectoryPath /myapp/dddml \
--boundedContextName Test.SuiCrowdfundingExample \
--suiMoveProjectDirectoryPath /myapp/sui-contracts \
--boundedContextSuiPackageName sui_crowdfunding_example \
--boundedContextJavaPackageName org.test.suicrowdfundingexample \
--javaProjectsDirectoryPath /myapp/sui-java-service \
--javaProjectNamePrefix suicrowdfundingexample \
--pomGroupId test.suicrowdfundingexample

The command parameters above are straightforward:

  • This line -v .:/myapp \ indicates mounting the local current directory into the /myapp directory inside the container.
  • dddmlDirectoryPath is the directory where the DDDML model files are located. It should be a directory path that can be read in the container.
  • Understand the value of the boundedContextName parameter as the name of the application you want to develop. When the name has multiple parts, separate them with dots and use the PascalCase naming convention for each part. Bounded-context is a term in Domain-driven design (DDD) that refers to a specific problem domain scope that contains specific business boundaries, constraints, and language. If you cannot understand this concept for the time being, it is not a big deal.
  • boundedContextJavaPackageName is the Java package name of the off-chain service. According to Java naming conventions, it should be all lowercase and the parts should be separated by dots.
  • boundedContextSuiPackageName is the package name of the on-chain Sui contracts. According to the Sui development convention, it should be named in snake_case style with all lowercase letters.
  • javaProjectsDirectoryPath is the directory path where the off-chain service code is placed. The off-chain service consists of multiple modules (projects). It should be a readable and writable directory path in the container.
  • javaProjectNamePrefix is the name prefix of each module of the off-chain service. It is recommended to use an all-lowercase name.
  • pomGroupId is the GroupId of the off-chain service. We use Maven as the project management tool for off-chain service. It should be all lowercase and the parts should be separated by dots.
  • suiMoveProjectDirectoryPath is the directory path where the on-chain Sui contract code is placed. It should be a readable and writable directory path in the container.

After the above command is successfully executed, two directories sui-java-service and sui-contracts should be added to the local current directory.

Implementing Business Logic of "Methods"

The tool has generated some files with the suffix _logic.move in the directory sui-contracts/sources.

Generally, these files contain the scaffolding code of functions that implement business logic, namely the signature part of the functions. You just need to fill in the implementation part of the functions.

You need to fill in the business logic in the following files:

Don't worry, as you can see, the function body part of these files that you need to fill in is very short.

  • You need to check the client's input in the verify function, capture the information from input and context to generate an "event object".
  • Then, in the mutate function, you need to modify the state of the objects, primarily by applying the information from the event object.

In the model file, we define three methods, Donate, Withdraw, and Refund, which use Balance, a resource type, as type of parameters or return values. This makes these methods very combinable - as a developer of Move, a "resource-oriented programming" language, you will already know this.

However, it's not easy to call them directly from Sui CLI. So, we added this file project_service.move. In this file, three entry functions are provided to facilitate clients to use the corresponding features directly.

That's the whole programming routine, isn't it simple?

Test Application

Below we will publish and test the application in test environment using the Sui CLI client.

Publish

In the test environment, before publishing the contract, you can change the crowdfunding time limit to 5 minutes. This way you can quickly test deadline-related features without waiting too long.

You can change the value of the FIFTEEN_DAYS_IN_MS const as prompted by the comments in the file sui-contracts/sources/project_start_logic.move.

Execute the following command in the directory sui-contracts to publish the contract on the chain:

sui client publish --gas-budget 200000000 --skip-dependency-verification

The output is similar to the following:

Transaction Digest: HvTguer3s3ha1vYEqZAa5azpRWeCcadDuBptW4KiRtP1


│ Created Objects:                                                                                                                                                                                                                                                                                                                                                                                      │
│  ┌──                                                                                                                                                                                                                                                                                                                                                                                                  │
│  │ ObjectID: 0x0cb4d8927585dcc2012c51284fcd7b7a616968c717c2c879fe56e7d08b9a9d47                                                                                                                                                                                                                                                                                                                       │
│  │ Sender: 0xfc50aa2363f3b3c5d80631cae512ec51a8ba94080500a981f4ae1a2ce4d201c2                                                                                                                                                                                                                                                                                                                         │
│  │ Owner: Shared                                                                                                                                                                                                                                                                                                                                                                                      │
│  │ ObjectType: 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9::platform::Platform                                                                                                                                                                                                                                                                                                 │
│  │ Version: 2352428                                                                                                                                                                                                                                                                                                                                                                                   │
│  │ Digest: 8dS1SQDtovS1mnLTETRRUpc1XvqF3runRNKMkjvP58JG                                                                                                                                                                                                                                                                                                                                               │


│ Published Objects:                                                                                                                                                                                                                                                                                                                                                                                    │
│  ┌──                                                                                                                                                                                                                                                                                                                                                                                                  │
│  │ PackageID: 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9                                                                                                                                                                                                                                                                                                                      │

Record the package Id of your published contract, as in the example above, the packageId is 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9; as well as the Id of the object that represents the crowdfunding platform, as in the example above, the objectId is 0x0cb4d8927585dcc2012c51284fcd7b7a616968c717c2c879fe56e7d08b9a9d47. In the following example commands we will use these IDs directly, in your test environment you will need to replace them with your actual values.

Record the publishing transaction digest, if you want to test off-chain service later.

Create & Start & Donate & Withdraw

Create project

Assume that you want to create a project to raise 0.1 coin for a Mango Orchard.

Create a crowdfunding project:

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_aggregate --function create \
--type-args '0x2::sui::SUI' \
--args \"0x0cb4d8927585dcc2012c51284fcd7b7a616968c717c2c879fe56e7d08b9a9d47\" \
'"Mango Orchard Crowdfunding"' '"This is a test!"' \
\"100000000\" '""' \
--gas-budget 100000000

The output is similar to the following:


│ Created Objects:                                                                                                    │
│  ┌──                                                                                                                │
│  │ ObjectID: 0x86f48589f321d357f2834656e8c2f32474c44ad00a4e72547480b9e288e1d72e                                     │
│  │ Sender: 0xfc50aa2363f3b3c5d80631cae512ec51a8ba94080500a981f4ae1a2ce4d201c2                                       │
│  │ Owner: Shared                                                                                                    │
│  │ ObjectType: 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9::project::Project<0x2::sui::SUI>  │

Make a note of the object Id of the project. In the above example, the object Id of the created project is 0x86f48589f321d357f2834656e8c2f32474c44ad00a4e72547480b9e288e1d72e. In the following example commands we will use this Id directly, in your test environment you will need to replace it with your actual value.

You can view the state of an object like this:

sui client object 0x86f48589f321d357f2834656e8c2f32474c44ad00a4e72547480b9e288e1d72e

Start project

Note that the deadline of the project is calculated from the time it starts.

Start the project:

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_aggregate --function start \
--type-args '0x2::sui::SUI' \
--args \"0x86f48589f321d357f2834656e8c2f32474c44ad00a4e72547480b9e288e1d72e\" '0x6' \
--gas-budget 100000000

Next, let's prepare to donate to this project.

Donate project

In less than 5 minutes after having started the project, you can donate it:

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_service --function donate \
--type-args '0x2::sui::SUI' \
--args \"0x86f48589f321d357f2834656e8c2f32474c44ad00a4e72547480b9e288e1d72e\" \
'0x1ed9b740efd757ed9135b4e1d53ea8974ee4fa7dda566ae9b9cce32c4f56dba4' \
'0x6' '100000000' \
--gas-budget 90000000

You donated 0.1 coin to the project using the above command. This makes the project's funding goal already reached.

Withdraw

Waiting for 5 minutes, as the owner of the project, you can take out the funds.

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_service --function withdraw \
--type-args '0x2::sui::SUI' \
--args \"0x86f48589f321d357f2834656e8c2f32474c44ad00a4e72547480b9e288e1d72e\" '0x6' \
--gas-budget 100000000

Cleanup: merge coins

To facilitate the next test, we can Merge the excess coin objects in the wallet.

See what coin objects are currently in your Sui CLI wallet:

sui client gas

The output is similar to the following:

╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ {COIN_OBJECT_1}                                                    │ xxxxxxxxxx         │ x.xx             │
│ {COIN_OBJECT_2}                                                    │ yyyyyyyyyy         │ y.yy             │
│ {COIN_OBJECT_3}                                                    │ zzzzzzzzzz         │ z.zz             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

You can see that there is an additional coin object in the wallet and the balance is the amount raised by the project.

You can merge two of the coins like below, too many coins may not be convenient for more testing:

sui client merge-coin \
--coin-to-merge {COIN_OBJECT_2} \
--primary-coin {COIN_OBJECT_1} \
--gas-budget 20000000

Create & Start & Donate & Refund

Create Project

Assume that you want to create a project to raise 0.5 coin for a Mango Orchard.

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_aggregate --function create \
--type-args '0x2::sui::SUI' \
--args \"0x0cb4d8927585dcc2012c51284fcd7b7a616968c717c2c879fe56e7d08b9a9d47\" \
'"Mango Orchard Crowdfunding2"' '"This is a test project"' \
\"500000000\" '""' \
--gas-budget 100000000

Assume that the created project object ID is 0x74569d20afb16698a02deb46d196c628189d18666a1786bf3e85b4f41a111f91.

Start project

Start the project:

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_aggregate --function start \
--type-args '0x2::sui::SUI' \
--args \"0x74569d20afb16698a02deb46d196c628189d18666a1786bf3e85b4f41a111f91\" '0x6' \
--gas-budget 100000000

Donate project

In less than 5 minute after having started the project, you can donate it. Assume that you want to donate 0.3 coin to the project:

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_service --function donate \
--type-args '0x2::sui::SUI' \
--args \"0x74569d20afb16698a02deb46d196c628189d18666a1786bf3e85b4f41a111f91\" \
'0x1ed9b740efd757ed9135b4e1d53ea8974ee4fa7dda566ae9b9cce32c4f56dba4' '0x6' \
'300000000' \
--gas-budget 100000000

The withdrawals should fail

If within 5 minute after the project has started, the owner of the project wants to withdraw the funds:

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_service --function withdraw \
--type-args '0x2::sui::SUI' \
--args \"0x74569d20afb16698a02deb46d196c628189d18666a1786bf3e85b4f41a111f91\" '0x6' \
--gas-budget 100000000

Well, it can't be successful. The output is similar to the following:

Error executing transaction: Failure {
    error: "MoveAbort(MoveLocation { module: ModuleId { address: f033ec079b048af82289a10c5e54caa177f1fda8804504014999f548f512caa3, name: Identifier(\"project_withdraw_logic\") }, function: 0, instruction: 26, function_name: Some(\"verify\") }, 184) in command 0",
}

It is assumed that no one else will donate to the project. 5 minute later, the project owner tries to withdraw the funds again. The output is similar to the following:

Error executing transaction: Failure {
    error: "MoveAbort(MoveLocation { module: ModuleId { address: f033ec079b048af82289a10c5e54caa177f1fda8804504014999f548f512caa3, name: Identifier(\"project_withdraw_logic\") }, function: 0, instruction: 40, function_name: Some(\"verify\") }, 185) in command 0",
}

You can see that the error code in the output has changed (184 -> 185). You can find what these codes represent in the source file project_withdraw_logic.move.

Refund

Because the deadline has been reached and the funding goal was not met, the donor can now refund:

sui client call --package 0xf59bf95203107c34cbb80b5a234fec78dfb645a4f81fd70f1f57a4f928d7d3a9 --module project_service --function refund \
--type-args '0x2::sui::SUI' \
--args \"0x74569d20afb16698a02deb46d196c628189d18666a1786bf3e85b4f41a111f91\" '0x6' \
--gas-budget 100000000

View state of the platform

You can view the state of the crowdfunding Platform object like this:

sui client object 0x0cb4d8927585dcc2012c51284fcd7b7a616968c717c2c879fe56e7d08b9a9d47

After the above test, you should see two project object IDs in the output message.

Test Off-Chain Service (indexer)

Configuring off-chain service

Open the application-test.yml file located in the directory sui-java-service/suicrowdfundingexample-service-rest/src/main/resources and set the publishing transaction digest.

After setting, it should look like this:

# ...
server:
  port: 8024

sui:
  contract:
    jsonrpc:
      url: "https://sui.devnet.m2.movementlabs.xyz/"
    package-publish-transaction: "HvTguer3s3ha1vYEqZAa5azpRWeCcadDuBptW4KiRtP1"

This is the only place where off-chain service need to be configured, and it's that simple.

Creating and initializing a database for off-chain service

Use a MySQL client to connect to the local MySQL server and execute the following script to create an empty database (assuming the name is suicrowdfundingexample_m2):

CREATE SCHEMA `suicrowdfundingexample_m2` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

Go to the sui-java-service directory and package the Java project:

mvn package

Then, run a command-line tool to initialize the database:

java -jar ./suicrowdfundingexample-service-cli/target/suicrowdfundingexample-service-cli-0.0.1-SNAPSHOT.jar ddl -d "./scripts" -c "jdbc:mysql://127.0.0.1:3306/suicrowdfundingexample_m2?enabledTLSProtocols=TLSv1.2&characterEncoding=utf8&serverTimezone=GMT%2b0&useLegacyDatetimeCode=false" -u root -p 123456

Starting off-chain service

In the sui-java-service directory, run the following command to start the off-chain service:

mvn -pl suicrowdfundingexample-service-rest -am spring-boot:run

You can now use your browser to open the Swagger (Open API) documentation for the off-chain service to see what interfaces are already available out-of-the-box: http://localhost:8024/api/swagger-ui/index.html