diff --git a/MigrationDashboard.tsx b/MigrationDashboard.tsx
new file mode 100644
index 00000000..aee54552
--- /dev/null
+++ b/MigrationDashboard.tsx
@@ -0,0 +1,199 @@
+"use client";
+
+import { useEffect, useState } from 'react';
+
+// --- Mocks & Placeholders ---
+// Replace these with your actual Soroban contract interaction library
+const sorobanUtils = {
+ getOldTokenBalance: async (userAddress: string) => "1000.0000000",
+ getAllowance: async (userAddress: string, spender: string) => "0.0000000",
+ getMigrationDeadline: async () => Date.now() / 1000 + 86400 * 7, // 7 days from now
+ getTotalMigrated: async () => "500000.0000000",
+ approve: async (spender: string, amount: string) => {
+ console.log(`Approving ${spender} for ${amount}`);
+ await new Promise(res => setTimeout(res, 2000));
+ return true;
+ },
+ migrate: async (amount: string) => {
+ console.log(`Migrating ${amount}`);
+ await new Promise(res => setTimeout(res, 2000));
+ return true;
+ },
+};
+
+const MIGRATION_CONTRACT_ID = "CD...YOUR_MIGRATION_CONTRACT_ID";
+const USER_ADDRESS = "GD...YOUR_USER_ADDRESS"; // This would come from a wallet connection
+
+const Countdown = ({ deadline }: { deadline: number }) => {
+ const [timeLeft, setTimeLeft] = useState("");
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const now = Date.now() / 1000;
+ const remaining = deadline - now;
+
+ if (remaining <= 0) {
+ setTimeLeft("Migration period has ended.");
+ clearInterval(interval);
+ return;
+ }
+
+ const days = Math.floor(remaining / 86400);
+ const hours = Math.floor((remaining % 86400) / 3600);
+ const minutes = Math.floor((remaining % 3600) / 60);
+ const seconds = Math.floor(remaining % 60);
+
+ setTimeLeft(`${days}d ${hours}h ${minutes}m ${seconds}s`);
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, [deadline]);
+
+ return
{timeLeft}
;
+};
+
+export default function MigrationDashboard() {
+ const [amount, setAmount] = useState('');
+ const [status, setStatus] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Data from blockchain
+ const [oldTokenBalance, setOldTokenBalance] = useState("0");
+ const [allowance, setAllowance] = useState("0");
+ const [deadline, setDeadline] = useState(0);
+ const [totalMigrated, setTotalMigrated] = useState("0");
+
+ const needsApproval = parseFloat(allowance) < parseFloat(amount || "0");
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const [balance, allowance, deadline, totalMigrated] = await Promise.all([
+ sorobanUtils.getOldTokenBalance(USER_ADDRESS),
+ sorobanUtils.getAllowance(USER_ADDRESS, MIGRATION_CONTRACT_ID),
+ sorobanUtils.getMigrationDeadline(),
+ sorobanUtils.getTotalMigrated(),
+ ]);
+ setOldTokenBalance(balance);
+ setAllowance(allowance);
+ setDeadline(deadline);
+ setTotalMigrated(totalMigrated);
+ } catch (error) {
+ console.error("Failed to fetch migration data:", error);
+ setStatus("Error: Could not load migration data.");
+ }
+ };
+ fetchData();
+ }, []);
+
+ const handleApprove = async () => {
+ setIsLoading(true);
+ setStatus(`Approving migration contract to spend ${amount} legacy tokens...`);
+ try {
+ await sorobanUtils.approve(MIGRATION_CONTRACT_ID, amount);
+ setAllowance(amount); // Optimistically update allowance
+ setStatus("Approval successful! You can now migrate.");
+ } catch (error) {
+ console.error(error);
+ setStatus("Error: Approval failed.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleMigrate = async () => {
+ setIsLoading(true);
+ setStatus(`Migrating ${amount} legacy tokens...`);
+ try {
+ await sorobanUtils.migrate(amount);
+ setStatus("Migration successful! Your new tokens have been minted.");
+ // Refetch data after migration
+ const newBalance = await sorobanUtils.getOldTokenBalance(USER_ADDRESS);
+ setOldTokenBalance(newBalance);
+ setAmount('');
+ } catch (error) {
+ console.error(error);
+ setStatus("Error: Migration failed.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
Token Migration Tool
+
+ Seamlessly swap your legacy RST tokens for the new, upgraded version.
+
+
+ {/* Stats Section */}
+
+
+
Time Remaining
+ {deadline > 0 ?
:
Loading...
}
+
+
+
Total Tokens Migrated
+
{parseFloat(totalMigrated).toLocaleString()}
+
+
+
+ {/* Swap Interface */}
+
+
+
+
+ setAmount(e.target.value)}
+ placeholder="0.0"
+ className="w-full p-3 bg-gray-900 border border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ disabled={isLoading}
+ />
+
+
+
Your Balance: {parseFloat(oldTokenBalance).toLocaleString()} Legacy RST
+
+
+ {/* Action Buttons */}
+
+ {needsApproval ? (
+
+ ) : (
+
✓ Approved
+ )}
+
+
+
+
+
+ {/* Status Message */}
+ {status && (
+
+ )}
+
+ );
+}
diff --git a/legacy_deprecation.rs b/legacy_deprecation.rs
new file mode 100644
index 00000000..56a50e7a
--- /dev/null
+++ b/legacy_deprecation.rs
@@ -0,0 +1,53 @@
+use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String};
+
+/// This module is intended to be integrated into an existing token contract
+/// via a contract upgrade. It cannot function as a standalone contract.
+
+#[contracttype]
+#[derive(Clone, Default)]
+pub struct DeprecationStatus {
+ pub is_deprecated: bool,
+ pub migration_contract: Option,
+ pub deprecation_timestamp: u64,
+}
+
+#[contracttype]
+pub enum DeprecationDataKey {
+ Status,
+}
+
+pub trait DeprecationTrait {
+ /// Deprecates the contract, allowing transfers only to the migration contract.
+ /// Must only be callable by the contract admin.
+ fn deprecate(env: Env, migration_contract: Address);
+
+ /// Helper function to check transfer validity during deprecation.
+ fn check_deprecation_status(env: &Env, to: &Address);
+}
+
+pub fn deprecate(env: Env, migration_contract: Address) {
+ // This function assumes it's called from within the legacy token contract
+ // and that admin authorization has already been checked.
+ let status = DeprecationStatus {
+ is_deprecated: true,
+ migration_contract: Some(migration_contract.clone()),
+ deprecation_timestamp: env.ledger().timestamp(),
+ };
+ env.storage().instance().set(&DeprecationDataKey::Status, &status);
+
+ env.events().publish(
+ (String::from_slice(&env, "deprecated"),),
+ (migration_contract,),
+ );
+}
+
+pub fn check_deprecation_status(env: &Env, to: &Address) {
+ if let Some(status) = env.storage().instance().get::<_, DeprecationStatus>(&DeprecationDataKey::Status) {
+ if status.is_deprecated {
+ let migration_target = status.migration_contract.unwrap();
+ if to != &migration_target {
+ panic!("Contract deprecated; transfers only allowed to the migration contract");
+ }
+ }
+ }
+}
diff --git a/token_migration.rs b/token_migration.rs
new file mode 100644
index 00000000..3cd77a86
--- /dev/null
+++ b/token_migration.rs
@@ -0,0 +1,217 @@
+use soroban_sdk::{
+ contract, contractimpl, contracttype, token, Address, Env, String,
+};
+
+use crate::legacy_deprecation::TokenClient as LegacyTokenClient;
+use crate::token::Client as NewTokenClient;
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MigrationConfig {
+ pub admin: Address,
+ pub old_token: Address,
+ pub new_token: Address,
+ pub ratio_new_per_old: u32, // e.g., 10 means 1 old -> 10 new
+ pub deadline: u64,
+}
+
+#[contracttype]
+enum DataKey {
+ Config,
+ UserMigrated(Address),
+ TotalMigrated,
+}
+
+#[contract]
+pub struct TokenMigrationContract;
+
+#[contractimpl]
+impl TokenMigrationContract {
+ /// Initializes the migration contract.
+ ///
+ /// # Arguments
+ /// * `admin` - The admin address, who can withdraw remaining legacy tokens after the deadline.
+ /// * `old_token` - The address of the legacy token contract.
+ /// * `new_token` - The address of the new token contract. This migration contract must have minting rights on it.
+ /// * `ratio_new_per_old` - The number of new tokens minted for 1 old token (in its smallest unit).
+ /// * `deadline` - A Unix timestamp after which migrations are no longer possible.
+ pub fn initialize(
+ env: Env,
+ admin: Address,
+ old_token: Address,
+ new_token: Address,
+ ratio_new_per_old: u32,
+ deadline: u64,
+ ) {
+ if env.storage().instance().has(&DataKey::Config) {
+ panic!("Already initialized");
+ }
+
+ let config = MigrationConfig {
+ admin,
+ old_token,
+ new_token,
+ ratio_new_per_old,
+ deadline,
+ };
+
+ env.storage().instance().set(&DataKey::Config, &config);
+ env.storage().instance().set(&DataKey::TotalMigrated, &0i128);
+
+ // Set TTL for all data
+ env.storage().instance().extend_ttl(100_000, 100_000);
+ }
+
+ /// Migrates a user's legacy tokens to new tokens.
+ /// The user must first approve this contract to spend their `old_token`.
+ ///
+ /// # Arguments
+ /// * `caller` - The user performing the migration.
+ /// * `amount` - The amount of old tokens to migrate.
+ pub fn migrate(env: Env, caller: Address, amount: i128) {
+ caller.require_auth();
+
+ let config: MigrationConfig = env.storage().instance().get(&DataKey::Config).unwrap();
+
+ if env.ledger().timestamp() > config.deadline {
+ panic!("Migration period has ended");
+ }
+
+ if amount <= 0 {
+ panic!("Amount must be positive");
+ }
+
+ // Calculate the amount of new tokens to mint
+ let new_amount = amount
+ .checked_mul(config.ratio_new_per_old as i128)
+ .expect("Amount overflow");
+
+ // Transfer old tokens from the user to this contract
+ let old_token_client = LegacyTokenClient::new(&env, &config.old_token);
+ old_token_client.transfer_from(&env.current_contract_address(), &caller, &env.current_contract_address(), &amount);
+
+ // Mint new tokens to the user
+ let new_token_client = NewTokenClient::new(&env, &config.new_token);
+ new_token_client.mint(&caller, &new_amount);
+
+ // Update user's migrated amount
+ let user_key = DataKey::UserMigrated(caller.clone());
+ let user_migrated: i128 = env.storage().persistent().get(&user_key).unwrap_or(0);
+ let new_user_migrated = user_migrated.checked_add(amount).expect("User amount overflow");
+ env.storage().persistent().set(&user_key, &new_user_migrated);
+ env.storage().persistent().extend_ttl(&user_key, 100_000, 100_000);
+
+ // Update total migrated amount
+ let total_migrated: i128 = env.storage().instance().get(&DataKey::TotalMigrated).unwrap();
+ let new_total_migrated = total_migrated.checked_add(amount).expect("Total amount overflow");
+ env.storage().instance().set(&DataKey::TotalMigrated, &new_total_migrated);
+
+ // Emit event
+ env.events().publish(
+ (String::from_slice(&env, "migrated"), caller),
+ (amount, new_amount),
+ );
+ }
+
+ /// Allows the admin to withdraw any remaining legacy tokens after the deadline.
+ /// This is for cleanup and to recover any tokens sent here by mistake.
+ pub fn withdraw_legacy(env: Env) {
+ let config: MigrationConfig = env.storage().instance().get(&DataKey::Config).unwrap();
+ config.admin.require_auth();
+
+ if env.ledger().timestamp() <= config.deadline {
+ panic!("Cannot withdraw before deadline");
+ }
+
+ let old_token_client = token::Client::new(&env, &config.old_token);
+ let balance = old_token_client.balance(&env.current_contract_address());
+
+ if balance > 0 {
+ old_token_client.transfer(&env.current_contract_address(), &config.admin, &balance);
+ }
+ }
+
+ // --- View Functions ---
+
+ /// Returns the migration configuration.
+ pub fn get_config(env: Env) -> MigrationConfig {
+ env.storage().instance().get(&DataKey::Config).unwrap()
+ }
+
+ /// Returns the total amount of old tokens a specific user has migrated.
+ pub fn get_user_migrated(env: Env, user: Address) -> i128 {
+ env.storage()
+ .persistent()
+ .get(&DataKey::UserMigrated(user))
+ .unwrap_or(0)
+ }
+
+ /// Returns the total amount of old tokens migrated across all users.
+ pub fn get_total_migrated(env: Env) -> i128 {
+ env.storage().instance().get(&DataKey::TotalMigrated).unwrap()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use soroban_sdk::{testutils::{Address as _, Events}, BytesN, IntoVal};
+
+ // Note: Full integration tests would require setting up three contracts:
+ // 1. Legacy Token
+ // 2. New Token
+ // 3. This Migration Contract
+ // The tests here are simplified to illustrate the logic.
+
+ #[test]
+ fn test_initialization_and_migration() {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::random(&env);
+ let user = Address::random(&env);
+ let old_token_address = Address::random(&env);
+ let new_token_address = Address::random(&env);
+ let migration_contract_id = env.register_contract(None, TokenMigrationContract);
+ let client = TokenMigrationContractClient::new(&env, &migration_contract_id);
+
+ let deadline = env.ledger().timestamp() + 1000;
+ let ratio = 1; // 1:1 ratio
+
+ client.initialize(&admin, &old_token_address, &new_token_address, &ratio, &deadline);
+
+ // --- Mock contract calls ---
+ // This is a simplified mock. In a real test, you'd register token contracts.
+ let expected_new_amount = 100 * ratio as i128;
+
+ // Mock the call to `transfer_from` on the old token
+ env.mock_contract_call(
+ &old_token_address,
+ "transfer_from",
+ (&migration_contract_id, &user, &migration_contract_id, &100i128).into_val(&env),
+ Ok(().into()),
+ );
+ // Mock the call to `mint` on the new token
+ env.mock_contract_call(
+ &new_token_address,
+ "mint",
+ (&user, &expected_new_amount).into_val(&env),
+ Ok(().into()),
+ );
+
+ client.migrate(&user, &100);
+
+ assert_eq!(client.get_user_migrated(&user), 100);
+ assert_eq!(client.get_total_migrated(), 100);
+
+ let event = env.events().all().last().unwrap();
+ assert_eq!(
+ event,
+ (
+ migration_contract_id.clone(),
+ (String::from_slice(&env, "migrated"), user.clone()).into_val(&env),
+ (100i128, expected_new_amount).into_val(&env)
+ )
+ );
+ }
+}