diff --git a/examples/x-to-zkevm-migration-app/backend/README.md b/examples/x-to-zkevm-migration-app/backend/README.md
index 8c56e10936..ffb7ba06b1 100644
--- a/examples/x-to-zkevm-migration-app/backend/README.md
+++ b/examples/x-to-zkevm-migration-app/backend/README.md
@@ -5,10 +5,11 @@ A sample backend service that listens for NFT transfers to a burn address on Imm
## Overview
This service:
-1. Listens for transfer events via webhooks from Immutable X
-2. When an NFT is transferred to the burn address (0x0000000000000000000000000000000000000000) from a specified collection
-3. Creates a mint request for the same NFT on Immutable zkEVM
-4. Uses the minting-backend module to handle the minting process
+1. Registers migrations in a migrations table to track the migration process.
+2. Listens for transfer events via webhooks from Immutable X
+3. When an NFT is transferred to the burn address (0x0000000000000000000000000000000000000000) from a specified collection
+4. Creates a mint request for the same NFT on Immutable zkEVM
+5. Uses the minting-backend module to handle the minting process
## Getting Started
@@ -61,6 +62,7 @@ localhost:3001
# Postgres
localhost:5432
```
+
## Expose Local Port for Webhooks
You can use services like below to expose ports locally.
@@ -110,10 +112,17 @@ The service uses:
3. If transfer matches criteria:
- Creates mint request for zkEVM
- Submits mint request via minting backend
-4. Minting backend handles the actual minting process
-5. Service receives mint status updates via webhook
+4. Listens for burn events and verifies corresponding migration in the migrations table before minting.
+5. Minting backend handles the actual minting process
+6. Updates the migration record when a mint event occurs.
## Database
The service uses PostgreSQL for persistence. Tables are automatically created on startup:
- Uses `im_assets` tables for mint requests
+- Includes a `migrations` table to track migration registrations.
+
+## APIs
+
+- **POST /migrations**: API to register a new migration.
+- **GET /migrations**: API to retrieve all pending migrations.
diff --git a/examples/x-to-zkevm-migration-app/backend/docker-compose.yml b/examples/x-to-zkevm-migration-app/backend/docker-compose.yml
index 80bcc4492c..31ec5f2329 100644
--- a/examples/x-to-zkevm-migration-app/backend/docker-compose.yml
+++ b/examples/x-to-zkevm-migration-app/backend/docker-compose.yml
@@ -13,6 +13,7 @@ services:
- 5432:5432
volumes:
- ../../../packages/minting-backend/sdk/src/persistence/pg/seed.sql:/docker-entrypoint-initdb.d/seed.sql
+ - ./persistence/migrations/seed.sql:/docker-entrypoint-initdb.d/migrations_seed.sql
backend:
image: node:20-alpine
restart: always
diff --git a/examples/x-to-zkevm-migration-app/backend/index.ts b/examples/x-to-zkevm-migration-app/backend/index.ts
index a06bee4628..48caa45d3d 100644
--- a/examples/x-to-zkevm-migration-app/backend/index.ts
+++ b/examples/x-to-zkevm-migration-app/backend/index.ts
@@ -1,14 +1,20 @@
+import cors from '@fastify/cors';
import { config, mintingBackend, webhook } from '@imtbl/sdk';
import 'dotenv/config';
-import Fastify from 'fastify';
+import Fastify, { FastifyRequest } from 'fastify';
import { Pool } from 'pg';
import { v4 as uuidv4 } from 'uuid';
-
+import { migrationPersistence } from './persistence/postgres';
const fastify = Fastify({
logger: true
});
+// Enable CORS
+fastify.register(cors, {
+ origin: '*',
+});
+
// setup database client
const pgClient = new Pool({
user: process.env.PG_USER || 'postgres',
@@ -18,6 +24,8 @@ const pgClient = new Pool({
port: 5432,
});
+const migrations = migrationPersistence(pgClient);
+
// persistence setup for minting backend
const mintingPersistence = mintingBackend.mintingPersistencePg(pgClient);
@@ -39,9 +47,24 @@ fastify.post('/webhook', async (request, reply) => {
{
zkevmMintRequestUpdated: async (event) => {
console.log('Received webhook event:', event);
+ const tokenAddress = event.data.contract_address;
+ const tokenId = event.data.token_id || '';
+
+ // Update migration status
+ if (tokenAddress && tokenId && event.data.status === 'succeeded') {
+ const migration = await migrations.getMigration(tokenId, { zkevmCollectionAddress: tokenAddress });
+ if (!migration) {
+ console.log(`Migration record not found for minted token ${tokenId}`);
+ return;
+ }
+
+ await migrations.updateMigration(migration.id, {
+ status: 'minted',
+ });
+ }
await minting.processMint(request.body as any);
- console.log('Processed minting update:', event);
+ console.log('Processed minting update');
},
xTransferCreated: async (event) => {
console.log('Received webhook event:', event);
@@ -50,17 +73,39 @@ fastify.post('/webhook', async (request, reply) => {
event.data.receiver.toLowerCase() === process.env.IMX_BURN_ADDRESS?.toLowerCase() &&
event.data.token?.data?.token_address?.toLowerCase() === process.env.IMX_MONITORED_COLLECTION_ADDRESS?.toLowerCase()
) {
- // Create mint request on zkEVM
- let mintRequest = {
- asset_id: uuidv4(),
- contract_address: process.env.ZKEVM_COLLECTION_ADDRESS!,
- owner_address: event.data.user,
- token_id: event.data.token.data.token_id,
- metadata: {} // Add any metadata if needed
- };
- await minting.recordMint(mintRequest);
-
- console.log(`Created mint request for burned token ${event.data.token.data.token_id}`);
+ // Check if we have a migration record for this token
+ const tokenAddress = event.data.token?.data?.token_address;
+ const tokenId = event.data.token?.data?.token_id;
+
+ if (tokenAddress && tokenId) {
+ const migration = await migrations.getMigration(tokenId, { xCollectionAddress: tokenAddress });
+ if (!migration) {
+ console.log(`Migration record not found for burned token ${tokenId}`);
+ return;
+ }
+
+ // Update migration status
+ await migrations.updateMigration(migration.id, {
+ burn_id: event.data.transaction_id.toString(),
+ status: 'burned',
+ });
+
+ // Create mint request on zkEVM
+ let mintRequest = {
+ asset_id: uuidv4(),
+ contract_address: process.env.ZKEVM_COLLECTION_ADDRESS!,
+ owner_address: migration.zkevm_wallet_address,
+ token_id: migration.token_id,
+ metadata: {} // Add any metadata if needed
+ };
+ await minting.recordMint(mintRequest);
+
+ console.log(`Updated migration status for burned token ${tokenId}`);
+
+ console.log(`Created mint request for burned token ${event.data.token.data.token_id}`);
+ } else {
+ console.log('Token address or token ID is undefined');
+ }
}
}
}
@@ -75,6 +120,43 @@ fastify.post('/webhook', async (request, reply) => {
});
}
});
+interface MigrationRequest {
+ migrationReqs: {
+ zkevm_wallet_address: string;
+ token_id: string;
+ }[];
+}
+
+// New endpoint to create or upsert a list of migrations
+fastify.post('/migrations', async (request: FastifyRequest<{ Body: MigrationRequest }>, reply) => {
+ const { migrationReqs } = request.body;
+
+ try {
+ for (const migration of migrationReqs) {
+ await migrations.insertMigration({
+ x_collection_address: process.env.IMX_MONITORED_COLLECTION_ADDRESS!,
+ zkevm_collection_address: process.env.ZKEVM_COLLECTION_ADDRESS!,
+ zkevm_wallet_address: migration.zkevm_wallet_address,
+ token_id: migration.token_id,
+ status: 'pending'
+ });
+ }
+ return reply.status(201).send({ message: 'Migrations created successfully' });
+ } catch (error) {
+ console.error(error);
+ return reply.status(500).send({ message: 'Error creating migrations' });
+ }
+});
+
+fastify.get('/migrations', async (request, reply) => {
+ try {
+ const pendingMigrations = await migrations.getAllPendingMigrations(); // Adjust this method based on your persistence layer
+ return reply.status(200).send(pendingMigrations);
+ } catch (error) {
+ console.error(error);
+ return reply.status(500).send({ message: 'Error retrieving migrations' });
+ }
+});
const start = async () => {
try {
diff --git a/examples/x-to-zkevm-migration-app/backend/persistence/migrations/seed.sql b/examples/x-to-zkevm-migration-app/backend/persistence/migrations/seed.sql
new file mode 100644
index 0000000000..5d40b14ba6
--- /dev/null
+++ b/examples/x-to-zkevm-migration-app/backend/persistence/migrations/seed.sql
@@ -0,0 +1,11 @@
+CREATE TABLE IF NOT EXISTS migrations (
+ id SERIAL PRIMARY KEY,
+ x_collection_address VARCHAR NOT NULL,
+ zkevm_collection_address VARCHAR NOT NULL,
+ token_id VARCHAR NOT NULL UNIQUE,
+ zkevm_wallet_address VARCHAR NOT NULL,
+ status VARCHAR NOT NULL,
+ burn_id VARCHAR,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file
diff --git a/examples/x-to-zkevm-migration-app/backend/persistence/postgres.ts b/examples/x-to-zkevm-migration-app/backend/persistence/postgres.ts
new file mode 100644
index 0000000000..fd9bdb2fa0
--- /dev/null
+++ b/examples/x-to-zkevm-migration-app/backend/persistence/postgres.ts
@@ -0,0 +1,72 @@
+import type { Pool } from 'pg';
+
+export const migrationPersistence = (client: Pool) => {
+ return {
+ insertMigration: async (migrationData: {
+ x_collection_address: string;
+ zkevm_collection_address: string;
+ zkevm_wallet_address: string;
+ token_id: string;
+ status: string;
+ }) => {
+ const result = await client.query(
+ `
+ INSERT INTO migrations (x_collection_address, zkevm_collection_address, zkevm_wallet_address, token_id, status)
+ VALUES ($1, $2, $3, $4, $5);
+ `,
+ [
+ migrationData.x_collection_address,
+ migrationData.zkevm_collection_address,
+ migrationData.zkevm_wallet_address,
+ migrationData.token_id,
+ migrationData.status,
+ ]
+ );
+ return result.rowCount !== null && result.rowCount > 0;
+ },
+
+ getMigration: async (tokenId: string, options?: { xCollectionAddress?: string; zkevmCollectionAddress?: string }) => {
+ let query = `
+ SELECT * FROM migrations WHERE token_id = $1
+ `;
+ let values = [tokenId];
+
+ if (options?.xCollectionAddress) {
+ query += ` AND x_collection_address = $2;`;
+ values.push(options.xCollectionAddress);
+ } else if (options?.zkevmCollectionAddress) {
+ query += ` AND zkevm_collection_address = $2;`;
+ values.push(options.zkevmCollectionAddress);
+ }
+
+ const res = await client.query(query, values);
+ return res.rows[0] || null;
+ },
+
+ getAllPendingMigrations: async () => {
+ const res = await client.query(
+ `
+ SELECT * FROM migrations WHERE status = 'pending';
+ `
+ );
+ return res.rows || [];
+ },
+
+ updateMigration: async (id: string, updateData: Partial<{
+ status?: string;
+ burn_id?: string;
+ }>) => {
+ const fields = Object.keys(updateData).map((key, index) => `${key} = $${index + 2}`).join(', ');
+ const values = [id, ...Object.values(updateData)];
+ const result = await client.query(
+ `
+ UPDATE migrations
+ SET ${fields}
+ WHERE id = $1;
+ `,
+ values
+ );
+ return result.rowCount !== null && result.rowCount > 0;
+ },
+ };
+};
\ No newline at end of file
diff --git a/examples/x-to-zkevm-migration-app/frontend/.env.example b/examples/x-to-zkevm-migration-app/frontend/.env.example
index ff6b2e1b0e..e06d9f8b56 100644
--- a/examples/x-to-zkevm-migration-app/frontend/.env.example
+++ b/examples/x-to-zkevm-migration-app/frontend/.env.example
@@ -3,3 +3,5 @@ NEXT_PUBLIC_BURN_ADDRESS=0x0000000000000000000000000000000000000000 # or whateve
NEXT_PUBLIC_PUBLISHABLE_KEY=
NEXT_PUBLIC_CLIENT_ID=
NEXT_PUBLIC_ALCHEMY_API_KEY=
+NEXT_PUBLIC_IMX_COLLECTION_ADDRESS=
+NEXT_PUBLIC_ZKEVM_COLLECTION_ADDRESS=
diff --git a/examples/x-to-zkevm-migration-app/frontend/README.md b/examples/x-to-zkevm-migration-app/frontend/README.md
index cba6f02208..accad2d0bf 100644
--- a/examples/x-to-zkevm-migration-app/frontend/README.md
+++ b/examples/x-to-zkevm-migration-app/frontend/README.md
@@ -4,10 +4,10 @@ This React application allows users to migrate their NFTs from Immutable X to Im
## Features
-- **Login with Passport**: Securely connect your wallet using Immutable Passport.
-- **View Immutable X Assets**: Display NFTs available for migration from Immutable X.
-- **View zkEVM Assets**: Display NFTs on zkEVM, including those migrated from Immutable X.
-- **Migrate NFTs**: Initiate a burn on Immutable X and mint the equivalent NFT on zkEVM.
+- **Login with Passport or Link for IMX**: Securely connect your wallet using Immutable Passport or Link for Immutable X.
+- **Login with Passport for zkEVM**: Connect your wallet using Immutable Passport for zkEVM.
+- **Stage Assets for Migration**: Stage your assets before initiating the migration process.
+- **Migrate All NFTs**: Click "Migrate All" to burn all staged assets on Immutable X and mint them on zkEVM.
## Prerequisites
@@ -34,6 +34,8 @@ This React application allows users to migrate their NFTs from Immutable X to Im
NEXT_PUBLIC_PUBLISHABLE_KEY=
NEXT_PUBLIC_CLIENT_ID=
NEXT_PUBLIC_API_KEY=
+ NEXT_PUBLIC_IMX_COLLECTION_ADDRESS= # Add your IMX collection address
+ NEXT_PUBLIC_ZKEVM_COLLECTION_ADDRESS= # Add your zkEVM collection address
```
3. **Start the development server**:
@@ -46,18 +48,19 @@ This React application allows users to migrate their NFTs from Immutable X to Im
## Usage
1. **Connect Wallet**
- - Click "Connect Wallet" to authenticate using Passport
+ - Click "Connect Wallet" to authenticate using Link or Passport for IMX
- Approve the connection request
+ - For zkEVM, click "Connect Wallet" to authenticate using Passport only
2. **View Your NFTs**
- **IMX NFTs**: Shows your available NFTs for migration
- **zkEVM NFTs**: Shows your NFTs on zkEVM network
3. **Migrate NFTs**
- - Select an NFT from your IMX collection
- - Click "Migrate" to initiate the transfer to the burn address
- - The backend will detect the burn and mint on zkEVM
- - New NFT will appear in the zkEVM tab once minted
+ - Stage your NFTs for migration
+ - Click "Migrate All" to initiate the transfer to the burn address
+ - The backend will detect the burns and mint on zkEVM
+ - New NFTs will appear in the zkEVM tab once minted
## Development
diff --git a/examples/x-to-zkevm-migration-app/frontend/package.json b/examples/x-to-zkevm-migration-app/frontend/package.json
index 44be560ad1..25816adcbb 100644
--- a/examples/x-to-zkevm-migration-app/frontend/package.json
+++ b/examples/x-to-zkevm-migration-app/frontend/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"@biom3/react": "^0.27.25",
+ "@imtbl/imx-sdk": "^3.8.2",
"@imtbl/sdk": "^1.52.0",
"dotenv": "^16.4.5",
"next": "14.2.10",
diff --git a/examples/x-to-zkevm-migration-app/frontend/src/app/layout.tsx b/examples/x-to-zkevm-migration-app/frontend/src/app/layout.tsx
index c80a704698..6e3b7e9bc6 100644
--- a/examples/x-to-zkevm-migration-app/frontend/src/app/layout.tsx
+++ b/examples/x-to-zkevm-migration-app/frontend/src/app/layout.tsx
@@ -1,10 +1,13 @@
'use client';
+import { BackendProvider } from '@/context/backend';
import { IMXProvider } from '@/context/imx';
+import { LinkProvider } from '@/context/link';
import { PassportProvider } from '@/context/passport';
import { ZkEVMProvider } from '@/context/zkevm';
import { BiomeCombinedProviders } from '@biom3/react';
import { Inter } from 'next/font/google';
+import React from 'react';
import './globals.css';
const inter = Inter({ subsets: ['latin'] })
@@ -17,15 +20,19 @@ export default function RootLayout({
return (