Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom indexer template #226

Merged
merged 22 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to the create-aptos-dapp tool will be captured in this file.

# Unreleased

- Add new custom indexer template
- Fix NFT and Token minting dapps navigation UI issues

# 0.0.30 (2024-09-26)
Expand Down
4 changes: 3 additions & 1 deletion src/generateDapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export async function generateDapp(selection: Selections) {
f !== "package-lock.json" &&
f !== ".aptos" &&
f !== "build" &&
f !== ".env"
f !== ".env" &&
f !== ".next" &&
f !== "target" // this is for the rust template
)
.map((file) => write(file))
);
Expand Down
11 changes: 11 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ export const ClickerGameTgTemplateInfo = {
"A clicker game Telegram Mini App template to start an Aptos dapp with",
};

export const CustomIndexerTemplateInfo = {
title: "Custom indexer Template",
value: {
path: "custom-indexer-template",
name: "Custom indexer template",
doc: "https://aptos.dev/en/build/create-aptos-dapp/templates/custom-indexer",
},
description:
"A full stack dapp template with a custom indexer to start an Aptos dapp with",
};

export const NftMintingDappTemplateInfo = {
title: "NFT minting dapp",
value: {
Expand Down
2 changes: 2 additions & 0 deletions src/utils/generateTemplateEnvFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export const generateTemplateEnvFile = async (
return await generateEnvFile();
case "nextjs-boilerplate-template":
return await generateEnvFile();
case "custom-indexer-template":
return await generateEnvFile(`DATABASE_URL=""`);
case "clicker-game-tg-mini-app-template":
return await generateEnvFile();
default:
Expand Down
11 changes: 11 additions & 0 deletions src/workflow/workflowOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TemplateSigningOption,
TokenMintingDappTemplateInfo,
TokenStakingDappTemplateInfo,
CustomIndexerTemplateInfo,
} from "../utils/constants.js";
import { validateProjectName } from "../utils/index.js";
import {
Expand Down Expand Up @@ -53,6 +54,7 @@ export const workflowOptions = {
NftMintingDappTemplateInfo,
TokenMintingDappTemplateInfo,
TokenStakingDappTemplateInfo,
CustomIndexerTemplateInfo,
],
initial: 0,
},
Expand Down Expand Up @@ -87,6 +89,15 @@ export const workflowOptions = {
value: TemplateFramework.NEXTJS,
},
];
} else if (
values.template.path === CustomIndexerTemplateInfo.value.path
) {
return [
{
title: "Server-side (Next.js app)",
value: TemplateFramework.NEXTJS,
},
];
}
return [
{ title: "Client-side (Vite app)", value: TemplateFramework.VITE },
Expand Down
3 changes: 3 additions & 0 deletions templates/custom-indexer-template/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
44 changes: 44 additions & 0 deletions templates/custom-indexer-template/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Create Aptos Dapp Custom Indexer Template

The custom indexer template provides a starter dapp with all components to run a full-stack app with indexer support.

## Read the Custom Indexer template docs

To get started with the Custom Indexer template and learn more about the template functionality and usage, head over to the [Custom Indexer template docs](https://aptos.dev/en/build/create-aptos-dapp/templates/custom-indexer)

## The Custom Indexer template provides:

- **Folder structure** - A pre-made dapp folder structure with `src` for frontend, `contract` for Move contract and `indexer` for custom indexer.
- **Dapp infrastructure** - All required dependencies a dapp needs to start building on the Aptos network.
- **Wallet Info implementation** - Pre-made `WalletInfo` components to demonstrate how one can use to read a connected Wallet info.
- **Message board functionality implementation** - Pre-made `MessageBoard` component to create, update and read messages from the Move smart contract.
- **Analytics dashboard** - Pre-made `Analytics` component to show the number of messages created and updated.
- **Point program** - Minimal example to show you how to define a point program based on events (e.g. create message, update message) and show that on the analytics dashboard, with sorting support.

## What tools the template uses?

- React framework
- shadcn/ui + tailwind for styling
- Aptos TS SDK
- Aptos Wallet Adapter
- Node based Move commands
- Rust based Aptos Indexer SDK

## What Move commands are available?

The tool utilizes [aptos-cli npm package](https://github.com/aptos-labs/aptos-cli) that lets us run Aptos CLI in a Node environment.

Some commands are built-in the template and can be ran as a npm script, for example:

- `npm run move:publish` - a command to publish the Move contract
- `npm run move:test` - a command to run Move unit tests
- `npm run move:compile` - a command to compile the Move contract
- `npm run move:upgrade` - a command to upgrade the Move contract
- `npm run dev` - a command to run the frontend locally
- `npm run deploy` - a command to deploy the dapp to Vercel

For all other available CLI commands, can run `npx aptos` and see a list of all available commands.

## Running the Custom Indexer template

Please refer to the [Custom Indexer template docs](https://aptos.dev/en/build/create-aptos-dapp/templates/custom-indexer) for more information on how to run the Custom Indexer template.
34 changes: 34 additions & 0 deletions templates/custom-indexer-template/_gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Aptos related files
.aptos
.env
contract/build

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
build
dist-ssr
*.local
package-lock.json
pnpm-lock.yaml

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

.next
20 changes: 20 additions & 0 deletions templates/custom-indexer-template/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
12 changes: 12 additions & 0 deletions templates/custom-indexer-template/contract/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "message-board"
version = "1.0.0"
authors = []

[addresses]
message_board_addr = "_"

[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "mainnet", subdir = "aptos-move/framework/aptos-framework" }

[dev-dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
script {
use std::string;

use message_board_addr::custom_indexer_ex_message_board;

// This Move script runs atomically, i.e. it creates 2 messages in the same transaction.
// Move script is how we batch multiple function calls in 1 tx
// Similar to Solana allows multiple instructions in 1 tx
fun create_2_messages(sender: &signer) {
custom_indexer_ex_message_board::create_message(sender, string::utf8(b"hello hhohohoho"));
custom_indexer_ex_message_board::create_message(sender, string::utf8(b"hello yeyeeee"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
script {
use std::string;

use aptos_framework::object;

use message_board_addr::custom_indexer_ex_message_board;

// This Move script runs atomically
fun update_message(sender: &signer) {
let message_obj_addr_1 =
@0x4d78c35d85ab22061e25b5ec540a7fabc89e58799dc58462305c9cc318ef6dad;
custom_indexer_ex_message_board::update_message(
sender,
object::address_to_object(message_obj_addr_1),
string::utf8(b"updated message 3")
);

let message_obj_addr_2 =
@0x663ecbbaaa75dc8f96d257684251e0678d8153f9f4872c61cf67025bdb51c7f9;
custom_indexer_ex_message_board::update_message(
sender,
object::address_to_object(message_obj_addr_2),
string::utf8(b"updated message 4")
);
custom_indexer_ex_message_board::update_message(
sender,
object::address_to_object(message_obj_addr_2),
string::utf8(b"updated message 5")
);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
module message_board_addr::custom_indexer_ex_message_board {
use std::signer;
use std::string::String;

use aptos_framework::event;
use aptos_framework::object::{Self, Object};
use aptos_framework::timestamp;

/// Only the message creator can update the message content
const ERR_ONLY_MESSAGE_CREATOR_CAN_UPDATE: u64 = 1;

struct Message has copy, drop, key, store {
creator: address,
content: String,
creation_timestamp: u64,
last_update_timestamp: u64,
}

#[event]
struct CreateMessageEvent has drop, store {
message_obj_addr: address,
message: Message,
}

#[event]
struct UpdateMessageEvent has drop, store {
message_obj_addr: address,
message: Message,
}

// This function is only called once when the module is published for the first time.
// init_module is optional, you can also have an entry function as the initializer.
fun init_module(_sender: &signer) {}

// ======================== Write functions ========================

/// Create a new message
public entry fun create_message(sender: &signer, content: String) {
let message_obj_constructor_ref = &object::create_object(@message_board_addr);
let message_obj_signer = &object::generate_signer(message_obj_constructor_ref);
let time_now = timestamp::now_seconds();
let message = Message {
creator: signer::address_of(sender),
content,
creation_timestamp: time_now,
last_update_timestamp:time_now,
};
move_to(message_obj_signer, message);

event::emit(CreateMessageEvent {
message_obj_addr: object::address_from_constructor_ref(message_obj_constructor_ref),
message,
});
}

/// Update the content of an existing message, only message creator can call
public entry fun update_message(sender: &signer, message_obj: Object<Message>, new_content: String) acquires Message {
let message = borrow_global_mut<Message>(object::object_address(&message_obj));
assert!(message.creator == signer::address_of(sender), ERR_ONLY_MESSAGE_CREATOR_CAN_UPDATE);
message.content = new_content;
message.last_update_timestamp = timestamp::now_seconds();

event::emit(UpdateMessageEvent {
message_obj_addr: object::object_address(&message_obj),
message: *message,
});
}

// ======================== Read Functions ========================

#[view]
/// Get the content of a message
public fun get_message_content(message_obj: Object<Message>): (String, address, u64, u64) acquires Message {
let message = borrow_global<Message>(object::object_address(&message_obj));
(
message.content,
message.creator,
message.creation_timestamp,
message.last_update_timestamp,
)
}

// ================================= Uint Tests Helper ================================== //

#[test_only]
public fun init_module_for_test(aptos_framework: &signer, sender: &signer) {
timestamp::set_time_has_started_for_testing(aptos_framework);
init_module(sender);
}

#[test_only]
public fun get_message_obj_from_create_message_event(event: &CreateMessageEvent): Object<Message> {
object::address_to_object(event.message_obj_addr)
}

#[test_only]
public fun get_message_obj_from_update_message_event(event: &UpdateMessageEvent): Object<Message> {
object::address_to_object(event.message_obj_addr)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#[test_only]
module message_board_addr::test_end_to_end {
use std::signer;
use std::string;
use std::vector;

use aptos_framework::event;

use message_board_addr::message_board;

#[test(aptos_framework = @aptos_framework, deployer = @message_board_addr, sender = @0x100)]
fun test_end_to_end(aptos_framework: &signer, deployer: &signer, sender: &signer) {
let sender_addr = signer::address_of(sender);

message_board::init_module_for_test(aptos_framework, deployer);

message_board::create_message(sender, string::utf8(b"hello world"));
let events = event::emitted_events();
let message_obj = message_board::get_message_obj_from_create_message_event(vector::borrow(&events, 0));
let (content, creator, _, _) = message_board::get_message_content(message_obj);
assert!(content == string::utf8(b"hello world"), 1);
assert!(creator == sender_addr, 1);

message_board::update_message(sender, message_obj, string::utf8(b"hello move"));
let events = event::emitted_events();
// Since we force event type to be UpdateMessageEvent when calling get_message_obj_from_update_message_event()
// This will filter out other events (e.g. CreateMessageEvent) when calling event::emitted_events()
let message_obj = message_board::get_message_obj_from_update_message_event(vector::borrow(&events, 0));
let (content, creator, _, _) = message_board::get_message_content(message_obj);
assert!(content == string::utf8(b"hello move"), 2);
assert!(creator == sender_addr, 2);
}

#[test(aptos_framework = @aptos_framework, deployer = @message_board_addr, sender1 = @0x100, sender2 = @0x101)]
#[expected_failure(abort_code = 1, location = message_board_addr::message_board)]
fun test_only_creator_can_update(aptos_framework: &signer, deployer: &signer, sender1: &signer, sender2: &signer) {
message_board::init_module_for_test(aptos_framework, deployer);

message_board::create_message(sender1, string::utf8(b"hello world"));

let events = event::emitted_events();
let message_obj = message_board::get_message_obj_from_create_message_event(vector::borrow(&events, 0));

message_board::update_message(sender2, message_obj, string::utf8(b"hello move"));
}
}
Loading
Loading