Skip to content

Commit

Permalink
Mint NFT
Browse files Browse the repository at this point in the history
  • Loading branch information
chiliec committed Feb 27, 2024
1 parent 086ea6d commit d50cb79
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 35 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ BOT_WEBHOOK=https://www.example.com/<BOT_TOKEN>
BOT_SERVER_HOST=localhost
BOT_SERVER_PORT=3000
BOT_ADMINS=[1]
COLLECTION_ADDRESS=EQ...
COLLECTION_ADDRESS=EQ...
PINATA_API_KEY=
PINATA_API_SECRET=
MNEMONICS=word1 word2 word3 ...
TONCENTER_API_KEY=
37 changes: 37 additions & 0 deletions src/bot/features/collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Composer } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import { NftCollection } from "#root/bot/models/nft-collection.js";
import { config } from "#root/config.js";
import { chatAction } from "@grammyjs/auto-chat-action";
import { openWallet, waitSeqno } from "#root/bot/helpers/ton.js";
import { isAdmin } from "#root/bot/filters/is-admin.js";

const composer = new Composer<Context>();

const feature = composer.chatType("private").filter(isAdmin);

feature.command(
"collection",
logHandle("command-collection"),
chatAction("upload_document"),
async (ctx) => {
const wallet = await openWallet(config.MNEMONICS!.split(" "), true);
const collectionData = {
ownerAddress: wallet.contract.address,
royaltyPercent: 0.49,
royaltyAddress: wallet.contract.address,
nextItemIndex: 0,

collectionContentUrl: `ipfs://QmXXjER4hMHLp6ESJFG21A7yrWaoBE57cLyAHKyt1XiFpF/collection.json`,
commonContentUrl: ``,
};
const collection = new NftCollection(collectionData);
const seqno = await collection.deploy(wallet);
ctx.logger.info(`Collection deployed: ${collection.address}`);
await waitSeqno(seqno, wallet);
ctx.reply(`Collection deployed: ${collection.address}`);
},
);

export { composer as collectionFeature };
1 change: 1 addition & 0 deletions src/bot/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./mint.js";
export * from "./start.js";
export * from "./dice.js";
export * from "./queue.js";
export * from "./collection.js";
30 changes: 30 additions & 0 deletions src/bot/features/queue.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
/* eslint-disable unicorn/no-null */
import { Composer, InputMediaBuilder } from "grammy";
import type { Context } from "#root/bot/context.js";
import { logHandle } from "#root/bot/helpers/logging.js";
import { queueMenu } from "#root/bot/keyboards/queue-menu.js";
import { isAdmin } from "#root/bot/filters/is-admin.js";
import { config } from "#root/config.js";
import { Address, toNano } from "ton-core";
import { changeImageData } from "../callback-data/image-selection.js";
import { getUserProfileFile } from "../helpers/photo.js";
import { SelectImageButton, photoKeyboard } from "../keyboards/photo.js";
import { NftCollection, mintParameters } from "../models/nft-collection.js";
import { openWallet, waitSeqno } from "../helpers/ton.js";
import { NftItem } from "../models/nft-item.js";

const composer = new Composer<Context>();

Expand Down Expand Up @@ -40,6 +45,31 @@ feature.callbackQuery(
break;
}
case SelectImageButton.Done: {
ctx.chatAction = "upload_document";
const address = Address.parse(config.COLLECTION_ADDRESS);
const testnet = true;
const nextItemIndex = await NftCollection.fetchNextItemIndex(
address,
testnet,
);
const wallet = await openWallet(config.MNEMONICS.split(" "), true);
const item = new NftItem();
const userAddress = Address.parse(ctx.dbuser.wallet ?? "");
const parameters: mintParameters = {
queryId: 0,
itemOwnerAddress: userAddress,
itemIndex: nextItemIndex,
amount: toNano("0.05"),
contentUri:
"ipfs://QmXXjER4hMHLp6ESJFG21A7yrWaoBE57cLyAHKyt1XiFpF/5.json",
};
const seqno = await item.deploy(wallet, address, parameters);
await waitSeqno(seqno, wallet);
const nftAddress = await NftItem.getAddressByIndex(
address,
nextItemIndex,
);
ctx.reply(nftAddress.toString());
break;
}
default: {
Expand Down
2 changes: 2 additions & 0 deletions src/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
startFeature,
diceFeature,
queueFeature,
collectionFeature,
} from "#root/bot/features/index.js";
import { errorHandler } from "#root/bot/handlers/index.js";
import { i18n, isMultipleLocales } from "#root/bot/i18n.js";
Expand Down Expand Up @@ -65,6 +66,7 @@ export function createBot(token: string, options: Options) {
protectedBot.use(resetFeature);
protectedBot.use(diceFeature);
protectedBot.use(queueFeature);
protectedBot.use(collectionFeature);
protectedBot.use(adminFeature);

if (isMultipleLocales) {
Expand Down
48 changes: 26 additions & 22 deletions src/bot/models/nft-collection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable class-methods-use-this */
import {
Address,
Cell,
Expand All @@ -9,22 +8,23 @@ import {
SendMode,
} from "ton-core";
import { encodeOffChainContent, OpenedWallet } from "#root/bot/helpers/ton.js";
import { TonClient } from "ton";

export type collectionData = {
ownerAddress: Address;
royaltyPercent: number;
royaltyAddress: Address;
nextItemIndex: number;
collectionContentUrl: string;
commonContentUrl: string;
collectionContentUrl: string; // path to collection.json
commonContentUrl: string; // base url with ended /
};

export type mintParameters = {
queryId: number;
itemOwnerAddress: Address;
itemIndex: number;
amount: bigint;
commonContentUrl: string;
contentUri: string;
};

export class NftCollection {
Expand All @@ -34,6 +34,27 @@ export class NftCollection {
this.data = data;
}

static async fetchNextItemIndex(
nftCollectionAddress: Address,
testnet: boolean,
): Promise<number> {
const toncenterBaseEndpoint: string = testnet
? "https://testnet.toncenter.com"
: "https://toncenter.com";

const client = new TonClient({
endpoint: `${toncenterBaseEndpoint}/api/v2/jsonRPC`,
apiKey: process.env.TONCENTER_API_KEY,
});

const { stack } = await client.runMethod(
nftCollectionAddress,
"get_collection_data",
);
const nextItemIndex = stack.readBigNumber();
return Number(nextItemIndex);
}

public async deploy(wallet: OpenedWallet): Promise<number> {
const seqno = await wallet.contract.getSeqno();
await wallet.contract.sendTransfer({
Expand Down Expand Up @@ -74,24 +95,6 @@ export class NftCollection {
return seqno;
}

public createMintBody(parameters: mintParameters): Cell {
const body = beginCell();
body.storeUint(1, 32);
body.storeUint(parameters.queryId || 0, 64);
body.storeUint(parameters.itemIndex, 64);
body.storeCoins(parameters.amount);

const nftItemContent = beginCell();
nftItemContent.storeAddress(parameters.itemOwnerAddress);

const uriContent = beginCell();
uriContent.storeBuffer(Buffer.from(parameters.commonContentUrl));
nftItemContent.storeRef(uriContent.endCell());

body.storeRef(nftItemContent.endCell());
return body.endCell();
}

public get stateInit(): StateInit {
const code = this.createCodeCell();
const data = this.createDataCell();
Expand All @@ -103,6 +106,7 @@ export class NftCollection {
return contractAddress(0, this.stateInit);
}

// eslint-disable-next-line class-methods-use-this
private createCodeCell(): Cell {
const NftCollectionCodeBoc =
"te6cckECFAEAAh8AART/APSkE/S88sgLAQIBYgkCAgEgBAMAJbyC32omh9IGmf6mpqGC3oahgsQCASAIBQIBIAcGAC209H2omh9IGmf6mpqGAovgngCOAD4AsAAvtdr9qJofSBpn+pqahg2IOhph+mH/SAYQAEO4tdMe1E0PpA0z/U1NQwECRfBNDUMdQw0HHIywcBzxbMyYAgLNDwoCASAMCwA9Ra8ARwIfAFd4AYyMsFWM8WUAT6AhPLaxLMzMlx+wCAIBIA4NABs+QB0yMsCEsoHy//J0IAAtAHIyz/4KM8WyXAgyMsBE/QA9ADLAMmAE59EGOASK3wAOhpgYC42Eit8H0gGADpj+mf9qJofSBpn+pqahhBCDSenKgpQF1HFBuvgoDoQQhUZYBWuEAIZGWCqALnixJ9AQpltQnlj+WfgOeLZMAgfYBwGyi544L5cMiS4ADxgRLgAXGBEuAB8YEYGYHgAkExIREAA8jhXU1DAQNEEwyFAFzxYTyz/MzMzJ7VTgXwSED/LwACwyNAH6QDBBRMhQBc8WE8s/zMzMye1UAKY1cAPUMI43gED0lm+lII4pBqQggQD6vpPywY/egQGTIaBTJbvy9AL6ANQwIlRLMPAGI7qTAqQC3gSSbCHis+YwMlBEQxPIUAXPFhPLP8zMzMntVABgNQLTP1MTu/LhklMTugH6ANQwKBA0WfAGjhIBpENDyFAFzxYTyz/MzMzJ7VSSXwXiN0CayQ==";
Expand Down
35 changes: 23 additions & 12 deletions src/bot/models/nft-item.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { TonClient } from "ton";
import { Address, beginCell, Cell, internal, SendMode, toNano } from "ton-core";
import { OpenedWallet } from "#root/bot/helpers/ton.js";
import {
NftCollection,
mintParameters,
} from "#root/bot/models/nft-collection.js";
import { mintParameters } from "#root/bot/models/nft-collection.js";

export class NftItem {
private collection: NftCollection;

constructor(collection: NftCollection) {
this.collection = collection;
}

public async deploy(
wallet: OpenedWallet,
collectionAddress: Address,
parameters: mintParameters,
): Promise<number> {
const seqno = await wallet.contract.getSeqno();
Expand All @@ -24,15 +16,34 @@ export class NftItem {
messages: [
internal({
value: "0.05",
to: this.collection.address,
body: this.collection.createMintBody(parameters),
to: collectionAddress,
body: this.createMintBody(parameters),
}),
],
sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY,
});
return seqno;
}

// eslint-disable-next-line class-methods-use-this
public createMintBody(parameters: mintParameters): Cell {
const body = beginCell();
body.storeUint(1, 32);
body.storeUint(parameters.queryId || 0, 64);
body.storeUint(parameters.itemIndex, 64);
body.storeCoins(parameters.amount);

const nftItemContent = beginCell();
nftItemContent.storeAddress(parameters.itemOwnerAddress);

const uriContent = beginCell();
uriContent.storeBuffer(Buffer.from(parameters.contentUri));
nftItemContent.storeRef(uriContent.endCell());

body.storeRef(nftItemContent.endCell());
return body.endCell();
}

static async getAddressByIndex(
collectionAddress: Address,
itemIndex: number,
Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const createConfigFromEnvironment = (environment: NodeJS.ProcessEnv) => {
MONGO: z.string(),
BOT_ADMINS: z.array(z.number()).default([]),
COLLECTION_ADDRESS: z.string(),
PINATA_API_KEY: z.string(),
PINATA_API_SECRET: z.string(),
MNEMONICS: z.string(),
TONCENTER_API_KEY: z.string(),
});

if (config.BOT_MODE === "webhook") {
Expand Down

0 comments on commit d50cb79

Please sign in to comment.