@@ -1820,4 +1820,299 @@ mod test {
18201820 "valid asset_lock identity_id should be preserved"
18211821 ) ;
18221822 }
1823+
1824+ /// Test migration from v0.9.0 schema (DB version 5) all the way to current.
1825+ /// This is the exact schema shipped in the v0.9.0 release, with realistic
1826+ /// data including wallets, addresses, identities, and asset locks.
1827+ #[ test]
1828+ fn test_migration_from_v090_to_current ( ) {
1829+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1830+ let db_file_path = temp_dir. path ( ) . join ( "v090.db" ) ;
1831+ let db = super :: Database :: new ( & db_file_path) . unwrap ( ) ;
1832+
1833+ {
1834+ let conn = db. conn . lock ( ) . unwrap ( ) ;
1835+
1836+ // Exact v0.9.0 schema — copied from git show v0.9.0:src/database/initialization.rs
1837+ conn. execute_batch (
1838+ "CREATE TABLE IF NOT EXISTS settings (
1839+ id INTEGER PRIMARY KEY CHECK (id = 1),
1840+ password_check BLOB,
1841+ main_password_salt BLOB,
1842+ main_password_nonce BLOB,
1843+ network TEXT NOT NULL,
1844+ start_root_screen INTEGER NOT NULL,
1845+ custom_dash_qt_path TEXT,
1846+ overwrite_dash_conf INTEGER,
1847+ database_version INTEGER NOT NULL
1848+ );
1849+
1850+ CREATE TABLE IF NOT EXISTS wallet (
1851+ seed_hash BLOB NOT NULL PRIMARY KEY,
1852+ encrypted_seed BLOB NOT NULL,
1853+ salt BLOB NOT NULL,
1854+ nonce BLOB NOT NULL,
1855+ master_ecdsa_bip44_account_0_epk BLOB NOT NULL,
1856+ alias TEXT,
1857+ is_main INTEGER,
1858+ uses_password INTEGER NOT NULL,
1859+ password_hint TEXT,
1860+ network TEXT NOT NULL
1861+ );
1862+
1863+ CREATE TABLE IF NOT EXISTS wallet_addresses (
1864+ seed_hash BLOB NOT NULL,
1865+ address TEXT NOT NULL,
1866+ derivation_path TEXT NOT NULL,
1867+ balance INTEGER,
1868+ path_reference INTEGER NOT NULL,
1869+ path_type INTEGER NOT NULL,
1870+ PRIMARY KEY (seed_hash, address),
1871+ FOREIGN KEY (seed_hash) REFERENCES wallet(seed_hash) ON DELETE CASCADE
1872+ );
1873+
1874+ CREATE INDEX IF NOT EXISTS idx_wallet_addresses_path_reference
1875+ ON wallet_addresses (path_reference);
1876+ CREATE INDEX IF NOT EXISTS idx_wallet_addresses_path_type
1877+ ON wallet_addresses (path_type);
1878+
1879+ CREATE TABLE IF NOT EXISTS utxos (
1880+ txid BLOB NOT NULL,
1881+ vout INTEGER NOT NULL,
1882+ address TEXT NOT NULL,
1883+ value INTEGER NOT NULL,
1884+ script_pubkey BLOB NOT NULL,
1885+ network TEXT NOT NULL,
1886+ PRIMARY KEY (txid, vout, network)
1887+ );
1888+
1889+ CREATE INDEX IF NOT EXISTS idx_utxos_address ON utxos (address);
1890+ CREATE INDEX IF NOT EXISTS idx_utxos_network ON utxos (network);
1891+
1892+ CREATE TABLE IF NOT EXISTS asset_lock_transaction (
1893+ tx_id BLOB PRIMARY KEY,
1894+ transaction_data BLOB NOT NULL,
1895+ amount INTEGER,
1896+ instant_lock_data BLOB,
1897+ chain_locked_height INTEGER,
1898+ identity_id BLOB,
1899+ identity_id_potentially_in_creation BLOB,
1900+ wallet BLOB NOT NULL,
1901+ network TEXT NOT NULL,
1902+ FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE,
1903+ FOREIGN KEY (identity_id_potentially_in_creation) REFERENCES identity(id),
1904+ FOREIGN KEY (wallet) REFERENCES wallet(seed_hash) ON DELETE CASCADE
1905+ );
1906+
1907+ CREATE TABLE IF NOT EXISTS identity (
1908+ id BLOB PRIMARY KEY,
1909+ data BLOB,
1910+ is_in_creation INTEGER NOT NULL DEFAULT 0,
1911+ is_local INTEGER NOT NULL,
1912+ alias TEXT,
1913+ info TEXT,
1914+ wallet BLOB,
1915+ wallet_index INTEGER,
1916+ identity_type TEXT,
1917+ network TEXT NOT NULL,
1918+ CHECK ((wallet IS NOT NULL AND wallet_index IS NOT NULL)
1919+ OR (wallet IS NULL AND wallet_index IS NULL)),
1920+ FOREIGN KEY (wallet) REFERENCES wallet(seed_hash) ON DELETE CASCADE
1921+ );
1922+
1923+ CREATE INDEX IF NOT EXISTS idx_identity_local_network_type
1924+ ON identity (is_local, network, identity_type);
1925+
1926+ CREATE TABLE IF NOT EXISTS contested_name (
1927+ normalized_contested_name TEXT NOT NULL,
1928+ locked_votes INTEGER,
1929+ abstain_votes INTEGER,
1930+ awarded_to BLOB,
1931+ end_time INTEGER,
1932+ locked INTEGER NOT NULL DEFAULT 0,
1933+ last_updated INTEGER,
1934+ network TEXT NOT NULL,
1935+ PRIMARY KEY (normalized_contested_name, network)
1936+ );
1937+
1938+ CREATE TABLE IF NOT EXISTS contestant (
1939+ normalized_contested_name TEXT NOT NULL,
1940+ identity_id BLOB NOT NULL,
1941+ name TEXT,
1942+ votes INTEGER,
1943+ created_at INTEGER,
1944+ created_at_block_height INTEGER,
1945+ created_at_core_block_height INTEGER,
1946+ document_id BLOB,
1947+ network TEXT NOT NULL,
1948+ PRIMARY KEY (normalized_contested_name, identity_id, network),
1949+ FOREIGN KEY (normalized_contested_name, network)
1950+ REFERENCES contested_name(normalized_contested_name, network)
1951+ ON DELETE CASCADE
1952+ );
1953+
1954+ CREATE TABLE IF NOT EXISTS contract (
1955+ contract_id BLOB,
1956+ contract BLOB,
1957+ name TEXT,
1958+ network TEXT NOT NULL,
1959+ PRIMARY KEY (contract_id, network)
1960+ );
1961+
1962+ CREATE INDEX IF NOT EXISTS idx_name_network ON contract (name, network);" ,
1963+ )
1964+ . unwrap ( ) ;
1965+
1966+ // v0.9.0 also created these via separate functions
1967+ // proof_log (v2)
1968+ conn. execute_batch (
1969+ "CREATE TABLE IF NOT EXISTS proof_log (
1970+ proof_log_id INTEGER PRIMARY KEY AUTOINCREMENT,
1971+ proof_log BLOB NOT NULL,
1972+ proof_log_timestamp INTEGER NOT NULL
1973+ );" ,
1974+ )
1975+ . unwrap ( ) ;
1976+
1977+ // top_up (v4)
1978+ conn. execute_batch (
1979+ "CREATE TABLE IF NOT EXISTS top_up (
1980+ identity_id BLOB NOT NULL,
1981+ top_up_index INTEGER NOT NULL,
1982+ amount INTEGER NOT NULL,
1983+ PRIMARY KEY (identity_id, top_up_index),
1984+ FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE
1985+ );" ,
1986+ )
1987+ . unwrap ( ) ;
1988+
1989+ // scheduled_votes (v5) — v0.9.0 schema had NO network column
1990+ // and NO FK to identity. The v6 migration handles both.
1991+ conn. execute_batch (
1992+ "CREATE TABLE IF NOT EXISTS scheduled_votes (
1993+ identity_id BLOB NOT NULL,
1994+ contested_name TEXT NOT NULL,
1995+ vote_choice TEXT NOT NULL,
1996+ time INTEGER NOT NULL,
1997+ executed INTEGER NOT NULL DEFAULT 0,
1998+ PRIMARY KEY (identity_id, contested_name)
1999+ );" ,
2000+ )
2001+ . unwrap ( ) ;
2002+
2003+ // Insert settings at version 5
2004+ conn. execute (
2005+ "INSERT INTO settings (id, network, start_root_screen, database_version)
2006+ VALUES (1, 'dash', 0, 5)" ,
2007+ [ ] ,
2008+ )
2009+ . unwrap ( ) ;
2010+
2011+ // Insert a wallet with some addresses and an identity
2012+ let seed_hash = vec ! [ 0xAAu8 ; 32 ] ;
2013+ conn. execute (
2014+ "INSERT INTO wallet (seed_hash, encrypted_seed, salt, nonce,
2015+ master_ecdsa_bip44_account_0_epk, alias, is_main, uses_password, network)
2016+ VALUES (?1, ?2, ?3, ?4, ?5, 'test-wallet', 1, 0, 'dash')" ,
2017+ params ! [
2018+ seed_hash,
2019+ vec![ 1u8 ; 64 ] ,
2020+ vec![ 2u8 ; 16 ] ,
2021+ vec![ 3u8 ; 12 ] ,
2022+ vec![ 4u8 ; 33 ]
2023+ ] ,
2024+ )
2025+ . unwrap ( ) ;
2026+
2027+ conn. execute (
2028+ "INSERT INTO wallet_addresses (seed_hash, address, derivation_path,
2029+ balance, path_reference, path_type)
2030+ VALUES (?1, 'yTestAddr1', 'm/44''/1''/0''/0/0', 50000, 0, 0)" ,
2031+ params ! [ seed_hash] ,
2032+ )
2033+ . unwrap ( ) ;
2034+
2035+ let identity_id = vec ! [ 0xBBu8 ; 32 ] ;
2036+ conn. execute (
2037+ "INSERT INTO identity (id, is_local, alias, wallet, wallet_index,
2038+ identity_type, network)
2039+ VALUES (?1, 1, 'my-identity', ?2, 0, 'user', 'dash')" ,
2040+ params ! [ identity_id, seed_hash] ,
2041+ )
2042+ . unwrap ( ) ;
2043+
2044+ conn. execute (
2045+ "INSERT INTO asset_lock_transaction (tx_id, transaction_data, amount,
2046+ identity_id, wallet, network)
2047+ VALUES (?1, ?2, 100000, ?3, ?4, 'dash')" ,
2048+ params ! [ vec![ 0xCCu8 ; 32 ] , vec![ 0u8 ; 50 ] , identity_id, seed_hash] ,
2049+ )
2050+ . unwrap ( ) ;
2051+
2052+ conn. execute (
2053+ "INSERT INTO contract (contract_id, contract, name, network)
2054+ VALUES (?1, ?2, 'dpns', 'dash')" ,
2055+ params ! [ vec![ 0xDDu8 ; 32 ] , vec![ 0u8 ; 100 ] ] ,
2056+ )
2057+ . unwrap ( ) ;
2058+ }
2059+
2060+ assert_eq ! ( db. db_schema_version( ) . unwrap( ) , 5 ) ;
2061+
2062+ // Run full migration from v5 to current
2063+ let result = db. try_perform_migration ( 5 , DEFAULT_DB_VERSION ) ;
2064+ assert ! (
2065+ result. is_ok( ) ,
2066+ "migration from v0.9.0 (v5) to v{DEFAULT_DB_VERSION} failed: {:?}" ,
2067+ result. err( )
2068+ ) ;
2069+
2070+ assert_eq ! ( db. db_schema_version( ) . unwrap( ) , DEFAULT_DB_VERSION ) ;
2071+
2072+ let conn = db. conn . lock ( ) . unwrap ( ) ;
2073+ assert_v33_schema ( & conn) ;
2074+
2075+ // Verify data survived migration
2076+ let wallet_network: String = conn
2077+ . query_row (
2078+ "SELECT network FROM wallet WHERE seed_hash = ?1" ,
2079+ params ! [ vec![ 0xAAu8 ; 32 ] ] ,
2080+ |row| row. get ( 0 ) ,
2081+ )
2082+ . unwrap ( ) ;
2083+ assert_eq ! (
2084+ wallet_network, "mainnet" ,
2085+ "wallet network should be renamed"
2086+ ) ;
2087+
2088+ // wallet_addresses should have total_received column (added by v17)
2089+ assert_column_exists ( & conn, "wallet_addresses" , "total_received" ) ;
2090+
2091+ // wallet should have balance columns (added by v16)
2092+ assert_column_exists ( & conn, "wallet" , "confirmed_balance" ) ;
2093+ assert_column_exists ( & conn, "wallet" , "total_balance" ) ;
2094+
2095+ // wallet should have core_wallet_name (added by v33)
2096+ assert_column_exists ( & conn, "wallet" , "core_wallet_name" ) ;
2097+
2098+ // Identity should survive with network renamed
2099+ let id_network: String = conn
2100+ . query_row (
2101+ "SELECT network FROM identity WHERE id = ?1" ,
2102+ params ! [ vec![ 0xBBu8 ; 32 ] ] ,
2103+ |row| row. get ( 0 ) ,
2104+ )
2105+ . unwrap ( ) ;
2106+ assert_eq ! ( id_network, "mainnet" ) ;
2107+
2108+ // Asset lock should survive with identity_id intact
2109+ let lock_identity: Option < Vec < u8 > > = conn
2110+ . query_row (
2111+ "SELECT identity_id FROM asset_lock_transaction WHERE tx_id = ?1" ,
2112+ params ! [ vec![ 0xCCu8 ; 32 ] ] ,
2113+ |row| row. get ( 0 ) ,
2114+ )
2115+ . unwrap ( ) ;
2116+ assert_eq ! ( lock_identity, Some ( vec![ 0xBBu8 ; 32 ] ) ) ;
2117+ }
18232118}
0 commit comments