| 1 | import 'dart:io'; |
| 2 | |
| 3 | import 'package:cw_core/db/sqlite_debug.dart'; |
| 4 | import 'package:cw_core/root_dir.dart'; |
| 5 | import 'package:cw_core/utils/print_verbose.dart'; |
| 6 | import 'package:flutter/foundation.dart'; |
| 7 | import 'package:sqflite_common_ffi/sqflite_ffi.dart'; |
| 8 | import 'package:path/path.dart' as p; |
| 9 | |
| 10 | Database? db; |
| 11 | |
| 12 | Future<void> _addColumnIfNotExists( |
| 13 | Database db, { |
| 14 | required String table, |
| 15 | required String column, |
| 16 | required String definition, |
| 17 | }) async { |
| 18 | final result = await db.rawQuery("PRAGMA table_info($table)"); |
| 19 | final columnExists = result.any((row) => row['name'] == column); |
| 20 | |
| 21 | if (!columnExists) { |
| 22 | await db.execute( |
| 23 | 'ALTER TABLE $table ADD COLUMN $column $definition;', |
| 24 | ); |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | Future<File> sqliteDebugMarkerFile() async { |
| 29 | final appDir = await getAppDir(); |
| 30 | final dbDebugMarker = p.join(appDir.path, ".sqlite_db_debug"); |
| 31 | return File(dbDebugMarker); |
| 32 | } |
| 33 | |
| 34 | Future<void> initDb({String? pathOverride}) async { |
| 35 | if (!kDebugMode && !kProfileMode) { |
| 36 | await _initDb(pathOverride: pathOverride); |
| 37 | return; |
| 38 | } |
| 39 | final dbDebugMarker = await sqliteDebugMarkerFile(); |
| 40 | try { |
| 41 | if (dbDebugMarker.existsSync()) { |
| 42 | throw Exception("Debug marker is present"); |
| 43 | } |
| 44 | await _initDb(pathOverride: pathOverride); |
| 45 | } catch (e, s) { |
| 46 | await handleSqliteError(e, s); |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | Future<void> _initDb({String? pathOverride}) async { |
| 51 | if (Platform.isLinux || Platform.isWindows) { |
| 52 | databaseFactory = databaseFactoryFfi; |
| 53 | } |
| 54 | |
| 55 | // getAppDir is predictable on all platforms and ensures the db gets included in backups. |
| 56 | final dbFileOld = File("${await getDatabasesPath()}/cake.db"); |
| 57 | final dbFile = File("${(await getAppDir()).path}/cake.db"); |
| 58 | |
| 59 | if (Platform.isAndroid && dbFileOld.existsSync() && dbFileOld.path != dbFile.path) { |
| 60 | final copied = dbFileOld.copySync(dbFile.path); |
| 61 | if (copied.existsSync()) { |
| 62 | dbFileOld.deleteSync(); |
| 63 | } |
| 64 | } |
| 65 | await db?.close(); |
| 66 | db = await openDatabase(dbFile.path, version: 10, |
| 67 | onUpgrade: (Database db, int oldVersion, int newVersion) async { |
| 68 | printV("migrating: $oldVersion, $newVersion"); |
| 69 | if (oldVersion <= 1) { |
| 70 | await db.execute(''' |
| 71 | DELETE FROM WalletInfo |
| 72 | WHERE walletInfoId NOT IN ( |
| 73 | SELECT MIN(walletInfoId) |
| 74 | FROM WalletInfo |
| 75 | GROUP BY id |
| 76 | ); |
| 77 | |
| 78 | CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique |
| 79 | ON WalletInfo (id); |
| 80 | '''); |
| 81 | } |
| 82 | if (oldVersion <= 2) { |
| 83 | await db.execute(''' |
| 84 | CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings ( |
| 85 | walletInfoId INTEGER, |
| 86 | accountIndex INTEGER DEFAULT -1, |
| 87 | gradientIndex INTEGER DEFAULT -1, |
| 88 | useSpecialDesign BOOLEAN DEFAULT FALSE, |
| 89 | backgroundImagePath TEXT DEFAULT "", |
| 90 | PRIMARY KEY (walletInfoId, accountIndex), |
| 91 | FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) |
| 92 | ); |
| 93 | '''); |
| 94 | await _addColumnIfNotExists( |
| 95 | db, |
| 96 | table: 'WalletInfo', |
| 97 | column: 'receiveInfoboxDismissed', |
| 98 | definition: 'BOOLEAN DEFAULT FALSE', |
| 99 | ); |
| 100 | |
| 101 | await _addColumnIfNotExists( |
| 102 | db, |
| 103 | table: 'BalanceCardStyleSettings', |
| 104 | column: 'cardOrder', |
| 105 | definition: 'INTEGER DEFAULT 0', |
| 106 | ); |
| 107 | } |
| 108 | if (oldVersion <= 3) { |
| 109 | await _addColumnIfNotExists(db, |
| 110 | table: "WalletInfo", column: "showCombinedBalance", definition: "BOOLEAN DEFAULT TRUE"); |
| 111 | // null - primary token (eth, sol etc) |
| 112 | // not null - address of fav token |
| 113 | // if address doesn't correspond to a valid token, fallback to primary token |
| 114 | await _addColumnIfNotExists(db, |
| 115 | table: "WalletInfo", column: "favoriteTokenAddress", definition: "TEXT DEFAULT NULL"); |
| 116 | } |
| 117 | |
| 118 | if (oldVersion <= 4) { |
| 119 | await _createBridgeTransferTable(db); |
| 120 | } |
| 121 | |
| 122 | if (oldVersion <= 5) { |
| 123 | await _createTradeTable(db); |
| 124 | } |
| 125 | if (oldVersion <= 6) { |
| 126 | await _addColumnIfNotExists( |
| 127 | db, |
| 128 | table: 'Trade', |
| 129 | column: 'toAddressExtraId', |
| 130 | definition: 'TEXT', |
| 131 | ); |
| 132 | } |
| 133 | if (oldVersion <= 7) { |
| 134 | await _createNodeTable(db); |
| 135 | } |
| 136 | if (oldVersion <= 8) { |
| 137 | await _addColumnIfNotExists( |
| 138 | db, |
| 139 | table: 'BalanceCardStyleSettings', |
| 140 | column: 'iconStyleIndex', |
| 141 | definition: 'INTEGER DEFAULT 0', |
| 142 | ); |
| 143 | await _addColumnIfNotExists( |
| 144 | db, |
| 145 | table: 'BalanceCardStyleSettings', |
| 146 | column: 'isGradientOnly', |
| 147 | definition: 'BOOLEAN DEFAULT FALSE', |
| 148 | ); |
| 149 | } |
| 150 | if (oldVersion <= 9) { |
| 151 | await _createErc20TokenTable(db); |
| 152 | await _createSplTokenTable(db); |
| 153 | await _createTronTokenTable(db); |
| 154 | } |
| 155 | }, onCreate: (Database db, int version) async { |
| 156 | await db.execute(''' |
| 157 | CREATE TABLE WalletInfo ( |
| 158 | walletInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| 159 | id TEXT NOT NULL, |
| 160 | name TEXT NOT NULL, |
| 161 | "type" INTEGER NOT NULL, |
| 162 | isRecovery INTEGER DEFAULT (0) NOT NULL, |
| 163 | walletInfoDerivationInfoId INTEGER NOT NULL, |
| 164 | restoreHeight INTEGER DEFAULT (0) NOT NULL, |
| 165 | "timestamp" INTEGER DEFAULT (0) NOT NULL, |
| 166 | dirPath TEXT NOT NULL, |
| 167 | "path" TEXT NOT NULL, |
| 168 | address TEXT NOT NULL, |
| 169 | yatEid TEXT, |
| 170 | yatLastUsedAddressRaw TEXT, |
| 171 | showIntroCakePayCard INTEGER DEFAULT (1), |
| 172 | addressPageType TEXT, |
| 173 | network TEXT, |
| 174 | hardwareWalletType INTEGER, |
| 175 | parentAddress TEXT, |
| 176 | hashedWalletIdentifier TEXT, |
| 177 | isNonSeedWallet INTEGER DEFAULT (0) NOT NULL, |
| 178 | sortOrder INTEGER DEFAULT (0) NOT NULL, |
| 179 | receiveInfoboxDismissed BOOLEAN DEFAULT FALSE, |
| 180 | showCombinedBalance BOOLEAN DEFAULT TRUE, |
| 181 | favoriteTokenAddress TEXT DEFAULT NULL |
| 182 | ); |
| 183 | '''); |
| 184 | |
| 185 | await db.execute(''' |
| 186 | CREATE TABLE WalletInfoDerivationInfo ( |
| 187 | walletInfoDerivationInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| 188 | address TEXT NOT NULL, |
| 189 | balance TEXT NOT NULL, |
| 190 | transactionsCount INTEGER DEFAULT (0) NOT NULL, |
| 191 | derivationType INTEGER NOT NULL, |
| 192 | derivationPath TEXT, |
| 193 | scriptType TEXT, |
| 194 | description TEXT |
| 195 | ); |
| 196 | '''); |
| 197 | |
| 198 | await db.execute(''' |
| 199 | CREATE TABLE WalletInfoAddress ( |
| 200 | walletInfoAddressId INTEGER PRIMARY KEY AUTOINCREMENT, |
| 201 | walletInfoId INTEGER, |
| 202 | "type" INTEGER NOT NULL, |
| 203 | address TEXT NOT NULL, |
| 204 | CONSTRAINT WalletInfoAddress_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) |
| 205 | ); |
| 206 | '''); |
| 207 | |
| 208 | await db.execute(''' |
| 209 | CREATE TABLE WalletInfoAddressInfo ( |
| 210 | walletInfoAddressInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| 211 | walletInfoId INTEGER NOT NULL, |
| 212 | mapKey INTEGER NOT NULL, |
| 213 | mapValueAccountIndex INTEGER NOT NULL, |
| 214 | mapValueAddress TEXT NOT NULL, |
| 215 | mapValueLabel TEXT NOT NULL, |
| 216 | CONSTRAINT WalletInfoAddressInfo_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) |
| 217 | ); |
| 218 | '''); |
| 219 | |
| 220 | await db.execute(''' |
| 221 | CREATE TABLE "WalletInfoAddressMap" ( |
| 222 | walletInfoAddressMapId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| 223 | walletInfoId INTEGER NOT NULL, |
| 224 | addressKey TEXT NOT NULL, |
| 225 | addressValue TEXT NOT NULL, |
| 226 | CONSTRAINT WalletInfoAddress_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) |
| 227 | ); |
| 228 | '''); |
| 229 | await db.execute(''' |
| 230 | CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique |
| 231 | ON WalletInfo (id); |
| 232 | '''); |
| 233 | await db.execute(''' |
| 234 | CREATE TABLE BalanceCardStyleSettings ( |
| 235 | walletInfoId INTEGER, |
| 236 | accountIndex INTEGER DEFAULT -1, |
| 237 | gradientIndex INTEGER DEFAULT -1, |
| 238 | useSpecialDesign BOOLEAN DEFAULT FALSE, |
| 239 | backgroundImagePath TEXT DEFAULT "", |
| 240 | iconStyleIndex INTEGER DEFAULT 0, |
| 241 | isGradientOnly BOOLEAN DEFAULT FALSE, |
| 242 | cardOrder INTEGER DEFAULT 0, |
| 243 | PRIMARY KEY (walletInfoId, accountIndex), |
| 244 | FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) |
| 245 | ); |
| 246 | '''); |
| 247 | await _createBridgeTransferTable(db); |
| 248 | await _createNodeTable(db); |
| 249 | await _createTradeTable(db); |
| 250 | await _createErc20TokenTable(db); |
| 251 | await _createSplTokenTable(db); |
| 252 | await _createTronTokenTable(db); |
| 253 | }); |
| 254 | } |
| 255 | |
| 256 | Future<void> _createTradeTable(Database db) async { |
| 257 | await db.execute(''' |
| 258 | CREATE TABLE IF NOT EXISTS Trade ( |
| 259 | tradeId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| 260 | id TEXT NOT NULL, |
| 261 | providerRaw INTEGER NOT NULL DEFAULT 0, |
| 262 | fromTitle TEXT, |
| 263 | fromName TEXT, |
| 264 | fromTag TEXT, |
| 265 | fromFullName TEXT, |
| 266 | fromDecimals INTEGER, |
| 267 | fromRaw INTEGER, |
| 268 | fromIconPath TEXT, |
| 269 | fromFlatIconPath TEXT, |
| 270 | fromChainIconPath TEXT, |
| 271 | toTitle TEXT, |
| 272 | toName TEXT, |
| 273 | toTag TEXT, |
| 274 | toFullName TEXT, |
| 275 | toDecimals INTEGER, |
| 276 | toRaw INTEGER, |
| 277 | toIconPath TEXT, |
| 278 | toFlatIconPath TEXT, |
| 279 | toChainIconPath TEXT, |
| 280 | stateRaw TEXT NOT NULL DEFAULT '', |
| 281 | createdAt INTEGER, |
| 282 | expiredAt INTEGER, |
| 283 | amount TEXT NOT NULL DEFAULT '', |
| 284 | receiveAmount TEXT, |
| 285 | inputAddress TEXT, |
| 286 | extraId TEXT, |
| 287 | outputTransaction TEXT, |
| 288 | refundAddress TEXT, |
| 289 | walletId TEXT, |
| 290 | payoutAddress TEXT, |
| 291 | toAddressExtraId TEXT, |
| 292 | password TEXT, |
| 293 | providerId TEXT, |
| 294 | providerName TEXT, |
| 295 | fromWalletAddress TEXT, |
| 296 | memo TEXT, |
| 297 | txId TEXT, |
| 298 | isRefund INTEGER DEFAULT 0, |
| 299 | isSendAll INTEGER DEFAULT 0, |
| 300 | router TEXT, |
| 301 | needToRegisterInSwapXyz INTEGER DEFAULT 0, |
| 302 | sourceTokenAddress TEXT, |
| 303 | sourceTokenDecimals INTEGER, |
| 304 | routerData TEXT, |
| 305 | routerValue TEXT, |
| 306 | routerChainId INTEGER, |
| 307 | sourceTokenAmountRaw TEXT, |
| 308 | requiresTokenApproval INTEGER DEFAULT 0, |
| 309 | chainId INTEGER, |
| 310 | fee REAL |
| 311 | ); |
| 312 | '''); |
| 313 | await db.execute(''' |
| 314 | CREATE UNIQUE INDEX IF NOT EXISTS idx_trade_id_unique |
| 315 | ON Trade (id); |
| 316 | '''); |
| 317 | } |
| 318 | |
| 319 | Future<Map<String, dynamic>> dumpDb() async { |
| 320 | try { |
| 321 | return await _dumpDb(); |
| 322 | } catch (e) { |
| 323 | return { |
| 324 | "error": e.toString(), |
| 325 | "stackTrace": StackTrace.current.toString(), |
| 326 | }; |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | Future<List<String>> _getTableNames(Database db) async { |
| 331 | final tableNames = await db.rawQuery('SELECT name FROM sqlite_master WHERE type = "table"'); |
| 332 | return tableNames.map((e) => (e["name"]).toString()).toList(); |
| 333 | } |
| 334 | |
| 335 | Future<Map<String, dynamic>> _dumpDb() async { |
| 336 | final tableNames = await _getTableNames(db!); |
| 337 | final ret = <String, dynamic>{}; |
| 338 | for (final tableName in tableNames) { |
| 339 | ret[tableName] = await db!.query(tableName); |
| 340 | } |
| 341 | return ret; |
| 342 | } |
| 343 | |
| 344 | Future<Map<String, dynamic>> dumpCustomDb(String path) async { |
| 345 | final db = await openDatabase(path); |
| 346 | final tableNames = await _getTableNames(db); |
| 347 | final ret = <String, dynamic>{}; |
| 348 | for (final tableName in tableNames) { |
| 349 | ret[tableName] = await db.query(tableName); |
| 350 | } |
| 351 | return ret; |
| 352 | } |
| 353 | |
| 354 | Future<void> _createBridgeTransferTable(Database db) async { |
| 355 | await db.execute(''' |
| 356 | CREATE TABLE IF NOT EXISTS BridgeTransfer ( |
| 357 | id TEXT NOT NULL PRIMARY KEY, |
| 358 | wallet_id TEXT NOT NULL, |
| 359 | source_chain_id INTEGER NOT NULL, |
| 360 | destination_chain_id INTEGER NOT NULL, |
| 361 | token_symbol TEXT NOT NULL, |
| 362 | token_contract TEXT NOT NULL, |
| 363 | amount TEXT NOT NULL, |
| 364 | recipient_address TEXT NOT NULL, |
| 365 | source_tx_hash TEXT NOT NULL, |
| 366 | status TEXT NOT NULL, |
| 367 | created_at INTEGER NOT NULL, |
| 368 | updated_at INTEGER, |
| 369 | confirmed_at INTEGER, |
| 370 | amount_raw TEXT, |
| 371 | error_message TEXT, |
| 372 | status_message TEXT |
| 373 | ); |
| 374 | '''); |
| 375 | await db.execute(''' |
| 376 | CREATE INDEX IF NOT EXISTS idx_bridgetransfer_wallet_id |
| 377 | ON BridgeTransfer(wallet_id); |
| 378 | '''); |
| 379 | } |
| 380 | |
| 381 | Future<void> _createErc20TokenTable(Database db) async { |
| 382 | await db.execute(""" |
| 383 | CREATE TABLE IF NOT EXISTS Erc20Token ( |
| 384 | Erc20TokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| 385 | walletName TEXT NOT NULL, |
| 386 | chainId INTEGER NOT NULL, |
| 387 | name TEXT NOT NULL DEFAULT '', |
| 388 | symbol TEXT NOT NULL DEFAULT '', |
| 389 | contractAddress TEXT NOT NULL, |
| 390 | decimal INTEGER NOT NULL DEFAULT 0, |
| 391 | enabled INTEGER NOT NULL DEFAULT 1, |
| 392 | iconPath TEXT, |
| 393 | tag TEXT, |
| 394 | isPotentialScam INTEGER NOT NULL DEFAULT 0 |
| 395 | ); |
| 396 | """); |
| 397 | await db.execute(""" |
| 398 | CREATE UNIQUE INDEX IF NOT EXISTS idx_erc20token_wallet_chain_contract |
| 399 | ON Erc20Token (walletName, chainId, contractAddress); |
| 400 | """); |
| 401 | } |
| 402 | |
| 403 | Future<void> _createSplTokenTable(Database db) async { |
| 404 | await db.execute(""" |
| 405 | CREATE TABLE IF NOT EXISTS SPLToken ( |
| 406 | SPLTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| 407 | walletName TEXT NOT NULL, |
| 408 | name TEXT NOT NULL DEFAULT '', |
| 409 | symbol TEXT NOT NULL DEFAULT '', |
| 410 | mintAddress TEXT NOT NULL, |
| 411 | decimal INTEGER NOT NULL DEFAULT 0, |
| 412 | mint TEXT NOT NULL DEFAULT '', |
| 413 | enabled INTEGER NOT NULL DEFAULT 1, |
| 414 | iconPath TEXT, |
| 415 | tag TEXT, |
| 416 | isPotentialScam INTEGER NOT NULL DEFAULT 0 |
| 417 | ); |
| 418 | """); |
| 419 | await db.execute(""" |
| 420 | CREATE UNIQUE INDEX IF NOT EXISTS idx_spltoken_wallet_mint |
| 421 | ON SPLToken (walletName, mintAddress); |
| 422 | """); |
| 423 | } |
| 424 | |
| 425 | Future<void> _createTronTokenTable(Database db) async { |
| 426 | await db.execute(""" |
| 427 | CREATE TABLE IF NOT EXISTS TronToken ( |
| 428 | TronTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
| 429 | walletName TEXT NOT NULL, |
| 430 | name TEXT NOT NULL DEFAULT '', |
| 431 | symbol TEXT NOT NULL DEFAULT '', |
| 432 | contractAddress TEXT NOT NULL, |
| 433 | decimal INTEGER NOT NULL DEFAULT 0, |
| 434 | enabled INTEGER NOT NULL DEFAULT 1, |
| 435 | iconPath TEXT, |
| 436 | tag TEXT, |
| 437 | isPotentialScam INTEGER NOT NULL DEFAULT 0 |
| 438 | ); |
| 439 | """); |
| 440 | await db.execute(""" |
| 441 | CREATE UNIQUE INDEX IF NOT EXISTS idx_trontoken_wallet_contract |
| 442 | ON TronToken (walletName, contractAddress); |
| 443 | """); |
| 444 | } |
| 445 | |
| 446 | Future<void> _createNodeTable(Database db) async { |
| 447 | db.execute(""" |
| 448 | CREATE TABLE Node ( |
| 449 | NodeId INTEGER PRIMARY KEY, |
| 450 | uri TEXT NOT NULL, |
| 451 | path TEXT, |
| 452 | login TEXT, |
| 453 | label TEXT, |
| 454 | password TEXT, |
| 455 | isPow INTEGER NOT NULL, |
| 456 | useSSL INTEGER, |
| 457 | typeRaw INTEGER NOT NULL, |
| 458 | trusted INTEGER NOT NULL, |
| 459 | socksProxyAddress TEXT, |
| 460 | isEnabledForAutoSwitching BOOLEAN DEFAULT FALSE, |
| 461 | isOfficial BOOLEAN DEFAULT FALSE, |
| 462 | isBuiltin BOOLEAN DEFAULT FALSE, |
| 463 | isDefault BOOLEAN DEFAULT FALSE |
| 464 | ); |
| 465 | """); |
| 466 | } |