Skip to content

Commit

Permalink
Free mint sample app (#1782)
Browse files Browse the repository at this point in the history
  • Loading branch information
shineli1984 authored May 17, 2024
1 parent 9d9c103 commit 364ee38
Show file tree
Hide file tree
Showing 87 changed files with 6,442 additions and 48 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
/packages/checkout/widgets-lib @immutable/commerce
/packages/blockchain-data @immutable/assets
/packages/minting-backend @shineli1984
/packages/webhook @shineli1984
/packages/game-bridge @immutable/sdk
/samples @immutable/sdk
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#Obtained at hub.immutable.com
SANDBOX_HUB_IMMUTABLE_API_KEY=
SANDBOX_RPS_IMMUTABLE_API_KEY=

MAINNET_HUB_IMMUTABLE_API_KEY=
MAINNET_RPS_IMMUTABLE_API_KEY=

# Example: file:./allowList.db
DATABASE_URL=
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
/node_modules
.DS_Store
prisma/allowList.db
prisma/allowList.db-journal
/logs
98 changes: 98 additions & 0 deletions packages/minting-backend/minting-sample-app/backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Minting API Backend for conducting a free mint

This project is a backend API for doing a free mint on IMX zkEVM.

## Disclaimer

The sample code provided is for reference purposes only. It has undergone best effort testing by Immutable to ensure basic functionality. However, it is essential that you thoroughly test this sample code within your own environment to confirm its functionality and reliability before deploying it in a production setting. Immutable disclaims any liability for any issues that arise due to the use of this sample code. By using this sample code, you agree to perform due diligence in testing and verifying its suitability for your applications.

## Features

- Uses the Immutable Minting API to ensure that minting is sponsored on top of transaction life cycle monitoring, nonce management etc. is abstracted.
- Async Minting by 1. store minting intention, 2. submit minting to Immutable Minting API in background, 3. listening to webhook for minting status.
- Passport authentication.
- Define phases that the mint should occur in, start times and end times.
- Ability to allowlist addresses for minting for different phases

## Setup Instructions

1. Install the dependencies:
```
npm i
```
2. Copy the example environment file and fill it with your API key, and DB path(should be `file:./allowList.db`):
```
cp .env.example .env
```
3. Make sure to configure `src/config.ts` with collection address etc after deploying the contract on hub.immutable.com. Pay specific attention to the mintPhases parameter:
```
mintPhases: [
{
name: "Presale",
startTime: 1629913600,
endTime: 1629999999,
maxSupply: 1000,
enableAllowList: true,
},
{
name: "Public Sale",
startTime: 1630000000,
endTime: 1719292800,
maxSupply: 9000,
enableAllowList: false,
maxPerWallet: 2,
}],
```
Keep in mind that you can configure a single phase if you're not planning a phased approach but just a start/end time.

This sample app only support the same metadata for all the mints. it is defined in the `metadata` field in the same `src/config.ts` file. Please make amend logic inside `server.ts` for calls to `mintingBackend.recordMint` to give metadata per token.

4. Run the DB migrations:

```
npx prisma migrate dev
```

Every time you change primsa schema you need to run the above.

5. Load your database, https://sqlitebrowser.org/ is great for this. You can also write a script that uses the Prisma client to load the database. Make sure you have your address allowlisted, and quantity is 1, isLocked is 0, hasMinted is 0.

6. Run the development server:

```
npm start
```

7. Create your webhook at https://hub.immutable.com/, use localtunnel for testing webhooks locally:

```
npx localtunnel --port 3000
```

Use the above URL for the webhook endpoint with the path `/webhook`. For example: `https://ten-rooms-vanish.loca.lt/webhook`.

8. Use Postgresql instead of SQLite
This example uses SQLite as database for its portability and self-contain-ness.
However, ** please do not use SQLite in production ** for its weak support of concurrency.

We recommend using postgres for the persistance. Immutable's sdk provides a postgres persistence for this purpose. You can replace `mintingBackend.mintingPersistencePrismaSqlite` with `mintingBackend.mintingPersistencePg` in the `server.ts` and change prisma schema according to the one provided by our sdk: [Postgres seed.sql](https://github.com/immutable/ts-immutable-sdk/blob/main/packages/minting-backend/sdk/src/minting/persistence/pg/seed.sql).

## Utility

Retry failed mints or mints recorded but does not exist in Immutable Minting API.

```
npm run retrymints
```

update minting status according to status from Immutable Minting API.

```
npm run updatemints
```

## To-Do List

- [ ] Add ERC1155 support once the minting API is ready
- [ ] Add the ability to choose whether you want mintByQuantity or mintByID
- [ ] this sample app will be ported over to use postgres in the future.
43 changes: 43 additions & 0 deletions packages/minting-backend/minting-sample-app/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "minting-api-backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "ts-node-dev --respawn --transpile-only src/server.ts",
"updatemints": "ts-node src/utils/updateMintStatus.ts",
"retrymints": "ts-node src/utils/mintFailsAndMissing.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/cors": "^9.0.1",
"@imtbl/sdk": "1.34.2-alpha",
"@prisma/client": "^5.12.1",
"@types/jsonwebtoken": "^9.0.6",
"aws-sdk": "^2.1603.0",
"ethereum-cryptography": "^2.1.3",
"ethers": "^6.12.1",
"fastify": "^4.26.2",
"jsonwebtoken": "^9.0.2",
"jwk-to-pem": "^2.0.5",
"prisma": "^5.12.1",
"sns-validator": "^0.3.5",
"util": "^0.12.5",
"uuid": "^9.0.1",
"viem": "^2.9.29",
"winston": "^3.13.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@types/node": "^20.12.7",
"@types/pg": "^8.11.6",
"@types/sns-validator": "^0.3.3",
"@types/uuid": "^9.0.8",
"artillery": "^2.0.11",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "im_assets" (
"id" TEXT NOT NULL PRIMARY KEY,
"assetId" TEXT NOT NULL,
"ownerAddress" TEXT NOT NULL,
"metadata" TEXT,
"tokenId" TEXT,
"contractAddress" TEXT NOT NULL,
"error" TEXT,
"mintingStatus" TEXT,
"metadataId" TEXT,
"triedCount" INTEGER NOT NULL DEFAULT 0,
"lastImtblZkevmMintRequestUpdatedId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- CreateIndex
CREATE UNIQUE INDEX "im_assets_assetId_contractAddress_key" ON "im_assets"("assetId", "contractAddress");
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateTable
CREATE TABLE "Allowlist" (
"address" TEXT NOT NULL PRIMARY KEY,
"phase" INTEGER NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

model ImAssets {
id String @id @default(uuid())
assetId String // Previously @db.Uuid in Postgres
ownerAddress String
metadata String? // Previously Json? @db.JsonB in Postgres
tokenId String?
contractAddress String
error String?
mintingStatus String?
metadataId String? // Previously @db.Uuid in Postgres
triedCount Int @default(0)
lastImtblZkevmMintRequestUpdatedId String? // Previously @db.Uuid in Postgres
createdAt DateTime @default(now()) // Stored as TEXT
updatedAt DateTime @default(now()) // Stored as TEXT
@@map("im_assets")
@@unique([assetId, contractAddress], name: "im_assets_uindex")
}

model Allowlist {
address String @id
phase Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import serverConfig, { environment } from "./config";
import { blockchainData } from "@imtbl/sdk";

export const blockchainDataClient = new blockchainData.BlockchainData({
baseConfig: {
environment: environment,
apiKey: serverConfig[environment].HUB_API_KEY,
rateLimitingKey: serverConfig[environment].RPS_API_KEY,
}
});
78 changes: 78 additions & 0 deletions packages/minting-backend/minting-sample-app/backend/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { config } from "@imtbl/sdk";
import { ServerConfig } from "./types";
require("dotenv").config();

//config.Environment.SANDBOX or config.Environment.PRODUCTION
export const environment = config.Environment.SANDBOX;

//Used for verification of the Passport JWTs
export const IMX_JWT_KEY_URL = "https://auth.immutable.com/.well-known/jwks.json?_gl=1*1g7a0qs*_ga*NDg1NTg3MDI3LjE2ODU1OTY1Mzg.*_ga_4JBHZ7F06X*MTY4ODUyNjkyNy4xNC4wLjE2ODg1MjY5MjcuMC4wLjA.*_ga_7XM4Y7T8YC*MTY4ODUyNjkyNy4yNy4wLjE2ODg1MjY5MjcuMC4wLjA.";

const serverConfig: ServerConfig = {
[config.Environment.SANDBOX]: {
API_URL: "https://api.sandbox.immutable.com",
HUB_API_KEY: process.env.SANDBOX_HUB_IMMUTABLE_API_KEY!,
RPS_API_KEY: process.env.SANDBOX_RPS_IMMUTABLE_API_KEY!,
HOST_IP: "localhost",
PORT: 3000,
chainName: "imtbl-zkevm-testnet",
collectionAddress: "",
enableFileLogging: true, //Should logs be output to files or just console?
maxTokenSupplyAcrossAllPhases: 1500,
logLevel: "debug",
eoaMintMessage: "Sign this message to verify your wallet address", //The message an EOA signs to verify their wallet address and mint
mintPhases: [
{
name: "Guaranteed",
startTime: 1715743355,
endTime: 1735743376,
},
{
name: "Waitlist",
startTime: 1714916593,
endTime: 1719292800,
},
],
metadata: {
name: "Your NFT name",
description: "Your NFT description",
image: "https://image-url.png",
animation_url: "https://video.mp4",
attributes: [],
},
},
[config.Environment.PRODUCTION]: {
API_URL: "https://api.immutable.com",
HUB_API_KEY: process.env.MAINNET_HUB_IMMUTABLE_API_KEY!,
RPS_API_KEY: process.env.MAINNET_RPS_IMMUTABLE_API_KEY!,
HOST_IP: "localhost",
PORT: 3000,
chainName: "imtbl-zkevm-mainnet",
collectionAddress: "",
enableFileLogging: true, //Should logs be output to files or just console?
maxTokenSupplyAcrossAllPhases: 1500,
logLevel: "debug",
eoaMintMessage: "Sign this message to verify your wallet address", //The message an EOA signs to verify their wallet address and mint
mintPhases: [
{
name: "Guaranteed",
startTime: 1629913600,
endTime: 1714623711,
},
{
name: "Waitlist",
startTime: 1714623712,
endTime: 1719292800,
},
],
metadata: {
name: "Your NFT name",
description: "Your NFT description",
image: "https://image-url.png",
animation_url: "https://video.mp4",
attributes: [],
},
},
};

export default serverConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { PrismaClient } from "@prisma/client";
import logger from "./logger";

export async function checkAddressMinted(address: string, client: PrismaClient): Promise<string | null> {
try {
logger.info(`Checking if user has minted: ${address}`);
const mintedAddress = await client.imAssets.findFirst({
where: {
ownerAddress: address,
},
});
logger.info(`User has minted: ${mintedAddress !== null}`);
return mintedAddress?.assetId ?? null;
} catch (error) {
logger.error(`Error checking if user has minted: ${error}`);
throw error;
}
}

export async function totalMintCountAcrossAllPhases(client: PrismaClient): Promise<number> {
try {
const mintCount = await client.imAssets.count();
return mintCount;
} catch (error) {
logger.error(`Error getting total mint count: ${error}`);
throw error;
}
}


export async function loadAddressesIntoAllowlist(addresses: string[], phase: number, client: PrismaClient) {
try {
for (let address of addresses) {
await client.allowlist.create({
data: {
address: address.toLowerCase(),
phase: phase,
},
});
}
console.log("Addresses have been successfully loaded into the database.");
} catch (error) {
console.error("Error loading addresses into the database:", error);
}
}

export async function readAddressesFromAllowlist(phase: number, client: PrismaClient): Promise<string[]> {
try {
const addresses = await client.allowlist.findMany({
where: {
phase: phase,
},
});
return addresses.map((address: any) => address.address.toLowerCase());
} catch (error) {
console.error("Error reading addresses from the database:", error);
throw error;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PrismaClient } from '@prisma/client';

const client = new PrismaClient();

export { client };
Loading

0 comments on commit 364ee38

Please sign in to comment.