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

docs: Core 2389 update migration sample #2460

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
21 changes: 15 additions & 6 deletions examples/x-to-zkevm-migration-app/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -61,6 +62,7 @@ localhost:3001
# Postgres
localhost:5432
```

## Expose Local Port for Webhooks

You can use services like below to expose ports locally.
Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
110 changes: 96 additions & 14 deletions examples/x-to-zkevm-migration-app/backend/index.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -18,6 +24,8 @@ const pgClient = new Pool({
port: 5432,
});

const migrations = migrationPersistence(pgClient);

// persistence setup for minting backend
const mintingPersistence = mintingBackend.mintingPersistencePg(pgClient);

Expand All @@ -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);
Expand All @@ -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');
}
}
}
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
Original file line number Diff line number Diff line change
@@ -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;
},
};
};
2 changes: 2 additions & 0 deletions examples/x-to-zkevm-migration-app/frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
21 changes: 12 additions & 9 deletions examples/x-to-zkevm-migration-app/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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**:
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions examples/x-to-zkevm-migration-app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 16 additions & 9 deletions examples/x-to-zkevm-migration-app/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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'] })
Expand All @@ -17,15 +20,19 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className}>
<BiomeCombinedProviders>
<IMXProvider>
<ZkEVMProvider>
<PassportProvider>
{children}
</PassportProvider>
</ZkEVMProvider>
</IMXProvider>
</BiomeCombinedProviders>
<BackendProvider>
<BiomeCombinedProviders>
<LinkProvider>
<IMXProvider>
<ZkEVMProvider>
<PassportProvider>
{children}
</PassportProvider>
</ZkEVMProvider>
</IMXProvider>
</LinkProvider>
</BiomeCombinedProviders>
</BackendProvider>
</body>
</html>
)
Expand Down
Loading
Loading