A TON NFT minting system with signature-based authorization. Users can mint NFTs only with a valid signature from the service.
User Wallet
│
▼ (1) Deploy + Mint
┌─────────────┐
│ MinterItem │ ── verifies signature, checks owner
└─────────────┘
│
▼ (2) Internal mint request
┌─────────────┐
│ Minter │ ── verifies sender address
└─────────────┘
│
▼ (3) Deploy NFT
┌─────────────┐
│ Collection │ ── deploys NFT item
└─────────────┘
├── contracts/
│ ├── nft_minter/ # Minter & MinterItem contracts (Tolk)
│ └── 02_nft/ # NFT Collection & Item contracts
├── wrappers/ # TypeScript contract wrappers
├── tests/ # Jest tests with gas analysis
├── scripts/ # Deployment scripts
└── service/ # Demo signing service (Node.js API)
npm installnpx blueprint build --allnpm testcd service
npx ts-node index.ts keysSave the public key - you'll need it for Minter deployment.
npx blueprint run deployNftCollection --testnetSave the collection address.
COLLECTION_ADDRESS=EQxxxxx npx blueprint run deployMinter --testnetTransfer the NFT collection admin rights to the Minter contract address:
COLLECTION_ADDRESS=EQxxxxx MINTER_ADDRESS=EQyyyyy npx blueprint run transferCollectionOwnership --testnetCopy .env.example to .env in the service/ folder:
cd service
cp .env.example .envEdit .env:
NETWORK=testnet
TONCENTER_API_KEY=your_api_key
MINTER_ADDRESS=EQxxxxx
COLLECTION_ADDRESS=EQxxxxx
DEFAULT_PRICE=1
PORT=3000cd service
npx ts-node index.ts serve| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /info |
Service info & public key |
| POST | /sign |
Sign NFT and get mint data |
| POST | /calculate-address |
Calculate MinterItem address |
| POST | /verify-deployment |
Check if contract deployed |
| POST | /batch-sign |
Sign multiple NFTs |
curl -X POST http://localhost:3000/sign \
-H "Content-Type: application/json" \
-d '{
"ownerAddress": "EQxxxxx",
"metadataUrl": "https://example.com/nft/1.json",
"price": "1"
}'Response:
{
"success": true,
"data": {
"minterItemAddress": "EQxxxxx",
"stateInit": "base64...",
"messageBody": "base64...",
"signature": "hex...",
"price": "1000000000",
"priceFormatted": "1.00 TON",
"metadataUrl": "https://example.com/nft/1.json",
"ownerAddress": "EQxxxxx",
"dataHash": "hex..."
}
}With the service running, test the full mint flow using the testMintViaService script:
# Terminal 1: Start the service
cd service && npx ts-node index.ts serve
# Terminal 2: Run the test script
METADATA_URL=https://example.com/nft/1.json PRICE=1 npx blueprint run testMintViaService --testnetThe script will:
- Connect to the signing service
- Request signed NFT data for your wallet address
- Send the mint transaction with state init and message body
- Wait for MinterItem deployment confirmation
Environment Variables:
| Variable | Description | Default |
|---|---|---|
SERVICE_URL |
URL of the signing service | http://localhost:3000 |
METADATA_URL |
NFT metadata JSON URL | https://example.com/nft/test.json |
PRICE |
Price in TON | 1 |
Example Output:
=== Test Mint via Service ===
Service URL: http://localhost:3000
Owner address: EQxxxxx
Metadata URL: https://example.com/nft/1.json
Price: 1 TON
Service is healthy.
Service info:
Minter address: EQyyyyy
Default price: 1.00 TON
Received mint data:
MinterItem address: EQzzzzz
Price: 1.00 TON
Data hash: abc123...
Sending mint transaction...
Value: 1.1500 TON
Transaction sent!
MinterItem address: EQzzzzz
Waiting for deployment...
MinterItem deployed successfully!
=== Done ===
See service/client-example.ts for a complete example:
import { requestSignedNft, executeMint } from './client-example';
// 1. Request signed data from service (price is optional, defaults to service config)
const mintData = await requestSignedNft(walletAddress, metadataUrl, '1'); // 1 TON
// 2. Send transaction from user wallet (value = price + gas buffer)
await executeMint(client, wallet, secretKey, mintData);| Operation | Cost |
|---|---|
| Minter Deploy | ~0.00015 TON |
| MinterItem Deploy + Mint | ~0.0017 TON |
| Minter Process | ~0.0019 TON |
| Full Mint Flow | ~0.0075 TON |
| Admin Claim | ~0.0029 TON |
Recommended mint transaction value: ~0.08 TON (includes NFT deploy amount + reserves)
| Code | Constant | Description |
|---|---|---|
| 201 | ERROR_MINT_DISABLED | Minting is disabled by admin |
| 202 | ERROR_NOT_OWNER_TRYING_TO_MINT | Wrong sender address |
| 203 | ERROR_NOT_ENOUGH_FUNDS_TO_MINT | Insufficient funds (less than price) |
| 204 | ERROR_MINTED_ALREADY | NFT already minted |
| 205 | ERROR_SIGNATURE_INVALID | Invalid service signature |
| 206 | ERROR_MINT_ITEM_ADDRESS_MISMATCH | Wrong MinterItem address |
| 207 | ERROR_NOT_ADMIN | Not admin for operation |
| 208 | ERROR_NOT_ENOUGH_BALANCE | Insufficient balance for claim |
| 209 | ERROR_CONTENT_NOT_FOUND | Content cleared (internal error) |
The admin can claim accumulated TON from the Minter contract (keeps 0.02 TON reserve):
await minter.sendAdminClaim(admin.getSender(), toNano('0.05'));The admin can enable or disable minting:
// Disable minting
await minter.sendAdminToggleMint(admin.getSender(), toNano('0.05'), false);
// Enable minting
await minter.sendAdminToggleMint(admin.getSender(), toNano('0.05'), true);The admin can transfer ownership of the NFT collection to another address:
await minter.sendAdminTransferCollectionOwnership(
admin.getSender(),
toNano('0.05'),
newOwnerAddress
);MIT