diff --git a/.sqlx/query-05c5b5fb801065c07a336435c72c318e7cd5ccdfde04660ea9d8e4de00cbeafd.json b/.sqlx/query-05c5b5fb801065c07a336435c72c318e7cd5ccdfde04660ea9d8e4de00cbeafd.json deleted file mode 100644 index 540fd1abb..000000000 --- a/.sqlx/query-05c5b5fb801065c07a336435c72c318e7cd5ccdfde04660ea9d8e4de00cbeafd.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `coin_states`.`coin_id`\n FROM `coin_states` INDEXED BY `coin_spent`\n INNER JOIN `cat_coins` ON `coin_states`.`coin_id` = `cat_coins`.`coin_id`\n WHERE `spent_height` IS NULL\n ", - "describe": { - "columns": [ - { - "name": "coin_id", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "05c5b5fb801065c07a336435c72c318e7cd5ccdfde04660ea9d8e4de00cbeafd" -} diff --git a/.sqlx/query-06597c3a99e3ac6448ef9ab5f46f7e80b9e84294af7b626712e4f13a7f23468b.json b/.sqlx/query-06597c3a99e3ac6448ef9ab5f46f7e80b9e84294af7b626712e4f13a7f23468b.json deleted file mode 100644 index 93bef2075..000000000 --- a/.sqlx/query-06597c3a99e3ac6448ef9ab5f46f7e80b9e84294af7b626712e4f13a7f23468b.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT \n collection_id, \n did_id, \n metadata_collection_id, \n visible, \n name, \n icon,\n COUNT(*) OVER() as total_count\n FROM collections INDEXED BY col_name\n ORDER BY visible DESC, is_named DESC, name ASC, collection_id ASC\n LIMIT ? OFFSET ?\n ", - "describe": { - "columns": [ - { - "name": "collection_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "did_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "metadata_collection_id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "visible", - "ordinal": 3, - "type_info": "Bool" - }, - { - "name": "name", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "icon", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "total_count", - "ordinal": 6, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - false - ] - }, - "hash": "06597c3a99e3ac6448ef9ab5f46f7e80b9e84294af7b626712e4f13a7f23468b" -} diff --git a/.sqlx/query-06802a4efba6dc3e16e65769f739dd1506bb3ac5b040e71c70a6f929e6a49f44.json b/.sqlx/query-06802a4efba6dc3e16e65769f739dd1506bb3ac5b040e71c70a6f929e6a49f44.json deleted file mode 100644 index 83fbae97c..000000000 --- a/.sqlx/query-06802a4efba6dc3e16e65769f739dd1506bb3ac5b040e71c70a6f929e6a49f44.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `launcher_id`, `coin_id`, `name`, `is_owned`, `visible`, `created_height`\n FROM `dids`\n WHERE `coin_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "launcher_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "coin_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "is_owned", - "ordinal": 3, - "type_info": "Bool" - }, - { - "name": "visible", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "created_height", - "ordinal": 5, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - true, - false, - false, - true - ] - }, - "hash": "06802a4efba6dc3e16e65769f739dd1506bb3ac5b040e71c70a6f929e6a49f44" -} diff --git a/.sqlx/query-08603dd734a2434722e2243d61a44577d5eb89f5d546db3b7f87b37bd420737d.json b/.sqlx/query-08603dd734a2434722e2243d61a44577d5eb89f5d546db3b7f87b37bd420737d.json deleted file mode 100644 index 8228e7651..000000000 --- a/.sqlx/query-08603dd734a2434722e2243d61a44577d5eb89f5d546db3b7f87b37bd420737d.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `unix_time`\n FROM `blockinfo`\n WHERE `height` = ?\n ", - "describe": { - "columns": [ - { - "name": "unix_time", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "08603dd734a2434722e2243d61a44577d5eb89f5d546db3b7f87b37bd420737d" -} diff --git a/.sqlx/query-09f846328374776c4980a0f6ca31b29fbce2044d7601c642c88a6da5b131e4af.json b/.sqlx/query-09f846328374776c4980a0f6ca31b29fbce2044d7601c642c88a6da5b131e4af.json deleted file mode 100644 index dcbe8c79e..000000000 --- a/.sqlx/query-09f846328374776c4980a0f6ca31b29fbce2044d7601c642c88a6da5b131e4af.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n `did_coins`.`coin_id`, `amount`, `p2_puzzle_hash`,\n `recovery_list_hash`, `created_height`, `transaction_id`\n FROM `did_coins`\n INNER JOIN `coin_states` ON `coin_states`.coin_id = `did_coins`.coin_id\n WHERE `did_coins`.`coin_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "p2_puzzle_hash", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "recovery_list_hash", - "ordinal": 3, - "type_info": "Blob" - }, - { - "name": "created_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - true, - true - ] - }, - "hash": "09f846328374776c4980a0f6ca31b29fbce2044d7601c642c88a6da5b131e4af" -} diff --git a/.sqlx/query-0a154eb3d84e9224d7f88acde6a2ecfa0cb47a5977f9dd9434247117ebc7a3ca.json b/.sqlx/query-0a154eb3d84e9224d7f88acde6a2ecfa0cb47a5977f9dd9434247117ebc7a3ca.json deleted file mode 100644 index a5eeba759..000000000 --- a/.sqlx/query-0a154eb3d84e9224d7f88acde6a2ecfa0cb47a5977f9dd9434247117ebc7a3ca.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT MAX(`index`) AS `max_index`\n FROM `derivations`\n WHERE EXISTS (\n SELECT 1 FROM `coin_states`\n WHERE `puzzle_hash` = `p2_puzzle_hash`\n OR `hint` = `p2_puzzle_hash`\n )\n AND `hardened` = ?\n ", - "describe": { - "columns": [ - { - "name": "max_index", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "0a154eb3d84e9224d7f88acde6a2ecfa0cb47a5977f9dd9434247117ebc7a3ca" -} diff --git a/.sqlx/query-0a55a34cdc4bd0cb4c0514ab938f061de3ceb1ac6b1c0dd4de92149ed2c4494a.json b/.sqlx/query-0a55a34cdc4bd0cb4c0514ab938f061de3ceb1ac6b1c0dd4de92149ed2c4494a.json deleted file mode 100644 index d5678107d..000000000 --- a/.sqlx/query-0a55a34cdc4bd0cb4c0514ab938f061de3ceb1ac6b1c0dd4de92149ed2c4494a.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `coin_states`.`amount` FROM `coin_states` INDEXED BY `coin_spent`\n INNER JOIN `cat_coins` ON `coin_states`.`coin_id` = `cat_coins`.`coin_id`\n LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id`\n WHERE `coin_states`.`spent_height` IS NULL\n AND `cat_coins`.`asset_id` = ?\n AND `transaction_spends`.`coin_id` IS NULL\n ", - "describe": { - "columns": [ - { - "name": "amount", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "0a55a34cdc4bd0cb4c0514ab938f061de3ceb1ac6b1c0dd4de92149ed2c4494a" -} diff --git a/.sqlx/query-0a9370ba52b140ceae15af6d25c3778baa8e5c4ce10fff5d90fa538895e1f2c0.json b/.sqlx/query-0a9370ba52b140ceae15af6d25c3778baa8e5c4ce10fff5d90fa538895e1f2c0.json new file mode 100644 index 000000000..5eaa25923 --- /dev/null +++ b/.sqlx/query-0a9370ba52b140ceae15af6d25c3778baa8e5c4ce10fff5d90fa538895e1f2c0.json @@ -0,0 +1,92 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n offers.hash as offer_id, assets.hash as asset_id,\n amount, royalty, is_requested, \n assets.description, assets.is_sensitive_content,\n assets.is_visible, assets.icon_url, assets.name,\n assets.ticker, assets.precision, assets.kind\n FROM offer_assets \n INNER JOIN assets ON offer_assets.asset_id = assets.id\n INNER JOIN offers ON offer_assets.offer_id = offers.id\n WHERE offers.hash = ?\n ", + "describe": { + "columns": [ + { + "name": "offer_id", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "asset_id", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "royalty", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "is_requested", + "ordinal": 4, + "type_info": "Bool" + }, + { + "name": "description", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "is_sensitive_content", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "is_visible", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "icon_url", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "ticker", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "precision", + "ordinal": 11, + "type_info": "Integer" + }, + { + "name": "kind", + "ordinal": 12, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + true, + true, + true, + false, + false + ] + }, + "hash": "0a9370ba52b140ceae15af6d25c3778baa8e5c4ce10fff5d90fa538895e1f2c0" +} diff --git a/.sqlx/query-0a9b0635b043ed3dd998a33fe214278ff59ddfdbc5fafd25a56b8e67115f1c51.json b/.sqlx/query-0a9b0635b043ed3dd998a33fe214278ff59ddfdbc5fafd25a56b8e67115f1c51.json new file mode 100644 index 000000000..91f3aff01 --- /dev/null +++ b/.sqlx/query-0a9b0635b043ed3dd998a33fe214278ff59ddfdbc5fafd25a56b8e67115f1c51.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT resized_images.data, mime_type\n FROM resized_images \n INNER JOIN files ON files.id = resized_images.file_id\n WHERE files.hash = ? AND kind = ?", + "describe": { + "columns": [ + { + "name": "data", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "mime_type", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + true + ] + }, + "hash": "0a9b0635b043ed3dd998a33fe214278ff59ddfdbc5fafd25a56b8e67115f1c51" +} diff --git a/.sqlx/query-0bd50a041814da8532dde48fd00f056204957e615443d30e0c1ede98af4e5519.json b/.sqlx/query-0bd50a041814da8532dde48fd00f056204957e615443d30e0c1ede98af4e5519.json deleted file mode 100644 index 8ffab97a6..000000000 --- a/.sqlx/query-0bd50a041814da8532dde48fd00f056204957e615443d30e0c1ede98af4e5519.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR IGNORE INTO `offered_coins` (`offer_id`, `coin_id`) VALUES (?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "0bd50a041814da8532dde48fd00f056204957e615443d30e0c1ede98af4e5519" -} diff --git a/.sqlx/query-65c223a91702ea7b5e4e4483f1dd9ae3f3f7f2c42a7776b6ada4e6f51dd25fb6.json b/.sqlx/query-0c294a9cc1e5b1c7bf72691d4824442f254e1be2089b3647bed117f4bd15cfe7.json similarity index 54% rename from .sqlx/query-65c223a91702ea7b5e4e4483f1dd9ae3f3f7f2c42a7776b6ada4e6f51dd25fb6.json rename to .sqlx/query-0c294a9cc1e5b1c7bf72691d4824442f254e1be2089b3647bed117f4bd15cfe7.json index 65484e6fa..adbbd767c 100644 --- a/.sqlx/query-65c223a91702ea7b5e4e4483f1dd9ae3f3f7f2c42a7776b6ada4e6f51dd25fb6.json +++ b/.sqlx/query-0c294a9cc1e5b1c7bf72691d4824442f254e1be2089b3647bed117f4bd15cfe7.json @@ -1,41 +1,51 @@ { "db_name": "SQLite", - "query": "\n SELECT `asset_id`, `name`, `ticker`, `description`, `icon`, `visible`, `fetched`\n FROM `cats`\n WHERE `asset_id` = ?\n ", + "query": "\n SELECT\n hash, kind, name, ticker, precision, icon_url, description,\n is_sensitive_content, is_visible\n FROM assets\n WHERE hash = ?\n ", "describe": { "columns": [ { - "name": "asset_id", + "name": "hash", "ordinal": 0, "type_info": "Blob" }, { - "name": "name", + "name": "kind", "ordinal": 1, - "type_info": "Text" + "type_info": "Integer" }, { - "name": "ticker", + "name": "name", "ordinal": 2, "type_info": "Text" }, { - "name": "description", + "name": "ticker", "ordinal": 3, "type_info": "Text" }, { - "name": "icon", + "name": "precision", "ordinal": 4, - "type_info": "Text" + "type_info": "Integer" }, { - "name": "visible", + "name": "icon_url", "ordinal": 5, - "type_info": "Bool" + "type_info": "Text" }, { - "name": "fetched", + "name": "description", "ordinal": 6, + "type_info": "Text" + }, + { + "name": "is_sensitive_content", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "is_visible", + "ordinal": 8, "type_info": "Bool" } ], @@ -43,14 +53,16 @@ "Right": 1 }, "nullable": [ + false, false, true, true, + false, true, true, false, false ] }, - "hash": "65c223a91702ea7b5e4e4483f1dd9ae3f3f7f2c42a7776b6ada4e6f51dd25fb6" + "hash": "0c294a9cc1e5b1c7bf72691d4824442f254e1be2089b3647bed117f4bd15cfe7" } diff --git a/.sqlx/query-0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078.json b/.sqlx/query-0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078.json deleted file mode 100644 index 52d7f7ae3..000000000 --- a/.sqlx/query-0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM `derivations` WHERE `hardened` = 0", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078" -} diff --git a/.sqlx/query-128ef3d7d903189eea35f141e1fa295f7116bbc3674f64a2ae680aef0fa33256.json b/.sqlx/query-128ef3d7d903189eea35f141e1fa295f7116bbc3674f64a2ae680aef0fa33256.json deleted file mode 100644 index 67f65e1b0..000000000 --- a/.sqlx/query-128ef3d7d903189eea35f141e1fa295f7116bbc3674f64a2ae680aef0fa33256.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `asset_id` FROM `cats` WHERE `fetched` = 0\n LIMIT 1\n ", - "describe": { - "columns": [ - { - "name": "asset_id", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "128ef3d7d903189eea35f141e1fa295f7116bbc3674f64a2ae680aef0fa33256" -} diff --git a/.sqlx/query-fbf2bab0983da7407305d09a38f70ee19845f21d3ae0b8281ce059a98a49fced.json b/.sqlx/query-13a603417d0dfc31a1330a24b57b1a14ab95b9c159618255eaedcb249b39ee86.json similarity index 59% rename from .sqlx/query-fbf2bab0983da7407305d09a38f70ee19845f21d3ae0b8281ce059a98a49fced.json rename to .sqlx/query-13a603417d0dfc31a1330a24b57b1a14ab95b9c159618255eaedcb249b39ee86.json index e575795ad..c94871599 100644 --- a/.sqlx/query-fbf2bab0983da7407305d09a38f70ee19845f21d3ae0b8281ce059a98a49fced.json +++ b/.sqlx/query-13a603417d0dfc31a1330a24b57b1a14ab95b9c159618255eaedcb249b39ee86.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT\n cs.parent_coin_id, cs.puzzle_hash, cs.amount,\n did.parent_parent_coin_id, did.parent_inner_puzzle_hash, did.parent_amount,\n did.launcher_id, did.recovery_list_hash, did.num_verifications_required,\n did.metadata, did.p2_puzzle_hash\n FROM `coin_states` AS cs\n INNER JOIN `did_coins` AS did ON cs.coin_id = did.coin_id\n LEFT JOIN `transaction_spends` ON cs.coin_id = transaction_spends.coin_id\n WHERE did.launcher_id = ?\n AND cs.spent_height IS NULL\n AND cs.created_height IS NOT NULL\n AND cs.transaction_id IS NULL\n AND transaction_spends.transaction_id IS NULL\n ", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, recovery_list_hash, num_verifications_required, metadata\n FROM spendable_coins\n INNER JOIN dids ON dids.asset_id = spendable_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id\n WHERE asset_hash = ?\n ", "describe": { "columns": [ { - "name": "parent_coin_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, @@ -19,42 +19,42 @@ "type_info": "Blob" }, { - "name": "parent_parent_coin_id", + "name": "p2_puzzle_hash", "ordinal": 3, "type_info": "Blob" }, { - "name": "parent_inner_puzzle_hash", + "name": "parent_parent_coin_hash", "ordinal": 4, "type_info": "Blob" }, { - "name": "parent_amount", + "name": "parent_inner_puzzle_hash", "ordinal": 5, "type_info": "Blob" }, { - "name": "launcher_id", + "name": "parent_amount", "ordinal": 6, "type_info": "Blob" }, { - "name": "recovery_list_hash", + "name": "launcher_id", "ordinal": 7, "type_info": "Blob" }, { - "name": "num_verifications_required", + "name": "recovery_list_hash", "ordinal": 8, "type_info": "Blob" }, { - "name": "metadata", + "name": "num_verifications_required", "ordinal": 9, - "type_info": "Blob" + "type_info": "Integer" }, { - "name": "p2_puzzle_hash", + "name": "metadata", "ordinal": 10, "type_info": "Blob" } @@ -70,11 +70,11 @@ false, false, false, - true, false, + true, false, false ] }, - "hash": "fbf2bab0983da7407305d09a38f70ee19845f21d3ae0b8281ce059a98a49fced" + "hash": "13a603417d0dfc31a1330a24b57b1a14ab95b9c159618255eaedcb249b39ee86" } diff --git a/.sqlx/query-14658cf99ae1c0de7ee9ba9f20b2487215a1824d255f5a1bfcf1021ef4ad3bee.json b/.sqlx/query-14658cf99ae1c0de7ee9ba9f20b2487215a1824d255f5a1bfcf1021ef4ad3bee.json deleted file mode 100644 index 20f7005f0..000000000 --- a/.sqlx/query-14658cf99ae1c0de7ee9ba9f20b2487215a1824d255f5a1bfcf1021ef4ad3bee.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT COUNT(*) as count\n FROM coin_states\n INNER JOIN `cat_coins` ON `coin_states`.`coin_id` = `cat_coins`.`coin_id`\n LEFT JOIN transaction_spends ON transaction_spends.coin_id = coin_states.coin_id\n WHERE 1=1 \n AND `cat_coins`.`asset_id` = ?\n AND created_height IS NOT NULL\n AND spent_height IS NULL\n AND coin_states.transaction_id IS NULL\n AND transaction_spends.coin_id IS NULL\n ", - "describe": { - "columns": [ - { - "name": "count", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "14658cf99ae1c0de7ee9ba9f20b2487215a1824d255f5a1bfcf1021ef4ad3bee" -} diff --git a/.sqlx/query-14a48914904875c2bad20f496d80c70a0a06fbfb8cc3b2b141ff266c3f8ce4d8.json b/.sqlx/query-14a48914904875c2bad20f496d80c70a0a06fbfb8cc3b2b141ff266c3f8ce4d8.json new file mode 100644 index 000000000..6d02a0aa5 --- /dev/null +++ b/.sqlx/query-14a48914904875c2bad20f496d80c70a0a06fbfb8cc3b2b141ff266c3f8ce4d8.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT DISTINCT minter_hash, COUNT(*) OVER() AS total_count \n FROM owned_nfts \n ORDER BY minter_hash ASC\n LIMIT ? OFFSET ?", + "describe": { + "columns": [ + { + "name": "minter_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "total_count", + "ordinal": 1, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + false + ] + }, + "hash": "14a48914904875c2bad20f496d80c70a0a06fbfb8cc3b2b141ff266c3f8ce4d8" +} diff --git a/.sqlx/query-de8da54c8ad83b7d81c49b1866d87d733ee46365df08154d151eee1e97b95381.json b/.sqlx/query-159906b7abba41809024015204615663b3b2f1c1c1682fe3e95f899bb4b1f7c2.json similarity index 53% rename from .sqlx/query-de8da54c8ad83b7d81c49b1866d87d733ee46365df08154d151eee1e97b95381.json rename to .sqlx/query-159906b7abba41809024015204615663b3b2f1c1c1682fe3e95f899bb4b1f7c2.json index 651818530..0292532aa 100644 --- a/.sqlx/query-de8da54c8ad83b7d81c49b1866d87d733ee46365df08154d151eee1e97b95381.json +++ b/.sqlx/query-159906b7abba41809024015204615663b3b2f1c1c1682fe3e95f899bb4b1f7c2.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "DELETE FROM `offers`", + "query": "DELETE FROM offers", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "de8da54c8ad83b7d81c49b1866d87d733ee46365df08154d151eee1e97b95381" + "hash": "159906b7abba41809024015204615663b3b2f1c1c1682fe3e95f899bb4b1f7c2" } diff --git a/.sqlx/query-159bba47079ec5a783aa08e73118c1f06a35e7bec0a1836ea2136ca999ac33d7.json b/.sqlx/query-159bba47079ec5a783aa08e73118c1f06a35e7bec0a1836ea2136ca999ac33d7.json new file mode 100644 index 000000000..2d06da898 --- /dev/null +++ b/.sqlx/query-159bba47079ec5a783aa08e73118c1f06a35e7bec0a1836ea2136ca999ac33d7.json @@ -0,0 +1,200 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT \n asset_hash, asset_name, asset_ticker, asset_precision, asset_icon_url,\n asset_description, asset_is_sensitive_content, asset_is_visible,\n collections.hash AS 'collection_hash?', collections.name AS collection_name, \n owned_nfts.minter_hash, owner_hash, metadata, metadata_updater_puzzle_hash,\n royalty_puzzle_hash, royalty_basis_points, data_hash, metadata_hash, license_hash,\n edition_number, edition_total,\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, created_height, spent_height,\n offer_hash AS 'offer_hash?', created_timestamp, spent_timestamp, clawback_expiration_seconds AS 'clawback_timestamp?'\n FROM owned_nfts\n LEFT JOIN collections ON collections.id = owned_nfts.collection_id\n WHERE owned_nfts.asset_hash = ?\n ", + "describe": { + "columns": [ + { + "name": "asset_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "asset_name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "asset_ticker", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "asset_precision", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "asset_icon_url", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "asset_description", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "asset_is_sensitive_content", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "asset_is_visible", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "collection_hash?", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "collection_name", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "minter_hash", + "ordinal": 10, + "type_info": "Blob" + }, + { + "name": "owner_hash", + "ordinal": 11, + "type_info": "Blob" + }, + { + "name": "metadata", + "ordinal": 12, + "type_info": "Blob" + }, + { + "name": "metadata_updater_puzzle_hash", + "ordinal": 13, + "type_info": "Blob" + }, + { + "name": "royalty_puzzle_hash", + "ordinal": 14, + "type_info": "Blob" + }, + { + "name": "royalty_basis_points", + "ordinal": 15, + "type_info": "Integer" + }, + { + "name": "data_hash", + "ordinal": 16, + "type_info": "Blob" + }, + { + "name": "metadata_hash", + "ordinal": 17, + "type_info": "Blob" + }, + { + "name": "license_hash", + "ordinal": 18, + "type_info": "Blob" + }, + { + "name": "edition_number", + "ordinal": 19, + "type_info": "Integer" + }, + { + "name": "edition_total", + "ordinal": 20, + "type_info": "Integer" + }, + { + "name": "parent_coin_hash", + "ordinal": 21, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 22, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 23, + "type_info": "Blob" + }, + { + "name": "p2_puzzle_hash", + "ordinal": 24, + "type_info": "Blob" + }, + { + "name": "created_height", + "ordinal": 25, + "type_info": "Integer" + }, + { + "name": "spent_height", + "ordinal": 26, + "type_info": "Integer" + }, + { + "name": "offer_hash?", + "ordinal": 27, + "type_info": "Blob" + }, + { + "name": "created_timestamp", + "ordinal": 28, + "type_info": "Integer" + }, + { + "name": "spent_timestamp", + "ordinal": 29, + "type_info": "Integer" + }, + { + "name": "clawback_timestamp?", + "ordinal": 30, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true, + true, + false, + true, + true, + false, + false, + false, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + false, + true, + true, + false + ] + }, + "hash": "159bba47079ec5a783aa08e73118c1f06a35e7bec0a1836ea2136ca999ac33d7" +} diff --git a/.sqlx/query-1697370a7e61f088e103f2d8e1977e9ac441d499a1557331b4e29ace0843c08f.json b/.sqlx/query-1697370a7e61f088e103f2d8e1977e9ac441d499a1557331b4e29ace0843c08f.json deleted file mode 100644 index c5cb6a7a4..000000000 --- a/.sqlx/query-1697370a7e61f088e103f2d8e1977e9ac441d499a1557331b4e29ace0843c08f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE `coin_states`\n SET `created_height` = ?, `spent_height` = ?, `transaction_id` = ?\n WHERE `coin_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "1697370a7e61f088e103f2d8e1977e9ac441d499a1557331b4e29ace0843c08f" -} diff --git a/.sqlx/query-16f87160e00064a9692f32cf5afa3bb56b2b9d44dddafba6bdbcdc24a40cfb6a.json b/.sqlx/query-16f87160e00064a9692f32cf5afa3bb56b2b9d44dddafba6bdbcdc24a40cfb6a.json deleted file mode 100644 index b7245bd21..000000000 --- a/.sqlx/query-16f87160e00064a9692f32cf5afa3bb56b2b9d44dddafba6bdbcdc24a40cfb6a.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT `metadata_hash` FROM `nft_coins` WHERE `launcher_id` = ?", - "describe": { - "columns": [ - { - "name": "metadata_hash", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "16f87160e00064a9692f32cf5afa3bb56b2b9d44dddafba6bdbcdc24a40cfb6a" -} diff --git a/.sqlx/query-1703f687f284012d335632b7bb603ea1ba181bd81c918ad4868dae4b19bbcd69.json b/.sqlx/query-1703f687f284012d335632b7bb603ea1ba181bd81c918ad4868dae4b19bbcd69.json new file mode 100644 index 000000000..620a88585 --- /dev/null +++ b/.sqlx/query-1703f687f284012d335632b7bb603ea1ba181bd81c918ad4868dae4b19bbcd69.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, underlying_coin_hash, underlying_delegated_puzzle_hash\n FROM wallet_coins\n INNER JOIN options ON options.asset_id = wallet_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = wallet_coins.coin_id\n WHERE asset_hash = ? AND spent_height IS NULL\n ", + "describe": { + "columns": [ + { + "name": "parent_coin_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "p2_puzzle_hash", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "parent_parent_coin_hash", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "parent_inner_puzzle_hash", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "parent_amount", + "ordinal": 6, + "type_info": "Blob" + }, + { + "name": "launcher_id", + "ordinal": 7, + "type_info": "Blob" + }, + { + "name": "underlying_coin_hash", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "underlying_delegated_puzzle_hash", + "ordinal": 9, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "1703f687f284012d335632b7bb603ea1ba181bd81c918ad4868dae4b19bbcd69" +} diff --git a/.sqlx/query-18d3d050762544ca73050ff24bec4dddf34afffa9ab95e17eaebf2eac7cc1ca6.json b/.sqlx/query-18d3d050762544ca73050ff24bec4dddf34afffa9ab95e17eaebf2eac7cc1ca6.json deleted file mode 100644 index 3804c5d9c..000000000 --- a/.sqlx/query-18d3d050762544ca73050ff24bec4dddf34afffa9ab95e17eaebf2eac7cc1ca6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `cat_coins` (\n `coin_id`,\n `parent_parent_coin_id`,\n `parent_inner_puzzle_hash`,\n `parent_amount`,\n `p2_puzzle_hash`,\n `asset_id`\n )\n VALUES (?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "18d3d050762544ca73050ff24bec4dddf34afffa9ab95e17eaebf2eac7cc1ca6" -} diff --git a/.sqlx/query-193aa911a0d0a4df1258102a595ba45a144f738dfc5896eb8db568d2f5794168.json b/.sqlx/query-193aa911a0d0a4df1258102a595ba45a144f738dfc5896eb8db568d2f5794168.json deleted file mode 100644 index ca1aaafe1..000000000 --- a/.sqlx/query-193aa911a0d0a4df1258102a595ba45a144f738dfc5896eb8db568d2f5794168.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `coin_states` (\n `coin_id`,\n `parent_coin_id`,\n `puzzle_hash`,\n `amount`,\n `created_height`,\n `spent_height`,\n `synced`,\n `transaction_id`\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "193aa911a0d0a4df1258102a595ba45a144f738dfc5896eb8db568d2f5794168" -} diff --git a/.sqlx/query-2698bed1b4a0d3d753b8357d3debdaf0b4f0aed18e288f55c7e0963f187e1500.json b/.sqlx/query-1e8f21fd0c35249917f77752d470d00c658c3951f52ca38d7d3cb68d0143265d.json similarity index 56% rename from .sqlx/query-2698bed1b4a0d3d753b8357d3debdaf0b4f0aed18e288f55c7e0963f187e1500.json rename to .sqlx/query-1e8f21fd0c35249917f77752d470d00c658c3951f52ca38d7d3cb68d0143265d.json index 45a0cb1ad..571b54974 100644 --- a/.sqlx/query-2698bed1b4a0d3d753b8357d3debdaf0b4f0aed18e288f55c7e0963f187e1500.json +++ b/.sqlx/query-1e8f21fd0c35249917f77752d470d00c658c3951f52ca38d7d3cb68d0143265d.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT `asset_id`, `name`, `ticker`, `description`, `icon`, `visible`, `fetched`\n FROM `cats` INDEXED BY `cat_name`\n ORDER BY `visible` DESC, `is_named` DESC, `name` ASC, `asset_id` ASC\n ", + "query": "\n SELECT\n hash, name, icon_url, description, ticker, precision,\n is_visible, is_sensitive_content\n FROM assets\n WHERE assets.kind = 0 AND assets.id != 0\n ORDER BY name ASC\n ", "describe": { "columns": [ { - "name": "asset_id", + "name": "hash", "ordinal": 0, "type_info": "Blob" }, @@ -14,7 +14,7 @@ "type_info": "Text" }, { - "name": "ticker", + "name": "icon_url", "ordinal": 2, "type_info": "Text" }, @@ -24,19 +24,24 @@ "type_info": "Text" }, { - "name": "icon", + "name": "ticker", "ordinal": 4, "type_info": "Text" }, { - "name": "visible", + "name": "precision", "ordinal": 5, - "type_info": "Bool" + "type_info": "Integer" }, { - "name": "fetched", + "name": "is_visible", "ordinal": 6, "type_info": "Bool" + }, + { + "name": "is_sensitive_content", + "ordinal": 7, + "type_info": "Bool" } ], "parameters": { @@ -49,8 +54,9 @@ true, true, false, + false, false ] }, - "hash": "2698bed1b4a0d3d753b8357d3debdaf0b4f0aed18e288f55c7e0963f187e1500" + "hash": "1e8f21fd0c35249917f77752d470d00c658c3951f52ca38d7d3cb68d0143265d" } diff --git a/.sqlx/query-1f02b69aed85e466920cf1890d478d481723d70c83c4370f4c528a574a69e7eb.json b/.sqlx/query-1f02b69aed85e466920cf1890d478d481723d70c83c4370f4c528a574a69e7eb.json deleted file mode 100644 index 1bc7e5bea..000000000 --- a/.sqlx/query-1f02b69aed85e466920cf1890d478d481723d70c83c4370f4c528a574a69e7eb.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount` FROM `coin_states`\n LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id`\n LEFT JOIN `offered_coins` ON `coin_states`.`coin_id` = `offered_coins`.`coin_id`\n LEFT JOIN `offers` ON `offered_coins`.`offer_id` = `offers`.`offer_id`\n WHERE `coin_states`.`spent_height` IS NULL\n AND `transaction_spends`.`coin_id` IS NULL\n AND (`offered_coins`.`coin_id` IS NULL OR `offers`.`status` > 0)\n AND `coin_states`.`transaction_id` IS NULL\n AND `kind` = 1\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "1f02b69aed85e466920cf1890d478d481723d70c83c4370f4c528a574a69e7eb" -} diff --git a/.sqlx/query-20382f9dec9385edf45692693fc58023d693b345002efdeb8ee9ba50a44e3f59.json b/.sqlx/query-20382f9dec9385edf45692693fc58023d693b345002efdeb8ee9ba50a44e3f59.json deleted file mode 100644 index 022d35fba..000000000 --- a/.sqlx/query-20382f9dec9385edf45692693fc58023d693b345002efdeb8ee9ba50a44e3f59.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states`\n WHERE `synced` = 0 AND `created_height` IS NOT NULL\n ORDER BY `spent_height` ASC\n LIMIT ?\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "created_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "spent_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "20382f9dec9385edf45692693fc58023d693b345002efdeb8ee9ba50a44e3f59" -} diff --git a/.sqlx/query-2038867d267903ab3730dee28abc3f09b6c88adda86af655522a11be2ce1e172.json b/.sqlx/query-2038867d267903ab3730dee28abc3f09b6c88adda86af655522a11be2ce1e172.json deleted file mode 100644 index 4b1207130..000000000 --- a/.sqlx/query-2038867d267903ab3730dee28abc3f09b6c88adda86af655522a11be2ce1e172.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `offered_coins`.`offer_id` FROM `offered_coins`\n INNER JOIN `offers` ON `offered_coins`.`offer_id` = `offers`.`offer_id`\n WHERE `coin_id` = ? AND `offers`.`status` = 0\n ", - "describe": { - "columns": [ - { - "name": "offer_id", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "2038867d267903ab3730dee28abc3f09b6c88adda86af655522a11be2ce1e172" -} diff --git a/.sqlx/query-20505bc0166a86dc2be61d1ce71251533e25a0291ce0f05f4f68c70cb6daba94.json b/.sqlx/query-20505bc0166a86dc2be61d1ce71251533e25a0291ce0f05f4f68c70cb6daba94.json deleted file mode 100644 index 823037398..000000000 --- a/.sqlx/query-20505bc0166a86dc2be61d1ce71251533e25a0291ce0f05f4f68c70cb6daba94.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `cats` (\n `asset_id`,\n `name`,\n `ticker`,\n `description`,\n `icon`,\n `visible`,\n `fetched`\n ) VALUES (?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "20505bc0166a86dc2be61d1ce71251533e25a0291ce0f05f4f68c70cb6daba94" -} diff --git a/.sqlx/query-211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281.json b/.sqlx/query-211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281.json deleted file mode 100644 index 5ff6b4b67..000000000 --- a/.sqlx/query-211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM `derivations` WHERE `hardened` = 1", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281" -} diff --git a/.sqlx/query-224faa271697d293d22786efb6651055fada3333fa015b66dc3c718c44260099.json b/.sqlx/query-224faa271697d293d22786efb6651055fada3333fa015b66dc3c718c44260099.json new file mode 100644 index 000000000..707ab7808 --- /dev/null +++ b/.sqlx/query-224faa271697d293d22786efb6651055fada3333fa015b66dc3c718c44260099.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE nfts SET collection_id = (SELECT id FROM collections WHERE hash = ?)\n WHERE asset_id = (SELECT id FROM assets WHERE hash = ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "224faa271697d293d22786efb6651055fada3333fa015b66dc3c718c44260099" +} diff --git a/.sqlx/query-10e105b51b9a1c4a9a92bb85b0d1c1ea21f7e636299e33d295c4d08bcc78cdac.json b/.sqlx/query-23a3a5e21db7039a455a7fae4ad0a5eb866a5176fb5df024ac8b0897f82d3008.json similarity index 56% rename from .sqlx/query-10e105b51b9a1c4a9a92bb85b0d1c1ea21f7e636299e33d295c4d08bcc78cdac.json rename to .sqlx/query-23a3a5e21db7039a455a7fae4ad0a5eb866a5176fb5df024ac8b0897f82d3008.json index 29351fad7..433fcc806 100644 --- a/.sqlx/query-10e105b51b9a1c4a9a92bb85b0d1c1ea21f7e636299e33d295c4d08bcc78cdac.json +++ b/.sqlx/query-23a3a5e21db7039a455a7fae4ad0a5eb866a5176fb5df024ac8b0897f82d3008.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "SELECT `collection_id` FROM `collections`", + "query": "SELECT hash FROM p2_puzzles WHERE kind = 0", "describe": { "columns": [ { - "name": "collection_id", + "name": "hash", "ordinal": 0, "type_info": "Blob" } @@ -16,5 +16,5 @@ false ] }, - "hash": "10e105b51b9a1c4a9a92bb85b0d1c1ea21f7e636299e33d295c4d08bcc78cdac" + "hash": "23a3a5e21db7039a455a7fae4ad0a5eb866a5176fb5df024ac8b0897f82d3008" } diff --git a/.sqlx/query-23e500a57f9d0a19a1ffa1c1b5d34a7bcdcf5a8898ef749e735e406e64485971.json b/.sqlx/query-23e500a57f9d0a19a1ffa1c1b5d34a7bcdcf5a8898ef749e735e406e64485971.json new file mode 100644 index 000000000..06a772cb2 --- /dev/null +++ b/.sqlx/query-23e500a57f9d0a19a1ffa1c1b5d34a7bcdcf5a8898ef749e735e406e64485971.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) AS count FROM coins\n WHERE asset_id IS NOT NULL\n AND (spent_height IS NULL OR is_children_synced = TRUE)\n ", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "23e500a57f9d0a19a1ffa1c1b5d34a7bcdcf5a8898ef749e735e406e64485971" +} diff --git a/.sqlx/query-23e9f0827989fb19576fb9bd37cd797f770c0126f4b0c9ee86c7c39a78529497.json b/.sqlx/query-23e9f0827989fb19576fb9bd37cd797f770c0126f4b0c9ee86c7c39a78529497.json deleted file mode 100644 index d9d9785fa..000000000 --- a/.sqlx/query-23e9f0827989fb19576fb9bd37cd797f770c0126f4b0c9ee86c7c39a78529497.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `synthetic_key`\n FROM `derivations`\n WHERE `p2_puzzle_hash` = ?\n ", - "describe": { - "columns": [ - { - "name": "synthetic_key", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "23e9f0827989fb19576fb9bd37cd797f770c0126f4b0c9ee86c7c39a78529497" -} diff --git a/.sqlx/query-24ea4bd05a6542302f7baa5a41b78acde5237581748cdea39893cc4ae99b1998.json b/.sqlx/query-24ea4bd05a6542302f7baa5a41b78acde5237581748cdea39893cc4ae99b1998.json deleted file mode 100644 index 318072cb5..000000000 --- a/.sqlx/query-24ea4bd05a6542302f7baa5a41b78acde5237581748cdea39893cc4ae99b1998.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE `collections` SET\n `collection_id` = ?,\n `did_id` = ?,\n `metadata_collection_id` = ?,\n `visible` = ?,\n `name` = ?,\n `icon` = ?\n WHERE `collection_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "24ea4bd05a6542302f7baa5a41b78acde5237581748cdea39893cc4ae99b1998" -} diff --git a/.sqlx/query-25d3891073241eac0d0684d473dd7009126954db3031a15e30f44445364cd06e.json b/.sqlx/query-25d3891073241eac0d0684d473dd7009126954db3031a15e30f44445364cd06e.json new file mode 100644 index 000000000..831c641e1 --- /dev/null +++ b/.sqlx/query-25d3891073241eac0d0684d473dd7009126954db3031a15e30f44445364cd06e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO p2_puzzles (hash, kind) VALUES (?, 0);\n\n INSERT OR IGNORE INTO public_keys (p2_puzzle_id, is_hardened, derivation_index, key)\n VALUES ((SELECT id FROM p2_puzzles WHERE hash = ?), ?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "25d3891073241eac0d0684d473dd7009126954db3031a15e30f44445364cd06e" +} diff --git a/.sqlx/query-26f35685c520800fcac9aa39af5eda01d241729c617dcefb200e4490bafb75ef.json b/.sqlx/query-26f35685c520800fcac9aa39af5eda01d241729c617dcefb200e4490bafb75ef.json deleted file mode 100644 index 300d3fbe0..000000000 --- a/.sqlx/query-26f35685c520800fcac9aa39af5eda01d241729c617dcefb200e4490bafb75ef.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE `coin_states` SET `synced` = 1, `hint` = ?, `kind` = ? WHERE `coin_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "26f35685c520800fcac9aa39af5eda01d241729c617dcefb200e4490bafb75ef" -} diff --git a/.sqlx/query-275b72aec6c3642621c64403265debc939d2d2e4c3fc788ce2a492eb5c2840d6.json b/.sqlx/query-275b72aec6c3642621c64403265debc939d2d2e4c3fc788ce2a492eb5c2840d6.json deleted file mode 100644 index b844f1f48..000000000 --- a/.sqlx/query-275b72aec6c3642621c64403265debc939d2d2e4c3fc788ce2a492eb5c2840d6.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `name` FROM `future_did_names`\n WHERE `launcher_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "275b72aec6c3642621c64403265debc939d2d2e4c3fc788ce2a492eb5c2840d6" -} diff --git a/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json b/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json new file mode 100644 index 000000000..f6bc5bea6 --- /dev/null +++ b/.sqlx/query-29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT key, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds\n FROM p2_puzzles\n INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id\n INNER JOIN public_keys ON public_keys.p2_puzzle_id IN (\n SELECT id FROM p2_puzzles\n WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds)\n OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds)\n LIMIT 1\n )\n WHERE p2_puzzles.hash = ?\n ", + "describe": { + "columns": [ + { + "name": "key", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "sender_puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "receiver_puzzle_hash", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "expiration_seconds", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "29948fd389ee08d87c21423ae16411721ec4b428a5b0739300f927b609e985d8" +} diff --git a/.sqlx/query-2b05eb244b5a2022c3b16cc13d1cb8a4bd59324eb1d37c4301dc7dda34a3159c.json b/.sqlx/query-2b05eb244b5a2022c3b16cc13d1cb8a4bd59324eb1d37c4301dc7dda34a3159c.json new file mode 100644 index 000000000..a5e049871 --- /dev/null +++ b/.sqlx/query-2b05eb244b5a2022c3b16cc13d1cb8a4bd59324eb1d37c4301dc7dda34a3159c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM files WHERE hash = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "2b05eb244b5a2022c3b16cc13d1cb8a4bd59324eb1d37c4301dc7dda34a3159c" +} diff --git a/.sqlx/query-2b20bc5b625d46f0cf5b886b229fcf4caa0fa983bb207521dc91fc523c0eedb6.json b/.sqlx/query-2b20bc5b625d46f0cf5b886b229fcf4caa0fa983bb207521dc91fc523c0eedb6.json deleted file mode 100644 index 5448a6f3b..000000000 --- a/.sqlx/query-2b20bc5b625d46f0cf5b886b229fcf4caa0fa983bb207521dc91fc523c0eedb6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "REPLACE INTO `nft_data` (`hash`, `data`, `mime_type`, `hash_matches`) VALUES (?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "2b20bc5b625d46f0cf5b886b229fcf4caa0fa983bb207521dc91fc523c0eedb6" -} diff --git a/.sqlx/query-95cd0c18c584c6bfa4e327906d0711a8dbb63d744e569f44e52e0cd9686ef3ea.json b/.sqlx/query-2b625086cd11f5ccf765d757e5c68c866f4e1fec9e27a805e384847a81019a0f.json similarity index 68% rename from .sqlx/query-95cd0c18c584c6bfa4e327906d0711a8dbb63d744e569f44e52e0cd9686ef3ea.json rename to .sqlx/query-2b625086cd11f5ccf765d757e5c68c866f4e1fec9e27a805e384847a81019a0f.json index dc0d8f86d..add01c7b9 100644 --- a/.sqlx/query-95cd0c18c584c6bfa4e327906d0711a8dbb63d744e569f44e52e0cd9686ef3ea.json +++ b/.sqlx/query-2b625086cd11f5ccf765d757e5c68c866f4e1fec9e27a805e384847a81019a0f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT * FROM `offers` WHERE `offer_id` = ?", + "query": "SELECT\n hash as offer_id,\n encoded_offer,\n fee,\n status,\n expiration_height,\n expiration_timestamp,\n inserted_timestamp\n FROM offers WHERE hash = ?", "describe": { "columns": [ { @@ -14,29 +14,29 @@ "type_info": "Text" }, { - "name": "expiration_height", + "name": "fee", "ordinal": 2, - "type_info": "Integer" + "type_info": "Blob" }, { - "name": "expiration_timestamp", + "name": "status", "ordinal": 3, - "type_info": "Blob" + "type_info": "Integer" }, { - "name": "fee", + "name": "expiration_height", "ordinal": 4, - "type_info": "Blob" + "type_info": "Integer" }, { - "name": "status", + "name": "expiration_timestamp", "ordinal": 5, "type_info": "Integer" }, { "name": "inserted_timestamp", "ordinal": 6, - "type_info": "Blob" + "type_info": "Integer" } ], "parameters": { @@ -45,12 +45,12 @@ "nullable": [ false, false, - true, - true, false, false, + true, + true, false ] }, - "hash": "95cd0c18c584c6bfa4e327906d0711a8dbb63d744e569f44e52e0cd9686ef3ea" + "hash": "2b625086cd11f5ccf765d757e5c68c866f4e1fec9e27a805e384847a81019a0f" } diff --git a/.sqlx/query-2b692c3f2917ae7bdf42da1abc2d309a00429c1eeede71b202673261f94a5dbd.json b/.sqlx/query-2b692c3f2917ae7bdf42da1abc2d309a00429c1eeede71b202673261f94a5dbd.json new file mode 100644 index 000000000..2da7dbd44 --- /dev/null +++ b/.sqlx/query-2b692c3f2917ae7bdf42da1abc2d309a00429c1eeede71b202673261f94a5dbd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE file_uris\n SET failed_attempts = failed_attempts + 1, last_checked_timestamp = unixepoch()\n WHERE file_id = (SELECT id FROM files WHERE hash = ?) AND uri = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "2b692c3f2917ae7bdf42da1abc2d309a00429c1eeede71b202673261f94a5dbd" +} diff --git a/.sqlx/query-2df1ebac13f87f697a9cdc5dee90f56c2bead83800519aaf6195a15d2333035a.json b/.sqlx/query-2df1ebac13f87f697a9cdc5dee90f56c2bead83800519aaf6195a15d2333035a.json new file mode 100644 index 000000000..4cc18366c --- /dev/null +++ b/.sqlx/query-2df1ebac13f87f697a9cdc5dee90f56c2bead83800519aaf6195a15d2333035a.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, underlying_coin_hash, underlying_delegated_puzzle_hash\n FROM spendable_coins\n INNER JOIN options ON options.asset_id = spendable_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id\n WHERE coin_hash = ?\n ", + "describe": { + "columns": [ + { + "name": "parent_coin_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "p2_puzzle_hash", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "parent_parent_coin_hash", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "parent_inner_puzzle_hash", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "parent_amount", + "ordinal": 6, + "type_info": "Blob" + }, + { + "name": "launcher_id", + "ordinal": 7, + "type_info": "Blob" + }, + { + "name": "underlying_coin_hash", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "underlying_delegated_puzzle_hash", + "ordinal": 9, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "2df1ebac13f87f697a9cdc5dee90f56c2bead83800519aaf6195a15d2333035a" +} diff --git a/.sqlx/query-2e39146351acfb22a8949733c22b4549dae27f804285e6b9108dd376d2fbc121.json b/.sqlx/query-2e39146351acfb22a8949733c22b4549dae27f804285e6b9108dd376d2fbc121.json new file mode 100644 index 000000000..a33468c85 --- /dev/null +++ b/.sqlx/query-2e39146351acfb22a8949733c22b4549dae27f804285e6b9108dd376d2fbc121.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM assets WHERE id != 0;\n DELETE FROM collections WHERE id != 0;\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "2e39146351acfb22a8949733c22b4549dae27f804285e6b9108dd376d2fbc121" +} diff --git a/.sqlx/query-fe0f5937a4656a93de70df4db1c8e1647c1023b3132f913d287097337e7df483.json b/.sqlx/query-2eefe8bb34ccd11b98116fac075f15ea2b6fca53a1a4779a509fc6dcbc27150c.json similarity index 66% rename from .sqlx/query-fe0f5937a4656a93de70df4db1c8e1647c1023b3132f913d287097337e7df483.json rename to .sqlx/query-2eefe8bb34ccd11b98116fac075f15ea2b6fca53a1a4779a509fc6dcbc27150c.json index 8c3f39ab6..0a13ee68d 100644 --- a/.sqlx/query-fe0f5937a4656a93de70df4db1c8e1647c1023b3132f913d287097337e7df483.json +++ b/.sqlx/query-2eefe8bb34ccd11b98116fac075f15ea2b6fca53a1a4779a509fc6dcbc27150c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT * FROM `offers` INDEXED BY `offer_timestamp` ORDER BY `inserted_timestamp` DESC", + "query": "SELECT\n hash as offer_id,\n encoded_offer,\n fee,\n status,\n expiration_height,\n expiration_timestamp,\n inserted_timestamp\n FROM offers \n WHERE status = ? OR ? IS NULL", "describe": { "columns": [ { @@ -14,43 +14,43 @@ "type_info": "Text" }, { - "name": "expiration_height", + "name": "fee", "ordinal": 2, - "type_info": "Integer" + "type_info": "Blob" }, { - "name": "expiration_timestamp", + "name": "status", "ordinal": 3, - "type_info": "Blob" + "type_info": "Integer" }, { - "name": "fee", + "name": "expiration_height", "ordinal": 4, - "type_info": "Blob" + "type_info": "Integer" }, { - "name": "status", + "name": "expiration_timestamp", "ordinal": 5, "type_info": "Integer" }, { "name": "inserted_timestamp", "ordinal": 6, - "type_info": "Blob" + "type_info": "Integer" } ], "parameters": { - "Right": 0 + "Right": 2 }, "nullable": [ false, false, - true, - true, false, false, + true, + true, false ] }, - "hash": "fe0f5937a4656a93de70df4db1c8e1647c1023b3132f913d287097337e7df483" + "hash": "2eefe8bb34ccd11b98116fac075f15ea2b6fca53a1a4779a509fc6dcbc27150c" } diff --git a/.sqlx/query-c4dac44b028c930fbf91d0459a8ba23c5d97ac61b9bc649fe7fa2f4881a2ded7.json b/.sqlx/query-2fe66f59631c17cb77d35843bc7e866e1be06c7e4b6245eb7df8c47a7a13d6ed.json similarity index 65% rename from .sqlx/query-c4dac44b028c930fbf91d0459a8ba23c5d97ac61b9bc649fe7fa2f4881a2ded7.json rename to .sqlx/query-2fe66f59631c17cb77d35843bc7e866e1be06c7e4b6245eb7df8c47a7a13d6ed.json index 8dceb16ce..3ba4fd6ee 100644 --- a/.sqlx/query-c4dac44b028c930fbf91d0459a8ba23c5d97ac61b9bc649fe7fa2f4881a2ded7.json +++ b/.sqlx/query-2fe66f59631c17cb77d35843bc7e866e1be06c7e4b6245eb7df8c47a7a13d6ed.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT\n `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`,\n `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`,\n `launcher_id`, `metadata`, `metadata_updater_puzzle_hash`, `current_owner`,\n `royalty_puzzle_hash`, `royalty_ten_thousandths`, `p2_puzzle_hash`\n FROM `nft_coins`\n INNER JOIN `coin_states` INDEXED BY `coin_height` ON `nft_coins`.`coin_id` = `coin_states`.`coin_id`\n WHERE `coin_states`.`coin_id` = ?\n ", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, metadata, metadata_updater_puzzle_hash,\n owner_hash, royalty_puzzle_hash, royalty_basis_points\n FROM spendable_coins\n INNER JOIN nfts ON nfts.asset_id = spendable_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id\n WHERE coin_hash = ?\n ", "describe": { "columns": [ { - "name": "parent_coin_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, @@ -19,54 +19,54 @@ "type_info": "Blob" }, { - "name": "parent_parent_coin_id", + "name": "p2_puzzle_hash", "ordinal": 3, "type_info": "Blob" }, { - "name": "parent_inner_puzzle_hash", + "name": "parent_parent_coin_hash", "ordinal": 4, "type_info": "Blob" }, { - "name": "parent_amount", + "name": "parent_inner_puzzle_hash", "ordinal": 5, "type_info": "Blob" }, { - "name": "launcher_id", + "name": "parent_amount", "ordinal": 6, "type_info": "Blob" }, { - "name": "metadata", + "name": "launcher_id", "ordinal": 7, "type_info": "Blob" }, { - "name": "metadata_updater_puzzle_hash", + "name": "metadata", "ordinal": 8, "type_info": "Blob" }, { - "name": "current_owner", + "name": "metadata_updater_puzzle_hash", "ordinal": 9, "type_info": "Blob" }, { - "name": "royalty_puzzle_hash", + "name": "owner_hash", "ordinal": 10, "type_info": "Blob" }, { - "name": "royalty_ten_thousandths", + "name": "royalty_puzzle_hash", "ordinal": 11, - "type_info": "Integer" + "type_info": "Blob" }, { - "name": "p2_puzzle_hash", + "name": "royalty_basis_points", "ordinal": 12, - "type_info": "Blob" + "type_info": "Integer" } ], "parameters": { @@ -82,11 +82,11 @@ false, false, false, - true, false, + true, false, false ] }, - "hash": "c4dac44b028c930fbf91d0459a8ba23c5d97ac61b9bc649fe7fa2f4881a2ded7" + "hash": "2fe66f59631c17cb77d35843bc7e866e1be06c7e4b6245eb7df8c47a7a13d6ed" } diff --git a/.sqlx/query-30a898b6a469141ebdc0093536dd8003235153dcfba83e47d8b35a7888a0fc66.json b/.sqlx/query-30a898b6a469141ebdc0093536dd8003235153dcfba83e47d8b35a7888a0fc66.json deleted file mode 100644 index 89105b168..000000000 --- a/.sqlx/query-30a898b6a469141ebdc0093536dd8003235153dcfba83e47d8b35a7888a0fc66.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT COUNT(*) AS `synced`\n FROM `coin_states`\n WHERE `synced` = 1\n ", - "describe": { - "columns": [ - { - "name": "synced", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "30a898b6a469141ebdc0093536dd8003235153dcfba83e47d8b35a7888a0fc66" -} diff --git a/.sqlx/query-8006fbdcf0e72b03dfd11c7d5a4a63aaa446a546284fcc67a70106303d8e1829.json b/.sqlx/query-31e0c1b1f756ce4521d90dabfc47bce39420af9ddd6a47586ab2f521d1243600.json similarity index 54% rename from .sqlx/query-8006fbdcf0e72b03dfd11c7d5a4a63aaa446a546284fcc67a70106303d8e1829.json rename to .sqlx/query-31e0c1b1f756ce4521d90dabfc47bce39420af9ddd6a47586ab2f521d1243600.json index 36a42dfd7..1ea3d451c 100644 --- a/.sqlx/query-8006fbdcf0e72b03dfd11c7d5a4a63aaa446a546284fcc67a70106303d8e1829.json +++ b/.sqlx/query-31e0c1b1f756ce4521d90dabfc47bce39420af9ddd6a47586ab2f521d1243600.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "SELECT `data_hash` FROM `nft_coins` WHERE `launcher_id` = ?", + "query": "SELECT data FROM files WHERE hash = ?", "describe": { "columns": [ { - "name": "data_hash", + "name": "data", "ordinal": 0, "type_info": "Blob" } @@ -16,5 +16,5 @@ true ] }, - "hash": "8006fbdcf0e72b03dfd11c7d5a4a63aaa446a546284fcc67a70106303d8e1829" + "hash": "31e0c1b1f756ce4521d90dabfc47bce39420af9ddd6a47586ab2f521d1243600" } diff --git a/.sqlx/query-322ec020e29d5523ca308fb0029d1e1633a66f50125404b90da98ac5bec89086.json b/.sqlx/query-322ec020e29d5523ca308fb0029d1e1633a66f50125404b90da98ac5bec89086.json new file mode 100644 index 000000000..4d3c0d4ed --- /dev/null +++ b/.sqlx/query-322ec020e29d5523ca308fb0029d1e1633a66f50125404b90da98ac5bec89086.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE rust_migrations SET version = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "322ec020e29d5523ca308fb0029d1e1633a66f50125404b90da98ac5bec89086" +} diff --git a/.sqlx/query-32da3c0d8d84766a57e4b992d4ff4767500966b7e5d13ec385dca0028e0cf7fa.json b/.sqlx/query-32da3c0d8d84766a57e4b992d4ff4767500966b7e5d13ec385dca0028e0cf7fa.json deleted file mode 100644 index 3d0a70546..000000000 --- a/.sqlx/query-32da3c0d8d84766a57e4b992d4ff4767500966b7e5d13ec385dca0028e0cf7fa.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n `transaction_id`,\n `aggregated_signature`\n FROM `transactions`\n WHERE `submitted_at` IS NULL OR `submitted_at` <= ?\n ", - "describe": { - "columns": [ - { - "name": "transaction_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "aggregated_signature", - "ordinal": 1, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false - ] - }, - "hash": "32da3c0d8d84766a57e4b992d4ff4767500966b7e5d13ec385dca0028e0cf7fa" -} diff --git a/.sqlx/query-34ac3069ef637b91e7bc06b1dc9b779cb34a573befc9027418d4791fa381375d.json b/.sqlx/query-34ac3069ef637b91e7bc06b1dc9b779cb34a573befc9027418d4791fa381375d.json deleted file mode 100644 index be0fce4ae..000000000 --- a/.sqlx/query-34ac3069ef637b91e7bc06b1dc9b779cb34a573befc9027418d4791fa381375d.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `p2_puzzle_hash`\n FROM `derivations`\n ORDER BY `index` ASC, `hardened` ASC\n ", - "describe": { - "columns": [ - { - "name": "p2_puzzle_hash", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "34ac3069ef637b91e7bc06b1dc9b779cb34a573befc9027418d4791fa381375d" -} diff --git a/.sqlx/query-35e1d67c92d373325db671e3a21d7d102115afd0adbf2e8fb279f33556f95e9d.json b/.sqlx/query-35e1d67c92d373325db671e3a21d7d102115afd0adbf2e8fb279f33556f95e9d.json new file mode 100644 index 000000000..ace8fabed --- /dev/null +++ b/.sqlx/query-35e1d67c92d373325db671e3a21d7d102115afd0adbf2e8fb279f33556f95e9d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM mempool_items;\n UPDATE blocks SET is_peak = FALSE WHERE is_peak = TRUE;\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "35e1d67c92d373325db671e3a21d7d102115afd0adbf2e8fb279f33556f95e9d" +} diff --git a/.sqlx/query-37bd891013a2e43f4daaf084b0b21f535e373a878e2a8acc8a15e17cf66e1dea.json b/.sqlx/query-37bd891013a2e43f4daaf084b0b21f535e373a878e2a8acc8a15e17cf66e1dea.json deleted file mode 100644 index be813523a..000000000 --- a/.sqlx/query-37bd891013a2e43f4daaf084b0b21f535e373a878e2a8acc8a15e17cf66e1dea.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO `offer_nfts` (\n `offer_id`, `requested`, `launcher_id`,\n `royalty_puzzle_hash`, `royalty_ten_thousandths`,\n `name`, `thumbnail`, `thumbnail_mime_type`\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "37bd891013a2e43f4daaf084b0b21f535e373a878e2a8acc8a15e17cf66e1dea" -} diff --git a/.sqlx/query-38e31984cf599af11b261493d8196aa6c9efc623610bdbc841b944588c10cf04.json b/.sqlx/query-38e31984cf599af11b261493d8196aa6c9efc623610bdbc841b944588c10cf04.json deleted file mode 100644 index cfe4a628f..000000000 --- a/.sqlx/query-38e31984cf599af11b261493d8196aa6c9efc623610bdbc841b944588c10cf04.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM `coin_states`;\n DELETE FROM `transactions`;\n DELETE FROM `peaks`;\n DELETE FROM `cats`;\n DELETE FROM `future_did_names`;\n DELETE FROM `collections`;\n DELETE FROM `nft_data`;\n DELETE FROM `nft_uris`;\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "38e31984cf599af11b261493d8196aa6c9efc623610bdbc841b944588c10cf04" -} diff --git a/.sqlx/query-d8df76631005b43bc5870a2337d8e1d44beaab6b0f6f9d0744f449967342b517.json b/.sqlx/query-39484626066ffa2cb1e7c8822230c84736678f055ddde0cff9ddc14da32dc6b5.json similarity index 58% rename from .sqlx/query-d8df76631005b43bc5870a2337d8e1d44beaab6b0f6f9d0744f449967342b517.json rename to .sqlx/query-39484626066ffa2cb1e7c8822230c84736678f055ddde0cff9ddc14da32dc6b5.json index 8722f1f53..64dbc3edd 100644 --- a/.sqlx/query-d8df76631005b43bc5870a2337d8e1d44beaab6b0f6f9d0744f449967342b517.json +++ b/.sqlx/query-39484626066ffa2cb1e7c8822230c84736678f055ddde0cff9ddc14da32dc6b5.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states` WHERE `coin_id` = ?\n ", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, created_height, spent_height,\n (asset_id IS NULL) AS is_asset_unsynced,\n (spent_height IS NOT NULL AND is_children_synced = FALSE) AS is_children_unsynced\n FROM coins\n WHERE asset_id IS NULL OR (spent_height IS NOT NULL AND is_children_synced = FALSE)\n LIMIT ?\n ", "describe": { "columns": [ { - "name": "parent_coin_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, @@ -29,23 +29,13 @@ "type_info": "Integer" }, { - "name": "transaction_id", + "name": "is_asset_unsynced", "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, "type_info": "Integer" }, { - "name": "spent_unixtime", - "ordinal": 8, + "name": "is_children_unsynced", + "ordinal": 6, "type_info": "Integer" } ], @@ -58,11 +48,9 @@ false, true, true, - true, false, - true, - true + false ] }, - "hash": "d8df76631005b43bc5870a2337d8e1d44beaab6b0f6f9d0744f449967342b517" + "hash": "39484626066ffa2cb1e7c8822230c84736678f055ddde0cff9ddc14da32dc6b5" } diff --git a/.sqlx/query-39eff83a71ed98f701821f0bfe822e78b097fdba91a5a409924a59c6ccbd3ffd.json b/.sqlx/query-39eff83a71ed98f701821f0bfe822e78b097fdba91a5a409924a59c6ccbd3ffd.json new file mode 100644 index 000000000..33eab1259 --- /dev/null +++ b/.sqlx/query-39eff83a71ed98f701821f0bfe822e78b097fdba91a5a409924a59c6ccbd3ffd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE file_uris \n SET failed_attempts = 0, last_checked_timestamp = NULL\n FROM files\n WHERE file_uris.file_id = files.id AND file_uris.uri = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "39eff83a71ed98f701821f0bfe822e78b097fdba91a5a409924a59c6ccbd3ffd" +} diff --git a/.sqlx/query-3a1c1141a639a8e7ac636507419d24137d7601848792f519766bbeb15557bbd6.json b/.sqlx/query-3a1c1141a639a8e7ac636507419d24137d7601848792f519766bbeb15557bbd6.json new file mode 100644 index 000000000..fb5368060 --- /dev/null +++ b/.sqlx/query-3a1c1141a639a8e7ac636507419d24137d7601848792f519766bbeb15557bbd6.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO mempool_items (hash, aggregated_signature, fee) VALUES (?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "3a1c1141a639a8e7ac636507419d24137d7601848792f519766bbeb15557bbd6" +} diff --git a/.sqlx/query-3b048718fa92886636e58a225c03d29d0fc87f8187bf2c856aed6e324cebbc73.json b/.sqlx/query-3b048718fa92886636e58a225c03d29d0fc87f8187bf2c856aed6e324cebbc73.json deleted file mode 100644 index b8f01c24a..000000000 --- a/.sqlx/query-3b048718fa92886636e58a225c03d29d0fc87f8187bf2c856aed6e324cebbc73.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states`\n INNER JOIN `nft_coins` ON `coin_states`.coin_id = `nft_coins`.coin_id\n WHERE `launcher_id` = ?\n AND `spent_height` IS NULL\n AND `created_height` IS NOT NULL\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "spent_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "3b048718fa92886636e58a225c03d29d0fc87f8187bf2c856aed6e324cebbc73" -} diff --git a/.sqlx/query-3b1eab15a2414f6edb8a5ea98a4e78f4145959c20b1854ce867af247a509327f.json b/.sqlx/query-3b1eab15a2414f6edb8a5ea98a4e78f4145959c20b1854ce867af247a509327f.json deleted file mode 100644 index 5a9aaf98a..000000000 --- a/.sqlx/query-3b1eab15a2414f6edb8a5ea98a4e78f4145959c20b1854ce867af247a509327f.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT DISTINCT `created_height`\n FROM `coin_states`\n WHERE `created_unixtime` IS NULL\n AND `created_height` IS NOT NULL\n ORDER BY `created_height` DESC \n LIMIT ?;\n ", - "describe": { - "columns": [ - { - "name": "created_height", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "3b1eab15a2414f6edb8a5ea98a4e78f4145959c20b1854ce867af247a509327f" -} diff --git a/.sqlx/query-3b5262f9282bc0d5f4e87b77b5be79de3761b5b4f06c315bc2edc54beb3c18ba.json b/.sqlx/query-3b5262f9282bc0d5f4e87b77b5be79de3761b5b4f06c315bc2edc54beb3c18ba.json new file mode 100644 index 000000000..4d9c04db8 --- /dev/null +++ b/.sqlx/query-3b5262f9282bc0d5f4e87b77b5be79de3761b5b4f06c315bc2edc54beb3c18ba.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT hash, aggregated_signature, fee, submitted_timestamp\n FROM mempool_items\n ORDER BY submitted_timestamp DESC, hash ASC\n ", + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "aggregated_signature", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "fee", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "submitted_timestamp", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "3b5262f9282bc0d5f4e87b77b5be79de3761b5b4f06c315bc2edc54beb3c18ba" +} diff --git a/.sqlx/query-d2de3c63e1eb71899f0635182fe94ea68e9fbe9caa5bf25c8873c3e82262ade5.json b/.sqlx/query-3bca02de2bd877d0fa2c2e6ba91864012485c473a94ab84ae9066e63ed434db1.json similarity index 57% rename from .sqlx/query-d2de3c63e1eb71899f0635182fe94ea68e9fbe9caa5bf25c8873c3e82262ade5.json rename to .sqlx/query-3bca02de2bd877d0fa2c2e6ba91864012485c473a94ab84ae9066e63ed434db1.json index 28ec57a75..fa0d532c2 100644 --- a/.sqlx/query-d2de3c63e1eb71899f0635182fe94ea68e9fbe9caa5bf25c8873c3e82262ade5.json +++ b/.sqlx/query-3bca02de2bd877d0fa2c2e6ba91864012485c473a94ab84ae9066e63ed434db1.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT `kind`\n FROM `coin_states`\n WHERE `coin_id` = ?\n ", + "query": "SELECT kind FROM assets WHERE hash = ?", "describe": { "columns": [ { @@ -16,5 +16,5 @@ false ] }, - "hash": "d2de3c63e1eb71899f0635182fe94ea68e9fbe9caa5bf25c8873c3e82262ade5" + "hash": "3bca02de2bd877d0fa2c2e6ba91864012485c473a94ab84ae9066e63ed434db1" } diff --git a/.sqlx/query-05bdd19eb31d4052f22f55021108733659fa1b636259c3aa36e10a823404c5fa.json b/.sqlx/query-3f591f559e03b4d7d22b1e01c1c483fc35d4fe729876b067edd27998ece72224.json similarity index 60% rename from .sqlx/query-05bdd19eb31d4052f22f55021108733659fa1b636259c3aa36e10a823404c5fa.json rename to .sqlx/query-3f591f559e03b4d7d22b1e01c1c483fc35d4fe729876b067edd27998ece72224.json index f85011c4a..517abf252 100644 --- a/.sqlx/query-05bdd19eb31d4052f22f55021108733659fa1b636259c3aa36e10a823404c5fa.json +++ b/.sqlx/query-3f591f559e03b4d7d22b1e01c1c483fc35d4fe729876b067edd27998ece72224.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT\n `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`,\n `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`,\n `launcher_id`, `metadata`, `metadata_updater_puzzle_hash`, `current_owner`,\n `royalty_puzzle_hash`, `royalty_ten_thousandths`, `p2_puzzle_hash`\n FROM `nft_coins` INDEXED BY `nft_launcher_id`\n INNER JOIN `coin_states` ON `nft_coins`.`coin_id` = `coin_states`.`coin_id`\n LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id`\n WHERE `launcher_id` = ?\n AND `spent_height` IS NULL\n AND `transaction_spends`.`transaction_id` IS NULL\n ", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, metadata, metadata_updater_puzzle_hash,\n owner_hash, royalty_puzzle_hash, royalty_basis_points\n FROM wallet_coins\n INNER JOIN nfts ON nfts.asset_id = wallet_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = wallet_coins.coin_id\n WHERE asset_hash = ? AND spent_height IS NULL\n ", "describe": { "columns": [ { - "name": "parent_coin_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, @@ -19,54 +19,54 @@ "type_info": "Blob" }, { - "name": "parent_parent_coin_id", + "name": "p2_puzzle_hash", "ordinal": 3, "type_info": "Blob" }, { - "name": "parent_inner_puzzle_hash", + "name": "parent_parent_coin_hash", "ordinal": 4, "type_info": "Blob" }, { - "name": "parent_amount", + "name": "parent_inner_puzzle_hash", "ordinal": 5, "type_info": "Blob" }, { - "name": "launcher_id", + "name": "parent_amount", "ordinal": 6, "type_info": "Blob" }, { - "name": "metadata", + "name": "launcher_id", "ordinal": 7, "type_info": "Blob" }, { - "name": "metadata_updater_puzzle_hash", + "name": "metadata", "ordinal": 8, "type_info": "Blob" }, { - "name": "current_owner", + "name": "metadata_updater_puzzle_hash", "ordinal": 9, "type_info": "Blob" }, { - "name": "royalty_puzzle_hash", + "name": "owner_hash", "ordinal": 10, "type_info": "Blob" }, { - "name": "royalty_ten_thousandths", + "name": "royalty_puzzle_hash", "ordinal": 11, - "type_info": "Integer" + "type_info": "Blob" }, { - "name": "p2_puzzle_hash", + "name": "royalty_basis_points", "ordinal": 12, - "type_info": "Blob" + "type_info": "Integer" } ], "parameters": { @@ -82,11 +82,11 @@ false, false, false, - true, false, + true, false, false ] }, - "hash": "05bdd19eb31d4052f22f55021108733659fa1b636259c3aa36e10a823404c5fa" + "hash": "3f591f559e03b4d7d22b1e01c1c483fc35d4fe729876b067edd27998ece72224" } diff --git a/.sqlx/query-30ebf1aa19c50530c019d217be1e5647fc77980c79dce94edc1aa5771b5b725f.json b/.sqlx/query-40d3e4be9538adb02187817711c25d315b9fcfbdec5e06f07c41066ffe53c390.json similarity index 64% rename from .sqlx/query-30ebf1aa19c50530c019d217be1e5647fc77980c79dce94edc1aa5771b5b725f.json rename to .sqlx/query-40d3e4be9538adb02187817711c25d315b9fcfbdec5e06f07c41066ffe53c390.json index e115f25a9..2a0ce0e2e 100644 --- a/.sqlx/query-30ebf1aa19c50530c019d217be1e5647fc77980c79dce94edc1aa5771b5b725f.json +++ b/.sqlx/query-40d3e4be9538adb02187817711c25d315b9fcfbdec5e06f07c41066ffe53c390.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT `version` FROM `rust_migrations` LIMIT 1", + "query": "SELECT version FROM rust_migrations LIMIT 1", "describe": { "columns": [ { @@ -16,5 +16,5 @@ false ] }, - "hash": "30ebf1aa19c50530c019d217be1e5647fc77980c79dce94edc1aa5771b5b725f" + "hash": "40d3e4be9538adb02187817711c25d315b9fcfbdec5e06f07c41066ffe53c390" } diff --git a/.sqlx/query-465942dc655535288a07def03b2fe68548d52c970900d409eee6076354100860.json b/.sqlx/query-465942dc655535288a07def03b2fe68548d52c970900d409eee6076354100860.json new file mode 100644 index 000000000..cafff9287 --- /dev/null +++ b/.sqlx/query-465942dc655535288a07def03b2fe68548d52c970900d409eee6076354100860.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE files SET data = ?, mime_type = ?, is_hash_match = ?\n WHERE hash = ?\n AND (data IS NULL OR NOT is_hash_match)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "465942dc655535288a07def03b2fe68548d52c970900d409eee6076354100860" +} diff --git a/.sqlx/query-4b49177ead33e2c1ade773c28a84c10f05d690f615fa9d91b90329c86d8a38a5.json b/.sqlx/query-4b49177ead33e2c1ade773c28a84c10f05d690f615fa9d91b90329c86d8a38a5.json deleted file mode 100644 index 1388b0679..000000000 --- a/.sqlx/query-4b49177ead33e2c1ade773c28a84c10f05d690f615fa9d91b90329c86d8a38a5.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n cs.`parent_coin_id`, cs.`puzzle_hash`, cs.`amount`, `p2_puzzle_hash`,\n `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`\n FROM `cat_coins` INDEXED BY `cat_asset_id`\n INNER JOIN `coin_states` AS cs ON `cat_coins`.`coin_id` = cs.`coin_id`\n LEFT JOIN `transaction_spends` ON cs.`coin_id` = `transaction_spends`.`coin_id`\n LEFT JOIN `offered_coins` ON cs.`coin_id` = `offered_coins`.`coin_id`\n LEFT JOIN `offers` ON `offered_coins`.`offer_id` = `offers`.`offer_id`\n WHERE `cat_coins`.`asset_id` = ?\n AND cs.`spent_height` IS NULL\n AND `transaction_spends`.`coin_id` IS NULL\n AND (`offered_coins`.`coin_id` IS NULL OR `offers`.`status` > 0)\n AND cs.`transaction_id` IS NULL\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "p2_puzzle_hash", - "ordinal": 3, - "type_info": "Blob" - }, - { - "name": "parent_parent_coin_id", - "ordinal": 4, - "type_info": "Blob" - }, - { - "name": "parent_inner_puzzle_hash", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "parent_amount", - "ordinal": 6, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false - ] - }, - "hash": "4b49177ead33e2c1ade773c28a84c10f05d690f615fa9d91b90329c86d8a38a5" -} diff --git a/.sqlx/query-95658e1a150b0c39be8a3061a65d4224218de0770010d2548803586389e0daaf.json b/.sqlx/query-4c0cfb85e8e561c95e73ff4f6dda3e1cf8b0ccb18ec02941b691e9917739a957.json similarity index 57% rename from .sqlx/query-95658e1a150b0c39be8a3061a65d4224218de0770010d2548803586389e0daaf.json rename to .sqlx/query-4c0cfb85e8e561c95e73ff4f6dda3e1cf8b0ccb18ec02941b691e9917739a957.json index 0d84f5f16..8708e0e65 100644 --- a/.sqlx/query-95658e1a150b0c39be8a3061a65d4224218de0770010d2548803586389e0daaf.json +++ b/.sqlx/query-4c0cfb85e8e561c95e73ff4f6dda3e1cf8b0ccb18ec02941b691e9917739a957.json @@ -1,38 +1,32 @@ { "db_name": "SQLite", - "query": "SELECT * FROM `offer_xch` WHERE `offer_id` = ?", + "query": "\n SELECT parent_coin_hash, puzzle_hash, amount\n FROM spendable_coins\n WHERE coin_hash = ? AND asset_id = 0\n ", "describe": { "columns": [ { - "name": "offer_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, { - "name": "requested", + "name": "puzzle_hash", "ordinal": 1, - "type_info": "Bool" + "type_info": "Blob" }, { "name": "amount", "ordinal": 2, "type_info": "Blob" - }, - { - "name": "royalty", - "ordinal": 3, - "type_info": "Blob" } ], "parameters": { "Right": 1 }, "nullable": [ - false, false, false, false ] }, - "hash": "95658e1a150b0c39be8a3061a65d4224218de0770010d2548803586389e0daaf" + "hash": "4c0cfb85e8e561c95e73ff4f6dda3e1cf8b0ccb18ec02941b691e9917739a957" } diff --git a/.sqlx/query-4d0c70f825bfd512daa0b520d2d4e1a99f5cd363ffaafd9b79306ad9e131f2ab.json b/.sqlx/query-4d0c70f825bfd512daa0b520d2d4e1a99f5cd363ffaafd9b79306ad9e131f2ab.json deleted file mode 100644 index 383c08a1b..000000000 --- a/.sqlx/query-4d0c70f825bfd512daa0b520d2d4e1a99f5cd363ffaafd9b79306ad9e131f2ab.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `blockinfo` (\n `height`,\n `unix_time`\n )\n VALUES (?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "4d0c70f825bfd512daa0b520d2d4e1a99f5cd363ffaafd9b79306ad9e131f2ab" -} diff --git a/.sqlx/query-4e4f8169ea7bb563413fc740092fc0441bff904d4220a8030e95479624801027.json b/.sqlx/query-4e4f8169ea7bb563413fc740092fc0441bff904d4220a8030e95479624801027.json deleted file mode 100644 index 9ce38c196..000000000 --- a/.sqlx/query-4e4f8169ea7bb563413fc740092fc0441bff904d4220a8030e95479624801027.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT * FROM `offers` INDEXED BY `offer_status` WHERE `status` = 0", - "describe": { - "columns": [ - { - "name": "offer_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "encoded_offer", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "expiration_height", - "ordinal": 2, - "type_info": "Integer" - }, - { - "name": "expiration_timestamp", - "ordinal": 3, - "type_info": "Blob" - }, - { - "name": "fee", - "ordinal": 4, - "type_info": "Blob" - }, - { - "name": "status", - "ordinal": 5, - "type_info": "Integer" - }, - { - "name": "inserted_timestamp", - "ordinal": 6, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - true, - true, - false, - false, - false - ] - }, - "hash": "4e4f8169ea7bb563413fc740092fc0441bff904d4220a8030e95479624801027" -} diff --git a/.sqlx/query-4e57a9bc7318609e6c2b2fe3130f64350fe914edea4a35d21dbf77bd687c1eb4.json b/.sqlx/query-4e57a9bc7318609e6c2b2fe3130f64350fe914edea4a35d21dbf77bd687c1eb4.json deleted file mode 100644 index 835105005..000000000 --- a/.sqlx/query-4e57a9bc7318609e6c2b2fe3130f64350fe914edea4a35d21dbf77bd687c1eb4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n REPLACE INTO `dids` (\n `launcher_id`,\n `coin_id`,\n `name`,\n `is_owned`,\n `visible`,\n `created_height`\n )\n VALUES (?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "4e57a9bc7318609e6c2b2fe3130f64350fe914edea4a35d21dbf77bd687c1eb4" -} diff --git a/.sqlx/query-de732b1577723b588fc9b5855074acbb44e2eb40b909da5316600bc8de74569a.json b/.sqlx/query-4e6a3a14b63bb1332a0cee2996e753fda5ecd8877225a67e9ca4f3949f76f0a0.json similarity index 61% rename from .sqlx/query-de732b1577723b588fc9b5855074acbb44e2eb40b909da5316600bc8de74569a.json rename to .sqlx/query-4e6a3a14b63bb1332a0cee2996e753fda5ecd8877225a67e9ca4f3949f76f0a0.json index 932e6e660..f971d1226 100644 --- a/.sqlx/query-de732b1577723b588fc9b5855074acbb44e2eb40b909da5316600bc8de74569a.json +++ b/.sqlx/query-4e6a3a14b63bb1332a0cee2996e753fda5ecd8877225a67e9ca4f3949f76f0a0.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT\n `parent_coin_id`,\n `puzzle_hash`,\n `amount`,\n `puzzle_reveal`,\n `solution`\n FROM `transaction_spends` INDEXED BY `indexed_spend`\n WHERE `transaction_id` = ?\n ORDER BY `index` ASC\n ", + "query": "\n SELECT parent_coin_hash, puzzle_hash, amount, puzzle_reveal, solution\n FROM mempool_spends\n INNER JOIN mempool_items ON mempool_items.id = mempool_spends.mempool_item_id\n WHERE mempool_items.hash = ?\n ORDER BY seq ASC\n ", "describe": { "columns": [ { - "name": "parent_coin_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, @@ -40,5 +40,5 @@ false ] }, - "hash": "de732b1577723b588fc9b5855074acbb44e2eb40b909da5316600bc8de74569a" + "hash": "4e6a3a14b63bb1332a0cee2996e753fda5ecd8877225a67e9ca4f3949f76f0a0" } diff --git a/.sqlx/query-4f26a0e9ee97580232e4400f774443a5c94f5df0e530b5409cef42924d59efeb.json b/.sqlx/query-4f26a0e9ee97580232e4400f774443a5c94f5df0e530b5409cef42924d59efeb.json new file mode 100644 index 000000000..b587b8fa4 --- /dev/null +++ b/.sqlx/query-4f26a0e9ee97580232e4400f774443a5c94f5df0e530b5409cef42924d59efeb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO blocks (height, is_peak) VALUES (?, FALSE)", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "4f26a0e9ee97580232e4400f774443a5c94f5df0e530b5409cef42924d59efeb" +} diff --git a/.sqlx/query-4fc797762589a9e2e86965a8d92edd9110a65bfacd3008d28e0e9440b1f65301.json b/.sqlx/query-4fc797762589a9e2e86965a8d92edd9110a65bfacd3008d28e0e9440b1f65301.json deleted file mode 100644 index 00abede56..000000000 --- a/.sqlx/query-4fc797762589a9e2e86965a8d92edd9110a65bfacd3008d28e0e9440b1f65301.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `coin_states` SET `synced` = 0 WHERE `coin_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "4fc797762589a9e2e86965a8d92edd9110a65bfacd3008d28e0e9440b1f65301" -} diff --git a/.sqlx/query-4feca15039a86c86f18e0019ebe51cab01b0552f3465c4b80ec24280d8ed50ca.json b/.sqlx/query-4feca15039a86c86f18e0019ebe51cab01b0552f3465c4b80ec24280d8ed50ca.json deleted file mode 100644 index 1484efae1..000000000 --- a/.sqlx/query-4feca15039a86c86f18e0019ebe51cab01b0552f3465c4b80ec24280d8ed50ca.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states`\n INNER JOIN `nft_coins` ON `coin_states`.coin_id = `nft_coins`.coin_id\n WHERE `spent_height` IS NULL\n AND `created_height` IS NOT NULL\n ORDER BY `created_height`, `coin_states`.`coin_id` LIMIT ? OFFSET ?\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "spent_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "4feca15039a86c86f18e0019ebe51cab01b0552f3465c4b80ec24280d8ed50ca" -} diff --git a/.sqlx/query-5023da5b532e946c8e7b13448d2ff5f92d068bce71e153eebacdaebfe310420b.json b/.sqlx/query-5023da5b532e946c8e7b13448d2ff5f92d068bce71e153eebacdaebfe310420b.json deleted file mode 100644 index 83832d055..000000000 --- a/.sqlx/query-5023da5b532e946c8e7b13448d2ff5f92d068bce71e153eebacdaebfe310420b.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `dids` SET `is_owned` = 0 WHERE `coin_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "5023da5b532e946c8e7b13448d2ff5f92d068bce71e153eebacdaebfe310420b" -} diff --git a/.sqlx/query-50fe43ca73207560216f6fae3ff66d6f095eb6de376f80f3fc4b62b0156c7e8d.json b/.sqlx/query-50fe43ca73207560216f6fae3ff66d6f095eb6de376f80f3fc4b62b0156c7e8d.json new file mode 100644 index 000000000..2c671d216 --- /dev/null +++ b/.sqlx/query-50fe43ca73207560216f6fae3ff66d6f095eb6de376f80f3fc4b62b0156c7e8d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO p2_puzzles (hash, kind) VALUES (?, 1);\n\n INSERT OR IGNORE INTO clawbacks (p2_puzzle_id, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds)\n VALUES ((SELECT id FROM p2_puzzles WHERE hash = ?), ?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "50fe43ca73207560216f6fae3ff66d6f095eb6de376f80f3fc4b62b0156c7e8d" +} diff --git a/.sqlx/query-52c521c4bb43566e5af15059e685f26111fbef0d32183bb1fdce95805dea8d33.json b/.sqlx/query-52c521c4bb43566e5af15059e685f26111fbef0d32183bb1fdce95805dea8d33.json deleted file mode 100644 index 6833f1cdb..000000000 --- a/.sqlx/query-52c521c4bb43566e5af15059e685f26111fbef0d32183bb1fdce95805dea8d33.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `nfts` SET `is_owned` = 0 WHERE `coin_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "52c521c4bb43566e5af15059e685f26111fbef0d32183bb1fdce95805dea8d33" -} diff --git a/.sqlx/query-5450c0ea6d1ca91dd37dc923d7a281e0b341cdafe217832ba2ada0e67b1f3763.json b/.sqlx/query-5450c0ea6d1ca91dd37dc923d7a281e0b341cdafe217832ba2ada0e67b1f3763.json new file mode 100644 index 000000000..cb2092a46 --- /dev/null +++ b/.sqlx/query-5450c0ea6d1ca91dd37dc923d7a281e0b341cdafe217832ba2ada0e67b1f3763.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE mempool_items SET submitted_timestamp = unixepoch() WHERE hash = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "5450c0ea6d1ca91dd37dc923d7a281e0b341cdafe217832ba2ada0e67b1f3763" +} diff --git a/.sqlx/query-5456bae707e9ee2251330755fb8fd4531c0b509ec408c8b53a875b0bf9fbe77a.json b/.sqlx/query-5456bae707e9ee2251330755fb8fd4531c0b509ec408c8b53a875b0bf9fbe77a.json new file mode 100644 index 000000000..6f07066b3 --- /dev/null +++ b/.sqlx/query-5456bae707e9ee2251330755fb8fd4531c0b509ec408c8b53a875b0bf9fbe77a.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT hash FROM p2_puzzles\n INNER JOIN public_keys ON public_keys.p2_puzzle_id = p2_puzzles.id\n WHERE public_keys.derivation_index = ? AND public_keys.is_hardened = ?\n ", + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "5456bae707e9ee2251330755fb8fd4531c0b509ec408c8b53a875b0bf9fbe77a" +} diff --git a/.sqlx/query-54bc4e8d02f738e808a018731322fc235b8b5b14e6243900d1673089f12c2507.json b/.sqlx/query-54bc4e8d02f738e808a018731322fc235b8b5b14e6243900d1673089f12c2507.json deleted file mode 100644 index 284c0ff7c..000000000 --- a/.sqlx/query-54bc4e8d02f738e808a018731322fc235b8b5b14e6243900d1673089f12c2507.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT launcher_id, coin_id, collection_id, minter_did, owner_did, visible, \n sensitive_content, name, is_owned, created_height, metadata_hash,\n is_named, is_pending, edition_number, edition_total\n FROM `nfts` INDEXED BY `nft_metadata`\n WHERE `metadata_hash` = ?\n ", - "describe": { - "columns": [ - { - "name": "launcher_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "coin_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "collection_id", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "minter_did", - "ordinal": 3, - "type_info": "Blob" - }, - { - "name": "owner_did", - "ordinal": 4, - "type_info": "Blob" - }, - { - "name": "visible", - "ordinal": 5, - "type_info": "Bool" - }, - { - "name": "sensitive_content", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "name", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "is_owned", - "ordinal": 8, - "type_info": "Bool" - }, - { - "name": "created_height", - "ordinal": 9, - "type_info": "Integer" - }, - { - "name": "metadata_hash", - "ordinal": 10, - "type_info": "Blob" - }, - { - "name": "is_named", - "ordinal": 11, - "type_info": "Bool" - }, - { - "name": "is_pending", - "ordinal": 12, - "type_info": "Bool" - }, - { - "name": "edition_number", - "ordinal": 13, - "type_info": "Integer" - }, - { - "name": "edition_total", - "ordinal": 14, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - true, - true, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true - ] - }, - "hash": "54bc4e8d02f738e808a018731322fc235b8b5b14e6243900d1673089f12c2507" -} diff --git a/.sqlx/query-f359ce154cf02af67d1de36d43fe0d875561ec8f7fa77d81455ade2dd12305c0.json b/.sqlx/query-54c36388c6dcf5df5dec2ed5ae90a85c1ef07a9b5db14d229f90f53425e2a20c.json similarity index 57% rename from .sqlx/query-f359ce154cf02af67d1de36d43fe0d875561ec8f7fa77d81455ade2dd12305c0.json rename to .sqlx/query-54c36388c6dcf5df5dec2ed5ae90a85c1ef07a9b5db14d229f90f53425e2a20c.json index 7439e3ac5..f3745b90d 100644 --- a/.sqlx/query-f359ce154cf02af67d1de36d43fe0d875561ec8f7fa77d81455ade2dd12305c0.json +++ b/.sqlx/query-54c36388c6dcf5df5dec2ed5ae90a85c1ef07a9b5db14d229f90f53425e2a20c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT COUNT(*) AS `count` FROM `nft_uris` WHERE `checked` = 1", + "query": "SELECT COUNT(*) AS count FROM coins WHERE hash = ?", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 0 + "Right": 1 }, "nullable": [ false ] }, - "hash": "f359ce154cf02af67d1de36d43fe0d875561ec8f7fa77d81455ade2dd12305c0" + "hash": "54c36388c6dcf5df5dec2ed5ae90a85c1ef07a9b5db14d229f90f53425e2a20c" } diff --git a/.sqlx/query-5529039170b9e0cae5729fe3251bbcc4944737ca8146a8bb3dcee695e1a232d4.json b/.sqlx/query-5529039170b9e0cae5729fe3251bbcc4944737ca8146a8bb3dcee695e1a232d4.json new file mode 100644 index 000000000..e3bdcffa0 --- /dev/null +++ b/.sqlx/query-5529039170b9e0cae5729fe3251bbcc4944737ca8146a8bb3dcee695e1a232d4.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) AS count FROM files\n WHERE EXISTS (\n SELECT 1 FROM file_uris\n WHERE file_uris.file_id = files.id\n AND file_uris.last_checked_timestamp IS NOT NULL\n )\n ", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "5529039170b9e0cae5729fe3251bbcc4944737ca8146a8bb3dcee695e1a232d4" +} diff --git a/.sqlx/query-56cf5d987d1c176bb5254b21c7d3ef5f293d117434fdf14fe6646afce74e77d8.json b/.sqlx/query-56cf5d987d1c176bb5254b21c7d3ef5f293d117434fdf14fe6646afce74e77d8.json deleted file mode 100644 index 207a7569b..000000000 --- a/.sqlx/query-56cf5d987d1c176bb5254b21c7d3ef5f293d117434fdf14fe6646afce74e77d8.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO `transaction_spends` (\n `coin_id`,\n `index`,\n `transaction_id`,\n `parent_coin_id`,\n `puzzle_hash`,\n `amount`,\n `puzzle_reveal`,\n `solution`\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "56cf5d987d1c176bb5254b21c7d3ef5f293d117434fdf14fe6646afce74e77d8" -} diff --git a/.sqlx/query-579cf65be1ff023aca72dfcd60ebd3883e75bb092b4ac965411f24fa4a3b96d2.json b/.sqlx/query-579cf65be1ff023aca72dfcd60ebd3883e75bb092b4ac965411f24fa4a3b96d2.json deleted file mode 100644 index 80f49f3ae..000000000 --- a/.sqlx/query-579cf65be1ff023aca72dfcd60ebd3883e75bb092b4ac965411f24fa4a3b96d2.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO `transactions` (\n `transaction_id`,\n `aggregated_signature`,\n `fee`\n )\n VALUES (?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "579cf65be1ff023aca72dfcd60ebd3883e75bb092b4ac965411f24fa4a3b96d2" -} diff --git a/.sqlx/query-58a5fc4e0127de1f53d3071b1d7597ef0088d86a2fa00036dc8d8f0eefc5f156.json b/.sqlx/query-58a5fc4e0127de1f53d3071b1d7597ef0088d86a2fa00036dc8d8f0eefc5f156.json deleted file mode 100644 index 24e007ec9..000000000 --- a/.sqlx/query-58a5fc4e0127de1f53d3071b1d7597ef0088d86a2fa00036dc8d8f0eefc5f156.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT `name` FROM `collections` WHERE `collection_id` = ?", - "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "58a5fc4e0127de1f53d3071b1d7597ef0088d86a2fa00036dc8d8f0eefc5f156" -} diff --git a/.sqlx/query-593ff26761c8cc24690c2bfefdc08703daf515f8da64299a1f82df5e2ca609c4.json b/.sqlx/query-593ff26761c8cc24690c2bfefdc08703daf515f8da64299a1f82df5e2ca609c4.json deleted file mode 100644 index 7c3be3f20..000000000 --- a/.sqlx/query-593ff26761c8cc24690c2bfefdc08703daf515f8da64299a1f82df5e2ca609c4.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT DISTINCT `spent_height`\n FROM `coin_states`\n WHERE `spent_unixtime` IS NULL\n AND `spent_height` IS NOT NULL\n ORDER BY `spent_height` DESC \n LIMIT ?;\n ", - "describe": { - "columns": [ - { - "name": "spent_height", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "593ff26761c8cc24690c2bfefdc08703daf515f8da64299a1f82df5e2ca609c4" -} diff --git a/.sqlx/query-59d61d8b1794878c7acbcc45c304b7943277fdf5f4263e5827a3b2b4255f468e.json b/.sqlx/query-59d61d8b1794878c7acbcc45c304b7943277fdf5f4263e5827a3b2b4255f468e.json deleted file mode 100644 index 5897c1052..000000000 --- a/.sqlx/query-59d61d8b1794878c7acbcc45c304b7943277fdf5f4263e5827a3b2b4255f468e.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states`\n WHERE `spent_height` IS NULL\n AND `created_height` IS NOT NULL\n AND `kind` = 1\n ORDER BY `created_height`, `coin_states`.`coin_id` LIMIT ? OFFSET ?\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "spent_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "59d61d8b1794878c7acbcc45c304b7943277fdf5f4263e5827a3b2b4255f468e" -} diff --git a/.sqlx/query-5e339e5d8783b0323d4fbb402826a14c2587b27426635e68942a5526a6a00b8c.json b/.sqlx/query-5e339e5d8783b0323d4fbb402826a14c2587b27426635e68942a5526a6a00b8c.json deleted file mode 100644 index 3aab1029c..000000000 --- a/.sqlx/query-5e339e5d8783b0323d4fbb402826a14c2587b27426635e68942a5526a6a00b8c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "REPLACE INTO `nft_thumbnails` (`hash`, `icon`, `thumbnail`) VALUES (?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "5e339e5d8783b0323d4fbb402826a14c2587b27426635e68942a5526a6a00b8c" -} diff --git a/.sqlx/query-5e37c04f21a27b0fc6d2f9601c3431b27088d9184f057d65ab3905f871f75f45.json b/.sqlx/query-5e37c04f21a27b0fc6d2f9601c3431b27088d9184f057d65ab3905f871f75f45.json deleted file mode 100644 index 28ce79d94..000000000 --- a/.sqlx/query-5e37c04f21a27b0fc6d2f9601c3431b27088d9184f057d65ab3905f871f75f45.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n REPLACE INTO `cats` (\n `asset_id`,\n `name`,\n `ticker`,\n `description`,\n `icon`,\n `visible`,\n `fetched`\n ) VALUES (?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "5e37c04f21a27b0fc6d2f9601c3431b27088d9184f057d65ab3905f871f75f45" -} diff --git a/.sqlx/query-5e740e307bf332541fc073cf20f72297912073527728adb5afd1391300b8ed0c.json b/.sqlx/query-5e740e307bf332541fc073cf20f72297912073527728adb5afd1391300b8ed0c.json new file mode 100644 index 000000000..619f0a517 --- /dev/null +++ b/.sqlx/query-5e740e307bf332541fc073cf20f72297912073527728adb5afd1391300b8ed0c.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "SELECT collections.hash, uuid, collections.minter_hash, collections.name, collections.icon_url, \n collections.banner_url, collections.description, collections.is_visible, COUNT(*) OVER() as total_count\n FROM collections\n WHERE 1=1\n AND EXISTS (SELECT 1 FROM owned_nfts WHERE owned_nfts.collection_id = collections.id)\n AND (? OR is_visible = 1)\n ORDER BY CASE WHEN collections.id = 0 THEN 1 ELSE 0 END, name ASC\n LIMIT ?\n OFFSET ?", + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "uuid", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "minter_hash", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "name", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "icon_url", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "banner_url", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "description", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "is_visible", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "total_count", + "ordinal": 8, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + true, + false, + false + ] + }, + "hash": "5e740e307bf332541fc073cf20f72297912073527728adb5afd1391300b8ed0c" +} diff --git a/.sqlx/query-61199b0c85bc47d5fc29e48ebba7c40918184d3103139bff1b821044ad5e9b1a.json b/.sqlx/query-61199b0c85bc47d5fc29e48ebba7c40918184d3103139bff1b821044ad5e9b1a.json new file mode 100644 index 000000000..ea9313039 --- /dev/null +++ b/.sqlx/query-61199b0c85bc47d5fc29e48ebba7c40918184d3103139bff1b821044ad5e9b1a.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) AS count FROM files\n WHERE EXISTS (\n SELECT 1 FROM file_uris\n WHERE file_uris.file_id = files.id\n )\n ", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "61199b0c85bc47d5fc29e48ebba7c40918184d3103139bff1b821044ad5e9b1a" +} diff --git a/.sqlx/query-626c7058b74c08dc5bfc94db18620970a866e66db717b10713db63406e4bd9c7.json b/.sqlx/query-626c7058b74c08dc5bfc94db18620970a866e66db717b10713db63406e4bd9c7.json new file mode 100644 index 000000000..ef4d40dbd --- /dev/null +++ b/.sqlx/query-626c7058b74c08dc5bfc94db18620970a866e66db717b10713db63406e4bd9c7.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM coins WHERE created_height IS NULL AND id IN (\n SELECT coin_id FROM mempool_coins\n INNER JOIN mempool_items ON mempool_items.id = mempool_coins.mempool_item_id\n WHERE hash = ? AND is_output = TRUE\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "626c7058b74c08dc5bfc94db18620970a866e66db717b10713db63406e4bd9c7" +} diff --git a/.sqlx/query-640f343dc11301a349c1568532ef1de2004b9e5a9ab555499c6513579fedae49.json b/.sqlx/query-640f343dc11301a349c1568532ef1de2004b9e5a9ab555499c6513579fedae49.json new file mode 100644 index 000000000..b22bd80c7 --- /dev/null +++ b/.sqlx/query-640f343dc11301a349c1568532ef1de2004b9e5a9ab555499c6513579fedae49.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COALESCE(MAX(derivation_index) + 1, 0) AS derivation_index\n FROM public_keys\n WHERE is_hardened = ?\n ", + "describe": { + "columns": [ + { + "name": "derivation_index", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "640f343dc11301a349c1568532ef1de2004b9e5a9ab555499c6513579fedae49" +} diff --git a/.sqlx/query-64b3d98ba64e09e6a5c1b91dbea0a8de088736f910c7e6febb3884691323c6b0.json b/.sqlx/query-64b3d98ba64e09e6a5c1b91dbea0a8de088736f910c7e6febb3884691323c6b0.json deleted file mode 100644 index 347f979b7..000000000 --- a/.sqlx/query-64b3d98ba64e09e6a5c1b91dbea0a8de088736f910c7e6febb3884691323c6b0.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `offers` (\n `offer_id`, `encoded_offer`, `expiration_height`,\n `expiration_timestamp`, `fee`, `status`, `inserted_timestamp`\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "64b3d98ba64e09e6a5c1b91dbea0a8de088736f910c7e6febb3884691323c6b0" -} diff --git a/.sqlx/query-6845c1ccfd21971b75aeed84b8b65ca9044f8c312bedeff84f7b43a7d8828ca6.json b/.sqlx/query-6845c1ccfd21971b75aeed84b8b65ca9044f8c312bedeff84f7b43a7d8828ca6.json deleted file mode 100644 index 7cd960e16..000000000 --- a/.sqlx/query-6845c1ccfd21971b75aeed84b8b65ca9044f8c312bedeff84f7b43a7d8828ca6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n REPLACE INTO `future_did_names` (`launcher_id`, `name`)\n VALUES (?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "6845c1ccfd21971b75aeed84b8b65ca9044f8c312bedeff84f7b43a7d8828ca6" -} diff --git a/.sqlx/query-6a12723fae419d966d1a0e4417a790a5123a579ac97bb287a6555499384e932d.json b/.sqlx/query-6a12723fae419d966d1a0e4417a790a5123a579ac97bb287a6555499384e932d.json new file mode 100644 index 000000000..1d9976507 --- /dev/null +++ b/.sqlx/query-6a12723fae419d966d1a0e4417a790a5123a579ac97bb287a6555499384e932d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE file_uris\n SET last_checked_timestamp = unixepoch()\n WHERE file_id = (SELECT id FROM files WHERE hash = ?) AND uri = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6a12723fae419d966d1a0e4417a790a5123a579ac97bb287a6555499384e932d" +} diff --git a/.sqlx/query-6c9af1c832bcead88d180b884b4bf8301dff3615197f78ecbf238529030c5930.json b/.sqlx/query-6c9af1c832bcead88d180b884b4bf8301dff3615197f78ecbf238529030c5930.json deleted file mode 100644 index 142567667..000000000 --- a/.sqlx/query-6c9af1c832bcead88d180b884b4bf8301dff3615197f78ecbf238529030c5930.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM `coin_states`\n WHERE `coin_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "6c9af1c832bcead88d180b884b4bf8301dff3615197f78ecbf238529030c5930" -} diff --git a/.sqlx/query-6d70a25c3dc7e5d3507a28d5394bb5041032fd39983269421d9884d43259314c.json b/.sqlx/query-6d70a25c3dc7e5d3507a28d5394bb5041032fd39983269421d9884d43259314c.json deleted file mode 100644 index baa925a5d..000000000 --- a/.sqlx/query-6d70a25c3dc7e5d3507a28d5394bb5041032fd39983269421d9884d43259314c.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `collection_id`, `did_id`, `metadata_collection_id`, `visible`, `name`, `icon`\n FROM `collections` WHERE `collection_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "collection_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "did_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "metadata_collection_id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "visible", - "ordinal": 3, - "type_info": "Bool" - }, - { - "name": "name", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "icon", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - true, - true - ] - }, - "hash": "6d70a25c3dc7e5d3507a28d5394bb5041032fd39983269421d9884d43259314c" -} diff --git a/.sqlx/query-6dcfb331c63266208f3d1a06bc0641c70f6145fc77d12419f4bb495067d11081.json b/.sqlx/query-6dcfb331c63266208f3d1a06bc0641c70f6145fc77d12419f4bb495067d11081.json deleted file mode 100644 index 9672ab66a..000000000 --- a/.sqlx/query-6dcfb331c63266208f3d1a06bc0641c70f6145fc77d12419f4bb495067d11081.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states`\n WHERE `coin_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "created_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "spent_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "6dcfb331c63266208f3d1a06bc0641c70f6145fc77d12419f4bb495067d11081" -} diff --git a/.sqlx/query-6fe095fd12b12a8223963f2569f59940a39d9665aedafdfc781b277f1e43a1ec.json b/.sqlx/query-6fe095fd12b12a8223963f2569f59940a39d9665aedafdfc781b277f1e43a1ec.json deleted file mode 100644 index 77de60232..000000000 --- a/.sqlx/query-6fe095fd12b12a8223963f2569f59940a39d9665aedafdfc781b277f1e43a1ec.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `nfts` SET `created_height` = ? WHERE `coin_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "6fe095fd12b12a8223963f2569f59940a39d9665aedafdfc781b277f1e43a1ec" -} diff --git a/.sqlx/query-702e92a8661b0fd07cbafcdc8a2cfb1d152538c17a2a0e9dff56e201a2d9942e.json b/.sqlx/query-702e92a8661b0fd07cbafcdc8a2cfb1d152538c17a2a0e9dff56e201a2d9942e.json new file mode 100644 index 000000000..9d86fa7a8 --- /dev/null +++ b/.sqlx/query-702e92a8661b0fd07cbafcdc8a2cfb1d152538c17a2a0e9dff56e201a2d9942e.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT key\n FROM p2_puzzles\n INNER JOIN public_keys ON public_keys.p2_puzzle_id = p2_puzzles.id\n WHERE p2_puzzles.hash = ?\n ", + "describe": { + "columns": [ + { + "name": "key", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "702e92a8661b0fd07cbafcdc8a2cfb1d152538c17a2a0e9dff56e201a2d9942e" +} diff --git a/.sqlx/query-704b7ef0f010994311ed21c209a166f7ea91fd21969cf1b9e8d095c51b1cd675.json b/.sqlx/query-704b7ef0f010994311ed21c209a166f7ea91fd21969cf1b9e8d095c51b1cd675.json deleted file mode 100644 index eaf550575..000000000 --- a/.sqlx/query-704b7ef0f010994311ed21c209a166f7ea91fd21969cf1b9e8d095c51b1cd675.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT `license_hash` FROM `nft_coins` WHERE `launcher_id` = ?", - "describe": { - "columns": [ - { - "name": "license_hash", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "704b7ef0f010994311ed21c209a166f7ea91fd21969cf1b9e8d095c51b1cd675" -} diff --git a/.sqlx/query-8a1d2035972caef23b9812e76a6ab707de72cab08ef668505ad13096532a058a.json b/.sqlx/query-70b797f774d0016a26c6d0668c75562ebdfec282146ec1759e53c5ed0986e3bc.json similarity index 53% rename from .sqlx/query-8a1d2035972caef23b9812e76a6ab707de72cab08ef668505ad13096532a058a.json rename to .sqlx/query-70b797f774d0016a26c6d0668c75562ebdfec282146ec1759e53c5ed0986e3bc.json index 54f8c9aa7..4757f1f44 100644 --- a/.sqlx/query-8a1d2035972caef23b9812e76a6ab707de72cab08ef668505ad13096532a058a.json +++ b/.sqlx/query-70b797f774d0016a26c6d0668c75562ebdfec282146ec1759e53c5ed0986e3bc.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "DELETE FROM `blockinfo`", + "query": "DELETE FROM coins", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "8a1d2035972caef23b9812e76a6ab707de72cab08ef668505ad13096532a058a" + "hash": "70b797f774d0016a26c6d0668c75562ebdfec282146ec1759e53c5ed0986e3bc" } diff --git a/.sqlx/query-76438520a51fa88577083a3afbb93c9228c8e9d3599ef18ac27b3eb21fe62493.json b/.sqlx/query-76438520a51fa88577083a3afbb93c9228c8e9d3599ef18ac27b3eb21fe62493.json deleted file mode 100644 index abf20210c..000000000 --- a/.sqlx/query-76438520a51fa88577083a3afbb93c9228c8e9d3599ef18ac27b3eb21fe62493.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `launcher_id`, `coin_id`, `name`, `is_owned`, `visible`, `created_height`\n FROM `dids`\n WHERE `launcher_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "launcher_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "coin_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "is_owned", - "ordinal": 3, - "type_info": "Bool" - }, - { - "name": "visible", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "created_height", - "ordinal": 5, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - true, - false, - false, - true - ] - }, - "hash": "76438520a51fa88577083a3afbb93c9228c8e9d3599ef18ac27b3eb21fe62493" -} diff --git a/.sqlx/query-788f82b77a246cd59bba8cdc782e0248aa00e3499f5ffa93153e2f6225d9117f.json b/.sqlx/query-788f82b77a246cd59bba8cdc782e0248aa00e3499f5ffa93153e2f6225d9117f.json new file mode 100644 index 000000000..b49627fbe --- /dev/null +++ b/.sqlx/query-788f82b77a246cd59bba8cdc782e0248aa00e3499f5ffa93153e2f6225d9117f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE dids\n SET\n metadata = ?,\n recovery_list_hash = ?,\n num_verifications_required = ?\n WHERE asset_id = (SELECT id FROM assets WHERE hash = ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "788f82b77a246cd59bba8cdc782e0248aa00e3499f5ffa93153e2f6225d9117f" +} diff --git a/.sqlx/query-7ac44bd2934972682709ad97f7f01827988910b5ec9e7a0cab753c8f7d7fc0a9.json b/.sqlx/query-7ac44bd2934972682709ad97f7f01827988910b5ec9e7a0cab753c8f7d7fc0a9.json deleted file mode 100644 index 78fdfdf74..000000000 --- a/.sqlx/query-7ac44bd2934972682709ad97f7f01827988910b5ec9e7a0cab753c8f7d7fc0a9.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `did_coins` (\n `coin_id`,\n `parent_parent_coin_id`,\n `parent_inner_puzzle_hash`,\n `parent_amount`,\n `launcher_id`,\n `recovery_list_hash`,\n `num_verifications_required`,\n `metadata`,\n `p2_puzzle_hash`\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - }, - "hash": "7ac44bd2934972682709ad97f7f01827988910b5ec9e7a0cab753c8f7d7fc0a9" -} diff --git a/.sqlx/query-7ec2a8e891186eceea5d05cacf13fa2645c556524114b07890411811ef6f375c.json b/.sqlx/query-7ec2a8e891186eceea5d05cacf13fa2645c556524114b07890411811ef6f375c.json deleted file mode 100644 index da48127eb..000000000 --- a/.sqlx/query-7ec2a8e891186eceea5d05cacf13fa2645c556524114b07890411811ef6f375c.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states`\n INNER JOIN `did_coins` ON `coin_states`.coin_id = `did_coins`.coin_id\n WHERE `spent_height` IS NULL\n AND `created_height` IS NOT NULL\n ORDER BY `created_height`, `coin_states`.`coin_id` LIMIT ? OFFSET ?\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "spent_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "7ec2a8e891186eceea5d05cacf13fa2645c556524114b07890411811ef6f375c" -} diff --git a/.sqlx/query-7f291cc0304d69ebe753b91062bbd11db4ffb5c5d324827eeeb5c44672ca026e.json b/.sqlx/query-7f291cc0304d69ebe753b91062bbd11db4ffb5c5d324827eeeb5c44672ca026e.json new file mode 100644 index 000000000..a0196ce7d --- /dev/null +++ b/.sqlx/query-7f291cc0304d69ebe753b91062bbd11db4ffb5c5d324827eeeb5c44672ca026e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE assets SET\n name = ?,\n description = ?,\n is_sensitive_content = ?\n WHERE hash = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "7f291cc0304d69ebe753b91062bbd11db4ffb5c5d324827eeeb5c44672ca026e" +} diff --git a/.sqlx/query-7f341318dbd63334655c356333fa47b7c306c976123a75f31c4c23995965f831.json b/.sqlx/query-7f341318dbd63334655c356333fa47b7c306c976123a75f31c4c23995965f831.json new file mode 100644 index 000000000..a42024233 --- /dev/null +++ b/.sqlx/query-7f341318dbd63334655c356333fa47b7c306c976123a75f31c4c23995965f831.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM p2_puzzles", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "7f341318dbd63334655c356333fa47b7c306c976123a75f31c4c23995965f831" +} diff --git a/.sqlx/query-808635aff0ab3dd13109fec8dbac511d2f781525bfa22dd83617e512e0c0efbb.json b/.sqlx/query-808635aff0ab3dd13109fec8dbac511d2f781525bfa22dd83617e512e0c0efbb.json deleted file mode 100644 index 8444fc24d..000000000 --- a/.sqlx/query-808635aff0ab3dd13109fec8dbac511d2f781525bfa22dd83617e512e0c0efbb.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `rust_migrations` SET `version` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "808635aff0ab3dd13109fec8dbac511d2f781525bfa22dd83617e512e0c0efbb" -} diff --git a/.sqlx/query-841d52810f68a85ca772db7473d7b07985fd3bf90c21ebcfd7c5b38ffe9c40d4.json b/.sqlx/query-841d52810f68a85ca772db7473d7b07985fd3bf90c21ebcfd7c5b38ffe9c40d4.json new file mode 100644 index 000000000..59e16e7f4 --- /dev/null +++ b/.sqlx/query-841d52810f68a85ca772db7473d7b07985fd3bf90c21ebcfd7c5b38ffe9c40d4.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COALESCE(MAX(derivation_index) + 1, 0) AS derivation_index\n FROM public_keys\n INNER JOIN coins ON coins.p2_puzzle_id = public_keys.p2_puzzle_id\n WHERE is_hardened = ?\n ", + "describe": { + "columns": [ + { + "name": "derivation_index", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "841d52810f68a85ca772db7473d7b07985fd3bf90c21ebcfd7c5b38ffe9c40d4" +} diff --git a/.sqlx/query-b2c427e39098073050e4a92c94492de21f1a89a47bc0d1e5af7094bab5230a68.json b/.sqlx/query-85d053be5e0669dab3ffa1095e0803208a5bb3d177e8f0dc0b881f84d42352e7.json similarity index 56% rename from .sqlx/query-b2c427e39098073050e4a92c94492de21f1a89a47bc0d1e5af7094bab5230a68.json rename to .sqlx/query-85d053be5e0669dab3ffa1095e0803208a5bb3d177e8f0dc0b881f84d42352e7.json index d2fce0f1e..2c8084f10 100644 --- a/.sqlx/query-b2c427e39098073050e4a92c94492de21f1a89a47bc0d1e5af7094bab5230a68.json +++ b/.sqlx/query-85d053be5e0669dab3ffa1095e0803208a5bb3d177e8f0dc0b881f84d42352e7.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "SELECT `icon` FROM `nft_thumbnails` WHERE `hash` = ?", + "query": "SELECT amount FROM owned_coins WHERE asset_hash = ?", "describe": { "columns": [ { - "name": "icon", + "name": "amount", "ordinal": 0, "type_info": "Blob" } @@ -16,5 +16,5 @@ false ] }, - "hash": "b2c427e39098073050e4a92c94492de21f1a89a47bc0d1e5af7094bab5230a68" + "hash": "85d053be5e0669dab3ffa1095e0803208a5bb3d177e8f0dc0b881f84d42352e7" } diff --git a/.sqlx/query-8866e82acf4f306390b1d14487cc484f5ad0eeb7393dce5d381bd71b816b6755.json b/.sqlx/query-8866e82acf4f306390b1d14487cc484f5ad0eeb7393dce5d381bd71b816b6755.json new file mode 100644 index 000000000..182a61ccf --- /dev/null +++ b/.sqlx/query-8866e82acf4f306390b1d14487cc484f5ad0eeb7393dce5d381bd71b816b6755.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE assets SET\n kind = ?,\n name = ?,\n ticker = ?,\n precision = ?,\n icon_url = ?,\n description = ?,\n is_sensitive_content = ?,\n is_visible = ?\n WHERE hash = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "8866e82acf4f306390b1d14487cc484f5ad0eeb7393dce5d381bd71b816b6755" +} diff --git a/.sqlx/query-8a728911b3877476b9649f890645c5b658d377a98d7c6fe8952fd6c9f7da6d84.json b/.sqlx/query-8a728911b3877476b9649f890645c5b658d377a98d7c6fe8952fd6c9f7da6d84.json deleted file mode 100644 index c4c96d862..000000000 --- a/.sqlx/query-8a728911b3877476b9649f890645c5b658d377a98d7c6fe8952fd6c9f7da6d84.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n `transaction_id`,\n `fee`,\n `submitted_at`\n FROM `transactions`\n ORDER BY `submitted_at` DESC, `transaction_id` ASC\n ", - "describe": { - "columns": [ - { - "name": "transaction_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "fee", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "submitted_at", - "ordinal": 2, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - true - ] - }, - "hash": "8a728911b3877476b9649f890645c5b658d377a98d7c6fe8952fd6c9f7da6d84" -} diff --git a/.sqlx/query-8ad96df6ec8bead1764dfb3e85f28c608fe60cddcfbfe56566b631763ac7bae5.json b/.sqlx/query-8ad96df6ec8bead1764dfb3e85f28c608fe60cddcfbfe56566b631763ac7bae5.json deleted file mode 100644 index e6ccdecd6..000000000 --- a/.sqlx/query-8ad96df6ec8bead1764dfb3e85f28c608fe60cddcfbfe56566b631763ac7bae5.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT * FROM `offer_cats` WHERE `offer_id` = ?", - "describe": { - "columns": [ - { - "name": "offer_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "requested", - "ordinal": 1, - "type_info": "Bool" - }, - { - "name": "asset_id", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 3, - "type_info": "Blob" - }, - { - "name": "royalty", - "ordinal": 4, - "type_info": "Blob" - }, - { - "name": "name", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "ticker", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "icon", - "ordinal": 7, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - true, - true - ] - }, - "hash": "8ad96df6ec8bead1764dfb3e85f28c608fe60cddcfbfe56566b631763ac7bae5" -} diff --git a/.sqlx/query-8bce2a4bbc98b2933ed8b4a3b2ccc3f3ea556b608b54d0d9d15dcf181790eb97.json b/.sqlx/query-8bce2a4bbc98b2933ed8b4a3b2ccc3f3ea556b608b54d0d9d15dcf181790eb97.json new file mode 100644 index 000000000..5cf6c2800 --- /dev/null +++ b/.sqlx/query-8bce2a4bbc98b2933ed8b4a3b2ccc3f3ea556b608b54d0d9d15dcf181790eb97.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO mempool_spends (mempool_item_id, coin_hash, parent_coin_hash, puzzle_hash, amount, puzzle_reveal, solution, seq)\n VALUES ((SELECT id FROM mempool_items WHERE hash = ?), ?, ?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "8bce2a4bbc98b2933ed8b4a3b2ccc3f3ea556b608b54d0d9d15dcf181790eb97" +} diff --git a/.sqlx/query-f9a4af9ab64ff0d5534dcacd98c14389ad37bb8163b360f4926d7177749ca69d.json b/.sqlx/query-8de75687cbcc5a104842a41125f3b162e86507480a3cab534b61c3e0a567b0ce.json similarity index 55% rename from .sqlx/query-f9a4af9ab64ff0d5534dcacd98c14389ad37bb8163b360f4926d7177749ca69d.json rename to .sqlx/query-8de75687cbcc5a104842a41125f3b162e86507480a3cab534b61c3e0a567b0ce.json index fc4cf9ec3..c3ab4b3a9 100644 --- a/.sqlx/query-f9a4af9ab64ff0d5534dcacd98c14389ad37bb8163b360f4926d7177749ca69d.json +++ b/.sqlx/query-8de75687cbcc5a104842a41125f3b162e86507480a3cab534b61c3e0a567b0ce.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "SELECT `thumbnail` FROM `nft_thumbnails` WHERE `hash` = ?", + "query": "SELECT amount FROM selectable_coins WHERE asset_hash = ?", "describe": { "columns": [ { - "name": "thumbnail", + "name": "amount", "ordinal": 0, "type_info": "Blob" } @@ -16,5 +16,5 @@ false ] }, - "hash": "f9a4af9ab64ff0d5534dcacd98c14389ad37bb8163b360f4926d7177749ca69d" + "hash": "8de75687cbcc5a104842a41125f3b162e86507480a3cab534b61c3e0a567b0ce" } diff --git a/.sqlx/query-8ed50206e026ccd548134de70e7cdeb02e95f99befdc94c3db861c86d0a2827e.json b/.sqlx/query-8ed50206e026ccd548134de70e7cdeb02e95f99befdc94c3db861c86d0a2827e.json deleted file mode 100644 index ad4490039..000000000 --- a/.sqlx/query-8ed50206e026ccd548134de70e7cdeb02e95f99befdc94c3db861c86d0a2827e.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT launcher_id, coin_id, collection_id, minter_did, owner_did, visible, \n sensitive_content, name, is_owned, created_height, metadata_hash,\n is_named, is_pending, edition_number, edition_total\n FROM `nfts` WHERE `coin_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "launcher_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "coin_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "collection_id", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "minter_did", - "ordinal": 3, - "type_info": "Blob" - }, - { - "name": "owner_did", - "ordinal": 4, - "type_info": "Blob" - }, - { - "name": "visible", - "ordinal": 5, - "type_info": "Bool" - }, - { - "name": "sensitive_content", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "name", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "is_owned", - "ordinal": 8, - "type_info": "Bool" - }, - { - "name": "created_height", - "ordinal": 9, - "type_info": "Integer" - }, - { - "name": "metadata_hash", - "ordinal": 10, - "type_info": "Blob" - }, - { - "name": "is_named", - "ordinal": 11, - "type_info": "Bool" - }, - { - "name": "is_pending", - "ordinal": 12, - "type_info": "Bool" - }, - { - "name": "edition_number", - "ordinal": 13, - "type_info": "Integer" - }, - { - "name": "edition_total", - "ordinal": 14, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - true, - true, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true - ] - }, - "hash": "8ed50206e026ccd548134de70e7cdeb02e95f99befdc94c3db861c86d0a2827e" -} diff --git a/.sqlx/query-8fb0ac19831a14e60813bc15679b32f97cc0cf9ff9ecc0a7adb60017831c8aa8.json b/.sqlx/query-8fb0ac19831a14e60813bc15679b32f97cc0cf9ff9ecc0a7adb60017831c8aa8.json new file mode 100644 index 000000000..9ed90dd09 --- /dev/null +++ b/.sqlx/query-8fb0ac19831a14e60813bc15679b32f97cc0cf9ff9ecc0a7adb60017831c8aa8.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT created_height AS height FROM coins\n INNER JOIN blocks ON blocks.height = coins.created_height\n WHERE blocks.timestamp IS NULL\n UNION\n SELECT spent_height AS height FROM coins\n INNER JOIN blocks ON blocks.height = coins.spent_height\n WHERE blocks.timestamp IS NULL\n ORDER BY height DESC\n LIMIT ?\n ", + "describe": { + "columns": [ + { + "name": "height", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "8fb0ac19831a14e60813bc15679b32f97cc0cf9ff9ecc0a7adb60017831c8aa8" +} diff --git a/.sqlx/query-8fb8faa14e3dfa97450e43260dbe0affdba56a986c679149f8a85de0b45617a8.json b/.sqlx/query-8fb8faa14e3dfa97450e43260dbe0affdba56a986c679149f8a85de0b45617a8.json new file mode 100644 index 000000000..c863789cd --- /dev/null +++ b/.sqlx/query-8fb8faa14e3dfa97450e43260dbe0affdba56a986c679149f8a85de0b45617a8.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, underlying_coin_hash, underlying_delegated_puzzle_hash\n FROM spendable_coins\n INNER JOIN options ON options.asset_id = spendable_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id\n WHERE asset_hash = ?\n ", + "describe": { + "columns": [ + { + "name": "parent_coin_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "p2_puzzle_hash", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "parent_parent_coin_hash", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "parent_inner_puzzle_hash", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "parent_amount", + "ordinal": 6, + "type_info": "Blob" + }, + { + "name": "launcher_id", + "ordinal": 7, + "type_info": "Blob" + }, + { + "name": "underlying_coin_hash", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "underlying_delegated_puzzle_hash", + "ordinal": 9, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "8fb8faa14e3dfa97450e43260dbe0affdba56a986c679149f8a85de0b45617a8" +} diff --git a/.sqlx/query-4a11391d34af85600ef2df66d5ac03ea1b14e6753763c02c23ca879d08bf71b7.json b/.sqlx/query-91054392bb590bd729d257989bee20e41f5e7b40a1434df439f0ccca126ee8ac.json similarity index 57% rename from .sqlx/query-4a11391d34af85600ef2df66d5ac03ea1b14e6753763c02c23ca879d08bf71b7.json rename to .sqlx/query-91054392bb590bd729d257989bee20e41f5e7b40a1434df439f0ccca126ee8ac.json index 941ad16ea..0b39cb3ab 100644 --- a/.sqlx/query-4a11391d34af85600ef2df66d5ac03ea1b14e6753763c02c23ca879d08bf71b7.json +++ b/.sqlx/query-91054392bb590bd729d257989bee20e41f5e7b40a1434df439f0ccca126ee8ac.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT COUNT(*) AS `count` FROM `derivations` WHERE `p2_puzzle_hash` = ?\n ", + "query": "SELECT COUNT(*) AS count FROM p2_puzzles WHERE hash = ? AND kind = 0", "describe": { "columns": [ { @@ -16,5 +16,5 @@ false ] }, - "hash": "4a11391d34af85600ef2df66d5ac03ea1b14e6753763c02c23ca879d08bf71b7" + "hash": "91054392bb590bd729d257989bee20e41f5e7b40a1434df439f0ccca126ee8ac" } diff --git a/.sqlx/query-91850dd89cc1da00047085e61ebcd989e1818612bc6abb6b7c3b7eaa9b179c30.json b/.sqlx/query-91850dd89cc1da00047085e61ebcd989e1818612bc6abb6b7c3b7eaa9b179c30.json new file mode 100644 index 000000000..659658265 --- /dev/null +++ b/.sqlx/query-91850dd89cc1da00047085e61ebcd989e1818612bc6abb6b7c3b7eaa9b179c30.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO resized_images (file_id, kind, data) VALUES ((SELECT id FROM files WHERE hash = ?), ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "91850dd89cc1da00047085e61ebcd989e1818612bc6abb6b7c3b7eaa9b179c30" +} diff --git a/.sqlx/query-9420892e33dedae25072fe284eb469e14ab37adfb836483468f8a8db54b7fdcf.json b/.sqlx/query-9420892e33dedae25072fe284eb469e14ab37adfb836483468f8a8db54b7fdcf.json new file mode 100644 index 000000000..18e966c67 --- /dev/null +++ b/.sqlx/query-9420892e33dedae25072fe284eb469e14ab37adfb836483468f8a8db54b7fdcf.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) AS count FROM selectable_coins WHERE asset_hash = ?", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "9420892e33dedae25072fe284eb469e14ab37adfb836483468f8a8db54b7fdcf" +} diff --git a/.sqlx/query-94923631955b98f8477aeaaf7bfe3aad3a1a0eb1ec11398de43fe7d044c8ade8.json b/.sqlx/query-94923631955b98f8477aeaaf7bfe3aad3a1a0eb1ec11398de43fe7d044c8ade8.json new file mode 100644 index 000000000..c18dea463 --- /dev/null +++ b/.sqlx/query-94923631955b98f8477aeaaf7bfe3aad3a1a0eb1ec11398de43fe7d044c8ade8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO assets (\n hash, kind, name, ticker, precision, icon_url, description,\n is_sensitive_content, is_visible\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(hash) DO UPDATE SET\n name = COALESCE(name, excluded.name),\n ticker = COALESCE(ticker, excluded.ticker),\n icon_url = COALESCE(icon_url, excluded.icon_url),\n description = COALESCE(description, excluded.description),\n is_sensitive_content = is_sensitive_content OR excluded.is_sensitive_content\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "94923631955b98f8477aeaaf7bfe3aad3a1a0eb1ec11398de43fe7d044c8ade8" +} diff --git a/.sqlx/query-955b4fba4689c0ae0a5eb46631fdc38ed92ac74d690c3f5a70a1fb94d9164a89.json b/.sqlx/query-955b4fba4689c0ae0a5eb46631fdc38ed92ac74d690c3f5a70a1fb94d9164a89.json deleted file mode 100644 index 99db9ff74..000000000 --- a/.sqlx/query-955b4fba4689c0ae0a5eb46631fdc38ed92ac74d690c3f5a70a1fb94d9164a89.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `launcher_id`, `coin_id`, `name`, `is_owned`, `visible`, `created_height`\n FROM `dids` INDEXED BY `did_name`\n WHERE `is_owned` = 1\n ORDER BY `visible` DESC, `is_pending` DESC, `is_named` DESC, `name` ASC, `launcher_id` ASC\n ", - "describe": { - "columns": [ - { - "name": "launcher_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "coin_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "is_owned", - "ordinal": 3, - "type_info": "Bool" - }, - { - "name": "visible", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "created_height", - "ordinal": 5, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - true, - false, - false, - true - ] - }, - "hash": "955b4fba4689c0ae0a5eb46631fdc38ed92ac74d690c3f5a70a1fb94d9164a89" -} diff --git a/.sqlx/query-95c52dca8c20bd82bbd640d503b68e6c9138772b9e1e8fff953a42e78de0269c.json b/.sqlx/query-95c52dca8c20bd82bbd640d503b68e6c9138772b9e1e8fff953a42e78de0269c.json new file mode 100644 index 000000000..8ee5dcb5e --- /dev/null +++ b/.sqlx/query-95c52dca8c20bd82bbd640d503b68e6c9138772b9e1e8fff953a42e78de0269c.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT parent_coin_hash, puzzle_hash, amount FROM selectable_coins WHERE asset_id = 0", + "describe": { + "columns": [ + { + "name": "parent_coin_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 2, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "95c52dca8c20bd82bbd640d503b68e6c9138772b9e1e8fff953a42e78de0269c" +} diff --git a/.sqlx/query-9738b036011c30ae2d1417ba38476c60e55e3debc90ea7205d08f182865aa980.json b/.sqlx/query-9738b036011c30ae2d1417ba38476c60e55e3debc90ea7205d08f182865aa980.json deleted file mode 100644 index 723c5e376..000000000 --- a/.sqlx/query-9738b036011c30ae2d1417ba38476c60e55e3debc90ea7205d08f182865aa980.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE `coin_states`\n SET `transaction_id` = NULL\n WHERE `coin_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "9738b036011c30ae2d1417ba38476c60e55e3debc90ea7205d08f182865aa980" -} diff --git a/.sqlx/query-9919b58097851e0ba213cddd61673d78156251d9ee5475f1fab50696139c4997.json b/.sqlx/query-9919b58097851e0ba213cddd61673d78156251d9ee5475f1fab50696139c4997.json deleted file mode 100644 index cd871166a..000000000 --- a/.sqlx/query-9919b58097851e0ba213cddd61673d78156251d9ee5475f1fab50696139c4997.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM `transactions`\n WHERE `transaction_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "9919b58097851e0ba213cddd61673d78156251d9ee5475f1fab50696139c4997" -} diff --git a/.sqlx/query-99ccce96cc2ec334724a1ff115c4f09114d37e70b84c3c1b2277c58ffe01adfc.json b/.sqlx/query-99ccce96cc2ec334724a1ff115c4f09114d37e70b84c3c1b2277c58ffe01adfc.json deleted file mode 100644 index 7db4b74e9..000000000 --- a/.sqlx/query-99ccce96cc2ec334724a1ff115c4f09114d37e70b84c3c1b2277c58ffe01adfc.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `did_coins`\n INNER JOIN `coin_states` ON `coin_states`.coin_id = `did_coins`.coin_id\n WHERE `launcher_id` = ?\n AND `spent_height` IS NULL\n AND `created_height` IS NOT NULL\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "spent_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "99ccce96cc2ec334724a1ff115c4f09114d37e70b84c3c1b2277c58ffe01adfc" -} diff --git a/.sqlx/query-99f82770337e8817fc98076f2c7a942f70b2c808b990078a0afedb589da24298.json b/.sqlx/query-99f82770337e8817fc98076f2c7a942f70b2c808b990078a0afedb589da24298.json deleted file mode 100644 index ffa5b76ef..000000000 --- a/.sqlx/query-99f82770337e8817fc98076f2c7a942f70b2c808b990078a0afedb589da24298.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE `transactions`\n SET `submitted_at` = ?\n WHERE `transaction_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "99f82770337e8817fc98076f2c7a942f70b2c808b990078a0afedb589da24298" -} diff --git a/.sqlx/query-9b3f53942d2d4bd8c4cb9100949e39d85edd1c4bdf1f6a741eff11ded22b278c.json b/.sqlx/query-9b3f53942d2d4bd8c4cb9100949e39d85edd1c4bdf1f6a741eff11ded22b278c.json new file mode 100644 index 000000000..822daa0fc --- /dev/null +++ b/.sqlx/query-9b3f53942d2d4bd8c4cb9100949e39d85edd1c4bdf1f6a741eff11ded22b278c.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n metadata, metadata_updater_puzzle_hash, royalty_puzzle_hash, royalty_basis_points\n FROM nfts\n INNER JOIN assets ON assets.id = nfts.asset_id\n WHERE hash = ?\n ", + "describe": { + "columns": [ + { + "name": "metadata", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "metadata_updater_puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "royalty_puzzle_hash", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "royalty_basis_points", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "9b3f53942d2d4bd8c4cb9100949e39d85edd1c4bdf1f6a741eff11ded22b278c" +} diff --git a/.sqlx/query-9e27bdf8912eeac4222a9575bb1fb0e54790aa895bd7fc96c598cc5876318227.json b/.sqlx/query-9e27bdf8912eeac4222a9575bb1fb0e54790aa895bd7fc96c598cc5876318227.json new file mode 100644 index 000000000..bec5e43b7 --- /dev/null +++ b/.sqlx/query-9e27bdf8912eeac4222a9575bb1fb0e54790aa895bd7fc96c598cc5876318227.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT asset_id, kind FROM coins INNER JOIN assets ON assets.id = asset_id WHERE coins.hash = ?", + "describe": { + "columns": [ + { + "name": "asset_id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "kind", + "ordinal": 1, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false + ] + }, + "hash": "9e27bdf8912eeac4222a9575bb1fb0e54790aa895bd7fc96c598cc5876318227" +} diff --git a/.sqlx/query-a19602191b734f77355f83d6ba469251e360f743ad338cf36706d1689d74c5b4.json b/.sqlx/query-a19602191b734f77355f83d6ba469251e360f743ad338cf36706d1689d74c5b4.json deleted file mode 100644 index 0795a20dc..000000000 --- a/.sqlx/query-a19602191b734f77355f83d6ba469251e360f743ad338cf36706d1689d74c5b4.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states` INDEXED BY `coin_created`\n WHERE `created_height` = ?\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "created_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "spent_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "a19602191b734f77355f83d6ba469251e360f743ad338cf36706d1689d74c5b4" -} diff --git a/.sqlx/query-a1ac83ba7dc421cba36a2d0cc869473821733ddc2d5322277d80ab52a751e52b.json b/.sqlx/query-a1ac83ba7dc421cba36a2d0cc869473821733ddc2d5322277d80ab52a751e52b.json new file mode 100644 index 000000000..20088f8e5 --- /dev/null +++ b/.sqlx/query-a1ac83ba7dc421cba36a2d0cc869473821733ddc2d5322277d80ab52a751e52b.json @@ -0,0 +1,80 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, recovery_list_hash, num_verifications_required, metadata\n FROM wallet_coins\n INNER JOIN dids ON dids.asset_id = wallet_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = wallet_coins.coin_id\n WHERE asset_hash = ? AND spent_height IS NULL\n ", + "describe": { + "columns": [ + { + "name": "parent_coin_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "p2_puzzle_hash", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "parent_parent_coin_hash", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "parent_inner_puzzle_hash", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "parent_amount", + "ordinal": 6, + "type_info": "Blob" + }, + { + "name": "launcher_id", + "ordinal": 7, + "type_info": "Blob" + }, + { + "name": "recovery_list_hash", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "num_verifications_required", + "ordinal": 9, + "type_info": "Integer" + }, + { + "name": "metadata", + "ordinal": 10, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "a1ac83ba7dc421cba36a2d0cc869473821733ddc2d5322277d80ab52a751e52b" +} diff --git a/.sqlx/query-a1dcbfe54b8cbdfd3b8760ca225c168eb04d423fccd6d0e3d408d0dc733fab34.json b/.sqlx/query-a1dcbfe54b8cbdfd3b8760ca225c168eb04d423fccd6d0e3d408d0dc733fab34.json deleted file mode 100644 index e921213b1..000000000 --- a/.sqlx/query-a1dcbfe54b8cbdfd3b8760ca225c168eb04d423fccd6d0e3d408d0dc733fab34.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM `offers` WHERE `offer_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "a1dcbfe54b8cbdfd3b8760ca225c168eb04d423fccd6d0e3d408d0dc733fab34" -} diff --git a/.sqlx/query-a21166c21fa64a3bc44aeda9035c5759059d5543076eb901bd2ae443d9c89f3a.json b/.sqlx/query-a21166c21fa64a3bc44aeda9035c5759059d5543076eb901bd2ae443d9c89f3a.json deleted file mode 100644 index 755afa0cb..000000000 --- a/.sqlx/query-a21166c21fa64a3bc44aeda9035c5759059d5543076eb901bd2ae443d9c89f3a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM `nft_data` WHERE `hash` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "a21166c21fa64a3bc44aeda9035c5759059d5543076eb901bd2ae443d9c89f3a" -} diff --git a/.sqlx/query-a27a932fd61aff027c2416fa86fd625bf7ce1a7ab5d0bf254df504b8708893c9.json b/.sqlx/query-a27a932fd61aff027c2416fa86fd625bf7ce1a7ab5d0bf254df504b8708893c9.json deleted file mode 100644 index e8b4fe21f..000000000 --- a/.sqlx/query-a27a932fd61aff027c2416fa86fd625bf7ce1a7ab5d0bf254df504b8708893c9.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `coin_states`.`coin_id`\n FROM `coin_states` INDEXED BY `coin_spent`\n INNER JOIN `nft_coins` ON `coin_states`.`coin_id` = `nft_coins`.`coin_id`\n WHERE `spent_height` IS NULL\n ", - "describe": { - "columns": [ - { - "name": "coin_id", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "a27a932fd61aff027c2416fa86fd625bf7ce1a7ab5d0bf254df504b8708893c9" -} diff --git a/.sqlx/query-a3d7c6b12d4f2cad5b702b8500f140d5c3ca584b8f98fd84c87fba58a37d262f.json b/.sqlx/query-a3d7c6b12d4f2cad5b702b8500f140d5c3ca584b8f98fd84c87fba58a37d262f.json new file mode 100644 index 000000000..6b8a1bb88 --- /dev/null +++ b/.sqlx/query-a3d7c6b12d4f2cad5b702b8500f140d5c3ca584b8f98fd84c87fba58a37d262f.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, hidden_puzzle_hash, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, asset_hash AS asset_id\n FROM spendable_coins\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id\n WHERE coin_hash = ?\n ", + "describe": { + "columns": [ + { + "name": "parent_coin_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "hidden_puzzle_hash", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "p2_puzzle_hash", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "parent_parent_coin_hash", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "parent_inner_puzzle_hash", + "ordinal": 6, + "type_info": "Blob" + }, + { + "name": "parent_amount", + "ordinal": 7, + "type_info": "Blob" + }, + { + "name": "asset_id", + "ordinal": 8, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "a3d7c6b12d4f2cad5b702b8500f140d5c3ca584b8f98fd84c87fba58a37d262f" +} diff --git a/.sqlx/query-a491d525db244a1d652c603b177ab5a798f65477480750946ae6b982bac32ddc.json b/.sqlx/query-a491d525db244a1d652c603b177ab5a798f65477480750946ae6b982bac32ddc.json deleted file mode 100644 index 217a20f4c..000000000 --- a/.sqlx/query-a491d525db244a1d652c603b177ab5a798f65477480750946ae6b982bac32ddc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `offers` SET `status` = ? WHERE `offer_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "a491d525db244a1d652c603b177ab5a798f65477480750946ae6b982bac32ddc" -} diff --git a/.sqlx/query-a5f50b4a6c72d804c5fb0139b5d804e8f59f2e47bde3dafdd78d72a0987f41a7.json b/.sqlx/query-a5f50b4a6c72d804c5fb0139b5d804e8f59f2e47bde3dafdd78d72a0987f41a7.json new file mode 100644 index 000000000..9ed9aabff --- /dev/null +++ b/.sqlx/query-a5f50b4a6c72d804c5fb0139b5d804e8f59f2e47bde3dafdd78d72a0987f41a7.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT coin_hash FROM wallet_coins\n WHERE spent_height IS NULL\n AND (asset_id != 0 OR p2_puzzle_kind != 0)\n ", + "describe": { + "columns": [ + { + "name": "coin_hash", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "a5f50b4a6c72d804c5fb0139b5d804e8f59f2e47bde3dafdd78d72a0987f41a7" +} diff --git a/.sqlx/query-a6444ee76ed09f8c46d1821464cb62da0dde34ac13fa28689390facd204797e4.json b/.sqlx/query-a6444ee76ed09f8c46d1821464cb62da0dde34ac13fa28689390facd204797e4.json new file mode 100644 index 000000000..d3d7b0cd4 --- /dev/null +++ b/.sqlx/query-a6444ee76ed09f8c46d1821464cb62da0dde34ac13fa28689390facd204797e4.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n hash, name, icon_url, description, ticker, precision,\n is_visible, is_sensitive_content\n FROM assets\n WHERE assets.kind = 0 AND assets.id != 0\n AND EXISTS (\n SELECT 1 FROM coins\n INNER JOIN p2_puzzles ON p2_puzzles.id = coins.p2_puzzle_id\n WHERE coins.asset_id = assets.id\n )\n ORDER BY name ASC\n ", + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "icon_url", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "ticker", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "precision", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "is_visible", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "is_sensitive_content", + "ordinal": 7, + "type_info": "Bool" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + true, + true, + true, + true, + false, + false, + false + ] + }, + "hash": "a6444ee76ed09f8c46d1821464cb62da0dde34ac13fa28689390facd204797e4" +} diff --git a/.sqlx/query-e402dbc527157d8d32ee0a0093e2f1c3b5f2f0e95a392b57752bae69421f0514.json b/.sqlx/query-a78a4e79afa33d57fcf510dcf1528aee25a61e365661d01cce8971d6ec0c0b1a.json similarity index 55% rename from .sqlx/query-e402dbc527157d8d32ee0a0093e2f1c3b5f2f0e95a392b57752bae69421f0514.json rename to .sqlx/query-a78a4e79afa33d57fcf510dcf1528aee25a61e365661d01cce8971d6ec0c0b1a.json index 1b3bd5c46..59ac9f4f5 100644 --- a/.sqlx/query-e402dbc527157d8d32ee0a0093e2f1c3b5f2f0e95a392b57752bae69421f0514.json +++ b/.sqlx/query-a78a4e79afa33d57fcf510dcf1528aee25a61e365661d01cce8971d6ec0c0b1a.json @@ -1,21 +1,26 @@ { "db_name": "SQLite", - "query": "SELECT `data`, `mime_type`, `hash_matches` FROM `nft_data` WHERE `hash` = ?", + "query": "SELECT hash, data, mime_type, is_hash_match FROM files WHERE hash = ?", "describe": { "columns": [ { - "name": "data", + "name": "hash", "ordinal": 0, "type_info": "Blob" }, { - "name": "mime_type", + "name": "data", "ordinal": 1, - "type_info": "Text" + "type_info": "Blob" }, { - "name": "hash_matches", + "name": "mime_type", "ordinal": 2, + "type_info": "Text" + }, + { + "name": "is_hash_match", + "ordinal": 3, "type_info": "Bool" } ], @@ -24,9 +29,10 @@ }, "nullable": [ false, - false, - false + true, + true, + true ] }, - "hash": "e402dbc527157d8d32ee0a0093e2f1c3b5f2f0e95a392b57752bae69421f0514" + "hash": "a78a4e79afa33d57fcf510dcf1528aee25a61e365661d01cce8971d6ec0c0b1a" } diff --git a/.sqlx/query-e01303c64eddb3a9064f553abec45c7447299857efa935e9109b102ad6d59c27.json b/.sqlx/query-a8933f2249fc03a445cee68022c63cda2de23f9edf88ccdefd684f56ee3618a2.json similarity index 65% rename from .sqlx/query-e01303c64eddb3a9064f553abec45c7447299857efa935e9109b102ad6d59c27.json rename to .sqlx/query-a8933f2249fc03a445cee68022c63cda2de23f9edf88ccdefd684f56ee3618a2.json index 052ff04d2..571a646f3 100644 --- a/.sqlx/query-e01303c64eddb3a9064f553abec45c7447299857efa935e9109b102ad6d59c27.json +++ b/.sqlx/query-a8933f2249fc03a445cee68022c63cda2de23f9edf88ccdefd684f56ee3618a2.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT\n cs.parent_coin_id, cs.puzzle_hash, cs.amount,\n did.parent_parent_coin_id, did.parent_inner_puzzle_hash, did.parent_amount,\n did.launcher_id, did.recovery_list_hash, did.num_verifications_required,\n did.metadata, did.p2_puzzle_hash\n FROM `coin_states` AS cs\n INNER JOIN `did_coins` AS did ON cs.coin_id = did.coin_id\n WHERE cs.coin_id = ?\n ", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, recovery_list_hash, num_verifications_required, metadata\n FROM spendable_coins\n INNER JOIN dids ON dids.asset_id = spendable_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id\n WHERE coin_hash = ?\n ", "describe": { "columns": [ { - "name": "parent_coin_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, @@ -19,42 +19,42 @@ "type_info": "Blob" }, { - "name": "parent_parent_coin_id", + "name": "p2_puzzle_hash", "ordinal": 3, "type_info": "Blob" }, { - "name": "parent_inner_puzzle_hash", + "name": "parent_parent_coin_hash", "ordinal": 4, "type_info": "Blob" }, { - "name": "parent_amount", + "name": "parent_inner_puzzle_hash", "ordinal": 5, "type_info": "Blob" }, { - "name": "launcher_id", + "name": "parent_amount", "ordinal": 6, "type_info": "Blob" }, { - "name": "recovery_list_hash", + "name": "launcher_id", "ordinal": 7, "type_info": "Blob" }, { - "name": "num_verifications_required", + "name": "recovery_list_hash", "ordinal": 8, "type_info": "Blob" }, { - "name": "metadata", + "name": "num_verifications_required", "ordinal": 9, - "type_info": "Blob" + "type_info": "Integer" }, { - "name": "p2_puzzle_hash", + "name": "metadata", "ordinal": 10, "type_info": "Blob" } @@ -70,11 +70,11 @@ false, false, false, - true, false, + true, false, false ] }, - "hash": "e01303c64eddb3a9064f553abec45c7447299857efa935e9109b102ad6d59c27" + "hash": "a8933f2249fc03a445cee68022c63cda2de23f9edf88ccdefd684f56ee3618a2" } diff --git a/.sqlx/query-a93526d49ab50f772b1c359d3ab88ef45570143ed28f265d76c6e17efdd12dce.json b/.sqlx/query-a93526d49ab50f772b1c359d3ab88ef45570143ed28f265d76c6e17efdd12dce.json deleted file mode 100644 index 44ca066e9..000000000 --- a/.sqlx/query-a93526d49ab50f772b1c359d3ab88ef45570143ed28f265d76c6e17efdd12dce.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO `offer_cats` (\n `offer_id`, `requested`, `asset_id`,\n `amount`, `royalty`, `name`, `ticker`, `icon`\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "a93526d49ab50f772b1c359d3ab88ef45570143ed28f265d76c6e17efdd12dce" -} diff --git a/.sqlx/query-a9820412ca803927e7d195db8d1f90ce25c5a99870e6a3a248cb1cecd912cd23.json b/.sqlx/query-a9820412ca803927e7d195db8d1f90ce25c5a99870e6a3a248cb1cecd912cd23.json deleted file mode 100644 index acbe13719..000000000 --- a/.sqlx/query-a9820412ca803927e7d195db8d1f90ce25c5a99870e6a3a248cb1cecd912cd23.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `transaction_id`\n FROM `transaction_spends`\n WHERE `coin_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "transaction_id", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "a9820412ca803927e7d195db8d1f90ce25c5a99870e6a3a248cb1cecd912cd23" -} diff --git a/.sqlx/query-aa8b2a615b069bd89079afcceb9484344c8731f7f767a9aa949c1356d99cd81a.json b/.sqlx/query-aa8b2a615b069bd89079afcceb9484344c8731f7f767a9aa949c1356d99cd81a.json new file mode 100644 index 000000000..9e33b755d --- /dev/null +++ b/.sqlx/query-aa8b2a615b069bd89079afcceb9484344c8731f7f767a9aa949c1356d99cd81a.json @@ -0,0 +1,122 @@ +{ + "db_name": "SQLite", + "query": "SELECT \t\n height, timestamp, coin_id, puzzle_hash, parent_coin_hash, amount,\n is_created_in_block, is_spent_in_block, asset_hash, asset_description,\n asset_is_visible, asset_is_sensitive_content, asset_name, asset_icon_url,\n asset_kind, p2_puzzle_hash, asset_ticker, asset_precision\n FROM transaction_coins \n WHERE height = ?", + "describe": { + "columns": [ + { + "name": "height", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "timestamp", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "coin_id", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "parent_coin_hash", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "is_created_in_block", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "is_spent_in_block", + "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "asset_hash", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "asset_description", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "asset_is_visible", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "asset_is_sensitive_content", + "ordinal": 11, + "type_info": "Bool" + }, + { + "name": "asset_name", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "asset_icon_url", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "asset_kind", + "ordinal": 14, + "type_info": "Integer" + }, + { + "name": "p2_puzzle_hash", + "ordinal": 15, + "type_info": "Blob" + }, + { + "name": "asset_ticker", + "ordinal": 16, + "type_info": "Text" + }, + { + "name": "asset_precision", + "ordinal": 17, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + true, + true, + false, + true, + true, + false + ] + }, + "hash": "aa8b2a615b069bd89079afcceb9484344c8731f7f767a9aa949c1356d99cd81a" +} diff --git a/.sqlx/query-ac56599044f381115785e659ed4a52d74dc2502e7d3f73539b805bc77f38604a.json b/.sqlx/query-ac56599044f381115785e659ed4a52d74dc2502e7d3f73539b805bc77f38604a.json deleted file mode 100644 index 1451c3d4d..000000000 --- a/.sqlx/query-ac56599044f381115785e659ed4a52d74dc2502e7d3f73539b805bc77f38604a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "REPLACE INTO `nfts` (\n `launcher_id`,\n `coin_id`,\n `collection_id`,\n `minter_did`,\n `owner_did`,\n `visible`,\n `sensitive_content`,\n `name`,\n `is_owned`,\n `created_height`,\n `metadata_hash`,\n `edition_number`,\n `edition_total`\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "ac56599044f381115785e659ed4a52d74dc2502e7d3f73539b805bc77f38604a" -} diff --git a/.sqlx/query-ad3a8a8993fc1a1b69c943cd91c030fe372bef663d75c9236a28f2fe887de9cb.json b/.sqlx/query-ad3a8a8993fc1a1b69c943cd91c030fe372bef663d75c9236a28f2fe887de9cb.json new file mode 100644 index 000000000..6c17701fc --- /dev/null +++ b/.sqlx/query-ad3a8a8993fc1a1b69c943cd91c030fe372bef663d75c9236a28f2fe887de9cb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM files", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "ad3a8a8993fc1a1b69c943cd91c030fe372bef663d75c9236a28f2fe887de9cb" +} diff --git a/.sqlx/query-ae598e995df3798384fe694f71e74e71d870b29c258bed2653f182f46b26728b.json b/.sqlx/query-ae598e995df3798384fe694f71e74e71d870b29c258bed2653f182f46b26728b.json new file mode 100644 index 000000000..01087e9c6 --- /dev/null +++ b/.sqlx/query-ae598e995df3798384fe694f71e74e71d870b29c258bed2653f182f46b26728b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO dids (\n asset_id, metadata, recovery_list_hash, num_verifications_required\n )\n VALUES ((SELECT id FROM assets WHERE hash = ?), ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "ae598e995df3798384fe694f71e74e71d870b29c258bed2653f182f46b26728b" +} diff --git a/.sqlx/query-ae767c36d341dac05bcc53e3bbae85358661c8c3c8c285074d1bbee18e3f8250.json b/.sqlx/query-ae767c36d341dac05bcc53e3bbae85358661c8c3c8c285074d1bbee18e3f8250.json deleted file mode 100644 index ed0908cda..000000000 --- a/.sqlx/query-ae767c36d341dac05bcc53e3bbae85358661c8c3c8c285074d1bbee18e3f8250.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM `nft_thumbnails` WHERE `hash` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "ae767c36d341dac05bcc53e3bbae85358661c8c3c8c285074d1bbee18e3f8250" -} diff --git a/.sqlx/query-b1107c7179c86c248b75414ee0bbb457126896eedd6ebd925203c4b6df464976.json b/.sqlx/query-b1107c7179c86c248b75414ee0bbb457126896eedd6ebd925203c4b6df464976.json deleted file mode 100644 index 00720f21b..000000000 --- a/.sqlx/query-b1107c7179c86c248b75414ee0bbb457126896eedd6ebd925203c4b6df464976.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `p2_puzzle_hash`\n FROM `derivations`\n WHERE `index` = ?\n AND `hardened` = ?\n ", - "describe": { - "columns": [ - { - "name": "p2_puzzle_hash", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false - ] - }, - "hash": "b1107c7179c86c248b75414ee0bbb457126896eedd6ebd925203c4b6df464976" -} diff --git a/.sqlx/query-b149fb8c13040be35c66ad83a26ff681cb0482e08e99b214d199a36a2c570859.json b/.sqlx/query-b149fb8c13040be35c66ad83a26ff681cb0482e08e99b214d199a36a2c570859.json deleted file mode 100644 index b1c8bb4f6..000000000 --- a/.sqlx/query-b149fb8c13040be35c66ad83a26ff681cb0482e08e99b214d199a36a2c570859.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `collections` SET `collection_id` = ? WHERE `collection_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "b149fb8c13040be35c66ad83a26ff681cb0482e08e99b214d199a36a2c570859" -} diff --git a/.sqlx/query-baf4a9108bf169fcbfa69969a4dc9b94265f7d71abd1646ce839e8e7941a2a90.json b/.sqlx/query-baf4a9108bf169fcbfa69969a4dc9b94265f7d71abd1646ce839e8e7941a2a90.json deleted file mode 100644 index cdd81d0dc..000000000 --- a/.sqlx/query-baf4a9108bf169fcbfa69969a4dc9b94265f7d71abd1646ce839e8e7941a2a90.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE OR IGNORE `coin_states`\n SET `spent_unixtime` = ?\n WHERE `spent_height` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "baf4a9108bf169fcbfa69969a4dc9b94265f7d71abd1646ce839e8e7941a2a90" -} diff --git a/.sqlx/query-bcd575c48884cbc133f407af02fb963d47a8ef0326e3dbb5dc8ab5654d1f392c.json b/.sqlx/query-bcd575c48884cbc133f407af02fb963d47a8ef0326e3dbb5dc8ab5654d1f392c.json deleted file mode 100644 index 2c059f942..000000000 --- a/.sqlx/query-bcd575c48884cbc133f407af02fb963d47a8ef0326e3dbb5dc8ab5654d1f392c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `nft_coins` (\n `coin_id`,\n `parent_parent_coin_id`,\n `parent_inner_puzzle_hash`,\n `parent_amount`,\n `launcher_id`,\n `metadata`,\n `metadata_updater_puzzle_hash`,\n `current_owner`,\n `royalty_puzzle_hash`,\n `royalty_ten_thousandths`,\n `p2_puzzle_hash`,\n `data_hash`,\n `metadata_hash`,\n `license_hash`\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 14 - }, - "nullable": [] - }, - "hash": "bcd575c48884cbc133f407af02fb963d47a8ef0326e3dbb5dc8ab5654d1f392c" -} diff --git a/.sqlx/query-bd1062f80c3996fbb1266e4cec6a6c2f5067f48f3754535d97c68cc04bdf7a66.json b/.sqlx/query-bd1062f80c3996fbb1266e4cec6a6c2f5067f48f3754535d97c68cc04bdf7a66.json new file mode 100644 index 000000000..90584ce33 --- /dev/null +++ b/.sqlx/query-bd1062f80c3996fbb1266e4cec6a6c2f5067f48f3754535d97c68cc04bdf7a66.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) AS count FROM p2_puzzles WHERE hash = ?", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "bd1062f80c3996fbb1266e4cec6a6c2f5067f48f3754535d97c68cc04bdf7a66" +} diff --git a/.sqlx/query-bde44fb0ed16ae37308d0a454108753a91fc8cc8007eaff6344be30fdf885f53.json b/.sqlx/query-bde44fb0ed16ae37308d0a454108753a91fc8cc8007eaff6344be30fdf885f53.json new file mode 100644 index 000000000..e57a9e7e0 --- /dev/null +++ b/.sqlx/query-bde44fb0ed16ae37308d0a454108753a91fc8cc8007eaff6344be30fdf885f53.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT mempool_items.hash AS mempool_item_hash \n FROM mempool_items\n INNER JOIN mempool_coins ON mempool_coins.mempool_item_id = mempool_items.id\n INNER JOIN coins ON coins.hash = ?\n WHERE mempool_coins.is_output = TRUE\n ", + "describe": { + "columns": [ + { + "name": "mempool_item_hash", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "bde44fb0ed16ae37308d0a454108753a91fc8cc8007eaff6344be30fdf885f53" +} diff --git a/.sqlx/query-be2448253ba62dbb2b57d24f7347ba0924b3015a31108cfc8d4a9c3d494c192c.json b/.sqlx/query-be2448253ba62dbb2b57d24f7347ba0924b3015a31108cfc8d4a9c3d494c192c.json deleted file mode 100644 index 176a74de7..000000000 --- a/.sqlx/query-be2448253ba62dbb2b57d24f7347ba0924b3015a31108cfc8d4a9c3d494c192c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `nfts` SET `visible` = ? WHERE `launcher_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "be2448253ba62dbb2b57d24f7347ba0924b3015a31108cfc8d4a9c3d494c192c" -} diff --git a/.sqlx/query-be4915e1e47971f74f9972e4b78fc645cb9cfc8d6d14d817ff09738b78f38524.json b/.sqlx/query-be4915e1e47971f74f9972e4b78fc645cb9cfc8d6d14d817ff09738b78f38524.json deleted file mode 100644 index 9971caa02..000000000 --- a/.sqlx/query-be4915e1e47971f74f9972e4b78fc645cb9cfc8d6d14d817ff09738b78f38524.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `coin_states`.`coin_id`\n FROM `coin_states` INDEXED BY `coin_spent`\n INNER JOIN `did_coins` ON `coin_states`.`coin_id` = `did_coins`.`coin_id`\n WHERE `spent_height` IS NULL\n ", - "describe": { - "columns": [ - { - "name": "coin_id", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "be4915e1e47971f74f9972e4b78fc645cb9cfc8d6d14d817ff09738b78f38524" -} diff --git a/.sqlx/query-bf1c1fbda216a00dd500e18326e6f487962c16e0125e780815105b6ab5480dc1.json b/.sqlx/query-bf1c1fbda216a00dd500e18326e6f487962c16e0125e780815105b6ab5480dc1.json new file mode 100644 index 000000000..33b7b1c4a --- /dev/null +++ b/.sqlx/query-bf1c1fbda216a00dd500e18326e6f487962c16e0125e780815105b6ab5480dc1.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n key, expiration_seconds, creator_puzzle_hash,\n option_asset.hash AS launcher_id,\n strike_asset.hash AS strike_asset_hash,\n strike_asset.id AS strike_asset_id,\n strike_hidden_puzzle_hash, strike_settlement_puzzle_hash,\n coins.amount AS underlying_amount, strike_amount\n FROM p2_puzzles\n INNER JOIN p2_options ON p2_options.p2_puzzle_id = p2_puzzles.id\n INNER JOIN assets AS option_asset ON option_asset.id = p2_options.option_asset_id\n INNER JOIN options ON options.asset_id = option_asset.id\n INNER JOIN assets AS strike_asset ON strike_asset.id = options.strike_asset_id\n INNER JOIN public_keys ON public_keys.p2_puzzle_id IN (\n SELECT id FROM p2_puzzles\n WHERE hash = options.creator_puzzle_hash\n AND unixepoch() >= options.expiration_seconds\n )\n INNER JOIN coins ON coins.hash = options.underlying_coin_hash\n WHERE p2_puzzles.hash = ?\n ", + "describe": { + "columns": [ + { + "name": "key", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "expiration_seconds", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "creator_puzzle_hash", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "launcher_id", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "strike_asset_hash", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "strike_asset_id", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "strike_hidden_puzzle_hash", + "ordinal": 6, + "type_info": "Blob" + }, + { + "name": "strike_settlement_puzzle_hash", + "ordinal": 7, + "type_info": "Blob" + }, + { + "name": "underlying_amount", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "strike_amount", + "ordinal": 9, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + true, + false, + false + ] + }, + "hash": "bf1c1fbda216a00dd500e18326e6f487962c16e0125e780815105b6ab5480dc1" +} diff --git a/.sqlx/query-c1b2ac8d018904a93e9e3c717e1a571fcda87b07c43416e757f5f3387a948c2c.json b/.sqlx/query-c1b2ac8d018904a93e9e3c717e1a571fcda87b07c43416e757f5f3387a948c2c.json new file mode 100644 index 000000000..426271b3e --- /dev/null +++ b/.sqlx/query-c1b2ac8d018904a93e9e3c717e1a571fcda87b07c43416e757f5f3387a948c2c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO mempool_coins (mempool_item_id, coin_id, is_input, is_output)\n VALUES ((SELECT id FROM mempool_items WHERE hash = ?), (SELECT id FROM coins WHERE hash = ?), ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "c1b2ac8d018904a93e9e3c717e1a571fcda87b07c43416e757f5f3387a948c2c" +} diff --git a/.sqlx/query-c267b6101039aff3465ac3ba7ded9a01404be4cb5781d74091bf022196cd2fad.json b/.sqlx/query-c267b6101039aff3465ac3ba7ded9a01404be4cb5781d74091bf022196cd2fad.json new file mode 100644 index 000000000..ec49e4b4f --- /dev/null +++ b/.sqlx/query-c267b6101039aff3465ac3ba7ded9a01404be4cb5781d74091bf022196cd2fad.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO collections (\n hash, uuid, minter_hash, name, icon_url,\n banner_url, description, is_visible\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "c267b6101039aff3465ac3ba7ded9a01404be4cb5781d74091bf022196cd2fad" +} diff --git a/.sqlx/query-c28c5534fa54f929c81d02b31276dfb5d621da40c7adc621a055e9b72d4dae30.json b/.sqlx/query-c28c5534fa54f929c81d02b31276dfb5d621da40c7adc621a055e9b72d4dae30.json deleted file mode 100644 index 15255bd19..000000000 --- a/.sqlx/query-c28c5534fa54f929c81d02b31276dfb5d621da40c7adc621a055e9b72d4dae30.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n WITH distinct_dids AS (\n SELECT DISTINCT minter_did \n FROM nfts \n WHERE minter_did IS NOT NULL\n )\n SELECT \n minter_did,\n COUNT(*) OVER() AS total_count\n FROM distinct_dids\n LIMIT ? OFFSET ?\n ", - "describe": { - "columns": [ - { - "name": "minter_did", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "total_count", - "ordinal": 1, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - true, - false - ] - }, - "hash": "c28c5534fa54f929c81d02b31276dfb5d621da40c7adc621a055e9b72d4dae30" -} diff --git a/.sqlx/query-c3eda0483d42cf1dc0ab7ea0fc8712469e66b6c4f7661f81c44c278d8ef48841.json b/.sqlx/query-c3eda0483d42cf1dc0ab7ea0fc8712469e66b6c4f7661f81c44c278d8ef48841.json deleted file mode 100644 index b87d7a5a0..000000000 --- a/.sqlx/query-c3eda0483d42cf1dc0ab7ea0fc8712469e66b6c4f7661f81c44c278d8ef48841.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `coin_states`.`amount` FROM `coin_states` INDEXED BY `coin_kind_spent`\n LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id`\n WHERE `coin_states`.`spent_height` IS NULL\n AND `transaction_spends`.`coin_id` IS NULL\n AND `kind` = 1\n ", - "describe": { - "columns": [ - { - "name": "amount", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "c3eda0483d42cf1dc0ab7ea0fc8712469e66b6c4f7661f81c44c278d8ef48841" -} diff --git a/.sqlx/query-b4ad9892410265bb0a06e51d8de8eb35e1c2bcfb1a276f633cf2493f2038231f.json b/.sqlx/query-c4989f8841631ca517d19ae6db282124662a0368439e4ae25e55256f5ea102c7.json similarity index 53% rename from .sqlx/query-b4ad9892410265bb0a06e51d8de8eb35e1c2bcfb1a276f633cf2493f2038231f.json rename to .sqlx/query-c4989f8841631ca517d19ae6db282124662a0368439e4ae25e55256f5ea102c7.json index a70915555..cf9bdb818 100644 --- a/.sqlx/query-b4ad9892410265bb0a06e51d8de8eb35e1c2bcfb1a276f633cf2493f2038231f.json +++ b/.sqlx/query-c4989f8841631ca517d19ae6db282124662a0368439e4ae25e55256f5ea102c7.json @@ -1,20 +1,20 @@ { "db_name": "SQLite", - "query": "SELECT COUNT(*) FROM collections", + "query": "SELECT kind FROM p2_puzzles WHERE hash = ?", "describe": { "columns": [ { - "name": "COUNT(*)", + "name": "kind", "ordinal": 0, "type_info": "Integer" } ], "parameters": { - "Right": 0 + "Right": 1 }, "nullable": [ false ] }, - "hash": "b4ad9892410265bb0a06e51d8de8eb35e1c2bcfb1a276f633cf2493f2038231f" + "hash": "c4989f8841631ca517d19ae6db282124662a0368439e4ae25e55256f5ea102c7" } diff --git a/.sqlx/query-c4a5ecd631de42f851bf90100d8f62de5b0181d60dda8670065ad8702ba13aba.json b/.sqlx/query-c4a5ecd631de42f851bf90100d8f62de5b0181d60dda8670065ad8702ba13aba.json deleted file mode 100644 index d1ee994fc..000000000 --- a/.sqlx/query-c4a5ecd631de42f851bf90100d8f62de5b0181d60dda8670065ad8702ba13aba.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `collections` SET `visible` = ? WHERE `collection_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "c4a5ecd631de42f851bf90100d8f62de5b0181d60dda8670065ad8702ba13aba" -} diff --git a/.sqlx/query-c5b1a7d051635ceafd46e18402b2016854de70dbeca4699fcc09621a906a7ca5.json b/.sqlx/query-c5b1a7d051635ceafd46e18402b2016854de70dbeca4699fcc09621a906a7ca5.json new file mode 100644 index 000000000..cfe6a0f3e --- /dev/null +++ b/.sqlx/query-c5b1a7d051635ceafd46e18402b2016854de70dbeca4699fcc09621a906a7ca5.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT hash, uri, last_checked_timestamp, failed_attempts\n FROM file_uris\n INNER JOIN files ON files.id = file_uris.file_id\n WHERE data IS NULL\n AND (last_checked_timestamp IS NULL OR unixepoch() - last_checked_timestamp >= ?)\n AND failed_attempts < ?\n ORDER BY last_checked_timestamp ASC\n LIMIT ?\n ", + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "uri", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "last_checked_timestamp", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "failed_attempts", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + true, + false + ] + }, + "hash": "c5b1a7d051635ceafd46e18402b2016854de70dbeca4699fcc09621a906a7ca5" +} diff --git a/.sqlx/query-c705ffec23bfb844c9b1032ae37d10f84bb89461e4acbeaadde7230943b2576b.json b/.sqlx/query-c705ffec23bfb844c9b1032ae37d10f84bb89461e4acbeaadde7230943b2576b.json deleted file mode 100644 index 41c2d8cb8..000000000 --- a/.sqlx/query-c705ffec23bfb844c9b1032ae37d10f84bb89461e4acbeaadde7230943b2576b.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT COUNT(*) AS `total`\n FROM `coin_states`\n ", - "describe": { - "columns": [ - { - "name": "total", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "c705ffec23bfb844c9b1032ae37d10f84bb89461e4acbeaadde7230943b2576b" -} diff --git a/.sqlx/query-c7e2c303ecfe666535e9a21e0d8e64103d4fe702e201c61ba2c5d5453fb8843c.json b/.sqlx/query-c7e2c303ecfe666535e9a21e0d8e64103d4fe702e201c61ba2c5d5453fb8843c.json deleted file mode 100644 index 9323bea20..000000000 --- a/.sqlx/query-c7e2c303ecfe666535e9a21e0d8e64103d4fe702e201c61ba2c5d5453fb8843c.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states`\n INNER JOIN `cat_coins` ON `coin_states`.coin_id = `cat_coins`.coin_id\n WHERE `asset_id` = ?\n AND `spent_height` IS NULL\n AND `created_height` IS NOT NULL\n ORDER BY `created_height`, `coin_states`.`coin_id` LIMIT ? OFFSET ?\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "spent_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "c7e2c303ecfe666535e9a21e0d8e64103d4fe702e201c61ba2c5d5453fb8843c" -} diff --git a/.sqlx/query-cb02cbe48e987386cd8649aee5aa08df514de8a8daf033c81fdd5c3e2ca1eaad.json b/.sqlx/query-cb02cbe48e987386cd8649aee5aa08df514de8a8daf033c81fdd5c3e2ca1eaad.json new file mode 100644 index 000000000..9f4e8b5fb --- /dev/null +++ b/.sqlx/query-cb02cbe48e987386cd8649aee5aa08df514de8a8daf033c81fdd5c3e2ca1eaad.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE nfts\n SET\n collection_id = (SELECT id FROM collections WHERE hash = ?),\n minter_hash = ?,\n owner_hash = ?,\n metadata = ?,\n metadata_updater_puzzle_hash = ?,\n royalty_puzzle_hash = ?,\n royalty_basis_points = ?,\n data_hash = ?,\n metadata_hash = ?,\n license_hash = ?,\n edition_number = ?,\n edition_total = ?\n WHERE asset_id = (SELECT id FROM assets WHERE hash = ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "cb02cbe48e987386cd8649aee5aa08df514de8a8daf033c81fdd5c3e2ca1eaad" +} diff --git a/.sqlx/query-0fb39ae38c33ccebf86306bbd284c6422043c4cc92fbe1eb5779b4a3449002c9.json b/.sqlx/query-cb3a74b912417fef1b29f55002d74edc7c55bb39fc5169c252d92bdc57f5a3af.json similarity index 54% rename from .sqlx/query-0fb39ae38c33ccebf86306bbd284c6422043c4cc92fbe1eb5779b4a3449002c9.json rename to .sqlx/query-cb3a74b912417fef1b29f55002d74edc7c55bb39fc5169c252d92bdc57f5a3af.json index abc0299be..bd41e8196 100644 --- a/.sqlx/query-0fb39ae38c33ccebf86306bbd284c6422043c4cc92fbe1eb5779b4a3449002c9.json +++ b/.sqlx/query-cb3a74b912417fef1b29f55002d74edc7c55bb39fc5169c252d92bdc57f5a3af.json @@ -1,47 +1,52 @@ { "db_name": "SQLite", - "query": "SELECT * FROM `offer_nfts` WHERE `offer_id` = ?", + "query": "SELECT id, hash, uuid, minter_hash, name, icon_url, banner_url, description, is_visible \n FROM collections\n WHERE hash = ?", "describe": { "columns": [ { - "name": "offer_id", + "name": "id", "ordinal": 0, - "type_info": "Blob" + "type_info": "Integer" }, { - "name": "requested", + "name": "hash", "ordinal": 1, - "type_info": "Bool" + "type_info": "Blob" }, { - "name": "launcher_id", + "name": "uuid", "ordinal": 2, - "type_info": "Blob" + "type_info": "Text" }, { - "name": "royalty_puzzle_hash", + "name": "minter_hash", "ordinal": 3, "type_info": "Blob" }, { - "name": "royalty_ten_thousandths", + "name": "name", "ordinal": 4, - "type_info": "Integer" + "type_info": "Text" }, { - "name": "name", + "name": "icon_url", "ordinal": 5, "type_info": "Text" }, { - "name": "thumbnail", + "name": "banner_url", "ordinal": 6, - "type_info": "Blob" + "type_info": "Text" }, { - "name": "thumbnail_mime_type", + "name": "description", "ordinal": 7, "type_info": "Text" + }, + { + "name": "is_visible", + "ordinal": 8, + "type_info": "Bool" } ], "parameters": { @@ -52,11 +57,12 @@ false, false, false, - false, true, true, - true + true, + true, + false ] }, - "hash": "0fb39ae38c33ccebf86306bbd284c6422043c4cc92fbe1eb5779b4a3449002c9" + "hash": "cb3a74b912417fef1b29f55002d74edc7c55bb39fc5169c252d92bdc57f5a3af" } diff --git a/.sqlx/query-cc24879f9012f1e056cc0fcfc664437aee6a72b858918c0ca70776ef36b50f33.json b/.sqlx/query-cc24879f9012f1e056cc0fcfc664437aee6a72b858918c0ca70776ef36b50f33.json deleted file mode 100644 index cb6292baa..000000000 --- a/.sqlx/query-cc24879f9012f1e056cc0fcfc664437aee6a72b858918c0ca70776ef36b50f33.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT COUNT(*) FROM collections WHERE visible = 1", - "describe": { - "columns": [ - { - "name": "COUNT(*)", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "cc24879f9012f1e056cc0fcfc664437aee6a72b858918c0ca70776ef36b50f33" -} diff --git a/.sqlx/query-47d39211fec4faae5520769cb47b3f6f1e0e8f106c1d5b4423abb09544a55040.json b/.sqlx/query-ccbfe5e0271b73c8b567954de158e91a6ff7e1c71a035d7aeb1a4ce1aa634c85.json similarity index 51% rename from .sqlx/query-47d39211fec4faae5520769cb47b3f6f1e0e8f106c1d5b4423abb09544a55040.json rename to .sqlx/query-ccbfe5e0271b73c8b567954de158e91a6ff7e1c71a035d7aeb1a4ce1aa634c85.json index d4b48c18f..127ed8e7d 100644 --- a/.sqlx/query-47d39211fec4faae5520769cb47b3f6f1e0e8f106c1d5b4423abb09544a55040.json +++ b/.sqlx/query-ccbfe5e0271b73c8b567954de158e91a6ff7e1c71a035d7aeb1a4ce1aa634c85.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT `hash`, `uri` FROM `nft_uris` WHERE `checked` = 0 LIMIT ?", + "query": "SELECT hash, minter_hash FROM nfts INNER JOIN assets ON assets.id = asset_id WHERE metadata_hash = ?", "describe": { "columns": [ { @@ -9,9 +9,9 @@ "type_info": "Blob" }, { - "name": "uri", + "name": "minter_hash", "ordinal": 1, - "type_info": "Text" + "type_info": "Blob" } ], "parameters": { @@ -19,8 +19,8 @@ }, "nullable": [ false, - false + true ] }, - "hash": "47d39211fec4faae5520769cb47b3f6f1e0e8f106c1d5b4423abb09544a55040" + "hash": "ccbfe5e0271b73c8b567954de158e91a6ff7e1c71a035d7aeb1a4ce1aa634c85" } diff --git a/.sqlx/query-ccdbb66c8c90b0117b5919a92c2785d7570b967a9a3a266163809a241308415d.json b/.sqlx/query-ccdbb66c8c90b0117b5919a92c2785d7570b967a9a3a266163809a241308415d.json deleted file mode 100644 index c99a926b1..000000000 --- a/.sqlx/query-ccdbb66c8c90b0117b5919a92c2785d7570b967a9a3a266163809a241308415d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n REPLACE INTO `peaks` (`height`, `header_hash`)\n VALUES (?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "ccdbb66c8c90b0117b5919a92c2785d7570b967a9a3a266163809a241308415d" -} diff --git a/.sqlx/query-ce2df892fe8381ec398f0b97741d36faf3c00c70aae47ec3818462e29a4e400a.json b/.sqlx/query-ce2df892fe8381ec398f0b97741d36faf3c00c70aae47ec3818462e29a4e400a.json deleted file mode 100644 index 5ba43dcc4..000000000 --- a/.sqlx/query-ce2df892fe8381ec398f0b97741d36faf3c00c70aae47ec3818462e29a4e400a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `collections` (\n `collection_id`,\n `did_id`,\n `metadata_collection_id`,\n `visible`,\n `name`,\n `icon`\n )\n VALUES (?, ?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "ce2df892fe8381ec398f0b97741d36faf3c00c70aae47ec3818462e29a4e400a" -} diff --git a/.sqlx/query-d00ab63cf624ef6b712c93e3a30442776c696ad3da620627fd2db2e05d6ade3a.json b/.sqlx/query-d00ab63cf624ef6b712c93e3a30442776c696ad3da620627fd2db2e05d6ade3a.json deleted file mode 100644 index 38128e944..000000000 --- a/.sqlx/query-d00ab63cf624ef6b712c93e3a30442776c696ad3da620627fd2db2e05d6ade3a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT OR IGNORE INTO `derivations` (`p2_puzzle_hash`, `index`, `hardened`, `synthetic_key`)\n VALUES (?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "d00ab63cf624ef6b712c93e3a30442776c696ad3da620627fd2db2e05d6ade3a" -} diff --git a/.sqlx/query-d033d94daf4703a65deb9d902aa3014828efe2982d8631aabcaf2508a345216c.json b/.sqlx/query-d033d94daf4703a65deb9d902aa3014828efe2982d8631aabcaf2508a345216c.json deleted file mode 100644 index 6d2e9f383..000000000 --- a/.sqlx/query-d033d94daf4703a65deb9d902aa3014828efe2982d8631aabcaf2508a345216c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `nft_uris` SET `checked` = 0 WHERE `uri` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "d033d94daf4703a65deb9d902aa3014828efe2982d8631aabcaf2508a345216c" -} diff --git a/.sqlx/query-d2defd73743b149b96d3e5e9597f433128d7f5144e30c4b5371e0a87dd5caabf.json b/.sqlx/query-d2defd73743b149b96d3e5e9597f433128d7f5144e30c4b5371e0a87dd5caabf.json deleted file mode 100644 index 0dd6ce16c..000000000 --- a/.sqlx/query-d2defd73743b149b96d3e5e9597f433128d7f5144e30c4b5371e0a87dd5caabf.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE `cats` SET `fetched` = 0 WHERE `asset_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "d2defd73743b149b96d3e5e9597f433128d7f5144e30c4b5371e0a87dd5caabf" -} diff --git a/.sqlx/query-d42c88a6e6ae43215089b798e6c021e03bf58c55439f7e3bead0d8aa3db18863.json b/.sqlx/query-d42c88a6e6ae43215089b798e6c021e03bf58c55439f7e3bead0d8aa3db18863.json deleted file mode 100644 index 4c4f5e47a..000000000 --- a/.sqlx/query-d42c88a6e6ae43215089b798e6c021e03bf58c55439f7e3bead0d8aa3db18863.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR IGNORE INTO `nft_uris` (`hash`, `uri`, `checked`, `hash_matches`) VALUES (?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "d42c88a6e6ae43215089b798e6c021e03bf58c55439f7e3bead0d8aa3db18863" -} diff --git a/.sqlx/query-d5d05aafad60309b27bb7e7ac9392b8c2e486aa26262d82f2c0ce89fdd19d70b.json b/.sqlx/query-d5d05aafad60309b27bb7e7ac9392b8c2e486aa26262d82f2c0ce89fdd19d70b.json deleted file mode 100644 index 90261d67b..000000000 --- a/.sqlx/query-d5d05aafad60309b27bb7e7ac9392b8c2e486aa26262d82f2c0ce89fdd19d70b.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE OR IGNORE `coin_states`\n SET `created_unixtime` = ?\n WHERE `created_height` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "d5d05aafad60309b27bb7e7ac9392b8c2e486aa26262d82f2c0ce89fdd19d70b" -} diff --git a/.sqlx/query-d64cd2cc9b3770abe5856ed378d407a274821528b71c61ae275bcc4fc821eddf.json b/.sqlx/query-d86c5743b146391f9b25f296a3af9a2f98a130cbe20e61c1f8cb9a139ff5c50f.json similarity index 64% rename from .sqlx/query-d64cd2cc9b3770abe5856ed378d407a274821528b71c61ae275bcc4fc821eddf.json rename to .sqlx/query-d86c5743b146391f9b25f296a3af9a2f98a130cbe20e61c1f8cb9a139ff5c50f.json index 1abe2f319..3f4e0f53b 100644 --- a/.sqlx/query-d64cd2cc9b3770abe5856ed378d407a274821528b71c61ae275bcc4fc821eddf.json +++ b/.sqlx/query-d86c5743b146391f9b25f296a3af9a2f98a130cbe20e61c1f8cb9a139ff5c50f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT COUNT(*) AS `count` FROM `nft_uris`", + "query": "SELECT COUNT(*) AS count FROM coins", "describe": { "columns": [ { @@ -16,5 +16,5 @@ false ] }, - "hash": "d64cd2cc9b3770abe5856ed378d407a274821528b71c61ae275bcc4fc821eddf" + "hash": "d86c5743b146391f9b25f296a3af9a2f98a130cbe20e61c1f8cb9a139ff5c50f" } diff --git a/.sqlx/query-d926e2a511801d06b405d089f13a98ca589afbfcef98fc5092a1a02a415f9b5e.json b/.sqlx/query-d926e2a511801d06b405d089f13a98ca589afbfcef98fc5092a1a02a415f9b5e.json new file mode 100644 index 000000000..bb3089785 --- /dev/null +++ b/.sqlx/query-d926e2a511801d06b405d089f13a98ca589afbfcef98fc5092a1a02a415f9b5e.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT hash, aggregated_signature, fee, submitted_timestamp\n FROM mempool_items\n WHERE submitted_timestamp IS NULL OR unixepoch() - submitted_timestamp >= ?\n LIMIT ?\n ", + "describe": { + "columns": [ + { + "name": "hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "aggregated_signature", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "fee", + "ordinal": 2, + "type_info": "Blob" + }, + { + "name": "submitted_timestamp", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "d926e2a511801d06b405d089f13a98ca589afbfcef98fc5092a1a02a415f9b5e" +} diff --git a/.sqlx/query-957be4a61c0438d1ea008fbf9288f755bdb7b31770078f6260fcd13fc81a3364.json b/.sqlx/query-d95629546bac213f319bd7e980e5a56a48319b94d7a9e42ede505d43131254cc.json similarity index 55% rename from .sqlx/query-957be4a61c0438d1ea008fbf9288f755bdb7b31770078f6260fcd13fc81a3364.json rename to .sqlx/query-d95629546bac213f319bd7e980e5a56a48319b94d7a9e42ede505d43131254cc.json index b459b44d0..7a32a77a4 100644 --- a/.sqlx/query-957be4a61c0438d1ea008fbf9288f755bdb7b31770078f6260fcd13fc81a3364.json +++ b/.sqlx/query-d95629546bac213f319bd7e980e5a56a48319b94d7a9e42ede505d43131254cc.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT `height`, `header_hash`\n FROM `peaks`\n ORDER BY `height` DESC\n LIMIT 1\n ", + "query": "\n SELECT height, header_hash\n FROM blocks\n WHERE header_hash IS NOT NULL AND is_peak = TRUE\n ORDER BY height DESC\n LIMIT 1\n ", "describe": { "columns": [ { @@ -19,8 +19,8 @@ }, "nullable": [ false, - false + true ] }, - "hash": "957be4a61c0438d1ea008fbf9288f755bdb7b31770078f6260fcd13fc81a3364" + "hash": "d95629546bac213f319bd7e980e5a56a48319b94d7a9e42ede505d43131254cc" } diff --git a/.sqlx/query-da4555012028d63d9d53a85c6247f0cc8eefbd24c6091ec6d465f52da3490202.json b/.sqlx/query-da4555012028d63d9d53a85c6247f0cc8eefbd24c6091ec6d465f52da3490202.json deleted file mode 100644 index ed2828845..000000000 --- a/.sqlx/query-da4555012028d63d9d53a85c6247f0cc8eefbd24c6091ec6d465f52da3490202.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `nfts` SET `collection_id` = ? WHERE `collection_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "da4555012028d63d9d53a85c6247f0cc8eefbd24c6091ec6d465f52da3490202" -} diff --git a/.sqlx/query-dbfe6794b0d552111c01cec93995a4af753cbee36b430767e372debb5df15ce4.json b/.sqlx/query-dbfe6794b0d552111c01cec93995a4af753cbee36b430767e372debb5df15ce4.json deleted file mode 100644 index f6b8b5499..000000000 --- a/.sqlx/query-dbfe6794b0d552111c01cec93995a4af753cbee36b430767e372debb5df15ce4.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT \n collection_id, \n did_id, \n metadata_collection_id, \n visible, \n name, \n icon,\n COUNT(*) OVER() as total_count\n FROM collections INDEXED BY col_name\n WHERE visible = 1\n ORDER BY name IS NOT NULL DESC, name ASC, collection_id ASC\n LIMIT ? OFFSET ?\n ", - "describe": { - "columns": [ - { - "name": "collection_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "did_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "metadata_collection_id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "visible", - "ordinal": 3, - "type_info": "Bool" - }, - { - "name": "name", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "icon", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "total_count", - "ordinal": 6, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - false - ] - }, - "hash": "dbfe6794b0d552111c01cec93995a4af753cbee36b430767e372debb5df15ce4" -} diff --git a/.sqlx/query-dc4778a9fdbb7a4071b925a01335de0fd08a55a97c5148f3d1252321256b0523.json b/.sqlx/query-dc4778a9fdbb7a4071b925a01335de0fd08a55a97c5148f3d1252321256b0523.json new file mode 100644 index 000000000..88030dec5 --- /dev/null +++ b/.sqlx/query-dc4778a9fdbb7a4071b925a01335de0fd08a55a97c5148f3d1252321256b0523.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE collections SET is_visible = ? WHERE hash = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "dc4778a9fdbb7a4071b925a01335de0fd08a55a97c5148f3d1252321256b0523" +} diff --git a/.sqlx/query-ddb15714a57d0084e6766e94b28c9090812dc6ba093d364b0732bc2f8e348e83.json b/.sqlx/query-ddb15714a57d0084e6766e94b28c9090812dc6ba093d364b0732bc2f8e348e83.json deleted file mode 100644 index 9e532c6f6..000000000 --- a/.sqlx/query-ddb15714a57d0084e6766e94b28c9090812dc6ba093d364b0732bc2f8e348e83.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT COUNT(*) as count\n FROM coin_states\n LEFT JOIN transaction_spends ON transaction_spends.coin_id = coin_states.coin_id\n WHERE 1=1 \n AND created_height IS NOT NULL\n AND spent_height IS NULL\n AND coin_states.transaction_id IS NULL\n AND transaction_spends.coin_id IS NULL\n AND `kind` = 1\n ", - "describe": { - "columns": [ - { - "name": "count", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "ddb15714a57d0084e6766e94b28c9090812dc6ba093d364b0732bc2f8e348e83" -} diff --git a/.sqlx/query-ddb6fea0a5725460382c09bf61fff370567e194f16fe55aae3059a98bccd28e3.json b/.sqlx/query-ddb6fea0a5725460382c09bf61fff370567e194f16fe55aae3059a98bccd28e3.json deleted file mode 100644 index 594803ffc..000000000 --- a/.sqlx/query-ddb6fea0a5725460382c09bf61fff370567e194f16fe55aae3059a98bccd28e3.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE `coin_states` SET `kind` = 1 WHERE `coin_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "ddb6fea0a5725460382c09bf61fff370567e194f16fe55aae3059a98bccd28e3" -} diff --git a/.sqlx/query-dddf21489fd32fffe7e171910cff66e1976e161cc9b1ab44ca5750d7b8c5417b.json b/.sqlx/query-dddf21489fd32fffe7e171910cff66e1976e161cc9b1ab44ca5750d7b8c5417b.json new file mode 100644 index 000000000..10c6dcac2 --- /dev/null +++ b/.sqlx/query-dddf21489fd32fffe7e171910cff66e1976e161cc9b1ab44ca5750d7b8c5417b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO coins\n (hash, parent_coin_hash, puzzle_hash, amount, created_height, spent_height)\n VALUES\n (?, ?, ?, ?, ?, ?)\n ON CONFLICT(hash) DO UPDATE SET\n created_height = excluded.created_height,\n spent_height = excluded.spent_height\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "dddf21489fd32fffe7e171910cff66e1976e161cc9b1ab44ca5750d7b8c5417b" +} diff --git a/.sqlx/query-ddf44629ce05d5213fe3f52eefb8b1fd0a2381d32c09a37c3452bde4f6bb356c.json b/.sqlx/query-ddf44629ce05d5213fe3f52eefb8b1fd0a2381d32c09a37c3452bde4f6bb356c.json new file mode 100644 index 000000000..747f8db78 --- /dev/null +++ b/.sqlx/query-ddf44629ce05d5213fe3f52eefb8b1fd0a2381d32c09a37c3452bde4f6bb356c.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n p2_puzzles.hash AS p2_puzzle_hash,\n public_keys.derivation_index,\n public_keys.is_hardened,\n public_keys.key AS synthetic_key,\n COUNT(*) OVER() AS total\n FROM p2_puzzles\n INNER JOIN public_keys ON public_keys.p2_puzzle_id = p2_puzzles.id\n WHERE public_keys.is_hardened = ?\n ORDER BY public_keys.derivation_index ASC\n LIMIT ? OFFSET ?\n ", + "describe": { + "columns": [ + { + "name": "p2_puzzle_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "derivation_index", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "is_hardened", + "ordinal": 2, + "type_info": "Bool" + }, + { + "name": "synthetic_key", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "total", + "ordinal": 4, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "ddf44629ce05d5213fe3f52eefb8b1fd0a2381d32c09a37c3452bde4f6bb356c" +} diff --git a/.sqlx/query-de862ad2cabdbe2ed6fdaebe3fb098a44978337234c1cac92179994c8a9c6eac.json b/.sqlx/query-de862ad2cabdbe2ed6fdaebe3fb098a44978337234c1cac92179994c8a9c6eac.json new file mode 100644 index 000000000..7556f1143 --- /dev/null +++ b/.sqlx/query-de862ad2cabdbe2ed6fdaebe3fb098a44978337234c1cac92179994c8a9c6eac.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM blocks", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "de862ad2cabdbe2ed6fdaebe3fb098a44978337234c1cac92179994c8a9c6eac" +} diff --git a/.sqlx/query-e0db9e9d4087d424a9ac831f673f622bd569b117ba045f65e3afe29dc95cb82c.json b/.sqlx/query-e0db9e9d4087d424a9ac831f673f622bd569b117ba045f65e3afe29dc95cb82c.json deleted file mode 100644 index 01c2fbb14..000000000 --- a/.sqlx/query-e0db9e9d4087d424a9ac831f673f622bd569b117ba045f65e3afe29dc95cb82c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE `coin_states`\n SET `transaction_id` = NULL\n WHERE `transaction_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "e0db9e9d4087d424a9ac831f673f622bd569b117ba045f65e3afe29dc95cb82c" -} diff --git a/.sqlx/query-e1481c4bccf1c591b7fa197681b9f7be06025ed7ca20f970452f9fd2697a8211.json b/.sqlx/query-e1481c4bccf1c591b7fa197681b9f7be06025ed7ca20f970452f9fd2697a8211.json new file mode 100644 index 000000000..f68c96a8f --- /dev/null +++ b/.sqlx/query-e1481c4bccf1c591b7fa197681b9f7be06025ed7ca20f970452f9fd2697a8211.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE coins SET is_children_synced = FALSE WHERE id IN (\n SELECT coin_id FROM mempool_coins\n INNER JOIN mempool_items ON mempool_items.id = mempool_coins.mempool_item_id\n WHERE mempool_items.hash = ? AND is_input = TRUE\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "e1481c4bccf1c591b7fa197681b9f7be06025ed7ca20f970452f9fd2697a8211" +} diff --git a/.sqlx/query-72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11.json b/.sqlx/query-e28bd12b98847231ff06752e774231aeab735f91414c33de4158f2701f25e9f8.json similarity index 54% rename from .sqlx/query-72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11.json rename to .sqlx/query-e28bd12b98847231ff06752e774231aeab735f91414c33de4158f2701f25e9f8.json index b71da9b27..a3c92cd1f 100644 --- a/.sqlx/query-72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11.json +++ b/.sqlx/query-e28bd12b98847231ff06752e774231aeab735f91414c33de4158f2701f25e9f8.json @@ -1,15 +1,15 @@ { "db_name": "SQLite", - "query": "\n SELECT `index`, `hardened`\n FROM `derivations`\n WHERE `synthetic_key` = ?\n ", + "query": "SELECT derivation_index, is_hardened FROM public_keys WHERE key = ?", "describe": { "columns": [ { - "name": "index", + "name": "derivation_index", "ordinal": 0, "type_info": "Integer" }, { - "name": "hardened", + "name": "is_hardened", "ordinal": 1, "type_info": "Bool" } @@ -22,5 +22,5 @@ false ] }, - "hash": "72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11" + "hash": "e28bd12b98847231ff06752e774231aeab735f91414c33de4158f2701f25e9f8" } diff --git a/.sqlx/query-e4e9561996f931ad5703c9434632218a11e9df3f9f3a5711001c95aee4c75e92.json b/.sqlx/query-e4e9561996f931ad5703c9434632218a11e9df3f9f3a5711001c95aee4c75e92.json new file mode 100644 index 000000000..8b39fb72f --- /dev/null +++ b/.sqlx/query-e4e9561996f931ad5703c9434632218a11e9df3f9f3a5711001c95aee4c75e92.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT mempool_items.hash AS mempool_item_hash \n FROM mempool_items\n INNER JOIN mempool_coins ON mempool_coins.mempool_item_id = mempool_items.id\n INNER JOIN coins ON coins.hash = ?\n WHERE mempool_coins.is_input = TRUE\n ", + "describe": { + "columns": [ + { + "name": "mempool_item_hash", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "e4e9561996f931ad5703c9434632218a11e9df3f9f3a5711001c95aee4c75e92" +} diff --git a/.sqlx/query-e531b06fc1144b65d9c3821b89d8ec5a95a7e12c74636270d37e45afd9c96f32.json b/.sqlx/query-e531b06fc1144b65d9c3821b89d8ec5a95a7e12c74636270d37e45afd9c96f32.json deleted file mode 100644 index caeffec3a..000000000 --- a/.sqlx/query-e531b06fc1144b65d9c3821b89d8ec5a95a7e12c74636270d37e45afd9c96f32.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT COUNT(*) AS `count`\n FROM `coin_states`\n LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id`\n LEFT JOIN `offered_coins` ON `coin_states`.`coin_id` = `offered_coins`.`coin_id`\n LEFT JOIN `offers` ON `offered_coins`.`offer_id` = `offers`.`offer_id`\n WHERE `coin_states`.`coin_id` = ?\n AND (`offers`.`offer_id` IS NULL OR `offers`.`status` > 0)\n AND `coin_states`.`transaction_id` IS NULL\n AND `transaction_spends`.`transaction_id` IS NULL\n ", - "describe": { - "columns": [ - { - "name": "count", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "e531b06fc1144b65d9c3821b89d8ec5a95a7e12c74636270d37e45afd9c96f32" -} diff --git a/.sqlx/query-e575ae62f524a270ef62da4ace6b8e3275484860cdc8e70ccd97b13061230f9e.json b/.sqlx/query-e575ae62f524a270ef62da4ace6b8e3275484860cdc8e70ccd97b13061230f9e.json deleted file mode 100644 index 1fe8cdba0..000000000 --- a/.sqlx/query-e575ae62f524a270ef62da4ace6b8e3275484860cdc8e70ccd97b13061230f9e.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`\n FROM `coin_states` INDEXED BY `coin_spent`\n WHERE `spent_height` = ?\n ", - "describe": { - "columns": [ - { - "name": "parent_coin_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "puzzle_hash", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "amount", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "created_height", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "spent_height", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "transaction_id", - "ordinal": 5, - "type_info": "Blob" - }, - { - "name": "kind", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "created_unixtime", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "spent_unixtime", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "e575ae62f524a270ef62da4ace6b8e3275484860cdc8e70ccd97b13061230f9e" -} diff --git a/.sqlx/query-e843f99da6d06b1fe07877738eb675eadcec252bcba9b251bd027d59f2e68d32.json b/.sqlx/query-e843f99da6d06b1fe07877738eb675eadcec252bcba9b251bd027d59f2e68d32.json new file mode 100644 index 000000000..3c1077be7 --- /dev/null +++ b/.sqlx/query-e843f99da6d06b1fe07877738eb675eadcec252bcba9b251bd027d59f2e68d32.json @@ -0,0 +1,140 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n asset_hash, asset_name, asset_ticker, asset_precision, asset_icon_url, asset_description,\n asset_is_visible, asset_is_sensitive_content,\n owned_coins.created_height, spent_height,\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n metadata, recovery_list_hash, num_verifications_required,\n offer_hash, created_timestamp, spent_timestamp,\n clawback_expiration_seconds AS clawback_timestamp\n FROM owned_coins\n INNER JOIN dids ON dids.asset_id = owned_coins.asset_id\n ORDER BY asset_name ASC\n ", + "describe": { + "columns": [ + { + "name": "asset_hash", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "asset_name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "asset_ticker", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "asset_precision", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "asset_icon_url", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "asset_description", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "asset_is_visible", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "asset_is_sensitive_content", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "created_height", + "ordinal": 8, + "type_info": "Integer" + }, + { + "name": "spent_height", + "ordinal": 9, + "type_info": "Integer" + }, + { + "name": "parent_coin_hash", + "ordinal": 10, + "type_info": "Blob" + }, + { + "name": "puzzle_hash", + "ordinal": 11, + "type_info": "Blob" + }, + { + "name": "amount", + "ordinal": 12, + "type_info": "Blob" + }, + { + "name": "p2_puzzle_hash", + "ordinal": 13, + "type_info": "Blob" + }, + { + "name": "metadata", + "ordinal": 14, + "type_info": "Blob" + }, + { + "name": "recovery_list_hash", + "ordinal": 15, + "type_info": "Blob" + }, + { + "name": "num_verifications_required", + "ordinal": 16, + "type_info": "Integer" + }, + { + "name": "offer_hash", + "ordinal": 17, + "type_info": "Blob" + }, + { + "name": "created_timestamp", + "ordinal": 18, + "type_info": "Integer" + }, + { + "name": "spent_timestamp", + "ordinal": 19, + "type_info": "Integer" + }, + { + "name": "clawback_timestamp", + "ordinal": 20, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + true, + true, + false, + true, + true, + false, + false, + true, + true, + false, + false, + false, + false, + false, + true, + false, + true, + true, + true, + true + ] + }, + "hash": "e843f99da6d06b1fe07877738eb675eadcec252bcba9b251bd027d59f2e68d32" +} diff --git a/.sqlx/query-e86f3625c0d3bfdf62bd013edbbd9c105fcb1f044387bed9e5b03a52a84799c4.json b/.sqlx/query-e86f3625c0d3bfdf62bd013edbbd9c105fcb1f044387bed9e5b03a52a84799c4.json new file mode 100644 index 000000000..e9be784df --- /dev/null +++ b/.sqlx/query-e86f3625c0d3bfdf62bd013edbbd9c105fcb1f044387bed9e5b03a52a84799c4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO file_uris (file_id, uri) VALUES ((SELECT id FROM files WHERE hash = ?), ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "e86f3625c0d3bfdf62bd013edbbd9c105fcb1f044387bed9e5b03a52a84799c4" +} diff --git a/.sqlx/query-e87cae330e1ce739c1ddf2cdbf3b5537719edeec73b4d9b10f85b913cff8a03a.json b/.sqlx/query-e87cae330e1ce739c1ddf2cdbf3b5537719edeec73b4d9b10f85b913cff8a03a.json deleted file mode 100644 index 351a4919a..000000000 --- a/.sqlx/query-e87cae330e1ce739c1ddf2cdbf3b5537719edeec73b4d9b10f85b913cff8a03a.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `coin_id` FROM `transaction_spends` WHERE `transaction_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "coin_id", - "ordinal": 0, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "e87cae330e1ce739c1ddf2cdbf3b5537719edeec73b4d9b10f85b913cff8a03a" -} diff --git a/.sqlx/query-e96b53e779b2d30afa02196660857ad5c80de5badf8b2a7b9facfcdad82b6fe8.json b/.sqlx/query-e96b53e779b2d30afa02196660857ad5c80de5badf8b2a7b9facfcdad82b6fe8.json new file mode 100644 index 000000000..cd8d5b0d6 --- /dev/null +++ b/.sqlx/query-e96b53e779b2d30afa02196660857ad5c80de5badf8b2a7b9facfcdad82b6fe8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE assets SET ticker = ? WHERE id = 0", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "e96b53e779b2d30afa02196660857ad5c80de5badf8b2a7b9facfcdad82b6fe8" +} diff --git a/.sqlx/query-e97c3dfaebb0e1340cf2b2b23c7aab1797964dab93a7c5e7091a18859c0a942f.json b/.sqlx/query-e97c3dfaebb0e1340cf2b2b23c7aab1797964dab93a7c5e7091a18859c0a942f.json new file mode 100644 index 000000000..8773d4bee --- /dev/null +++ b/.sqlx/query-e97c3dfaebb0e1340cf2b2b23c7aab1797964dab93a7c5e7091a18859c0a942f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE assets SET icon_url = ?\n WHERE assets.id IN (\n SELECT asset_id FROM nfts\n WHERE data_hash = ?\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "e97c3dfaebb0e1340cf2b2b23c7aab1797964dab93a7c5e7091a18859c0a942f" +} diff --git a/.sqlx/query-e985be1270a2c2433ff175aab6577290676f135dc341a2ed41cd71400db41d73.json b/.sqlx/query-e985be1270a2c2433ff175aab6577290676f135dc341a2ed41cd71400db41d73.json new file mode 100644 index 000000000..d47931b20 --- /dev/null +++ b/.sqlx/query-e985be1270a2c2433ff175aab6577290676f135dc341a2ed41cd71400db41d73.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE coins SET is_children_synced = TRUE WHERE hash = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "e985be1270a2c2433ff175aab6577290676f135dc341a2ed41cd71400db41d73" +} diff --git a/.sqlx/query-ea4bb56089373de61b9d5e4f7865c1090b0f684567d1e4de391373d3b36faa9d.json b/.sqlx/query-ea4bb56089373de61b9d5e4f7865c1090b0f684567d1e4de391373d3b36faa9d.json deleted file mode 100644 index 0adce0754..000000000 --- a/.sqlx/query-ea4bb56089373de61b9d5e4f7865c1090b0f684567d1e4de391373d3b36faa9d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO `offer_xch` (\n `offer_id`, `requested`, `amount`, `royalty`\n )\n VALUES (?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "ea4bb56089373de61b9d5e4f7865c1090b0f684567d1e4de391373d3b36faa9d" -} diff --git a/.sqlx/query-ecdabf3ddfb2b9e7ddaee36c57cee4818b113faad1fa6169e486a7a1bc1be4da.json b/.sqlx/query-ecdabf3ddfb2b9e7ddaee36c57cee4818b113faad1fa6169e486a7a1bc1be4da.json deleted file mode 100644 index 1ff9f8b76..000000000 --- a/.sqlx/query-ecdabf3ddfb2b9e7ddaee36c57cee4818b113faad1fa6169e486a7a1bc1be4da.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `name`\n FROM `dids`\n WHERE `launcher_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "ecdabf3ddfb2b9e7ddaee36c57cee4818b113faad1fa6169e486a7a1bc1be4da" -} diff --git a/.sqlx/query-ee307ddbdd1ededf7d89140b631763945ae6ce871d40d7c6f627379ea7c80e14.json b/.sqlx/query-ee307ddbdd1ededf7d89140b631763945ae6ce871d40d7c6f627379ea7c80e14.json deleted file mode 100644 index 917614c9f..000000000 --- a/.sqlx/query-ee307ddbdd1ededf7d89140b631763945ae6ce871d40d7c6f627379ea7c80e14.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `dids` SET `created_height` = ? WHERE `coin_id` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "ee307ddbdd1ededf7d89140b631763945ae6ce871d40d7c6f627379ea7c80e14" -} diff --git a/.sqlx/query-eee8bbee035366aafb313becb3cb5442cf24b1b9497d0929691dbf93386c12d4.json b/.sqlx/query-eee8bbee035366aafb313becb3cb5442cf24b1b9497d0929691dbf93386c12d4.json new file mode 100644 index 000000000..ff76cf81c --- /dev/null +++ b/.sqlx/query-eee8bbee035366aafb313becb3cb5442cf24b1b9497d0929691dbf93386c12d4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO files (hash) VALUES (?)", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "eee8bbee035366aafb313becb3cb5442cf24b1b9497d0929691dbf93386c12d4" +} diff --git a/.sqlx/query-f15d7d6399b9895af9c5a74a45a38d0d40b721a1632ab17dc958dcc9eedb4446.json b/.sqlx/query-f15d7d6399b9895af9c5a74a45a38d0d40b721a1632ab17dc958dcc9eedb4446.json new file mode 100644 index 000000000..695906ebd --- /dev/null +++ b/.sqlx/query-f15d7d6399b9895af9c5a74a45a38d0d40b721a1632ab17dc958dcc9eedb4446.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO blocks (height, timestamp, header_hash, is_peak) VALUES (?, ?, ?, ?)\n ON CONFLICT (height) DO UPDATE SET\n timestamp = COALESCE(excluded.timestamp, timestamp),\n header_hash = excluded.header_hash,\n is_peak = (excluded.is_peak OR is_peak)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "f15d7d6399b9895af9c5a74a45a38d0d40b721a1632ab17dc958dcc9eedb4446" +} diff --git a/.sqlx/query-9b57663e0de9a8296a221242ff363e9a7d8e503afacc87df9e340f0042a86b86.json b/.sqlx/query-f173bb7b26be9126db98852ac84e71d1aaf7d8296902694390c9cfd6d2376807.json similarity index 62% rename from .sqlx/query-9b57663e0de9a8296a221242ff363e9a7d8e503afacc87df9e340f0042a86b86.json rename to .sqlx/query-f173bb7b26be9126db98852ac84e71d1aaf7d8296902694390c9cfd6d2376807.json index b59ae9e0c..f4379352d 100644 --- a/.sqlx/query-9b57663e0de9a8296a221242ff363e9a7d8e503afacc87df9e340f0042a86b86.json +++ b/.sqlx/query-f173bb7b26be9126db98852ac84e71d1aaf7d8296902694390c9cfd6d2376807.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT\n `parent_coin_id`, `puzzle_hash`, `amount`,\n `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`,\n `asset_id`, `p2_puzzle_hash`\n FROM `coin_states`\n INNER JOIN `cat_coins` ON `coin_states`.`coin_id` = `cat_coins`.`coin_id`\n WHERE `coin_states`.`coin_id` = ?\n ", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, hidden_puzzle_hash, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount\n FROM selectable_coins\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = selectable_coins.coin_id\n WHERE asset_hash = ?\n ", "describe": { "columns": [ { - "name": "parent_coin_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, @@ -19,27 +19,27 @@ "type_info": "Blob" }, { - "name": "parent_parent_coin_id", + "name": "hidden_puzzle_hash", "ordinal": 3, "type_info": "Blob" }, { - "name": "parent_inner_puzzle_hash", + "name": "p2_puzzle_hash", "ordinal": 4, "type_info": "Blob" }, { - "name": "parent_amount", + "name": "parent_parent_coin_hash", "ordinal": 5, "type_info": "Blob" }, { - "name": "asset_id", + "name": "parent_inner_puzzle_hash", "ordinal": 6, "type_info": "Blob" }, { - "name": "p2_puzzle_hash", + "name": "parent_amount", "ordinal": 7, "type_info": "Blob" } @@ -51,12 +51,12 @@ false, false, false, - false, + true, false, false, false, false ] }, - "hash": "9b57663e0de9a8296a221242ff363e9a7d8e503afacc87df9e340f0042a86b86" + "hash": "f173bb7b26be9126db98852ac84e71d1aaf7d8296902694390c9cfd6d2376807" } diff --git a/.sqlx/query-f2be1dbffa92a251025e98deb70406bf2981db4c0196b5acb0b766995bea4484.json b/.sqlx/query-f2be1dbffa92a251025e98deb70406bf2981db4c0196b5acb0b766995bea4484.json new file mode 100644 index 000000000..2414bd501 --- /dev/null +++ b/.sqlx/query-f2be1dbffa92a251025e98deb70406bf2981db4c0196b5acb0b766995bea4484.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT MAX(derivation_index) AS derivation_index \n FROM public_keys \n WHERE is_hardened = ?\n ", + "describe": { + "columns": [ + { + "name": "derivation_index", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "f2be1dbffa92a251025e98deb70406bf2981db4c0196b5acb0b766995bea4484" +} diff --git a/.sqlx/query-f2e5255b96347dccb31a5997ad7ea69f8629756d2c74270c76bd0fc85f2e939f.json b/.sqlx/query-f2e5255b96347dccb31a5997ad7ea69f8629756d2c74270c76bd0fc85f2e939f.json new file mode 100644 index 000000000..b4d9471a3 --- /dev/null +++ b/.sqlx/query-f2e5255b96347dccb31a5997ad7ea69f8629756d2c74270c76bd0fc85f2e939f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE coins SET\n asset_id = (SELECT id FROM assets WHERE hash = ?),\n p2_puzzle_id = (SELECT id FROM p2_puzzles WHERE hash = ?),\n hidden_puzzle_hash = ?\n WHERE hash = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "f2e5255b96347dccb31a5997ad7ea69f8629756d2c74270c76bd0fc85f2e939f" +} diff --git a/.sqlx/query-f7245203c71cdf192fc3334935d25d871270bb9e7da09948231b6dc61703da18.json b/.sqlx/query-f7245203c71cdf192fc3334935d25d871270bb9e7da09948231b6dc61703da18.json new file mode 100644 index 000000000..bb5633f0f --- /dev/null +++ b/.sqlx/query-f7245203c71cdf192fc3334935d25d871270bb9e7da09948231b6dc61703da18.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO lineage_proofs\n (coin_id, parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount)\n VALUES\n ((SELECT id FROM coins WHERE hash = ?), ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "f7245203c71cdf192fc3334935d25d871270bb9e7da09948231b6dc61703da18" +} diff --git a/.sqlx/query-f78c569713a5c185a26f6e154881dd2b970bc97c12285e92f638916f3f7270bb.json b/.sqlx/query-f78c569713a5c185a26f6e154881dd2b970bc97c12285e92f638916f3f7270bb.json deleted file mode 100644 index 27df06415..000000000 --- a/.sqlx/query-f78c569713a5c185a26f6e154881dd2b970bc97c12285e92f638916f3f7270bb.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT launcher_id, coin_id, collection_id, minter_did, owner_did, visible, \n sensitive_content, name, is_owned, created_height, metadata_hash,\n is_named, is_pending, edition_number, edition_total\n FROM `nfts` WHERE `launcher_id` = ?\n ", - "describe": { - "columns": [ - { - "name": "launcher_id", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "coin_id", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "collection_id", - "ordinal": 2, - "type_info": "Blob" - }, - { - "name": "minter_did", - "ordinal": 3, - "type_info": "Blob" - }, - { - "name": "owner_did", - "ordinal": 4, - "type_info": "Blob" - }, - { - "name": "visible", - "ordinal": 5, - "type_info": "Bool" - }, - { - "name": "sensitive_content", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "name", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "is_owned", - "ordinal": 8, - "type_info": "Bool" - }, - { - "name": "created_height", - "ordinal": 9, - "type_info": "Integer" - }, - { - "name": "metadata_hash", - "ordinal": 10, - "type_info": "Blob" - }, - { - "name": "is_named", - "ordinal": 11, - "type_info": "Bool" - }, - { - "name": "is_pending", - "ordinal": 12, - "type_info": "Bool" - }, - { - "name": "edition_number", - "ordinal": 13, - "type_info": "Integer" - }, - { - "name": "edition_total", - "ordinal": 14, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - true, - true, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true - ] - }, - "hash": "f78c569713a5c185a26f6e154881dd2b970bc97c12285e92f638916f3f7270bb" -} diff --git a/.sqlx/query-f8603a648f790812c2963330ec461b03e3bbc0055ac53cd966bdcdb51ba8f511.json b/.sqlx/query-f8603a648f790812c2963330ec461b03e3bbc0055ac53cd966bdcdb51ba8f511.json new file mode 100644 index 000000000..39e5cdd03 --- /dev/null +++ b/.sqlx/query-f8603a648f790812c2963330ec461b03e3bbc0055ac53cd966bdcdb51ba8f511.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO nfts (\n asset_id, collection_id, minter_hash, owner_hash, metadata, metadata_updater_puzzle_hash,\n royalty_puzzle_hash, royalty_basis_points, data_hash, metadata_hash, license_hash,\n edition_number, edition_total\n )\n VALUES ((SELECT id FROM assets WHERE hash = ?), (SELECT id FROM collections WHERE hash = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "f8603a648f790812c2963330ec461b03e3bbc0055ac53cd966bdcdb51ba8f511" +} diff --git a/.sqlx/query-82e25960cba013361d6f302dbf318841dd4c45ebf343d91d51eb82f425dcccc3.json b/.sqlx/query-f9151afa46f2b2ec5992cacf048b4844b1ae212a59a29ff33e4399a5556221dc.json similarity index 58% rename from .sqlx/query-82e25960cba013361d6f302dbf318841dd4c45ebf343d91d51eb82f425dcccc3.json rename to .sqlx/query-f9151afa46f2b2ec5992cacf048b4844b1ae212a59a29ff33e4399a5556221dc.json index c80827218..b535ef005 100644 --- a/.sqlx/query-82e25960cba013361d6f302dbf318841dd4c45ebf343d91d51eb82f425dcccc3.json +++ b/.sqlx/query-f9151afa46f2b2ec5992cacf048b4844b1ae212a59a29ff33e4399a5556221dc.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT\n `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`,\n `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`,\n `launcher_id`, `metadata`, `metadata_updater_puzzle_hash`, `current_owner`,\n `royalty_puzzle_hash`, `royalty_ten_thousandths`, `p2_puzzle_hash`\n FROM `nft_coins`\n INNER JOIN `coin_states` INDEXED BY `coin_height` ON `nft_coins`.`coin_id` = `coin_states`.`coin_id`\n LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id`\n WHERE `launcher_id` = ?\n AND `spent_height` IS NULL\n AND `created_height` IS NOT NULL\n AND `coin_states`.`transaction_id` IS NULL\n AND `transaction_spends`.`transaction_id` IS NULL\n ", + "query": "\n SELECT\n parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash,\n parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount,\n asset_hash AS launcher_id, metadata, metadata_updater_puzzle_hash,\n owner_hash, royalty_puzzle_hash, royalty_basis_points\n FROM spendable_coins\n INNER JOIN nfts ON nfts.asset_id = spendable_coins.asset_id\n INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id\n WHERE asset_hash = ?\n ", "describe": { "columns": [ { - "name": "parent_coin_id", + "name": "parent_coin_hash", "ordinal": 0, "type_info": "Blob" }, @@ -19,54 +19,54 @@ "type_info": "Blob" }, { - "name": "parent_parent_coin_id", + "name": "p2_puzzle_hash", "ordinal": 3, "type_info": "Blob" }, { - "name": "parent_inner_puzzle_hash", + "name": "parent_parent_coin_hash", "ordinal": 4, "type_info": "Blob" }, { - "name": "parent_amount", + "name": "parent_inner_puzzle_hash", "ordinal": 5, "type_info": "Blob" }, { - "name": "launcher_id", + "name": "parent_amount", "ordinal": 6, "type_info": "Blob" }, { - "name": "metadata", + "name": "launcher_id", "ordinal": 7, "type_info": "Blob" }, { - "name": "metadata_updater_puzzle_hash", + "name": "metadata", "ordinal": 8, "type_info": "Blob" }, { - "name": "current_owner", + "name": "metadata_updater_puzzle_hash", "ordinal": 9, "type_info": "Blob" }, { - "name": "royalty_puzzle_hash", + "name": "owner_hash", "ordinal": 10, "type_info": "Blob" }, { - "name": "royalty_ten_thousandths", + "name": "royalty_puzzle_hash", "ordinal": 11, - "type_info": "Integer" + "type_info": "Blob" }, { - "name": "p2_puzzle_hash", + "name": "royalty_basis_points", "ordinal": 12, - "type_info": "Blob" + "type_info": "Integer" } ], "parameters": { @@ -82,11 +82,11 @@ false, false, false, - true, false, + true, false, false ] }, - "hash": "82e25960cba013361d6f302dbf318841dd4c45ebf343d91d51eb82f425dcccc3" + "hash": "f9151afa46f2b2ec5992cacf048b4844b1ae212a59a29ff33e4399a5556221dc" } diff --git a/.sqlx/query-f9a67288d0213f38b4f37c5d13c8588bb1dd679252837a33778367ed24da1129.json b/.sqlx/query-f9a67288d0213f38b4f37c5d13c8588bb1dd679252837a33778367ed24da1129.json new file mode 100644 index 000000000..b6e319233 --- /dev/null +++ b/.sqlx/query-f9a67288d0213f38b4f37c5d13c8588bb1dd679252837a33778367ed24da1129.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM coins WHERE hash = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "f9a67288d0213f38b4f37c5d13c8588bb1dd679252837a33778367ed24da1129" +} diff --git a/.sqlx/query-fe6b2ff2c4a327f5363b9c48e0520cce4cfe92743dd761188dc236c044606b18.json b/.sqlx/query-fe6b2ff2c4a327f5363b9c48e0520cce4cfe92743dd761188dc236c044606b18.json new file mode 100644 index 000000000..6a146d6e7 --- /dev/null +++ b/.sqlx/query-fe6b2ff2c4a327f5363b9c48e0520cce4cfe92743dd761188dc236c044606b18.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM mempool_items WHERE hash = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "fe6b2ff2c4a327f5363b9c48e0520cce4cfe92743dd761188dc236c044606b18" +} diff --git a/.sqlx/query-fe6fa6692a17c1f93c883c4ededcf7235705166af82c6b5d5ea8863f9a3846d4.json b/.sqlx/query-fe6fa6692a17c1f93c883c4ededcf7235705166af82c6b5d5ea8863f9a3846d4.json deleted file mode 100644 index 584b0c86f..000000000 --- a/.sqlx/query-fe6fa6692a17c1f93c883c4ededcf7235705166af82c6b5d5ea8863f9a3846d4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE `nft_uris` SET `checked` = 1, `hash_matches` = ? WHERE `hash` = ? AND `uri` = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "fe6fa6692a17c1f93c883c4ededcf7235705166af82c6b5d5ea8863f9a3846d4" -} diff --git a/.sqlx/query-ffeb193a66da6b7d218b6e21353dcf3854a56af2ae3bb1d31a5334db682b7471.json b/.sqlx/query-ffeb193a66da6b7d218b6e21353dcf3854a56af2ae3bb1d31a5334db682b7471.json deleted file mode 100644 index 4124cf745..000000000 --- a/.sqlx/query-ffeb193a66da6b7d218b6e21353dcf3854a56af2ae3bb1d31a5334db682b7471.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n DELETE FROM `future_did_names` WHERE `launcher_id` = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "ffeb193a66da6b7d218b6e21353dcf3854a56af2ae3bb1d31a5334db682b7471" -} diff --git a/.sqlx/query-fffa560e5861900542cef64dbe9be940485eea480762caf4e602732f9c5bfc73.json b/.sqlx/query-fffa560e5861900542cef64dbe9be940485eea480762caf4e602732f9c5bfc73.json deleted file mode 100644 index 0da0e0096..000000000 --- a/.sqlx/query-fffa560e5861900542cef64dbe9be940485eea480762caf4e602732f9c5bfc73.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT MAX(`index`) AS `max_index`\n FROM `derivations`\n WHERE `hardened` = ?\n ", - "describe": { - "columns": [ - { - "name": "max_index", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "fffa560e5861900542cef64dbe9be940485eea480762caf4e602732f9c5bfc73" -} diff --git a/Cargo.lock b/Cargo.lock index 669cdc333..a2a3fedc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,7 +206,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -275,7 +275,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "synstructure", ] @@ -287,7 +287,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -395,7 +395,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -430,7 +430,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -708,7 +708,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.103", + "syn 2.0.104", "which", ] @@ -1036,20 +1036,20 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chia" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e23293631a6453f729ac18ecd1caf5aad3c8686de79daaaeead77d09957f68" +checksum = "b27699b864f74bfe17216295e445db63f1aedbe3be9560833009a5890ce5d46d" dependencies = [ - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-client", "chia-consensus", "chia-protocol", "chia-puzzle-types", "chia-secp", "chia-serde", - "chia-sha2 0.25.0", + "chia-sha2 0.26.0", "chia-ssl", - "chia-traits 0.25.0", + "chia-traits 0.26.0", "clvm-traits", "clvm-utils", "clvmr", @@ -1073,14 +1073,14 @@ dependencies = [ [[package]] name = "chia-bls" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c16b9296d434304de07601f24f520dd8dcd127ab217f6c3de2f08b9215e2665" +checksum = "fc9ac7e90ae816e814dc26fb332615a5449d1d26e965eb7ad068ae530f9c3c7b" dependencies = [ "blst", "chia-serde", - "chia-sha2 0.25.0", - "chia-traits 0.25.0", + "chia-sha2 0.26.0", + "chia-traits 0.26.0", "hex", "hkdf", "linked-hash-map", @@ -1091,12 +1091,12 @@ dependencies = [ [[package]] name = "chia-client" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320a354175bea6a8b8d13e2c803217179f6b818e647304a294755c4ac9e1ec90" +checksum = "3874f2e4c87f246457a42890d4f8e0cae9d019131b54f80f66c561c8d05cd444" dependencies = [ "chia-protocol", - "chia-traits 0.25.0", + "chia-traits 0.26.0", "futures-util", "thiserror 1.0.69", "tokio", @@ -1106,17 +1106,17 @@ dependencies = [ [[package]] name = "chia-consensus" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716e9c3b8c4418049cac10c8e859246e64f6581ccf433fe65a569b8c43b1b361" +checksum = "481208db317ed106dc249a5669a6916c565374633850584480a40b639409a940" dependencies = [ - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-protocol", "chia-puzzle-types", "chia-puzzles", - "chia-sha2 0.25.0", - "chia-traits 0.25.0", - "chia_streamable_macro 0.25.0", + "chia-sha2 0.26.0", + "chia-traits 0.26.0", + "chia_streamable_macro 0.26.0", "clvm-traits", "clvm-utils", "clvmr", @@ -1127,15 +1127,15 @@ dependencies = [ [[package]] name = "chia-protocol" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d09bfaf24a4a3e9ec7f89d0b95fadf093617aa87cec089aa651a905e81d1af1" +checksum = "a9256179e4c912313532d7a47a50f67f6128313ee86a3798e54050afb73d6042" dependencies = [ - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-serde", - "chia-sha2 0.25.0", - "chia-traits 0.25.0", - "chia_streamable_macro 0.25.0", + "chia-sha2 0.26.0", + "chia-traits 0.26.0", + "chia_streamable_macro 0.26.0", "clvm-traits", "clvm-utils", "clvmr", @@ -1145,14 +1145,14 @@ dependencies = [ [[package]] name = "chia-puzzle-types" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378afbe1412a2862f7afdb7b6b536957955a28c48bc282223dd70fd0b94a1cf1" +checksum = "239406b55be27d99a6903057e6785e0279fe9fdc48270faf7ef9270dcfa05c8d" dependencies = [ - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-protocol", "chia-puzzles", - "chia-sha2 0.25.0", + "chia-sha2 0.26.0", "clvm-traits", "clvm-utils", "clvmr", @@ -1172,15 +1172,14 @@ dependencies = [ [[package]] name = "chia-sdk-client" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9ce02d27d3ce19509c15be930ba082e94553291e6792dcaa82fecf85f0fb3f" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ "aws-lc-rs", "chia-protocol", "chia-sdk-types", "chia-ssl", - "chia-traits 0.25.0", + "chia-traits 0.26.0", "futures-util", "once_cell", "rustls", @@ -1194,9 +1193,8 @@ dependencies = [ [[package]] name = "chia-sdk-coinset" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9462f26a23f5b92bd1512989721fd3d21028223880c6c536f5fb146224b78278" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ "chia-protocol", "hex", @@ -1208,24 +1206,22 @@ dependencies = [ [[package]] name = "chia-sdk-derive" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f05ddfc048ef665eb1455ea109452524a0a18c77c7b726ee7417ef9e0fae46a" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ "convert_case 0.6.0", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] name = "chia-sdk-driver" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb9a91810da115b7b2c595350f43d8a8a84d89b977b0da9a00cb6dd296c1073" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ "bech32", "bigdecimal", - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-consensus", "chia-protocol", "chia-puzzle-types", @@ -1233,33 +1229,34 @@ dependencies = [ "chia-sdk-signer", "chia-sdk-types", "chia-secp", - "chia-sha2 0.25.0", - "chia-traits 0.25.0", - "chia_streamable_macro 0.25.0", + "chia-sha2 0.26.0", + "chia-traits 0.26.0", + "chia_streamable_macro 0.26.0", "clvm-traits", "clvm-utils", "clvmr", "flate2", "hex", "hex-literal", - "indexmap 2.9.0", + "indexmap 2.10.0", "num-bigint", "once_cell", + "rand 0.8.5", + "rand_chacha 0.3.1", "thiserror 2.0.12", ] [[package]] name = "chia-sdk-signer" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97967a49012c42b7cd922571c85600a0c1e89f92f921a41d7b7b7656ab92e380" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-consensus", "chia-protocol", "chia-sdk-types", "chia-secp", - "chia-sha2 0.25.0", + "chia-sha2 0.26.0", "clvm-traits", "clvmr", "k256", @@ -1268,13 +1265,12 @@ dependencies = [ [[package]] name = "chia-sdk-test" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31339f6f47bd8b017a5972fcef6fd8755cb110477a8453fc444d05c8f3a41a8" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ "anyhow", "bip39", - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-consensus", "chia-protocol", "chia-puzzle-types", @@ -1282,14 +1278,14 @@ dependencies = [ "chia-sdk-signer", "chia-sdk-types", "chia-secp", - "chia-traits 0.25.0", + "chia-traits 0.26.0", "clvm-traits", "clvm-utils", "clvmr", "futures-channel", "futures-util", "hex", - "indexmap 2.9.0", + "indexmap 2.10.0", "itertools 0.13.0", "rand 0.8.5", "rand_chacha 0.3.1", @@ -1302,18 +1298,17 @@ dependencies = [ [[package]] name = "chia-sdk-types" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5105970c2e29e64f1f07bbbe8d73336d0505e7d27ceeb6a1930312c8cdb66a7b" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-consensus", "chia-protocol", "chia-puzzle-types", "chia-puzzles", "chia-sdk-derive", "chia-secp", - "chia-sha2 0.25.0", + "chia-sha2 0.26.0", "clvm-traits", "clvm-utils", "clvm_tools_rs", @@ -1325,13 +1320,12 @@ dependencies = [ [[package]] name = "chia-sdk-utils" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabb3a85abb572978a05a9e988340dbb0245d9a3eedcfc196db8f7eb86544660" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ "bech32", "chia-protocol", - "indexmap 2.9.0", + "indexmap 2.10.0", "rand 0.8.5", "rand_chacha 0.3.1", "thiserror 2.0.12", @@ -1339,11 +1333,11 @@ dependencies = [ [[package]] name = "chia-secp" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727ab0317d4dc3e74152e0db4a60c86e4e2d5f69c879baf112ca6500866d5d60" +checksum = "8ae00dfc70f7a3b20c5074ea3684b25f7b64225ad44ca3ea766b4e908f86c2d5" dependencies = [ - "chia-sha2 0.25.0", + "chia-sha2 0.26.0", "hex", "k256", "p256", @@ -1351,9 +1345,9 @@ dependencies = [ [[package]] name = "chia-serde" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffa2ea6267199feaa1bb0836095cd57d79c664277d8be28d370a44f36e47df1" +checksum = "2b013e263888739bfecf7456552eb4a9f2260f920db8b46b0723fb553ab1e030" dependencies = [ "hex", "serde", @@ -1370,18 +1364,18 @@ dependencies = [ [[package]] name = "chia-sha2" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0074799301bb192744727aa91f88a3ee75d2504bfb8a843b02d2e303a58dd87" +checksum = "3f3ab374b1f248d87516c34e4c128a91d2086e76a46bb44e386c96a71ea39129" dependencies = [ "sha2 0.10.9", ] [[package]] name = "chia-ssl" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb268d7d4d08e83bda869a2b931dbe519ea4d60426515079c4dbd22f7731a2f4" +checksum = "51af8606c8376d730d6607cb69fe5edd87f5d0d106e5620065f58a31e7f348a4" dependencies = [ "rand 0.8.5", "rcgen", @@ -1403,22 +1397,21 @@ dependencies = [ [[package]] name = "chia-traits" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b67043daeab938c9b4d205b70e018d3352288fdd62709e87f6e2b331db5baca" +checksum = "440a88dfa8f685a87c4bae5f0b12c7bec1466ca1ff44dfc6b09acc605f7848fe" dependencies = [ - "chia-sha2 0.25.0", - "chia_streamable_macro 0.25.0", + "chia-sha2 0.26.0", + "chia_streamable_macro 0.26.0", "thiserror 1.0.69", ] [[package]] name = "chia-wallet-sdk" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf8a6dda3ac2e723261f27fa00dfb0261f0c1c7a39c1abffbc327063a482fc5" +version = "0.27.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?rev=c9a35dacaf5387483c99208591d8831692bcc1ab#c9a35dacaf5387483c99208591d8831692bcc1ab" dependencies = [ - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-protocol", "chia-sdk-client", "chia-sdk-coinset", @@ -1441,19 +1434,19 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] name = "chia_streamable_macro" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc5c6400d47fc7b0bef1e08791e96fc391d678012090c4b95411cfa516c894c" +checksum = "068278c78a0c53786012f6330a088f3f6ffe83bd39cb8f21d308eeec2f43a3dc" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1464,10 +1457,8 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-link", ] @@ -1523,7 +1514,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1543,22 +1534,22 @@ dependencies = [ [[package]] name = "clvm-derive" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184e2f4aa7473bf377bd0f03e61406668124eb0d16ccdd4591ec5b6cb8ea533" +checksum = "68ded863cb5482335498872e91989487283f012adb7ca4ddf54ad7c6995058db" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] name = "clvm-traits" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a93051ecabce23522df9d597c915b5df1be00a8b6d28f852a306304d9fe94d" +checksum = "469c44226c83de37509415482c89c00763f58de198844c34d2d62161b087d0ce" dependencies = [ - "chia-bls 0.25.0", + "chia-bls 0.26.0", "chia-secp", "clvm-derive", "clvmr", @@ -1568,11 +1559,11 @@ dependencies = [ [[package]] name = "clvm-utils" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3cfd58c573d44d4ecf9b17af614861a53e7e9170523decd212df162162b190" +checksum = "5646cd8d02f9c55c76ef6ff7598df54e6c8361f27fe8c9122e3ae9ef55935fcd" dependencies = [ - "chia-sha2 0.25.0", + "chia-sha2 0.26.0", "clvm-traits", "clvmr", "hex", @@ -1880,7 +1871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1890,7 +1881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1923,7 +1914,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1934,7 +1925,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1988,7 +1979,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2090,7 +2081,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2119,7 +2110,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2272,7 +2263,7 @@ checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2478,7 +2469,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2593,7 +2584,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2894,7 +2885,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2984,7 +2975,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2999,7 +2990,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -3383,7 +3374,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -3465,9 +3456,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3506,7 +3497,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4186,7 +4177,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4258,7 +4249,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4681,7 +4672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "indexmap 2.10.0", ] [[package]] @@ -4788,7 +4779,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -4875,7 +4866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64 0.22.1", - "indexmap 2.9.0", + "indexmap 2.10.0", "quick-xml 0.32.0", "serde", "time", @@ -4949,7 +4940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -5044,7 +5035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -5621,9 +5612,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "aws-lc-rs", "log", @@ -5688,11 +5679,9 @@ dependencies = [ "chia", "chia-puzzles", "chia-wallet-sdk", - "chrono", "clvmr", "hex", - "hex-literal", - "indexmap 2.9.0", + "indexmap 2.10.0", "itertools 0.13.0", "log", "rand 0.8.5", @@ -5718,7 +5707,6 @@ dependencies = [ name = "sage-api" version = "0.10.3" dependencies = [ - "indexmap 2.9.0", "once_cell", "sage-config", "serde", @@ -5731,7 +5719,7 @@ name = "sage-api-macro" version = "0.10.3" dependencies = [ "convert_case 0.8.0", - "indexmap 2.9.0", + "indexmap 2.10.0", "proc-macro2", "quote", "serde_json", @@ -5741,6 +5729,7 @@ dependencies = [ name = "sage-assets" version = "0.10.3" dependencies = [ + "base64 0.22.1", "chia", "futures-lite", "futures-util", @@ -5797,7 +5786,7 @@ dependencies = [ "chia-wallet-sdk", "expect-test", "hex", - "indexmap 2.9.0", + "indexmap 2.10.0", "once_cell", "serde", "serde_json", @@ -5896,22 +5885,22 @@ dependencies = [ "anyhow", "chia", "chia-puzzles", - "chia-sha2 0.25.0", - "chia-traits 0.25.0", + "chia-sha2 0.26.0", + "chia-traits 0.26.0", "chia-wallet-sdk", - "chia_streamable_macro 0.25.0", + "chia_streamable_macro 0.26.0", "clvmr", "futures-lite", "futures-util", + "hex", "hex-literal", - "indexmap 2.9.0", + "indexmap 2.10.0", "itertools 0.13.0", "rand 0.8.5", "reqwest", "sage-assets", "sage-config", "sage-database", - "serde", "sqlx", "test-log", "thiserror 1.0.69", @@ -5952,7 +5941,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6032,7 +6021,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6043,7 +6032,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6076,7 +6065,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6110,7 +6099,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_derive", "serde_json", @@ -6127,7 +6116,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6351,7 +6340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ccbb212565d2dc177bc15ecb7b039d66c4490da892436a4eee5b394d620c9bc" dependencies = [ "bigdecimal", - "indexmap 2.9.0", + "indexmap 2.10.0", "paste", "specta-macros", "thiserror 1.0.69", @@ -6366,7 +6355,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6440,7 +6429,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.2", "hashlink", - "indexmap 2.9.0", + "indexmap 2.10.0", "log", "memchr", "once_cell", @@ -6466,7 +6455,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6489,7 +6478,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.103", + "syn 2.0.104", "tempfile", "tokio", "url", @@ -6682,9 +6671,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -6708,7 +6697,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6780,7 +6769,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -6893,7 +6882,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "syn 2.0.103", + "syn 2.0.104", "tauri-utils", "thiserror 2.0.12", "time", @@ -6911,7 +6900,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "tauri-codegen", "tauri-utils", ] @@ -7184,7 +7173,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -7232,7 +7221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" dependencies = [ "embed-resource", - "indexmap 2.9.0", + "indexmap 2.10.0", "toml", ] @@ -7279,7 +7268,7 @@ checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -7314,7 +7303,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -7325,7 +7314,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -7441,7 +7430,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -7500,7 +7489,7 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", @@ -7522,7 +7511,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "toml_datetime", "winnow 0.5.40", ] @@ -7533,7 +7522,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "toml_datetime", "winnow 0.5.40", ] @@ -7544,7 +7533,7 @@ version = "0.22.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", @@ -7636,7 +7625,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -8041,7 +8030,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -8076,7 +8065,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8287,7 +8276,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -8473,7 +8462,7 @@ checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -8484,7 +8473,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -8495,7 +8484,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -9001,7 +8990,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "synstructure", ] @@ -9051,7 +9040,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "zbus_names", "zvariant", "zvariant_utils", @@ -9086,7 +9075,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -9106,7 +9095,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "synstructure", ] @@ -9127,7 +9116,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -9149,7 +9138,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -9201,7 +9190,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "zvariant_utils", ] @@ -9215,6 +9204,6 @@ dependencies = [ "quote", "serde", "static_assertions", - "syn 2.0.103", + "syn 2.0.104", "winnow 0.7.7", ] diff --git a/Cargo.toml b/Cargo.toml index ba9bbc016..0f678a288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,13 +84,13 @@ specta-typescript = "0.0.7" tauri-specta = "2.0.0-rc.20" # Chia -chia = "0.25.0" -chia_streamable_macro = "0.25.0" -chia-traits = "0.25.0" -chia-sha2 = "0.25.0" +chia = "0.26.0" +chia_streamable_macro = "0.26.0" +chia-traits = "0.26.0" +chia-sha2 = "0.26.0" chia-puzzles = "0.20.1" clvmr = "0.14.0" -chia-wallet-sdk = { version = "0.26.0", features = ["rustls", "offer-compression", "peer-simulator"] } +chia-wallet-sdk = { git = "https://github.com/xch-dev/chia-wallet-sdk", rev = "c9a35dacaf5387483c99208591d8831692bcc1ab", features = ["rustls", "offer-compression", "peer-simulator"] } bip39 = "2.0.0" bech32 = "0.9.1" rand = "0.8.5" @@ -112,7 +112,6 @@ anyhow = "1.0.86" thiserror = "1.0.63" hex-literal = "0.4.1" once_cell = "1.19.0" -chrono = "0.4.38" glob = "0.3.1" num-bigint = "0.4.6" mime-sniffer = "0.1.3" diff --git a/crates/sage-api/Cargo.toml b/crates/sage-api/Cargo.toml index 36c94fad7..2957048a8 100644 --- a/crates/sage-api/Cargo.toml +++ b/crates/sage-api/Cargo.toml @@ -23,4 +23,3 @@ serde = { workspace = true, features = ["derive"] } tauri-specta = { workspace = true, features = ["derive"], optional = true } specta = { workspace = true, features = ["derive", "bigdecimal"], optional = true } once_cell = { workspace = true } -indexmap = { workspace = true, features = ["serde"] } diff --git a/crates/sage-api/endpoints.json b/crates/sage-api/endpoints.json index 6fd70bfe4..9c6a409e9 100644 --- a/crates/sage-api/endpoints.json +++ b/crates/sage-api/endpoints.json @@ -5,24 +5,28 @@ "generate_mnemonic": false, "import_key": true, "delete_key": false, + "delete_database": false, "rename_key": false, "get_key": false, "get_secret_key": false, "get_keys": false, "get_sync_status": true, "get_version": false, + "get_database_stats": true, + "perform_database_maintenance": true, "check_address": true, "get_derivations": true, "get_are_coins_spendable": true, "get_spendable_coin_count": true, "get_coins_by_ids": true, - "get_xch_coins": true, - "get_cat_coins": true, + "get_coins": true, "get_cats": true, - "get_cat": true, + "get_all_cats": true, + "get_token": true, "get_dids": true, "get_minter_did_ids": true, "get_pending_transactions": true, + "get_transaction": true, "get_transactions": true, "get_nft_collections": true, "get_nft_collection": true, @@ -33,12 +37,10 @@ "get_nft_data": true, "send_xch": true, "bulk_send_xch": true, - "combine_xch": true, + "combine": true, + "split": true, "auto_combine_xch": true, - "split_xch": true, - "combine_cat": true, "auto_combine_cat": true, - "split_cat": true, "issue_cat": true, "send_cat": true, "bulk_send_cat": true, @@ -72,7 +74,9 @@ "set_network_override": true, "get_networks": false, "get_network": false, - "remove_cat": true, + "set_delta_sync": false, + "set_delta_sync_override": false, + "resync_cat": true, "update_cat": true, "update_did": true, "update_nft": true, diff --git a/crates/sage-api/src/records.rs b/crates/sage-api/src/records.rs index b60e9a474..eb5a7092e 100644 --- a/crates/sage-api/src/records.rs +++ b/crates/sage-api/src/records.rs @@ -1,4 +1,3 @@ -mod cat; mod coin; mod derivation; mod did; @@ -8,10 +7,10 @@ mod offer; mod offer_summary; mod peer; mod pending_transaction; +mod token; mod transaction; mod transaction_summary; -pub use cat::*; pub use coin::*; pub use derivation::*; pub use did::*; @@ -21,5 +20,6 @@ pub use offer::*; pub use offer_summary::*; pub use peer::*; pub use pending_transaction::*; +pub use token::*; pub use transaction::*; pub use transaction_summary::*; diff --git a/crates/sage-api/src/records/coin.rs b/crates/sage-api/src/records/coin.rs index f8de89550..ad5c9ba82 100644 --- a/crates/sage-api/src/records/coin.rs +++ b/crates/sage-api/src/records/coin.rs @@ -8,11 +8,11 @@ pub struct CoinRecord { pub coin_id: String, pub address: String, pub amount: Amount, + pub transaction_id: Option, + pub offer_id: Option, + pub clawback_timestamp: Option, pub created_height: Option, pub spent_height: Option, - pub create_transaction_id: Option, - pub spend_transaction_id: Option, - pub offer_id: Option, - pub spent_timestamp: Option, - pub created_timestamp: Option, + pub spent_timestamp: Option, + pub created_timestamp: Option, } diff --git a/crates/sage-api/src/records/did.rs b/crates/sage-api/src/records/did.rs index c98b15a2f..52c854a8c 100644 --- a/crates/sage-api/src/records/did.rs +++ b/crates/sage-api/src/records/did.rs @@ -13,5 +13,4 @@ pub struct DidRecord { pub amount: Amount, pub recovery_hash: Option, pub created_height: Option, - pub create_transaction_id: Option, } diff --git a/crates/sage-api/src/records/nft.rs b/crates/sage-api/src/records/nft.rs index 8da8a4a1b..d65f19fb7 100644 --- a/crates/sage-api/src/records/nft.rs +++ b/crates/sage-api/src/records/nft.rs @@ -24,6 +24,7 @@ pub struct NftRecord { pub license_hash: Option, pub edition_number: Option, pub edition_total: Option, + pub icon_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/sage-api/src/records/offer.rs b/crates/sage-api/src/records/offer.rs index ca693eafe..3e084b190 100644 --- a/crates/sage-api/src/records/offer.rs +++ b/crates/sage-api/src/records/offer.rs @@ -8,7 +8,7 @@ pub struct OfferRecord { pub offer_id: String, pub offer: String, pub status: OfferRecordStatus, - pub creation_date: String, + pub creation_timestamp: u64, pub summary: OfferSummary, } @@ -16,8 +16,9 @@ pub struct OfferRecord { #[cfg_attr(feature = "tauri", derive(specta::Type))] #[serde(rename_all = "snake_case")] pub enum OfferRecordStatus { - Active = 0, - Completed = 1, - Cancelled = 2, - Expired = 3, + Pending = 0, + Active = 1, + Completed = 2, + Cancelled = 3, + Expired = 4, } diff --git a/crates/sage-api/src/records/offer_summary.rs b/crates/sage-api/src/records/offer_summary.rs index 26ecdcb0d..9ced2e45b 100644 --- a/crates/sage-api/src/records/offer_summary.rs +++ b/crates/sage-api/src/records/offer_summary.rs @@ -1,48 +1,29 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -use crate::Amount; +use crate::{Amount, Asset}; #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct OfferSummary { pub fee: Amount, - pub maker: OfferAssets, - pub taker: OfferAssets, + pub maker: Vec, + pub taker: Vec, pub expiration_height: Option, pub expiration_timestamp: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct OfferAssets { - pub xch: OfferXch, - pub cats: IndexMap, - pub nfts: IndexMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct OfferXch { - pub amount: Amount, - pub royalty: Amount, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct OfferCat { +pub struct OfferAsset { + pub asset: Asset, pub amount: Amount, pub royalty: Amount, - pub name: Option, - pub ticker: Option, - pub icon_url: Option, + pub nft_royalty: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct OfferNft { - pub icon: Option, - pub name: Option, - pub royalty_ten_thousandths: u16, +pub struct NftRoyalty { pub royalty_address: String, + pub royalty_basis_points: u16, } diff --git a/crates/sage-api/src/records/cat.rs b/crates/sage-api/src/records/token.rs similarity index 80% rename from crates/sage-api/src/records/cat.rs rename to crates/sage-api/src/records/token.rs index 3b13a8555..26c4720ab 100644 --- a/crates/sage-api/src/records/cat.rs +++ b/crates/sage-api/src/records/token.rs @@ -4,10 +4,11 @@ use crate::Amount; #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct CatRecord { - pub asset_id: String, +pub struct TokenRecord { + pub asset_id: Option, pub name: Option, pub ticker: Option, + pub precision: u8, pub description: Option, pub icon_url: Option, pub visible: bool, diff --git a/crates/sage-api/src/records/transaction.rs b/crates/sage-api/src/records/transaction.rs index ede399fbe..1818fb97f 100644 --- a/crates/sage-api/src/records/transaction.rs +++ b/crates/sage-api/src/records/transaction.rs @@ -1,23 +1,22 @@ use serde::{Deserialize, Serialize}; -use crate::{AddressKind, Amount, AssetKind}; +use crate::{AddressKind, Amount, Asset}; #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct TransactionRecord { pub height: u32, - pub timestamp: Option, - pub spent: Vec, - pub created: Vec, + pub timestamp: Option, + pub spent: Vec, + pub created: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct TransactionCoin { +pub struct TransactionCoinRecord { pub coin_id: String, pub amount: Amount, pub address: Option, pub address_kind: AddressKind, - #[serde(flatten)] - pub kind: AssetKind, + pub asset: Asset, } diff --git a/crates/sage-api/src/records/transaction_summary.rs b/crates/sage-api/src/records/transaction_summary.rs index b32af2fbb..21514f95a 100644 --- a/crates/sage-api/src/records/transaction_summary.rs +++ b/crates/sage-api/src/records/transaction_summary.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{Amount, AssetKind}; +use crate::{Amount, Asset}; #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] @@ -38,8 +38,7 @@ pub struct TransactionInput { pub coin_id: String, pub amount: Amount, pub address: String, - #[serde(flatten)] - pub kind: AssetKind, + pub asset: Option, pub outputs: Vec, } diff --git a/crates/sage-api/src/requests/actions.rs b/crates/sage-api/src/requests/actions.rs index 14387eb32..ff3e8b9e7 100644 --- a/crates/sage-api/src/requests/actions.rs +++ b/crates/sage-api/src/requests/actions.rs @@ -1,21 +1,21 @@ use serde::{Deserialize, Serialize}; -use crate::CatRecord; +use crate::TokenRecord; #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct RemoveCat { +pub struct ResyncCat { pub asset_id: String, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct RemoveCatResponse {} +pub struct ResyncCatResponse {} #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct UpdateCat { - pub record: CatRecord, + pub record: TokenRecord, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] diff --git a/crates/sage-api/src/requests/data.rs b/crates/sage-api/src/requests/data.rs index 201d74223..c693d0d61 100644 --- a/crates/sage-api/src/requests/data.rs +++ b/crates/sage-api/src/requests/data.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; use crate::{ - Amount, CatRecord, CoinRecord, DerivationRecord, DidRecord, NftCollectionRecord, NftData, - NftRecord, PendingTransactionRecord, TransactionRecord, Unit, + Amount, CoinRecord, DerivationRecord, DidRecord, NftCollectionRecord, NftData, NftRecord, + PendingTransactionRecord, TokenRecord, TransactionRecord, Unit, }; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -33,6 +33,39 @@ pub struct GetDerivationsResponse { pub total: u32, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct PerformDatabaseMaintenance { + pub force_vacuum: bool, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct PerformDatabaseMaintenanceResponse { + pub vacuum_duration_ms: u64, + pub analyze_duration_ms: u64, + pub wal_checkpoint_duration_ms: u64, + pub total_duration_ms: u64, + pub pages_vacuumed: i64, + pub wal_pages_checkpointed: i64, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetDatabaseStats {} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetDatabaseStatsResponse { + pub total_pages: i64, + pub free_pages: i64, + pub free_percentage: f64, + pub page_size: i64, + pub database_size_bytes: i64, + pub free_space_bytes: i64, + pub wal_pages: i64, +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetSyncStatus {} @@ -48,8 +81,8 @@ pub struct GetSyncStatusResponse { pub burn_address: String, pub unhardened_derivation_index: u32, pub hardened_derivation_index: u32, - pub checked_uris: u32, - pub total_uris: u32, + pub checked_files: u32, + pub total_files: u32, pub database_size: u64, } @@ -63,22 +96,6 @@ pub struct GetVersionResponse { pub version: String, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -#[serde(rename_all = "snake_case")] -pub enum CoinSortMode { - CoinId, - Amount, - CreatedHeight, - SpentHeight, -} - -impl Default for CoinSortMode { - fn default() -> Self { - Self::CreatedHeight - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetAreCoinsSpendable { @@ -94,7 +111,7 @@ pub struct GetAreCoinsSpendableResponse { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetSpendableCoinCount { - pub asset_id: String, + pub asset_id: Option, } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] @@ -103,57 +120,72 @@ pub struct GetSpendableCoinCountResponse { pub count: u32, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct GetXchCoins { +#[serde(rename_all = "snake_case")] +pub enum CoinSortMode { + CoinId, + Amount, + #[default] + CreatedHeight, + SpentHeight, + ClawbackTimestamp, +} + +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +#[serde(rename_all = "snake_case")] +pub enum CoinFilterMode { + All, + #[default] + Selectable, + Owned, + Spent, + Clawback, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetCoins { + #[serde(default)] + pub asset_id: Option, pub offset: u32, pub limit: u32, #[serde(default)] pub sort_mode: CoinSortMode, #[serde(default)] - pub ascending: bool, + pub filter_mode: CoinFilterMode, #[serde(default)] - pub include_spent_coins: bool, + pub ascending: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct GetCoinsByIds { - pub coin_ids: Vec, +pub struct GetCoinsResponse { + pub coins: Vec, + pub total: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct GetCoinsByIdsResponse { - pub coins: Vec, +pub struct GetCoinsByIds { + pub coin_ids: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct GetXchCoinsResponse { +pub struct GetCoinsByIdsResponse { pub coins: Vec, - pub total: u32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct GetCatCoins { - pub asset_id: String, - pub offset: u32, - pub limit: u32, - #[serde(default)] - pub sort_mode: CoinSortMode, - #[serde(default)] - pub ascending: bool, - #[serde(default)] - pub include_spent_coins: bool, -} +pub struct GetAllCats {} #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct GetCatCoinsResponse { - pub coins: Vec, - pub total: u32, +pub struct GetAllCatsResponse { + pub cats: Vec, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] @@ -163,19 +195,19 @@ pub struct GetCats {} #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetCatsResponse { - pub cats: Vec, + pub cats: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct GetCat { - pub asset_id: String, +pub struct GetToken { + pub asset_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct GetCatResponse { - pub cat: Option, +pub struct GetTokenResponse { + pub token: Option, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] @@ -206,6 +238,18 @@ pub struct GetMinterDidIdsResponse { #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetPendingTransactions {} +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetTransaction { + pub height: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetTransactionResponse { + pub transaction: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetPendingTransactionsResponse { diff --git a/crates/sage-api/src/requests/keys.rs b/crates/sage-api/src/requests/keys.rs index d0d512252..0ece98e37 100644 --- a/crates/sage-api/src/requests/keys.rs +++ b/crates/sage-api/src/requests/keys.rs @@ -26,13 +26,17 @@ pub struct LogoutResponse {} pub struct Resync { pub fingerprint: u32, #[serde(default)] - pub delete_offer_files: bool, + pub delete_coins: bool, #[serde(default)] - pub delete_unhardened_derivations: bool, + pub delete_assets: bool, #[serde(default)] - pub delete_hardened_derivations: bool, + pub delete_files: bool, #[serde(default)] - pub delete_blockinfo: bool, + pub delete_offers: bool, + #[serde(default)] + pub delete_addresses: bool, + #[serde(default)] + pub delete_blocks: bool, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] @@ -74,6 +78,17 @@ pub struct ImportKeyResponse { pub fingerprint: u32, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct DeleteDatabase { + pub fingerprint: u32, + pub network: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct DeleteDatabaseResponse {} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct DeleteKey { diff --git a/crates/sage-api/src/requests/offers.rs b/crates/sage-api/src/requests/offers.rs index b816a3835..daf4ba3e9 100644 --- a/crates/sage-api/src/requests/offers.rs +++ b/crates/sage-api/src/requests/offers.rs @@ -47,8 +47,6 @@ pub struct TakeOffer { pub fee: Amount, #[serde(default)] pub auto_submit: bool, - #[serde(default = "yes")] - pub auto_import: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/sage-api/src/requests/settings.rs b/crates/sage-api/src/requests/settings.rs index 8032e01fd..03414d929 100644 --- a/crates/sage-api/src/requests/settings.rs +++ b/crates/sage-api/src/requests/settings.rs @@ -20,50 +20,30 @@ pub struct RemovePeer { pub ban: bool, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct RemovePeerResponse {} - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct AddPeer { pub ip: String, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct AddPeerResponse {} - #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct SetDiscoverPeers { pub discover_peers: bool, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct SetDiscoverPeersResponse {} - #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct SetTargetPeers { pub target_peers: u32, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct SetTargetPeersResponse {} - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct SetNetwork { pub name: String, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct SetNetworkResponse {} - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct SetNetworkOverride { @@ -71,10 +51,6 @@ pub struct SetNetworkOverride { pub name: Option, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct SetNetworkOverrideResponse {} - #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetNetworks {} @@ -100,3 +76,29 @@ pub enum NetworkKind { Testnet, Unknown, } + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct SetDeltaSync { + pub delta_sync: bool, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct SetDeltaSyncOverride { + pub fingerprint: u32, + pub delta_sync: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct EmptyResponse {} + +pub type AddPeerResponse = EmptyResponse; +pub type RemovePeerResponse = EmptyResponse; +pub type SetDiscoverPeersResponse = EmptyResponse; +pub type SetTargetPeersResponse = EmptyResponse; +pub type SetNetworkResponse = EmptyResponse; +pub type SetNetworkOverrideResponse = EmptyResponse; +pub type SetDeltaSyncResponse = EmptyResponse; +pub type SetDeltaSyncOverrideResponse = EmptyResponse; diff --git a/crates/sage-api/src/requests/transactions.rs b/crates/sage-api/src/requests/transactions.rs index b1509395d..e098b2429 100644 --- a/crates/sage-api/src/requests/transactions.rs +++ b/crates/sage-api/src/requests/transactions.rs @@ -9,7 +9,9 @@ pub struct SendXch { pub amount: Amount, pub fee: Amount, #[serde(default)] - pub memos: Option>, + pub memos: Vec, + #[serde(default)] + pub clawback: Option, #[serde(default)] pub auto_submit: bool, } @@ -21,14 +23,14 @@ pub struct BulkSendXch { pub amount: Amount, pub fee: Amount, #[serde(default)] - pub memos: Option>, + pub memos: Vec, #[serde(default)] pub auto_submit: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct CombineXch { +pub struct Combine { pub coin_ids: Vec, pub fee: Amount, #[serde(default)] @@ -37,9 +39,9 @@ pub struct CombineXch { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct AutoCombineXch { - pub max_coins: u32, - pub max_coin_amount: Option, +pub struct Split { + pub coin_ids: Vec, + pub output_count: u32, pub fee: Amount, #[serde(default)] pub auto_submit: bool, @@ -47,17 +49,9 @@ pub struct AutoCombineXch { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct AutoCombineXchResponse { - pub coin_ids: Vec, - pub summary: TransactionSummary, - pub coin_spends: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct SplitXch { - pub coin_ids: Vec, - pub output_count: u32, +pub struct AutoCombineXch { + pub max_coins: u32, + pub max_coin_amount: Option, pub fee: Amount, #[serde(default)] pub auto_submit: bool, @@ -65,11 +59,10 @@ pub struct SplitXch { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct CombineCat { +pub struct AutoCombineXchResponse { pub coin_ids: Vec, - pub fee: Amount, - #[serde(default)] - pub auto_submit: bool, + pub summary: TransactionSummary, + pub coin_spends: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -91,16 +84,6 @@ pub struct AutoCombineCatResponse { pub coin_spends: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -pub struct SplitCat { - pub coin_ids: Vec, - pub output_count: u32, - pub fee: Amount, - #[serde(default)] - pub auto_submit: bool, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct IssueCat { @@ -122,7 +105,9 @@ pub struct SendCat { #[serde(default = "yes")] pub include_hint: bool, #[serde(default)] - pub memos: Option>, + pub memos: Vec, + #[serde(default)] + pub clawback: Option, #[serde(default)] pub auto_submit: bool, } @@ -137,7 +122,7 @@ pub struct BulkSendCat { #[serde(default = "yes")] pub include_hint: bool, #[serde(default)] - pub memos: Option>, + pub memos: Vec, #[serde(default)] pub auto_submit: bool, } @@ -163,7 +148,7 @@ pub struct Payment { pub address: String, pub amount: Amount, #[serde(default)] - pub memos: Option>, + pub memos: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -227,6 +212,8 @@ pub struct TransferNfts { pub address: String, pub fee: Amount, #[serde(default)] + pub clawback: Option, + #[serde(default)] pub auto_submit: bool, } @@ -267,6 +254,8 @@ pub struct TransferDids { pub address: String, pub fee: Amount, #[serde(default)] + pub clawback: Option, + #[serde(default)] pub auto_submit: bool, } @@ -326,10 +315,8 @@ pub struct TransactionResponse { pub type SendXchResponse = TransactionResponse; pub type BulkSendXchResponse = TransactionResponse; -pub type CombineXchResponse = TransactionResponse; -pub type SplitXchResponse = TransactionResponse; -pub type CombineCatResponse = TransactionResponse; -pub type SplitCatResponse = TransactionResponse; +pub type CombineResponse = TransactionResponse; +pub type SplitResponse = TransactionResponse; pub type IssueCatResponse = TransactionResponse; pub type SendCatResponse = TransactionResponse; pub type BulkSendCatResponse = TransactionResponse; diff --git a/crates/sage-api/src/types.rs b/crates/sage-api/src/types.rs index 564f980f0..b17cbe1d7 100644 --- a/crates/sage-api/src/types.rs +++ b/crates/sage-api/src/types.rs @@ -1,13 +1,13 @@ mod address_kind; mod amount; -mod asset_kind; +mod asset; mod error_kind; mod key_info; mod unit; pub use address_kind::*; pub use amount::*; -pub use asset_kind::*; +pub use asset::*; pub use error_kind::*; pub use key_info::*; pub use unit::*; diff --git a/crates/sage-api/src/types/asset.rs b/crates/sage-api/src/types/asset.rs new file mode 100644 index 000000000..71d568b9f --- /dev/null +++ b/crates/sage-api/src/types/asset.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct Asset { + pub asset_id: Option, + pub name: Option, + pub ticker: Option, + pub precision: u8, + pub icon_url: Option, + pub description: Option, + pub is_sensitive_content: bool, + pub is_visible: bool, + pub kind: AssetKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +#[serde(rename_all = "snake_case")] +pub enum AssetKind { + Token, + Nft, + Did, + Option, +} diff --git a/crates/sage-api/src/types/asset_kind.rs b/crates/sage-api/src/types/asset_kind.rs deleted file mode 100644 index 42d4f5f1d..000000000 --- a/crates/sage-api/src/types/asset_kind.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "tauri", derive(specta::Type))] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum AssetKind { - Unknown, - Xch, - Launcher, - Cat { - asset_id: String, - name: Option, - ticker: Option, - icon_url: Option, - }, - Did { - launcher_id: String, - name: Option, - }, - Nft { - launcher_id: String, - icon: Option, - name: Option, - }, -} diff --git a/crates/sage-assets/Cargo.toml b/crates/sage-assets/Cargo.toml index 8dbb117c1..ba808452d 100644 --- a/crates/sage-assets/Cargo.toml +++ b/crates/sage-assets/Cargo.toml @@ -28,3 +28,4 @@ mime-sniffer = { workspace = true } image = { workspace = true } webp = { workspace = true } tokio = { workspace = true, features = ["sync"] } +base64 = { workspace = true } diff --git a/crates/sage-assets/src/cats.rs b/crates/sage-assets/src/cats.rs new file mode 100644 index 000000000..03124d38b --- /dev/null +++ b/crates/sage-assets/src/cats.rs @@ -0,0 +1,106 @@ +use chia::protocol::Bytes32; +use serde::Deserialize; + +use crate::UriError; + +#[derive(Debug, Clone)] +pub struct DexieCat { + pub hash: Bytes32, + pub name: Option, + pub icon_url: Option, + pub description: Option, + pub ticker: Option, +} + +impl DexieCat { + pub async fn fetch_all(testnet: bool) -> Result, UriError> { + let mut page = 1; + let mut assets = Vec::new(); + + loop { + let response = reqwest::get(format!( + "{}/assets?page_size=100&page={page}&type=cat", + dexie_base_url(testnet) + )) + .await? + .json::() + .await?; + + if response.assets.is_empty() { + break; + } + + for asset in response.assets { + assets.push(Self { + hash: asset.id, + name: asset.name, + icon_url: Some(format!( + "{}/{}.webp", + dexie_image_base_url(testnet), + asset.id + )), + description: asset.description, + ticker: asset.code, + }); + } + + page += 1; + } + + Ok(assets) + } + + pub async fn fetch(asset_id: Bytes32, testnet: bool) -> Result { + let response = reqwest::get(format!( + "{}/assets?page_size=25&page=1&type=cat&code={asset_id}", + dexie_base_url(testnet) + )) + .await? + .json::() + .await?; + + let asset = response.assets.first().cloned().unwrap_or_default(); + let icon_url = (!response.assets.is_empty()).then_some(format!( + "{}/{}.webp", + dexie_image_base_url(testnet), + asset.id + )); + + Ok(Self { + hash: asset_id, + name: asset.name, + icon_url, + description: asset.description, + ticker: asset.code, + }) + } +} + +#[derive(Deserialize)] +struct AssetResponse { + assets: Vec, +} + +#[derive(Deserialize, Default, Clone)] +struct AssetData { + id: Bytes32, + name: Option, + code: Option, + description: Option, +} + +fn dexie_base_url(testnet: bool) -> &'static str { + if testnet { + "https://api-testnet.dexie.space/v1" + } else { + "https://api.dexie.space/v1" + } +} + +fn dexie_image_base_url(testnet: bool) -> &'static str { + if testnet { + "https://icons-testnet.dexie.space" + } else { + "https://icons.dexie.space" + } +} diff --git a/crates/sage-assets/src/error.rs b/crates/sage-assets/src/error.rs new file mode 100644 index 000000000..97d634623 --- /dev/null +++ b/crates/sage-assets/src/error.rs @@ -0,0 +1,25 @@ +use chia::protocol::Bytes32; +use thiserror::Error; + +use crate::ThumbnailError; + +#[derive(Debug, Error)] +pub enum UriError { + #[error("Failed to fetch NFT data: {0}")] + Fetch(#[from] reqwest::Error), + + #[error("Missing or invalid content type")] + InvalidContentType, + + #[error("Mime type mismatch, expected {expected} but found {found}")] + MimeTypeMismatch { expected: String, found: String }, + + #[error("Hash mismatch, expected {expected} but found {found}")] + HashMismatch { expected: Bytes32, found: Bytes32 }, + + #[error("No URIs provided")] + NoUris, + + #[error("Failed to create thumbnail: {0}")] + Thumbnail(#[from] ThumbnailError), +} diff --git a/crates/sage-assets/src/lib.rs b/crates/sage-assets/src/lib.rs index 06f365bea..c4eb6e57d 100644 --- a/crates/sage-assets/src/lib.rs +++ b/crates/sage-assets/src/lib.rs @@ -1,3 +1,7 @@ +mod cats; +mod error; mod nfts; +pub use cats::*; +pub use error::*; pub use nfts::*; diff --git a/crates/sage-assets/src/nfts.rs b/crates/sage-assets/src/nfts.rs index cceadd4c0..4f7fbfeda 100644 --- a/crates/sage-assets/src/nfts.rs +++ b/crates/sage-assets/src/nfts.rs @@ -1,7 +1,9 @@ mod chip0007_metadata; +mod data_uri; mod fetch_nft_uri; mod thumbnail; pub use chip0007_metadata::*; +pub use data_uri::*; pub use fetch_nft_uri::*; pub use thumbnail::*; diff --git a/crates/sage-assets/src/nfts/data_uri.rs b/crates/sage-assets/src/nfts/data_uri.rs new file mode 100644 index 000000000..0e0b5e68d --- /dev/null +++ b/crates/sage-assets/src/nfts/data_uri.rs @@ -0,0 +1,5 @@ +use base64::{prelude::BASE64_STANDARD, Engine}; + +pub fn base64_data_uri(blob: &[u8], mime_type: &str) -> String { + format!("data:{mime_type};base64,{}", BASE64_STANDARD.encode(blob)) +} diff --git a/crates/sage-assets/src/nfts/fetch_nft_uri.rs b/crates/sage-assets/src/nfts/fetch_nft_uri.rs index 4fe4de895..70ccc1fdf 100644 --- a/crates/sage-assets/src/nfts/fetch_nft_uri.rs +++ b/crates/sage-assets/src/nfts/fetch_nft_uri.rs @@ -6,11 +6,12 @@ use futures_lite::StreamExt; use futures_util::stream::FuturesUnordered; use mime_sniffer::MimeTypeSniffer; use reqwest::{header::CONTENT_TYPE, StatusCode}; -use thiserror::Error; use tokio::task::spawn_blocking; use tracing::debug; -use super::{thumbnail as make_thumbnail, Thumbnail, ThumbnailError}; +use crate::UriError; + +use super::{thumbnail as make_thumbnail, Thumbnail}; #[derive(Debug, Clone)] pub struct Data { @@ -20,27 +21,6 @@ pub struct Data { pub thumbnail: Option, } -#[derive(Debug, Error)] -pub enum UriError { - #[error("Failed to fetch NFT data: {0}")] - Fetch(#[from] reqwest::Error), - - #[error("Missing or invalid content type")] - InvalidContentType, - - #[error("Mime type mismatch, expected {expected} but found {found}")] - MimeTypeMismatch { expected: String, found: String }, - - #[error("Hash mismatch, expected {expected} but found {found}")] - HashMismatch { expected: Bytes32, found: Bytes32 }, - - #[error("No URIs provided")] - NoUris, - - #[error("Failed to create thumbnail: {0}")] - Thumbnail(#[from] ThumbnailError), -} - pub async fn fetch_uri(uri: String) -> Result { let response = reqwest::get(&uri).await?; diff --git a/crates/sage-config/src/old.rs b/crates/sage-config/src/old.rs index 721ae2098..45dd842bf 100644 --- a/crates/sage-config/src/old.rs +++ b/crates/sage-config/src/old.rs @@ -158,6 +158,7 @@ pub fn migrate_config(old: OldConfig) -> Result<(Config, WalletConfig), ParseInt }, change: ChangeMode::Default, network: None, + delta_sync: None, }); } diff --git a/crates/sage-config/src/wallet.rs b/crates/sage-config/src/wallet.rs index aa133bb5f..624ef89cc 100644 --- a/crates/sage-config/src/wallet.rs +++ b/crates/sage-config/src/wallet.rs @@ -13,6 +13,7 @@ pub struct WalletConfig { pub struct WalletDefaults { pub change: ChangeMode, pub derivation: DerivationMode, + pub delta_sync: bool, } impl Default for WalletDefaults { @@ -22,25 +23,56 @@ impl Default for WalletDefaults { derivation: DerivationMode::Auto { derivation_batch_size: 1000, }, + delta_sync: true, } } } #[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(default)] pub struct Wallet { - #[serde(default = "Wallet::default_name")] pub name: String, pub fingerprint: u32, - #[serde(default, skip_serializing_if = "ChangeMode::is_default")] + #[serde(skip_serializing_if = "ChangeMode::is_default")] pub change: ChangeMode, - #[serde(default, skip_serializing_if = "DerivationMode::is_default")] + #[serde(skip_serializing_if = "DerivationMode::is_default")] pub derivation: DerivationMode, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub network: Option, + pub delta_sync: Option, } impl Wallet { - pub fn default_name() -> String { - "Unnamed".to_string() + pub fn change(&self, defaults: &WalletDefaults) -> ChangeMode { + if matches!(self.change, ChangeMode::Default) { + defaults.change + } else { + self.change + } + } + + pub fn derivation(&self, defaults: &WalletDefaults) -> DerivationMode { + if matches!(self.derivation, DerivationMode::Default) { + defaults.derivation + } else { + self.derivation + } + } + + pub fn delta_sync(&self, defaults: &WalletDefaults) -> bool { + self.delta_sync.unwrap_or(defaults.delta_sync) + } +} + +impl Default for Wallet { + fn default() -> Self { + Self { + name: "Unnamed Wallet".to_string(), + fingerprint: 0, + change: ChangeMode::Default, + derivation: DerivationMode::Default, + network: None, + delta_sync: None, + } } } @@ -99,6 +131,7 @@ mod tests { change: ChangeMode::Default, derivation: DerivationMode::Default, network: None, + delta_sync: None, } } @@ -119,6 +152,9 @@ mod tests { check( config, &expect![[r#" + [defaults] + delta_sync = true + [defaults.change] mode = "same" @@ -139,12 +175,14 @@ mod tests { "derivation": { "mode": "auto", "derivation_batch_size": 1000 - } + }, + "delta_sync": true }, "wallets": [ { "name": "Main", - "fingerprint": 1000000 + "fingerprint": 1000000, + "delta_sync": null } ] }"#]], @@ -163,6 +201,9 @@ mod tests { check( config, &expect![[r#" + [defaults] + delta_sync = true + [defaults.change] mode = "same" @@ -190,7 +231,8 @@ mod tests { "derivation": { "mode": "auto", "derivation_batch_size": 1000 - } + }, + "delta_sync": true }, "wallets": [ { @@ -202,7 +244,8 @@ mod tests { "derivation": { "mode": "auto", "derivation_batch_size": 1000 - } + }, + "delta_sync": null } ] }"#]], diff --git a/crates/sage-database/Cargo.toml b/crates/sage-database/Cargo.toml index 76b2e1dd4..944e5a3de 100644 --- a/crates/sage-database/Cargo.toml +++ b/crates/sage-database/Cargo.toml @@ -19,5 +19,5 @@ chia = { workspace = true } chia-wallet-sdk = { workspace = true } sqlx = { workspace = true, features = ["sqlite"] } thiserror = { workspace = true } -hex = { workspace = true } tracing = { workspace = true } +hex = { workspace = true } diff --git a/crates/sage-database/src/blockinfo.rs b/crates/sage-database/src/blockinfo.rs deleted file mode 100644 index 9b2ac4dfe..000000000 --- a/crates/sage-database/src/blockinfo.rs +++ /dev/null @@ -1,162 +0,0 @@ -use sqlx::SqliteExecutor; - -use crate::{Database, DatabaseTx, Result}; -//use std::error::Error; - -impl Database { - pub async fn find_created_timestamp_null(&self, limit: u32) -> Result> { - find_created_timestamp_null(&self.pool, limit).await - } - - pub async fn find_spent_timestamp_null(&self, limit: u32) -> Result> { - find_spent_timestamp_null(&self.pool, limit).await - } - - pub async fn check_blockinfo(&self, height: u32) -> Result> { - check_blockinfo(&self.pool, height).await - } - - pub async fn update_created_timestamp(&self, height: u32, timestamp: i64) -> Result<()> { - update_created_timestamp(&self.pool, height, timestamp).await - } - - pub async fn update_spent_timestamp(&self, height: u32, timestamp: i64) -> Result<()> { - update_spent_timestamp(&self.pool, height, timestamp).await - } - - pub async fn insert_timestamp_height(&self, height: u32, timestamp: i64) -> Result<()> { - insert_timestamp_height(&self.pool, height, timestamp).await - } -} - -impl DatabaseTx<'_> { - pub async fn find_created_timestamp_null(&mut self, limit: u32) -> Result> { - find_created_timestamp_null(&mut *self.tx, limit).await - } - - pub async fn find_spent_timestamp_null(&mut self, limit: u32) -> Result> { - find_spent_timestamp_null(&mut *self.tx, limit).await - } -} - -async fn find_created_timestamp_null( - conn: impl SqliteExecutor<'_>, - limit: u32, -) -> Result> { - let row = sqlx::query!( - " - SELECT DISTINCT `created_height` - FROM `coin_states` - WHERE `created_unixtime` IS NULL - AND `created_height` IS NOT NULL - ORDER BY `created_height` DESC - LIMIT ?; - ", - limit - ) - .fetch_all(conn) - .await?; - - row.into_iter() - .filter_map(|r| r.created_height) - .map(|height| Ok(height.try_into()?)) - .collect::>>() -} - -async fn find_spent_timestamp_null(conn: impl SqliteExecutor<'_>, limit: u32) -> Result> { - let row = sqlx::query!( - " - SELECT DISTINCT `spent_height` - FROM `coin_states` - WHERE `spent_unixtime` IS NULL - AND `spent_height` IS NOT NULL - ORDER BY `spent_height` DESC - LIMIT ?; - ", - limit - ) - .fetch_all(conn) - .await?; - - row.into_iter() - .filter_map(|r| r.spent_height) - .map(|height| Ok(height.try_into()?)) - .collect::>>() -} - -async fn check_blockinfo(conn: impl SqliteExecutor<'_>, height: u32) -> Result> { - let row = sqlx::query!( - " - SELECT `unix_time` - FROM `blockinfo` - WHERE `height` = ? - ", - height - ) - .fetch_optional(conn) - .await?; - - Ok(row.map(|r| r.unix_time)) -} - -async fn update_created_timestamp( - conn: impl SqliteExecutor<'_>, - height: u32, - timestamp: i64, -) -> Result<()> { - sqlx::query!( - " - UPDATE OR IGNORE `coin_states` - SET `created_unixtime` = ? - WHERE `created_height` = ? - ", - timestamp, - height - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn update_spent_timestamp( - conn: impl SqliteExecutor<'_>, - height: u32, - timestamp: i64, -) -> Result<()> { - sqlx::query!( - " - UPDATE OR IGNORE `coin_states` - SET `spent_unixtime` = ? - WHERE `spent_height` = ? - ", - timestamp, - height - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn insert_timestamp_height( - conn: impl SqliteExecutor<'_>, - height: u32, - unix_timestamp: i64, -) -> Result<()> { - sqlx::query!( - " - INSERT OR IGNORE INTO `blockinfo` ( - `height`, - `unix_time` - ) - VALUES (?, ?) - ", - height, - unix_timestamp - ) - .execute(conn) - .await?; - - Ok(()) -} diff --git a/crates/sage-database/src/coin_states.rs b/crates/sage-database/src/coin_states.rs deleted file mode 100644 index fb002526c..000000000 --- a/crates/sage-database/src/coin_states.rs +++ /dev/null @@ -1,718 +0,0 @@ -use chia::protocol::{Bytes32, CoinState}; -use sqlx::SqliteExecutor; -use sqlx::{sqlite::SqliteRow, Row}; - -use crate::{ - into_row, to_bytes32, CoinKind, CoinStateRow, CoinStateSql, Database, DatabaseTx, IntoRow, - Result, -}; - -impl Database { - pub async fn unsynced_coin_states(&self, limit: usize) -> Result> { - unsynced_coin_states(&self.pool, limit).await - } - - pub async fn coin_state(&self, coin_id: Bytes32) -> Result> { - coin_state(&self.pool, coin_id).await - } - - pub async fn full_coin_state(&self, coin_id: Bytes32) -> Result> { - full_coin_state(&self.pool, coin_id).await - } - - pub async fn unspent_nft_coin_ids(&self) -> Result> { - unspent_nft_coin_ids(&self.pool).await - } - - pub async fn unspent_did_coin_ids(&self) -> Result> { - unspent_did_coin_ids(&self.pool).await - } - - pub async fn unspent_cat_coin_ids(&self) -> Result> { - unspent_cat_coin_ids(&self.pool).await - } - - pub async fn delete_coin_state(&self, coin_id: Bytes32) -> Result<()> { - delete_coin_state(&self.pool, coin_id).await - } - - pub async fn total_coin_count(&self) -> Result { - total_coin_count(&self.pool).await - } - - pub async fn synced_coin_count(&self) -> Result { - synced_coin_count(&self.pool).await - } - - pub async fn sync_coin( - &self, - coin_id: Bytes32, - hint: Option, - kind: CoinKind, - ) -> Result<()> { - sync_coin(&self.pool, coin_id, hint, kind).await - } - - pub async fn is_coin_locked(&self, coin_id: Bytes32) -> Result { - is_coin_locked(&self.pool, coin_id).await - } - - pub async fn get_transaction_coins( - &self, - offset: u32, - limit: u32, - asc: bool, - find_value: Option, - ) -> Result<(Vec, u32)> { - get_transaction_coins(&self.pool, offset, limit, asc, find_value).await - } - - pub async fn get_coin_states_by_created_height( - &self, - height: u32, - ) -> Result> { - get_coin_states_by_created_height(&self.pool, height).await - } - - pub async fn get_coin_states_by_spent_height(&self, height: u32) -> Result> { - get_coin_states_by_spent_height(&self.pool, height).await - } - - pub async fn get_are_coins_spendable(&self, coin_ids: &[String]) -> Result { - get_are_coins_spendable(&self.pool, coin_ids).await - } -} - -impl DatabaseTx<'_> { - pub async fn insert_coin_state( - &mut self, - coin_state: CoinState, - synced: bool, - transaction_id: Option, - ) -> Result<()> { - insert_coin_state(&mut *self.tx, coin_state, synced, transaction_id).await - } - - pub async fn update_coin_state( - &mut self, - coin_id: Bytes32, - created_height: Option, - spent_height: Option, - transaction_id: Option, - ) -> Result<()> { - update_coin_state( - &mut *self.tx, - coin_id, - created_height, - spent_height, - transaction_id, - ) - .await - } - - pub async fn sync_coin( - &mut self, - coin_id: Bytes32, - hint: Option, - kind: CoinKind, - ) -> Result<()> { - sync_coin(&mut *self.tx, coin_id, hint, kind).await - } - - pub async fn unsync_coin(&mut self, coin_id: Bytes32) -> Result<()> { - unsync_coin(&mut *self.tx, coin_id).await - } - - pub async fn remove_coin_transaction_id(&mut self, coin_id: Bytes32) -> Result<()> { - remove_coin_transaction_id(&mut *self.tx, coin_id).await - } - - pub async fn is_p2_coin(&mut self, coin_id: Bytes32) -> Result> { - is_p2_coin(&mut *self.tx, coin_id).await - } -} - -async fn insert_coin_state( - conn: impl SqliteExecutor<'_>, - coin_state: CoinState, - synced: bool, - transaction_id: Option, -) -> Result<()> { - let coin_id = coin_state.coin.coin_id(); - let coin_id_ref = coin_id.as_ref(); - let parent_coin_id = coin_state.coin.parent_coin_info.as_ref(); - let puzzle_hash = coin_state.coin.puzzle_hash.as_ref(); - let amount = coin_state.coin.amount.to_be_bytes(); - let amount_ref = amount.as_ref(); - let transaction_id = transaction_id.as_deref(); - - sqlx::query!( - " - INSERT OR IGNORE INTO `coin_states` ( - `coin_id`, - `parent_coin_id`, - `puzzle_hash`, - `amount`, - `created_height`, - `spent_height`, - `synced`, - `transaction_id` - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ", - coin_id_ref, - parent_coin_id, - puzzle_hash, - amount_ref, - coin_state.created_height, - coin_state.spent_height, - synced, - transaction_id - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn update_coin_state( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, - created_height: Option, - spent_height: Option, - transaction_id: Option, -) -> Result<()> { - let coin_id = coin_id.as_ref(); - let transaction_id = transaction_id.as_deref(); - - sqlx::query!( - " - UPDATE `coin_states` - SET `created_height` = ?, `spent_height` = ?, `transaction_id` = ? - WHERE `coin_id` = ? - ", - created_height, - spent_height, - transaction_id, - coin_id - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn delete_coin_state(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result<()> { - let coin_id = coin_id.as_ref(); - sqlx::query!( - " - DELETE FROM `coin_states` - WHERE `coin_id` = ? - ", - coin_id - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn unsynced_coin_states( - conn: impl SqliteExecutor<'_>, - limit: usize, -) -> Result> { - let limit: i64 = limit.try_into()?; - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` - WHERE `synced` = 0 AND `created_height` IS NOT NULL - ORDER BY `spent_height` ASC - LIMIT ? - ", - limit - ) - .fetch_all(conn) - .await?; - rows.into_iter() - .map(|sql| sql.into_row().map(|row| row.coin_state)) - .collect() -} - -async fn sync_coin( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, - hint: Option, - kind: CoinKind, -) -> Result<()> { - let coin_id = coin_id.as_ref(); - let kind = kind as u32; - let hint = hint.as_deref(); - - sqlx::query!( - " - UPDATE `coin_states` SET `synced` = 1, `hint` = ?, `kind` = ? WHERE `coin_id` = ? - ", - hint, - kind, - coin_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn unsync_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result<()> { - let coin_id = coin_id.as_ref(); - - sqlx::query!( - "UPDATE `coin_states` SET `synced` = 0 WHERE `coin_id` = ?", - coin_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn remove_coin_transaction_id(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result<()> { - let coin_id = coin_id.as_ref(); - sqlx::query!( - " - UPDATE `coin_states` - SET `transaction_id` = NULL - WHERE `coin_id` = ? - ", - coin_id - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn total_coin_count(conn: impl SqliteExecutor<'_>) -> Result { - let row = sqlx::query!( - " - SELECT COUNT(*) AS `total` - FROM `coin_states` - " - ) - .fetch_one(conn) - .await?; - Ok(row.total.try_into()?) -} - -async fn synced_coin_count(conn: impl SqliteExecutor<'_>) -> Result { - let row = sqlx::query!( - " - SELECT COUNT(*) AS `synced` - FROM `coin_states` - WHERE `synced` = 1 - " - ) - .fetch_one(conn) - .await?; - Ok(row.synced.try_into()?) -} - -async fn coin_state(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result> { - let coin_id = coin_id.as_ref(); - - let Some(sql) = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` - WHERE `coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(sql.into_row()?.coin_state)) -} - -async fn unspent_nft_coin_ids(conn: impl SqliteExecutor<'_>) -> Result> { - let rows = sqlx::query!( - " - SELECT `coin_states`.`coin_id` - FROM `coin_states` INDEXED BY `coin_spent` - INNER JOIN `nft_coins` ON `coin_states`.`coin_id` = `nft_coins`.`coin_id` - WHERE `spent_height` IS NULL - " - ) - .fetch_all(conn) - .await?; - - rows.into_iter() - .map(|row| to_bytes32(&row.coin_id)) - .collect() -} - -async fn unspent_did_coin_ids(conn: impl SqliteExecutor<'_>) -> Result> { - let rows = sqlx::query!( - " - SELECT `coin_states`.`coin_id` - FROM `coin_states` INDEXED BY `coin_spent` - INNER JOIN `did_coins` ON `coin_states`.`coin_id` = `did_coins`.`coin_id` - WHERE `spent_height` IS NULL - " - ) - .fetch_all(conn) - .await?; - - rows.into_iter() - .map(|row| to_bytes32(&row.coin_id)) - .collect() -} - -async fn unspent_cat_coin_ids(conn: impl SqliteExecutor<'_>) -> Result> { - let rows = sqlx::query!( - " - SELECT `coin_states`.`coin_id` - FROM `coin_states` INDEXED BY `coin_spent` - INNER JOIN `cat_coins` ON `coin_states`.`coin_id` = `cat_coins`.`coin_id` - WHERE `spent_height` IS NULL - " - ) - .fetch_all(conn) - .await?; - - rows.into_iter() - .map(|row| to_bytes32(&row.coin_id)) - .collect() -} - -async fn is_p2_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result> { - let coin_id = coin_id.as_ref(); - - let row = sqlx::query!( - " - SELECT `kind` - FROM `coin_states` - WHERE `coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await?; - - Ok(row.map(|row| row.kind == 1)) -} - -async fn is_coin_locked(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result { - let coin_id = coin_id.as_ref(); - - let row = sqlx::query!( - " - SELECT COUNT(*) AS `count` - FROM `coin_states` - LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id` - LEFT JOIN `offered_coins` ON `coin_states`.`coin_id` = `offered_coins`.`coin_id` - LEFT JOIN `offers` ON `offered_coins`.`offer_id` = `offers`.`offer_id` - WHERE `coin_states`.`coin_id` = ? - AND (`offers`.`offer_id` IS NULL OR `offers`.`status` > 0) - AND `coin_states`.`transaction_id` IS NULL - AND `transaction_spends`.`transaction_id` IS NULL - ", - coin_id - ) - .fetch_one(conn) - .await?; - - Ok(row.count > 0) -} - -async fn get_transaction_coins( - conn: impl SqliteExecutor<'_>, - offset: u32, - limit: u32, - asc: bool, - find_value: Option, -) -> Result<(Vec, u32)> { - let mut query = sqlx::QueryBuilder::new( - " - WITH coin_states_with_heights AS ( - SELECT - cs.coin_id, - cs.created_height as height - FROM coin_states cs - WHERE cs.created_height IS NOT NULL - - UNION ALL - - SELECT - cs.coin_id, - cs.spent_height as height - FROM coin_states cs - WHERE cs.spent_height IS NOT NULL - ), - joined_coin_states AS ( - SELECT - DISTINCT h.height - FROM coin_states_with_heights h - LEFT JOIN cat_coins ON h.coin_id = cat_coins.coin_id - LEFT JOIN cats ON cat_coins.asset_id = cats.asset_id - LEFT JOIN did_coins ON h.coin_id = did_coins.coin_id - LEFT JOIN dids ON did_coins.launcher_id = dids.launcher_id - LEFT JOIN nft_coins ON h.coin_id = nft_coins.coin_id - LEFT JOIN nfts ON nft_coins.launcher_id = nfts.launcher_id - WHERE 1=1 - ", - ); - - if let Some(value) = &find_value { - // Check if searching for XCH (matches "x", "xc", or "xch") - let should_filter_xch = if value.len() <= 3 { - let value_lower = value.to_lowercase(); - value_lower == "x" || value_lower == "xc" || value_lower == "xch" - } else { - false - }; - - query.push(" AND ("); - - if should_filter_xch { - // XCH coins have kind = 1 (standard P2 coins) - query.push("kind = 1 OR "); - } - - if is_valid_asset_id(value) { - query.push("cats.asset_id = X'").push(value).push("' OR "); - } else if is_valid_address(value, "nft") { - if let Some(puzzle_hash) = puzzle_hash_from_address(value) { - query - .push("nfts.launcher_id = X'") - .push(puzzle_hash) - .push("' OR "); - } - } else if is_valid_address(value, "did:chia:") { - if let Some(puzzle_hash) = puzzle_hash_from_address(value) { - query - .push("dids.launcher_id = X'") - .push(puzzle_hash) - .push("' OR "); - } - } - - if is_valid_height(value) { - query.push("height = ").push(value).push(" OR "); - } - - query - .push("ticker LIKE ") - .push_bind(format!("%{value}%")) - .push(" OR cats.name LIKE ") - .push_bind(format!("%{value}%")) - .push(" OR dids.name LIKE ") - .push_bind(format!("%{value}%")) - .push(" OR nfts.name LIKE ") - .push_bind(format!("%{value}%")) - .push(")"); - } - query.push( - " - ), - paged_heights AS ( - SELECT - height, - COUNT(*) OVER() as total_count - FROM joined_coin_states - ORDER BY height ", - ); - query.push(if asc { "ASC" } else { "DESC" }); - query.push(" LIMIT ? OFFSET ? )"); - - query.push(" - SELECT - paged_heights.total_count, - 'created' AS action_type, - cs.created_height AS height, - created_unixtime AS unixtime, - cs.coin_id, - cs.kind, - amount, - COALESCE (cat_coins.p2_puzzle_hash, nft_coins.p2_puzzle_hash, did_coins.p2_puzzle_hash, puzzle_hash) AS p2_puzzle_hash, - COALESCE (cats.name, nfts.name, dids.name, NULL) AS name, - COALESCE (cats.asset_id, nfts.launcher_id, dids.launcher_id, NULL) AS item_id, - nft_coins.metadata AS nft_metadata, - cats.ticker, - cats.icon AS cat_icon_url, - nft_thumbnails.icon AS nft_icon, - (SELECT COUNT(*) - FROM derivations d - WHERE d.p2_puzzle_hash = COALESCE(cat_coins.p2_puzzle_hash, nft_coins.p2_puzzle_hash, did_coins.p2_puzzle_hash, cs.puzzle_hash)) AS derivation_count - FROM coin_states cs - INNER JOIN paged_heights ON cs.created_height = paged_heights.height - LEFT JOIN cat_coins ON cs.coin_id = cat_coins.coin_id - LEFT JOIN cats ON cat_coins.asset_id = cats.asset_id - LEFT JOIN did_coins ON cs.coin_id = did_coins.coin_id - LEFT JOIN dids ON did_coins.launcher_id = dids.launcher_id - LEFT JOIN nft_coins ON cs.coin_id = nft_coins.coin_id - LEFT JOIN nfts ON nft_coins.launcher_id = nfts.launcher_id - LEFT JOIN nft_thumbnails ON nft_coins.data_hash = nft_thumbnails.hash - - UNION ALL - - SELECT - paged_heights.total_count, - 'spent' AS action_type, - cs.spent_height AS height, - spent_unixtime AS unixtime, - cs.coin_id, - cs.kind, - amount, - COALESCE (cat_coins.p2_puzzle_hash, nft_coins.p2_puzzle_hash, did_coins.p2_puzzle_hash,puzzle_hash) AS p2_puzzle_hash, - COALESCE (cats.name, nfts.name, dids.name, NULL) AS name, - COALESCE (cats.asset_id, nfts.launcher_id, dids.launcher_id, NULL) AS item_id, - nft_coins.metadata AS nft_metadata, - cats.ticker, - cats.icon AS cat_icon_url, - nft_thumbnails.icon AS nft_icon, - (SELECT COUNT(*) - FROM derivations d - WHERE d.p2_puzzle_hash = COALESCE(cat_coins.p2_puzzle_hash, nft_coins.p2_puzzle_hash, did_coins.p2_puzzle_hash, cs.puzzle_hash)) AS derivation_count - FROM coin_states cs - INNER JOIN paged_heights ON cs.spent_height = paged_heights.height - LEFT JOIN cat_coins ON cs.coin_id = cat_coins.coin_id - LEFT JOIN cats ON cat_coins.asset_id = cats.asset_id - LEFT JOIN did_coins ON cs.coin_id = did_coins.coin_id - LEFT JOIN dids ON did_coins.launcher_id = dids.launcher_id - LEFT JOIN nft_coins ON cs.coin_id = nft_coins.coin_id - LEFT JOIN nfts ON nft_coins.launcher_id = nfts.launcher_id - LEFT JOIN nft_thumbnails ON nft_coins.data_hash = nft_thumbnails.hash"); - let built_query = query.build(); - let rows = built_query.bind(limit).bind(offset).fetch_all(conn).await?; - - let Some(first_row) = rows.first() else { - return Ok((vec![], 0)); - }; - - let total: u32 = first_row.try_get("total_count")?; - - Ok((rows, total)) -} - -async fn get_coin_states_by_created_height( - conn: impl SqliteExecutor<'_>, - height: u32, -) -> Result> { - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` INDEXED BY `coin_created` - WHERE `created_height` = ? - ", - height - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn get_coin_states_by_spent_height( - conn: impl SqliteExecutor<'_>, - height: u32, -) -> Result> { - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` INDEXED BY `coin_spent` - WHERE `spent_height` = ? - ", - height - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn full_coin_state( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, -) -> Result> { - let coin_id = coin_id.as_ref(); - - let Some(sql) = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `created_height`, `spent_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` WHERE `coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(sql.into_row()?)) -} - -/// Checks if the provided string is a valid asset ID (64 character hex string) -pub fn is_valid_asset_id(asset_id: &str) -> bool { - asset_id.len() == 64 && asset_id.chars().all(|c| c.is_ascii_hexdigit()) -} - -/// Checks if a given address is valid for the specified prefix -pub fn is_valid_address(address: &str, prefix: &str) -> bool { - if let Ok(decoded) = chia_wallet_sdk::utils::Address::decode(address) { - // Check if the prefix matches and the puzzle hash is valid (32 bytes) - decoded.prefix == prefix && decoded.puzzle_hash.as_ref().len() == 32 - } else { - false - } -} - -/// Extracts puzzle hash from an address -fn puzzle_hash_from_address(address: &str) -> Option { - chia_wallet_sdk::utils::Address::decode(address) - .map(|decoded| hex::encode(decoded.puzzle_hash.as_ref())) - .ok() -} - -/// Checks if a string is a valid block height (non-negative integer) -pub fn is_valid_height(height_str: &str) -> bool { - height_str.parse::().is_ok() -} - -async fn get_are_coins_spendable( - conn: impl SqliteExecutor<'_>, - coin_ids: &[String], -) -> Result { - if coin_ids.is_empty() { - return Ok(false); - } - - let mut query = sqlx::QueryBuilder::new( - " - SELECT COUNT(*) - FROM coin_states - LEFT JOIN transaction_spends ON transaction_spends.coin_id = coin_states.coin_id - WHERE 1=1 - AND created_height IS NOT NULL - AND spent_height IS NULL - AND coin_states.transaction_id IS NULL - AND transaction_spends.coin_id IS NULL - AND coin_states.coin_id IN (", - ); - - let mut separated = query.separated(", "); - for coin_id in coin_ids { - separated.push(format!("X'{coin_id}'")); - } - separated.push_unseparated(")"); - - let count: i64 = query.build().fetch_one(conn).await?.get(0); - - #[allow(clippy::cast_possible_wrap)] - Ok(count == coin_ids.len() as i64) -} diff --git a/crates/sage-database/src/derivations.rs b/crates/sage-database/src/derivations.rs deleted file mode 100644 index 09d80cb4d..000000000 --- a/crates/sage-database/src/derivations.rs +++ /dev/null @@ -1,293 +0,0 @@ -use chia::{bls::PublicKey, protocol::Bytes32}; -use sqlx::{Row, SqliteExecutor}; - -use crate::{ - into_row, to_bytes, to_bytes32, Database, DatabaseTx, DerivationRow, DerivationSql, Result, -}; - -#[derive(Debug, Clone, Copy)] -pub struct SyntheticKeyInfo { - pub index: u32, - pub hardened: bool, -} - -impl Database { - pub async fn derivation_index(&self, hardened: bool) -> Result { - derivation_index(&self.pool, hardened).await - } - - pub async fn derivations( - &self, - hardened: bool, - limit: u32, - offset: u32, - ) -> Result<(Vec, u32)> { - derivations(&self.pool, hardened, limit, offset).await - } - - pub async fn p2_puzzle_hashes(&self) -> Result> { - p2_puzzle_hashes(&self.pool).await - } - - pub async fn synthetic_key(&self, p2_puzzle_hash: Bytes32) -> Result { - synthetic_key(&self.pool, p2_puzzle_hash).await - } - - pub async fn synthetic_key_info( - &self, - synthetic_key: PublicKey, - ) -> Result> { - synthetic_key_info(&self.pool, synthetic_key).await - } - - pub async fn is_p2_puzzle_hash(&self, p2_puzzle_hash: Bytes32) -> Result { - is_p2_puzzle_hash(&self.pool, p2_puzzle_hash).await - } -} - -impl DatabaseTx<'_> { - pub async fn insert_derivation( - &mut self, - p2_puzzle_hash: Bytes32, - index: u32, - hardened: bool, - synthetic_key: PublicKey, - ) -> Result<()> { - insert_derivation( - &mut *self.tx, - p2_puzzle_hash, - index, - hardened, - synthetic_key, - ) - .await - } - - pub async fn derivation_index(&mut self, hardened: bool) -> Result { - derivation_index(&mut *self.tx, hardened).await - } - - pub async fn max_used_derivation_index(&mut self, hardened: bool) -> Result> { - max_used_derivation_index(&mut *self.tx, hardened).await - } - - pub async fn synthetic_key(&mut self, p2_puzzle_hash: Bytes32) -> Result { - synthetic_key(&mut *self.tx, p2_puzzle_hash).await - } - - pub async fn p2_puzzle_hash(&mut self, index: u32, hardened: bool) -> Result { - p2_puzzle_hash(&mut *self.tx, index, hardened).await - } - - pub async fn is_p2_puzzle_hash(&mut self, p2_puzzle_hash: Bytes32) -> Result { - is_p2_puzzle_hash(&mut *self.tx, p2_puzzle_hash).await - } -} - -async fn insert_derivation( - conn: impl SqliteExecutor<'_>, - p2_puzzle_hash: Bytes32, - index: u32, - hardened: bool, - synthetic_key: PublicKey, -) -> Result<()> { - let p2_puzzle_hash = p2_puzzle_hash.as_ref(); - let synthetic_key = synthetic_key.to_bytes(); - let synthetic_key_ref = synthetic_key.as_ref(); - sqlx::query!( - " - INSERT OR IGNORE INTO `derivations` (`p2_puzzle_hash`, `index`, `hardened`, `synthetic_key`) - VALUES (?, ?, ?, ?) - ", - p2_puzzle_hash, - index, - hardened, - synthetic_key_ref - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn derivation_index(conn: impl SqliteExecutor<'_>, hardened: bool) -> Result { - Ok(sqlx::query!( - " - SELECT MAX(`index`) AS `max_index` - FROM `derivations` - WHERE `hardened` = ? - ", - hardened - ) - .fetch_one(conn) - .await? - .max_index - .map_or(0, |index| index + 1) - .try_into()?) -} - -async fn max_used_derivation_index( - conn: impl SqliteExecutor<'_>, - hardened: bool, -) -> Result> { - let row = sqlx::query!( - " - SELECT MAX(`index`) AS `max_index` - FROM `derivations` - WHERE EXISTS ( - SELECT 1 FROM `coin_states` - WHERE `puzzle_hash` = `p2_puzzle_hash` - OR `hint` = `p2_puzzle_hash` - ) - AND `hardened` = ? - ", - hardened - ) - .fetch_one(conn) - .await?; - Ok(row.max_index.map(TryInto::<_>::try_into).transpose()?) -} - -async fn p2_puzzle_hashes(conn: impl SqliteExecutor<'_>) -> Result> { - let rows = sqlx::query!( - " - SELECT `p2_puzzle_hash` - FROM `derivations` - ORDER BY `index` ASC, `hardened` ASC - " - ) - .fetch_all(conn) - .await?; - rows.into_iter() - .map(|row| to_bytes32(&row.p2_puzzle_hash)) - .collect::>() -} - -async fn derivations( - conn: impl SqliteExecutor<'_>, - hardened: bool, - limit: u32, - offset: u32, -) -> Result<(Vec, u32)> { - let mut query = sqlx::QueryBuilder::new( - " - SELECT - `p2_puzzle_hash`, - `index`, - `hardened`, - `synthetic_key`, - COUNT(*) OVER() as total_count - FROM `derivations` - WHERE `hardened` = - ", - ); - - query.push_bind(hardened); - query.push(" ORDER BY `index` ASC"); - query.push(" LIMIT "); - query.push_bind(limit); - query.push(" OFFSET "); - query.push_bind(offset); - - // Build the query and bind the hardened parameter - let sql = query.build(); - let rows = sql.bind(hardened).fetch_all(conn).await?; - - let Some(first_row) = rows.first() else { - return Ok((vec![], 0)); - }; - - let total: u32 = first_row.try_get("total_count")?; - - let mut derivations = Vec::with_capacity(rows.len()); - - for row in rows { - let sql = DerivationSql { - p2_puzzle_hash: row.try_get("p2_puzzle_hash")?, - index: row.try_get("index")?, - hardened: row.try_get("hardened")?, - synthetic_key: row.try_get("synthetic_key")?, - }; - derivations.push(into_row(sql)?); - } - - Ok((derivations, total)) -} - -async fn synthetic_key( - conn: impl SqliteExecutor<'_>, - p2_puzzle_hash: Bytes32, -) -> Result { - let p2_puzzle_hash = p2_puzzle_hash.as_ref(); - let row = sqlx::query!( - " - SELECT `synthetic_key` - FROM `derivations` - WHERE `p2_puzzle_hash` = ? - ", - p2_puzzle_hash - ) - .fetch_one(conn) - .await?; - let bytes = row.synthetic_key.as_slice(); - Ok(PublicKey::from_bytes(&to_bytes(bytes)?)?) -} - -async fn synthetic_key_info( - conn: impl SqliteExecutor<'_>, - synthetic_key: PublicKey, -) -> Result> { - let synthetic_key = synthetic_key.to_bytes(); - let synthetic_key_ref = synthetic_key.as_ref(); - - sqlx::query!( - " - SELECT `index`, `hardened` - FROM `derivations` - WHERE `synthetic_key` = ? - ", - synthetic_key_ref - ) - .fetch_optional(conn) - .await? - .map(|row| { - Ok(SyntheticKeyInfo { - index: row.index.try_into()?, - hardened: row.hardened, - }) - }) - .transpose() -} - -async fn p2_puzzle_hash( - conn: impl SqliteExecutor<'_>, - index: u32, - hardened: bool, -) -> Result { - let row = sqlx::query!( - " - SELECT `p2_puzzle_hash` - FROM `derivations` - WHERE `index` = ? - AND `hardened` = ? - ", - index, - hardened - ) - .fetch_one(conn) - .await?; - to_bytes32(&row.p2_puzzle_hash) -} - -async fn is_p2_puzzle_hash(conn: impl SqliteExecutor<'_>, p2_puzzle_hash: Bytes32) -> Result { - let p2_puzzle_hash = p2_puzzle_hash.as_ref(); - Ok(sqlx::query!( - " - SELECT COUNT(*) AS `count` FROM `derivations` WHERE `p2_puzzle_hash` = ? - ", - p2_puzzle_hash - ) - .fetch_one(conn) - .await? - .count - > 0) -} diff --git a/crates/sage-database/src/lib.rs b/crates/sage-database/src/lib.rs index 7ba161257..cd7be3e59 100644 --- a/crates/sage-database/src/lib.rs +++ b/crates/sage-database/src/lib.rs @@ -1,24 +1,17 @@ -mod blockinfo; -mod coin_states; -mod derivations; -mod offers; -mod peaks; -mod primitives; -mod rows; -mod transactions; +mod maintenance; +mod tables; mod utils; -pub use primitives::*; -pub use rows::*; -pub use transactions::*; +pub use maintenance::*; +pub use tables::*; pub(crate) use utils::*; use std::num::TryFromIntError; -use sqlx::{Sqlite, SqlitePool, Transaction}; +use sqlx::{Sqlite, SqlitePool, Transaction as SqliteTransaction}; use thiserror::Error; -use tracing::{debug, info}; +use tracing::info; #[derive(Debug, Clone)] pub struct Database { @@ -35,7 +28,7 @@ impl Database { Ok(DatabaseTx::new(tx)) } - pub async fn run_rust_migrations(&self) -> Result<()> { + pub async fn run_rust_migrations(&self, ticker: String) -> Result<()> { let mut tx = self.tx().await?; let version = tx.rust_migration_version().await?; @@ -43,30 +36,11 @@ impl Database { info!("The current Sage migration version is {version}"); if version < 1 { - info!("Migrating to version 1 (fixed collection id calculation)"); - - for collection_id in tx.collection_ids().await? { - let collection = tx - .collection(collection_id) - .await? - .expect("collection not found"); - - let new_collection_id = - calculate_collection_id(collection.did_id, &collection.metadata_collection_id); - - if collection_id == new_collection_id { - continue; - } - - debug!("Migrating collection {collection_id} to {new_collection_id}"); - - tx.update_collection_id(collection_id, new_collection_id) - .await?; - - tx.update_nft_collection_ids(collection_id, new_collection_id) - .await?; - } - + let ticker_upper = ticker.to_uppercase(); + info!("Migrating to version 1 - setting chia token ticker to {ticker_upper}"); + sqlx::query!("UPDATE assets SET ticker = ? WHERE id = 0", ticker_upper) + .execute(&mut *tx.tx) + .await?; tx.set_rust_migration_version(1).await?; } @@ -74,30 +48,15 @@ impl Database { Ok(()) } - - /// Count collections, optionally including hidden ones - pub async fn count_collections(&self, include_hidden: bool) -> Result { - let count: i64 = if include_hidden { - sqlx::query_scalar!("SELECT COUNT(*) FROM collections") - .fetch_one(&self.pool) - .await? - } else { - sqlx::query_scalar!("SELECT COUNT(*) FROM collections WHERE visible = 1") - .fetch_one(&self.pool) - .await? - }; - - Ok(count) - } } #[derive(Debug)] pub struct DatabaseTx<'a> { - pub(crate) tx: Transaction<'a, Sqlite>, + pub(crate) tx: SqliteTransaction<'a, Sqlite>, } impl<'a> DatabaseTx<'a> { - pub fn new(tx: Transaction<'a, Sqlite>) -> Self { + pub fn new(tx: SqliteTransaction<'a, Sqlite>) -> Self { Self { tx } } @@ -110,7 +69,7 @@ impl<'a> DatabaseTx<'a> { } pub async fn rust_migration_version(&mut self) -> Result { - let row = sqlx::query_scalar!("SELECT `version` FROM `rust_migrations` LIMIT 1") + let row = sqlx::query_scalar!("SELECT version FROM rust_migrations LIMIT 1") .fetch_one(&mut *self.tx) .await?; @@ -118,7 +77,7 @@ impl<'a> DatabaseTx<'a> { } pub async fn set_rust_migration_version(&mut self, version: i64) -> Result<()> { - sqlx::query!("UPDATE `rust_migrations` SET `version` = ?", version) + sqlx::query!("UPDATE rust_migrations SET version = ?", version) .execute(&mut *self.tx) .await?; @@ -143,9 +102,6 @@ pub enum DatabaseError { #[error("Invalid enum variant")] InvalidEnumVariant, - #[error("Invalid offer status {0}")] - InvalidOfferStatus(i64), - #[error("Invalid address")] InvalidAddress, } diff --git a/crates/sage-database/src/maintenance.rs b/crates/sage-database/src/maintenance.rs new file mode 100644 index 000000000..04e068361 --- /dev/null +++ b/crates/sage-database/src/maintenance.rs @@ -0,0 +1,175 @@ +use crate::{Database, Result}; +use sqlx::Row; +use std::time::Instant; +use tracing::{error, warn}; + +#[derive(Debug, Clone, Copy)] +pub struct MaintenanceStats { + pub vacuum_duration_ms: u64, + pub analyze_duration_ms: u64, + pub wal_checkpoint_duration_ms: u64, + pub total_duration_ms: u64, + pub pages_vacuumed: i64, + pub wal_pages_checkpointed: i64, +} + +#[derive(Debug, Clone, Copy)] +pub struct DatabaseStats { + pub total_pages: i64, + pub free_pages: i64, + pub free_percentage: f64, + pub page_size: i64, + pub database_size_bytes: i64, + pub free_space_bytes: i64, + pub wal_pages: i64, +} + +impl Database { + /// Get current database statistics without performing any maintenance + pub async fn get_database_stats(&self) -> Result { + let free_pages_row = sqlx::query("PRAGMA freelist_count") + .fetch_one(&self.pool) + .await?; + let free_pages: i64 = free_pages_row.try_get(0)?; + + let total_pages_row = sqlx::query("PRAGMA page_count") + .fetch_one(&self.pool) + .await?; + let total_pages: i64 = total_pages_row.try_get(0)?; + + let page_size_row = sqlx::query("PRAGMA page_size") + .fetch_one(&self.pool) + .await?; + let page_size: i64 = page_size_row.try_get(0)?; + + let wal_pages_row = sqlx::query("PRAGMA wal_checkpoint") + .fetch_one(&self.pool) + .await?; + let wal_pages: i64 = wal_pages_row.try_get(1).unwrap_or(0); // log_pages + + #[allow(clippy::cast_precision_loss)] + let free_percentage = if total_pages == 0 { + 0.0 + } else { + (free_pages as f64 / total_pages as f64) * 100.0 + }; + + Ok(DatabaseStats { + total_pages, + free_pages, + free_percentage, + page_size, + database_size_bytes: total_pages * page_size, + free_space_bytes: free_pages * page_size, + wal_pages, + }) + } + + pub async fn perform_sqlite_maintenance(&self, force_vacuum: bool) -> Result { + let total_start = Instant::now(); + let mut stats = MaintenanceStats { + vacuum_duration_ms: 0, + analyze_duration_ms: 0, + wal_checkpoint_duration_ms: 0, + total_duration_ms: 0, + pages_vacuumed: 0, + wal_pages_checkpointed: 0, + }; + + // 1. Update table statistics with ANALYZE + let analyze_start = Instant::now(); + sqlx::query("ANALYZE").execute(&self.pool).await?; + stats.analyze_duration_ms = analyze_start.elapsed().as_millis() as u64; + + let wal_start = Instant::now(); + + // Use TRUNCATE mode to reset WAL file size + match sqlx::query("PRAGMA wal_checkpoint(TRUNCATE)") + .execute(&self.pool) + .await + { + Ok(_) => { + // Get checkpoint statistics + let checkpoint_row = sqlx::query("PRAGMA wal_checkpoint") + .fetch_one(&self.pool) + .await?; + + let _busy: i64 = checkpoint_row.try_get(0).unwrap_or(0); + let _log_pages: i64 = checkpoint_row.try_get(1).unwrap_or(0); + let checkpointed: i64 = checkpoint_row.try_get(2).unwrap_or(0); + + stats.wal_pages_checkpointed = checkpointed; + stats.wal_checkpoint_duration_ms = wal_start.elapsed().as_millis() as u64; + } + Err(e) => { + warn!("WAL checkpoint failed: {}", e); + // Convert to DatabaseError which will be handled by the Result type + return Err(e.into()); + } + } + + // 3. Check if VACUUM is needed (unless forced) + let should_vacuum = if force_vacuum { + true + } else { + let db_stats = self.get_database_stats().await?; + + // VACUUM if more than 10% free space or more than 1000 free pages + db_stats.free_percentage > 10.0 || db_stats.free_pages > 1000 + }; + + // 4. Perform VACUUM if needed + if should_vacuum { + let vacuum_start = Instant::now(); + + // Get page count before vacuum + let pages_before_row = sqlx::query("PRAGMA page_count") + .fetch_one(&self.pool) + .await?; + let pages_before: i64 = pages_before_row.try_get(0)?; + + match sqlx::query("VACUUM").execute(&self.pool).await { + Ok(_) => { + let pages_after_row = sqlx::query("PRAGMA page_count") + .fetch_one(&self.pool) + .await?; + let pages_after: i64 = pages_after_row.try_get(0)?; + + stats.pages_vacuumed = pages_before - pages_after; + stats.vacuum_duration_ms = vacuum_start.elapsed().as_millis() as u64; + } + Err(e) => { + error!("VACUUM failed: {}", e); + // Convert to DatabaseError which will be handled by the Result type + return Err(e.into()); + } + } + } + + // Final optimization - update statistics again if we vacuumed + if should_vacuum { + let final_analyze_start = Instant::now(); + sqlx::query("ANALYZE").execute(&self.pool).await?; + stats.analyze_duration_ms += final_analyze_start.elapsed().as_millis() as u64; + } + + stats.total_duration_ms = total_start.elapsed().as_millis() as u64; + + Ok(stats) + } + + /// Performs a quick maintenance routine suitable for regular automated runs + /// + /// This is a lighter version that only runs ANALYZE and WAL checkpoint, + /// skipping VACUUM unless specifically needed. + pub async fn perform_quick_maintenance(&self) -> Result { + self.perform_sqlite_maintenance(false).await + } + + /// Performs a full maintenance routine including forced VACUUM + /// + /// This is suitable for periodic deep maintenance (e.g., weekly or monthly) + pub async fn perform_full_maintenance(&self) -> Result { + self.perform_sqlite_maintenance(true).await + } +} diff --git a/crates/sage-database/src/offers.rs b/crates/sage-database/src/offers.rs deleted file mode 100644 index b6f99ef83..000000000 --- a/crates/sage-database/src/offers.rs +++ /dev/null @@ -1,338 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use chia::protocol::Bytes32; -use sqlx::SqliteExecutor; - -use crate::{ - into_row, to_bytes32, Database, DatabaseTx, OfferCatRow, OfferCatSql, OfferNftRow, OfferNftSql, - OfferRow, OfferSql, OfferStatus, OfferXchRow, OfferXchSql, Result, -}; - -impl Database { - pub async fn active_offers(&self) -> Result> { - active_offers(&self.pool).await - } - - pub async fn get_offers(&self) -> Result> { - get_offers(&self.pool).await - } - - pub async fn delete_offer(&self, offer_id: Bytes32) -> Result<()> { - delete_offer(&self.pool, offer_id).await - } - - pub async fn offer_xch(&self, offer_id: Bytes32) -> Result> { - offer_xch(&self.pool, offer_id).await - } - - pub async fn offer_nfts(&self, offer_id: Bytes32) -> Result> { - offer_nfts(&self.pool, offer_id).await - } - - pub async fn offer_cats(&self, offer_id: Bytes32) -> Result> { - offer_cats(&self.pool, offer_id).await - } - - pub async fn get_offer(&self, offer_id: Bytes32) -> Result> { - get_offer(&self.pool, offer_id).await - } - - pub async fn update_offer_status(&self, offer_id: Bytes32, status: OfferStatus) -> Result<()> { - update_offer_status(&self.pool, offer_id, status).await - } - - pub async fn coin_offer_id(&self, coin_id: Bytes32) -> Result> { - coin_offer_id(&self.pool, coin_id).await - } -} - -impl DatabaseTx<'_> { - pub async fn insert_offer(&mut self, row: OfferRow) -> Result<()> { - insert_offer(&mut *self.tx, row).await - } - - pub async fn insert_offered_coin(&mut self, offer_id: Bytes32, coin_id: Bytes32) -> Result<()> { - insert_offered_coin(&mut *self.tx, offer_id, coin_id).await - } - - pub async fn insert_offer_xch(&mut self, row: OfferXchRow) -> Result<()> { - insert_offer_xch(&mut *self.tx, row).await - } - - pub async fn insert_offer_nft(&mut self, row: OfferNftRow) -> Result<()> { - insert_offer_nft(&mut *self.tx, row).await - } - - pub async fn insert_offer_cat(&mut self, row: OfferCatRow) -> Result<()> { - insert_offer_cat(&mut *self.tx, row).await - } -} - -async fn insert_offer(conn: impl SqliteExecutor<'_>, row: OfferRow) -> Result<()> { - let offer_id = row.offer_id.as_ref(); - let expiration_timestamp = row.expiration_timestamp.map(|ts| ts.to_be_bytes().to_vec()); - let status = row.status as u8; - - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time is before the UNIX epoch") - .as_secs() - .to_be_bytes(); - let timestamp = timestamp.as_ref(); - - let fee = row.fee.to_be_bytes(); - let fee = fee.as_ref(); - - sqlx::query!( - " - INSERT OR IGNORE INTO `offers` ( - `offer_id`, `encoded_offer`, `expiration_height`, - `expiration_timestamp`, `fee`, `status`, `inserted_timestamp` - ) - VALUES (?, ?, ?, ?, ?, ?, ?) - ", - offer_id, - row.encoded_offer, - row.expiration_height, - expiration_timestamp, - fee, - status, - timestamp - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn insert_offered_coin( - conn: impl SqliteExecutor<'_>, - offer_id: Bytes32, - coin_id: Bytes32, -) -> Result<()> { - let offer_id = offer_id.as_ref(); - let coin_id = coin_id.as_ref(); - - sqlx::query!( - "INSERT OR IGNORE INTO `offered_coins` (`offer_id`, `coin_id`) VALUES (?, ?)", - offer_id, - coin_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn insert_offer_xch(conn: impl SqliteExecutor<'_>, row: OfferXchRow) -> Result<()> { - let offer_id = row.offer_id.as_ref(); - let amount = row.amount.to_be_bytes(); - let amount = amount.as_ref(); - let royalty = row.royalty.to_be_bytes(); - let royalty = royalty.as_ref(); - - sqlx::query!( - " - INSERT INTO `offer_xch` ( - `offer_id`, `requested`, `amount`, `royalty` - ) - VALUES (?, ?, ?, ?) - ", - offer_id, - row.requested, - amount, - royalty - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn insert_offer_nft(conn: impl SqliteExecutor<'_>, row: OfferNftRow) -> Result<()> { - let offer_id = row.offer_id.as_ref(); - let launcher_id = row.launcher_id.as_ref(); - let royalty_puzzle_hash = row.royalty_puzzle_hash.as_ref(); - - sqlx::query!( - " - INSERT INTO `offer_nfts` ( - `offer_id`, `requested`, `launcher_id`, - `royalty_puzzle_hash`, `royalty_ten_thousandths`, - `name`, `thumbnail`, `thumbnail_mime_type` - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ", - offer_id, - row.requested, - launcher_id, - royalty_puzzle_hash, - row.royalty_ten_thousandths, - row.name, - row.thumbnail, - row.thumbnail_mime_type - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn insert_offer_cat(conn: impl SqliteExecutor<'_>, row: OfferCatRow) -> Result<()> { - let offer_id = row.offer_id.as_ref(); - let asset_id = row.asset_id.as_ref(); - let amount = row.amount.to_be_bytes(); - let amount = amount.as_ref(); - let royalty = row.royalty.to_be_bytes(); - let royalty = royalty.as_ref(); - - sqlx::query!( - " - INSERT INTO `offer_cats` ( - `offer_id`, `requested`, `asset_id`, - `amount`, `royalty`, `name`, `ticker`, `icon` - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ", - offer_id, - row.requested, - asset_id, - amount, - royalty, - row.name, - row.ticker, - row.icon - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn active_offers(conn: impl SqliteExecutor<'_>) -> Result> { - sqlx::query_as!( - OfferSql, - "SELECT * FROM `offers` INDEXED BY `offer_status` WHERE `status` = 0" - ) - .fetch_all(conn) - .await? - .into_iter() - .map(into_row) - .collect() -} - -async fn get_offers(conn: impl SqliteExecutor<'_>) -> Result> { - sqlx::query_as!( - OfferSql, - "SELECT * FROM `offers` INDEXED BY `offer_timestamp` ORDER BY `inserted_timestamp` DESC" - ) - .fetch_all(conn) - .await? - .into_iter() - .map(into_row) - .collect() -} - -async fn get_offer(conn: impl SqliteExecutor<'_>, offer_id: Bytes32) -> Result> { - let offer_id = offer_id.as_ref(); - - sqlx::query_as!( - OfferSql, - "SELECT * FROM `offers` WHERE `offer_id` = ?", - offer_id - ) - .fetch_optional(conn) - .await? - .map(into_row) - .transpose() -} - -async fn delete_offer(conn: impl SqliteExecutor<'_>, offer_id: Bytes32) -> Result<()> { - let offer_id = offer_id.as_ref(); - - sqlx::query!("DELETE FROM `offers` WHERE `offer_id` = ?", offer_id) - .execute(conn) - .await?; - - Ok(()) -} - -async fn offer_xch(conn: impl SqliteExecutor<'_>, offer_id: Bytes32) -> Result> { - let offer_id = offer_id.as_ref(); - - sqlx::query_as!( - OfferXchSql, - "SELECT * FROM `offer_xch` WHERE `offer_id` = ?", - offer_id - ) - .fetch_all(conn) - .await? - .into_iter() - .map(into_row) - .collect() -} - -async fn offer_nfts(conn: impl SqliteExecutor<'_>, offer_id: Bytes32) -> Result> { - let offer_id = offer_id.as_ref(); - - sqlx::query_as!( - OfferNftSql, - "SELECT * FROM `offer_nfts` WHERE `offer_id` = ?", - offer_id - ) - .fetch_all(conn) - .await? - .into_iter() - .map(into_row) - .collect() -} - -async fn offer_cats(conn: impl SqliteExecutor<'_>, offer_id: Bytes32) -> Result> { - let offer_id = offer_id.as_ref(); - - sqlx::query_as!( - OfferCatSql, - "SELECT * FROM `offer_cats` WHERE `offer_id` = ?", - offer_id - ) - .fetch_all(conn) - .await? - .into_iter() - .map(into_row) - .collect() -} - -async fn update_offer_status( - conn: impl SqliteExecutor<'_>, - offer_id: Bytes32, - status: OfferStatus, -) -> Result<()> { - let offer_id = offer_id.as_ref(); - let status = status as u8; - - sqlx::query!( - "UPDATE `offers` SET `status` = ? WHERE `offer_id` = ?", - status, - offer_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn coin_offer_id(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result> { - let coin_id = coin_id.as_ref(); - - sqlx::query!( - " - SELECT `offered_coins`.`offer_id` FROM `offered_coins` - INNER JOIN `offers` ON `offered_coins`.`offer_id` = `offers`.`offer_id` - WHERE `coin_id` = ? AND `offers`.`status` = 0 - ", - coin_id - ) - .fetch_optional(conn) - .await? - .map(|row| to_bytes32(&row.offer_id)) - .transpose() -} diff --git a/crates/sage-database/src/peaks.rs b/crates/sage-database/src/peaks.rs deleted file mode 100644 index b415182f4..000000000 --- a/crates/sage-database/src/peaks.rs +++ /dev/null @@ -1,54 +0,0 @@ -use chia::protocol::Bytes32; -use sqlx::SqliteExecutor; - -use crate::{to_bytes32, Database, DatabaseTx, Result}; - -impl Database { - pub async fn insert_peak(&self, height: u32, header_hash: Bytes32) -> Result<()> { - insert_peak(&self.pool, height, header_hash).await - } - - pub async fn latest_peak(&self) -> Result> { - latest_peak(&self.pool).await - } -} - -impl DatabaseTx<'_> { - pub async fn latest_peak(&mut self) -> Result> { - latest_peak(&mut *self.tx).await - } -} - -async fn insert_peak( - conn: impl SqliteExecutor<'_>, - height: u32, - header_hash: Bytes32, -) -> Result<()> { - let header_hash = header_hash.as_ref(); - sqlx::query!( - " - REPLACE INTO `peaks` (`height`, `header_hash`) - VALUES (?, ?) - ", - height, - header_hash - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn latest_peak(conn: impl SqliteExecutor<'_>) -> Result> { - sqlx::query!( - " - SELECT `height`, `header_hash` - FROM `peaks` - ORDER BY `height` DESC - LIMIT 1 - " - ) - .fetch_optional(conn) - .await? - .map(|row| Ok((row.height.try_into()?, to_bytes32(&row.header_hash)?))) - .transpose() -} diff --git a/crates/sage-database/src/primitives.rs b/crates/sage-database/src/primitives.rs deleted file mode 100644 index ca73e8b0d..000000000 --- a/crates/sage-database/src/primitives.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod cats; -mod dids; -mod nfts; -mod xch; - -pub use cats::*; -pub use nfts::*; diff --git a/crates/sage-database/src/primitives/cats.rs b/crates/sage-database/src/primitives/cats.rs deleted file mode 100644 index a31efc4f5..000000000 --- a/crates/sage-database/src/primitives/cats.rs +++ /dev/null @@ -1,555 +0,0 @@ -use chia::{protocol::Bytes32, puzzles::LineageProof}; -use chia_wallet_sdk::driver::Cat; -use sqlx::{Row, SqliteExecutor}; - -use crate::{ - into_row, to_bytes, to_bytes32, CatCoinRow, CatCoinSql, CatRow, CatSql, CoinStateRow, - CoinStateSql, Database, DatabaseTx, EnhancedCoinStateRow, FullCatCoinSql, Result, -}; - -#[derive(Debug, Clone, Copy)] -pub enum CoinSortMode { - CoinId, - Amount, - CreatedHeight, - SpentHeight, -} - -impl Default for CoinSortMode { - fn default() -> Self { - Self::CreatedHeight - } -} - -impl Database { - pub async fn insert_cat(&self, row: CatRow) -> Result<()> { - insert_cat(&self.pool, row).await - } - - pub async fn update_cat(&self, row: CatRow) -> Result<()> { - update_cat(&self.pool, row).await - } - - pub async fn cats_by_name(&self) -> Result> { - cats_by_name(&self.pool).await - } - - pub async fn cat(&self, asset_id: Bytes32) -> Result> { - cat(&self.pool, asset_id).await - } - - pub async fn unfetched_cat(&self) -> Result> { - unfetched_cat(&self.pool).await - } - - pub async fn spendable_cat_coins(&self, asset_id: Bytes32) -> Result> { - spendable_cat_coins(&self.pool, asset_id).await - } - - pub async fn cat_balance(&self, asset_id: Bytes32) -> Result { - cat_balance(&self.pool, asset_id).await - } - - pub async fn cat_coin(&self, coin_id: Bytes32) -> Result> { - cat_coin(&self.pool, coin_id).await - } - - pub async fn refetch_cat(&self, asset_id: Bytes32) -> Result<()> { - refetch_cat(&self.pool, asset_id).await - } - - pub async fn spendable_cat_coin_count(&self, asset_id: Bytes32) -> Result { - spendable_cat_coin_count(&self.pool, asset_id).await - } - - pub async fn coin_states_by_ids( - &self, - coin_ids: &[String], - ) -> Result> { - coin_states_by_ids(&self.pool, coin_ids).await - } - - pub async fn cat_coin_states( - &self, - asset_id: Bytes32, - limit: u32, - offset: u32, - sort_mode: CoinSortMode, - ascending: bool, - include_spent_coins: bool, - ) -> Result<(Vec, u32)> { - cat_coin_states( - &self.pool, - asset_id, - limit, - offset, - sort_mode, - ascending, - include_spent_coins, - ) - .await - } - - pub async fn created_unspent_cat_coin_states( - &self, - asset_id: Bytes32, - limit: u32, - offset: u32, - ) -> Result> { - created_unspent_cat_coin_states(&self.pool, asset_id, limit, offset).await - } -} - -impl DatabaseTx<'_> { - pub async fn insert_cat(&mut self, row: CatRow) -> Result<()> { - insert_cat(&mut *self.tx, row).await - } - - pub async fn insert_cat_coin( - &mut self, - coin_id: Bytes32, - lineage_proof: LineageProof, - p2_puzzle_hash: Bytes32, - asset_id: Bytes32, - ) -> Result<()> { - insert_cat_coin( - &mut *self.tx, - coin_id, - lineage_proof, - p2_puzzle_hash, - asset_id, - ) - .await - } -} - -async fn insert_cat(conn: impl SqliteExecutor<'_>, row: CatRow) -> Result<()> { - let asset_id = row.asset_id.as_ref(); - - sqlx::query!( - " - INSERT OR IGNORE INTO `cats` ( - `asset_id`, - `name`, - `ticker`, - `description`, - `icon`, - `visible`, - `fetched` - ) VALUES (?, ?, ?, ?, ?, ?, ?) - ", - asset_id, - row.name, - row.ticker, - row.description, - row.icon, - row.visible, - row.fetched - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn update_cat(conn: impl SqliteExecutor<'_>, row: CatRow) -> Result<()> { - let asset_id = row.asset_id.as_ref(); - - sqlx::query!( - " - REPLACE INTO `cats` ( - `asset_id`, - `name`, - `ticker`, - `description`, - `icon`, - `visible`, - `fetched` - ) VALUES (?, ?, ?, ?, ?, ?, ?) - ", - asset_id, - row.name, - row.ticker, - row.description, - row.icon, - row.visible, - row.fetched - ) - .execute(conn) - .await?; - Ok(()) -} - -async fn cats_by_name(conn: impl SqliteExecutor<'_>) -> Result> { - let rows = sqlx::query_as!( - CatSql, - " - SELECT `asset_id`, `name`, `ticker`, `description`, `icon`, `visible`, `fetched` - FROM `cats` INDEXED BY `cat_name` - ORDER BY `visible` DESC, `is_named` DESC, `name` ASC, `asset_id` ASC - " - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn cat(conn: impl SqliteExecutor<'_>, asset_id: Bytes32) -> Result> { - let asset_id = asset_id.as_ref(); - - let row = sqlx::query_as!( - CatSql, - " - SELECT `asset_id`, `name`, `ticker`, `description`, `icon`, `visible`, `fetched` - FROM `cats` - WHERE `asset_id` = ? - ", - asset_id - ) - .fetch_optional(conn) - .await?; - - row.map(into_row).transpose() -} - -async fn unfetched_cat(conn: impl SqliteExecutor<'_>) -> Result> { - let rows = sqlx::query!( - " - SELECT `asset_id` FROM `cats` WHERE `fetched` = 0 - LIMIT 1 - " - ) - .fetch_optional(conn) - .await?; - rows.map(|row| to_bytes32(&row.asset_id)).transpose() -} - -async fn insert_cat_coin( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, - lineage_proof: LineageProof, - p2_puzzle_hash: Bytes32, - asset_id: Bytes32, -) -> Result<()> { - let coin_id = coin_id.as_ref(); - let parent_parent_coin_id = lineage_proof.parent_parent_coin_info.as_ref(); - let parent_inner_puzzle_hash = lineage_proof.parent_inner_puzzle_hash.as_ref(); - let parent_amount = lineage_proof.parent_amount.to_be_bytes(); - let parent_amount = parent_amount.as_ref(); - let p2_puzzle_hash = p2_puzzle_hash.as_ref(); - let asset_id = asset_id.as_ref(); - - sqlx::query!( - " - INSERT OR IGNORE INTO `cat_coins` ( - `coin_id`, - `parent_parent_coin_id`, - `parent_inner_puzzle_hash`, - `parent_amount`, - `p2_puzzle_hash`, - `asset_id` - ) - VALUES (?, ?, ?, ?, ?, ?) - ", - coin_id, - parent_parent_coin_id, - parent_inner_puzzle_hash, - parent_amount, - p2_puzzle_hash, - asset_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn spendable_cat_coins( - conn: impl SqliteExecutor<'_>, - asset_id: Bytes32, -) -> Result> { - let asset_id = asset_id.as_ref(); - - let rows = sqlx::query_as!( - CatCoinSql, - " - SELECT - cs.`parent_coin_id`, cs.`puzzle_hash`, cs.`amount`, `p2_puzzle_hash`, - `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount` - FROM `cat_coins` INDEXED BY `cat_asset_id` - INNER JOIN `coin_states` AS cs ON `cat_coins`.`coin_id` = cs.`coin_id` - LEFT JOIN `transaction_spends` ON cs.`coin_id` = `transaction_spends`.`coin_id` - LEFT JOIN `offered_coins` ON cs.`coin_id` = `offered_coins`.`coin_id` - LEFT JOIN `offers` ON `offered_coins`.`offer_id` = `offers`.`offer_id` - WHERE `cat_coins`.`asset_id` = ? - AND cs.`spent_height` IS NULL - AND `transaction_spends`.`coin_id` IS NULL - AND (`offered_coins`.`coin_id` IS NULL OR `offers`.`status` > 0) - AND cs.`transaction_id` IS NULL - ", - asset_id - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn cat_balance(conn: impl SqliteExecutor<'_>, asset_id: Bytes32) -> Result { - let asset_id = asset_id.as_ref(); - - let row = sqlx::query!( - " - SELECT `coin_states`.`amount` FROM `coin_states` INDEXED BY `coin_spent` - INNER JOIN `cat_coins` ON `coin_states`.`coin_id` = `cat_coins`.`coin_id` - LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id` - WHERE `coin_states`.`spent_height` IS NULL - AND `cat_coins`.`asset_id` = ? - AND `transaction_spends`.`coin_id` IS NULL - ", - asset_id - ) - .fetch_all(conn) - .await?; - - row.iter() - .map(|row| Ok(u64::from_be_bytes(to_bytes(&row.amount)?) as u128)) - .sum::>() -} - -async fn spendable_cat_coin_count(conn: impl SqliteExecutor<'_>, asset_id: Bytes32) -> Result { - let asset_id = asset_id.as_ref(); - - let row = sqlx::query!( - " - SELECT COUNT(*) as count - FROM coin_states - INNER JOIN `cat_coins` ON `coin_states`.`coin_id` = `cat_coins`.`coin_id` - LEFT JOIN transaction_spends ON transaction_spends.coin_id = coin_states.coin_id - WHERE 1=1 - AND `cat_coins`.`asset_id` = ? - AND created_height IS NOT NULL - AND spent_height IS NULL - AND coin_states.transaction_id IS NULL - AND transaction_spends.coin_id IS NULL - ", - asset_id - ) - .fetch_one(conn) - .await?; - - Ok(row.count.try_into()?) -} - -async fn coin_states_by_ids( - conn: impl SqliteExecutor<'_>, - coin_ids: &[String], -) -> Result> { - let mut query = sqlx::QueryBuilder::new( - " - SELECT `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`, `spent_height`, `created_height`, - `coin_states`.`transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`, - `offered_coins`.offer_id, `transaction_spends`.transaction_id as spend_transaction_id - FROM `coin_states` - LEFT JOIN `offered_coins` ON `coin_states`.coin_id = `offered_coins`.coin_id - LEFT JOIN `transaction_spends` ON `coin_states`.coin_id = `transaction_spends`.coin_id - WHERE 1=1 - AND coin_states.coin_id IN (", - ); - - let mut separated = query.separated(", "); - for coin_id in coin_ids { - separated.push(format!("X'{coin_id}'")); - } - separated.push_unseparated(")"); - let rows = query.build().fetch_all(conn).await?; - - if rows.is_empty() { - return Ok(vec![]); - } - - let mut coin_states = Vec::with_capacity(rows.len()); - for row in rows { - let sql = CoinStateSql { - parent_coin_id: row.try_get("parent_coin_id")?, - puzzle_hash: row.try_get("puzzle_hash")?, - amount: row.try_get("amount")?, - spent_height: row.try_get("spent_height")?, - created_height: row.try_get("created_height")?, - transaction_id: row.try_get("transaction_id")?, - kind: row.try_get("kind")?, - created_unixtime: row.try_get("created_unixtime")?, - spent_unixtime: row.try_get("spent_unixtime")?, - }; - - let coin_state_row = into_row(sql)?; - - let mut enhanced_row = EnhancedCoinStateRow::from(coin_state_row); - - enhanced_row.offer_id = row.try_get("offer_id").ok(); - enhanced_row.spend_transaction_id = row.try_get("spend_transaction_id").ok(); - - coin_states.push(enhanced_row); - } - - Ok(coin_states) -} - -async fn cat_coin_states( - conn: impl SqliteExecutor<'_> + Clone, - asset_id: Bytes32, - limit: u32, - offset: u32, - sort_mode: CoinSortMode, - ascending: bool, - include_spent_coins: bool, -) -> Result<(Vec, u32)> { - let asset_id = asset_id.as_ref(); - - let mut query = sqlx::QueryBuilder::new( - " - SELECT `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`, `spent_height`, `created_height`, - `coin_states`.`transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`, - `offered_coins`.offer_id, `transaction_spends`.transaction_id as spend_transaction_id, - COUNT(*) OVER() as total_count - FROM `cat_coins` INDEXED BY `cat_asset_id` - INNER JOIN `coin_states` ON `coin_states`.coin_id = `cat_coins`.coin_id - LEFT JOIN `offered_coins` ON `coin_states`.coin_id = `offered_coins`.coin_id - LEFT JOIN `transaction_spends` ON `coin_states`.coin_id = `transaction_spends`.coin_id - WHERE `asset_id` = ", - ); - query.push_bind(asset_id); - - if !include_spent_coins { - query.push(" AND `spent_height` IS NULL"); - } - - query.push(" ORDER BY "); - - match sort_mode { - CoinSortMode::CoinId => { - query.push("`coin_states`.`coin_id`"); - } - CoinSortMode::Amount => { - query.push("`coin_states`.`amount`"); - } - CoinSortMode::CreatedHeight => { - query.push("`coin_states`.`created_height`"); - } - CoinSortMode::SpentHeight => { - query.push("`coin_states`.`spent_height`"); - } - } - - if ascending { - query.push(" ASC"); - } else { - query.push(" DESC"); - } - - query.push(" LIMIT "); - query.push_bind(limit); - query.push(" OFFSET "); - query.push_bind(offset); - - let rows = query.build().fetch_all(conn).await?; - - let Some(first_row) = rows.first() else { - return Ok((vec![], 0)); - }; - - let total: u32 = first_row.try_get("total_count")?; - - let mut coin_states = Vec::with_capacity(rows.len()); - - for row in rows { - let sql = CoinStateSql { - parent_coin_id: row.try_get("parent_coin_id")?, - puzzle_hash: row.try_get("puzzle_hash")?, - amount: row.try_get("amount")?, - spent_height: row.try_get("spent_height")?, - created_height: row.try_get("created_height")?, - transaction_id: row.try_get("transaction_id")?, - kind: row.try_get("kind")?, - created_unixtime: row.try_get("created_unixtime")?, - spent_unixtime: row.try_get("spent_unixtime")?, - }; - - let coin_state_row = into_row(sql)?; - - let mut enhanced_row = EnhancedCoinStateRow::from(coin_state_row); - - enhanced_row.offer_id = row.try_get("offer_id").ok(); - enhanced_row.spend_transaction_id = row.try_get("spend_transaction_id").ok(); - - coin_states.push(enhanced_row); - } - - Ok((coin_states, total)) -} - -async fn created_unspent_cat_coin_states( - conn: impl SqliteExecutor<'_>, - asset_id: Bytes32, - limit: u32, - offset: u32, -) -> Result> { - let asset_id = asset_id.as_ref(); - - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` - INNER JOIN `cat_coins` ON `coin_states`.coin_id = `cat_coins`.coin_id - WHERE `asset_id` = ? - AND `spent_height` IS NULL - AND `created_height` IS NOT NULL - ORDER BY `created_height`, `coin_states`.`coin_id` LIMIT ? OFFSET ? - ", - asset_id, - limit, - offset - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn cat_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result> { - let coin_id = coin_id.as_ref(); - - let row = sqlx::query_as!( - FullCatCoinSql, - " - SELECT - `parent_coin_id`, `puzzle_hash`, `amount`, - `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`, - `asset_id`, `p2_puzzle_hash` - FROM `coin_states` - INNER JOIN `cat_coins` ON `coin_states`.`coin_id` = `cat_coins`.`coin_id` - WHERE `coin_states`.`coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await?; - - row.map(into_row).transpose() -} - -async fn refetch_cat(conn: impl SqliteExecutor<'_>, asset_id: Bytes32) -> Result<()> { - let asset_id = asset_id.as_ref(); - - sqlx::query!( - " - UPDATE `cats` SET `fetched` = 0 WHERE `asset_id` = ? - ", - asset_id - ) - .execute(conn) - .await?; - - Ok(()) -} diff --git a/crates/sage-database/src/primitives/dids.rs b/crates/sage-database/src/primitives/dids.rs deleted file mode 100644 index bbedb4286..000000000 --- a/crates/sage-database/src/primitives/dids.rs +++ /dev/null @@ -1,480 +0,0 @@ -use chia::{ - protocol::{Bytes32, Program}, - puzzles::LineageProof, -}; -use chia_wallet_sdk::driver::{Did, DidInfo}; -use sqlx::SqliteExecutor; - -use crate::{ - into_row, CoinStateRow, CoinStateSql, Database, DatabaseTx, DidCoinInfo, DidCoinInfoSql, - DidRow, DidSql, FullDidCoinSql, IntoRow, Result, -}; - -impl Database { - pub async fn insert_did(&self, row: DidRow) -> Result<()> { - insert_did(&self.pool, row).await - } - - pub async fn dids_by_name(&self) -> Result> { - dids_by_name(&self.pool).await - } - - pub async fn did_row(&self, launcher_id: Bytes32) -> Result> { - did_row(&self.pool, launcher_id).await - } - - pub async fn did_coin_info(&self, coin_id: Bytes32) -> Result> { - did_coin_info(&self.pool, coin_id).await - } - - pub async fn spendable_did(&self, did_id: Bytes32) -> Result>> { - spendable_did(&self.pool, did_id).await - } - - pub async fn did_name(&self, launcher_id: Bytes32) -> Result> { - did_name(&self.pool, launcher_id).await - } - - pub async fn set_future_did_name(&self, launcher_id: Bytes32, name: String) -> Result<()> { - set_future_did_name(&self.pool, launcher_id, name).await - } - - pub async fn created_unspent_did_coin_states( - &self, - limit: u32, - offset: u32, - ) -> Result> { - created_unspent_did_coin_states(&self.pool, limit, offset).await - } - - pub async fn created_unspent_did_coin_state( - &self, - launcher_id: Bytes32, - ) -> Result> { - created_unspent_did_coin_state(&self.pool, launcher_id).await - } - - pub async fn did_by_coin_id(&self, coin_id: Bytes32) -> Result>> { - did_by_coin_id(&self.pool, coin_id).await - } -} - -impl DatabaseTx<'_> { - pub async fn insert_did(&mut self, row: DidRow) -> Result<()> { - insert_did(&mut *self.tx, row).await - } - - pub async fn insert_did_coin( - &mut self, - coin_id: Bytes32, - lineage_proof: LineageProof, - did_info: DidInfo, - ) -> Result<()> { - insert_did_coin(&mut *self.tx, coin_id, lineage_proof, did_info).await - } - - pub async fn delete_future_did_name(&mut self, launcher_id: Bytes32) -> Result<()> { - delete_future_did_name(&mut *self.tx, launcher_id).await - } - - pub async fn get_future_did_name(&mut self, launcher_id: Bytes32) -> Result> { - get_future_did_name(&mut *self.tx, launcher_id).await - } - - pub async fn did_row(&mut self, launcher_id: Bytes32) -> Result> { - did_row(&mut *self.tx, launcher_id).await - } - - pub async fn did_row_by_coin(&mut self, coin_id: Bytes32) -> Result> { - did_row_by_coin(&mut *self.tx, coin_id).await - } - - pub async fn set_did_not_owned(&mut self, coin_id: Bytes32) -> Result<()> { - set_did_not_owned(&mut *self.tx, coin_id).await - } - - pub async fn set_did_created_height( - &mut self, - coin_id: Bytes32, - height: Option, - ) -> Result<()> { - set_did_created_height(&mut *self.tx, coin_id, height).await - } -} - -async fn insert_did(conn: impl SqliteExecutor<'_>, row: DidRow) -> Result<()> { - let launcher_id = row.launcher_id.as_ref(); - let coin_id = row.coin_id.as_ref(); - - sqlx::query!( - " - REPLACE INTO `dids` ( - `launcher_id`, - `coin_id`, - `name`, - `is_owned`, - `visible`, - `created_height` - ) - VALUES (?, ?, ?, ?, ?, ?) - ", - launcher_id, - coin_id, - row.name, - row.is_owned, - row.visible, - row.created_height - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn insert_did_coin( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, - lineage_proof: LineageProof, - did_info: DidInfo, -) -> Result<()> { - let coin_id = coin_id.as_ref(); - let parent_parent_coin_id = lineage_proof.parent_parent_coin_info.as_ref(); - let parent_inner_puzzle_hash = lineage_proof.parent_inner_puzzle_hash.as_ref(); - let parent_amount = lineage_proof.parent_amount.to_be_bytes(); - let parent_amount = parent_amount.as_ref(); - let launcher_id = did_info.launcher_id.as_ref(); - let recovery_list_hash = did_info.recovery_list_hash.as_deref(); - let num_verifications_required = did_info.num_verifications_required.to_be_bytes(); - let num_verifications_required = num_verifications_required.as_ref(); - let metadata = did_info.metadata.as_ref(); - let p2_puzzle_hash = did_info.p2_puzzle_hash.as_ref(); - - sqlx::query!( - " - INSERT OR IGNORE INTO `did_coins` ( - `coin_id`, - `parent_parent_coin_id`, - `parent_inner_puzzle_hash`, - `parent_amount`, - `launcher_id`, - `recovery_list_hash`, - `num_verifications_required`, - `metadata`, - `p2_puzzle_hash` - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ", - coin_id, - parent_parent_coin_id, - parent_inner_puzzle_hash, - parent_amount, - launcher_id, - recovery_list_hash, - num_verifications_required, - metadata, - p2_puzzle_hash - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn dids_by_name(conn: impl SqliteExecutor<'_>) -> Result> { - sqlx::query_as!( - DidSql, - " - SELECT `launcher_id`, `coin_id`, `name`, `is_owned`, `visible`, `created_height` - FROM `dids` INDEXED BY `did_name` - WHERE `is_owned` = 1 - ORDER BY `visible` DESC, `is_pending` DESC, `is_named` DESC, `name` ASC, `launcher_id` ASC - " - ) - .fetch_all(conn) - .await? - .into_iter() - .map(into_row) - .collect() -} - -async fn did_row(conn: impl SqliteExecutor<'_>, launcher_id: Bytes32) -> Result> { - let launcher_id = launcher_id.as_ref(); - - sqlx::query_as!( - DidSql, - " - SELECT `launcher_id`, `coin_id`, `name`, `is_owned`, `visible`, `created_height` - FROM `dids` - WHERE `launcher_id` = ? - ", - launcher_id - ) - .fetch_optional(conn) - .await? - .map(into_row) - .transpose() -} - -async fn did_row_by_coin( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, -) -> Result> { - let coin_id = coin_id.as_ref(); - - sqlx::query_as!( - DidSql, - " - SELECT `launcher_id`, `coin_id`, `name`, `is_owned`, `visible`, `created_height` - FROM `dids` - WHERE `coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - .map(into_row) - .transpose() -} - -async fn set_did_not_owned(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result<()> { - let coin_id = coin_id.as_ref(); - - sqlx::query!( - "UPDATE `dids` SET `is_owned` = 0 WHERE `coin_id` = ?", - coin_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn set_did_created_height( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, - height: Option, -) -> Result<()> { - let coin_id = coin_id.as_ref(); - - sqlx::query!( - "UPDATE `dids` SET `created_height` = ? WHERE `coin_id` = ?", - height, - coin_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn did_coin_info( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, -) -> Result> { - let coin_id = coin_id.as_ref(); - - let Some(sql) = sqlx::query_as!( - DidCoinInfoSql, - " - SELECT - `did_coins`.`coin_id`, `amount`, `p2_puzzle_hash`, - `recovery_list_hash`, `created_height`, `transaction_id` - FROM `did_coins` - INNER JOIN `coin_states` ON `coin_states`.coin_id = `did_coins`.coin_id - WHERE `did_coins`.`coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(sql.into_row()?)) -} - -async fn spendable_did( - conn: impl SqliteExecutor<'_>, - did_id: Bytes32, -) -> Result>> { - let did_id = did_id.as_ref(); - - let Some(sql) = sqlx::query_as!( - FullDidCoinSql, - " - SELECT - cs.parent_coin_id, cs.puzzle_hash, cs.amount, - did.parent_parent_coin_id, did.parent_inner_puzzle_hash, did.parent_amount, - did.launcher_id, did.recovery_list_hash, did.num_verifications_required, - did.metadata, did.p2_puzzle_hash - FROM `coin_states` AS cs - INNER JOIN `did_coins` AS did ON cs.coin_id = did.coin_id - LEFT JOIN `transaction_spends` ON cs.coin_id = transaction_spends.coin_id - WHERE did.launcher_id = ? - AND cs.spent_height IS NULL - AND cs.created_height IS NOT NULL - AND cs.transaction_id IS NULL - AND transaction_spends.transaction_id IS NULL - ", - did_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(sql.into_row()?)) -} - -async fn did_name(conn: impl SqliteExecutor<'_>, launcher_id: Bytes32) -> Result> { - let launcher_id = launcher_id.as_ref(); - - let Some(row) = sqlx::query!( - " - SELECT `name` - FROM `dids` - WHERE `launcher_id` = ? - ", - launcher_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(row.name) -} - -async fn set_future_did_name( - conn: impl SqliteExecutor<'_>, - launcher_id: Bytes32, - name: String, -) -> Result<()> { - let launcher_id = launcher_id.as_ref(); - - sqlx::query!( - " - REPLACE INTO `future_did_names` (`launcher_id`, `name`) - VALUES (?, ?) - ", - launcher_id, - name - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn get_future_did_name( - conn: impl SqliteExecutor<'_>, - launcher_id: Bytes32, -) -> Result> { - let launcher_id = launcher_id.as_ref(); - - Ok(sqlx::query!( - " - SELECT `name` FROM `future_did_names` - WHERE `launcher_id` = ? - ", - launcher_id - ) - .fetch_optional(conn) - .await? - .map(|row| row.name)) -} - -async fn delete_future_did_name(conn: impl SqliteExecutor<'_>, launcher_id: Bytes32) -> Result<()> { - let launcher_id = launcher_id.as_ref(); - - sqlx::query!( - " - DELETE FROM `future_did_names` WHERE `launcher_id` = ? - ", - launcher_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn created_unspent_did_coin_states( - conn: impl SqliteExecutor<'_>, - limit: u32, - offset: u32, -) -> Result> { - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` - INNER JOIN `did_coins` ON `coin_states`.coin_id = `did_coins`.coin_id - WHERE `spent_height` IS NULL - AND `created_height` IS NOT NULL - ORDER BY `created_height`, `coin_states`.`coin_id` LIMIT ? OFFSET ? - ", - limit, - offset - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn created_unspent_did_coin_state( - conn: impl SqliteExecutor<'_>, - launcher_id: Bytes32, -) -> Result> { - let launcher_id = launcher_id.as_ref(); - - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `did_coins` - INNER JOIN `coin_states` ON `coin_states`.coin_id = `did_coins`.coin_id - WHERE `launcher_id` = ? - AND `spent_height` IS NULL - AND `created_height` IS NOT NULL - ", - launcher_id, - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn did_by_coin_id( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, -) -> Result>> { - let coin_id = coin_id.as_ref(); - - let Some(sql) = sqlx::query_as!( - FullDidCoinSql, - " - SELECT - cs.parent_coin_id, cs.puzzle_hash, cs.amount, - did.parent_parent_coin_id, did.parent_inner_puzzle_hash, did.parent_amount, - did.launcher_id, did.recovery_list_hash, did.num_verifications_required, - did.metadata, did.p2_puzzle_hash - FROM `coin_states` AS cs - INNER JOIN `did_coins` AS did ON cs.coin_id = did.coin_id - WHERE cs.coin_id = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(sql.into_row()?)) -} diff --git a/crates/sage-database/src/primitives/nfts.rs b/crates/sage-database/src/primitives/nfts.rs deleted file mode 100644 index 4525d2af3..000000000 --- a/crates/sage-database/src/primitives/nfts.rs +++ /dev/null @@ -1,1341 +0,0 @@ -use chia::{ - protocol::{Bytes32, Program}, - puzzles::LineageProof, - sha2::Sha256, -}; -use chia_wallet_sdk::driver::{Nft, NftInfo}; -use sqlx::SqliteExecutor; - -use crate::{ - into_row, to_bytes32, CoinStateRow, CoinStateSql, CollectionRow, CollectionSql, Database, - DatabaseTx, FullNftCoinSql, IntoRow, NftRow, NftSql, Result, -}; - -#[derive(Debug, Clone)] -pub struct NftData { - pub blob: Vec, - pub mime_type: String, - pub hash_matches: bool, -} - -#[derive(Debug, Clone)] -pub struct NftUri { - pub hash: Bytes32, - pub uri: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum NftSortMode { - Recent, - Name, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum NftGroup { - Collection(Bytes32), - NoCollection, - MinterDid(Bytes32), - NoMinterDid, - OwnerDid(Bytes32), - NoOwnerDid, -} - -#[derive(Debug, Clone)] -pub struct NftSearchParams { - pub sort_mode: NftSortMode, - pub include_hidden: bool, - pub group: Option, - pub name: Option, -} - -#[derive(sqlx::FromRow)] -struct NftSearchRow { - #[sqlx(flatten)] - nft: NftSql, - total_count: i32, -} - -#[derive(sqlx::FromRow)] -struct CollectionSearchSql { - collection_id: Vec, - did_id: Vec, - metadata_collection_id: String, - visible: bool, - name: Option, - icon: Option, - total_count: i64, -} - -pub fn calculate_collection_id(did_id: Bytes32, json_collection_id: &str) -> Bytes32 { - let mut hasher = Sha256::new(); - hasher.update(hex::encode(did_id)); - hasher.update(json_collection_id); - hasher.finalize().into() -} - -impl Database { - pub async fn unchecked_nft_uris(&self, limit: u32) -> Result> { - unchecked_nft_uris(&self.pool, limit).await - } - - pub async fn set_nft_visible(&self, launcher_id: Bytes32, visible: bool) -> Result<()> { - set_nft_visible(&self.pool, launcher_id, visible).await - } - - pub async fn spendable_nft(&self, launcher_id: Bytes32) -> Result>> { - spendable_nft(&self.pool, launcher_id).await - } - - pub async fn fetch_nft_data(&self, hash: Bytes32) -> Result> { - fetch_nft_data(&self.pool, hash).await - } - - pub async fn distinct_minter_dids( - &self, - limit: u32, - offset: u32, - ) -> Result<(Vec>, u32)> { - distinct_minter_dids(&self.pool, limit, offset).await - } - - pub async fn search_nfts( - &self, - params: NftSearchParams, - limit: u32, - offset: u32, - ) -> Result<(Vec, u32)> { - search_nfts(&self.pool, params, limit, offset).await - } - - pub async fn collection(&self, collection_id: Bytes32) -> Result> { - collection(&self.pool, collection_id).await - } - - pub async fn collections_visible_named( - &self, - limit: u32, - offset: u32, - ) -> Result<(Vec, u32)> { - collections_visible_named(&self.pool, limit, offset).await - } - - pub async fn collections_named( - &self, - limit: u32, - offset: u32, - ) -> Result<(Vec, u32)> { - collections_named(&self.pool, limit, offset).await - } - - pub async fn nft_row(&self, launcher_id: Bytes32) -> Result> { - nft_row(&self.pool, launcher_id).await - } - - pub async fn nft(&self, launcher_id: Bytes32) -> Result>> { - nft(&self.pool, launcher_id).await - } - - pub async fn collection_name(&self, collection_id: Bytes32) -> Result> { - collection_name(&self.pool, collection_id).await - } - - pub async fn data_hash(&self, launcher_id: Bytes32) -> Result> { - data_hash(&self.pool, launcher_id).await - } - - pub async fn metadata_hash(&self, launcher_id: Bytes32) -> Result> { - metadata_hash(&self.pool, launcher_id).await - } - - pub async fn license_hash(&self, launcher_id: Bytes32) -> Result> { - license_hash(&self.pool, launcher_id).await - } - - pub async fn created_unspent_nft_coin_states( - &self, - limit: u32, - offset: u32, - ) -> Result> { - created_unspent_nft_coin_states(&self.pool, limit, offset).await - } - - pub async fn created_unspent_nft_coin_state( - &self, - launcher_id: Bytes32, - ) -> Result> { - created_unspent_nft_coin_state(&self.pool, launcher_id).await - } - - pub async fn nft_by_coin_id(&self, coin_id: Bytes32) -> Result>> { - nft_by_coin_id(&self.pool, coin_id).await - } - - pub async fn set_collection_visible( - &self, - collection_id: Bytes32, - visible: bool, - ) -> Result<()> { - set_collection_visible(&self.pool, collection_id, visible).await - } - - pub async fn nft_icon(&self, hash: Bytes32) -> Result>> { - nft_icon(&self.pool, hash).await - } - - pub async fn nft_thumbnail(&self, hash: Bytes32) -> Result>> { - nft_thumbnail(&self.pool, hash).await - } - - pub async fn total_uris(&self) -> Result { - total_uris(&self.pool).await - } - - pub async fn checked_uris(&self) -> Result { - checked_uris(&self.pool).await - } -} - -impl DatabaseTx<'_> { - pub async fn insert_nft_coin( - &mut self, - coin_id: Bytes32, - lineage_proof: LineageProof, - nft_info: NftInfo, - data_hash: Option, - metadata_hash: Option, - license_hash: Option, - ) -> Result<()> { - insert_nft_coin( - &mut *self.tx, - coin_id, - lineage_proof, - nft_info, - data_hash, - metadata_hash, - license_hash, - ) - .await - } - - pub async fn insert_nft_uri(&mut self, uri: String, hash: Bytes32) -> Result<()> { - insert_nft_uri(&mut *self.tx, uri, hash).await - } - - pub async fn set_nft_uri_checked( - &mut self, - uri: String, - hash: Bytes32, - hash_matches: Option, - ) -> Result<()> { - set_nft_uri_checked(&mut *self.tx, uri, hash, hash_matches).await - } - - pub async fn set_nft_uri_unchecked(&mut self, uri: String) -> Result<()> { - set_nft_uri_unchecked(&mut *self.tx, uri).await - } - - pub async fn delete_nft_data(&mut self, hash: Bytes32) -> Result<()> { - delete_nft_data(&mut *self.tx, hash).await - } - - pub async fn delete_nft_thumbnail(&mut self, hash: Bytes32) -> Result<()> { - delete_nft_thumbnail(&mut *self.tx, hash).await - } - - pub async fn insert_nft_data(&mut self, hash: Bytes32, nft_data: NftData) -> Result<()> { - insert_nft_data(&mut *self.tx, hash, nft_data).await - } - - pub async fn insert_nft_thumbnail( - &mut self, - hash: Bytes32, - icon: Vec, - thumbnail: Vec, - ) -> Result<()> { - insert_nft_thumbnail(&mut *self.tx, hash, icon, thumbnail).await - } - - pub async fn fetch_nft_data(&mut self, hash: Bytes32) -> Result> { - fetch_nft_data(&mut *self.tx, hash).await - } - - pub async fn insert_nft(&mut self, row: NftRow) -> Result<()> { - insert_nft(&mut *self.tx, row).await - } - - pub async fn nft_row(&mut self, launcher_id: Bytes32) -> Result> { - nft_row(&mut *self.tx, launcher_id).await - } - - pub async fn nft_row_by_coin(&mut self, coin_id: Bytes32) -> Result> { - nft_row_by_coin(&mut *self.tx, coin_id).await - } - - pub async fn nfts_by_metadata_hash(&mut self, metadata_hash: Bytes32) -> Result> { - nfts_by_metadata_hash(&mut *self.tx, metadata_hash).await - } - - pub async fn nft(&mut self, launcher_id: Bytes32) -> Result>> { - nft(&mut *self.tx, launcher_id).await - } - - pub async fn collection(&mut self, collection_id: Bytes32) -> Result> { - collection(&mut *self.tx, collection_id).await - } - - pub async fn insert_collection(&mut self, row: CollectionRow) -> Result<()> { - insert_collection(&mut *self.tx, row).await - } - - pub async fn update_collection( - &mut self, - collection_id: Bytes32, - row: CollectionRow, - ) -> Result<()> { - update_collection(&mut *self.tx, collection_id, row).await - } - - pub async fn set_nft_not_owned(&mut self, coin_id: Bytes32) -> Result<()> { - set_nft_not_owned(&mut *self.tx, coin_id).await - } - - pub async fn set_nft_created_height( - &mut self, - coin_id: Bytes32, - height: Option, - ) -> Result<()> { - set_nft_created_height(&mut *self.tx, coin_id, height).await - } - - pub async fn collection_ids(&mut self) -> Result> { - collection_ids(&mut *self.tx).await - } - - pub async fn update_nft_collection_ids( - &mut self, - collection_id: Bytes32, - new_collection_id: Bytes32, - ) -> Result<()> { - update_nft_collection_ids(&mut *self.tx, collection_id, new_collection_id).await - } - - pub async fn update_collection_id( - &mut self, - collection_id: Bytes32, - new_collection_id: Bytes32, - ) -> Result<()> { - update_collection_id(&mut *self.tx, collection_id, new_collection_id).await - } -} - -async fn insert_collection(conn: impl SqliteExecutor<'_>, row: CollectionRow) -> Result<()> { - let collection_id = row.collection_id.as_ref(); - let did_id = row.did_id.as_ref(); - let name = row.name.as_deref(); - let icon = row.icon.as_deref(); - - sqlx::query!( - " - INSERT OR IGNORE INTO `collections` ( - `collection_id`, - `did_id`, - `metadata_collection_id`, - `visible`, - `name`, - `icon` - ) - VALUES (?, ?, ?, ?, ?, ?) - ", - collection_id, - did_id, - row.metadata_collection_id, - row.visible, - name, - icon - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn update_collection( - conn: impl SqliteExecutor<'_>, - collection_id: Bytes32, - row: CollectionRow, -) -> Result<()> { - let collection_id = collection_id.as_ref(); - let new_collection_id = row.collection_id.as_ref(); - let did_id = row.did_id.as_ref(); - let name = row.name.as_deref(); - let icon = row.icon.as_deref(); - - sqlx::query!( - " - UPDATE `collections` SET - `collection_id` = ?, - `did_id` = ?, - `metadata_collection_id` = ?, - `visible` = ?, - `name` = ?, - `icon` = ? - WHERE `collection_id` = ? - ", - new_collection_id, - did_id, - row.metadata_collection_id, - row.visible, - name, - icon, - collection_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn collection_name( - conn: impl SqliteExecutor<'_>, - collection_id: Bytes32, -) -> Result> { - let collection_id = collection_id.as_ref(); - - let row = sqlx::query!( - "SELECT `name` FROM `collections` WHERE `collection_id` = ?", - collection_id - ) - .fetch_optional(conn) - .await?; - - Ok(row.and_then(|row| row.name)) -} - -async fn collection( - conn: impl SqliteExecutor<'_>, - collection_id: Bytes32, -) -> Result> { - let collection_id = collection_id.as_ref(); - - sqlx::query_as!( - CollectionSql, - " - SELECT `collection_id`, `did_id`, `metadata_collection_id`, `visible`, `name`, `icon` - FROM `collections` WHERE `collection_id` = ? - ", - collection_id - ) - .fetch_optional(conn) - .await? - .map(into_row) - .transpose() -} - -async fn collections_visible_named( - conn: impl SqliteExecutor<'_>, - limit: u32, - offset: u32, -) -> Result<(Vec, u32)> { - let rows = sqlx::query_as!( - CollectionSearchSql, - r#" - SELECT - collection_id, - did_id, - metadata_collection_id, - visible, - name, - icon, - COUNT(*) OVER() as total_count - FROM collections INDEXED BY col_name - WHERE visible = 1 - ORDER BY name IS NOT NULL DESC, name ASC, collection_id ASC - LIMIT ? OFFSET ? - "#, - limit, - offset - ) - .fetch_all(conn) - .await?; - - let total = rows.first().map_or(Ok(0), |r| r.total_count.try_into())?; - let collections = rows - .into_iter() - .map(|row| { - into_row(CollectionSql { - collection_id: row.collection_id, - did_id: row.did_id, - metadata_collection_id: row.metadata_collection_id, - visible: row.visible, - name: row.name, - icon: row.icon, - }) - }) - .collect::>>()?; - - Ok((collections, total)) -} - -async fn collections_named( - conn: impl SqliteExecutor<'_>, - limit: u32, - offset: u32, -) -> Result<(Vec, u32)> { - let rows = sqlx::query_as!( - CollectionSearchSql, - r#" - SELECT - collection_id, - did_id, - metadata_collection_id, - visible, - name, - icon, - COUNT(*) OVER() as total_count - FROM collections INDEXED BY col_name - ORDER BY visible DESC, is_named DESC, name ASC, collection_id ASC - LIMIT ? OFFSET ? - "#, - limit, - offset - ) - .fetch_all(conn) - .await?; - - let total = rows.first().map_or(Ok(0), |r| r.total_count.try_into())?; - let collections = rows - .into_iter() - .map(|row| { - into_row(CollectionSql { - collection_id: row.collection_id, - did_id: row.did_id, - metadata_collection_id: row.metadata_collection_id, - visible: row.visible, - name: row.name, - icon: row.icon, - }) - }) - .collect::>>()?; - - Ok((collections, total)) -} - -async fn insert_nft_uri(conn: impl SqliteExecutor<'_>, uri: String, hash: Bytes32) -> Result<()> { - let hash = hash.as_ref(); - - sqlx::query!( - "INSERT OR IGNORE INTO `nft_uris` (`hash`, `uri`, `checked`, `hash_matches`) VALUES (?, ?, ?, ?)", - hash, - uri, - false, - None:: - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn unchecked_nft_uris(conn: impl SqliteExecutor<'_>, limit: u32) -> Result> { - let rows = sqlx::query!( - "SELECT `hash`, `uri` FROM `nft_uris` WHERE `checked` = 0 LIMIT ?", - limit - ) - .fetch_all(conn) - .await?; - - let mut uris = Vec::new(); - - for row in rows { - let hash = to_bytes32(&row.hash)?; - let uri = row.uri; - - uris.push(NftUri { hash, uri }); - } - - Ok(uris) -} - -async fn set_nft_uri_checked( - conn: impl SqliteExecutor<'_>, - uri: String, - hash: Bytes32, - hash_matches: Option, -) -> Result<()> { - let hash = hash.as_ref(); - - sqlx::query!( - "UPDATE `nft_uris` SET `checked` = 1, `hash_matches` = ? WHERE `hash` = ? AND `uri` = ?", - hash_matches, - hash, - uri - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn set_nft_uri_unchecked(conn: impl SqliteExecutor<'_>, uri: String) -> Result<()> { - sqlx::query!("UPDATE `nft_uris` SET `checked` = 0 WHERE `uri` = ?", uri) - .execute(conn) - .await?; - - Ok(()) -} - -async fn insert_nft_data( - conn: impl SqliteExecutor<'_>, - hash: Bytes32, - nft_data: NftData, -) -> Result<()> { - let hash = hash.as_ref(); - let data = nft_data.blob; - let mime_type = nft_data.mime_type; - - sqlx::query!( - "REPLACE INTO `nft_data` (`hash`, `data`, `mime_type`, `hash_matches`) VALUES (?, ?, ?, ?)", - hash, - data, - mime_type, - nft_data.hash_matches - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn delete_nft_data(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result<()> { - let hash = hash.as_ref(); - - sqlx::query!("DELETE FROM `nft_data` WHERE `hash` = ?", hash) - .execute(conn) - .await?; - - Ok(()) -} - -async fn insert_nft_thumbnail( - conn: impl SqliteExecutor<'_>, - hash: Bytes32, - icon: Vec, - thumbnail: Vec, -) -> Result<()> { - let hash = hash.as_ref(); - - sqlx::query!( - "REPLACE INTO `nft_thumbnails` (`hash`, `icon`, `thumbnail`) VALUES (?, ?, ?)", - hash, - icon, - thumbnail - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn delete_nft_thumbnail(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result<()> { - let hash = hash.as_ref(); - - sqlx::query!("DELETE FROM `nft_thumbnails` WHERE `hash` = ?", hash) - .execute(conn) - .await?; - - Ok(()) -} - -async fn nft_icon(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result>> { - let hash = hash.as_ref(); - - let row = sqlx::query!("SELECT `icon` FROM `nft_thumbnails` WHERE `hash` = ?", hash) - .fetch_optional(conn) - .await?; - - Ok(row.map(|row| row.icon)) -} - -async fn nft_thumbnail(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result>> { - let hash = hash.as_ref(); - - let row = sqlx::query!( - "SELECT `thumbnail` FROM `nft_thumbnails` WHERE `hash` = ?", - hash - ) - .fetch_optional(conn) - .await?; - - Ok(row.map(|row| row.thumbnail)) -} - -async fn distinct_minter_dids( - conn: impl SqliteExecutor<'_>, - limit: u32, - offset: u32, -) -> Result<(Vec>, u32)> { - let rows = sqlx::query!( - r#" - WITH distinct_dids AS ( - SELECT DISTINCT minter_did - FROM nfts - WHERE minter_did IS NOT NULL - ) - SELECT - minter_did, - COUNT(*) OVER() AS total_count - FROM distinct_dids - LIMIT ? OFFSET ? - "#, - limit, - offset - ) - .fetch_all(conn) - .await?; - - let total_count = rows - .first() - .map_or(Ok(0), |row| row.total_count.try_into())?; - let dids = rows - .into_iter() - .map(|row| row.minter_did.map(|bytes| to_bytes32(&bytes)).transpose()) - .collect::>>()?; - - Ok((dids, total_count)) -} - -async fn fetch_nft_data(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result> { - let hash = hash.as_ref(); - - let row = sqlx::query!( - "SELECT `data`, `mime_type`, `hash_matches` FROM `nft_data` WHERE `hash` = ?", - hash - ) - .fetch_optional(conn) - .await?; - - Ok(row.map(|row| NftData { - blob: row.data, - mime_type: row.mime_type, - hash_matches: row.hash_matches, - })) -} - -async fn insert_nft(conn: impl SqliteExecutor<'_>, row: NftRow) -> Result<()> { - let launcher_id = row.launcher_id.as_ref(); - let coin_id = row.coin_id.as_ref(); - let collection_id = row.collection_id.as_deref(); - let minter_did = row.minter_did.as_deref(); - let owner_did = row.owner_did.as_deref(); - let name = row.name.as_deref(); - let metadata_hash = row.metadata_hash.as_deref(); - - sqlx::query!( - "REPLACE INTO `nfts` ( - `launcher_id`, - `coin_id`, - `collection_id`, - `minter_did`, - `owner_did`, - `visible`, - `sensitive_content`, - `name`, - `is_owned`, - `created_height`, - `metadata_hash`, - `edition_number`, - `edition_total` - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - launcher_id, - coin_id, - collection_id, - minter_did, - owner_did, - row.visible, - row.sensitive_content, - name, - row.is_owned, - row.created_height, - metadata_hash, - row.edition_number, - row.edition_total - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn nft_row(conn: impl SqliteExecutor<'_>, launcher_id: Bytes32) -> Result> { - let launcher_id = launcher_id.as_ref(); - - sqlx::query_as!( - NftSql, - " - SELECT launcher_id, coin_id, collection_id, minter_did, owner_did, visible, - sensitive_content, name, is_owned, created_height, metadata_hash, - is_named, is_pending, edition_number, edition_total - FROM `nfts` WHERE `launcher_id` = ? - ", - launcher_id - ) - .fetch_optional(conn) - .await? - .map(into_row) - .transpose() -} - -async fn nft_row_by_coin( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, -) -> Result> { - let coin_id = coin_id.as_ref(); - - sqlx::query_as!( - NftSql, - " - SELECT launcher_id, coin_id, collection_id, minter_did, owner_did, visible, - sensitive_content, name, is_owned, created_height, metadata_hash, - is_named, is_pending, edition_number, edition_total - FROM `nfts` WHERE `coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - .map(into_row) - .transpose() -} - -async fn set_nft_visible( - conn: impl SqliteExecutor<'_>, - launcher_id: Bytes32, - visible: bool, -) -> Result<()> { - let launcher_id = launcher_id.as_ref(); - - sqlx::query!( - "UPDATE `nfts` SET `visible` = ? WHERE `launcher_id` = ?", - visible, - launcher_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn set_collection_visible( - conn: impl SqliteExecutor<'_>, - collection_id: Bytes32, - visible: bool, -) -> Result<()> { - let collection_id = collection_id.as_ref(); - - sqlx::query!( - "UPDATE `collections` SET `visible` = ? WHERE `collection_id` = ?", - visible, - collection_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn spendable_nft( - conn: impl SqliteExecutor<'_>, - launcher_id: Bytes32, -) -> Result>> { - let launcher_id = launcher_id.as_ref(); - - let Some(sql) = sqlx::query_as!( - FullNftCoinSql, - " - SELECT - `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`, - `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`, - `launcher_id`, `metadata`, `metadata_updater_puzzle_hash`, `current_owner`, - `royalty_puzzle_hash`, `royalty_ten_thousandths`, `p2_puzzle_hash` - FROM `nft_coins` - INNER JOIN `coin_states` INDEXED BY `coin_height` ON `nft_coins`.`coin_id` = `coin_states`.`coin_id` - LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id` - WHERE `launcher_id` = ? - AND `spent_height` IS NULL - AND `created_height` IS NOT NULL - AND `coin_states`.`transaction_id` IS NULL - AND `transaction_spends`.`transaction_id` IS NULL - ", - launcher_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(sql.into_row()?)) -} - -async fn nft(conn: impl SqliteExecutor<'_>, launcher_id: Bytes32) -> Result>> { - let launcher_id = launcher_id.as_ref(); - - let Some(sql) = sqlx::query_as!( - FullNftCoinSql, - " - SELECT - `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`, - `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`, - `launcher_id`, `metadata`, `metadata_updater_puzzle_hash`, `current_owner`, - `royalty_puzzle_hash`, `royalty_ten_thousandths`, `p2_puzzle_hash` - FROM `nft_coins` INDEXED BY `nft_launcher_id` - INNER JOIN `coin_states` ON `nft_coins`.`coin_id` = `coin_states`.`coin_id` - LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id` - WHERE `launcher_id` = ? - AND `spent_height` IS NULL - AND `transaction_spends`.`transaction_id` IS NULL - ", - launcher_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(sql.into_row()?)) -} - -async fn nfts_by_metadata_hash( - conn: impl SqliteExecutor<'_>, - metadata_hash: Bytes32, -) -> Result> { - let metadata_hash = metadata_hash.as_ref(); - - sqlx::query_as!( - NftSql, - " - SELECT launcher_id, coin_id, collection_id, minter_did, owner_did, visible, - sensitive_content, name, is_owned, created_height, metadata_hash, - is_named, is_pending, edition_number, edition_total - FROM `nfts` INDEXED BY `nft_metadata` - WHERE `metadata_hash` = ? - ", - metadata_hash - ) - .fetch_all(conn) - .await? - .into_iter() - .map(into_row) - .collect() -} - -async fn search_nfts( - conn: impl SqliteExecutor<'_>, - params: NftSearchParams, - limit: u32, - offset: u32, -) -> Result<(Vec, u32)> { - let mut query = sqlx::QueryBuilder::new( - "SELECT launcher_id, - coin_id, - collection_id, - minter_did, - owner_did, - visible, - sensitive_content, - name, - is_owned, - created_height, - metadata_hash, - is_named, - is_pending, - edition_number, - edition_total, - COUNT(*) OVER() as total_count - FROM nfts - WHERE 1=1 - AND is_owned = 1 - ", - ); - - // Add visibility condition if not including hidden NFTs - if !params.include_hidden { - query.push(" AND visible = 1"); - } - - // Add group filtering (Collection/DID) - if let Some(group) = ¶ms.group { - match group { - NftGroup::Collection(id) => { - query.push(" AND collection_id = "); - query.push_bind(id.as_ref()); - } - NftGroup::NoCollection => { - query.push(" AND collection_id IS NULL"); - } - NftGroup::MinterDid(id) => { - query.push(" AND minter_did = "); - query.push_bind(id.as_ref()); - } - NftGroup::NoMinterDid => { - query.push(" AND minter_did IS NULL"); - } - NftGroup::OwnerDid(id) => { - query.push(" AND owner_did = "); - query.push_bind(id.as_ref()); - } - NftGroup::NoOwnerDid => { - query.push(" AND owner_did IS NULL"); - } - } - } - - // Add name search if present - if let Some(name_search) = ¶ms.name { - query.push(" AND name LIKE "); - query.push_bind(format!("%{name_search}%")); - } - - // Add ORDER BY clause based on sort_mode - query.push(" ORDER BY "); - - // Add visible DESC to sort order if including hidden NFTs - if params.include_hidden { - query.push("visible DESC, "); - } - - match params.sort_mode { - NftSortMode::Recent => { - query.push("is_pending DESC, created_height DESC, launcher_id ASC"); - } - NftSortMode::Name => { - query.push( - "is_pending DESC, is_named DESC, name ASC, edition_number ASC, launcher_id ASC", - ); - } - } - - query.push(" LIMIT ? OFFSET ?"); - - let query = query.build_query_as::(); - - // Bind limit and offset - let query = query.bind(limit).bind(offset); - - let rows = query.fetch_all(conn).await?; - let total_count = rows - .first() - .map_or(Ok(0), |row| row.total_count.try_into())?; - let nfts = rows - .into_iter() - .map(|row| into_row(row.nft)) - .collect::>>()?; - - Ok((nfts, total_count)) -} - -async fn insert_nft_coin( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, - lineage_proof: LineageProof, - nft_info: NftInfo, - data_hash: Option, - metadata_hash: Option, - license_hash: Option, -) -> Result<()> { - let coin_id = coin_id.as_ref(); - let parent_parent_coin_id = lineage_proof.parent_parent_coin_info.as_ref(); - let parent_inner_puzzle_hash = lineage_proof.parent_inner_puzzle_hash.as_ref(); - let parent_amount = lineage_proof.parent_amount.to_be_bytes(); - let parent_amount = parent_amount.as_ref(); - let launcher_id = nft_info.launcher_id.as_ref(); - let metadata = nft_info.metadata.as_ref(); - let metadata_updater_puzzle_hash = nft_info.metadata_updater_puzzle_hash.as_ref(); - let current_owner = nft_info.current_owner.as_deref(); - let royalty_puzzle_hash = nft_info.royalty_puzzle_hash.as_ref(); - let royalty_ten_thousandths = nft_info.royalty_basis_points; - let p2_puzzle_hash = nft_info.p2_puzzle_hash.as_ref(); - let data_hash = data_hash.as_deref(); - let metadata_hash = metadata_hash.as_deref(); - let license_hash = license_hash.as_deref(); - - sqlx::query!( - " - INSERT OR IGNORE INTO `nft_coins` ( - `coin_id`, - `parent_parent_coin_id`, - `parent_inner_puzzle_hash`, - `parent_amount`, - `launcher_id`, - `metadata`, - `metadata_updater_puzzle_hash`, - `current_owner`, - `royalty_puzzle_hash`, - `royalty_ten_thousandths`, - `p2_puzzle_hash`, - `data_hash`, - `metadata_hash`, - `license_hash` - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ", - coin_id, - parent_parent_coin_id, - parent_inner_puzzle_hash, - parent_amount, - launcher_id, - metadata, - metadata_updater_puzzle_hash, - current_owner, - royalty_puzzle_hash, - royalty_ten_thousandths, - p2_puzzle_hash, - data_hash, - metadata_hash, - license_hash - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn data_hash(conn: impl SqliteExecutor<'_>, launcher_id: Bytes32) -> Result> { - let launcher_id = launcher_id.as_ref(); - - let Some(row) = sqlx::query!( - "SELECT `data_hash` FROM `nft_coins` WHERE `launcher_id` = ?", - launcher_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - let Some(data_hash) = row.data_hash else { - return Ok(None); - }; - - Ok(Some(to_bytes32(&data_hash)?)) -} - -async fn metadata_hash( - conn: impl SqliteExecutor<'_>, - launcher_id: Bytes32, -) -> Result> { - let launcher_id = launcher_id.as_ref(); - - let Some(row) = sqlx::query!( - "SELECT `metadata_hash` FROM `nft_coins` WHERE `launcher_id` = ?", - launcher_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - let Some(metadata_hash) = row.metadata_hash else { - return Ok(None); - }; - - Ok(Some(to_bytes32(&metadata_hash)?)) -} - -async fn license_hash( - conn: impl SqliteExecutor<'_>, - launcher_id: Bytes32, -) -> Result> { - let launcher_id = launcher_id.as_ref(); - - let Some(row) = sqlx::query!( - "SELECT `license_hash` FROM `nft_coins` WHERE `launcher_id` = ?", - launcher_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - let Some(license_hash) = row.license_hash else { - return Ok(None); - }; - - Ok(Some(to_bytes32(&license_hash)?)) -} - -async fn created_unspent_nft_coin_states( - conn: impl SqliteExecutor<'_>, - limit: u32, - offset: u32, -) -> Result> { - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` - INNER JOIN `nft_coins` ON `coin_states`.coin_id = `nft_coins`.coin_id - WHERE `spent_height` IS NULL - AND `created_height` IS NOT NULL - ORDER BY `created_height`, `coin_states`.`coin_id` LIMIT ? OFFSET ? - ", - limit, - offset - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn created_unspent_nft_coin_state( - conn: impl SqliteExecutor<'_>, - launcher_id: Bytes32, -) -> Result> { - let launcher_id = launcher_id.as_ref(); - - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` - INNER JOIN `nft_coins` ON `coin_states`.coin_id = `nft_coins`.coin_id - WHERE `launcher_id` = ? - AND `spent_height` IS NULL - AND `created_height` IS NOT NULL - ", - launcher_id - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} - -async fn nft_by_coin_id( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, -) -> Result>> { - let coin_id = coin_id.as_ref(); - - let Some(sql) = sqlx::query_as!( - FullNftCoinSql, - " - SELECT - `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`, - `parent_parent_coin_id`, `parent_inner_puzzle_hash`, `parent_amount`, - `launcher_id`, `metadata`, `metadata_updater_puzzle_hash`, `current_owner`, - `royalty_puzzle_hash`, `royalty_ten_thousandths`, `p2_puzzle_hash` - FROM `nft_coins` - INNER JOIN `coin_states` INDEXED BY `coin_height` ON `nft_coins`.`coin_id` = `coin_states`.`coin_id` - WHERE `coin_states`.`coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(sql.into_row()?)) -} - -async fn set_nft_not_owned(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result<()> { - let coin_id = coin_id.as_ref(); - - sqlx::query!( - "UPDATE `nfts` SET `is_owned` = 0 WHERE `coin_id` = ?", - coin_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn set_nft_created_height( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, - height: Option, -) -> Result<()> { - let coin_id = coin_id.as_ref(); - - sqlx::query!( - "UPDATE `nfts` SET `created_height` = ? WHERE `coin_id` = ?", - height, - coin_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn collection_ids(conn: impl SqliteExecutor<'_>) -> Result> { - sqlx::query_scalar!("SELECT `collection_id` FROM `collections`") - .fetch_all(conn) - .await? - .into_iter() - .map(|row| to_bytes32(&row)) - .collect() -} - -async fn update_collection_id( - conn: impl SqliteExecutor<'_>, - collection_id: Bytes32, - new_collection_id: Bytes32, -) -> Result<()> { - let collection_id = collection_id.as_ref(); - let new_collection_id = new_collection_id.as_ref(); - - sqlx::query!( - "UPDATE `collections` SET `collection_id` = ? WHERE `collection_id` = ?", - new_collection_id, - collection_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn update_nft_collection_ids( - conn: impl SqliteExecutor<'_>, - collection_id: Bytes32, - new_collection_id: Bytes32, -) -> Result<()> { - let collection_id = collection_id.as_ref(); - let new_collection_id = new_collection_id.as_ref(); - - sqlx::query!( - "UPDATE `nfts` SET `collection_id` = ? WHERE `collection_id` = ?", - new_collection_id, - collection_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -pub async fn total_uris(conn: impl SqliteExecutor<'_>) -> Result { - Ok(sqlx::query!("SELECT COUNT(*) AS `count` FROM `nft_uris`") - .fetch_one(conn) - .await? - .count - .try_into()?) -} - -pub async fn checked_uris(conn: impl SqliteExecutor<'_>) -> Result { - Ok( - sqlx::query!("SELECT COUNT(*) AS `count` FROM `nft_uris` WHERE `checked` = 1") - .fetch_one(conn) - .await? - .count - .try_into()?, - ) -} diff --git a/crates/sage-database/src/primitives/xch.rs b/crates/sage-database/src/primitives/xch.rs deleted file mode 100644 index 0bcb35070..000000000 --- a/crates/sage-database/src/primitives/xch.rs +++ /dev/null @@ -1,242 +0,0 @@ -use chia::protocol::{Bytes32, Coin}; -use sqlx::{Row, SqliteExecutor}; - -use crate::{ - into_row, to_bytes, CoinSortMode, CoinSql, CoinStateRow, CoinStateSql, Database, DatabaseTx, - EnhancedCoinStateRow, Result, -}; - -impl Database { - pub async fn spendable_coins(&self) -> Result> { - spendable_coins(&self.pool).await - } - - pub async fn balance(&self) -> Result { - balance(&self.pool).await - } - - pub async fn spendable_p2_coin_count(&self) -> Result { - spendable_p2_coin_count(&self.pool).await - } - - pub async fn p2_coin_states( - &self, - limit: u32, - offset: u32, - sort_mode: CoinSortMode, - ascending: bool, - include_spent_coins: bool, - ) -> Result<(Vec, u32)> { - p2_coin_states( - &self.pool, - limit, - offset, - sort_mode, - ascending, - include_spent_coins, - ) - .await - } - - pub async fn created_unspent_p2_coin_states( - &self, - limit: u32, - offset: u32, - ) -> Result> { - created_unspent_p2_coin_states(&self.pool, limit, offset).await - } -} - -impl DatabaseTx<'_> { - pub async fn insert_p2_coin(&mut self, coin_id: Bytes32) -> Result<()> { - insert_p2_coin(&mut *self.tx, coin_id).await - } -} - -async fn insert_p2_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result<()> { - let coin_id = coin_id.as_ref(); - - sqlx::query!( - " - UPDATE `coin_states` SET `kind` = 1 WHERE `coin_id` = ? - ", - coin_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn balance(conn: impl SqliteExecutor<'_>) -> Result { - let row = sqlx::query!( - " - SELECT `coin_states`.`amount` FROM `coin_states` INDEXED BY `coin_kind_spent` - LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id` - WHERE `coin_states`.`spent_height` IS NULL - AND `transaction_spends`.`coin_id` IS NULL - AND `kind` = 1 - " - ) - .fetch_all(conn) - .await?; - - row.iter() - .map(|row| Ok(u64::from_be_bytes(to_bytes(&row.amount)?) as u128)) - .sum::>() -} - -async fn spendable_coins(conn: impl SqliteExecutor<'_>) -> Result> { - sqlx::query_as!( - CoinSql, - " - SELECT `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount` FROM `coin_states` - LEFT JOIN `transaction_spends` ON `coin_states`.`coin_id` = `transaction_spends`.`coin_id` - LEFT JOIN `offered_coins` ON `coin_states`.`coin_id` = `offered_coins`.`coin_id` - LEFT JOIN `offers` ON `offered_coins`.`offer_id` = `offers`.`offer_id` - WHERE `coin_states`.`spent_height` IS NULL - AND `transaction_spends`.`coin_id` IS NULL - AND (`offered_coins`.`coin_id` IS NULL OR `offers`.`status` > 0) - AND `coin_states`.`transaction_id` IS NULL - AND `kind` = 1 - " - ) - .fetch_all(conn) - .await? - .into_iter() - .map(into_row) - .collect() -} - -async fn spendable_p2_coin_count(conn: impl SqliteExecutor<'_>) -> Result { - let row = sqlx::query!( - " - SELECT COUNT(*) as count - FROM coin_states - LEFT JOIN transaction_spends ON transaction_spends.coin_id = coin_states.coin_id - WHERE 1=1 - AND created_height IS NOT NULL - AND spent_height IS NULL - AND coin_states.transaction_id IS NULL - AND transaction_spends.coin_id IS NULL - AND `kind` = 1 - " - ) - .fetch_one(conn) - .await?; - - Ok(row.count.try_into()?) -} - -async fn p2_coin_states( - conn: impl SqliteExecutor<'_>, - limit: u32, - offset: u32, - sort_mode: CoinSortMode, - ascending: bool, - include_spent_coins: bool, -) -> Result<(Vec, u32)> { - let mut query = sqlx::QueryBuilder::new( - " - SELECT `coin_states`.`parent_coin_id`, `coin_states`.`puzzle_hash`, `coin_states`.`amount`, `spent_height`, `created_height`, - `coin_states`.`transaction_id`, `kind`, `created_unixtime`, `spent_unixtime`, - `offered_coins`.offer_id, `transaction_spends`.transaction_id as spend_transaction_id, - COUNT(*) OVER() as total_count - FROM `coin_states` - LEFT JOIN `offered_coins` ON `coin_states`.coin_id = `offered_coins`.coin_id - LEFT JOIN `transaction_spends` ON `coin_states`.coin_id = `transaction_spends`.coin_id - WHERE `kind` = 1 - ", - ); - - if !include_spent_coins { - query.push(" AND `spent_height` IS NULL"); - } - - query.push(" ORDER BY "); - - match sort_mode { - CoinSortMode::CoinId => { - query.push("`coin_states`.`coin_id`"); - } - CoinSortMode::Amount => { - query.push("`coin_states`.`amount`"); - } - CoinSortMode::CreatedHeight => { - query.push("`created_height`"); - } - CoinSortMode::SpentHeight => { - query.push("`spent_height`"); - } - } - - if ascending { - query.push(" ASC"); - } else { - query.push(" DESC"); - } - - query.push(" LIMIT "); - query.push_bind(limit); - query.push(" OFFSET "); - query.push_bind(offset); - - let rows = query.build().fetch_all(conn).await?; - - let Some(first_row) = rows.first() else { - return Ok((vec![], 0)); - }; - - let total: u32 = first_row.try_get("total_count")?; - - let mut coin_states = Vec::with_capacity(rows.len()); - - for row in rows { - let sql = CoinStateSql { - parent_coin_id: row.try_get("parent_coin_id")?, - puzzle_hash: row.try_get("puzzle_hash")?, - amount: row.try_get("amount")?, - spent_height: row.try_get("spent_height")?, - created_height: row.try_get("created_height")?, - transaction_id: row.try_get("transaction_id")?, - kind: row.try_get("kind")?, - created_unixtime: row.try_get("created_unixtime")?, - spent_unixtime: row.try_get("spent_unixtime")?, - }; - - let coin_state_row = into_row(sql)?; - - let mut enhanced_row = EnhancedCoinStateRow::from(coin_state_row); - - enhanced_row.offer_id = row.try_get("offer_id").ok(); - enhanced_row.spend_transaction_id = row.try_get("spend_transaction_id").ok(); - - coin_states.push(enhanced_row); - } - - Ok((coin_states, total)) -} - -async fn created_unspent_p2_coin_states( - conn: impl SqliteExecutor<'_>, - limit: u32, - offset: u32, -) -> Result> { - let rows = sqlx::query_as!( - CoinStateSql, - " - SELECT `parent_coin_id`, `puzzle_hash`, `amount`, `spent_height`, `created_height`, `transaction_id`, `kind`, `created_unixtime`, `spent_unixtime` - FROM `coin_states` - WHERE `spent_height` IS NULL - AND `created_height` IS NOT NULL - AND `kind` = 1 - ORDER BY `created_height`, `coin_states`.`coin_id` LIMIT ? OFFSET ? - ", - limit, - offset - ) - .fetch_all(conn) - .await?; - - rows.into_iter().map(into_row).collect() -} diff --git a/crates/sage-database/src/rows.rs b/crates/sage-database/src/rows.rs deleted file mode 100644 index 37575ad97..000000000 --- a/crates/sage-database/src/rows.rs +++ /dev/null @@ -1,33 +0,0 @@ -mod cat; -mod cat_coin; -mod coin_state; -mod collection; -mod derivation; -mod did; -mod did_coin; -mod nft; -mod nft_coin; -mod offer; - -pub use cat::*; -pub use cat_coin::*; -pub use coin_state::*; -pub use collection::*; -pub use derivation::*; -pub use did::*; -pub use did_coin::*; -pub use nft::*; -pub(crate) use nft_coin::*; -pub use offer::*; - -use crate::DatabaseError; - -pub(crate) trait IntoRow { - type Row; - - fn into_row(self) -> Result; -} - -pub(crate) fn into_row(t: T) -> Result { - t.into_row() -} diff --git a/crates/sage-database/src/rows/cat.rs b/crates/sage-database/src/rows/cat.rs deleted file mode 100644 index ad52c4b2d..000000000 --- a/crates/sage-database/src/rows/cat.rs +++ /dev/null @@ -1,42 +0,0 @@ -use chia::protocol::Bytes32; - -use crate::{to_bytes32, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct CatSql { - pub asset_id: Vec, - pub name: Option, - pub ticker: Option, - pub description: Option, - pub icon: Option, - pub visible: bool, - pub fetched: bool, -} - -#[derive(Debug, Clone)] -pub struct CatRow { - pub asset_id: Bytes32, - pub name: Option, - pub ticker: Option, - pub description: Option, - pub icon: Option, - pub visible: bool, - pub fetched: bool, -} - -impl IntoRow for CatSql { - type Row = CatRow; - - fn into_row(self) -> Result { - Ok(CatRow { - asset_id: to_bytes32(&self.asset_id)?, - name: self.name.clone(), - ticker: self.ticker.clone(), - description: self.description.clone(), - icon: self.icon.clone(), - visible: self.visible, - fetched: self.fetched, - }) - } -} diff --git a/crates/sage-database/src/rows/cat_coin.rs b/crates/sage-database/src/rows/cat_coin.rs deleted file mode 100644 index c814397ed..000000000 --- a/crates/sage-database/src/rows/cat_coin.rs +++ /dev/null @@ -1,81 +0,0 @@ -use chia::{ - protocol::{Bytes32, Coin}, - puzzles::LineageProof, -}; -use chia_wallet_sdk::driver::{Cat, CatInfo}; - -use crate::{to_bytes32, to_u64, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct CatCoinSql { - pub parent_coin_id: Vec, - pub puzzle_hash: Vec, - pub amount: Vec, - pub parent_parent_coin_id: Vec, - pub parent_inner_puzzle_hash: Vec, - pub parent_amount: Vec, - pub p2_puzzle_hash: Vec, -} - -pub(crate) struct FullCatCoinSql { - pub parent_coin_id: Vec, - pub puzzle_hash: Vec, - pub amount: Vec, - pub parent_parent_coin_id: Vec, - pub parent_inner_puzzle_hash: Vec, - pub parent_amount: Vec, - pub p2_puzzle_hash: Vec, - pub asset_id: Vec, -} - -#[derive(Debug, Clone, Copy)] -pub struct CatCoinRow { - pub coin: Coin, - pub lineage_proof: LineageProof, - pub p2_puzzle_hash: Bytes32, -} - -impl IntoRow for CatCoinSql { - type Row = CatCoinRow; - - fn into_row(self) -> Result { - Ok(CatCoinRow { - coin: Coin { - parent_coin_info: to_bytes32(&self.parent_coin_id)?, - puzzle_hash: to_bytes32(&self.puzzle_hash)?, - amount: to_u64(&self.amount)?, - }, - lineage_proof: LineageProof { - parent_parent_coin_info: to_bytes32(&self.parent_parent_coin_id)?, - parent_inner_puzzle_hash: to_bytes32(&self.parent_inner_puzzle_hash)?, - parent_amount: to_u64(&self.parent_amount)?, - }, - p2_puzzle_hash: to_bytes32(&self.p2_puzzle_hash)?, - }) - } -} - -impl IntoRow for FullCatCoinSql { - type Row = Cat; - - fn into_row(self) -> Result { - Ok(Cat::new( - Coin::new( - to_bytes32(&self.parent_coin_id)?, - to_bytes32(&self.puzzle_hash)?, - to_u64(&self.amount)?, - ), - Some(LineageProof { - parent_parent_coin_info: to_bytes32(&self.parent_parent_coin_id)?, - parent_inner_puzzle_hash: to_bytes32(&self.parent_inner_puzzle_hash)?, - parent_amount: to_u64(&self.parent_amount)?, - }), - CatInfo::new( - to_bytes32(&self.asset_id)?, - None, - to_bytes32(&self.p2_puzzle_hash)?, - ), - )) - } -} diff --git a/crates/sage-database/src/rows/coin_state.rs b/crates/sage-database/src/rows/coin_state.rs deleted file mode 100644 index 60759f7d2..000000000 --- a/crates/sage-database/src/rows/coin_state.rs +++ /dev/null @@ -1,104 +0,0 @@ -use chia::protocol::{Bytes32, Coin, CoinState}; - -use crate::{to_bytes32, to_u64, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct CoinStateSql { - pub parent_coin_id: Vec, - pub puzzle_hash: Vec, - pub amount: Vec, - pub spent_height: Option, - pub created_height: Option, - pub transaction_id: Option>, - pub kind: i64, - pub created_unixtime: Option, - pub spent_unixtime: Option, -} - -pub(crate) struct CoinSql { - pub parent_coin_id: Vec, - pub puzzle_hash: Vec, - pub amount: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum CoinKind { - Unknown, - Xch, - Cat, - Nft, - Did, -} - -impl CoinKind { - pub fn from_i64(value: i64) -> Self { - match value { - 1 => Self::Xch, - 2 => Self::Cat, - 3 => Self::Nft, - 4 => Self::Did, - _ => Self::Unknown, - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct CoinStateRow { - pub coin_state: CoinState, - pub transaction_id: Option, - pub kind: CoinKind, - pub created_timestamp: Option, - pub spent_timestamp: Option, -} - -#[derive(Debug, Clone)] -pub struct EnhancedCoinStateRow { - pub base: CoinStateRow, - pub offer_id: Option, - pub spend_transaction_id: Option, -} - -impl From for EnhancedCoinStateRow { - fn from(base: CoinStateRow) -> Self { - Self { - base, - offer_id: None, - spend_transaction_id: None, - } - } -} - -impl IntoRow for CoinStateSql { - type Row = CoinStateRow; - - fn into_row(self) -> Result { - Ok(CoinStateRow { - coin_state: CoinState { - coin: Coin { - parent_coin_info: to_bytes32(&self.parent_coin_id)?, - puzzle_hash: to_bytes32(&self.puzzle_hash)?, - amount: to_u64(&self.amount)?, - }, - spent_height: self.spent_height.map(TryInto::try_into).transpose()?, - created_height: self.created_height.map(TryInto::try_into).transpose()?, - }, - transaction_id: self.transaction_id.as_deref().map(to_bytes32).transpose()?, - kind: CoinKind::from_i64(self.kind), - created_timestamp: self.created_unixtime.map(|t| t as u32), - spent_timestamp: self.spent_unixtime.map(|t| t as u32), - }) - } -} - -impl IntoRow for CoinSql { - type Row = Coin; - - fn into_row(self) -> Result { - Ok(Coin { - parent_coin_info: to_bytes32(&self.parent_coin_id)?, - puzzle_hash: to_bytes32(&self.puzzle_hash)?, - amount: to_u64(&self.amount)?, - }) - } -} diff --git a/crates/sage-database/src/rows/collection.rs b/crates/sage-database/src/rows/collection.rs deleted file mode 100644 index 72292c146..000000000 --- a/crates/sage-database/src/rows/collection.rs +++ /dev/null @@ -1,39 +0,0 @@ -use chia::protocol::Bytes32; - -use crate::{to_bytes32, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct CollectionSql { - pub collection_id: Vec, - pub did_id: Vec, - pub metadata_collection_id: String, - pub visible: bool, - pub name: Option, - pub icon: Option, -} - -#[derive(Debug, Clone)] -pub struct CollectionRow { - pub collection_id: Bytes32, - pub did_id: Bytes32, - pub metadata_collection_id: String, - pub visible: bool, - pub name: Option, - pub icon: Option, -} - -impl IntoRow for CollectionSql { - type Row = CollectionRow; - - fn into_row(self) -> Result { - Ok(CollectionRow { - collection_id: to_bytes32(&self.collection_id)?, - did_id: to_bytes32(&self.did_id)?, - metadata_collection_id: self.metadata_collection_id.clone(), - visible: self.visible, - name: self.name.clone(), - icon: self.icon.clone(), - }) - } -} diff --git a/crates/sage-database/src/rows/derivation.rs b/crates/sage-database/src/rows/derivation.rs deleted file mode 100644 index fc4af43dc..000000000 --- a/crates/sage-database/src/rows/derivation.rs +++ /dev/null @@ -1,33 +0,0 @@ -use chia::{bls::PublicKey, protocol::Bytes32}; - -use crate::{to_bytes, to_bytes32, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct DerivationSql { - pub p2_puzzle_hash: Vec, - pub index: i64, - pub hardened: bool, - pub synthetic_key: Vec, -} - -#[derive(Debug, Clone, Copy)] -pub struct DerivationRow { - pub p2_puzzle_hash: Bytes32, - pub index: u32, - pub hardened: bool, - pub synthetic_key: PublicKey, -} - -impl IntoRow for DerivationSql { - type Row = DerivationRow; - - fn into_row(self) -> Result { - Ok(DerivationRow { - p2_puzzle_hash: to_bytes32(&self.p2_puzzle_hash)?, - index: self.index.try_into()?, - hardened: self.hardened, - synthetic_key: PublicKey::from_bytes(&to_bytes(&self.synthetic_key)?)?, - }) - } -} diff --git a/crates/sage-database/src/rows/did.rs b/crates/sage-database/src/rows/did.rs deleted file mode 100644 index f3f0b53b2..000000000 --- a/crates/sage-database/src/rows/did.rs +++ /dev/null @@ -1,39 +0,0 @@ -use chia::protocol::Bytes32; - -use crate::{to_bytes32, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct DidSql { - pub launcher_id: Vec, - pub coin_id: Vec, - pub name: Option, - pub visible: bool, - pub is_owned: bool, - pub created_height: Option, -} - -#[derive(Debug, Clone)] -pub struct DidRow { - pub launcher_id: Bytes32, - pub coin_id: Bytes32, - pub name: Option, - pub is_owned: bool, - pub visible: bool, - pub created_height: Option, -} - -impl IntoRow for DidSql { - type Row = DidRow; - - fn into_row(self) -> Result { - Ok(DidRow { - launcher_id: to_bytes32(&self.launcher_id)?, - coin_id: to_bytes32(&self.coin_id)?, - name: self.name, - is_owned: self.is_owned, - visible: self.visible, - created_height: self.created_height.map(TryInto::try_into).transpose()?, - }) - } -} diff --git a/crates/sage-database/src/rows/did_coin.rs b/crates/sage-database/src/rows/did_coin.rs deleted file mode 100644 index 6a13c4770..000000000 --- a/crates/sage-database/src/rows/did_coin.rs +++ /dev/null @@ -1,91 +0,0 @@ -use chia::{ - protocol::{Bytes32, Coin, Program}, - puzzles::{LineageProof, Proof}, -}; -use chia_wallet_sdk::driver::{Did, DidInfo}; - -use crate::{to_bytes32, to_u64, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct FullDidCoinSql { - pub parent_coin_id: Vec, - pub puzzle_hash: Vec, - pub amount: Vec, - pub parent_parent_coin_id: Vec, - pub parent_inner_puzzle_hash: Vec, - pub parent_amount: Vec, - pub launcher_id: Vec, - pub recovery_list_hash: Option>, - pub num_verifications_required: Vec, - pub metadata: Vec, - pub p2_puzzle_hash: Vec, -} - -pub(crate) struct DidCoinInfoSql { - pub coin_id: Vec, - pub amount: Vec, - pub p2_puzzle_hash: Vec, - pub recovery_list_hash: Option>, - pub created_height: Option, - pub transaction_id: Option>, -} - -#[derive(Debug, Clone, Copy)] -pub struct DidCoinInfo { - pub coin_id: Bytes32, - pub amount: u64, - pub p2_puzzle_hash: Bytes32, - pub recovery_list_hash: Option, - pub created_height: Option, - pub transaction_id: Option, -} - -impl IntoRow for FullDidCoinSql { - type Row = Did; - - fn into_row(self) -> Result, DatabaseError> { - Ok(Did { - coin: Coin { - parent_coin_info: to_bytes32(&self.parent_coin_id)?, - puzzle_hash: to_bytes32(&self.puzzle_hash)?, - amount: to_u64(&self.amount)?, - }, - proof: Proof::Lineage(LineageProof { - parent_parent_coin_info: to_bytes32(&self.parent_parent_coin_id)?, - parent_inner_puzzle_hash: to_bytes32(&self.parent_inner_puzzle_hash)?, - parent_amount: to_u64(&self.parent_amount)?, - }), - info: DidInfo { - launcher_id: to_bytes32(&self.launcher_id)?, - recovery_list_hash: self - .recovery_list_hash - .as_deref() - .map(to_bytes32) - .transpose()?, - num_verifications_required: to_u64(&self.num_verifications_required)?, - metadata: self.metadata.into(), - p2_puzzle_hash: to_bytes32(&self.p2_puzzle_hash)?, - }, - }) - } -} - -impl IntoRow for DidCoinInfoSql { - type Row = DidCoinInfo; - - fn into_row(self) -> Result { - Ok(DidCoinInfo { - coin_id: to_bytes32(&self.coin_id)?, - amount: to_u64(&self.amount)?, - p2_puzzle_hash: to_bytes32(&self.p2_puzzle_hash)?, - recovery_list_hash: self - .recovery_list_hash - .as_deref() - .map(to_bytes32) - .transpose()?, - created_height: self.created_height.map(TryInto::try_into).transpose()?, - transaction_id: self.transaction_id.as_deref().map(to_bytes32).transpose()?, - }) - } -} diff --git a/crates/sage-database/src/rows/nft.rs b/crates/sage-database/src/rows/nft.rs deleted file mode 100644 index 67cf92221..000000000 --- a/crates/sage-database/src/rows/nft.rs +++ /dev/null @@ -1,86 +0,0 @@ -use chia::protocol::Bytes32; -use sqlx::{sqlite::SqliteRow, FromRow, Row}; - -use crate::{to_bytes32, DatabaseError}; - -use super::IntoRow; - -#[allow(unused)] -pub(crate) struct NftSql { - pub launcher_id: Vec, - pub coin_id: Vec, - pub collection_id: Option>, - pub minter_did: Option>, - pub owner_did: Option>, - pub visible: bool, - pub sensitive_content: bool, - pub name: Option, - pub is_owned: bool, - pub created_height: Option, - pub metadata_hash: Option>, - pub is_named: Option, - pub is_pending: Option, - pub edition_number: Option, - pub edition_total: Option, -} - -impl FromRow<'_, SqliteRow> for NftSql { - fn from_row(row: &SqliteRow) -> Result { - Ok(NftSql { - launcher_id: row.try_get("launcher_id")?, - coin_id: row.try_get("coin_id")?, - collection_id: row.try_get("collection_id")?, - minter_did: row.try_get("minter_did")?, - owner_did: row.try_get("owner_did")?, - visible: row.try_get("visible")?, - sensitive_content: row.try_get("sensitive_content")?, - name: row.try_get("name")?, - is_owned: row.try_get("is_owned")?, - created_height: row.try_get("created_height")?, - metadata_hash: row.try_get("metadata_hash")?, - is_named: row.try_get("is_named")?, - is_pending: row.try_get("is_pending")?, - edition_number: row.try_get("edition_number")?, - edition_total: row.try_get("edition_total")?, - }) - } -} - -#[derive(Debug, Clone)] -pub struct NftRow { - pub launcher_id: Bytes32, - pub coin_id: Bytes32, - pub collection_id: Option, - pub minter_did: Option, - pub owner_did: Option, - pub visible: bool, - pub sensitive_content: bool, - pub name: Option, - pub is_owned: bool, - pub created_height: Option, - pub metadata_hash: Option, - pub edition_number: Option, - pub edition_total: Option, -} - -impl IntoRow for NftSql { - type Row = NftRow; - - fn into_row(self) -> Result { - Ok(NftRow { - launcher_id: to_bytes32(&self.launcher_id)?, - coin_id: to_bytes32(&self.coin_id)?, - collection_id: self.collection_id.as_deref().map(to_bytes32).transpose()?, - minter_did: self.minter_did.as_deref().map(to_bytes32).transpose()?, - owner_did: self.owner_did.as_deref().map(to_bytes32).transpose()?, - visible: self.visible, - sensitive_content: self.sensitive_content, - name: self.name, - is_owned: self.is_owned, - created_height: self.created_height.map(TryInto::try_into).transpose()?, - metadata_hash: self.metadata_hash.as_deref().map(to_bytes32).transpose()?, - edition_number: self.edition_number.map(TryInto::try_into).transpose()?, - edition_total: self.edition_total.map(TryInto::try_into).transpose()?, - }) - } -} diff --git a/crates/sage-database/src/rows/nft_coin.rs b/crates/sage-database/src/rows/nft_coin.rs deleted file mode 100644 index 7837b3c15..000000000 --- a/crates/sage-database/src/rows/nft_coin.rs +++ /dev/null @@ -1,53 +0,0 @@ -use chia::{ - protocol::{Coin, Program}, - puzzles::{LineageProof, Proof}, -}; -use chia_wallet_sdk::driver::{Nft, NftInfo}; - -use crate::{to_bytes32, to_u64, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct FullNftCoinSql { - pub parent_coin_id: Vec, - pub puzzle_hash: Vec, - pub amount: Vec, - pub parent_parent_coin_id: Vec, - pub parent_inner_puzzle_hash: Vec, - pub parent_amount: Vec, - pub launcher_id: Vec, - pub metadata: Vec, - pub metadata_updater_puzzle_hash: Vec, - pub current_owner: Option>, - pub royalty_puzzle_hash: Vec, - pub royalty_ten_thousandths: i64, - pub p2_puzzle_hash: Vec, -} - -impl IntoRow for FullNftCoinSql { - type Row = Nft; - - fn into_row(self) -> Result, DatabaseError> { - Ok(Nft { - coin: Coin { - parent_coin_info: to_bytes32(&self.parent_coin_id)?, - puzzle_hash: to_bytes32(&self.puzzle_hash)?, - amount: to_u64(&self.amount)?, - }, - proof: Proof::Lineage(LineageProof { - parent_parent_coin_info: to_bytes32(&self.parent_parent_coin_id)?, - parent_inner_puzzle_hash: to_bytes32(&self.parent_inner_puzzle_hash)?, - parent_amount: to_u64(&self.parent_amount)?, - }), - info: NftInfo { - launcher_id: to_bytes32(&self.launcher_id)?, - metadata: self.metadata.into(), - metadata_updater_puzzle_hash: to_bytes32(&self.metadata_updater_puzzle_hash)?, - current_owner: self.current_owner.as_deref().map(to_bytes32).transpose()?, - royalty_puzzle_hash: to_bytes32(&self.royalty_puzzle_hash)?, - royalty_basis_points: self.royalty_ten_thousandths.try_into()?, - p2_puzzle_hash: to_bytes32(&self.p2_puzzle_hash)?, - }, - }) - } -} diff --git a/crates/sage-database/src/rows/offer.rs b/crates/sage-database/src/rows/offer.rs deleted file mode 100644 index da86fe7c8..000000000 --- a/crates/sage-database/src/rows/offer.rs +++ /dev/null @@ -1,169 +0,0 @@ -use chia::protocol::Bytes32; - -use crate::{to_bytes32, to_u64, DatabaseError}; - -use super::IntoRow; - -pub(crate) struct OfferSql { - pub offer_id: Vec, - pub encoded_offer: String, - pub expiration_height: Option, - pub expiration_timestamp: Option>, - pub fee: Vec, - pub status: i64, - pub inserted_timestamp: Vec, -} - -#[derive(Debug, Clone)] -pub struct OfferRow { - pub offer_id: Bytes32, - pub encoded_offer: String, - pub expiration_height: Option, - pub expiration_timestamp: Option, - pub fee: u64, - pub status: OfferStatus, - pub inserted_timestamp: u64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(u8)] -pub enum OfferStatus { - Active = 0, - Completed = 1, - Cancelled = 2, - Expired = 3, -} - -impl IntoRow for OfferSql { - type Row = OfferRow; - - fn into_row(self) -> Result { - Ok(OfferRow { - offer_id: to_bytes32(&self.offer_id)?, - encoded_offer: self.encoded_offer, - expiration_height: self.expiration_height.map(TryInto::try_into).transpose()?, - expiration_timestamp: self - .expiration_timestamp - .as_deref() - .map(to_u64) - .transpose()?, - fee: to_u64(&self.fee)?, - status: match self.status { - 0 => OfferStatus::Active, - 1 => OfferStatus::Completed, - 2 => OfferStatus::Cancelled, - 3 => OfferStatus::Expired, - _ => return Err(DatabaseError::InvalidOfferStatus(self.status)), - }, - inserted_timestamp: to_u64(&self.inserted_timestamp)?, - }) - } -} - -pub(crate) struct OfferXchSql { - pub offer_id: Vec, - pub requested: bool, - pub amount: Vec, - pub royalty: Vec, -} - -#[derive(Debug, Clone, Copy)] -pub struct OfferXchRow { - pub offer_id: Bytes32, - pub requested: bool, - pub amount: u64, - pub royalty: u64, -} - -impl IntoRow for OfferXchSql { - type Row = OfferXchRow; - - fn into_row(self) -> Result { - Ok(OfferXchRow { - offer_id: to_bytes32(&self.offer_id)?, - requested: self.requested, - amount: to_u64(&self.amount)?, - royalty: to_u64(&self.royalty)?, - }) - } -} - -pub(crate) struct OfferNftSql { - pub offer_id: Vec, - pub requested: bool, - pub launcher_id: Vec, - pub royalty_puzzle_hash: Vec, - pub royalty_ten_thousandths: i64, - pub name: Option, - pub thumbnail: Option>, - pub thumbnail_mime_type: Option, -} - -#[derive(Debug, Clone)] -pub struct OfferNftRow { - pub offer_id: Bytes32, - pub requested: bool, - pub launcher_id: Bytes32, - pub royalty_puzzle_hash: Bytes32, - pub royalty_ten_thousandths: u16, - pub name: Option, - pub thumbnail: Option>, - pub thumbnail_mime_type: Option, -} - -impl IntoRow for OfferNftSql { - type Row = OfferNftRow; - - fn into_row(self) -> Result { - Ok(OfferNftRow { - offer_id: to_bytes32(&self.offer_id)?, - requested: self.requested, - launcher_id: to_bytes32(&self.launcher_id)?, - royalty_puzzle_hash: to_bytes32(&self.royalty_puzzle_hash)?, - royalty_ten_thousandths: self.royalty_ten_thousandths.try_into()?, - name: self.name, - thumbnail: self.thumbnail, - thumbnail_mime_type: self.thumbnail_mime_type, - }) - } -} - -pub(crate) struct OfferCatSql { - pub offer_id: Vec, - pub requested: bool, - pub asset_id: Vec, - pub amount: Vec, - pub royalty: Vec, - pub name: Option, - pub ticker: Option, - pub icon: Option, -} - -#[derive(Debug, Clone)] -pub struct OfferCatRow { - pub offer_id: Bytes32, - pub requested: bool, - pub asset_id: Bytes32, - pub amount: u64, - pub royalty: u64, - pub name: Option, - pub ticker: Option, - pub icon: Option, -} - -impl IntoRow for OfferCatSql { - type Row = OfferCatRow; - - fn into_row(self) -> Result { - Ok(OfferCatRow { - offer_id: to_bytes32(&self.offer_id)?, - requested: self.requested, - asset_id: to_bytes32(&self.asset_id)?, - amount: to_u64(&self.amount)?, - royalty: to_u64(&self.royalty)?, - name: self.name, - ticker: self.ticker, - icon: self.icon, - }) - } -} diff --git a/crates/sage-database/src/tables.rs b/crates/sage-database/src/tables.rs new file mode 100644 index 000000000..30c5d3638 --- /dev/null +++ b/crates/sage-database/src/tables.rs @@ -0,0 +1,18 @@ +mod assets; +mod blocks; +mod coins; +mod collections; +mod files; +mod mempool_items; +mod offers; +mod p2_puzzles; +mod transactions; + +pub use assets::*; +pub use coins::*; +pub use collections::*; +pub use files::*; +pub use mempool_items::*; +pub use offers::*; +pub use p2_puzzles::*; +pub use transactions::*; diff --git a/crates/sage-database/src/tables/assets.rs b/crates/sage-database/src/tables/assets.rs new file mode 100644 index 000000000..312a2b5bb --- /dev/null +++ b/crates/sage-database/src/tables/assets.rs @@ -0,0 +1,8 @@ +mod asset; +mod cat; +mod did; +mod nft; + +pub use asset::*; +pub use did::*; +pub use nft::*; diff --git a/crates/sage-database/src/tables/assets/asset.rs b/crates/sage-database/src/tables/assets/asset.rs new file mode 100644 index 000000000..b6136d651 --- /dev/null +++ b/crates/sage-database/src/tables/assets/asset.rs @@ -0,0 +1,177 @@ +use chia::protocol::Bytes32; +use sqlx::{query, SqliteExecutor}; + +use crate::{Convert, Database, DatabaseError, DatabaseTx, Result}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AssetKind { + Token, + Nft, + Did, + Option, +} + +impl Convert for i64 { + fn convert(self) -> Result { + Ok(match self { + 0 => AssetKind::Token, + 1 => AssetKind::Nft, + 2 => AssetKind::Did, + 3 => AssetKind::Option, + _ => return Err(DatabaseError::InvalidEnumVariant), + }) + } +} + +#[derive(Debug, Clone)] +pub struct Asset { + pub hash: Bytes32, + pub name: Option, + pub ticker: Option, + pub precision: u8, + pub icon_url: Option, + pub description: Option, + pub is_sensitive_content: bool, + pub is_visible: bool, + pub kind: AssetKind, +} + +impl Asset { + pub fn default_cat(hash: Bytes32) -> Self { + Self { + hash, + name: None, + ticker: None, + precision: 3, + icon_url: None, + description: None, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Token, + } + } +} + +impl Database { + pub async fn insert_asset(&self, asset: Asset) -> Result<()> { + insert_asset(&self.pool, asset).await?; + + Ok(()) + } + + pub async fn update_asset(&self, asset: Asset) -> Result<()> { + let hash = asset.hash.as_ref(); + let kind = asset.kind as i64; + + query!( + " + UPDATE assets SET + kind = ?, + name = ?, + ticker = ?, + precision = ?, + icon_url = ?, + description = ?, + is_sensitive_content = ?, + is_visible = ? + WHERE hash = ? + ", + kind, + asset.name, + asset.ticker, + asset.precision, + asset.icon_url, + asset.description, + asset.is_sensitive_content, + asset.is_visible, + hash, + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn asset_kind(&self, hash: Bytes32) -> Result> { + let hash = hash.as_ref(); + + query!("SELECT kind FROM assets WHERE hash = ?", hash) + .fetch_optional(&self.pool) + .await? + .map(|row| row.kind.convert()) + .transpose() + } + + pub async fn asset(&self, hash: Bytes32) -> Result> { + let hash = hash.as_ref(); + + query!( + " + SELECT + hash, kind, name, ticker, precision, icon_url, description, + is_sensitive_content, is_visible + FROM assets + WHERE hash = ? + ", + hash + ) + .fetch_optional(&self.pool) + .await? + .map(|row| { + Ok(Asset { + hash: row.hash.convert()?, + kind: row.kind.convert()?, + name: row.name, + ticker: row.ticker, + precision: row.precision.convert()?, + icon_url: row.icon_url, + description: row.description, + is_sensitive_content: row.is_sensitive_content, + is_visible: row.is_visible, + }) + }) + .transpose() + } +} + +impl DatabaseTx<'_> { + pub async fn insert_asset(&mut self, asset: Asset) -> Result<()> { + insert_asset(&mut *self.tx, asset).await?; + + Ok(()) + } +} + +async fn insert_asset(conn: impl SqliteExecutor<'_>, asset: Asset) -> Result<()> { + let hash = asset.hash.as_ref(); + let kind = asset.kind as i64; + + query!( + " + INSERT INTO assets ( + hash, kind, name, ticker, precision, icon_url, description, + is_sensitive_content, is_visible + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hash) DO UPDATE SET + name = COALESCE(name, excluded.name), + ticker = COALESCE(ticker, excluded.ticker), + icon_url = COALESCE(icon_url, excluded.icon_url), + description = COALESCE(description, excluded.description), + is_sensitive_content = is_sensitive_content OR excluded.is_sensitive_content + ", + hash, + kind, + asset.name, + asset.ticker, + asset.precision, + asset.icon_url, + asset.description, + asset.is_sensitive_content, + asset.is_visible, + ) + .execute(conn) + .await?; + + Ok(()) +} diff --git a/crates/sage-database/src/tables/assets/cat.rs b/crates/sage-database/src/tables/assets/cat.rs new file mode 100644 index 000000000..01b98c555 --- /dev/null +++ b/crates/sage-database/src/tables/assets/cat.rs @@ -0,0 +1,70 @@ +use sqlx::query; + +use crate::{Asset, AssetKind, Convert, Database, Result}; + +impl Database { + pub async fn all_cats(&self) -> Result> { + query!( + " + SELECT + hash, name, icon_url, description, ticker, precision, + is_visible, is_sensitive_content + FROM assets + WHERE assets.kind = 0 AND assets.id != 0 + ORDER BY name ASC + " + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|row| { + Ok(Asset { + hash: row.hash.convert()?, + name: row.name, + ticker: row.ticker, + precision: row.precision.convert()?, + icon_url: row.icon_url, + description: row.description, + is_visible: row.is_visible, + is_sensitive_content: row.is_sensitive_content, + kind: AssetKind::Token, + }) + }) + .collect() + } + + pub async fn owned_cats(&self) -> Result> { + query!( + " + SELECT + hash, name, icon_url, description, ticker, precision, + is_visible, is_sensitive_content + FROM assets + WHERE assets.kind = 0 AND assets.id != 0 + AND EXISTS ( + SELECT 1 FROM coins + INNER JOIN p2_puzzles ON p2_puzzles.id = coins.p2_puzzle_id + WHERE coins.asset_id = assets.id + ) + ORDER BY name ASC + " + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|row| { + Ok(Asset { + hash: row.hash.convert()?, + name: row.name, + ticker: row.ticker, + precision: row.precision.convert()?, + icon_url: row.icon_url, + description: row.description, + is_visible: row.is_visible, + is_sensitive_content: row.is_sensitive_content, + kind: AssetKind::Token, + }) + }) + .collect() + } +} diff --git a/crates/sage-database/src/tables/assets/did.rs b/crates/sage-database/src/tables/assets/did.rs new file mode 100644 index 000000000..77b086323 --- /dev/null +++ b/crates/sage-database/src/tables/assets/did.rs @@ -0,0 +1,130 @@ +use chia::protocol::{Bytes32, Coin, Program}; +use sqlx::query; + +use crate::{Asset, AssetKind, CoinKind, CoinRow, Convert, Database, DatabaseTx, Result}; + +#[derive(Debug, Clone)] +pub struct DidCoinInfo { + pub metadata: Program, + pub recovery_list_hash: Option, + pub num_verifications_required: u64, +} + +#[derive(Debug, Clone)] +pub struct DidRow { + pub asset: Asset, + pub did_info: DidCoinInfo, + pub coin_row: CoinRow, +} + +impl Database { + pub async fn owned_dids(&self) -> Result> { + query!( + " + SELECT + asset_hash, asset_name, asset_ticker, asset_precision, asset_icon_url, asset_description, + asset_is_visible, asset_is_sensitive_content, + owned_coins.created_height, spent_height, + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + metadata, recovery_list_hash, num_verifications_required, + offer_hash, created_timestamp, spent_timestamp, + clawback_expiration_seconds AS clawback_timestamp + FROM owned_coins + INNER JOIN dids ON dids.asset_id = owned_coins.asset_id + ORDER BY asset_name ASC + " + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|row| { + Ok(DidRow { + asset: Asset { + hash: row.asset_hash.convert()?, + name: row.asset_name, + ticker: row.asset_ticker, + precision: row.asset_precision.convert()?, + icon_url: row.asset_icon_url, + description: row.asset_description, + is_visible: row.asset_is_visible, + is_sensitive_content: row.asset_is_sensitive_content, + kind: AssetKind::Did, + }, + did_info: DidCoinInfo { + metadata: row.metadata.into(), + recovery_list_hash: row.recovery_list_hash.convert()?, + num_verifications_required: row.num_verifications_required.convert()?, + }, + coin_row: CoinRow { + coin: Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + p2_puzzle_hash: row.p2_puzzle_hash.convert()?, + kind: CoinKind::Did, + mempool_item_hash: None, + offer_hash: row.offer_hash.convert()?, + clawback_timestamp: row.clawback_timestamp.convert()?, + created_height: row.created_height.convert()?, + spent_height: row.spent_height.convert()?, + created_timestamp: row.created_timestamp.convert()?, + spent_timestamp: row.spent_timestamp.convert()?, + }, + }) + }) + .collect() + } +} + +impl DatabaseTx<'_> { + pub async fn insert_did(&mut self, hash: Bytes32, coin_info: &DidCoinInfo) -> Result<()> { + let hash = hash.as_ref(); + let metadata = coin_info.metadata.as_slice(); + let recovery_list_hash = coin_info.recovery_list_hash.as_deref(); + let num_verifications_required: i64 = coin_info.num_verifications_required.try_into()?; + + query!( + " + INSERT OR IGNORE INTO dids ( + asset_id, metadata, recovery_list_hash, num_verifications_required + ) + VALUES ((SELECT id FROM assets WHERE hash = ?), ?, ?, ?) + ", + hash, + metadata, + recovery_list_hash, + num_verifications_required + ) + .execute(&mut *self.tx) + .await?; + + Ok(()) + } + + pub async fn update_did(&mut self, hash: Bytes32, coin_info: &DidCoinInfo) -> Result<()> { + let hash = hash.as_ref(); + let metadata = coin_info.metadata.as_slice(); + let recovery_list_hash = coin_info.recovery_list_hash.as_deref(); + let num_verifications_required: i64 = coin_info.num_verifications_required.try_into()?; + + query!( + " + UPDATE dids + SET + metadata = ?, + recovery_list_hash = ?, + num_verifications_required = ? + WHERE asset_id = (SELECT id FROM assets WHERE hash = ?) + ", + metadata, + recovery_list_hash, + num_verifications_required, + hash + ) + .execute(&mut *self.tx) + .await?; + + Ok(()) + } +} diff --git a/crates/sage-database/src/tables/assets/nft.rs b/crates/sage-database/src/tables/assets/nft.rs new file mode 100644 index 000000000..25b5afe7e --- /dev/null +++ b/crates/sage-database/src/tables/assets/nft.rs @@ -0,0 +1,512 @@ +use chia::protocol::{Bytes32, Coin, Program}; +use sqlx::{query, Row}; + +use crate::{Asset, AssetKind, CoinKind, CoinRow, Convert, Database, DatabaseTx, Result}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NftSortMode { + Recent, + Name, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NftGroupSearch { + Collection(Bytes32), + NoCollection, + MinterDid(Bytes32), + NoMinterDid, + OwnerDid(Bytes32), + NoOwnerDid, +} + +#[derive(Debug, Clone)] +pub struct NftCoinInfo { + pub collection_hash: Bytes32, + pub collection_name: Option, + pub minter_hash: Option, + pub owner_hash: Option, + pub metadata: Program, + pub metadata_updater_puzzle_hash: Bytes32, + pub royalty_puzzle_hash: Bytes32, + pub royalty_basis_points: u16, + pub data_hash: Option, + pub metadata_hash: Option, + pub license_hash: Option, + pub edition_number: Option, + pub edition_total: Option, +} + +#[derive(Debug, Clone)] +pub struct NftRow { + pub asset: Asset, + pub nft_info: NftCoinInfo, + pub coin_row: CoinRow, +} + +#[derive(Debug, Clone)] +pub struct NftMetadataInfo { + pub name: Option, + pub description: Option, + pub is_sensitive_content: bool, + pub collection_id: Bytes32, +} + +#[derive(Debug, Clone)] +pub struct NftOfferInfo { + pub metadata: Program, + pub metadata_updater_puzzle_hash: Bytes32, + pub royalty_puzzle_hash: Bytes32, + pub royalty_basis_points: u16, +} + +impl Database { + pub async fn owned_nft(&self, hash: Bytes32) -> Result> { + let hash = hash.as_ref(); + + query!( + " + SELECT + asset_hash, asset_name, asset_ticker, asset_precision, asset_icon_url, + asset_description, asset_is_sensitive_content, asset_is_visible, + collections.hash AS 'collection_hash?', collections.name AS collection_name, + owned_nfts.minter_hash, owner_hash, metadata, metadata_updater_puzzle_hash, + royalty_puzzle_hash, royalty_basis_points, data_hash, metadata_hash, license_hash, + edition_number, edition_total, + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, created_height, spent_height, + offer_hash AS 'offer_hash?', created_timestamp, spent_timestamp, clawback_expiration_seconds AS 'clawback_timestamp?' + FROM owned_nfts + LEFT JOIN collections ON collections.id = owned_nfts.collection_id + WHERE owned_nfts.asset_hash = ? + ", + hash + ) + .fetch_optional(&self.pool) + .await? + .map(|row| { + Ok(NftRow { + asset: Asset { + hash: row.asset_hash.convert()?, + name: row.asset_name, + ticker: row.asset_ticker, + precision: row.asset_precision.convert()?, + icon_url: row.asset_icon_url, + description: row.asset_description, + is_sensitive_content: row.asset_is_sensitive_content, + is_visible: row.asset_is_visible, + kind: AssetKind::Nft, + }, + nft_info: NftCoinInfo { + collection_hash: row.collection_hash.convert()?.unwrap_or_default(), + collection_name: row.collection_name, + minter_hash: row.minter_hash.convert()?, + owner_hash: row.owner_hash.convert()?, + metadata: row.metadata.into(), + metadata_updater_puzzle_hash: row.metadata_updater_puzzle_hash.convert()?, + royalty_puzzle_hash: row.royalty_puzzle_hash.convert()?, + royalty_basis_points: row.royalty_basis_points.convert()?, + data_hash: row.data_hash.convert()?, + metadata_hash: row.metadata_hash.convert()?, + license_hash: row.license_hash.convert()?, + edition_number: row.edition_number.convert()?, + edition_total: row.edition_total.convert()?, + }, + coin_row: CoinRow { + coin: Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + p2_puzzle_hash: row.p2_puzzle_hash.convert()?, + kind: CoinKind::Nft, + mempool_item_hash: None, + offer_hash: row.offer_hash.convert()?, + clawback_timestamp: row.clawback_timestamp.convert()?, + created_height: row.created_height.convert()?, + spent_height: row.spent_height.convert()?, + created_timestamp: row.created_timestamp.convert()?, + spent_timestamp: row.spent_timestamp.convert()?, + }, + }) + }) + .transpose() + } + + pub async fn owned_nfts( + &self, + name_search: Option, + group_search: Option, + sort_mode: NftSortMode, + include_hidden: bool, + limit: u32, + offset: u32, + ) -> Result<(Vec, u32)> { + let mut query = sqlx::QueryBuilder::new( + " + SELECT + asset_hash, asset_name, asset_ticker, asset_precision, asset_icon_url, + asset_description, asset_is_sensitive_content, + asset_is_visible AND (collections.id IS NULL OR collections.is_visible) as is_visible, + collections.hash AS collection_hash, collections.name AS collection_name, + owned_nfts.minter_hash, owner_hash, metadata, metadata_updater_puzzle_hash, + royalty_puzzle_hash, royalty_basis_points, data_hash, metadata_hash, license_hash, + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, edition_number, edition_total, + created_height, spent_height, offer_hash, created_timestamp, spent_timestamp, + clawback_expiration_seconds AS clawback_timestamp, COUNT(*) OVER() as total_count + FROM owned_nfts + LEFT JOIN collections ON collections.id = owned_nfts.collection_id + WHERE 1=1 + ", + ); + + if !include_hidden { + query.push("AND (asset_is_visible = 1 AND (owned_nfts.collection_id IS NULL OR collections.is_visible = 1))"); + } + + if let Some(name_search) = name_search { + query.push("AND asset_name LIKE "); + query.push_bind(format!("%{name_search}%")); + } + + if let Some(group) = group_search { + match group { + NftGroupSearch::Collection(id) => { + query.push(" AND collections.hash = "); + query.push_bind(id.as_ref().to_vec()); + } + NftGroupSearch::NoCollection => { + query.push(" AND collections.hash IS NULL"); + } + NftGroupSearch::MinterDid(id) => { + query.push(" AND owned_nfts.minter_hash = "); + query.push_bind(id.as_ref().to_vec()); + } + NftGroupSearch::NoMinterDid => { + query.push(" AND owned_nfts.minter_hash IS NULL"); + } + NftGroupSearch::OwnerDid(id) => { + query.push(" AND owned_nfts.owner_hash = "); + query.push_bind(id.as_ref().to_vec()); + } + NftGroupSearch::NoOwnerDid => { + query.push(" AND owner_hash IS NULL"); + } + } + } + // Add ORDER BY clause based on sort_mode + query.push(" ORDER BY "); + + match sort_mode { + NftSortMode::Recent => { + query.push("(created_height IS NULL) DESC, created_height DESC"); + } + NftSortMode::Name => { + query.push("(asset_name IS NULL) ASC, asset_name ASC, edition_number ASC"); + } + } + + query.push(" LIMIT "); + query.push_bind(limit); + query.push(" OFFSET "); + query.push_bind(offset); + let query = query.build(); + + let rows = query.fetch_all(&self.pool).await?; + let total_count = rows + .first() + .map_or(Ok(0), |row| row.get::("total_count").try_into())?; + + let nfts = rows + .into_iter() + .map(|row| { + Ok(NftRow { + asset: Asset { + hash: row.get::, _>("asset_hash").convert()?, + name: row.get::, _>("asset_name"), + ticker: row.get::, _>("asset_ticker"), + precision: row.get::("asset_precision").convert()?, + icon_url: row.get::, _>("asset_icon_url"), + description: row.get::, _>("asset_description"), + is_visible: row.get::("is_visible"), + is_sensitive_content: row.get::("asset_is_sensitive_content"), + kind: AssetKind::Nft, + }, + nft_info: NftCoinInfo { + collection_hash: row + .get::>, _>("collection_hash") + .convert()? + .unwrap_or_default(), + collection_name: row.get::, _>("collection_name"), + minter_hash: row.get::>, _>("minter_hash").convert()?, + owner_hash: row.get::>, _>("owner_hash").convert()?, + metadata: row.get::, _>("metadata").into(), + metadata_updater_puzzle_hash: row + .get::, _>("metadata_updater_puzzle_hash") + .convert()?, + royalty_puzzle_hash: row + .get::, _>("royalty_puzzle_hash") + .convert()?, + royalty_basis_points: row + .get::("royalty_basis_points") + .convert()?, + data_hash: row.get::>, _>("data_hash").convert()?, + metadata_hash: row.get::>, _>("metadata_hash").convert()?, + license_hash: row.get::>, _>("license_hash").convert()?, + edition_number: row.get::, _>("edition_number").convert()?, + edition_total: row.get::, _>("edition_total").convert()?, + }, + coin_row: CoinRow { + coin: Coin::new( + row.get::, _>("parent_coin_hash").convert()?, + row.get::, _>("puzzle_hash").convert()?, + row.get::, _>("amount").convert()?, + ), + p2_puzzle_hash: row.get::, _>("p2_puzzle_hash").convert()?, + kind: CoinKind::Nft, + mempool_item_hash: None, + offer_hash: row.get::>, _>("offer_hash").convert()?, + clawback_timestamp: row + .get::, _>("clawback_timestamp") + .convert()?, + created_height: row.get::, _>("created_height").convert()?, + spent_height: row.get::, _>("spent_height").convert()?, + created_timestamp: row + .get::, _>("created_timestamp") + .convert()?, + spent_timestamp: row.get::, _>("spent_timestamp").convert()?, + }, + }) + }) + .collect::>>()?; + + Ok((nfts, total_count)) + } + + pub async fn distinct_minter_dids( + &self, + limit: u32, + offset: u32, + ) -> Result<(Vec, u32)> { + let rows = query!( + "SELECT DISTINCT minter_hash + FROM owned_nfts + WHERE minter_hash IS NOT NULL + ORDER BY minter_hash ASC + LIMIT ? OFFSET ?", + limit, + offset + ) + .fetch_all(&self.pool) + .await?; + + let total_count = query!( + "SELECT COUNT(DISTINCT minter_hash) AS total_count + FROM owned_nfts + WHERE minter_hash IS NOT NULL" + ) + .fetch_one(&self.pool) + .await? + .total_count + .try_into() + .map_err(crate::DatabaseError::PrecisionLost)?; + + let dids = rows + .into_iter() + .filter_map(|row| row.minter_hash.convert().transpose()) + .collect::>>()?; + + Ok((dids, total_count)) + } + + pub async fn offer_nft_info(&self, hash: Bytes32) -> Result> { + let hash = hash.as_ref(); + + query!( + " + SELECT + metadata, metadata_updater_puzzle_hash, royalty_puzzle_hash, royalty_basis_points + FROM nfts + INNER JOIN assets ON assets.id = nfts.asset_id + WHERE hash = ? + ", + hash + ) + .fetch_optional(&self.pool) + .await? + .map(|row| { + Ok(NftOfferInfo { + metadata: Program::from(row.metadata), + metadata_updater_puzzle_hash: row.metadata_updater_puzzle_hash.convert()?, + royalty_puzzle_hash: row.royalty_puzzle_hash.convert()?, + royalty_basis_points: row.royalty_basis_points.convert()?, + }) + }) + .transpose() + } +} + +impl DatabaseTx<'_> { + pub async fn insert_nft(&mut self, hash: Bytes32, coin_info: &NftCoinInfo) -> Result<()> { + let hash = hash.as_ref(); + let collection_id = coin_info.collection_hash.as_ref(); + let minter_hash = coin_info.minter_hash.as_deref(); + let owner_hash = coin_info.owner_hash.as_deref(); + let metadata = coin_info.metadata.as_slice(); + let metadata_updater_puzzle_hash = coin_info.metadata_updater_puzzle_hash.as_ref(); + let royalty_puzzle_hash = coin_info.royalty_puzzle_hash.as_ref(); + let data_hash = coin_info.data_hash.as_deref(); + let metadata_hash = coin_info.metadata_hash.as_deref(); + let license_hash = coin_info.license_hash.as_deref(); + let edition_number: Option = coin_info + .edition_number + .map(TryInto::try_into) + .transpose()?; + let edition_total: Option = + coin_info.edition_total.map(TryInto::try_into).transpose()?; + + query!( + " + INSERT OR IGNORE INTO nfts ( + asset_id, collection_id, minter_hash, owner_hash, metadata, metadata_updater_puzzle_hash, + royalty_puzzle_hash, royalty_basis_points, data_hash, metadata_hash, license_hash, + edition_number, edition_total + ) + VALUES ((SELECT id FROM assets WHERE hash = ?), (SELECT id FROM collections WHERE hash = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ", + hash, + collection_id, + minter_hash, + owner_hash, + metadata, + metadata_updater_puzzle_hash, + royalty_puzzle_hash, + coin_info.royalty_basis_points, + data_hash, + metadata_hash, + license_hash, + edition_number, + edition_total + ) + .execute(&mut *self.tx) + .await?; + + Ok(()) + } + + pub async fn update_nft(&mut self, hash: Bytes32, coin_info: &NftCoinInfo) -> Result<()> { + let hash = hash.as_ref(); + let collection_hash = coin_info.collection_hash.as_ref(); + let minter_hash = coin_info.minter_hash.as_deref(); + let owner_hash = coin_info.owner_hash.as_deref(); + let metadata = coin_info.metadata.as_slice(); + let metadata_updater_puzzle_hash = coin_info.metadata_updater_puzzle_hash.as_ref(); + let royalty_puzzle_hash = coin_info.royalty_puzzle_hash.as_ref(); + let data_hash = coin_info.data_hash.as_deref(); + let metadata_hash = coin_info.metadata_hash.as_deref(); + let license_hash = coin_info.license_hash.as_deref(); + let edition_number: Option = coin_info + .edition_number + .map(TryInto::try_into) + .transpose()?; + let edition_total: Option = + coin_info.edition_total.map(TryInto::try_into).transpose()?; + + query!( + " + UPDATE nfts + SET + collection_id = (SELECT id FROM collections WHERE hash = ?), + minter_hash = ?, + owner_hash = ?, + metadata = ?, + metadata_updater_puzzle_hash = ?, + royalty_puzzle_hash = ?, + royalty_basis_points = ?, + data_hash = ?, + metadata_hash = ?, + license_hash = ?, + edition_number = ?, + edition_total = ? + WHERE asset_id = (SELECT id FROM assets WHERE hash = ?) + ", + collection_hash, + minter_hash, + owner_hash, + metadata, + metadata_updater_puzzle_hash, + royalty_puzzle_hash, + coin_info.royalty_basis_points, + data_hash, + metadata_hash, + license_hash, + edition_number, + edition_total, + hash + ) + .execute(&mut *self.tx) + .await?; + + Ok(()) + } + + pub async fn update_nft_data_hash_urls( + &mut self, + data_hash: Bytes32, + icon_url: String, + ) -> Result<()> { + let data_hash = data_hash.as_ref(); + + query!( + " + UPDATE assets SET icon_url = ? + WHERE assets.id IN ( + SELECT asset_id FROM nfts + WHERE data_hash = ? + ) + ", + icon_url, + data_hash + ) + .execute(&mut *self.tx) + .await?; + + Ok(()) + } + + pub async fn update_nft_metadata( + &mut self, + hash: Bytes32, + metadata_info: NftMetadataInfo, + ) -> Result<()> { + let hash = hash.as_ref(); + let collection_id = metadata_info.collection_id.as_ref(); + + query!( + " + UPDATE assets SET + name = ?, + description = ?, + is_sensitive_content = ? + WHERE hash = ? + ", + metadata_info.name, + metadata_info.description, + metadata_info.is_sensitive_content, + hash + ) + .execute(&mut *self.tx) + .await?; + + query!( + " + UPDATE nfts SET collection_id = (SELECT id FROM collections WHERE hash = ?) + WHERE asset_id = (SELECT id FROM assets WHERE hash = ?) + ", + collection_id, + hash + ) + .execute(&mut *self.tx) + .await?; + + Ok(()) + } +} diff --git a/crates/sage-database/src/tables/blocks.rs b/crates/sage-database/src/tables/blocks.rs new file mode 100644 index 000000000..9cfa448a4 --- /dev/null +++ b/crates/sage-database/src/tables/blocks.rs @@ -0,0 +1,109 @@ +use crate::{Convert, Database, DatabaseTx, Result}; +use chia::protocol::Bytes32; +use sqlx::SqliteExecutor; + +impl Database { + pub async fn unsynced_blocks(&self, limit: u32) -> Result> { + unsynced_blocks(&self.pool, limit).await + } + + pub async fn insert_block( + &self, + height: u32, + header_hash: Bytes32, + timestamp: Option, + is_peak: bool, + ) -> Result<()> { + insert_block(&self.pool, height, header_hash, timestamp, is_peak).await + } + + pub async fn latest_peak(&self) -> Result> { + latest_peak(&self.pool).await + } +} + +impl DatabaseTx<'_> { + pub async fn insert_height(&mut self, height: u32) -> Result<()> { + insert_height(&mut *self.tx, height).await + } +} + +async fn insert_height(conn: impl SqliteExecutor<'_>, height: u32) -> Result<()> { + sqlx::query!( + "INSERT OR IGNORE INTO blocks (height, is_peak) VALUES (?, FALSE)", + height + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn unsynced_blocks(conn: impl SqliteExecutor<'_>, limit: u32) -> Result> { + let row = sqlx::query!( + " + SELECT created_height AS height FROM coins + INNER JOIN blocks ON blocks.height = coins.created_height + WHERE blocks.timestamp IS NULL + UNION + SELECT spent_height AS height FROM coins + INNER JOIN blocks ON blocks.height = coins.spent_height + WHERE blocks.timestamp IS NULL + ORDER BY height DESC + LIMIT ? + ", + limit + ) + .fetch_all(conn) + .await?; + + row.into_iter() + .filter_map(|r| r.height.convert().transpose()) + .collect() +} + +async fn insert_block( + conn: impl SqliteExecutor<'_>, + height: u32, + header_hash: Bytes32, + unix_timestamp: Option, + is_peak: bool, +) -> Result<()> { + let header_hash = header_hash.as_ref(); + sqlx::query!( + " + INSERT INTO blocks (height, timestamp, header_hash, is_peak) VALUES (?, ?, ?, ?) + ON CONFLICT (height) DO UPDATE SET + timestamp = COALESCE(excluded.timestamp, timestamp), + header_hash = excluded.header_hash, + is_peak = (excluded.is_peak OR is_peak) + ", + height, + unix_timestamp, + header_hash, + is_peak + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn latest_peak(conn: impl SqliteExecutor<'_>) -> Result> { + sqlx::query!( + " + SELECT height, header_hash + FROM blocks + WHERE header_hash IS NOT NULL AND is_peak = TRUE + ORDER BY height DESC + LIMIT 1 + " + ) + .fetch_optional(conn) + .await? + .and_then(|row| { + row.header_hash + .map(|hash| Ok((row.height.convert()?, hash.convert()?))) + }) + .transpose() +} diff --git a/crates/sage-database/src/tables/coins.rs b/crates/sage-database/src/tables/coins.rs new file mode 100644 index 000000000..88c469448 --- /dev/null +++ b/crates/sage-database/src/tables/coins.rs @@ -0,0 +1,1265 @@ +use chia::{ + protocol::{Bytes32, Coin, CoinState, Program}, + puzzles::{LineageProof, Proof}, +}; +use chia_wallet_sdk::driver::{ + Cat, CatInfo, Did, DidInfo, Nft, NftInfo, OptionContract, OptionInfo, +}; +use sqlx::{query, Row, SqliteExecutor}; + +use crate::{AssetKind, Convert, Database, DatabaseError, DatabaseTx, Result}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CoinKind { + Xch, + Cat, + Did, + Option, + Nft, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CoinSortMode { + CoinId, + Amount, + CreatedHeight, + SpentHeight, + ClawbackTimestamp, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CoinFilterMode { + All, + Selectable, + Owned, + Spent, + Clawback, +} + +#[derive(Debug, Clone, Copy)] +pub enum AssetFilter { + Id(Bytes32), + Nfts, + Dids, +} + +#[derive(Debug, Clone, Copy)] +pub struct CoinRow { + pub coin: Coin, + pub p2_puzzle_hash: Bytes32, + pub kind: CoinKind, + pub mempool_item_hash: Option, + pub offer_hash: Option, + pub clawback_timestamp: Option, + pub created_height: Option, + pub spent_height: Option, + pub created_timestamp: Option, + pub spent_timestamp: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct UnsyncedCoin { + pub coin_state: CoinState, + pub is_asset_unsynced: bool, + pub is_children_unsynced: bool, +} + +impl Database { + pub async fn coins_by_ids(&self, coin_ids: &[String]) -> Result> { + coins_by_ids(&self.pool, coin_ids).await + } + + pub async fn coin_records( + &self, + asset_filter: AssetFilter, + limit: u32, + offset: u32, + sort_mode: CoinSortMode, + ascending: bool, + filter_mode: CoinFilterMode, + ) -> Result<(Vec, u32)> { + coin_records( + &self.pool, + asset_filter, + limit, + offset, + sort_mode, + ascending, + filter_mode, + ) + .await + } + + pub async fn are_coins_spendable(&self, coin_ids: &[String]) -> Result { + are_coins_spendable(&self.pool, coin_ids).await + } + + pub async fn total_coin_count(&self) -> Result { + total_coin_count(&self.pool).await + } + + pub async fn selectable_xch_coin_count(&self) -> Result { + selectable_coin_count(&self.pool, Bytes32::default()).await + } + + pub async fn selectable_cat_coin_count(&self, asset_id: Bytes32) -> Result { + selectable_coin_count(&self.pool, asset_id).await + } + + pub async fn synced_coin_count(&self) -> Result { + synced_coin_count(&self.pool).await + } + + pub async fn unsynced_coins(&self, limit: usize) -> Result> { + unsynced_coins(&self.pool, limit).await + } + + pub async fn update_coin( + &self, + coin_id: Bytes32, + asset_hash: Bytes32, + p2_puzzle_hash: Bytes32, + hidden_puzzle_hash: Option, + ) -> Result<()> { + update_coin( + &self.pool, + coin_id, + asset_hash, + p2_puzzle_hash, + hidden_puzzle_hash, + ) + .await + } + + pub async fn subscription_coin_ids(&self) -> Result> { + subscription_coin_ids(&self.pool).await + } + + pub async fn xch_balance(&self) -> Result { + token_balance(&self.pool, Bytes32::default()).await + } + + pub async fn cat_balance(&self, asset_id: Bytes32) -> Result { + token_balance(&self.pool, asset_id).await + } + + pub async fn selectable_xch_balance(&self) -> Result { + selectable_token_balance(&self.pool, Bytes32::default()).await + } + + pub async fn selectable_cat_balance(&self, asset_id: Bytes32) -> Result { + selectable_token_balance(&self.pool, asset_id).await + } + + pub async fn selectable_xch_coins(&self) -> Result> { + selectable_xch_coins(&self.pool).await + } + + pub async fn selectable_cat_coins(&self, asset_id: Bytes32) -> Result> { + selectable_cat_coins(&self.pool, asset_id).await + } + + pub async fn coin_kind(&self, coin_id: Bytes32) -> Result> { + coin_kind(&self.pool, coin_id).await + } + + pub async fn xch_coin(&self, coin_id: Bytes32) -> Result> { + xch_coin(&self.pool, coin_id).await + } + + pub async fn cat_coin(&self, coin_id: Bytes32) -> Result> { + cat_coin(&self.pool, coin_id).await + } + + pub async fn did_coin(&self, coin_id: Bytes32) -> Result>> { + did_coin(&self.pool, coin_id).await + } + + pub async fn nft_coin(&self, coin_id: Bytes32) -> Result>> { + nft_coin(&self.pool, coin_id).await + } + + pub async fn option_coin(&self, coin_id: Bytes32) -> Result> { + option_coin(&self.pool, coin_id).await + } + + pub async fn did(&self, launcher_id: Bytes32) -> Result>> { + did(&self.pool, launcher_id).await + } + + pub async fn spendable_did(&self, launcher_id: Bytes32) -> Result>> { + spendable_did(&self.pool, launcher_id).await + } + + pub async fn nft(&self, launcher_id: Bytes32) -> Result>> { + nft(&self.pool, launcher_id).await + } + + pub async fn spendable_nft(&self, launcher_id: Bytes32) -> Result>> { + spendable_nft(&self.pool, launcher_id).await + } + + pub async fn option(&self, launcher_id: Bytes32) -> Result> { + option(&self.pool, launcher_id).await + } + + pub async fn spendable_option(&self, launcher_id: Bytes32) -> Result> { + spendable_option(&self.pool, launcher_id).await + } +} + +impl DatabaseTx<'_> { + pub async fn insert_coin(&mut self, coin_state: CoinState) -> Result<()> { + insert_coin(&mut *self.tx, coin_state).await + } + + pub async fn is_known_coin(&mut self, coin_id: Bytes32) -> Result { + is_known_coin(&mut *self.tx, coin_id).await + } + + pub async fn update_coin( + &mut self, + coin_id: Bytes32, + asset_hash: Bytes32, + p2_puzzle_hash: Bytes32, + hidden_puzzle_hash: Option, + ) -> Result<()> { + update_coin( + &mut *self.tx, + coin_id, + asset_hash, + p2_puzzle_hash, + hidden_puzzle_hash, + ) + .await + } + + pub async fn set_children_synced(&mut self, coin_id: Bytes32) -> Result<()> { + set_children_synced(&mut *self.tx, coin_id).await + } + + pub async fn set_transaction_children_unsynced( + &mut self, + mempool_item_id: Bytes32, + ) -> Result<()> { + set_transaction_children_unsynced(&mut *self.tx, mempool_item_id).await + } + + pub async fn delete_coin(&mut self, coin_id: Bytes32) -> Result<()> { + delete_coin(&mut *self.tx, coin_id).await + } + + pub async fn insert_lineage_proof( + &mut self, + coin_id: Bytes32, + lineage_proof: LineageProof, + ) -> Result<()> { + insert_lineage_proof(&mut *self.tx, coin_id, lineage_proof).await + } +} + +async fn are_coins_spendable(conn: impl SqliteExecutor<'_>, coin_ids: &[String]) -> Result { + if coin_ids.is_empty() { + return Ok(false); + } + + let mut query = sqlx::QueryBuilder::new( + " + SELECT COUNT(*) AS count + FROM spendable_coins + WHERE 1=1 + AND coin_hash IN (", + ); + + let mut separated = query.separated(", "); + for coin_id in coin_ids { + separated.push(format!("X'{coin_id}'")); + } + separated.push_unseparated(")"); + + let count: i64 = query.build().fetch_one(conn).await?.get("count"); + + #[allow(clippy::cast_possible_wrap)] + Ok(count == coin_ids.len() as i64) +} + +async fn insert_coin(conn: impl SqliteExecutor<'_>, coin_state: CoinState) -> Result<()> { + let hash = coin_state.coin.coin_id(); + let hash = hash.as_ref(); + let parent_coin_hash = coin_state.coin.parent_coin_info.as_ref(); + let puzzle_hash = coin_state.coin.puzzle_hash.as_ref(); + let amount = coin_state.coin.amount.to_be_bytes().to_vec(); + + query!( + " + INSERT INTO coins + (hash, parent_coin_hash, puzzle_hash, amount, created_height, spent_height) + VALUES + (?, ?, ?, ?, ?, ?) + ON CONFLICT(hash) DO UPDATE SET + created_height = excluded.created_height, + spent_height = excluded.spent_height + ", + hash, + parent_coin_hash, + puzzle_hash, + amount, + coin_state.created_height, + coin_state.spent_height, + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn is_known_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result { + let coin_id_ref = coin_id.as_ref(); + + let row = query!( + "SELECT COUNT(*) AS count FROM coins WHERE hash = ?", + coin_id_ref + ) + .fetch_one(conn) + .await?; + + Ok(row.count > 0) +} + +async fn unsynced_coins(conn: impl SqliteExecutor<'_>, limit: usize) -> Result> { + let limit = i64::try_from(limit)?; + + query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, created_height, spent_height, + (asset_id IS NULL) AS is_asset_unsynced, + (spent_height IS NOT NULL AND is_children_synced = FALSE) AS is_children_unsynced + FROM coins + WHERE asset_id IS NULL OR (spent_height IS NOT NULL AND is_children_synced = FALSE) + LIMIT ? + ", + limit + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + Ok(UnsyncedCoin { + coin_state: CoinState::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + row.spent_height.convert()?, + row.created_height.convert()?, + ), + is_asset_unsynced: row.is_asset_unsynced != 0, + is_children_unsynced: row.is_children_unsynced != 0, + }) + }) + .collect() +} + +async fn delete_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result<()> { + let coin_id_ref = coin_id.as_ref(); + + query!("DELETE FROM coins WHERE hash = ?", coin_id_ref) + .execute(conn) + .await?; + + Ok(()) +} + +async fn update_coin( + conn: impl SqliteExecutor<'_>, + coin_id: Bytes32, + asset_hash: Bytes32, + p2_puzzle_hash: Bytes32, + hidden_puzzle_hash: Option, +) -> Result<()> { + let coin_id = coin_id.as_ref(); + let asset_hash = asset_hash.as_ref(); + let p2_puzzle_hash = p2_puzzle_hash.as_ref(); + let hidden_puzzle_hash = hidden_puzzle_hash.as_deref(); + + query!( + " + UPDATE coins SET + asset_id = (SELECT id FROM assets WHERE hash = ?), + p2_puzzle_id = (SELECT id FROM p2_puzzles WHERE hash = ?), + hidden_puzzle_hash = ? + WHERE hash = ? + ", + asset_hash, + p2_puzzle_hash, + hidden_puzzle_hash, + coin_id, + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn set_children_synced(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result<()> { + let coin_id = coin_id.as_ref(); + + query!( + "UPDATE coins SET is_children_synced = TRUE WHERE hash = ?", + coin_id + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn set_transaction_children_unsynced( + conn: impl SqliteExecutor<'_>, + mempool_item_id: Bytes32, +) -> Result<()> { + let mempool_item_id = mempool_item_id.as_ref(); + + query!( + " + UPDATE coins SET is_children_synced = FALSE WHERE id IN ( + SELECT coin_id FROM mempool_coins + INNER JOIN mempool_items ON mempool_items.id = mempool_coins.mempool_item_id + WHERE mempool_items.hash = ? AND is_input = TRUE + ) + ", + mempool_item_id + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn insert_lineage_proof( + conn: impl SqliteExecutor<'_>, + coin_id: Bytes32, + lineage_proof: LineageProof, +) -> Result<()> { + let coin_id = coin_id.as_ref(); + let parent_parent_coin_hash = lineage_proof.parent_parent_coin_info.as_ref(); + let parent_inner_puzzle_hash = lineage_proof.parent_inner_puzzle_hash.as_ref(); + let parent_amount = lineage_proof.parent_amount.to_be_bytes().to_vec(); + + query!( + "INSERT OR IGNORE INTO lineage_proofs + (coin_id, parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount) + VALUES + ((SELECT id FROM coins WHERE hash = ?), ?, ?, ?) + ", + coin_id, + parent_parent_coin_hash, + parent_inner_puzzle_hash, + parent_amount, + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn subscription_coin_ids(conn: impl SqliteExecutor<'_>) -> Result> { + query!( + " + SELECT coin_hash FROM wallet_coins + WHERE spent_height IS NULL + AND (asset_id != 0 OR p2_puzzle_kind != 0) + " + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| row.coin_hash.convert()) + .collect() +} + +async fn selectable_coin_count(conn: impl SqliteExecutor<'_>, asset_id: Bytes32) -> Result { + let asset_id_ref = asset_id.as_ref(); + + query!( + "SELECT COUNT(*) AS count FROM selectable_coins WHERE asset_hash = ?", + asset_id_ref + ) + .fetch_one(conn) + .await? + .count + .convert() +} + +async fn total_coin_count(conn: impl SqliteExecutor<'_>) -> Result { + query!("SELECT COUNT(*) AS count FROM coins") + .fetch_one(conn) + .await? + .count + .convert() +} + +async fn synced_coin_count(conn: impl SqliteExecutor<'_>) -> Result { + query!( + " + SELECT COUNT(*) AS count FROM coins + WHERE asset_id IS NOT NULL + AND (spent_height IS NULL OR is_children_synced = TRUE) + " + ) + .fetch_one(conn) + .await? + .count + .convert() +} + +async fn token_balance(conn: impl SqliteExecutor<'_>, asset_id: Bytes32) -> Result { + let asset_id_ref = asset_id.as_ref(); + + query!( + "SELECT amount FROM owned_coins WHERE asset_hash = ?", + asset_id_ref + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + let amount: u64 = row.amount.convert()?; + Ok(amount as u128) + }) + .sum() +} + +async fn selectable_token_balance( + conn: impl SqliteExecutor<'_>, + asset_id: Bytes32, +) -> Result { + let asset_id_ref = asset_id.as_ref(); + + query!( + "SELECT amount FROM selectable_coins WHERE asset_hash = ?", + asset_id_ref + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + let amount: u64 = row.amount.convert()?; + Ok(amount as u128) + }) + .sum() +} + +async fn coins_by_ids(conn: impl SqliteExecutor<'_>, coin_ids: &[String]) -> Result> { + let mut query = sqlx::QueryBuilder::new( + " + SELECT + parent_coin_hash, puzzle_hash, amount, spent_height, created_height, p2_puzzle_hash, + mempool_item_hash, offer_hash, created_timestamp, spent_timestamp, clawback_expiration_seconds AS clawback_timestamp + FROM wallet_coins + WHERE coin_hash IN (", + ); + let mut separated = query.separated(", "); + + for coin_id in coin_ids { + separated.push(format!("X'{coin_id}'")); + } + separated.push_unseparated(")"); + + let rows = query.build().fetch_all(conn).await?; + + let coins = rows + .into_iter() + .map(|row| { + Ok(CoinRow { + coin: Coin::new( + row.get::, _>("parent_coin_hash").convert()?, + row.get::, _>("puzzle_hash").convert()?, + row.get::, _>("amount").convert()?, + ), + p2_puzzle_hash: row.get::, _>("p2_puzzle_hash").convert()?, + mempool_item_hash: row + .get::>, _>("mempool_item_hash") + .map(Convert::convert) + .transpose()?, + offer_hash: row + .get::>, _>("offer_hash") + .map(Convert::convert) + .transpose()?, + kind: CoinKind::Xch, + clawback_timestamp: row.get::, _>("clawback_timestamp").convert()?, + created_height: row.get::, _>("created_height"), + spent_height: row.get::, _>("spent_height"), + created_timestamp: row.get::, _>("created_timestamp").convert()?, + spent_timestamp: row.get::, _>("spent_timestamp").convert()?, + }) + }) + .collect::>>()?; + + Ok(coins) +} + +async fn coin_records( + conn: impl SqliteExecutor<'_>, + asset_filter: AssetFilter, + limit: u32, + offset: u32, + sort_mode: CoinSortMode, + ascending: bool, + filter_mode: CoinFilterMode, +) -> Result<(Vec, u32)> { + let table = match filter_mode { + CoinFilterMode::All => "wallet_coins", + CoinFilterMode::Selectable => "selectable_coins", + CoinFilterMode::Owned => "owned_coins", + CoinFilterMode::Spent => "spent_coins", + CoinFilterMode::Clawback => "clawback_coins", + }; + + let mut query = sqlx::QueryBuilder::new(format!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, + spent_height, created_height, p2_puzzle_hash, + mempool_item_hash, offer_hash, created_timestamp, spent_timestamp, + clawback_expiration_seconds AS clawback_timestamp, + COUNT(*) OVER () AS total_count + FROM {table} + ", + )); + + match asset_filter { + AssetFilter::Id(asset_id) => { + query.push(" WHERE asset_hash = "); + query.push_bind(asset_id.to_vec()); + } + AssetFilter::Nfts => { + query.push(" WHERE asset_kind = 1"); + } + AssetFilter::Dids => { + query.push(" WHERE asset_kind = 2"); + } + } + + query.push(" ORDER BY "); + match sort_mode { + CoinSortMode::CoinId if ascending => query.push("coin_hash ASC"), + CoinSortMode::CoinId => query.push("coin_hash DESC"), + CoinSortMode::Amount if ascending => query.push("amount ASC"), + CoinSortMode::Amount => query.push("amount DESC"), + CoinSortMode::CreatedHeight if ascending => query.push("created_height ASC NULLS LAST"), + CoinSortMode::CreatedHeight => query.push("created_height DESC NULLS FIRST"), + CoinSortMode::SpentHeight if ascending => query.push("spent_height ASC NULLS LAST"), + CoinSortMode::SpentHeight => query.push("spent_height DESC NULLS FIRST"), + CoinSortMode::ClawbackTimestamp if ascending => query.push("clawback_timestamp ASC"), + CoinSortMode::ClawbackTimestamp => query.push("clawback_timestamp DESC"), + }; + + query.push(" LIMIT "); + query.push_bind(limit as i64); + query.push(" OFFSET "); + query.push_bind(offset as i64); + + let rows = query.build().fetch_all(conn).await?; + let total_count = rows + .first() + .map_or(Ok(0), |row| row.get::("total_count").try_into())?; + let coins = rows + .into_iter() + .map(|row| { + Ok(CoinRow { + coin: Coin::new( + row.get::, _>("parent_coin_hash").convert()?, + row.get::, _>("puzzle_hash").convert()?, + row.get::, _>("amount").convert()?, + ), + p2_puzzle_hash: row.get::, _>("p2_puzzle_hash").convert()?, + mempool_item_hash: row + .get::>, _>("mempool_item_hash") + .map(Convert::convert) + .transpose()?, + offer_hash: row + .get::>, _>("offer_hash") + .map(Convert::convert) + .transpose()?, + kind: CoinKind::Xch, + clawback_timestamp: row.get::, _>("clawback_timestamp").convert()?, + created_height: row.get::, _>("created_height"), + spent_height: row.get::, _>("spent_height"), + created_timestamp: row + .get::, _>("created_timestamp") + .map(TryInto::try_into) + .transpose()?, + spent_timestamp: row + .get::, _>("spent_timestamp") + .map(TryInto::try_into) + .transpose()?, + }) + }) + .collect::>>()?; + + Ok((coins, total_count)) +} + +async fn selectable_xch_coins(conn: impl SqliteExecutor<'_>) -> Result> { + query!("SELECT parent_coin_hash, puzzle_hash, amount FROM selectable_coins WHERE asset_id = 0") + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + Ok(Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + )) + }) + .collect() +} + +async fn selectable_cat_coins( + conn: impl SqliteExecutor<'_>, + asset_id: Bytes32, +) -> Result> { + let asset_id_ref = asset_id.as_ref(); + + query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, hidden_puzzle_hash, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount + FROM selectable_coins + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = selectable_coins.coin_id + WHERE asset_hash = ? + ", + asset_id_ref + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + Ok(Cat::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Some(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + CatInfo::new( + asset_id, + row.hidden_puzzle_hash.convert()?, + row.p2_puzzle_hash.convert()?, + ), + )) + }) + .collect() +} + +async fn coin_kind(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result> { + let coin_id_ref = coin_id.as_ref(); + + let Some(row) = query!( + "SELECT asset_id, kind FROM coins INNER JOIN assets ON assets.id = asset_id WHERE coins.hash = ?", + coin_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + let Some(asset_id) = row.asset_id else { + return Err(DatabaseError::InvalidEnumVariant); + }; + + let kind: AssetKind = row.kind.convert()?; + + Ok(Some(match kind { + AssetKind::Token => { + if asset_id == 0 { + CoinKind::Xch + } else { + CoinKind::Cat + } + } + AssetKind::Nft => CoinKind::Nft, + AssetKind::Did => CoinKind::Did, + AssetKind::Option => CoinKind::Option, + })) +} + +async fn xch_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result> { + let coin_id_ref = coin_id.as_ref(); + + let Some(row) = query!( + " + SELECT parent_coin_hash, puzzle_hash, amount + FROM spendable_coins + WHERE coin_hash = ? AND asset_id = 0 + ", + coin_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ))) +} + +async fn cat_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result> { + let coin_id_ref = coin_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, hidden_puzzle_hash, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, asset_hash AS asset_id + FROM spendable_coins + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id + WHERE coin_hash = ? + ", + coin_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Cat::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Some(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + CatInfo::new( + row.asset_id.convert()?, + row.hidden_puzzle_hash.convert()?, + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn did_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result>> { + let coin_id_ref = coin_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, recovery_list_hash, num_verifications_required, metadata + FROM spendable_coins + INNER JOIN dids ON dids.asset_id = spendable_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id + WHERE coin_hash = ? + ", + coin_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Did::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + DidInfo::new( + row.launcher_id.convert()?, + row.recovery_list_hash.convert()?, + row.num_verifications_required.convert()?, + row.metadata.into(), + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn nft_coin(conn: impl SqliteExecutor<'_>, coin_id: Bytes32) -> Result>> { + let coin_id_ref = coin_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, metadata, metadata_updater_puzzle_hash, + owner_hash, royalty_puzzle_hash, royalty_basis_points + FROM spendable_coins + INNER JOIN nfts ON nfts.asset_id = spendable_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id + WHERE coin_hash = ? + ", + coin_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Nft::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + NftInfo::new( + row.launcher_id.convert()?, + row.metadata.into(), + row.metadata_updater_puzzle_hash.convert()?, + row.owner_hash.convert()?, + row.royalty_puzzle_hash.convert()?, + row.royalty_basis_points.convert()?, + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn option_coin( + conn: impl SqliteExecutor<'_>, + coin_id: Bytes32, +) -> Result> { + let coin_id_ref = coin_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, underlying_coin_hash, underlying_delegated_puzzle_hash + FROM spendable_coins + INNER JOIN options ON options.asset_id = spendable_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id + WHERE coin_hash = ? + ", + coin_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(OptionContract::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + OptionInfo::new( + row.launcher_id.convert()?, + row.underlying_coin_hash.convert()?, + row.underlying_delegated_puzzle_hash.convert()?, + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn did(conn: impl SqliteExecutor<'_>, launcher_id: Bytes32) -> Result>> { + let launcher_id_ref = launcher_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, recovery_list_hash, num_verifications_required, metadata + FROM wallet_coins + INNER JOIN dids ON dids.asset_id = wallet_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = wallet_coins.coin_id + WHERE asset_hash = ? AND spent_height IS NULL + ", + launcher_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Did::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + DidInfo::new( + row.launcher_id.convert()?, + row.recovery_list_hash.convert()?, + row.num_verifications_required.convert()?, + row.metadata.into(), + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn spendable_did( + conn: impl SqliteExecutor<'_>, + launcher_id: Bytes32, +) -> Result>> { + let launcher_id_ref = launcher_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, recovery_list_hash, num_verifications_required, metadata + FROM spendable_coins + INNER JOIN dids ON dids.asset_id = spendable_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id + WHERE asset_hash = ? + ", + launcher_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Did::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + DidInfo::new( + row.launcher_id.convert()?, + row.recovery_list_hash.convert()?, + row.num_verifications_required.convert()?, + row.metadata.into(), + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn nft(conn: impl SqliteExecutor<'_>, launcher_id: Bytes32) -> Result>> { + let launcher_id_ref = launcher_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, metadata, metadata_updater_puzzle_hash, + owner_hash, royalty_puzzle_hash, royalty_basis_points + FROM wallet_coins + INNER JOIN nfts ON nfts.asset_id = wallet_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = wallet_coins.coin_id + WHERE asset_hash = ? AND spent_height IS NULL + ", + launcher_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Nft::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + NftInfo::new( + row.launcher_id.convert()?, + row.metadata.into(), + row.metadata_updater_puzzle_hash.convert()?, + row.owner_hash.convert()?, + row.royalty_puzzle_hash.convert()?, + row.royalty_basis_points.convert()?, + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn spendable_nft( + conn: impl SqliteExecutor<'_>, + launcher_id: Bytes32, +) -> Result>> { + let launcher_id_ref = launcher_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, metadata, metadata_updater_puzzle_hash, + owner_hash, royalty_puzzle_hash, royalty_basis_points + FROM spendable_coins + INNER JOIN nfts ON nfts.asset_id = spendable_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id + WHERE asset_hash = ? + ", + launcher_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Nft::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + NftInfo::new( + row.launcher_id.convert()?, + row.metadata.into(), + row.metadata_updater_puzzle_hash.convert()?, + row.owner_hash.convert()?, + row.royalty_puzzle_hash.convert()?, + row.royalty_basis_points.convert()?, + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn option( + conn: impl SqliteExecutor<'_>, + launcher_id: Bytes32, +) -> Result> { + let launcher_id_ref = launcher_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, underlying_coin_hash, underlying_delegated_puzzle_hash + FROM wallet_coins + INNER JOIN options ON options.asset_id = wallet_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = wallet_coins.coin_id + WHERE asset_hash = ? AND spent_height IS NULL + ", + launcher_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(OptionContract::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + OptionInfo::new( + row.launcher_id.convert()?, + row.underlying_coin_hash.convert()?, + row.underlying_delegated_puzzle_hash.convert()?, + row.p2_puzzle_hash.convert()?, + ), + ))) +} + +async fn spendable_option( + conn: impl SqliteExecutor<'_>, + launcher_id: Bytes32, +) -> Result> { + let launcher_id_ref = launcher_id.as_ref(); + + let Some(row) = query!( + " + SELECT + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + parent_parent_coin_hash, parent_inner_puzzle_hash, parent_amount, + asset_hash AS launcher_id, underlying_coin_hash, underlying_delegated_puzzle_hash + FROM spendable_coins + INNER JOIN options ON options.asset_id = spendable_coins.asset_id + INNER JOIN lineage_proofs ON lineage_proofs.coin_id = spendable_coins.coin_id + WHERE asset_hash = ? + ", + launcher_id_ref + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(OptionContract::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + Proof::Lineage(LineageProof { + parent_parent_coin_info: row.parent_parent_coin_hash.convert()?, + parent_inner_puzzle_hash: row.parent_inner_puzzle_hash.convert()?, + parent_amount: row.parent_amount.convert()?, + }), + OptionInfo::new( + row.launcher_id.convert()?, + row.underlying_coin_hash.convert()?, + row.underlying_delegated_puzzle_hash.convert()?, + row.p2_puzzle_hash.convert()?, + ), + ))) +} diff --git a/crates/sage-database/src/tables/collections.rs b/crates/sage-database/src/tables/collections.rs new file mode 100644 index 000000000..417ae6ba6 --- /dev/null +++ b/crates/sage-database/src/tables/collections.rs @@ -0,0 +1,160 @@ +use crate::{Convert, Database, DatabaseTx, Result}; +use chia::protocol::Bytes32; +use sqlx::{query, SqliteExecutor}; + +#[derive(Debug, Clone)] +pub struct CollectionRow { + pub hash: Bytes32, + pub uuid: String, + pub minter_hash: Bytes32, + pub name: Option, + pub icon_url: Option, + pub banner_url: Option, + pub description: Option, + pub is_visible: bool, +} + +impl Database { + pub async fn collections( + &self, + limit: u32, + offset: u32, + include_hidden: bool, + ) -> Result<(Vec, u32)> { + collections(&self.pool, limit, offset, include_hidden).await + } + + pub async fn collection(&self, hash: Bytes32) -> Result> { + collection(&self.pool, hash).await + } + + pub async fn set_collection_visible(&self, hash: Bytes32, visible: bool) -> Result<()> { + set_collection_visible(&self.pool, hash, visible).await + } +} + +impl DatabaseTx<'_> { + pub async fn insert_collection(&mut self, row: CollectionRow) -> Result<()> { + insert_collection(&mut *self.tx, row).await + } + + pub async fn set_collection_visible(&mut self, hash: Bytes32, visible: bool) -> Result<()> { + set_collection_visible(&mut *self.tx, hash, visible).await + } +} + +async fn collection(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result> { + let hash_ref = hash.as_ref(); + let row = query!( + "SELECT id, hash, uuid, minter_hash, name, icon_url, banner_url, description, is_visible + FROM collections + WHERE hash = ?", + hash_ref + ) + .fetch_optional(conn) + .await?; + + row.map(|row| { + Ok(CollectionRow { + hash: row.hash.convert()?, + uuid: row.uuid, + minter_hash: row.minter_hash.convert()?, + name: row.name, + icon_url: row.icon_url, + banner_url: row.banner_url, + description: row.description, + is_visible: row.is_visible, + }) + }) + .transpose() +} + +async fn collections( + conn: impl SqliteExecutor<'_>, + limit: u32, + offset: u32, + include_hidden: bool, +) -> Result<(Vec, u32)> { + // we only return collections that have nfts + let rows = query!( + "SELECT collections.hash, uuid, collections.minter_hash, collections.name, collections.icon_url, + collections.banner_url, collections.description, collections.is_visible, COUNT(*) OVER() as total_count + FROM collections + WHERE 1=1 + AND EXISTS (SELECT 1 FROM owned_nfts WHERE owned_nfts.collection_id = collections.id) + AND (? OR is_visible = 1) + ORDER BY CASE WHEN collections.id = 0 THEN 1 ELSE 0 END, name ASC + LIMIT ? + OFFSET ?", + include_hidden, + limit, + offset + ) + .fetch_all(conn) + .await?; + + let total_count = rows + .first() + .map_or(Ok(0), |row| row.total_count.try_into())?; + + let collections = rows + .into_iter() + .map(|row| { + Ok(CollectionRow { + hash: row.hash.convert()?, + uuid: row.uuid, + minter_hash: row.minter_hash.convert()?, + name: row.name, + icon_url: row.icon_url, + banner_url: row.banner_url, + description: row.description, + is_visible: row.is_visible, + }) + }) + .collect::>>()?; + + Ok((collections, total_count)) +} + +async fn insert_collection(conn: impl SqliteExecutor<'_>, row: CollectionRow) -> Result<()> { + let hash_ref = row.hash.as_ref(); + let minter_hash_ref = row.minter_hash.as_ref(); + query!( + " + INSERT OR IGNORE INTO collections ( + hash, uuid, minter_hash, name, icon_url, + banner_url, description, is_visible + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ", + hash_ref, + row.uuid, + minter_hash_ref, + row.name, + row.icon_url, + row.banner_url, + row.description, + row.is_visible, + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn set_collection_visible( + conn: impl SqliteExecutor<'_>, + hash: Bytes32, + visible: bool, +) -> Result<()> { + let hash_ref = hash.as_ref(); + query!( + "UPDATE collections SET is_visible = ? WHERE hash = ?", + visible, + hash_ref + ) + .execute(conn) + .await?; + + Ok(()) +} diff --git a/crates/sage-database/src/tables/files.rs b/crates/sage-database/src/tables/files.rs new file mode 100644 index 000000000..e9bcfbdde --- /dev/null +++ b/crates/sage-database/src/tables/files.rs @@ -0,0 +1,411 @@ +use chia::protocol::Bytes32; +use sqlx::{query, SqliteExecutor}; + +use crate::{Convert, Database, DatabaseTx, Result}; + +#[derive(Debug, Clone)] +pub struct FileUri { + pub hash: Bytes32, + pub uri: String, + pub last_checked_timestamp: Option, + pub failed_attempts: u32, +} + +#[derive(Debug, Clone, Copy)] +pub struct UpdateableNft { + pub hash: Bytes32, + pub minter_hash: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ResizedImageKind { + Icon, + Thumbnail, +} + +#[derive(Debug, Clone)] +pub struct FileData { + pub hash: Bytes32, + pub data: Vec, + pub mime_type: String, + pub is_hash_match: bool, +} + +#[derive(Debug, Clone)] +pub struct ResizedImage { + pub data: Vec, + pub mime_type: Option, +} + +impl Database { + pub async fn candidates_for_download( + &self, + check_every_seconds: i64, + max_failed_attempts: u32, + limit: u32, + ) -> Result> { + candidates_for_download(&self.pool, check_every_seconds, max_failed_attempts, limit).await + } + + pub async fn thumbnail(&self, hash: Bytes32) -> Result> { + resized_image(&self.pool, hash, ResizedImageKind::Thumbnail).await + } + + pub async fn icon(&self, hash: Bytes32) -> Result> { + resized_image(&self.pool, hash, ResizedImageKind::Icon).await + } + + pub async fn full_file_data(&self, hash: Bytes32) -> Result> { + full_file_data(&self.pool, hash).await + } + + pub async fn checked_files(&self) -> Result { + checked_files(&self.pool).await + } + + pub async fn total_files(&self) -> Result { + total_files(&self.pool).await + } +} + +impl DatabaseTx<'_> { + pub async fn insert_file(&mut self, hash: Bytes32) -> Result<()> { + insert_file(&mut *self.tx, hash).await + } + + pub async fn insert_file_uri(&mut self, hash: Bytes32, uri: String) -> Result<()> { + insert_file_uri(&mut *self.tx, hash, uri).await + } + + pub async fn file_data(&mut self, hash: Bytes32) -> Result>> { + file_data(&mut *self.tx, hash).await + } + + pub async fn update_checked_uri(&mut self, hash: Bytes32, uri: String) -> Result<()> { + update_checked_uri(&mut *self.tx, hash, uri).await + } + + pub async fn update_failed_uri(&mut self, hash: Bytes32, uri: String) -> Result<()> { + update_failed_uri(&mut *self.tx, hash, uri).await + } + + pub async fn update_file( + &mut self, + hash: Bytes32, + data: Vec, + mime_type: String, + is_hash_match: bool, + ) -> Result<()> { + update_file(&mut *self.tx, hash, data, mime_type, is_hash_match).await + } + + pub async fn insert_resized_image( + &mut self, + file_hash: Bytes32, + kind: ResizedImageKind, + data: Vec, + ) -> Result<()> { + insert_resized_image(&mut *self.tx, file_hash, kind, data).await + } + + pub async fn icon(&mut self, hash: Bytes32) -> Result> { + resized_image(&mut *self.tx, hash, ResizedImageKind::Icon).await + } + + pub async fn nfts_with_metadata_hash(&mut self, hash: Bytes32) -> Result> { + nfts_with_metadata_hash(&mut *self.tx, hash).await + } + + pub async fn delete_file_data(&mut self, hash: Bytes32) -> Result<()> { + delete_file_data(&mut *self.tx, hash).await + } + + pub async fn set_uri_unchecked(&mut self, uri: String) -> Result<()> { + set_uri_unchecked(&mut *self.tx, uri).await + } +} + +async fn insert_file(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result<()> { + let hash = hash.as_ref(); + + query!("INSERT OR IGNORE INTO files (hash) VALUES (?)", hash) + .execute(conn) + .await?; + + Ok(()) +} + +async fn insert_file_uri(conn: impl SqliteExecutor<'_>, hash: Bytes32, uri: String) -> Result<()> { + let hash = hash.as_ref(); + + query!( + "INSERT OR IGNORE INTO file_uris (file_id, uri) VALUES ((SELECT id FROM files WHERE hash = ?), ?)", + hash, + uri + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn file_data(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result>> { + let hash = hash.as_ref(); + + let row = query!("SELECT data FROM files WHERE hash = ?", hash) + .fetch_optional(conn) + .await?; + + Ok(row.and_then(|row| row.data)) +} + +async fn full_file_data(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result> { + let hash = hash.as_ref(); + + let row = query!( + "SELECT hash, data, mime_type, is_hash_match FROM files WHERE hash = ?", + hash + ) + .fetch_optional(conn) + .await?; + + row.map(|row| { + Ok(FileData { + hash: row.hash.convert()?, + data: row.data.unwrap_or_default(), + mime_type: row.mime_type.unwrap_or_default(), + is_hash_match: row.is_hash_match.unwrap_or_default(), + }) + }) + .transpose() +} + +async fn candidates_for_download( + conn: impl SqliteExecutor<'_>, + check_every_seconds: i64, + max_failed_attempts: u32, + limit: u32, +) -> Result> { + query!( + " + SELECT hash, uri, last_checked_timestamp, failed_attempts + FROM file_uris + INNER JOIN files ON files.id = file_uris.file_id + WHERE data IS NULL + AND (last_checked_timestamp IS NULL OR unixepoch() - last_checked_timestamp >= ?) + AND failed_attempts < ? + ORDER BY last_checked_timestamp ASC + LIMIT ? + ", + check_every_seconds, + max_failed_attempts, + limit + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + Ok(FileUri { + hash: row.hash.convert()?, + uri: row.uri, + last_checked_timestamp: row.last_checked_timestamp.convert()?, + failed_attempts: row.failed_attempts.convert()?, + }) + }) + .collect() +} + +async fn update_failed_uri( + conn: impl SqliteExecutor<'_>, + hash: Bytes32, + uri: String, +) -> Result<()> { + let hash = hash.as_ref(); + + query!( + " + UPDATE file_uris + SET failed_attempts = failed_attempts + 1, last_checked_timestamp = unixepoch() + WHERE file_id = (SELECT id FROM files WHERE hash = ?) AND uri = ? + ", + hash, + uri + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn update_checked_uri( + conn: impl SqliteExecutor<'_>, + hash: Bytes32, + uri: String, +) -> Result<()> { + let hash = hash.as_ref(); + + query!( + " + UPDATE file_uris + SET last_checked_timestamp = unixepoch() + WHERE file_id = (SELECT id FROM files WHERE hash = ?) AND uri = ? + ", + hash, + uri + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn update_file( + conn: impl SqliteExecutor<'_>, + hash: Bytes32, + data: Vec, + mime_type: String, + is_hash_match: bool, +) -> Result<()> { + let hash = hash.as_ref(); + + query!( + " + UPDATE files SET data = ?, mime_type = ?, is_hash_match = ? + WHERE hash = ? + AND (data IS NULL OR NOT is_hash_match) + ", + data, + mime_type, + is_hash_match, + hash + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn nfts_with_metadata_hash( + conn: impl SqliteExecutor<'_>, + hash: Bytes32, +) -> Result> { + let hash = hash.as_ref(); + + query!( + "SELECT hash, minter_hash FROM nfts INNER JOIN assets ON assets.id = asset_id WHERE metadata_hash = ?", + hash + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| Ok(UpdateableNft { + hash: row.hash.convert()?, + minter_hash: row.minter_hash.convert()?, + })) + .collect() +} + +async fn insert_resized_image( + conn: impl SqliteExecutor<'_>, + file_hash: Bytes32, + kind: ResizedImageKind, + data: Vec, +) -> Result<()> { + let file_hash = file_hash.as_ref(); + let kind = kind as i64; + + query!( + "INSERT INTO resized_images (file_id, kind, data) VALUES ((SELECT id FROM files WHERE hash = ?), ?, ?)", + file_hash, + kind, + data + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn resized_image( + conn: impl SqliteExecutor<'_>, + hash: Bytes32, + kind: ResizedImageKind, +) -> Result> { + let hash = hash.as_ref(); + let kind = kind as i64; + + let row = query!( + "SELECT resized_images.data, mime_type + FROM resized_images + INNER JOIN files ON files.id = resized_images.file_id + WHERE files.hash = ? AND kind = ?", + hash, + kind + ) + .fetch_optional(conn) + .await?; + + Ok(row.map(|row| ResizedImage { + data: row.data, + mime_type: row.mime_type, + })) +} + +async fn delete_file_data(conn: impl SqliteExecutor<'_>, hash: Bytes32) -> Result<()> { + let hash = hash.as_ref(); + + query!("UPDATE files SET data = NULL WHERE hash = ?", hash) + .execute(conn) + .await?; + + Ok(()) +} + +async fn set_uri_unchecked(conn: impl SqliteExecutor<'_>, uri: String) -> Result<()> { + query!( + "UPDATE file_uris + SET failed_attempts = 0, last_checked_timestamp = NULL + FROM files + WHERE file_uris.file_id = files.id AND file_uris.uri = ?", + uri + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn checked_files(conn: impl SqliteExecutor<'_>) -> Result { + query!( + " + SELECT COUNT(*) AS count FROM files + WHERE EXISTS ( + SELECT 1 FROM file_uris + WHERE file_uris.file_id = files.id + AND file_uris.last_checked_timestamp IS NOT NULL + ) + " + ) + .fetch_one(conn) + .await? + .count + .try_into() + .map_err(crate::DatabaseError::PrecisionLost) +} + +async fn total_files(conn: impl SqliteExecutor<'_>) -> Result { + query!( + " + SELECT COUNT(*) AS count FROM files + WHERE EXISTS ( + SELECT 1 FROM file_uris + WHERE file_uris.file_id = files.id + ) + " + ) + .fetch_one(conn) + .await? + .count + .try_into() + .map_err(crate::DatabaseError::PrecisionLost) +} diff --git a/crates/sage-database/src/tables/mempool_items.rs b/crates/sage-database/src/tables/mempool_items.rs new file mode 100644 index 000000000..72d048bc2 --- /dev/null +++ b/crates/sage-database/src/tables/mempool_items.rs @@ -0,0 +1,335 @@ +use chia::{ + bls::Signature, + protocol::{Bytes32, Coin, CoinSpend}, +}; +use sqlx::{query, SqliteConnection, SqliteExecutor}; + +use crate::{Convert, Database, DatabaseTx, Result}; + +#[derive(Debug, Clone)] +pub struct MempoolItem { + pub hash: Bytes32, + pub aggregated_signature: Signature, + pub fee: u64, + pub submitted_timestamp: i64, +} + +impl Database { + pub async fn mempool_items_to_submit( + &self, + check_every_seconds: i64, + limit: i64, + ) -> Result> { + mempool_items_to_submit(&self.pool, check_every_seconds, limit).await + } + + pub async fn mempool_coin_spends(&self, mempool_item_id: Bytes32) -> Result> { + mempool_coin_spends(&self.pool, mempool_item_id).await + } + + pub async fn update_mempool_item_time(&self, mempool_item_id: Bytes32) -> Result<()> { + update_mempool_item_time(&self.pool, mempool_item_id).await + } + + pub async fn mempool_items(&self) -> Result> { + mempool_items(&self.pool).await + } +} + +impl DatabaseTx<'_> { + pub async fn insert_mempool_item( + &mut self, + hash: Bytes32, + aggregated_signature: Signature, + fee: u64, + ) -> Result<()> { + insert_mempool_item(&mut *self.tx, hash, aggregated_signature, fee).await + } + + pub async fn insert_mempool_coin( + &mut self, + mempool_item_id: Bytes32, + coin_id: Bytes32, + is_input: bool, + is_output: bool, + ) -> Result<()> { + insert_mempool_coin(&mut *self.tx, mempool_item_id, coin_id, is_input, is_output).await + } + + pub async fn insert_mempool_spend( + &mut self, + mempool_item_id: Bytes32, + coin_spend: CoinSpend, + seq: usize, + ) -> Result<()> { + insert_mempool_spend(&mut *self.tx, mempool_item_id, coin_spend, seq).await + } + + pub async fn mempool_items_for_input(&mut self, coin_id: Bytes32) -> Result> { + mempool_items_for_input(&mut *self.tx, coin_id).await + } + + pub async fn mempool_items_for_output(&mut self, coin_id: Bytes32) -> Result> { + mempool_items_for_output(&mut *self.tx, coin_id).await + } + + pub async fn remove_mempool_item(&mut self, mempool_item_id: Bytes32) -> Result<()> { + remove_mempool_item(&mut self.tx, mempool_item_id).await + } +} + +async fn insert_mempool_item( + conn: impl SqliteExecutor<'_>, + hash: Bytes32, + aggregated_signature: Signature, + fee: u64, +) -> Result<()> { + let hash = hash.as_ref(); + let aggregated_signature = aggregated_signature.to_bytes(); + let aggregated_signature = aggregated_signature.as_ref(); + let fee = fee.to_be_bytes().to_vec(); + + query!( + " + INSERT OR IGNORE INTO mempool_items (hash, aggregated_signature, fee) VALUES (?, ?, ?) + ", + hash, + aggregated_signature, + fee + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn insert_mempool_coin( + conn: impl SqliteExecutor<'_>, + mempool_item_id: Bytes32, + coin_id: Bytes32, + is_input: bool, + is_output: bool, +) -> Result<()> { + let mempool_item_id = mempool_item_id.as_ref(); + let coin_id = coin_id.as_ref(); + + query!( + " + INSERT OR IGNORE INTO mempool_coins (mempool_item_id, coin_id, is_input, is_output) + VALUES ((SELECT id FROM mempool_items WHERE hash = ?), (SELECT id FROM coins WHERE hash = ?), ?, ?) + ", + mempool_item_id, + coin_id, + is_input, + is_output + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn insert_mempool_spend( + conn: impl SqliteExecutor<'_>, + mempool_item_id: Bytes32, + coin_spend: CoinSpend, + seq: usize, +) -> Result<()> { + let mempool_item_id = mempool_item_id.as_ref(); + let coin_id = coin_spend.coin.coin_id(); + let coin_id = coin_id.as_ref(); + let parent_coin_hash = coin_spend.coin.parent_coin_info.as_ref(); + let puzzle_hash = coin_spend.coin.puzzle_hash.as_ref(); + let amount = coin_spend.coin.amount.to_be_bytes().to_vec(); + let puzzle_reveal = coin_spend.puzzle_reveal.into_bytes(); + let solution = coin_spend.solution.into_bytes(); + let seq: i64 = seq.try_into()?; + + query!( + " + INSERT OR IGNORE INTO mempool_spends (mempool_item_id, coin_hash, parent_coin_hash, puzzle_hash, amount, puzzle_reveal, solution, seq) + VALUES ((SELECT id FROM mempool_items WHERE hash = ?), ?, ?, ?, ?, ?, ?, ?) + ", + mempool_item_id, + coin_id, + parent_coin_hash, + puzzle_hash, + amount, + puzzle_reveal, + solution, + seq + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn mempool_items_to_submit( + conn: impl SqliteExecutor<'_>, + check_every_seconds: i64, + limit: i64, +) -> Result> { + query!( + " + SELECT hash, aggregated_signature, fee, submitted_timestamp + FROM mempool_items + WHERE submitted_timestamp IS NULL OR unixepoch() - submitted_timestamp >= ? + LIMIT ? + ", + check_every_seconds, + limit + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + Ok(MempoolItem { + hash: row.hash.convert()?, + aggregated_signature: row.aggregated_signature.convert()?, + fee: row.fee.convert()?, + submitted_timestamp: row.submitted_timestamp.unwrap_or(0), + }) + }) + .collect() +} + +async fn mempool_coin_spends( + conn: impl SqliteExecutor<'_>, + mempool_item_id: Bytes32, +) -> Result> { + let mempool_item_id = mempool_item_id.as_ref(); + + query!( + " + SELECT parent_coin_hash, puzzle_hash, amount, puzzle_reveal, solution + FROM mempool_spends + INNER JOIN mempool_items ON mempool_items.id = mempool_spends.mempool_item_id + WHERE mempool_items.hash = ? + ORDER BY seq ASC + ", + mempool_item_id + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + Ok(CoinSpend::new( + Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + row.puzzle_reveal.into(), + row.solution.into(), + )) + }) + .collect() +} + +async fn mempool_items_for_input( + conn: impl SqliteExecutor<'_>, + coin_id: Bytes32, +) -> Result> { + let coin_id = coin_id.as_ref(); + + query!( + " + SELECT mempool_items.hash AS mempool_item_hash + FROM mempool_items + INNER JOIN mempool_coins ON mempool_coins.mempool_item_id = mempool_items.id + INNER JOIN coins ON coins.hash = ? + WHERE mempool_coins.is_input = TRUE + ", + coin_id + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| row.mempool_item_hash.convert()) + .collect() +} + +async fn mempool_items_for_output( + conn: impl SqliteExecutor<'_>, + coin_id: Bytes32, +) -> Result> { + let coin_id = coin_id.as_ref(); + + query!( + " + SELECT mempool_items.hash AS mempool_item_hash + FROM mempool_items + INNER JOIN mempool_coins ON mempool_coins.mempool_item_id = mempool_items.id + INNER JOIN coins ON coins.hash = ? + WHERE mempool_coins.is_output = TRUE + ", + coin_id + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| row.mempool_item_hash.convert()) + .collect() +} + +async fn remove_mempool_item(conn: &mut SqliteConnection, mempool_item_id: Bytes32) -> Result<()> { + let mempool_item_id = mempool_item_id.as_ref(); + + query!( + " + DELETE FROM coins WHERE created_height IS NULL AND id IN ( + SELECT coin_id FROM mempool_coins + INNER JOIN mempool_items ON mempool_items.id = mempool_coins.mempool_item_id + WHERE hash = ? AND is_output = TRUE + ) + ", + mempool_item_id + ) + .execute(&mut *conn) + .await?; + + query!("DELETE FROM mempool_items WHERE hash = ?", mempool_item_id) + .execute(conn) + .await?; + + Ok(()) +} + +async fn update_mempool_item_time( + conn: impl SqliteExecutor<'_>, + mempool_item_id: Bytes32, +) -> Result<()> { + let mempool_item_id = mempool_item_id.as_ref(); + + query!( + "UPDATE mempool_items SET submitted_timestamp = unixepoch() WHERE hash = ?", + mempool_item_id + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn mempool_items(conn: impl SqliteExecutor<'_>) -> Result> { + query!( + " + SELECT hash, aggregated_signature, fee, submitted_timestamp + FROM mempool_items + ORDER BY submitted_timestamp DESC, hash ASC + ", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|row| { + Ok(MempoolItem { + hash: row.hash.convert()?, + aggregated_signature: row.aggregated_signature.convert()?, + fee: row.fee.convert()?, + submitted_timestamp: row.submitted_timestamp.unwrap_or(0), + }) + }) + .collect() +} diff --git a/crates/sage-database/src/tables/offers.rs b/crates/sage-database/src/tables/offers.rs new file mode 100644 index 000000000..818451c96 --- /dev/null +++ b/crates/sage-database/src/tables/offers.rs @@ -0,0 +1,331 @@ +use crate::{Asset, Convert, Database, DatabaseTx, Result}; +use chia::protocol::Bytes32; +use sqlx::SqliteExecutor; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum OfferStatus { + Pending = 0, + Active = 1, + Completed = 2, + Cancelled = 3, + Expired = 4, +} + +#[derive(Debug, Clone)] +pub struct OfferRow { + pub offer_id: Bytes32, + pub encoded_offer: String, + pub expiration_height: Option, + pub expiration_timestamp: Option, + pub fee: u64, + pub status: OfferStatus, + pub inserted_timestamp: u64, +} + +#[derive(Debug, Clone)] +pub struct OfferedAsset { + pub offer_id: Bytes32, + pub asset: Asset, + pub is_requested: bool, + pub amount: u64, + pub royalty: u64, +} + +impl Database { + pub async fn offer(&self, offer_id: Bytes32) -> Result> { + offer(&self.pool, offer_id).await + } + + pub async fn offer_assets(&self, offer_id: Bytes32) -> Result> { + offer_assets(&self.pool, offer_id).await + } + + pub async fn delete_offer(&self, offer_id: Bytes32) -> Result<()> { + delete_offer(&self.pool, offer_id).await + } + + pub async fn offers(&self, status: Option) -> Result> { + offers(&self.pool, status).await + } + + pub async fn update_offer_status(&self, offer_id: Bytes32, status: OfferStatus) -> Result<()> { + update_offer_status(&self.pool, offer_id, status).await + } +} + +impl DatabaseTx<'_> { + pub async fn insert_offer(&mut self, offer: OfferRow) -> Result<()> { + insert_offer(&mut *self.tx, offer).await + } + + pub async fn insert_offered_coin(&mut self, offer_id: Bytes32, coin_id: Bytes32) -> Result<()> { + insert_offered_coin(&mut *self.tx, offer_id, coin_id).await + } + + pub async fn insert_offer_asset( + &mut self, + offer_id: Bytes32, + asset_id: Bytes32, + amount: u64, + royalty: u64, + is_requested: bool, + ) -> Result<()> { + insert_offer_asset( + &mut *self.tx, + offer_id, + asset_id, + amount, + royalty, + is_requested, + ) + .await + } + + pub async fn update_offer_status( + &mut self, + offer_id: Bytes32, + status: OfferStatus, + ) -> Result<()> { + update_offer_status(&mut *self.tx, offer_id, status).await + } +} + +async fn offer_assets( + conn: impl SqliteExecutor<'_>, + offer_id: Bytes32, +) -> Result> { + let offer_id_ref = offer_id.as_ref(); + + let rows = sqlx::query!( + " + SELECT + offers.hash as offer_id, assets.hash as asset_id, + amount, royalty, is_requested, + assets.description, assets.is_sensitive_content, + assets.is_visible, assets.icon_url, assets.name, + assets.ticker, assets.precision, assets.kind + FROM offer_assets + INNER JOIN assets ON offer_assets.asset_id = assets.id + INNER JOIN offers ON offer_assets.offer_id = offers.id + WHERE offers.hash = ? + ", + offer_id_ref + ) + .fetch_all(conn) + .await?; + + rows.into_iter() + .map(|row| { + Ok(OfferedAsset { + offer_id: row.offer_id.convert()?, + asset: Asset { + hash: row.asset_id.convert()?, + description: row.description, + is_sensitive_content: row.is_sensitive_content, + is_visible: row.is_visible, + icon_url: row.icon_url, + kind: row.kind.convert()?, + name: row.name, + ticker: row.ticker, + precision: row.precision.convert()?, + }, + amount: row.amount.convert()?, + royalty: row.royalty.convert()?, + is_requested: row.is_requested, + }) + }) + .collect() +} + +async fn insert_offer(conn: impl SqliteExecutor<'_>, offer: OfferRow) -> Result<()> { + let offer_id_ref = offer.offer_id.as_ref(); + + let expiration_height: Option = offer.expiration_height.map(Into::into); + let expiration_timestamp: Option = offer + .expiration_timestamp + .map(TryInto::try_into) + .transpose()?; + let inserted_timestamp: i64 = offer.inserted_timestamp.try_into()?; + let fee = offer.fee.to_be_bytes().to_vec(); + + sqlx::query( + " + INSERT OR IGNORE INTO offers ( + hash, encoded_offer, fee, status, + expiration_height, expiration_timestamp, inserted_timestamp + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + ", + ) + .bind(offer_id_ref) + .bind(offer.encoded_offer) + .bind(fee) + .bind(offer.status as u8) + .bind(expiration_height) + .bind(expiration_timestamp) + .bind(inserted_timestamp) + .execute(conn) + .await?; + Ok(()) +} + +async fn insert_offer_asset( + conn: impl SqliteExecutor<'_>, + offer_id: Bytes32, + asset_id: Bytes32, + amount: u64, + royalty: u64, + is_requested: bool, +) -> Result<()> { + let offer_id_ref = offer_id.as_ref(); + let asset_id_ref = asset_id.as_ref(); + + let amount = amount.to_be_bytes().to_vec(); + let royalty = royalty.to_be_bytes().to_vec(); + + sqlx::query( + " + INSERT OR IGNORE INTO offer_assets (offer_id, asset_id, amount, royalty, is_requested) + VALUES ( + (SELECT id FROM offers WHERE hash = ?), + (SELECT id FROM assets WHERE hash = ?), + ?, ?, ? + ) + ", + ) + .bind(offer_id_ref) + .bind(asset_id_ref) + .bind(amount) + .bind(royalty) + .bind(is_requested) + .execute(conn) + .await?; + + Ok(()) +} + +async fn insert_offered_coin( + conn: impl SqliteExecutor<'_>, + offer_hash: Bytes32, + coin_hash: Bytes32, +) -> Result<()> { + let offer_id_ref = offer_hash.as_ref(); + let coin_hash_ref = coin_hash.as_ref(); + sqlx::query( + "INSERT OR IGNORE INTO offer_coins (offer_id, coin_id) + VALUES ((SELECT id FROM offers WHERE hash = ?), (SELECT id FROM coins WHERE hash = ?))", + ) + .bind(offer_id_ref) + .bind(coin_hash_ref) + .execute(conn) + .await?; + + Ok(()) +} + +async fn offer(conn: impl SqliteExecutor<'_>, offer_id: Bytes32) -> Result> { + let offer_id_ref = offer_id.as_ref(); + let row = sqlx::query!( + "SELECT + hash as offer_id, + encoded_offer, + fee, + status, + expiration_height, + expiration_timestamp, + inserted_timestamp + FROM offers WHERE hash = ?", + offer_id_ref + ) + .fetch_optional(conn) + .await?; + + row.map(|row| { + Ok(OfferRow { + offer_id: row.offer_id.convert()?, + encoded_offer: row.encoded_offer, + expiration_height: row.expiration_height.map(|h| h as u32), + expiration_timestamp: row.expiration_timestamp.map(|t| t as u64), + fee: row.fee.convert()?, + status: match row.status { + 0 => OfferStatus::Pending, + 1 => OfferStatus::Active, + 2 => OfferStatus::Completed, + 3 => OfferStatus::Cancelled, + 4 => OfferStatus::Expired, + _ => return Err(crate::DatabaseError::InvalidEnumVariant), + }, + inserted_timestamp: row.inserted_timestamp as u64, + }) + }) + .transpose() +} + +async fn offers( + conn: impl SqliteExecutor<'_>, + status: Option, +) -> Result> { + let status_value = status.map(|s| s as u8); + let rows = sqlx::query!( + "SELECT + hash as offer_id, + encoded_offer, + fee, + status, + expiration_height, + expiration_timestamp, + inserted_timestamp + FROM offers + WHERE status = ? OR ? IS NULL + ORDER BY inserted_timestamp DESC", + status_value, + status_value + ) + .fetch_all(conn) + .await?; + + rows.into_iter() + .map(|row| { + Ok(OfferRow { + offer_id: row.offer_id.convert()?, + encoded_offer: row.encoded_offer, + expiration_height: row.expiration_height.map(|h| h as u32), + expiration_timestamp: row.expiration_timestamp.map(|t| t as u64), + fee: row.fee.convert()?, + status: match row.status { + 0 => OfferStatus::Pending, + 1 => OfferStatus::Active, + 2 => OfferStatus::Completed, + 3 => OfferStatus::Cancelled, + 4 => OfferStatus::Expired, + _ => return Err(crate::DatabaseError::InvalidEnumVariant), + }, + inserted_timestamp: row.inserted_timestamp as u64, + }) + }) + .collect() +} + +async fn delete_offer(conn: impl SqliteExecutor<'_>, offer_id: Bytes32) -> Result<()> { + let offer_id_ref = offer_id.as_ref(); + sqlx::query("DELETE FROM offers WHERE hash = ?") + .bind(offer_id_ref) + .execute(conn) + .await?; + Ok(()) +} + +async fn update_offer_status( + conn: impl SqliteExecutor<'_>, + offer_id: Bytes32, + status: OfferStatus, +) -> Result<()> { + let offer_id_bytes = offer_id.to_vec(); + sqlx::query("UPDATE offers SET status = ? WHERE hash = ?") + .bind(status as u8) + .bind(&offer_id_bytes) + .execute(conn) + .await?; + Ok(()) +} diff --git a/crates/sage-database/src/tables/p2_puzzles.rs b/crates/sage-database/src/tables/p2_puzzles.rs new file mode 100644 index 000000000..b7ece9471 --- /dev/null +++ b/crates/sage-database/src/tables/p2_puzzles.rs @@ -0,0 +1,504 @@ +use chia::{bls::PublicKey, clvm_utils::ToTreeHash, protocol::Bytes32}; +use chia_wallet_sdk::driver::{ClawbackV2, OptionType, OptionUnderlying}; +use sqlx::{query, SqliteExecutor}; + +use crate::{Convert, Database, DatabaseError, DatabaseTx, Result}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum P2PuzzleKind { + PublicKey, + Clawback, + OptionUnderlying, +} + +#[derive(Debug, Clone, Copy)] +pub enum P2Puzzle { + PublicKey(PublicKey), + Clawback(Clawback), + OptionUnderlying(OptionUnderlyingWithKey), +} + +#[derive(Debug, Clone, Copy)] +pub struct Clawback { + pub public_key: PublicKey, + pub sender_puzzle_hash: Bytes32, + pub receiver_puzzle_hash: Bytes32, + pub seconds: u64, +} + +#[derive(Debug, Clone, Copy)] +pub struct OptionUnderlyingWithKey { + pub public_key: PublicKey, + pub option: OptionUnderlying, +} + +#[derive(Debug, Clone, Copy)] +pub struct Derivation { + pub derivation_index: u32, + pub is_hardened: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct DerivationRow { + pub p2_puzzle_hash: Bytes32, + pub index: u32, + pub hardened: bool, + pub synthetic_key: PublicKey, +} + +impl Database { + pub async fn public_key(&self, p2_puzzle_hash: Bytes32) -> Result> { + public_key(&self.pool, p2_puzzle_hash).await + } + + pub async fn custody_p2_puzzle_hashes(&self) -> Result> { + custody_p2_puzzle_hashes(&self.pool).await + } + + pub async fn is_custody_p2_puzzle_hash(&self, puzzle_hash: Bytes32) -> Result { + is_custody_p2_puzzle_hash(&self.pool, puzzle_hash).await + } + + pub async fn is_p2_puzzle_hash(&self, puzzle_hash: Bytes32) -> Result { + is_p2_puzzle_hash(&self.pool, puzzle_hash).await + } + + pub async fn p2_puzzle(&self, puzzle_hash: Bytes32) -> Result { + match p2_puzzle_kind(&self.pool, puzzle_hash).await? { + P2PuzzleKind::PublicKey => { + let Some(key) = public_key(&self.pool, puzzle_hash).await? else { + return Err(DatabaseError::InvalidEnumVariant); + }; + Ok(P2Puzzle::PublicKey(key)) + } + P2PuzzleKind::Clawback => { + Ok(P2Puzzle::Clawback(clawback(&self.pool, puzzle_hash).await?)) + } + P2PuzzleKind::OptionUnderlying => Ok(P2Puzzle::OptionUnderlying( + option_underlying(&self.pool, puzzle_hash).await?, + )), + } + } + + pub async fn derivation(&self, public_key: PublicKey) -> Result> { + derivation(&self.pool, public_key).await + } + + pub async fn derivations( + &self, + is_hardened: bool, + limit: u32, + offset: u32, + ) -> Result<(Vec, u32)> { + derivations(&self.pool, is_hardened, limit, offset).await + } + + pub async fn max_derivation_index(&self, is_hardened: bool) -> Result { + max_derivation_index(&self.pool, is_hardened).await + } +} + +impl DatabaseTx<'_> { + pub async fn custody_p2_puzzle_hash( + &mut self, + derivation_index: u32, + is_hardened: bool, + ) -> Result { + custody_p2_puzzle_hash(&mut *self.tx, derivation_index, is_hardened).await + } + + pub async fn is_custody_p2_puzzle_hash(&mut self, puzzle_hash: Bytes32) -> Result { + is_custody_p2_puzzle_hash(&mut *self.tx, puzzle_hash).await + } + + pub async fn is_p2_puzzle_hash(&mut self, puzzle_hash: Bytes32) -> Result { + is_p2_puzzle_hash(&mut *self.tx, puzzle_hash).await + } + + pub async fn derivation_index(&mut self, is_hardened: bool) -> Result { + derivation_index(&mut *self.tx, is_hardened).await + } + + pub async fn unused_derivation_index(&mut self, is_hardened: bool) -> Result { + unused_derivation_index(&mut *self.tx, is_hardened).await + } + + pub async fn insert_custody_p2_puzzle( + &mut self, + p2_puzzle_hash: Bytes32, + key: PublicKey, + derivation: Derivation, + ) -> Result<()> { + insert_custody_p2_puzzle(&mut *self.tx, p2_puzzle_hash, key, derivation).await + } + + pub async fn insert_clawback_p2_puzzle(&mut self, clawback: ClawbackV2) -> Result<()> { + insert_clawback_p2_puzzle(&mut *self.tx, clawback).await + } +} + +async fn custody_p2_puzzle_hashes(conn: impl SqliteExecutor<'_>) -> Result> { + query!("SELECT hash FROM p2_puzzles WHERE kind = 0") + .fetch_all(conn) + .await? + .into_iter() + .map(|row| row.hash.convert()) + .collect() +} + +async fn custody_p2_puzzle_hash( + conn: impl SqliteExecutor<'_>, + derivation_index: u32, + is_hardened: bool, +) -> Result { + query!( + " + SELECT hash FROM p2_puzzles + INNER JOIN public_keys ON public_keys.p2_puzzle_id = p2_puzzles.id + WHERE public_keys.derivation_index = ? AND public_keys.is_hardened = ? + ", + derivation_index, + is_hardened + ) + .fetch_one(conn) + .await? + .hash + .convert() +} + +async fn is_custody_p2_puzzle_hash( + conn: impl SqliteExecutor<'_>, + puzzle_hash: Bytes32, +) -> Result { + let puzzle_hash = puzzle_hash.as_ref(); + + Ok(query!( + "SELECT COUNT(*) AS count FROM p2_puzzles WHERE hash = ? AND kind = 0", + puzzle_hash + ) + .fetch_one(conn) + .await? + .count + > 0) +} + +async fn is_p2_puzzle_hash(conn: impl SqliteExecutor<'_>, puzzle_hash: Bytes32) -> Result { + let puzzle_hash = puzzle_hash.as_ref(); + + Ok(query!( + "SELECT COUNT(*) AS count FROM p2_puzzles WHERE hash = ?", + puzzle_hash + ) + .fetch_one(conn) + .await? + .count + > 0) +} + +async fn derivation_index(conn: impl SqliteExecutor<'_>, is_hardened: bool) -> Result { + query!( + " + SELECT COALESCE(MAX(derivation_index) + 1, 0) AS derivation_index + FROM public_keys + WHERE is_hardened = ? + ", + is_hardened + ) + .fetch_one(conn) + .await? + .derivation_index + .convert() +} + +async fn max_derivation_index(conn: impl SqliteExecutor<'_>, is_hardened: bool) -> Result { + let row = query!( + " + SELECT MAX(derivation_index) AS derivation_index + FROM public_keys + WHERE is_hardened = ? + ", + is_hardened + ) + .fetch_one(conn) + .await?; + + Ok(row + .derivation_index + .map_or(0, |idx| idx.try_into().unwrap_or(0))) +} + +async fn derivations( + conn: impl SqliteExecutor<'_>, + is_hardened: bool, + limit: u32, + offset: u32, +) -> Result<(Vec, u32)> { + let rows = query!( + " + SELECT + p2_puzzles.hash AS p2_puzzle_hash, + public_keys.derivation_index, + public_keys.is_hardened, + public_keys.key AS synthetic_key, + COUNT(*) OVER() AS total + FROM p2_puzzles + INNER JOIN public_keys ON public_keys.p2_puzzle_id = p2_puzzles.id + WHERE public_keys.is_hardened = ? + ORDER BY public_keys.derivation_index ASC + LIMIT ? OFFSET ? + ", + is_hardened, + limit, + offset + ) + .fetch_all(conn) + .await?; + + let total_count = rows.first().map_or(Ok(0), |row| row.total.try_into())?; + + let derivations = rows + .into_iter() + .map(|row| { + Ok(DerivationRow { + p2_puzzle_hash: row.p2_puzzle_hash.convert()?, + index: row.derivation_index.convert()?, + hardened: row.is_hardened, + synthetic_key: row.synthetic_key.convert()?, + }) + }) + .collect::>>()?; + + Ok((derivations, total_count)) +} + +async fn unused_derivation_index(conn: impl SqliteExecutor<'_>, is_hardened: bool) -> Result { + query!( + " + SELECT COALESCE(MAX(derivation_index) + 1, 0) AS derivation_index + FROM public_keys + INNER JOIN coins ON coins.p2_puzzle_id = public_keys.p2_puzzle_id + WHERE is_hardened = ? + ", + is_hardened + ) + .fetch_one(conn) + .await? + .derivation_index + .convert() +} + +async fn insert_custody_p2_puzzle( + conn: impl SqliteExecutor<'_>, + p2_puzzle_hash: Bytes32, + key: PublicKey, + derivation: Derivation, +) -> Result<()> { + let p2_puzzle_hash = p2_puzzle_hash.as_ref(); + let key = key.to_bytes(); + let key = key.as_ref(); + + query!( + " + INSERT OR IGNORE INTO p2_puzzles (hash, kind) VALUES (?, 0); + + INSERT OR IGNORE INTO public_keys (p2_puzzle_id, is_hardened, derivation_index, key) + VALUES ((SELECT id FROM p2_puzzles WHERE hash = ?), ?, ?, ?); + ", + p2_puzzle_hash, + p2_puzzle_hash, + derivation.is_hardened, + derivation.derivation_index, + key, + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn insert_clawback_p2_puzzle( + conn: impl SqliteExecutor<'_>, + clawback: ClawbackV2, +) -> Result<()> { + let p2_puzzle_hash = clawback.tree_hash().to_vec(); + let sender_puzzle_hash = clawback.sender_puzzle_hash.as_ref(); + let receiver_puzzle_hash = clawback.receiver_puzzle_hash.as_ref(); + let seconds: i64 = clawback.seconds.try_into()?; + + query!( + " + INSERT OR IGNORE INTO p2_puzzles (hash, kind) VALUES (?, 1); + + INSERT OR IGNORE INTO clawbacks (p2_puzzle_id, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds) + VALUES ((SELECT id FROM p2_puzzles WHERE hash = ?), ?, ?, ?); + ", + p2_puzzle_hash, + p2_puzzle_hash, + sender_puzzle_hash, + receiver_puzzle_hash, + seconds, + ) + .execute(conn) + .await?; + + Ok(()) +} + +async fn p2_puzzle_kind( + conn: impl SqliteExecutor<'_>, + p2_puzzle_hash: Bytes32, +) -> Result { + let p2_puzzle_hash = p2_puzzle_hash.as_ref(); + + let row = query!("SELECT kind FROM p2_puzzles WHERE hash = ?", p2_puzzle_hash) + .fetch_one(conn) + .await?; + + Ok(match row.kind { + 0 => P2PuzzleKind::PublicKey, + 1 => P2PuzzleKind::Clawback, + 2 => P2PuzzleKind::OptionUnderlying, + _ => return Err(DatabaseError::InvalidEnumVariant), + }) +} + +async fn public_key( + conn: impl SqliteExecutor<'_>, + p2_puzzle_hash: Bytes32, +) -> Result> { + let p2_puzzle_hash = p2_puzzle_hash.as_ref(); + + let row = query!( + " + SELECT key + FROM p2_puzzles + INNER JOIN public_keys ON public_keys.p2_puzzle_id = p2_puzzles.id + WHERE p2_puzzles.hash = ? + ", + p2_puzzle_hash + ) + .fetch_optional(conn) + .await?; + + row.map(|row| row.key.convert()).transpose() +} + +async fn clawback(conn: impl SqliteExecutor<'_>, p2_puzzle_hash: Bytes32) -> Result { + let p2_puzzle_hash = p2_puzzle_hash.as_ref(); + + let row = query!( + " + SELECT key, sender_puzzle_hash, receiver_puzzle_hash, expiration_seconds + FROM p2_puzzles + INNER JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id + INNER JOIN public_keys ON public_keys.p2_puzzle_id IN ( + SELECT id FROM p2_puzzles + WHERE (hash = sender_puzzle_hash AND unixepoch() < expiration_seconds) + OR (hash = receiver_puzzle_hash AND unixepoch() >= expiration_seconds) + LIMIT 1 + ) + WHERE p2_puzzles.hash = ? + ", + p2_puzzle_hash + ) + .fetch_one(conn) + .await?; + + Ok(Clawback { + public_key: row.key.convert()?, + sender_puzzle_hash: row.sender_puzzle_hash.convert()?, + receiver_puzzle_hash: row.receiver_puzzle_hash.convert()?, + seconds: row.expiration_seconds.convert()?, + }) +} + +async fn option_underlying( + conn: impl SqliteExecutor<'_>, + p2_puzzle_hash: Bytes32, +) -> Result { + let p2_puzzle_hash = p2_puzzle_hash.as_ref(); + + let row = query!( + " + SELECT + key, expiration_seconds, creator_puzzle_hash, + option_asset.hash AS launcher_id, + strike_asset.hash AS strike_asset_hash, + strike_asset.id AS strike_asset_id, + strike_hidden_puzzle_hash, strike_settlement_puzzle_hash, + coins.amount AS underlying_amount, strike_amount + FROM p2_puzzles + INNER JOIN p2_options ON p2_options.p2_puzzle_id = p2_puzzles.id + INNER JOIN assets AS option_asset ON option_asset.id = p2_options.option_asset_id + INNER JOIN options ON options.asset_id = option_asset.id + INNER JOIN assets AS strike_asset ON strike_asset.id = options.strike_asset_id + INNER JOIN public_keys ON public_keys.p2_puzzle_id IN ( + SELECT id FROM p2_puzzles + WHERE hash = options.creator_puzzle_hash + AND unixepoch() >= options.expiration_seconds + ) + INNER JOIN coins ON coins.hash = options.underlying_coin_hash + WHERE p2_puzzles.hash = ? + ", + p2_puzzle_hash + ) + .fetch_one(conn) + .await?; + + Ok(OptionUnderlyingWithKey { + public_key: row.key.convert()?, + option: OptionUnderlying { + launcher_id: row.launcher_id.convert()?, + creator_puzzle_hash: row.creator_puzzle_hash.convert()?, + seconds: row.expiration_seconds.convert()?, + amount: row.underlying_amount.convert()?, + strike_type: if row.strike_asset_id == 0 { + OptionType::Xch { + amount: row.strike_amount.convert()?, + } + } else if let Some(settlement_puzzle_hash) = + row.strike_settlement_puzzle_hash.convert()? + { + OptionType::Nft { + launcher_id: row.strike_asset_hash.convert()?, + settlement_puzzle_hash, + amount: row.strike_amount.convert()?, + } + } else if let Some(hidden_puzzle_hash) = row.strike_hidden_puzzle_hash.convert()? { + OptionType::RevocableCat { + asset_id: row.strike_asset_hash.convert()?, + hidden_puzzle_hash, + amount: row.strike_amount.convert()?, + } + } else { + OptionType::Cat { + asset_id: row.strike_asset_hash.convert()?, + amount: row.strike_amount.convert()?, + } + }, + }, + }) +} + +async fn derivation( + conn: impl SqliteExecutor<'_>, + public_key: PublicKey, +) -> Result> { + let public_key = public_key.to_bytes(); + let public_key = public_key.as_ref(); + + let Some(row) = query!( + "SELECT derivation_index, is_hardened FROM public_keys WHERE key = ?", + public_key + ) + .fetch_optional(conn) + .await? + else { + return Ok(None); + }; + + Ok(Some(Derivation { + derivation_index: row.derivation_index.convert()?, + is_hardened: row.is_hardened, + })) +} diff --git a/crates/sage-database/src/tables/transactions.rs b/crates/sage-database/src/tables/transactions.rs new file mode 100644 index 000000000..a2dad95ac --- /dev/null +++ b/crates/sage-database/src/tables/transactions.rs @@ -0,0 +1,269 @@ +use chia::protocol::{Bytes32, Coin}; +use sqlx::{Row, SqliteExecutor}; + +use crate::{Asset, Convert, Database, Result}; + +#[derive(Debug, Clone)] +pub struct Transaction { + pub height: u32, + pub timestamp: Option, + pub spent: Vec, + pub created: Vec, +} + +#[derive(Debug, Clone)] +pub struct TransactionCoin { + pub coin: Coin, + pub asset: Asset, + pub p2_puzzle_hash: Option, +} + +impl Database { + pub async fn transaction(&self, height: u32) -> Result> { + transaction(&self.pool, height).await + } + + pub async fn transactions( + &self, + find_value: Option, + sort_ascending: bool, + limit: u32, + offset: u32, + ) -> Result<(Vec, u32)> { + transactions(&self.pool, find_value, sort_ascending, limit, offset).await + } +} + +// Helper function to create a TransactionCoin from a database row +fn create_transaction_coin(row: &sqlx::sqlite::SqliteRow) -> Result { + let coin = Coin::new( + row.get::, _>("parent_coin_hash").convert()?, + row.get::, _>("puzzle_hash").convert()?, + row.get::, _>("amount").convert()?, + ); + + let asset = Asset { + hash: row.get::, _>("asset_hash").convert()?, + name: row.get::, _>("asset_name"), + ticker: row.get::, _>("asset_ticker"), + precision: row.get::("asset_precision").convert()?, + icon_url: row.get::, _>("asset_icon_url"), + description: row.get::, _>("asset_description"), + is_sensitive_content: row.get::("asset_is_sensitive_content"), + is_visible: row.get::("asset_is_visible"), + kind: row + .get::, _>("asset_kind") + .map(Convert::convert) + .transpose()? + .unwrap_or(crate::AssetKind::Token), + }; + + let p2_puzzle_hash = row.get::>, _>("p2_puzzle_hash").convert()?; + + Ok(TransactionCoin { + coin, + asset, + p2_puzzle_hash, + }) +} + +async fn transaction(conn: impl SqliteExecutor<'_>, height: u32) -> Result> { + let rows = sqlx::query!( + "SELECT + height, timestamp, coin_id, puzzle_hash, parent_coin_hash, amount, + is_created_in_block, is_spent_in_block, asset_hash, asset_description, + asset_is_visible, asset_is_sensitive_content, asset_name, asset_icon_url, + asset_kind, p2_puzzle_hash, asset_ticker, asset_precision + FROM transaction_coins + WHERE height = ?", + height + ) + .fetch_all(conn) + .await?; + + if rows.is_empty() { + return Ok(None); + } + + let mut spent_coins = Vec::new(); + let mut created_coins = Vec::new(); + let mut timestamp = None; + + for row in rows { + if timestamp.is_none() { + timestamp = row.timestamp.map(|ts| ts as u64); + } + + let coin = Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ); + + let asset = Asset { + hash: row.asset_hash.convert()?, + name: row.asset_name, + ticker: row.asset_ticker, + precision: row.asset_precision.convert()?, + icon_url: row.asset_icon_url, + description: row.asset_description, + is_sensitive_content: row.asset_is_sensitive_content, + is_visible: row.asset_is_visible, + kind: row.asset_kind.convert()?, + }; + + let transaction_coin = TransactionCoin { + coin, + asset, + p2_puzzle_hash: row.p2_puzzle_hash.convert()?, + }; + + // these represent whether the coins was spent and/or created in this block + if row.is_spent_in_block == 1 { + spent_coins.push(transaction_coin.clone()); + } + + if row.is_created_in_block == 1 { + created_coins.push(transaction_coin); + } + } + + Ok(Some(Transaction { + height, + timestamp, + spent: spent_coins, + created: created_coins, + })) +} + +async fn transactions( + conn: impl SqliteExecutor<'_>, + find_value: Option, + sort_ascending: bool, + limit: u32, + offset: u32, +) -> Result<(Vec, u32)> { + let mut query = sqlx::QueryBuilder::new( + "SELECT + height, timestamp, coin_id, puzzle_hash, parent_coin_hash, amount, + is_created_in_block, is_spent_in_block, asset_hash, asset_description, + asset_is_visible, asset_is_sensitive_content, asset_name, asset_icon_url, + asset_kind, p2_puzzle_hash, asset_ticker, asset_precision, + COUNT(*) OVER() as total_count + FROM transaction_coins + WHERE 1=1", + ); + + if let Some(find_value) = find_value { + query.push(" AND (asset_name LIKE "); + query.push_bind(format!("%{find_value}%")); + query.push(" OR asset_ticker LIKE "); + query.push_bind(format!("%{find_value}%")); + + if is_valid_asset_id(&find_value) { + query.push(" OR asset_hash = X'"); + query.push(find_value.clone()); + query.push("'"); + } + + // match on nft or did launcher id + if let Some(puzzle_hash) = puzzle_hash_from_address(&find_value) { + query.push(" OR asset_hash = X'"); + query.push(puzzle_hash); + query.push("'"); + } + + // match on height if the find value is parsable as a u32 + if let Ok(height) = find_value.parse::() { + query.push(" OR height = "); + query.push_bind(height); + } + query.push(")"); + } + + if sort_ascending { + query.push(" ORDER BY height ASC"); + } else { + query.push(" ORDER BY height DESC"); + } + + query.push(" LIMIT "); + query.push_bind(limit); + query.push(" OFFSET "); + query.push_bind(offset); + let query = query.build(); + + let rows = query.fetch_all(conn).await?; + let total_count = rows + .first() + .map_or(Ok(0), |row| row.get::("total_count").try_into())?; + + let transactions = group_rows_into_transactions(rows, sort_ascending)?; + + Ok((transactions, total_count as u32)) +} + +pub fn is_valid_asset_id(asset_id: &str) -> bool { + asset_id.len() == 64 && asset_id.chars().all(|c| c.is_ascii_hexdigit()) +} + +fn puzzle_hash_from_address(address: &str) -> Option { + chia_wallet_sdk::utils::Address::decode(address) + .map(|decoded| hex::encode(decoded.puzzle_hash.as_ref())) + .ok() +} + +// Helper function to group rows by height and create Transaction structs +fn group_rows_into_transactions( + rows: Vec, + sort_ascending: bool, +) -> Result> { + use std::collections::HashMap; + + #[allow(clippy::type_complexity)] + let mut transactions_by_height: HashMap< + u32, + (Option, Vec, Vec), + > = HashMap::new(); + + for row in rows { + let height: u32 = row.get("height"); + let timestamp: Option = row.get("timestamp"); + let is_spent_in_block: i64 = row.get("is_spent_in_block"); + let is_created_in_block: i64 = row.get("is_created_in_block"); + + let transaction_coin = create_transaction_coin(&row)?; + + let entry = transactions_by_height + .entry(height) + .or_insert_with(|| (timestamp.map(|ts| ts as u64), Vec::new(), Vec::new())); + + // these represent whether the coin was spent and/or created in this block + if is_spent_in_block == 1 { + entry.1.push(transaction_coin.clone()); + } + + if is_created_in_block == 1 { + entry.2.push(transaction_coin); + } + } + + let mut transactions = Vec::new(); + for (height, (timestamp, spent_coins, created_coins)) in transactions_by_height { + transactions.push(Transaction { + height, + timestamp, + spent: spent_coins, + created: created_coins, + }); + } + + // Sort transactions by height to maintain the order from the SQL query + if sort_ascending { + transactions.sort_by_key(|tx| tx.height); + } else { + transactions.sort_by_key(|tx| std::cmp::Reverse(tx.height)); + } + + Ok(transactions) +} diff --git a/crates/sage-database/src/transactions.rs b/crates/sage-database/src/transactions.rs deleted file mode 100644 index 9f871d2b2..000000000 --- a/crates/sage-database/src/transactions.rs +++ /dev/null @@ -1,367 +0,0 @@ -use chia::{ - bls::Signature, - protocol::{Bytes32, Coin, CoinSpend, Program}, -}; -use sqlx::SqliteExecutor; - -use crate::{to_bytes, to_bytes32, Database, DatabaseTx, Result}; - -#[derive(Debug, Clone, Copy)] -pub struct TransactionRow { - pub transaction_id: Bytes32, - pub fee: u64, - pub submitted_at: Option, -} - -impl Database { - pub async fn update_transaction_mempool_time( - &self, - transaction_id: Bytes32, - timestamp: i64, - ) -> Result<()> { - update_transaction_mempool_time(&self.pool, transaction_id, timestamp).await - } - - pub async fn transactions(&self) -> Result> { - transactions(&self.pool).await - } - - pub async fn resubmittable_transactions( - &self, - threshold: i64, - ) -> Result> { - resubmittable_transactions(&self.pool, threshold).await - } - - pub async fn coin_spends(&self, transaction_id: Bytes32) -> Result> { - coin_spends(&self.pool, transaction_id).await - } - - pub async fn coin_transaction_id(&self, coin_id: Bytes32) -> Result> { - coin_transaction_id(&self.pool, coin_id).await - } -} - -impl DatabaseTx<'_> { - pub async fn insert_pending_transaction( - &mut self, - transaction_id: Bytes32, - aggregated_signature: Signature, - fee: u64, - ) -> Result<()> { - insert_pending_transaction(&mut *self.tx, transaction_id, aggregated_signature, fee).await - } - - pub async fn insert_transaction_spend( - &mut self, - transaction_id: Bytes32, - coin_spend: CoinSpend, - index: usize, - ) -> Result<()> { - insert_transaction_spend(&mut *self.tx, transaction_id, coin_spend, index).await - } - - pub async fn confirm_coins(&mut self, transaction_id: Bytes32) -> Result<()> { - confirm_coins(&mut *self.tx, transaction_id).await - } - - pub async fn remove_transaction(&mut self, transaction_id: Bytes32) -> Result<()> { - remove_transaction(&mut *self.tx, transaction_id).await - } - - pub async fn transaction_for_spent_coin( - &mut self, - coin_id: Bytes32, - ) -> Result> { - transaction_for_spent_coin(&mut *self.tx, coin_id).await - } - - pub async fn transaction_coin_ids(&mut self, transaction_id: Bytes32) -> Result> { - transaction_coin_ids(&mut *self.tx, transaction_id).await - } -} - -async fn insert_pending_transaction( - conn: impl SqliteExecutor<'_>, - transaction_id: Bytes32, - aggregated_signature: Signature, - fee: u64, -) -> Result<()> { - let transaction_id = transaction_id.as_ref(); - let aggregated_signature = aggregated_signature.to_bytes(); - let aggregated_signature = aggregated_signature.as_ref(); - let fee = fee.to_be_bytes(); - let fee = fee.as_ref(); - - sqlx::query!( - " - INSERT INTO `transactions` ( - `transaction_id`, - `aggregated_signature`, - `fee` - ) - VALUES (?, ?, ?) - ", - transaction_id, - aggregated_signature, - fee - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn insert_transaction_spend( - conn: impl SqliteExecutor<'_>, - transaction_id: Bytes32, - coin_spend: CoinSpend, - index: usize, -) -> Result<()> { - let coin_id = coin_spend.coin.coin_id(); - let coin_id = coin_id.as_ref(); - let index: i64 = index.try_into()?; - let transaction_id = transaction_id.as_ref(); - let parent_coin_id = coin_spend.coin.parent_coin_info.as_ref(); - let puzzle_hash = coin_spend.coin.puzzle_hash.as_ref(); - let amount = coin_spend.coin.amount.to_be_bytes(); - let amount = amount.as_ref(); - let puzzle_reveal = coin_spend.puzzle_reveal.as_ref(); - let solution = coin_spend.solution.as_ref(); - - sqlx::query!( - " - INSERT INTO `transaction_spends` ( - `coin_id`, - `index`, - `transaction_id`, - `parent_coin_id`, - `puzzle_hash`, - `amount`, - `puzzle_reveal`, - `solution` - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ", - coin_id, - index, - transaction_id, - parent_coin_id, - puzzle_hash, - amount, - puzzle_reveal, - solution - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn update_transaction_mempool_time( - conn: impl SqliteExecutor<'_>, - transaction_id: Bytes32, - timestamp: i64, -) -> Result<()> { - let transaction_id = transaction_id.as_ref(); - - sqlx::query!( - " - UPDATE `transactions` - SET `submitted_at` = ? - WHERE `transaction_id` = ? - ", - timestamp, - transaction_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn transactions(conn: impl SqliteExecutor<'_>) -> Result> { - let rows = sqlx::query!( - " - SELECT - `transaction_id`, - `fee`, - `submitted_at` - FROM `transactions` - ORDER BY `submitted_at` DESC, `transaction_id` ASC - " - ) - .fetch_all(conn) - .await?; - - rows.into_iter() - .map(|row| { - Ok(TransactionRow { - transaction_id: to_bytes32(&row.transaction_id)?, - fee: u64::from_be_bytes(to_bytes(&row.fee)?), - submitted_at: row.submitted_at, - }) - }) - .collect() -} - -async fn coin_transaction_id( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, -) -> Result> { - let coin_id = coin_id.as_ref(); - - sqlx::query!( - " - SELECT `transaction_id` - FROM `transaction_spends` - WHERE `coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - .map(|row| to_bytes32(&row.transaction_id)) - .transpose() -} - -async fn resubmittable_transactions( - conn: impl SqliteExecutor<'_>, - threshold: i64, -) -> Result> { - let rows = sqlx::query!( - " - SELECT - `transaction_id`, - `aggregated_signature` - FROM `transactions` - WHERE `submitted_at` IS NULL OR `submitted_at` <= ? - ", - threshold - ) - .fetch_all(conn) - .await?; - - rows.into_iter() - .map(|row| { - Ok(( - to_bytes32(&row.transaction_id)?, - Signature::from_bytes(&to_bytes(&row.aggregated_signature)?)?, - )) - }) - .collect() -} - -async fn coin_spends( - conn: impl SqliteExecutor<'_>, - transaction_id: Bytes32, -) -> Result> { - let transaction_id = transaction_id.as_ref(); - - let rows = sqlx::query!( - " - SELECT - `parent_coin_id`, - `puzzle_hash`, - `amount`, - `puzzle_reveal`, - `solution` - FROM `transaction_spends` INDEXED BY `indexed_spend` - WHERE `transaction_id` = ? - ORDER BY `index` ASC - ", - transaction_id - ) - .fetch_all(conn) - .await?; - - rows.into_iter() - .map(|row| { - Ok(CoinSpend::new( - Coin { - parent_coin_info: to_bytes32(&row.parent_coin_id)?, - puzzle_hash: to_bytes32(&row.puzzle_hash)?, - amount: u64::from_be_bytes(to_bytes(&row.amount)?), - }, - Program::from(row.puzzle_reveal), - Program::from(row.solution), - )) - }) - .collect() -} - -async fn remove_transaction(conn: impl SqliteExecutor<'_>, transaction_id: Bytes32) -> Result<()> { - let transaction_id = transaction_id.as_ref(); - - sqlx::query!( - " - DELETE FROM `transactions` - WHERE `transaction_id` = ? - ", - transaction_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn confirm_coins(conn: impl SqliteExecutor<'_>, transaction_id: Bytes32) -> Result<()> { - let transaction_id = transaction_id.as_ref(); - - sqlx::query!( - " - UPDATE `coin_states` - SET `transaction_id` = NULL - WHERE `transaction_id` = ? - ", - transaction_id - ) - .execute(conn) - .await?; - - Ok(()) -} - -async fn transaction_for_spent_coin( - conn: impl SqliteExecutor<'_>, - coin_id: Bytes32, -) -> Result> { - let coin_id = coin_id.as_ref(); - - let Some(row) = sqlx::query!( - " - SELECT `transaction_id` - FROM `transaction_spends` - WHERE `coin_id` = ? - ", - coin_id - ) - .fetch_optional(conn) - .await? - else { - return Ok(None); - }; - - Ok(Some(to_bytes32(&row.transaction_id)?)) -} - -async fn transaction_coin_ids( - conn: impl SqliteExecutor<'_>, - transaction_id: Bytes32, -) -> Result> { - let transaction_id = transaction_id.as_ref(); - - let rows = sqlx::query!( - " - SELECT `coin_id` FROM `transaction_spends` WHERE `transaction_id` = ? - ", - transaction_id - ) - .fetch_all(conn) - .await?; - - rows.into_iter() - .map(|row| to_bytes32(&row.coin_id)) - .collect() -} diff --git a/crates/sage-database/src/utils.rs b/crates/sage-database/src/utils.rs index 4d7ecd83a..577e671f1 100644 --- a/crates/sage-database/src/utils.rs +++ b/crates/sage-database/src/utils.rs @@ -1,17 +1,81 @@ -use chia::protocol::Bytes32; +use chia::{ + bls::{PublicKey, Signature}, + protocol::BytesImpl, +}; use crate::{DatabaseError, Result}; -pub fn to_bytes(slice: &[u8]) -> Result<[u8; N]> { - slice - .try_into() - .map_err(|_| DatabaseError::InvalidLength(slice.len(), N)) +pub trait Convert { + fn convert(self) -> Result; } -pub fn to_bytes32(slice: &[u8]) -> Result { - to_bytes(slice).map(Bytes32::new) +impl Convert<[u8; N]> for Vec { + fn convert(self) -> Result<[u8; N]> { + let length = self.len(); + self.try_into() + .map_err(|_| DatabaseError::InvalidLength(length, N)) + } } -pub fn to_u64(slice: &[u8]) -> Result { - Ok(u64::from_be_bytes(to_bytes::<8>(slice)?)) +impl Convert> for Vec { + fn convert(self) -> Result> { + Ok(BytesImpl::new(self.convert()?)) + } +} + +impl Convert for Vec { + fn convert(self) -> Result { + Ok(PublicKey::from_bytes(&self.convert()?)?) + } +} + +impl Convert for Vec { + fn convert(self) -> Result { + Ok(Signature::from_bytes(&self.convert()?)?) + } +} + +impl Convert for Vec { + fn convert(self) -> Result { + Ok(u64::from_be_bytes(self.convert()?)) + } +} + +impl Convert for Vec { + fn convert(self) -> Result { + Ok(u128::from_be_bytes(self.convert()?)) + } +} + +impl Convert for i64 { + fn convert(self) -> Result { + Ok(self.try_into()?) + } +} + +impl Convert for i64 { + fn convert(self) -> Result { + Ok(self.try_into()?) + } +} + +impl Convert for i64 { + fn convert(self) -> Result { + Ok(self.try_into()?) + } +} + +impl Convert for i64 { + fn convert(self) -> Result { + Ok(self.try_into()?) + } +} + +impl Convert> for Option +where + U: Convert, +{ + fn convert(self) -> Result> { + self.map(U::convert).transpose() + } } diff --git a/crates/sage-wallet/Cargo.toml b/crates/sage-wallet/Cargo.toml index e785a64d8..b42130f38 100644 --- a/crates/sage-wallet/Cargo.toml +++ b/crates/sage-wallet/Cargo.toml @@ -32,9 +32,9 @@ itertools = { workspace = true } futures-util = { workspace = true } futures-lite = { workspace = true } reqwest = { workspace = true, default-features = false, features = ["http2", "rustls-tls-webpki-roots", "json"] } -serde = { workspace = true, features = ["derive"] } indexmap = { workspace = true } rand = { workspace = true } +hex = { workspace = true } [dev-dependencies] anyhow = { workspace = true } @@ -44,4 +44,4 @@ tokio = { workspace = true } hex-literal = { workspace = true } [package.metadata.cargo-machete] -ignored = ["chia-sha2", "chia-traits"] +ignored = ["chia-traits"] diff --git a/crates/sage-wallet/src/child_kind.rs b/crates/sage-wallet/src/child_kind.rs index 3af3e6af7..5625c1079 100644 --- a/crates/sage-wallet/src/child_kind.rs +++ b/crates/sage-wallet/src/child_kind.rs @@ -1,12 +1,13 @@ use chia::{ clvm_traits::{FromClvm, ToClvm}, + clvm_utils::ToTreeHash, protocol::{Bytes32, Coin, Program}, - puzzles::{nft::NftMetadata, LineageProof, Proof}, + puzzles::{nft::NftMetadata, LineageProof, Memos, Proof}, }; use chia_puzzles::SINGLETON_LAUNCHER_HASH; use chia_wallet_sdk::{ - driver::{Cat, Did, DidInfo, HashedPtr, Nft, NftInfo, Puzzle}, - prelude::Memos, + driver::{Cat, CatInfo, ClawbackV2, Did, DidInfo, HashedPtr, Nft, NftInfo, Puzzle}, + prelude::CreateCoin, types::{run_puzzle, Condition}, }; use clvmr::{Allocator, NodePtr}; @@ -17,23 +18,26 @@ use crate::WalletError; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum ChildKind { - Unknown { - hint: Option, - }, + Unknown, Launcher, + Clawback { + info: ClawbackV2, + }, Cat { - asset_id: Bytes32, - p2_puzzle_hash: Bytes32, + info: CatInfo, lineage_proof: LineageProof, + clawback: Option, }, Did { info: DidInfo, lineage_proof: LineageProof, + clawback: Option, }, Nft { info: NftInfo, lineage_proof: LineageProof, metadata: Option, + clawback: Option, }, } @@ -53,24 +57,84 @@ impl ChildKind { let output = run_puzzle(&mut allocator, parent_puzzle.ptr(), parent_solution)?; let conditions = Vec::::from_clvm(&allocator, output)?; + let Some(create_coin) = conditions + .into_iter() + .filter_map(Condition::into_create_coin) + .find(|create_coin| { + let child_coin = Coin::new( + parent_coin.coin_id(), + create_coin.puzzle_hash, + create_coin.amount, + ); + + child_coin == coin + }) + else { + return Ok(Self::Unknown); + }; + Self::from_parent_cached( &mut allocator, parent_coin, parent_puzzle, parent_solution, - conditions, - coin, + create_coin, ) } - pub fn from_parent_cached( + pub fn parse_children( + parent_coin: Coin, + parent_puzzle: &Program, + parent_solution: &Program, + ) -> Result, WalletError> { + let mut allocator = Allocator::new(); + + let parent_puzzle_ptr = parent_puzzle.to_clvm(&mut allocator)?; + let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle_ptr); + let parent_solution = parent_solution.to_clvm(&mut allocator)?; + + let output = run_puzzle(&mut allocator, parent_puzzle.ptr(), parent_solution)?; + let conditions = Vec::::from_clvm(&allocator, output)?; + + Ok(conditions + .into_iter() + .filter_map(Condition::into_create_coin) + .filter_map(|create_coin| { + Self::from_parent_cached( + &mut allocator, + parent_coin, + parent_puzzle, + parent_solution, + create_coin, + ) + .ok() + .map(|kind| { + ( + Coin::new( + parent_coin.coin_id(), + create_coin.puzzle_hash, + create_coin.amount, + ), + kind, + ) + }) + }) + .collect()) + } + + fn from_parent_cached( allocator: &mut Allocator, parent_coin: Coin, parent_puzzle: Puzzle, parent_solution: NodePtr, - conditions: Vec, - coin: Coin, + create_coin: CreateCoin, ) -> Result { + let coin = Coin::new( + parent_coin.coin_id(), + create_coin.puzzle_hash, + create_coin.amount, + ); + let parse_span = debug_span!( "parse from parent", parent_coin = %parent_coin.coin_id(), @@ -82,46 +146,32 @@ impl ChildKind { return Ok(Self::Launcher); } - let Some(create_coin) = conditions - .into_iter() - .filter_map(Condition::into_create_coin) - .find(|cond| cond.puzzle_hash == coin.puzzle_hash && cond.amount == coin.amount) - else { - return Ok(Self::Unknown { hint: None }); - }; - - let hint = if let Memos::Some(memos) = create_coin.memos { - let memos = <(Bytes32, NodePtr)>::from_clvm(allocator, memos).ok(); - memos.map(|memos| memos.0) - } else { - None - }; - - let unknown = Self::Unknown { hint }; - match Cat::parse_children(allocator, parent_coin, parent_puzzle, parent_solution) { // If there was an error parsing the CAT, we can exit early. Err(error) => { warn!("Invalid CAT: {}", error); - return Ok(unknown); + return Ok(Self::Unknown); } // If the coin is a CAT coin, return the relevant information. Ok(Some(cats)) => { let Some(cat) = cats.into_iter().find(|cat| cat.coin == coin) else { warn!("CAT coin not found in children"); - return Ok(unknown); + return Ok(Self::Unknown); }; // We don't support parsing eve CATs during syncing. let Some(lineage_proof) = cat.lineage_proof else { - return Ok(unknown); + return Ok(Self::Unknown); }; + let clawback = parse_clawback_unchecked(allocator, &create_coin, true) + .filter(|clawback| clawback.tree_hash() == cat.info.p2_puzzle_hash.into()); + return Ok(Self::Cat { - asset_id: cat.info.asset_id, + info: cat.info, lineage_proof, - p2_puzzle_hash: cat.info.p2_puzzle_hash, + clawback, }); } @@ -129,88 +179,200 @@ impl ChildKind { Ok(None) => {} } - match Nft::::parse_child(allocator, parent_coin, parent_puzzle, parent_solution) - { - // If there was an error parsing the NFT, we can exit early. - Err(error) => { - warn!("Invalid NFT: {}", error); - return Ok(unknown); + if coin.amount % 2 == 1 { + match Nft::::parse_child( + allocator, + parent_coin, + parent_puzzle, + parent_solution, + ) { + // If there was an error parsing the NFT, we can exit early. + Err(error) => { + warn!("Invalid NFT: {}", error); + return Ok(Self::Unknown); + } + + // If the coin is a NFT coin, return the relevant information. + Ok(Some(nft)) => { + if nft.coin != coin { + warn!( + "NFT coin {:?} does not match expected coin {:?}", + nft.coin, coin + ); + return Ok(Self::Unknown); + } + + // We don't support parsing eve NFTs during syncing. + let Proof::Lineage(lineage_proof) = nft.proof else { + return Ok(Self::Unknown); + }; + + let metadata_program = Program::from_clvm(allocator, nft.info.metadata.ptr())?; + let metadata = NftMetadata::from_clvm(allocator, nft.info.metadata.ptr()).ok(); + + let clawback = parse_clawback_unchecked(allocator, &create_coin, true) + .filter(|clawback| clawback.tree_hash() == nft.info.p2_puzzle_hash.into()); + + return Ok(Self::Nft { + lineage_proof, + info: nft.info.with_metadata(metadata_program), + metadata, + clawback, + }); + } + + // If the coin is not a NFT coin, continue parsing. + Ok(None) => {} } - // If the coin is a NFT coin, return the relevant information. - Ok(Some(nft)) => { - if nft.coin != coin { - warn!( - "NFT coin {:?} does not match expected coin {:?}", - nft.coin, coin - ); - return Ok(unknown); + match Did::::parse_child( + allocator, + parent_coin, + parent_puzzle, + parent_solution, + coin, + ) { + // If there was an error parsing the DID, we can exit early. + Err(error) => { + warn!("Invalid DID: {}", error); + return Ok(Self::Unknown); } - // We don't support parsing eve NFTs during syncing. - let Proof::Lineage(lineage_proof) = nft.proof else { - return Ok(unknown); - }; + // If the coin is a DID coin, return the relevant information. + Ok(Some(did)) => { + // We don't support parsing eve DIDs during syncing. + let Proof::Lineage(lineage_proof) = did.proof else { + return Ok(Self::Unknown); + }; - let metadata_program = Program::from_clvm(allocator, nft.info.metadata.ptr())?; - let metadata = NftMetadata::from_clvm(allocator, nft.info.metadata.ptr()).ok(); + let metadata = Program::from_clvm(allocator, did.info.metadata.ptr())?; - return Ok(Self::Nft { - lineage_proof, - info: nft.info.with_metadata(metadata_program), - metadata, - }); - } + let clawback = parse_clawback_unchecked(allocator, &create_coin, true); - // If the coin is not a NFT coin, continue parsing. - Ok(None) => {} - } + let did = if let Some(clawback) = clawback { + let p2_puzzle_hash = clawback.tree_hash().into(); - match Did::::parse_child( - allocator, - parent_coin, - parent_puzzle, - parent_solution, - coin, - ) { - // If there was an error parsing the DID, we can exit early. - Err(error) => { - warn!("Invalid DID: {}", error); - return Ok(unknown); - } + let clawback_did = Did::new( + did.coin, + did.proof, + DidInfo::new( + did.info.launcher_id, + did.info.recovery_list_hash, + did.info.num_verifications_required, + did.info.metadata, + p2_puzzle_hash, + ), + ); - // If the coin is a DID coin, return the relevant information. - Ok(Some(did)) => { - // We don't support parsing eve DIDs during syncing. - let Proof::Lineage(lineage_proof) = did.proof else { - return Ok(unknown); - }; + if clawback_did.info.puzzle_hash() == coin.puzzle_hash.into() { + clawback_did + } else { + did + } + } else { + did + }; - let metadata = Program::from_clvm(allocator, did.info.metadata.ptr())?; + return Ok(Self::Did { + lineage_proof, + info: did.info.with_metadata(metadata), + clawback, + }); + } - return Ok(Self::Did { - lineage_proof, - info: did.info.with_metadata(metadata), - }); + // If the coin is not a DID coin, continue parsing. + Ok(None) => {} } + } - // If the coin is not a DID coin, continue parsing. - Ok(None) => {} + if let Some(clawback) = parse_clawback_unchecked(allocator, &create_coin, false) + .filter(|clawback| clawback.tree_hash() == coin.puzzle_hash.into()) + { + return Ok(Self::Clawback { info: clawback }); } - Ok(unknown) + Ok(Self::Unknown) + } + + pub fn custody_p2_puzzle_hashes(&self) -> Vec { + match self { + // TODO: Should we add the puzzle hash of the coin as a candidate? + Self::Launcher | Self::Unknown => vec![], + Self::Clawback { info } => vec![info.sender_puzzle_hash, info.receiver_puzzle_hash], + Self::Cat { info, clawback, .. } => clawback + .map_or(vec![info.p2_puzzle_hash], |clawback| { + vec![clawback.sender_puzzle_hash, clawback.receiver_puzzle_hash] + }), + Self::Did { info, clawback, .. } => clawback + .map_or(vec![info.p2_puzzle_hash], |clawback| { + vec![clawback.sender_puzzle_hash, clawback.receiver_puzzle_hash] + }), + Self::Nft { info, clawback, .. } => clawback + .map_or(vec![info.p2_puzzle_hash], |clawback| { + vec![clawback.sender_puzzle_hash, clawback.receiver_puzzle_hash] + }), + } } - pub fn p2_puzzle_hash(&self) -> Option { + pub fn receiver_custody_p2_puzzle_hash(&self) -> Option { match self { - Self::Launcher | Self::Unknown { .. } => None, - Self::Cat { p2_puzzle_hash, .. } => Some(*p2_puzzle_hash), - Self::Did { info, .. } => Some(info.p2_puzzle_hash), - Self::Nft { info, .. } => Some(info.p2_puzzle_hash), + Self::Launcher | Self::Unknown => None, + Self::Clawback { info } => Some(info.receiver_puzzle_hash), + Self::Cat { info, clawback, .. } => clawback + .map_or(Some(info.p2_puzzle_hash), |clawback| { + Some(clawback.receiver_puzzle_hash) + }), + Self::Did { info, clawback, .. } => clawback + .map_or(Some(info.p2_puzzle_hash), |clawback| { + Some(clawback.receiver_puzzle_hash) + }), + Self::Nft { info, clawback, .. } => clawback + .map_or(Some(info.p2_puzzle_hash), |clawback| { + Some(clawback.receiver_puzzle_hash) + }), } } pub fn subscribe(&self) -> bool { - matches!(self, Self::Cat { .. } | Self::Did { .. } | Self::Nft { .. }) + matches!( + self, + Self::Clawback { .. } | Self::Cat { .. } | Self::Did { .. } | Self::Nft { .. } + ) } } + +fn parse_clawback_unchecked( + allocator: &Allocator, + create_coin: &CreateCoin, + hinted: bool, +) -> Option { + let Memos::Some(memos) = create_coin.memos else { + return None; + }; + + let (hint, (clawback_memo, _)) = + <(Bytes32, (NodePtr, NodePtr))>::from_clvm(allocator, memos).ok()?; + + clawback_from_memo_unchecked(allocator, clawback_memo, hint, create_coin.amount, hinted) +} + +pub fn clawback_from_memo_unchecked( + allocator: &Allocator, + memo: NodePtr, + receiver_puzzle_hash: Bytes32, + amount: u64, + hinted: bool, +) -> Option { + let (sender_puzzle_hash, (seconds, ())) = + <(Bytes32, (u64, ()))>::from_clvm(allocator, memo).ok()?; + + let clawback = ClawbackV2 { + sender_puzzle_hash, + receiver_puzzle_hash, + seconds, + amount, + hinted, + }; + + Some(clawback) +} diff --git a/crates/sage-wallet/src/coin_kind.rs b/crates/sage-wallet/src/coin_kind.rs index 051b4ce73..7fb2506f3 100644 --- a/crates/sage-wallet/src/coin_kind.rs +++ b/crates/sage-wallet/src/coin_kind.rs @@ -1,10 +1,10 @@ use chia::{ clvm_traits::{FromClvm, ToClvm}, - protocol::{Bytes32, Program}, + protocol::Program, puzzles::nft::NftMetadata, }; use chia_puzzles::SINGLETON_LAUNCHER_HASH; -use chia_wallet_sdk::driver::{CatLayer, DidInfo, HashedPtr, Layer, NftInfo, Puzzle}; +use chia_wallet_sdk::driver::{CatInfo, DidInfo, HashedPtr, NftInfo, Puzzle}; use clvmr::Allocator; use tracing::{debug_span, warn}; @@ -16,8 +16,7 @@ pub enum CoinKind { Unknown, Launcher, Cat { - asset_id: Bytes32, - p2_puzzle_hash: Bytes32, + info: CatInfo, }, Did { info: DidInfo, @@ -46,7 +45,7 @@ impl CoinKind { return Ok(Self::Launcher); } - match CatLayer::::parse_puzzle(allocator, puzzle) { + match CatInfo::parse(allocator, puzzle) { // If there was an error parsing the CAT, we can exit early. Err(error) => { warn!("Invalid CAT: {}", error); @@ -54,11 +53,8 @@ impl CoinKind { } // If the coin is a CAT coin, return the relevant information. - Ok(Some(cat)) => { - return Ok(Self::Cat { - asset_id: cat.asset_id, - p2_puzzle_hash: cat.inner_puzzle.tree_hash().into(), - }); + Ok(Some((cat, _inner_puzzle))) => { + return Ok(Self::Cat { info: cat }); } // If the coin is not a CAT coin, continue parsing. diff --git a/crates/sage-wallet/src/database.rs b/crates/sage-wallet/src/database.rs index 728abd312..4d4b44b7d 100644 --- a/crates/sage-wallet/src/database.rs +++ b/crates/sage-wallet/src/database.rs @@ -1,287 +1,272 @@ -use std::{ - collections::HashMap, - time::{Duration, Instant}, -}; +use std::collections::{HashMap, HashSet}; use chia::{ bls::Signature, - protocol::{Bytes32, CoinState}, + clvm_utils::ToTreeHash, + protocol::{Bytes32, CoinState, Program}, + puzzles::{nft::NftMetadata, LineageProof}, }; -use sage_database::{CatRow, CoinKind, Database, DatabaseTx, DidRow, NftRow}; +use chia_wallet_sdk::driver::NftInfo; +use sage_assets::base64_data_uri; +use sage_database::{Asset, AssetKind, Database, DatabaseTx, DidCoinInfo, NftCoinInfo}; +use tracing::warn; use crate::{compute_nft_info, fetch_nft_did, ChildKind, Transaction, WalletError, WalletPeer}; -#[derive(Debug, Default, Clone, Copy)] -pub struct UpsertCounters { - pub is_p2: Duration, - pub insert_coin_state: Duration, - pub update_coin_state: Duration, - pub insert_p2_coin: Duration, - pub update_created_puzzle: Duration, - pub delete_puzzle: Duration, -} - -pub async fn upsert_coin( +pub async fn insert_puzzle( tx: &mut DatabaseTx<'_>, coin_state: CoinState, - transaction_id: Option, - counters: &mut UpsertCounters, -) -> Result<(), WalletError> { + info: ChildKind, + minter_hash: Option, +) -> Result { let coin_id = coin_state.coin.coin_id(); - // Check if the coin is plain XCH, rather than an asset that wraps the p2 puzzle hash. - let start = Instant::now(); - let is_p2 = tx.is_p2_puzzle_hash(coin_state.coin.puzzle_hash).await?; - counters.is_p2 += start.elapsed(); + let custody_p2_puzzle_hashes = info.custody_p2_puzzle_hashes(); - // If the coin is XCH, there's no reason to sync the puzzle. - let start = Instant::now(); - tx.insert_coin_state(coin_state, is_p2, transaction_id) - .await?; - counters.insert_coin_state += start.elapsed(); - - // If the coin already existed, instead of replacing it we will just update it. - let start = Instant::now(); - tx.update_coin_state( - coin_id, - coin_state.created_height, - coin_state.spent_height, - transaction_id, - ) - .await?; - counters.update_coin_state += start.elapsed(); - - // This allows querying for XCH coins without joining on the derivations table. - if is_p2 { - let start = Instant::now(); - tx.insert_p2_coin(coin_id).await?; - counters.insert_p2_coin += start.elapsed(); - } else { - let start = Instant::now(); - update_created_puzzle(tx, coin_state).await?; - counters.update_created_puzzle += start.elapsed(); - } + let mut is_relevant = false; - Ok(()) -} + for custody_p2_puzzle_hash in custody_p2_puzzle_hashes { + if tx.is_custody_p2_puzzle_hash(custody_p2_puzzle_hash).await? { + is_relevant = true; + break; + } + } -pub async fn insert_puzzle( - tx: &mut DatabaseTx<'_>, - coin_state: CoinState, - info: ChildKind, - minter_did: Option, -) -> Result<(), WalletError> { - let coin_id = coin_state.coin.coin_id(); + if !is_relevant { + warn!( + "Deleting coin {} because it isn't relevant to this wallet", + coin_id + ); + tx.delete_coin(coin_id).await?; + return Ok(false); + } match info { - ChildKind::Launcher | ChildKind::Unknown { .. } => {} + ChildKind::Launcher | ChildKind::Unknown => { + warn!("Deleting coin {} because it has an unknown puzzle", coin_id); + tx.delete_coin(coin_id).await?; + return Ok(false); + } + ChildKind::Clawback { info } => { + tx.insert_clawback_p2_puzzle(info).await?; + + tx.update_coin(coin_id, Bytes32::default(), info.tree_hash().into(), None) + .await?; + } ChildKind::Cat { - asset_id, + info, lineage_proof, - p2_puzzle_hash, + clawback, } => { - tx.sync_coin(coin_id, Some(p2_puzzle_hash), CoinKind::Cat) - .await?; - tx.insert_cat(CatRow { - asset_id, + tx.insert_lineage_proof(coin_id, lineage_proof).await?; + + tx.insert_asset(Asset { + hash: info.asset_id, name: None, ticker: None, + precision: 3, + icon_url: None, description: None, - icon: None, - visible: true, - fetched: false, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Token, }) .await?; - tx.insert_cat_coin(coin_id, lineage_proof, p2_puzzle_hash, asset_id) - .await?; + + if let Some(clawback) = clawback { + tx.insert_clawback_p2_puzzle(clawback).await?; + } + + tx.update_coin( + coin_id, + info.asset_id, + info.p2_puzzle_hash, + info.hidden_puzzle_hash, + ) + .await?; } ChildKind::Did { lineage_proof, info, + clawback, } => { - let launcher_id = info.launcher_id; - - tx.sync_coin(coin_id, Some(info.p2_puzzle_hash), CoinKind::Did) - .await?; - tx.insert_did_coin(coin_id, lineage_proof, info).await?; + tx.insert_lineage_proof(coin_id, lineage_proof).await?; - let name = tx.get_future_did_name(launcher_id).await?; + let coin_info = DidCoinInfo { + metadata: info.metadata, + recovery_list_hash: info.recovery_list_hash, + num_verifications_required: info.num_verifications_required, + }; - if name.is_some() { - tx.delete_future_did_name(launcher_id).await?; - } + tx.insert_asset(Asset { + hash: info.launcher_id, + name: None, + ticker: None, + precision: 1, + icon_url: None, + description: None, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Did, + }) + .await?; - let mut row = tx.did_row(launcher_id).await?.unwrap_or(DidRow { - launcher_id, - coin_id, - name, - is_owned: coin_state.spent_height.is_none(), - visible: true, - created_height: coin_state.created_height, - }); - - if coin_state.spent_height.is_some() - && (row.created_height.is_none() - || row.created_height > coin_state.created_height - || row.is_owned) - { - return Ok(()); - } + tx.insert_did(info.launcher_id, &coin_info).await?; if coin_state.spent_height.is_none() { - row.is_owned = true; + tx.update_did(info.launcher_id, &coin_info).await?; } - row.coin_id = coin_id; - row.created_height = coin_state.created_height; + if let Some(clawback) = clawback { + tx.insert_clawback_p2_puzzle(clawback).await?; + } - tx.insert_did(row).await?; + tx.update_coin(coin_id, info.launcher_id, info.p2_puzzle_hash, None) + .await?; } ChildKind::Nft { lineage_proof, info, metadata, + clawback, } => { - let data_hash = metadata.as_ref().and_then(|m| m.data_hash); - let metadata_hash = metadata.as_ref().and_then(|m| m.metadata_hash); - let license_hash = metadata.as_ref().and_then(|m| m.license_hash); - let launcher_id = info.launcher_id; - let owner_did = info.current_owner; - - tx.sync_coin(coin_id, Some(info.p2_puzzle_hash), CoinKind::Nft) - .await?; + if let Some(clawback) = clawback { + tx.insert_clawback_p2_puzzle(clawback).await?; + } - tx.insert_nft_coin( - coin_id, - lineage_proof, + insert_nft( + tx, + coin_state, + Some(lineage_proof), info, - data_hash, - metadata_hash, - license_hash, + metadata, + minter_hash, ) .await?; + } + } - let mut row = tx.nft_row(launcher_id).await?.unwrap_or(NftRow { - launcher_id, - coin_id, - collection_id: None, - minter_did, - owner_did, - visible: true, - sensitive_content: false, - name: None, - is_owned: coin_state.spent_height.is_none(), - created_height: coin_state.created_height, - metadata_hash, - edition_number: metadata - .as_ref() - .and_then(|m| m.edition_number.try_into().ok()), - edition_total: metadata - .as_ref() - .and_then(|m| m.edition_total.try_into().ok()), - }); - - if coin_state.spent_height.is_some() - && (row.created_height.is_none() - || row.created_height > coin_state.created_height - || row.is_owned) - { - return Ok(()); - } - - if coin_state.spent_height.is_none() { - row.is_owned = true; - } - - let metadata_row = if let Some(metadata_hash) = metadata_hash { - tx.fetch_nft_data(metadata_hash).await? - } else { - None - }; - - let computed_info = compute_nft_info( - minter_did, - metadata_row.as_ref().map(|data| data.blob.as_slice()), - ); - - row.coin_id = coin_id; + Ok(true) +} - row.sensitive_content |= computed_info.sensitive_content; +pub async fn insert_nft( + tx: &mut DatabaseTx<'_>, + coin_state: CoinState, + lineage_proof: Option, + info: NftInfo, + metadata: Option, + minter_hash: Option, +) -> Result<(), WalletError> { + if let Some(lineage_proof) = lineage_proof { + tx.insert_lineage_proof(coin_state.coin.coin_id(), lineage_proof) + .await?; + } - if row.name.is_none() { - row.name = computed_info.name; + let icon_url = + if let Some(data_hash) = metadata.as_ref().and_then(|metadata| metadata.data_hash) { + tx.icon(data_hash).await?.map(|icon| { + base64_data_uri( + &icon.data, + &icon.mime_type.unwrap_or_else(|| "image/png".to_string()), + ) + }) + } else { + None + }; + + let mut asset = Asset { + hash: info.launcher_id, + name: None, + ticker: None, + precision: 1, + icon_url, + description: None, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Nft, + }; + + let mut coin_info = NftCoinInfo { + collection_hash: Bytes32::default(), + collection_name: None, + minter_hash, + owner_hash: info.current_owner, + metadata: info.metadata, + metadata_updater_puzzle_hash: info.metadata_updater_puzzle_hash, + royalty_puzzle_hash: info.royalty_puzzle_hash, + royalty_basis_points: info.royalty_basis_points, + data_hash: metadata.as_ref().and_then(|m| m.data_hash), + metadata_hash: metadata.as_ref().and_then(|m| m.metadata_hash), + license_hash: metadata.as_ref().and_then(|m| m.license_hash), + edition_number: metadata.as_ref().map(|m| m.edition_number), + edition_total: metadata.as_ref().map(|m| m.edition_total), + }; + + if let Some(metadata_hash) = &metadata.as_ref().and_then(|m| m.metadata_hash) { + if let Some(blob) = tx.file_data(*metadata_hash).await? { + let computed = compute_nft_info(minter_hash, &blob); + asset.name = computed.name; + asset.description = computed.description; + asset.is_sensitive_content = computed.sensitive_content; + + if let Some(collection) = computed.collection { + coin_info.collection_hash = collection.hash; + tx.insert_collection(collection).await?; } + } + }; - if row.collection_id.is_none() && metadata_row.is_some_and(|data| data.hash_matches) { - row.collection_id = computed_info - .collection - .as_ref() - .map(|col| col.collection_id); + tx.insert_asset(asset).await?; - if let Some(collection) = computed_info.collection { - tx.insert_collection(collection).await?; - } - } - - row.owner_did = owner_did; - row.created_height = coin_state.created_height; + tx.insert_nft(info.launcher_id, &coin_info).await?; - // Update edition information if not already set - if row.edition_number.is_none() { - row.edition_number = metadata - .as_ref() - .and_then(|m| m.edition_number.try_into().ok()); - } - if row.edition_total.is_none() { - row.edition_total = metadata - .as_ref() - .and_then(|m| m.edition_total.try_into().ok()); - } + if coin_state.spent_height.is_none() || lineage_proof.is_none() { + tx.update_nft(info.launcher_id, &coin_info).await?; + } - tx.insert_nft(row).await?; + if lineage_proof.is_some() { + tx.update_coin( + coin_state.coin.coin_id(), + info.launcher_id, + info.p2_puzzle_hash, + None, + ) + .await?; + } - if let Some(metadata) = metadata { - if let Some(hash) = data_hash { - for uri in metadata.data_uris { - tx.insert_nft_uri(uri, hash).await?; - } - } + let (data_uris, metadata_uris, license_uris) = metadata + .map(|metadata| { + ( + metadata.data_uris, + metadata.metadata_uris, + metadata.license_uris, + ) + }) + .unwrap_or_default(); - if let Some(hash) = metadata_hash { - for uri in metadata.metadata_uris { - tx.insert_nft_uri(uri, hash).await?; - } - } + if let Some(hash) = coin_info.data_hash { + tx.insert_file(hash).await?; - if let Some(hash) = license_hash { - for uri in metadata.license_uris { - tx.insert_nft_uri(uri, hash).await?; - } - } - } + for uri in data_uris { + tx.insert_file_uri(hash, uri).await?; } } - Ok(()) -} - -pub async fn delete_puzzle(tx: &mut DatabaseTx<'_>, coin_id: Bytes32) -> Result<(), WalletError> { - tx.set_did_not_owned(coin_id).await?; - tx.set_nft_not_owned(coin_id).await?; - Ok(()) -} + if let Some(hash) = coin_info.metadata_hash { + tx.insert_file(hash).await?; -pub async fn update_created_puzzle( - tx: &mut DatabaseTx<'_>, - coin_state: CoinState, -) -> Result<(), WalletError> { - let coin_id = coin_state.coin.coin_id(); + for uri in metadata_uris { + tx.insert_file_uri(hash, uri).await?; + } + } - tx.set_did_created_height(coin_id, coin_state.created_height) - .await?; + if let Some(hash) = coin_info.license_hash { + tx.insert_file(hash).await?; - tx.set_nft_created_height(coin_id, coin_state.created_height) - .await?; + for uri in license_uris { + tx.insert_file_uri(hash, uri).await?; + } + } Ok(()) } @@ -294,12 +279,19 @@ pub async fn insert_transaction( transaction: Transaction, aggregated_signature: Signature, ) -> Result, WalletError> { + // Make lookups faster for inputs and outputs, and prepare pending coin spends. let mut coin_spends = HashMap::new(); + let mut output_coin_ids = HashSet::new(); for input in &transaction.inputs { coin_spends.insert(input.coin_spend.coin.coin_id(), input.coin_spend.clone()); + + for output in &input.outputs { + output_coin_ids.insert(output.coin.coin_id()); + } } + // Fetch minter DIDs for created NFTs in the transaction from the blockchain. let mut minter_dids = HashMap::new(); for input in &transaction.inputs { @@ -316,88 +308,106 @@ pub async fn insert_transaction( } } + // Insert the transaction into the database. let mut tx = db.tx().await?; - tx.insert_pending_transaction(transaction_id, aggregated_signature, transaction.fee) + tx.insert_mempool_item(transaction_id, aggregated_signature, transaction.fee) .await?; - for coin_id in transaction - .inputs - .iter() - .map(|input| input.coin_spend.coin.coin_id()) - { - delete_puzzle(&mut tx, coin_id).await?; - } - let mut subscriptions = Vec::new(); - for (index, input) in transaction.inputs.into_iter().enumerate() { - tx.insert_transaction_spend(transaction_id, input.coin_spend, index) + for (index, input) in transaction.inputs.iter().enumerate() { + let input_coin_id = input.coin_spend.coin.coin_id(); + + // Insert the spend into the database in the proper order so it can be reconstructed later. + tx.insert_mempool_spend(transaction_id, input.coin_spend.clone(), index) .await?; - for output in input.outputs { + // If the coin isn't ephemeral (exists on-chain) and we already have it in the database, + // we can attach it to the transaction as our coin for display purposes. + if !output_coin_ids.contains(&input_coin_id) && tx.is_known_coin(input_coin_id).await? { + tx.insert_mempool_coin(transaction_id, input_coin_id, true, false) + .await?; + } + + for output in &input.outputs { + // Coins that don't exist on-chain yet don't have a created or spent height. let coin_state = CoinState::new(output.coin, None, None); let coin_id = output.coin.coin_id(); - if tx.is_p2_puzzle_hash(output.coin.puzzle_hash).await? { - tx.insert_coin_state(coin_state, true, Some(transaction_id)) + // If it's an XCH coin, we can insert it and sync it immediately. + // Attach it to the transaction as an output for display purposes. + if tx + .is_custody_p2_puzzle_hash(output.coin.puzzle_hash) + .await? + { + tx.insert_coin(coin_state).await?; + + tx.update_coin(coin_id, Bytes32::default(), output.coin.puzzle_hash, None) .await?; - tx.insert_p2_coin(coin_id).await?; + + tx.insert_mempool_coin( + transaction_id, + coin_id, + coin_spends.contains_key(&coin_id), + true, + ) + .await?; + continue; } - let Some(p2_puzzle_hash) = output.kind.p2_puzzle_hash() else { - continue; - }; + // We don't want to insert output coins that we won't own in the future. + let custody_p2_puzzle_hashes = output.kind.custody_p2_puzzle_hashes(); + + let mut is_relevant = false; + + for custody_p2_puzzle_hash in custody_p2_puzzle_hashes { + if tx.is_custody_p2_puzzle_hash(custody_p2_puzzle_hash).await? { + is_relevant = true; + break; + } + } - if !tx.is_p2_puzzle_hash(p2_puzzle_hash).await? { + if !is_relevant { continue; } - tx.insert_coin_state(coin_state, true, Some(transaction_id)) - .await?; - tx.sync_coin( + // Insert the coin into the database and attach it to the transaction as an output for display purposes. + tx.insert_coin(coin_state).await?; + + tx.insert_mempool_coin( + transaction_id, coin_id, - Some(p2_puzzle_hash), - match output.kind { - ChildKind::Unknown { .. } | ChildKind::Launcher => CoinKind::Unknown, - ChildKind::Cat { .. } => CoinKind::Cat, - ChildKind::Did { .. } => CoinKind::Did, - ChildKind::Nft { .. } => CoinKind::Nft, - }, + coin_spends.contains_key(&coin_id), + true, ) .await?; + // We should subscribe to the coin so we know when it's created on-chain. + // TODO: Is this necessary? Subscribing to the p2 puzzle hash is probably sufficient for created coins. if output.kind.subscribe() { subscriptions.push(coin_id); } + // Do the busy work of inserting the asset information into the database now that the coin exists. insert_puzzle( &mut tx, coin_state, - output.kind, + output.kind.clone(), minter_dids.get(&output.coin.coin_id()).copied(), ) .await?; } } - tx.commit().await?; - - Ok(subscriptions) -} - -pub async fn safely_remove_transaction( - tx: &mut DatabaseTx<'_>, - transaction_id: Bytes32, -) -> Result<(), WalletError> { - for coin_id in tx.transaction_coin_ids(transaction_id).await? { - if tx.is_p2_coin(coin_id).await? == Some(false) { - tx.unsync_coin(coin_id).await?; - } + for input in transaction.inputs { + // We are inserting the children as part of inserting the transaction, so we don't need to do it again + tx.set_children_synced(input.coin_spend.coin.coin_id()) + .await?; } - tx.remove_transaction(transaction_id).await?; + tx.commit().await?; - Ok(()) + Ok(subscriptions) } diff --git a/crates/sage-wallet/src/error.rs b/crates/sage-wallet/src/error.rs index 4319fcee1..2a0b16113 100644 --- a/crates/sage-wallet/src/error.rs +++ b/crates/sage-wallet/src/error.rs @@ -8,6 +8,7 @@ use chia_wallet_sdk::{ client::ClientError, driver::DriverError, signer::SignerError, utils::CoinSelectionError, }; use clvmr::reduction::EvalErr; +use sage_assets::UriError; use sage_database::DatabaseError; use thiserror::Error; use tokio::{task::JoinError, time::error::Elapsed}; @@ -32,12 +33,12 @@ pub enum WalletError { #[error("Request error: {0}")] Request(#[from] reqwest::Error), + #[error("URI error: {0}")] + Uri(#[from] UriError), + #[error("Timeout exceeded")] Elapsed(#[from] Elapsed), - #[error("Missing coin with id {0}")] - MissingCoin(Bytes32), - #[error("Missing spend with id {0}")] MissingSpend(Bytes32), @@ -77,32 +78,41 @@ pub enum WalletError { #[error("SECP is not supported")] SecpNotSupported, - #[error("Missing DID with id {0}. It may have been spent recently. Please try again later.")] - MissingDid(Bytes32), + #[error("Missing XCH coin with id {0}")] + MissingXchCoin(Bytes32), - #[error("Missing NFT with id {0}")] - MissingNft(Bytes32), + #[error("Missing CAT coin with id {0}")] + MissingCatCoin(Bytes32), - #[error("Uncancellable offer")] - UncancellableOffer, + #[error("Missing DID coin with id {0}")] + MissingDidCoin(Bytes32), - #[error("Invalid trade price")] - InvalidTradePrice, + #[error("Missing NFT coin with id {0}")] + MissingNftCoin(Bytes32), - #[error("Invalid royalty amount")] - InvalidRoyaltyAmount, + #[error("Missing coin with id {0}")] + MissingCoin(Bytes32), - #[error("Invalid requested payment")] - InvalidRequestedPayment, + #[error("Missing option contract coin with id {0}")] + MissingOptionCoin(Bytes32), - #[error("Unknown requested payment mod hash {0}")] - UnknownRequestedPayment(Bytes32), + #[error("Missing DID with id {0}. It may have been spent recently. Please try again later.")] + MissingDid(Bytes32), + + #[error("Missing NFT with id {0}. It may have been spent recently. Please try again later.")] + MissingNft(Bytes32), - #[error("Duplicate NFT requested payment with id {0}")] - DuplicateNftRequestedPayment(Bytes32), + #[error("Missing option contract with id {0}. It may have been spent recently. Please try again later.")] + MissingOptionContract(Bytes32), + + #[error("Missing asset with id {0}")] + MissingAsset(Bytes32), + + #[error("Uncancellable offer")] + UncancellableOffer, - #[error("Empty bulk transfer")] - EmptyBulkTransfer, + #[error("Cannot import offer that doesn't belong to this wallet")] + CannotImportOffer, #[error("Try from int error: {0}")] TryFromInt(#[from] TryFromIntError), diff --git a/crates/sage-wallet/src/queues/blocktime_queue.rs b/crates/sage-wallet/src/queues/blocktime_queue.rs index 407be9148..cee776329 100644 --- a/crates/sage-wallet/src/queues/blocktime_queue.rs +++ b/crates/sage-wallet/src/queues/blocktime_queue.rs @@ -44,12 +44,7 @@ impl BlockTimeQueue { let peers = self.state.lock().await.peers(); let limit = 5 * u32::try_from(peers.len())?; - let mut heights = self.db.find_created_timestamp_null(limit).await?; - heights.extend( - self.db - .find_spent_timestamp_null(limit.saturating_sub(heights.len().try_into()?)) - .await?, - ); + let heights = self.db.unsynced_blocks(limit).await?; if heights.is_empty() { return Ok(()); @@ -63,7 +58,7 @@ impl BlockTimeQueue { for peer in peers { for _ in 0..5 { if let Some(height) = heights.next() { - tasks.push(self.fetch_and_process_blockinfo(peer.clone(), height)); + tasks.push(self.fetch_block(peer.clone(), height)); } } } @@ -79,27 +74,13 @@ impl BlockTimeQueue { Ok(()) } - async fn fetch_and_process_blockinfo( - &self, - peer: WalletPeer, - height: u32, - ) -> Result<(), WalletError> { - let check_blockinfo = self.db.check_blockinfo(height).await?; - - if let Some(unix_time) = check_blockinfo { - self.update_coinstates(height, unix_time).await?; - return Ok(()); - } - + async fn fetch_block(&self, peer: WalletPeer, height: u32) -> Result<(), WalletError> { match peer.block_timestamp(height).await { - Ok(Some(timestamp)) => { - self.update_coinstates(height, timestamp.try_into()?) + Ok((header_hash, timestamp)) => { + self.db + .insert_block(height, header_hash, Some(timestamp.try_into()?), false) .await?; } - Ok(None) => { - error!("No timestamp found for block {height}"); - return Err(WalletError::PeerMisbehaved); - } Err(error) => { error!("Failed to fetch block {height} timestamp: {error}"); return Err(error); @@ -108,12 +89,4 @@ impl BlockTimeQueue { Ok(()) } - - async fn update_coinstates(&self, height: u32, timestamp: i64) -> Result<(), WalletError> { - self.db.update_created_timestamp(height, timestamp).await?; - self.db.update_spent_timestamp(height, timestamp).await?; - self.db.insert_timestamp_height(height, timestamp).await?; - - Ok(()) - } } diff --git a/crates/sage-wallet/src/queues/cat_queue.rs b/crates/sage-wallet/src/queues/cat_queue.rs index eedca7399..9630e2bc6 100644 --- a/crates/sage-wallet/src/queues/cat_queue.rs +++ b/crates/sage-wallet/src/queues/cat_queue.rs @@ -1,28 +1,15 @@ use std::time::Duration; -use chia::protocol::Bytes32; -use sage_database::{CatRow, Database}; -use serde::Deserialize; +use sage_assets::DexieCat; +use sage_database::{Asset, AssetKind, Database}; +//use serde::Deserialize; use tokio::{ sync::mpsc, time::{sleep, timeout}, }; -use tracing::info; use crate::{SyncEvent, WalletError}; -#[derive(Deserialize)] -struct Response { - assets: Vec, -} - -#[derive(Deserialize, Clone)] -struct AssetData { - name: Option, - code: Option, - description: Option, -} - #[derive(Debug)] pub struct CatQueue { db: Database, @@ -47,80 +34,33 @@ impl CatQueue { } async fn process_batch(&self) -> Result<(), WalletError> { - let Some(asset_id) = self.db.unfetched_cat().await? else { - return Ok(()); - }; - - info!( - "Looking up CAT with asset id {} from spacescan.io", - asset_id - ); + let cats = timeout(Duration::from_secs(120), DexieCat::fetch_all(self.testnet)).await??; - let asset = match timeout(Duration::from_secs(10), lookup_cat(asset_id, self.testnet)).await - { - Ok(Ok(response)) => response.assets.first().cloned().unwrap_or(AssetData { - name: None, - code: None, - description: None, - }), - Ok(Err(error)) => { - info!("Failed to fetch CAT: {:?}", error); - AssetData { - name: None, - code: None, - description: None, - } - } - Err(_) => { - info!("Timeout fetching CAT"); - AssetData { - name: None, - code: None, - description: None, - } - } - }; - - let dexie_image_base_url = if self.testnet { - "https://icons-testnet.dexie.space" - } else { - "https://icons.dexie.space" - }; + if cats.is_empty() { + return Ok(()); + } - self.db - .update_cat(CatRow { - asset_id, - name: asset.name, - ticker: asset.code, - description: asset.description, - icon: Some(format!("{dexie_image_base_url}/{asset_id}.webp")), - visible: true, - fetched: true, + let mut tx = self.db.tx().await?; + + for cat in cats { + tx.insert_asset(Asset { + hash: cat.hash, + name: cat.name, + ticker: cat.ticker, + precision: 3, + icon_url: cat.icon_url, + description: cat.description, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Token, }) .await?; + } + + tx.commit().await?; self.sync_sender.send(SyncEvent::CatInfo).await.ok(); Ok(()) } } - -async fn lookup_cat(asset_id: Bytes32, testnet: bool) -> Result { - let dexie_base_url = if testnet { - "https://api-testnet.dexie.space/v1" - } else { - "https://api.dexie.space/v1" - }; - - let response = timeout( - Duration::from_secs(10), - reqwest::get(format!( - "{dexie_base_url}/assets?page_size=25&page=1&type=all&code={asset_id}" - )), - ) - .await?? - .json::() - .await?; - - Ok(response) -} diff --git a/crates/sage-wallet/src/queues/nft_uri_queue.rs b/crates/sage-wallet/src/queues/nft_uri_queue.rs index 48404d45a..f56103407 100644 --- a/crates/sage-wallet/src/queues/nft_uri_queue.rs +++ b/crates/sage-wallet/src/queues/nft_uri_queue.rs @@ -2,8 +2,8 @@ use std::time::Duration; use futures_lite::StreamExt; use futures_util::stream::FuturesUnordered; -use sage_assets::fetch_uri; -use sage_database::{Database, NftData}; +use sage_assets::{base64_data_uri, fetch_uri}; +use sage_database::{Database, NftMetadataInfo, ResizedImageKind}; use tokio::{ sync::mpsc, time::{sleep, timeout}, @@ -31,7 +31,7 @@ impl NftUriQueue { } async fn process_batch(&self) -> Result<(), WalletError> { - let batch = self.db.unchecked_nft_uris(25).await?; + let batch = self.db.candidates_for_download(60 * 60 * 24, 3, 25).await?; if batch.is_empty() { return Ok(()); @@ -51,73 +51,73 @@ impl NftUriQueue { while let Some((item, result)) = futures.next().await { let mut tx = self.db.tx().await?; - let hash_matches = match result { + match result { Ok(Ok(data)) => { - let hash_matches = data.hash == item.hash; + let is_hash_match = data.hash == item.hash; - if !hash_matches { + if !is_hash_match { warn!( "Hash mismatch for URI {} (expected {} but found {})", item.uri, item.hash, data.hash ); } - let existing = tx.fetch_nft_data(item.hash).await?; + let icon_url = data + .thumbnail + .as_ref() + .map(|thumbnail| base64_data_uri(&thumbnail.icon, &data.mime_type)); - if existing.as_ref().is_none_or(|data| !data.hash_matches) { - tx.insert_nft_data( - item.hash, - NftData { - mime_type: data.mime_type, - blob: data.blob.clone(), - hash_matches, - }, - ) - .await?; + if let Some(icon_url) = icon_url { + tx.update_nft_data_hash_urls(item.hash, icon_url).await?; + } - if let Some(thumbnail) = data.thumbnail { - tx.insert_nft_thumbnail(item.hash, thumbnail.icon, thumbnail.thumbnail) - .await?; - } + for nft in tx.nfts_with_metadata_hash(item.hash).await? { + let info = compute_nft_info(nft.minter_hash, &data.blob); - let nfts = tx.nfts_by_metadata_hash(item.hash).await?; + let collection_id = info.collection.as_ref().map(|col| col.hash); - for mut nft in nfts { - let info = compute_nft_info(nft.minter_did, Some(&data.blob)); + if let Some(collection) = info.collection { + tx.insert_collection(collection).await?; + } - nft.sensitive_content = info.sensitive_content; - nft.name = info.name; + tx.update_nft_metadata( + nft.hash, + NftMetadataInfo { + name: info.name, + description: info.description, + is_sensitive_content: info.sensitive_content, + collection_id: collection_id.unwrap_or_default(), + }, + ) + .await?; + } - // TODO: Is this correct? - if hash_matches { - nft.collection_id = - info.collection.as_ref().map(|col| col.collection_id); + tx.update_file(item.hash, data.blob, data.mime_type, is_hash_match) + .await?; - if let Some(collection) = info.collection { - tx.insert_collection(collection).await?; - } - } + if let Some(thumbnail) = data.thumbnail { + tx.insert_resized_image(item.hash, ResizedImageKind::Icon, thumbnail.icon) + .await?; - tx.insert_nft(nft).await?; - } + tx.insert_resized_image( + item.hash, + ResizedImageKind::Thumbnail, + thumbnail.thumbnail, + ) + .await?; } - Some(hash_matches) + tx.update_checked_uri(item.hash, item.uri).await?; } Ok(Err(error)) => { debug!("Error fetching URI {}: {error}", item.uri); - - None + tx.update_failed_uri(item.hash, item.uri).await?; } Err(_error) => { debug!("Timed out fetching URI {}", item.uri); - - None + tx.update_failed_uri(item.hash, item.uri).await?; } - }; - - tx.set_nft_uri_checked(item.uri, item.hash, hash_matches) - .await?; + } tx.commit().await?; } diff --git a/crates/sage-wallet/src/queues/offer_queue.rs b/crates/sage-wallet/src/queues/offer_queue.rs index bf95f8073..06f66a387 100644 --- a/crates/sage-wallet/src/queues/offer_queue.rs +++ b/crates/sage-wallet/src/queues/offer_queue.rs @@ -51,7 +51,11 @@ impl OfferQueue { return Ok(()); } - let offers = self.db.active_offers().await?; + let offers = self.db.offers(Some(OfferStatus::Active)).await?; + + if offers.is_empty() { + return Ok(()); + } let mut settlement_coin_ids = HashMap::new(); let mut input_coin_ids = HashMap::new(); diff --git a/crates/sage-wallet/src/queues/puzzle_queue.rs b/crates/sage-wallet/src/queues/puzzle_queue.rs index e3f540246..a424d6ac9 100644 --- a/crates/sage-wallet/src/queues/puzzle_queue.rs +++ b/crates/sage-wallet/src/queues/puzzle_queue.rs @@ -1,14 +1,13 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; -use chia::protocol::{Bytes32, Coin}; +use chia::protocol::{Bytes32, CoinState}; use futures_util::{stream::FuturesUnordered, StreamExt}; -use sage_database::{CoinKind, Database}; +use sage_database::{Database, UnsyncedCoin}; use tokio::{ sync::{mpsc, Mutex}, - task::spawn_blocking, time::{sleep, timeout}, }; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; use crate::{ database::insert_puzzle, fetch_nft_did, ChildKind, PeerState, SyncCommand, SyncEvent, @@ -58,79 +57,149 @@ impl PuzzleQueue { return Ok(()); } - let coin_states = self - .db - .unsynced_coin_states(peers.len() * self.batch_size_per_peer) - .await?; + let limit = peers.len() * self.batch_size_per_peer; + + let coin_states = self.db.unsynced_coins(limit).await?; if coin_states.is_empty() { return Ok(()); } - debug!("Syncing a batch of {} coins", coin_states.len()); + info!( + "Syncing a batch of {} coins from {} peers", + coin_states.len(), + peers.len() + ); let mut futures = FuturesUnordered::new(); - - let mut coin_states_iter = coin_states.into_iter(); + let mut remaining = coin_states.into_iter(); for peer in peers { for _ in 0..self.batch_size_per_peer { - let Some(coin_state) = coin_states_iter.next() else { + let Some(row) = remaining.next() else { break; }; - let db = self.db.clone(); - let genesis_challenge = self.genesis_challenge; - let addr = peer.socket_addr(); let peer = peer.clone(); - - if db.is_p2_puzzle_hash(coin_state.coin.puzzle_hash).await? { - db.sync_coin(coin_state.coin.coin_id(), None, CoinKind::Xch) - .await?; - warn!( - "Coin {} should already be synced, but isn't", - coin_state.coin.coin_id() - ); - continue; - } + let genesis_challenge = self.genesis_challenge; + let is_custody_p2_puzzle_hash = self + .db + .is_custody_p2_puzzle_hash(row.coin_state.coin.puzzle_hash) + .await?; futures.push(async move { - let result = fetch_puzzle(&peer, genesis_challenge, coin_state.coin).await; - (addr, coin_state, result) + let result = + fetch_puzzles(&peer, genesis_challenge, row, is_custody_p2_puzzle_hash) + .await; + (peer.socket_addr(), row, result) }); } } let mut subscriptions = Vec::new(); + let mut send_events = false; + + while let Some((addr, root, synced_coins)) = futures.next().await { + match synced_coins { + Ok(synced_coins) => { + let mut tx = self.db.tx().await?; + + if root.is_children_unsynced { + debug!( + "Children have been synced for coin {}", + root.coin_state.coin.coin_id() + ); + tx.set_children_synced(root.coin_state.coin.coin_id()) + .await?; + send_events = true; + } - while let Some((addr, coin_state, result)) = futures.next().await { - let coin_id = coin_state.coin.coin_id(); + for item in synced_coins { + let coin_id = item.coin_state.coin.coin_id(); + let is_root = root.coin_state.coin.coin_id() == coin_id; - match result { - Ok((info, minter_did)) => { - let subscribe = info.subscribe(); + // We want to skip children that we already know about + if !is_root && tx.is_known_coin(coin_id).await? { + debug!("Skipping child coin {coin_id} because it is already known"); + continue; + } - let remove = match info.p2_puzzle_hash() { - Some(p2_puzzle_hash) => !self.db.is_p2_puzzle_hash(p2_puzzle_hash).await?, - None => true, - }; + let is_custody_p2_puzzle_hash = + tx.is_custody_p2_puzzle_hash(coin_id).await?; - if remove { - self.db.delete_coin_state(coin_state.coin.coin_id()).await?; - } else { - let mut tx = self.db.tx().await?; - insert_puzzle(&mut tx, coin_state, info, minter_did).await?; - tx.commit().await?; - } + let Some(kind) = item.kind.filter(|_| !is_custody_p2_puzzle_hash) else { + warn!("Retroactively inserting XCH coin that should have already been synced: {coin_id}"); - if subscribe { - subscriptions.push(coin_id); + self.db + .update_coin( + coin_id, + Bytes32::default(), + item.coin_state.coin.puzzle_hash, + None, + ) + .await?; + send_events = true; + continue; + }; + + // We don't want to insert child coins that we don't own. + let custody_p2_puzzle_hashes = kind.custody_p2_puzzle_hashes(); + + let mut is_relevant = false; + + for custody_p2_puzzle_hash in custody_p2_puzzle_hashes { + if tx.is_custody_p2_puzzle_hash(custody_p2_puzzle_hash).await? { + is_relevant = true; + break; + } + } + + if !is_relevant { + if is_root { + warn!("Deleting unexpected coin {coin_id} because it is not relevant to this wallet"); + tx.delete_coin(coin_id).await?; + send_events = true; + } else { + debug!("Skipping coin {coin_id} because it is not relevant to this wallet"); + } + continue; + } + + if is_root { + debug!("Synced puzzle for coin {coin_id}"); + } else { + debug!("Found relevant child coin {coin_id} which wasn't synced"); + } + + send_events = true; + + if let Some(height) = item.coin_state.created_height { + tx.insert_height(height).await?; + } + + if let Some(height) = item.coin_state.spent_height { + tx.insert_height(height).await?; + } + + tx.insert_coin(item.coin_state).await?; + + let subscribe = kind.subscribe(); + + if insert_puzzle(&mut tx, item.coin_state, kind, item.minter_hash).await? + && subscribe + { + subscriptions.push(coin_id); + } } + + tx.commit().await?; } Err(error) => { debug!( - "Failed to lookup puzzle of {} from peer {}: {}", - coin_id, addr, error + "Failed to sync {} from peer {}: {}", + root.coin_state.coin.coin_id(), + addr, + error ); if matches!( @@ -149,49 +218,119 @@ impl PuzzleQueue { } } - self.command_sender - .send(SyncCommand::SubscribeCoins { - coin_ids: subscriptions, - }) - .await - .ok(); - - self.sync_sender - .send(SyncEvent::PuzzleBatchSynced) - .await - .ok(); + if send_events { + self.command_sender + .send(SyncCommand::SubscribeCoins { + coin_ids: subscriptions, + }) + .await + .ok(); + self.sync_sender + .send(SyncEvent::PuzzleBatchSynced) + .await + .ok(); + } Ok(()) } } -/// Fetches info for a coin's puzzle and inserts it into the database. -async fn fetch_puzzle( +#[derive(Debug, Clone)] +struct SyncedCoin { + coin_state: CoinState, + kind: Option, + minter_hash: Option, +} + +async fn fetch_puzzles( peer: &WalletPeer, genesis_challenge: Bytes32, - coin: Coin, -) -> Result<(ChildKind, Option), WalletError> { - let parent_spend = timeout( - Duration::from_secs(15), - peer.fetch_coin_spend(coin.parent_coin_info, genesis_challenge), - ) - .await??; - - let info = spawn_blocking(move || { - ChildKind::from_parent( - parent_spend.coin, - &parent_spend.puzzle_reveal, - &parent_spend.solution, - coin, - ) - }) - .await??; - - let minter_did = if let ChildKind::Nft { info, .. } = &info { - fetch_nft_did(peer, genesis_challenge, info.launcher_id, &HashMap::new()).await? - } else { - None - }; - - Ok((info, minter_did)) + unsynced_coin: UnsyncedCoin, + is_custody_p2_puzzle_hash: bool, +) -> Result, WalletError> { + let coin = unsynced_coin.coin_state.coin; + + let mut synced_coins = Vec::new(); + + if unsynced_coin.is_asset_unsynced { + let parent_spend = if is_custody_p2_puzzle_hash { + None + } else { + timeout( + Duration::from_secs(15), + peer.fetch_optional_coin_spend(coin.parent_coin_info, genesis_challenge), + ) + .await?? + }; + + if let Some(parent_spend) = parent_spend { + let kind = ChildKind::from_parent( + parent_spend.coin, + &parent_spend.puzzle_reveal, + &parent_spend.solution, + coin, + )?; + + let minter_hash = if let ChildKind::Nft { info, .. } = &kind { + fetch_nft_did(peer, genesis_challenge, info.launcher_id, &HashMap::new()).await? + } else { + None + }; + + synced_coins.push(SyncedCoin { + coin_state: unsynced_coin.coin_state, + kind: Some(kind), + minter_hash, + }); + } else { + synced_coins.push(SyncedCoin { + coin_state: unsynced_coin.coin_state, + kind: None, + minter_hash: None, + }); + } + } + + if unsynced_coin.is_children_unsynced { + if let Some(spent_height) = unsynced_coin.coin_state.spent_height { + let (puzzle_reveal, solution) = timeout( + Duration::from_secs(15), + peer.fetch_puzzle_solution(coin.coin_id(), spent_height), + ) + .await??; + + let children = ChildKind::parse_children(coin, &puzzle_reveal, &solution)?; + + let coin_states = peer + .fetch_coins( + children.iter().map(|(child, _)| child.coin_id()).collect(), + genesis_challenge, + ) + .await?; + + for (child_coin, kind) in children { + let Some(&coin_state) = coin_states + .iter() + .find(|coin_state| coin_state.coin.coin_id() == child_coin.coin_id()) + else { + continue; + }; + + let minter_hash = if let ChildKind::Nft { info, .. } = &kind { + fetch_nft_did(peer, genesis_challenge, info.launcher_id, &HashMap::new()) + .await? + } else { + None + }; + + synced_coins.push(SyncedCoin { + coin_state, + kind: Some(kind), + minter_hash, + }); + } + } + } + + Ok(synced_coins) } diff --git a/crates/sage-wallet/src/queues/transaction_queue.rs b/crates/sage-wallet/src/queues/transaction_queue.rs index 160143ae8..cedabd2a1 100644 --- a/crates/sage-wallet/src/queues/transaction_queue.rs +++ b/crates/sage-wallet/src/queues/transaction_queue.rs @@ -1,9 +1,6 @@ -use std::{ - sync::Arc, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; +use std::{sync::Arc, time::Duration}; -use chia::protocol::{Bytes32, SpendBundle}; +use chia::protocol::SpendBundle; use sage_database::Database; use tokio::{ sync::{mpsc, Mutex}, @@ -11,14 +8,11 @@ use tokio::{ }; use tracing::{info, warn}; -use crate::{ - safely_remove_transaction, submit_to_peers, PeerState, Status, SyncEvent, WalletError, -}; +use crate::{submit_to_peers, PeerState, Status, SyncEvent, WalletError}; #[derive(Debug)] pub struct TransactionQueue { db: Database, - genesis_challenge: Bytes32, state: Arc>, sync_sender: mpsc::Sender, } @@ -26,13 +20,11 @@ pub struct TransactionQueue { impl TransactionQueue { pub fn new( db: Database, - genesis_challenge: Bytes32, state: Arc>, sync_sender: mpsc::Sender, ) -> Self { Self { db, - genesis_challenge, state, sync_sender, } @@ -52,80 +44,44 @@ impl TransactionQueue { return Ok(()); } - let timestamp: i64 = SystemTime::now() - .duration_since(UNIX_EPOCH)? - .as_secs() - .try_into() - .expect("timestamp does not fit in i64"); - let mut spend_bundles = Vec::new(); - let rows = self.db.resubmittable_transactions(timestamp - 180).await?; + let rows = self.db.mempool_items_to_submit(120, 3).await?; if rows.is_empty() { return Ok(()); } - for (transaction_id, aggregated_signature) in rows { - let coin_spends = self.db.coin_spends(transaction_id).await?; - spend_bundles.push(SpendBundle::new(coin_spends, aggregated_signature)); + for row in rows { + let coin_spends = self.db.mempool_coin_spends(row.hash).await?; + spend_bundles.push(SpendBundle::new(coin_spends, row.aggregated_signature)); } - info!( - "Submitting the following transactions: {:?}", - spend_bundles - .iter() - .map(|sb| ( - sb.name(), - sb.coin_spends - .iter() - .map(|cs| cs.coin.coin_id()) - .collect::>() - )) - .collect::>() - ); - for spend_bundle in spend_bundles { sleep(Duration::from_secs(1)).await; - let transaction_id = spend_bundle.name(); - let peers = self.state.lock().await.peers(); if peers.is_empty() { return Ok(()); } - match submit_to_peers(&peers, self.genesis_challenge, spend_bundle).await? { - Status::Success => { - info!("Transaction {transaction_id} confirmed, removing and confirming coins"); + let transaction_id = spend_bundle.name(); - let mut tx = self.db.tx().await?; - tx.confirm_coins(transaction_id).await?; - safely_remove_transaction(&mut tx, transaction_id).await?; - tx.commit().await?; + info!( + "Submitting transaction with id {transaction_id}: {:?}", + spend_bundle + .coin_spends + .iter() + .map(|cs| cs.coin.coin_id()) + .collect::>() + ); - self.sync_sender - .send(SyncEvent::TransactionEnded { - transaction_id, - error: None, - success: true, - }) - .await - .ok(); - } + match submit_to_peers(&peers, spend_bundle).await? { Status::Pending => { info!("Transaction inclusion in mempool successful, updating timestamp"); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH)? - .as_secs() - .try_into() - .expect("timestamp exceeds i64"); - - self.db - .update_transaction_mempool_time(transaction_id, timestamp) - .await?; + self.db.update_mempool_item_time(transaction_id).await?; self.sync_sender .send(SyncEvent::TransactionUpdated { transaction_id }) @@ -138,14 +94,16 @@ impl TransactionQueue { ); let mut tx = self.db.tx().await?; - safely_remove_transaction(&mut tx, transaction_id).await?; + + tx.set_transaction_children_unsynced(transaction_id).await?; + tx.remove_mempool_item(transaction_id).await?; + tx.commit().await?; self.sync_sender - .send(SyncEvent::TransactionEnded { + .send(SyncEvent::TransactionFailed { transaction_id, error, - success: false, }) .await .ok(); diff --git a/crates/sage-wallet/src/sync_manager.rs b/crates/sage-wallet/src/sync_manager.rs index 7b2d7a17c..fd060afa5 100644 --- a/crates/sage-wallet/src/sync_manager.rs +++ b/crates/sage-wallet/src/sync_manager.rs @@ -151,10 +151,11 @@ impl SyncManager { async fn process_commands(&mut self) { while let Ok(command) = self.command_receiver.try_recv() { match command { - SyncCommand::SwitchWallet { wallet } => { + SyncCommand::SwitchWallet { wallet, delta_sync } => { self.clear_subscriptions().await; self.abort_wallet_tasks(); self.wallet = wallet; + self.options.delta_sync = delta_sync; } SyncCommand::SwitchNetwork(network) => { if self.network.network_id() != network.network_id() @@ -327,11 +328,6 @@ impl SyncManager { incremental_sync(wallet, message.items, true, &self.event_sender).await?; - wallet - .db - .insert_peak(message.height, message.peak_hash) - .await?; - info!( "Received {} unspent coins, {} spent coins, and synced to peak {} with header hash {}", unspent_count, spent_count, @@ -379,6 +375,7 @@ impl SyncManager { peer, self.state.clone(), self.event_sender.clone(), + self.options.delta_sync, )); *sync = InitialWalletSync::Syncing { ip, task }; self.event_sender.send(SyncEvent::Start(ip)).await.ok(); @@ -443,7 +440,6 @@ impl SyncManager { let task = tokio::spawn( TransactionQueue::new( wallet.db.clone(), - wallet.genesis_challenge, self.state.clone(), self.event_sender.clone(), ) @@ -518,6 +514,23 @@ impl SyncManager { } } + if let Some(task) = &mut self.puzzle_lookup_task { + match poll_once(task).await { + Some(Err(error)) => { + warn!("Puzzle lookup queue failed with panic: {error}"); + self.puzzle_lookup_task = None; + } + Some(Ok(Err(error))) => { + warn!("Puzzle lookup queue failed with error: {error}"); + self.puzzle_lookup_task = None; + } + Some(Ok(Ok(()))) => { + self.puzzle_lookup_task = None; + } + None => {} + } + } + if let Some(task) = &mut self.cat_queue_task { match poll_once(task).await { Some(Err(error)) => { diff --git a/crates/sage-wallet/src/sync_manager/options.rs b/crates/sage-wallet/src/sync_manager/options.rs index 643fd088c..c97ae8189 100644 --- a/crates/sage-wallet/src/sync_manager/options.rs +++ b/crates/sage-wallet/src/sync_manager/options.rs @@ -7,6 +7,7 @@ pub struct SyncOptions { pub dns_batch_size: usize, pub connection_batch_size: usize, pub max_peer_age_seconds: u64, + pub delta_sync: bool, pub puzzle_batch_size_per_peer: usize, pub timeouts: Timeouts, pub testing: bool, @@ -33,7 +34,7 @@ impl Default for Timeouts { fn default() -> Self { Self { sync_delay: Duration::from_secs(1), - cat_delay: Duration::from_secs(1), + cat_delay: Duration::from_secs(3600), nft_uri_delay: Duration::from_millis(500), puzzle_delay: Duration::from_secs(1), transaction_delay: Duration::from_secs(1), diff --git a/crates/sage-wallet/src/sync_manager/sync_command.rs b/crates/sage-wallet/src/sync_manager/sync_command.rs index c383e8e5b..b270fa3d0 100644 --- a/crates/sage-wallet/src/sync_manager/sync_command.rs +++ b/crates/sage-wallet/src/sync_manager/sync_command.rs @@ -7,12 +7,25 @@ use crate::Wallet; #[derive(Debug)] pub enum SyncCommand { - SwitchWallet { wallet: Option> }, + SwitchWallet { + wallet: Option>, + delta_sync: bool, + }, SwitchNetwork(Network), - HandleMessage { ip: IpAddr, message: Message }, - ConnectPeer { ip: IpAddr, user_managed: bool }, - SubscribeCoins { coin_ids: Vec }, - SubscribePuzzles { puzzle_hashes: Vec }, + HandleMessage { + ip: IpAddr, + message: Message, + }, + ConnectPeer { + ip: IpAddr, + user_managed: bool, + }, + SubscribeCoins { + coin_ids: Vec, + }, + SubscribePuzzles { + puzzle_hashes: Vec, + }, ConnectionClosed(IpAddr), SetTargetPeers(usize), SetDiscoverPeers(bool), diff --git a/crates/sage-wallet/src/sync_manager/sync_event.rs b/crates/sage-wallet/src/sync_manager/sync_event.rs index 03f17356a..fed1d8a00 100644 --- a/crates/sage-wallet/src/sync_manager/sync_event.rs +++ b/crates/sage-wallet/src/sync_manager/sync_event.rs @@ -1,7 +1,6 @@ -use std::net::IpAddr; - use chia::protocol::Bytes32; use sage_database::OfferStatus; +use std::net::IpAddr; #[derive(Debug, Clone, PartialEq, Eq)] pub enum SyncEvent { @@ -15,10 +14,9 @@ pub enum SyncEvent { TransactionUpdated { transaction_id: Bytes32, }, - TransactionEnded { + TransactionFailed { transaction_id: Bytes32, error: Option, - success: bool, }, OfferUpdated { offer_id: Bytes32, diff --git a/crates/sage-wallet/src/sync_manager/wallet_sync.rs b/crates/sage-wallet/src/sync_manager/wallet_sync.rs index c91f11a98..da5f0d58a 100644 --- a/crates/sage-wallet/src/sync_manager/wallet_sync.rs +++ b/crates/sage-wallet/src/sync_manager/wallet_sync.rs @@ -1,7 +1,4 @@ -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; +use std::{collections::HashSet, sync::Arc, time::Duration}; use chia::protocol::{Bytes32, CoinState, CoinStateFilters}; use sage_database::DatabaseTx; @@ -9,9 +6,9 @@ use tokio::{ sync::{mpsc, Mutex}, time::{sleep, timeout}, }; -use tracing::{debug, info, warn}; +use tracing::{info, warn}; -use crate::{delete_puzzle, upsert_coin, UpsertCounters, Wallet, WalletError, WalletPeer}; +use crate::{Wallet, WalletError, WalletPeer}; use super::{PeerState, SyncEvent}; @@ -20,20 +17,23 @@ pub async fn sync_wallet( peer: WalletPeer, state: Arc>, sync_sender: mpsc::Sender, + delta_sync: bool, ) -> Result<(), WalletError> { info!("Starting sync against peer {}", peer.socket_addr()); - let p2_puzzle_hashes = wallet.db.p2_puzzle_hashes().await?; + let p2_puzzle_hashes = wallet.db.custody_p2_puzzle_hashes().await?; - let (start_height, start_header_hash) = wallet.db.latest_peak().await?.map_or_else( + let (start_height, start_header_hash) = if delta_sync { + wallet.db.latest_peak().await? + } else { + None + } + .map_or_else( || (None, wallet.genesis_challenge), |(peak, header_hash)| (Some(peak), header_hash), ); - let mut coin_ids = Vec::new(); - coin_ids.extend(wallet.db.unspent_nft_coin_ids().await?); - coin_ids.extend(wallet.db.unspent_did_coin_ids().await?); - coin_ids.extend(wallet.db.unspent_cat_coin_ids().await?); + let coin_ids = wallet.db.subscription_coin_ids().await?; sync_coin_ids( &wallet, @@ -88,15 +88,20 @@ pub async fn sync_wallet( } } - if let Some((height, header_hash)) = state.lock().await.peak_of(peer.socket_addr().ip()) { - // TODO: Maybe look into a better way. - info!( - "Updating peak from peer to {} with header hash {}", - height, header_hash - ); - wallet.db.insert_peak(height, header_hash).await?; - } else { - warn!("No peak found"); + if delta_sync { + if let Some((height, header_hash)) = state.lock().await.peak_of(peer.socket_addr().ip()) { + info!( + "Updating peak from peer to {} with header hash {}", + height, header_hash + ); + + wallet + .db + .insert_block(height, header_hash, None, true) + .await?; + } else { + warn!("No peak found"); + } } Ok(()) @@ -116,7 +121,7 @@ async fn sync_coin_ids( sleep(Duration::from_millis(500)).await; } - debug!( + info!( "Subscribing to {} coins from peer {}", coin_ids.len(), peer.socket_addr() @@ -128,7 +133,7 @@ async fn sync_coin_ids( ) .await??; - debug!("Received {} coin states", coin_states.len()); + info!("Received {} coin states", coin_states.len()); if coin_states .iter() @@ -157,8 +162,9 @@ async fn sync_puzzle_hashes( let mut prev_header_hash = start_header_hash; loop { - debug!( - "Subscribing to puzzles at height {:?} and header hash {} from peer {}", + info!( + "Subscribing to {} puzzle hashes at height {:?} and header hash {} from peer {}", + puzzle_hashes.len(), prev_height, prev_header_hash, peer.socket_addr() @@ -175,7 +181,7 @@ async fn sync_puzzle_hashes( ) .await??; - debug!("Received {} coin states", data.coin_states.len()); + info!("Received {} coin states", data.coin_states.len()); if !data.coin_states.is_empty() { incremental_sync(wallet, data.coin_states, true, &sync_sender).await?; @@ -199,27 +205,48 @@ pub async fn incremental_sync( sync_sender: &mpsc::Sender, ) -> Result<(), WalletError> { let mut tx = wallet.db.tx().await?; + let mut confirmed_transactions = HashSet::new(); - let start = Instant::now(); + for &coin_state in &coin_states { + if let Some(height) = coin_state.created_height { + tx.insert_height(height).await?; + } - let mut counters = UpsertCounters::default(); + if let Some(height) = coin_state.spent_height { + tx.insert_height(height).await?; + } - for &coin_state in &coin_states { - upsert_coin(&mut tx, coin_state, None, &mut counters).await?; + tx.insert_coin(coin_state).await?; + + if tx + .is_custody_p2_puzzle_hash(coin_state.coin.puzzle_hash) + .await? + { + tx.update_coin( + coin_state.coin.coin_id(), + Bytes32::default(), + coin_state.coin.puzzle_hash, + None, + ) + .await?; + } + + confirmed_transactions.extend( + tx.mempool_items_for_output(coin_state.coin.coin_id()) + .await?, + ); if coin_state.spent_height.is_some() { - let start = Instant::now(); - delete_puzzle(&mut tx, coin_state.coin.coin_id()).await?; - counters.delete_puzzle += start.elapsed(); + confirmed_transactions.extend( + tx.mempool_items_for_input(coin_state.coin.coin_id()) + .await?, + ); } } - debug!( - "Upserted {} coins in {:?}, with counters {:?}", - coin_states.len(), - start.elapsed(), - counters - ); + for mempool_item_id in confirmed_transactions { + tx.remove_mempool_item(mempool_item_id).await?; + } let mut derived = false; @@ -254,12 +281,8 @@ async fn auto_insert_unhardened_derivations( let mut derivations = Vec::new(); let mut next_index = tx.derivation_index(false).await?; - let max_index = tx - .max_used_derivation_index(false) - .await? - .map_or(0, |index| index + 1); + let max_index = tx.unused_derivation_index(false).await?; - //1000 derivations on initial sync while max_index + 500 >= next_index { derivations.extend( wallet diff --git a/crates/sage-wallet/src/test.rs b/crates/sage-wallet/src/test.rs index 0aa12c2e8..a37097335 100644 --- a/crates/sage-wallet/src/test.rs +++ b/crates/sage-wallet/src/test.rs @@ -1,4 +1,7 @@ -use std::{sync::Arc, time::Duration}; +use std::{ + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; use chia::{ bls::{ @@ -15,7 +18,7 @@ use chia_wallet_sdk::{ types::TESTNET11_CONSTANTS, }; use sage_config::TESTNET11; -use sage_database::Database; +use sage_database::{Database, Derivation}; use sqlx::{migrate, SqlitePool}; use tokio::{ sync::{ @@ -88,7 +91,7 @@ impl TestWallet { SqlitePool::connect(&format!("file:testdb{db_index}?mode=memory&cache=shared")).await?; migrate!("../../migrations").run(&pool).await?; let db = Database::new(pool); - db.run_rust_migrations().await?; + db.run_rust_migrations("TXCH".to_string()).await?; let sk = BlsPair::default().sk.derive_unhardened(key_index); let pk = sk.public_key(); @@ -106,8 +109,15 @@ impl TestWallet { .derive_synthetic() .public_key(); let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); - tx.insert_derivation(p2_puzzle_hash, index, true, synthetic_key) - .await?; + tx.insert_custody_p2_puzzle( + p2_puzzle_hash, + synthetic_key, + Derivation { + derivation_index: index, + is_hardened: true, + }, + ) + .await?; } tx.commit().await?; @@ -169,7 +179,7 @@ impl TestWallet { test.consume_until(|event| matches!(event, SyncEvent::Subscribed)) .await; - assert_eq!(test.wallet.db.balance().await?, balance as u128); + assert_eq!(test.wallet.db.xch_balance().await?, balance as u128); Ok(test) } @@ -224,7 +234,7 @@ impl TestWallet { .unwrap_or_else(|_| panic!("timed out listening for event")) .unwrap_or_else(|| panic!("missing next event")); - debug!("Consuming event: {next:?}"); + debug!("Consuming event for wallet {}: {next:?}", self.index); if f(next) { return; @@ -241,6 +251,14 @@ impl TestWallet { self.consume_until(|event| matches!(event, SyncEvent::PuzzleBatchSynced)) .await; } + + pub async fn new_block_with_current_time(&self) -> anyhow::Result { + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let mut sim = self.sim.lock().await; + sim.set_next_timestamp(timestamp)?; + sim.create_block(); + Ok(timestamp) + } } pub fn default_test_options() -> SyncOptions { @@ -250,6 +268,7 @@ pub fn default_test_options() -> SyncOptions { dns_batch_size: 0, connection_batch_size: 0, max_peer_age_seconds: 0, + delta_sync: true, puzzle_batch_size_per_peer: 5, timeouts: Timeouts { sync_delay: Duration::from_millis(100), diff --git a/crates/sage-wallet/src/utils.rs b/crates/sage-wallet/src/utils.rs index 2e244d02c..8d0476e15 100644 --- a/crates/sage-wallet/src/utils.rs +++ b/crates/sage-wallet/src/utils.rs @@ -1,9 +1,7 @@ mod fetch_nft_did; -mod fetch_nft_offer_details; mod offchain_metadata; mod submit; pub use fetch_nft_did::*; -pub use fetch_nft_offer_details::*; pub use offchain_metadata::*; pub use submit::*; diff --git a/crates/sage-wallet/src/utils/fetch_nft_offer_details.rs b/crates/sage-wallet/src/utils/fetch_nft_offer_details.rs deleted file mode 100644 index 215e28857..000000000 --- a/crates/sage-wallet/src/utils/fetch_nft_offer_details.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::time::Duration; - -use chia::{ - clvm_traits::{FromClvm, ToClvm}, - protocol::{Bytes32, Program}, -}; -use chia_wallet_sdk::driver::{HashedPtr, Nft, Puzzle}; -use clvmr::Allocator; -use tokio::time::{sleep, timeout}; - -use crate::{RequestedNft, WalletError, WalletPeer}; - -pub async fn fetch_nft_offer_details( - peer: &WalletPeer, - launcher_id: Bytes32, -) -> Result, WalletError> { - let mut offer_details = None::; - let mut current_id = launcher_id; - let mut parent = None; - - loop { - let Some(child) = - timeout(Duration::from_secs(5), peer.try_fetch_child(current_id)).await?? - else { - return Ok(None); - }; - - if child.spent_height.is_some() { - parent = Some(child); - current_id = child.coin.coin_id(); - sleep(Duration::from_secs(1)).await; - continue; - } - - let parent = parent.expect("parent not found"); - - let (parent_puzzle_reveal, parent_solution) = timeout( - Duration::from_secs(15), - peer.fetch_puzzle_solution( - parent.coin.coin_id(), - parent.spent_height.ok_or(WalletError::PeerMisbehaved)?, - ), - ) - .await??; - - let mut allocator = Allocator::new(); - let parent_puzzle = parent_puzzle_reveal.to_clvm(&mut allocator)?; - let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle); - let parent_solution = parent_solution.to_clvm(&mut allocator)?; - - if let Some(nft) = Nft::::parse_child( - &mut allocator, - parent.coin, - parent_puzzle, - parent_solution, - ) - .ok() - .flatten() - { - offer_details = Some(RequestedNft { - metadata: Program::from_clvm(&allocator, nft.info.metadata.ptr())?, - metadata_updater_puzzle_hash: nft.info.metadata_updater_puzzle_hash, - royalty_puzzle_hash: nft.info.royalty_puzzle_hash, - royalty_basis_points: nft.info.royalty_basis_points, - }); - break; - } - - break; - } - - Ok(offer_details) -} diff --git a/crates/sage-wallet/src/utils/offchain_metadata.rs b/crates/sage-wallet/src/utils/offchain_metadata.rs index 6838aa311..a6cfc1938 100644 --- a/crates/sage-wallet/src/utils/offchain_metadata.rs +++ b/crates/sage-wallet/src/utils/offchain_metadata.rs @@ -1,27 +1,28 @@ use chia::protocol::Bytes32; +use chia_sha2::Sha256; use sage_assets::{Chip0007Metadata, Collection}; -use sage_database::{calculate_collection_id, CollectionRow}; -use tracing::warn; +use sage_database::CollectionRow; +use tracing::debug; #[derive(Debug, Default, Clone)] pub struct ComputedNftInfo { pub name: Option, + pub description: Option, pub sensitive_content: bool, pub collection: Option, } -pub fn compute_nft_info(did_id: Option, blob: Option<&[u8]>) -> ComputedNftInfo { - let Some(json) = blob.and_then(|blob| { - Chip0007Metadata::from_bytes(blob) - .map_err(|error| { - warn!( - "Error parsing offchain metadata: {error}, {}", - String::from_utf8_lossy(blob) - ); - error - }) - .ok() - }) else { +pub fn compute_nft_info(did_id: Option, blob: &[u8]) -> ComputedNftInfo { + let Some(json) = Chip0007Metadata::from_bytes(blob) + .map_err(|error| { + debug!( + "Error parsing offchain metadata: {error}, {}", + String::from_utf8_lossy(blob) + ); + error + }) + .ok() + else { return ComputedNftInfo::default(); }; @@ -36,18 +37,26 @@ pub fn compute_nft_info(did_id: Option, blob: Option<&[u8]>) -> Compute }), ) = (did_id, json.collection) { + let attributes = attributes.unwrap_or_default(); Some(CollectionRow { - collection_id: calculate_collection_id(did_id, &metadata_collection_id.to_string()), - did_id, - metadata_collection_id: metadata_collection_id.to_string(), + description: None, + hash: calculate_collection_id(did_id, &metadata_collection_id.to_string()), + minter_hash: did_id, + uuid: metadata_collection_id.to_string(), name: Some(name), - icon: attributes.unwrap_or_default().into_iter().find_map(|item| { + icon_url: attributes.iter().find_map(|item| { match (item.kind.as_str(), item.value.as_str()) { (Some("icon"), Some(icon)) => Some(icon.to_string()), _ => None, } }), - visible: true, + banner_url: attributes.iter().find_map(|item| { + match (item.kind.as_str(), item.value.as_str()) { + (Some("banner"), Some(banner)) => Some(banner.to_string()), + _ => None, + } + }), + is_visible: true, }) } else { None @@ -55,11 +64,19 @@ pub fn compute_nft_info(did_id: Option, blob: Option<&[u8]>) -> Compute ComputedNftInfo { name: Some(json.name.clone()), + description: Some(json.description.clone()), sensitive_content, collection, } } +fn calculate_collection_id(did_id: Bytes32, json_collection_id: &str) -> Bytes32 { + let mut hasher = Sha256::new(); + hasher.update(hex::encode(did_id)); + hasher.update(json_collection_id); + hasher.finalize().into() +} + #[cfg(test)] mod tests { use hex_literal::hex; diff --git a/crates/sage-wallet/src/utils/submit.rs b/crates/sage-wallet/src/utils/submit.rs index d88078c00..07cfcee8c 100644 --- a/crates/sage-wallet/src/utils/submit.rs +++ b/crates/sage-wallet/src/utils/submit.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use chia::protocol::{Bytes32, SpendBundle}; +use chia::protocol::SpendBundle; use tokio::time::timeout; use tracing::{info, warn}; @@ -9,14 +9,12 @@ use crate::{WalletError, WalletPeer}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Status { Pending, - Success, Failed(u8, Option), Unknown, } pub async fn submit_to_peers( peers: &[WalletPeer], - genesis_challenge: Bytes32, spend_bundle: SpendBundle, ) -> Result { let transaction_id = spend_bundle.name(); @@ -24,22 +22,21 @@ pub async fn submit_to_peers( info!("Checking transaction id {}", transaction_id); let mut mempool = false; + let mut failed = false; let mut status = 0; let mut error = None; for peer in peers { let ip = peer.socket_addr().ip(); - match submit_transaction(peer, spend_bundle.clone(), genesis_challenge).await? { + match submit_transaction(peer, spend_bundle.clone()).await? { Status::Pending => { mempool = true; } - Status::Success => { - return Ok(Status::Success); - } Status::Failed(failure_status, failure_error) => { info!("Transaction {transaction_id} failed according to peer {ip}, but will check other peers"); status = failure_status; error = failure_error; + failed = true; } Status::Unknown => {} } @@ -47,49 +44,17 @@ pub async fn submit_to_peers( Ok(if mempool { Status::Pending - } else { + } else if failed { Status::Failed(status, error) + } else { + Status::Unknown }) } pub async fn submit_transaction( peer: &WalletPeer, spend_bundle: SpendBundle, - genesis_challenge: Bytes32, ) -> Result { - let coin_ids: Vec = spend_bundle - .coin_spends - .iter() - .map(|cs| cs.coin.coin_id()) - .collect(); - - let coin_states = match timeout( - Duration::from_secs(2), - peer.fetch_coins(coin_ids.clone(), genesis_challenge), - ) - .await - { - Ok(Ok(coin_states)) => coin_states, - Err(_timeout) => { - warn!("Coin lookup timed out for {}", peer.socket_addr()); - return Ok(Status::Unknown); - } - Ok(Err(err)) => { - warn!("Coin lookup failed for {}: {}", peer.socket_addr(), err); - return Ok(Status::Unknown); - } - }; - - if coin_states.iter().all(|cs| cs.spent_height.is_some()) - && coin_ids - .into_iter() - .all(|coin_id| coin_states.iter().any(|cs| cs.coin.coin_id() == coin_id)) - { - return Ok(Status::Success); - } else if coin_states.iter().any(|cs| cs.spent_height.is_some()) { - return Ok(Status::Failed(3, None)); - } - info!("Submitting transaction to {}", peer.socket_addr()); let ack = match timeout( diff --git a/crates/sage-wallet/src/wallet.rs b/crates/sage-wallet/src/wallet.rs index 080e9cb36..2b5262e8a 100644 --- a/crates/sage-wallet/src/wallet.rs +++ b/crates/sage-wallet/src/wallet.rs @@ -1,18 +1,26 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + time::{SystemTime, UNIX_EPOCH}, +}; use chia::{ bls::PublicKey, + clvm_utils::ToTreeHash, protocol::{Bytes32, Coin}, + puzzles::offer::SettlementPaymentsSolution, }; use chia_puzzles::SETTLEMENT_PAYMENT_HASH; use chia_wallet_sdk::{ - driver::{Action, Cat, CatInfo, Deltas, Id, Outputs, Relation, SpendContext, Spends}, + driver::{ + Action, Cat, ClawbackV2, Deltas, DriverError, Id, Layer, Outputs, Relation, + SettlementLayer, SpendContext, SpendKind, SpendWithConditions, SpendableAsset, Spends, + StandardLayer, + }, signer::AggSigConstants, utils::select_coins, }; -use indexmap::{indexmap, IndexMap}; -use itertools::Itertools; -use sage_database::{CoinKind, Database}; +use indexmap::IndexMap; +use sage_database::{AssetKind, CoinKind, Database, P2Puzzle}; mod cats; mod coin_management; @@ -22,8 +30,8 @@ mod memos; mod multi_send; mod nfts; mod offer; -mod p2_send; mod signing; +mod xch; pub use multi_send::*; pub use nfts::*; @@ -57,12 +65,12 @@ impl Wallet { } } - async fn select_p2_coins( + async fn select_xch_coins( &self, amount: u64, selected_coin_ids: &HashSet, ) -> Result, WalletError> { - let mut spendable_coins = self.db.spendable_coins().await?; + let mut spendable_coins = self.db.selectable_xch_coins().await?; spendable_coins.retain(|coin| !selected_coin_ids.contains(&coin.coin_id())); Ok(select_coins(spendable_coins, amount)?) @@ -74,21 +82,14 @@ impl Wallet { amount: u64, selected_coin_ids: &HashSet, ) -> Result, WalletError> { - let mut cat_coins = self.db.spendable_cat_coins(asset_id).await?; + let mut cat_coins = self.db.selectable_cat_coins(asset_id).await?; cat_coins.retain(|cat| !selected_coin_ids.contains(&cat.coin.coin_id())); - let mut cats = HashMap::with_capacity(cat_coins.len()); - let mut spendable_coins = Vec::with_capacity(cat_coins.len()); - - for cat in &cat_coins { - cats.insert( - cat.coin, - Cat::new( - cat.coin, - Some(cat.lineage_proof), - CatInfo::new(asset_id, None, cat.p2_puzzle_hash), - ), - ); + let mut cats = HashMap::new(); + let mut spendable_coins = Vec::new(); + + for cat in cat_coins { + cats.insert(cat.coin, cat); spendable_coins.push(cat.coin); } @@ -119,39 +120,55 @@ impl Wallet { let mut spends = Spends::new(self_puzzle_hash); for &coin_id in selected_coin_ids { - let Some(row) = self.db.full_coin_state(coin_id).await? else { - return Err(WalletError::MissingCoin(coin_id)); - }; - - let coin = row.coin_state.coin; + match self.db.coin_kind(coin_id).await? { + Some(CoinKind::Xch) => { + let coin = self + .db + .xch_coin(coin_id) + .await? + .ok_or(WalletError::MissingXchCoin(coin_id))?; - match row.kind { - CoinKind::Xch => { spends.add(coin); } - CoinKind::Cat => { - let Some(cat) = self.db.cat_coin(coin_id).await? else { - return Err(WalletError::MissingCoin(coin_id)); - }; + Some(CoinKind::Cat) => { + let cat = self + .db + .cat_coin(coin_id) + .await? + .ok_or(WalletError::MissingCatCoin(coin_id))?; + spends.add(cat); } - CoinKind::Did => { - let Some(did) = self.db.did_by_coin_id(coin_id).await? else { - return Err(WalletError::MissingCoin(coin_id)); - }; + Some(CoinKind::Did) => { + let did = self + .db + .did_coin(coin_id) + .await? + .ok_or(WalletError::MissingDidCoin(coin_id))?; + let metadata_ptr = ctx.alloc_hashed(&did.info.metadata)?; spends.add(did.with_metadata(metadata_ptr)); } - CoinKind::Nft => { - let Some(nft) = self.db.nft_by_coin_id(coin_id).await? else { - return Err(WalletError::MissingCoin(coin_id)); - }; + Some(CoinKind::Nft) => { + let nft = self + .db + .nft_coin(coin_id) + .await? + .ok_or(WalletError::MissingNftCoin(coin_id))?; + let metadata_ptr = ctx.alloc_hashed(&nft.info.metadata)?; spends.add(nft.with_metadata(metadata_ptr)); } - CoinKind::Unknown => { - return Err(WalletError::MissingCoin(coin_id)); + Some(CoinKind::Option) => { + let option = self + .db + .option_coin(coin_id) + .await? + .ok_or(WalletError::MissingOptionCoin(coin_id))?; + + spends.add(option); } + None => return Err(WalletError::MissingCoin(coin_id)), } } @@ -181,88 +198,76 @@ impl Wallet { selected_coin_ids: Vec, actions: &[Action], ) -> Result<(), WalletError> { - let deltas = Deltas::from_actions(actions); + let mut deltas = Deltas::from_actions(actions); - for &id in deltas.ids() { - let Id::Existing(launcher_id) = id else { - continue; - }; - - if spends.dids.contains_key(&id) || spends.nfts.contains_key(&id) { - continue; - } + deltas.update(Id::Xch).input += spends.xch.selected_amount(); - if let Some(did) = self.db.spendable_did(launcher_id).await? { - let metadata_ptr = ctx.alloc_hashed(&did.info.metadata)?; - spends.add(did.with_metadata(metadata_ptr)); - } else if let Some(nft) = self.db.spendable_nft(launcher_id).await? { - let metadata_ptr = ctx.alloc_hashed(&nft.info.metadata)?; - spends.add(nft.with_metadata(metadata_ptr)); - } + for (&id, cat) in &spends.cats { + deltas.update(id).input += cat.selected_amount(); } - let mut selected = indexmap! { None => spends.xch.selected_amount() }; - - for cat in spends.cats.values() { - if cat.items.is_empty() { - continue; - } - - let asset_id = cat.items[0].asset.info.asset_id; - - *selected.entry(Some(asset_id)).or_insert(0) += cat.selected_amount(); - } - - let requested = deltas - .ids() - .filter_map(|&id| { - let Id::Existing(asset_id) = id else { - return None; - }; - - if selected.contains_key(&Some(asset_id)) { - return None; - } - - Some((Some(asset_id), 0)) - }) - .collect_vec(); - let selected_coin_ids: HashSet = selected_coin_ids.into_iter().collect(); - for (asset_id, amount) in selected.into_iter().chain(requested) { - let id = asset_id.map(Id::Existing); + for &id in deltas.ids() { + let delta = deltas.get(&id).copied().unwrap_or_default(); + let required_amount = delta.output.saturating_sub(delta.input); - if let Some(id) = &id { - if spends.dids.contains_key(id) - || spends.nfts.contains_key(id) - || spends.options.contains_key(id) - { - continue; - } + if required_amount == 0 && !deltas.is_needed(&id) { + continue; } - let id = id.unwrap_or(Id::Xch); + match id { + Id::New(_) => continue, + Id::Xch => { + let coins = self + .select_xch_coins(required_amount, &selected_coin_ids) + .await?; - let delta = deltas.get(&id).copied().unwrap_or_default(); - let required_amount = delta.output.saturating_sub(amount + delta.input); - - if required_amount > 0 || deltas.is_needed(&id) { - if let Some(asset_id) = asset_id { - for cat in self - .select_cat_coins(asset_id, required_amount, &selected_coin_ids) - .await? - { - spends.add(cat); - } - } else { - for coin in self - .select_p2_coins(required_amount, &selected_coin_ids) - .await? - { + for coin in coins { spends.add(coin); } } + Id::Existing(asset_id) => match self.db.asset_kind(asset_id).await? { + Some(AssetKind::Token) => { + let coins = self + .select_cat_coins(asset_id, required_amount, &selected_coin_ids) + .await?; + + for coin in coins { + spends.add(coin); + } + } + Some(AssetKind::Did) => { + let did = self + .db + .did(asset_id) + .await? + .ok_or(WalletError::MissingDid(asset_id))?; + + let metadata_ptr = ctx.alloc_hashed(&did.info.metadata)?; + spends.add(did.with_metadata(metadata_ptr)); + } + Some(AssetKind::Nft) => { + let nft = self + .db + .nft(asset_id) + .await? + .ok_or(WalletError::MissingNft(asset_id))?; + + let metadata_ptr = ctx.alloc_hashed(&nft.info.metadata)?; + spends.add(nft.with_metadata(metadata_ptr)); + } + Some(AssetKind::Option) => { + let option = self + .db + .option(asset_id) + .await? + .ok_or(WalletError::MissingOptionContract(asset_id))?; + + spends.add(option); + } + None => return Err(WalletError::MissingAsset(asset_id)), + }, } } @@ -275,17 +280,72 @@ impl Wallet { deltas: &Deltas, spends: Spends, ) -> Result { - let mut keys = IndexMap::new(); + let mut p2_puzzles = IndexMap::new(); for p2_puzzle_hash in spends.p2_puzzle_hashes() { if p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() { continue; } - let key = self.db.synthetic_key(p2_puzzle_hash).await?; - keys.insert(p2_puzzle_hash, key); + let p2_puzzle = self.db.p2_puzzle(p2_puzzle_hash).await?; + + p2_puzzles.insert(p2_puzzle_hash, p2_puzzle); } - Ok(spends.finish_with_keys(ctx, deltas, Relation::AssertConcurrent, &keys)?) + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + + Ok(spends.finish( + ctx, + deltas, + Relation::AssertConcurrent, + |ctx, asset, kind| match kind { + SpendKind::Conditions(spend) => { + let Some(p2_puzzle) = p2_puzzles.get(&asset.p2_puzzle_hash()) else { + return Err(DriverError::MissingKey); + }; + + match p2_puzzle { + P2Puzzle::PublicKey(public_key) => StandardLayer::new(*public_key) + .spend_with_conditions(ctx, spend.finish()), + P2Puzzle::Clawback(clawback) => { + let custody = StandardLayer::new(clawback.public_key); + let spend = custody.spend_with_conditions(ctx, spend.finish())?; + + let clawback = ClawbackV2::new( + clawback.sender_puzzle_hash, + clawback.receiver_puzzle_hash, + clawback.seconds, + asset.coin().amount, + !matches!(asset, SpendableAsset::Xch(..)), + ); + + let is_receiver = + custody.tree_hash() == clawback.receiver_puzzle_hash.into(); + let is_sender = + custody.tree_hash() == clawback.sender_puzzle_hash.into(); + + if is_sender && timestamp < clawback.seconds { + clawback.sender_spend(ctx, spend) + } else if is_receiver && timestamp >= clawback.seconds { + clawback.receiver_spend(ctx, spend) + } else if is_sender || is_receiver { + return Err(DriverError::Custom( + "Cannot fulfill clawback spend".to_string(), + )); + } else { + return Err(DriverError::MissingKey); + } + } + P2Puzzle::OptionUnderlying(p2) => { + let custody = StandardLayer::new(p2.public_key); + let spend = custody.spend_with_conditions(ctx, spend.finish())?; + p2.option.clawback_spend(ctx, spend) + } + } + } + SpendKind::Settlement(spend) => SettlementLayer + .construct_spend(ctx, SettlementPaymentsSolution::new(spend.finish())), + }, + )?) } } diff --git a/crates/sage-wallet/src/wallet/cats.rs b/crates/sage-wallet/src/wallet/cats.rs index 12b795777..efc449a8f 100644 --- a/crates/sage-wallet/src/wallet/cats.rs +++ b/crates/sage-wallet/src/wallet/cats.rs @@ -1,12 +1,13 @@ use chia::{ bls::PublicKey, + clvm_utils::ToTreeHash, protocol::{Bytes, Bytes32, CoinSpend}, puzzles::cat::EverythingWithSignatureTailArgs, }; -use chia_wallet_sdk::driver::{Action, Id, Spend, SpendContext}; +use chia_wallet_sdk::driver::{Action, ClawbackV2, Id, Spend, SpendContext}; use clvmr::NodePtr; -use crate::WalletError; +use crate::{wallet::memos::Hint, WalletError}; use super::{memos::calculate_memos, Wallet}; @@ -39,16 +40,40 @@ impl Wallet { amounts: Vec<(Bytes32, u64)>, fee: u64, include_hint: bool, - memos: Option>, + memos: Vec, + clawback: Option, ) -> Result, WalletError> { + let sender_puzzle_hash = self.p2_puzzle_hash(false, true).await?; + let mut ctx = SpendContext::new(); let mut actions = vec![Action::fee(fee)]; for (puzzle_hash, amount) in amounts { - let memos = calculate_memos(&mut ctx, puzzle_hash, include_hint, memos.clone())?; + let clawback = clawback.map(|seconds| { + ClawbackV2::new(sender_puzzle_hash, puzzle_hash, seconds, amount, true) + }); + + let memos = calculate_memos( + &mut ctx, + if let Some(clawback) = clawback { + Hint::Clawback(clawback) + } else if include_hint { + Hint::P2PuzzleHash(puzzle_hash) + } else { + Hint::None + }, + memos.clone(), + )?; + + let p2_puzzle_hash = if let Some(clawback) = clawback { + clawback.tree_hash().into() + } else { + puzzle_hash + }; + actions.push(Action::send( Id::Existing(asset_id), - puzzle_hash, + p2_puzzle_hash, amount, memos, )); @@ -62,7 +87,10 @@ impl Wallet { #[cfg(test)] mod tests { + use std::time::Duration; + use test_log::test; + use tokio::time::sleep; use crate::TestWallet; @@ -76,14 +104,24 @@ mod tests { test.transact(coin_spends).await?; test.wait_for_coins().await; - assert_eq!(test.wallet.db.balance().await?, 500); - assert_eq!(test.wallet.db.spendable_coins().await?.len(), 1); + assert_eq!(test.wallet.db.xch_balance().await?, 500); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 1); assert_eq!(test.wallet.db.cat_balance(asset_id).await?, 1000); - assert_eq!(test.wallet.db.spendable_cat_coins(asset_id).await?.len(), 1); + assert_eq!( + test.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 1 + ); let coin_spends = test .wallet - .send_cat(asset_id, vec![(test.puzzle_hash, 750)], 0, true, None) + .send_cat( + asset_id, + vec![(test.puzzle_hash, 750)], + 0, + true, + vec![], + None, + ) .await?; assert_eq!(coin_spends.len(), 1); @@ -91,21 +129,191 @@ mod tests { test.wait_for_coins().await; assert_eq!(test.wallet.db.cat_balance(asset_id).await?, 1000); - assert_eq!(test.wallet.db.spendable_cat_coins(asset_id).await?.len(), 2); + assert_eq!( + test.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 2 + ); let coin_spends = test .wallet - .send_cat(asset_id, vec![(test.puzzle_hash, 1000)], 500, true, None) + .send_cat( + asset_id, + vec![(test.puzzle_hash, 1000)], + 500, + true, + vec![], + None, + ) .await?; assert_eq!(coin_spends.len(), 3); test.transact(coin_spends).await?; test.wait_for_coins().await; - assert_eq!(test.wallet.db.balance().await?, 0); - assert_eq!(test.wallet.db.spendable_coins().await?.len(), 0); + assert_eq!(test.wallet.db.xch_balance().await?, 0); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 0); + assert_eq!(test.wallet.db.cat_balance(asset_id).await?, 1000); + assert_eq!( + test.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 1 + ); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_send_cat_with_clawback_self() -> anyhow::Result<()> { + let mut test = TestWallet::new(1000).await?; + + let (coin_spends, asset_id) = test.wallet.issue_cat(1000, 0, None).await?; + assert_eq!(coin_spends.len(), 2); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + let timestamp = test.new_block_with_current_time().await?; + + let coin_spends = test + .wallet + .send_cat( + asset_id, + vec![(test.puzzle_hash, 1000)], + 0, + true, + vec![], + Some(timestamp + 5), + ) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.selectable_cat_balance(asset_id).await?, 0); + assert_eq!( + test.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 0 + ); + + sleep(Duration::from_secs(6)).await; + test.new_block_with_current_time().await?; + + assert_eq!(test.wallet.db.selectable_cat_balance(asset_id).await?, 1000); + assert_eq!( + test.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 1 + ); + + let coin_spends = test + .wallet + .send_cat( + asset_id, + vec![(test.puzzle_hash, 1000)], + 0, + true, + vec![], + None, + ) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + assert_eq!(test.wallet.db.cat_balance(asset_id).await?, 1000); - assert_eq!(test.wallet.db.spendable_cat_coins(asset_id).await?.len(), 1); + assert_eq!( + test.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 1 + ); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_send_cat_with_clawback_external() -> anyhow::Result<()> { + let mut alice = TestWallet::new(1000).await?; + let mut bob = alice.next(0).await?; + + let (coin_spends, asset_id) = alice.wallet.issue_cat(1000, 0, None).await?; + assert_eq!(coin_spends.len(), 2); + + alice.transact(coin_spends).await?; + alice.wait_for_coins().await; + + let timestamp = alice.new_block_with_current_time().await?; + + let coin_spends = alice + .wallet + .send_cat( + asset_id, + vec![(bob.puzzle_hash, 1000)], + 0, + true, + vec![], + Some(timestamp + 5), + ) + .await?; + + assert_eq!(coin_spends.len(), 1); + + alice.transact(coin_spends).await?; + + alice.wait_for_coins().await; + + assert_eq!(alice.wallet.db.selectable_cat_balance(asset_id).await?, 0); + assert_eq!( + alice.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 0 + ); + + bob.wait_for_puzzles().await; + + assert_eq!(bob.wallet.db.selectable_cat_balance(asset_id).await?, 0); + assert_eq!(bob.wallet.db.selectable_cat_coins(asset_id).await?.len(), 0); + + sleep(Duration::from_secs(6)).await; + bob.new_block_with_current_time().await?; + + assert_eq!(alice.wallet.db.selectable_cat_balance(asset_id).await?, 0); + assert_eq!( + alice.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 0 + ); + + assert_eq!(bob.wallet.db.selectable_cat_balance(asset_id).await?, 1000); + assert_eq!(bob.wallet.db.selectable_cat_coins(asset_id).await?.len(), 1); + + let coin_spends = bob + .wallet + .send_cat( + asset_id, + vec![(alice.puzzle_hash, 1000)], + 0, + true, + vec![], + None, + ) + .await?; + + assert_eq!(coin_spends.len(), 1); + + bob.transact(coin_spends).await?; + bob.wait_for_coins().await; + alice.wait_for_puzzles().await; + + assert_eq!( + alice.wallet.db.selectable_cat_balance(asset_id).await?, + 1000 + ); + assert_eq!( + alice.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 1 + ); + + assert_eq!(bob.wallet.db.selectable_cat_balance(asset_id).await?, 0); + assert_eq!(bob.wallet.db.selectable_cat_coins(asset_id).await?.len(), 0); Ok(()) } diff --git a/crates/sage-wallet/src/wallet/coin_management.rs b/crates/sage-wallet/src/wallet/coin_management.rs index e3423556d..a26fad125 100644 --- a/crates/sage-wallet/src/wallet/coin_management.rs +++ b/crates/sage-wallet/src/wallet/coin_management.rs @@ -103,7 +103,7 @@ mod tests { async fn test_xch_coin_management() -> anyhow::Result<()> { let mut test = TestWallet::new(1000).await?; - let coins = test.wallet.db.spendable_coins().await?; + let coins = test.wallet.db.selectable_xch_coins().await?; let coin_spends = test .wallet .split(coins.iter().map(Coin::coin_id).collect(), 3, 0) @@ -113,10 +113,10 @@ mod tests { test.transact(coin_spends).await?; test.wait_for_coins().await; - assert_eq!(test.wallet.db.balance().await?, 1000); - assert_eq!(test.wallet.db.spendable_coins().await?.len(), 3); + assert_eq!(test.wallet.db.xch_balance().await?, 1000); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 3); - let coins = test.wallet.db.spendable_coins().await?; + let coins = test.wallet.db.selectable_xch_coins().await?; let coin_spends = test .wallet .combine(coins.iter().map(Coin::coin_id).collect(), 0) @@ -126,8 +126,8 @@ mod tests { test.transact(coin_spends).await?; test.wait_for_coins().await; - assert_eq!(test.wallet.db.balance().await?, 1000); - assert_eq!(test.wallet.db.spendable_coins().await?.len(), 1); + assert_eq!(test.wallet.db.xch_balance().await?, 1000); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 1); Ok(()) } @@ -140,21 +140,16 @@ mod tests { test.transact(coin_spends).await?; test.wait_for_coins().await; - let mut cats = test.wallet.db.spendable_cat_coins(asset_id).await?; + let mut cats = test.wallet.db.selectable_cat_coins(asset_id).await?; assert_eq!(test.wallet.db.cat_balance(asset_id).await?, 100); assert_eq!(cats.len(), 1); - let cat = test - .wallet - .db - .cat_coin(cats.remove(0).coin.coin_id()) - .await? - .expect("missing cat"); + let cat = cats.remove(0); let coin_spends = test.wallet.split(vec![cat.coin.coin_id()], 2, 0).await?; test.transact(coin_spends).await?; test.wait_for_coins().await; - let cats = test.wallet.db.spendable_cat_coins(asset_id).await?; + let cats = test.wallet.db.selectable_cat_coins(asset_id).await?; assert_eq!(test.wallet.db.cat_balance(asset_id).await?, 100); assert_eq!(cats.len(), 2); @@ -166,7 +161,10 @@ mod tests { test.wait_for_coins().await; assert_eq!(test.wallet.db.cat_balance(asset_id).await?, 100); - assert_eq!(test.wallet.db.spendable_cat_coins(asset_id).await?.len(), 1); + assert_eq!( + test.wallet.db.selectable_cat_coins(asset_id).await?.len(), + 1 + ); Ok(()) } diff --git a/crates/sage-wallet/src/wallet/derivations.rs b/crates/sage-wallet/src/wallet/derivations.rs index bcbeb0256..d91b66d54 100644 --- a/crates/sage-wallet/src/wallet/derivations.rs +++ b/crates/sage-wallet/src/wallet/derivations.rs @@ -5,7 +5,7 @@ use chia::{ protocol::Bytes32, puzzles::{standard::StandardArgs, DeriveSynthetic}, }; -use sage_database::DatabaseTx; +use sage_database::{DatabaseTx, Derivation}; use crate::WalletError; @@ -28,8 +28,15 @@ impl Wallet { let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); - tx.insert_derivation(p2_puzzle_hash, index, false, synthetic_key) - .await?; + tx.insert_custody_p2_puzzle( + p2_puzzle_hash, + synthetic_key, + Derivation { + derivation_index: index, + is_hardened: false, + }, + ) + .await?; puzzle_hashes.push(p2_puzzle_hash); } @@ -45,35 +52,25 @@ impl Wallet { ) -> Result, WalletError> { let mut tx = self.db.tx().await?; - let max_used = tx.max_used_derivation_index(hardened).await?; + let unused_index = tx.unused_derivation_index(hardened).await?; let next_index = tx.derivation_index(hardened).await?; - let (mut start, mut end) = if reuse { - let start = max_used.unwrap_or(0); + let range = if reuse { + let start = unused_index.saturating_sub(count); let end = next_index.min(start + count); - (start, end) + start..end } else { - let start = max_used.map_or(0, |i| i + 1); - let end = next_index.min(start + count); - (start, end) + unused_index..(unused_index + count) }; - if end - start < count && reuse { - start = start.saturating_sub(count - (end - start)); - } - - if end - start < count { - end = next_index.min(end + count - (end - start)); - } - - if end - start < count { + if range.len() < count as usize { return Err(WalletError::InsufficientDerivations); } let mut p2_puzzle_hashes = Vec::new(); - for index in start..end { - let p2_puzzle_hash = tx.p2_puzzle_hash(index, hardened).await?; + for index in range { + let p2_puzzle_hash = tx.custody_p2_puzzle_hash(index, hardened).await?; p2_puzzle_hashes.push(p2_puzzle_hash); } diff --git a/crates/sage-wallet/src/wallet/dids.rs b/crates/sage-wallet/src/wallet/dids.rs index d70f30ba0..30743b7c7 100644 --- a/crates/sage-wallet/src/wallet/dids.rs +++ b/crates/sage-wallet/src/wallet/dids.rs @@ -1,10 +1,13 @@ use chia::{ - clvm_utils::tree_hash_atom, + clvm_utils::{tree_hash_atom, ToTreeHash}, protocol::{Bytes32, CoinSpend, Program}, }; -use chia_wallet_sdk::driver::{Action, Did, Id, SpendContext}; +use chia_wallet_sdk::driver::{Action, ClawbackV2, Did, Id, SpendContext}; -use crate::WalletError; +use crate::{ + wallet::memos::{calculate_memos, Hint}, + WalletError, +}; use super::Wallet; @@ -34,13 +37,48 @@ impl Wallet { did_ids: Vec, puzzle_hash: Bytes32, fee: u64, + clawback: Option, ) -> Result, WalletError> { + let sender_puzzle_hash = self.p2_puzzle_hash(false, true).await?; + let mut ctx = SpendContext::new(); let mut actions = vec![Action::fee(fee)]; for did_id in did_ids { - let hint = ctx.hint(puzzle_hash)?; - actions.push(Action::send(Id::Existing(did_id), puzzle_hash, 1, hint)); + let amount = self + .db + .did(did_id) + .await? + .ok_or(WalletError::MissingDid(did_id))? + .coin + .amount; + + let clawback = clawback.map(|seconds| { + ClawbackV2::new(sender_puzzle_hash, puzzle_hash, seconds, amount, true) + }); + + let memos = calculate_memos( + &mut ctx, + if let Some(clawback) = clawback { + Hint::Clawback(clawback) + } else { + Hint::P2PuzzleHash(puzzle_hash) + }, + vec![], + )?; + + let p2_puzzle_hash = if let Some(clawback) = clawback { + clawback.tree_hash().into() + } else { + puzzle_hash + }; + + actions.push(Action::send( + Id::Existing(did_id), + p2_puzzle_hash, + amount, + memos, + )); } self.spend(&mut ctx, vec![], &actions).await?; @@ -73,9 +111,12 @@ impl Wallet { #[cfg(test)] mod tests { + use std::time::Duration; + use crate::TestWallet; use test_log::test; + use tokio::time::sleep; #[test(tokio::test)] async fn test_create_did() -> anyhow::Result<()> { @@ -88,7 +129,7 @@ mod tests { for _ in 0..2 { let coin_spends = test .wallet - .transfer_dids(vec![did.info.launcher_id], test.puzzle_hash, 0) + .transfer_dids(vec![did.info.launcher_id], test.puzzle_hash, 0, None) .await?; test.transact(coin_spends).await?; @@ -103,10 +144,139 @@ mod tests { test.wait_for_coins().await; } - assert_ne!( - test.wallet.db.spendable_did(did.info.launcher_id).await?, - None - ); + assert_ne!(test.wallet.db.did(did.info.launcher_id).await?, None); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_transfer_did_external() -> anyhow::Result<()> { + let mut alice = TestWallet::new(1000).await?; + let mut bob = alice.next(0).await?; + + let (coin_spends, did) = alice.wallet.create_did(0).await?; + alice.transact(coin_spends).await?; + alice.wait_for_coins().await; + + let did_id = did.info.launcher_id; + + let coin_spends = alice + .wallet + .transfer_dids(vec![did_id], bob.puzzle_hash, 0, None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + alice.transact(coin_spends).await?; + + alice.wait_for_coins().await; + + assert!(alice.wallet.db.did(did_id).await?.is_none()); + assert!(alice.wallet.db.spendable_did(did_id).await?.is_none()); + + bob.wait_for_puzzles().await; + + assert!(bob.wallet.db.did(did_id).await?.is_some()); + assert!(bob.wallet.db.spendable_did(did_id).await?.is_some()); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_transfer_did_with_clawback_self() -> anyhow::Result<()> { + let mut test = TestWallet::new(1000).await?; + + let (coin_spends, did) = test.wallet.create_did(0).await?; + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + let did_id = did.info.launcher_id; + + let timestamp = test.new_block_with_current_time().await?; + + let coin_spends = test + .wallet + .transfer_dids(vec![did_id], test.puzzle_hash, 0, Some(timestamp + 5)) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert!(test.wallet.db.spendable_did(did_id).await?.is_some()); + + sleep(Duration::from_secs(6)).await; + test.new_block_with_current_time().await?; + + assert!(test.wallet.db.spendable_did(did_id).await?.is_some()); + + let coin_spends = test + .wallet + .transfer_dids(vec![did_id], test.puzzle_hash, 0, None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert!(test.wallet.db.spendable_did(did_id).await?.is_some()); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_transfer_did_with_clawback_external() -> anyhow::Result<()> { + let mut alice = TestWallet::new(1000).await?; + let mut bob = alice.next(0).await?; + + let (coin_spends, did) = alice.wallet.create_did(0).await?; + alice.transact(coin_spends).await?; + alice.wait_for_coins().await; + + let did_id = did.info.launcher_id; + + let timestamp = alice.new_block_with_current_time().await?; + + let coin_spends = alice + .wallet + .transfer_dids(vec![did_id], bob.puzzle_hash, 0, Some(timestamp + 5)) + .await?; + + assert_eq!(coin_spends.len(), 1); + + alice.transact(coin_spends).await?; + + alice.wait_for_coins().await; + + assert!(alice.wallet.db.spendable_did(did_id).await?.is_some()); + assert!(bob.wallet.db.spendable_did(did_id).await?.is_none()); + + bob.wait_for_puzzles().await; + + assert!(alice.wallet.db.spendable_did(did_id).await?.is_some()); + assert!(bob.wallet.db.spendable_did(did_id).await?.is_none()); + + sleep(Duration::from_secs(6)).await; + bob.new_block_with_current_time().await?; + + assert!(alice.wallet.db.spendable_did(did_id).await?.is_none()); + assert!(bob.wallet.db.spendable_did(did_id).await?.is_some()); + + let coin_spends = bob + .wallet + .transfer_dids(vec![did_id], alice.puzzle_hash, 0, None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + bob.transact(coin_spends).await?; + bob.wait_for_coins().await; + alice.wait_for_puzzles().await; + + assert!(alice.wallet.db.spendable_did(did_id).await?.is_some()); + assert!(bob.wallet.db.spendable_did(did_id).await?.is_none()); Ok(()) } diff --git a/crates/sage-wallet/src/wallet/memos.rs b/crates/sage-wallet/src/wallet/memos.rs index 20b18b02c..be89cc2f7 100644 --- a/crates/sage-wallet/src/wallet/memos.rs +++ b/crates/sage-wallet/src/wallet/memos.rs @@ -1,31 +1,43 @@ use chia::protocol::{Bytes, Bytes32}; -use chia_wallet_sdk::{driver::SpendContext, prelude::Memos}; +use chia_wallet_sdk::{ + driver::{ClawbackV2, SpendContext}, + prelude::Memos, +}; use crate::WalletError; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Hint { + None, + P2PuzzleHash(Bytes32), + Clawback(ClawbackV2), +} + pub fn calculate_memos( ctx: &mut SpendContext, - p2_puzzle_hash: Bytes32, - include_hint: bool, - memos: Option>, + hint: Hint, + memos: Vec, ) -> Result { - let mut result = None; + let mut result = Vec::new(); - if include_hint { - result = Some(vec![p2_puzzle_hash.into()]); + match hint { + Hint::None => {} + Hint::P2PuzzleHash(p2_puzzle_hash) => { + result.push(ctx.alloc(&p2_puzzle_hash)?); + } + Hint::Clawback(clawback) => { + result.push(ctx.alloc(&clawback.receiver_puzzle_hash)?); + result.push(ctx.alloc(&clawback.memo())?); + } } - if let Some(memos) = memos { - if let Some(result) = result.as_mut() { - result.extend(memos); - } else { - result = Some(memos); - } + for memo in memos { + result.push(ctx.alloc(&memo)?); } - Ok(if let Some(result) = result { - ctx.memos(&result)? - } else { + Ok(if result.is_empty() { Memos::None + } else { + ctx.memos(&result)? }) } diff --git a/crates/sage-wallet/src/wallet/multi_send.rs b/crates/sage-wallet/src/wallet/multi_send.rs index 2bdb0da78..7e81ac9c4 100644 --- a/crates/sage-wallet/src/wallet/multi_send.rs +++ b/crates/sage-wallet/src/wallet/multi_send.rs @@ -1,7 +1,7 @@ use chia::protocol::{Bytes, Bytes32, CoinSpend}; use chia_wallet_sdk::driver::{Action, Id, SpendContext}; -use crate::WalletError; +use crate::{wallet::memos::Hint, WalletError}; use super::{memos::calculate_memos, Wallet}; @@ -10,7 +10,7 @@ pub struct MultiSendPayment { pub asset_id: Option, pub amount: u64, pub puzzle_hash: Bytes32, - pub memos: Option>, + pub memos: Vec, } impl MultiSendPayment { @@ -19,7 +19,7 @@ impl MultiSendPayment { asset_id: None, amount, puzzle_hash, - memos: None, + memos: vec![], } } @@ -28,7 +28,7 @@ impl MultiSendPayment { asset_id: Some(asset_id), amount, puzzle_hash, - memos: None, + memos: vec![], } } @@ -54,8 +54,11 @@ impl Wallet { for payment in payments { let memos = calculate_memos( &mut ctx, - payment.puzzle_hash, - payment.asset_id.is_some(), + if payment.asset_id.is_some() { + Hint::P2PuzzleHash(payment.puzzle_hash) + } else { + Hint::None + }, payment.memos, )?; actions.push(Action::send( @@ -101,7 +104,7 @@ mod tests { alice.transact(coin_spends).await?; alice.wait_for_coins().await; - assert_eq!(alice.wallet.db.balance().await?, 2000); + assert_eq!(alice.wallet.db.xch_balance().await?, 2000); assert_eq!(alice.wallet.db.cat_balance(bronze).await?, 1000); assert_eq!(alice.wallet.db.cat_balance(silver).await?, 1000); assert_eq!(alice.wallet.db.cat_balance(gold).await?, 1000); @@ -123,12 +126,12 @@ mod tests { alice.wait_for_coins().await; bob.wait_for_puzzles().await; - assert_eq!(alice.wallet.db.balance().await?, 2000); + assert_eq!(alice.wallet.db.xch_balance().await?, 2000); assert_eq!(alice.wallet.db.cat_balance(bronze).await?, 0); assert_eq!(alice.wallet.db.cat_balance(silver).await?, 500); assert_eq!(alice.wallet.db.cat_balance(gold).await?, 900); - assert_eq!(bob.wallet.db.balance().await?, 0); + assert_eq!(bob.wallet.db.xch_balance().await?, 0); assert_eq!(bob.wallet.db.cat_balance(bronze).await?, 1000); assert_eq!(bob.wallet.db.cat_balance(silver).await?, 500); assert_eq!(bob.wallet.db.cat_balance(gold).await?, 100); @@ -143,12 +146,12 @@ mod tests { alice.wait_for_coins().await; bob.wait_for_coins().await; - assert_eq!(alice.wallet.db.balance().await?, 1250); + assert_eq!(alice.wallet.db.xch_balance().await?, 1250); assert_eq!(alice.wallet.db.cat_balance(bronze).await?, 0); assert_eq!(alice.wallet.db.cat_balance(silver).await?, 500); assert_eq!(alice.wallet.db.cat_balance(gold).await?, 900); - assert_eq!(bob.wallet.db.balance().await?, 500); + assert_eq!(bob.wallet.db.xch_balance().await?, 500); assert_eq!(bob.wallet.db.cat_balance(bronze).await?, 1000); assert_eq!(bob.wallet.db.cat_balance(silver).await?, 500); assert_eq!(bob.wallet.db.cat_balance(gold).await?, 100); @@ -163,12 +166,12 @@ mod tests { alice.wait_for_coins().await; bob.wait_for_coins().await; - assert_eq!(alice.wallet.db.balance().await?, 750); + assert_eq!(alice.wallet.db.xch_balance().await?, 750); assert_eq!(alice.wallet.db.cat_balance(bronze).await?, 0); assert_eq!(alice.wallet.db.cat_balance(silver).await?, 500); assert_eq!(alice.wallet.db.cat_balance(gold).await?, 900); - assert_eq!(bob.wallet.db.balance().await?, 1000); + assert_eq!(bob.wallet.db.xch_balance().await?, 1000); assert_eq!(bob.wallet.db.cat_balance(bronze).await?, 1000); assert_eq!(bob.wallet.db.cat_balance(silver).await?, 500); assert_eq!(bob.wallet.db.cat_balance(gold).await?, 100); @@ -190,12 +193,12 @@ mod tests { alice.wait_for_coins().await; bob.wait_for_puzzles().await; - assert_eq!(alice.wallet.db.balance().await?, 0); + assert_eq!(alice.wallet.db.xch_balance().await?, 0); assert_eq!(alice.wallet.db.cat_balance(bronze).await?, 0); assert_eq!(alice.wallet.db.cat_balance(silver).await?, 0); assert_eq!(alice.wallet.db.cat_balance(gold).await?, 0); - assert_eq!(bob.wallet.db.balance().await?, 1350); + assert_eq!(bob.wallet.db.xch_balance().await?, 1350); assert_eq!(bob.wallet.db.cat_balance(bronze).await?, 1000); assert_eq!(bob.wallet.db.cat_balance(silver).await?, 1000); assert_eq!(bob.wallet.db.cat_balance(gold).await?, 1000); diff --git a/crates/sage-wallet/src/wallet/nfts.rs b/crates/sage-wallet/src/wallet/nfts.rs index 38c4b4715..83302d837 100644 --- a/crates/sage-wallet/src/wallet/nfts.rs +++ b/crates/sage-wallet/src/wallet/nfts.rs @@ -1,11 +1,17 @@ use chia::{ + clvm_utils::ToTreeHash, protocol::{Bytes32, CoinSpend, Program}, puzzles::nft::NftMetadata, }; use chia_puzzles::NFT_METADATA_UPDATER_DEFAULT_HASH; -use chia_wallet_sdk::driver::{Action, Id, MetadataUpdate, Nft, SpendContext, TransferNftById}; +use chia_wallet_sdk::driver::{ + Action, ClawbackV2, Id, MetadataUpdate, Nft, SpendContext, TransferNftById, +}; -use crate::WalletError; +use crate::{ + wallet::memos::{calculate_memos, Hint}, + WalletError, +}; use super::Wallet; @@ -76,14 +82,42 @@ impl Wallet { nft_ids: Vec, puzzle_hash: Bytes32, fee: u64, + clawback: Option, ) -> Result, WalletError> { - let is_external = !self.db.is_p2_puzzle_hash(puzzle_hash).await?; + let sender_puzzle_hash = self.p2_puzzle_hash(false, true).await?; + let is_external = !self.db.is_custody_p2_puzzle_hash(puzzle_hash).await?; let mut ctx = SpendContext::new(); let mut actions = vec![Action::fee(fee)]; for nft_id in nft_ids { - let hint = ctx.hint(puzzle_hash)?; + let amount = self + .db + .nft(nft_id) + .await? + .ok_or(WalletError::MissingNft(nft_id))? + .coin + .amount; + + let clawback = clawback.map(|seconds| { + ClawbackV2::new(sender_puzzle_hash, puzzle_hash, seconds, amount, true) + }); + + let memos = calculate_memos( + &mut ctx, + if let Some(clawback) = clawback { + Hint::Clawback(clawback) + } else { + Hint::P2PuzzleHash(puzzle_hash) + }, + vec![], + )?; + + let p2_puzzle_hash = if let Some(clawback) = clawback { + clawback.tree_hash().into() + } else { + puzzle_hash + }; if is_external { actions.push(Action::update_nft( @@ -93,7 +127,12 @@ impl Wallet { )); } - actions.push(Action::send(Id::Existing(nft_id), puzzle_hash, 1, hint)); + actions.push(Action::send( + Id::Existing(nft_id), + p2_puzzle_hash, + amount, + memos, + )); } self.spend(&mut ctx, vec![], &actions).await?; @@ -149,7 +188,10 @@ impl Wallet { #[cfg(test)] mod tests { + use std::time::Duration; + use test_log::test; + use tokio::time::sleep; use crate::TestWallet; @@ -199,7 +241,7 @@ mod tests { for _ in 0..2 { let coin_spends = test .wallet - .transfer_nfts(vec![nft.info.launcher_id], puzzle_hash, 0) + .transfer_nfts(vec![nft.info.launcher_id], puzzle_hash, 0, None) .await?; test.transact(coin_spends).await?; test.wait_for_coins().await; @@ -208,10 +250,10 @@ mod tests { let nft = test .wallet .db - .nft_row(nft.info.launcher_id) + .nft(nft.info.launcher_id) .await? .expect("missing nft"); - assert_eq!(nft.owner_did, Some(did.info.launcher_id)); + assert_eq!(nft.info.current_owner, Some(did.info.launcher_id)); Ok(()) } @@ -246,7 +288,7 @@ mod tests { let coin_spends = test .wallet - .transfer_nfts(vec![nft.info.launcher_id], puzzle_hash, 0) + .transfer_nfts(vec![nft.info.launcher_id], puzzle_hash, 0, None) .await?; test.transact(coin_spends).await?; test.wait_for_coins().await; @@ -254,10 +296,10 @@ mod tests { let nft = test .wallet .db - .nft_row(nft.info.launcher_id) + .nft(nft.info.launcher_id) .await? .expect("missing nft"); - assert_eq!(nft.owner_did, Some(did.info.launcher_id)); + assert_eq!(nft.info.current_owner, Some(did.info.launcher_id)); Ok(()) } @@ -297,7 +339,7 @@ mod tests { let coin_spends = alice .wallet - .transfer_nfts(vec![nft.info.launcher_id], puzzle_hash, 0) + .transfer_nfts(vec![nft.info.launcher_id], puzzle_hash, 0, None) .await?; alice.transact(coin_spends).await?; bob.wait_for_puzzles().await; @@ -305,10 +347,10 @@ mod tests { let row = bob .wallet .db - .nft_row(nft.info.launcher_id) + .nft(nft.info.launcher_id) .await? .expect("missing nft"); - assert_eq!(row.owner_did, None); + assert_eq!(row.info.current_owner, None); let coin_spends = bob .wallet @@ -323,7 +365,7 @@ mod tests { let coin_spends = bob .wallet - .transfer_nfts(vec![nft.info.launcher_id], puzzle_hash, 0) + .transfer_nfts(vec![nft.info.launcher_id], puzzle_hash, 0, None) .await?; bob.transact(coin_spends).await?; bob.wait_for_coins().await; @@ -331,10 +373,10 @@ mod tests { let row = bob .wallet .db - .nft_row(nft.info.launcher_id) + .nft(nft.info.launcher_id) .await? .expect("missing nft"); - assert_eq!(row.owner_did, Some(bob_did.info.launcher_id)); + assert_eq!(row.info.current_owner, Some(bob_did.info.launcher_id)); Ok(()) } @@ -381,4 +423,136 @@ mod tests { Ok(()) } + + #[test(tokio::test)] + async fn test_transfer_nft_with_clawback_self() -> anyhow::Result<()> { + let mut test = TestWallet::new(1000).await?; + + let (coin_spends, did) = test.wallet.create_did(0).await?; + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + let (coin_spends, mut nfts) = test + .wallet + .bulk_mint_nfts( + 0, + did.info.launcher_id, + vec![WalletNftMint { + metadata: NftMetadata::default(), + p2_puzzle_hash: None, + royalty_puzzle_hash: Some(Bytes32::default()), + royalty_basis_points: 300, + }], + ) + .await?; + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + let nft_id = nfts.remove(0).info.launcher_id; + + let timestamp = test.new_block_with_current_time().await?; + + let coin_spends = test + .wallet + .transfer_nfts(vec![nft_id], test.puzzle_hash, 0, Some(timestamp + 5)) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert!(test.wallet.db.spendable_nft(nft_id).await?.is_some()); + + sleep(Duration::from_secs(6)).await; + test.new_block_with_current_time().await?; + + assert!(test.wallet.db.spendable_nft(nft_id).await?.is_some()); + + let coin_spends = test + .wallet + .transfer_nfts(vec![nft_id], test.puzzle_hash, 0, None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert!(test.wallet.db.nft(nft_id).await?.is_some()); + assert!(test.wallet.db.spendable_nft(nft_id).await?.is_some()); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_transfer_nft_with_clawback_external() -> anyhow::Result<()> { + let mut alice = TestWallet::new(1000).await?; + let mut bob = alice.next(0).await?; + + let (coin_spends, did) = alice.wallet.create_did(0).await?; + alice.transact(coin_spends).await?; + alice.wait_for_coins().await; + + let (coin_spends, mut nfts) = alice + .wallet + .bulk_mint_nfts( + 0, + did.info.launcher_id, + vec![WalletNftMint { + metadata: NftMetadata::default(), + p2_puzzle_hash: None, + royalty_puzzle_hash: Some(Bytes32::default()), + royalty_basis_points: 300, + }], + ) + .await?; + alice.transact(coin_spends).await?; + alice.wait_for_coins().await; + + let nft_id = nfts.remove(0).info.launcher_id; + + let timestamp = alice.new_block_with_current_time().await?; + + let coin_spends = alice + .wallet + .transfer_nfts(vec![nft_id], bob.puzzle_hash, 0, Some(timestamp + 5)) + .await?; + + assert_eq!(coin_spends.len(), 1); + + alice.transact(coin_spends).await?; + + alice.wait_for_coins().await; + + assert!(alice.wallet.db.spendable_nft(nft_id).await?.is_some()); + assert!(bob.wallet.db.spendable_nft(nft_id).await?.is_none()); + + bob.wait_for_puzzles().await; + + assert!(alice.wallet.db.spendable_nft(nft_id).await?.is_some()); + assert!(bob.wallet.db.spendable_nft(nft_id).await?.is_none()); + + sleep(Duration::from_secs(6)).await; + bob.new_block_with_current_time().await?; + + assert!(alice.wallet.db.spendable_nft(nft_id).await?.is_none()); + assert!(bob.wallet.db.spendable_nft(nft_id).await?.is_some()); + + let coin_spends = bob + .wallet + .transfer_nfts(vec![nft_id], alice.puzzle_hash, 0, None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + bob.transact(coin_spends).await?; + bob.wait_for_coins().await; + alice.wait_for_puzzles().await; + + assert!(alice.wallet.db.spendable_nft(nft_id).await?.is_some()); + assert!(bob.wallet.db.spendable_nft(nft_id).await?.is_none()); + + Ok(()) + } } diff --git a/crates/sage-wallet/src/wallet/offer.rs b/crates/sage-wallet/src/wallet/offer.rs index 185c398a6..05f891ee0 100644 --- a/crates/sage-wallet/src/wallet/offer.rs +++ b/crates/sage-wallet/src/wallet/offer.rs @@ -1,6 +1,7 @@ mod aggregate_offer; mod cancel_offer; mod make_offer; +mod offer_assets; mod take_offer; pub use aggregate_offer::*; @@ -8,8 +9,6 @@ pub use make_offer::*; #[cfg(test)] mod tests { - use std::time::Duration; - use chia::{ clvm_traits::{FromClvm, ToClvm}, protocol::{Bytes32, Program}, @@ -17,12 +16,10 @@ mod tests { }; use clvmr::Allocator; use indexmap::indexmap; + use sage_database::NftOfferInfo; use test_log::test; - use crate::{ - default_test_options, Offered, Requested, RequestedNft, SyncOptions, TestWallet, Timeouts, - WalletNftMint, - }; + use crate::{Offered, Requested, TestWallet, WalletNftMint}; use super::aggregate_offers; @@ -71,7 +68,7 @@ mod tests { // Check balances assert_eq!(alice.wallet.db.cat_balance(asset_id).await?, 1000); - assert_eq!(bob.wallet.db.balance().await?, 750); + assert_eq!(bob.wallet.db.xch_balance().await?, 750); Ok(()) } @@ -117,7 +114,7 @@ mod tests { }, Requested { nfts: indexmap! { - nft.info.launcher_id => RequestedNft { + nft.info.launcher_id => NftOfferInfo { metadata, metadata_updater_puzzle_hash: nft.info.metadata_updater_puzzle_hash, royalty_puzzle_hash: nft.info.royalty_puzzle_hash, @@ -147,11 +144,8 @@ mod tests { alice.wait_for_puzzles().await; // Check balances - assert_ne!( - alice.wallet.db.spendable_nft(nft.info.launcher_id).await?, - None - ); - assert_eq!(bob.wallet.db.balance().await?, 1000); + assert_ne!(alice.wallet.db.nft(nft.info.launcher_id).await?, None); + assert_eq!(bob.wallet.db.xch_balance().await?, 1000); Ok(()) } @@ -216,11 +210,8 @@ mod tests { bob.wait_for_coins().await; // Check balances - assert_eq!(alice.wallet.db.balance().await?, 1000); - assert_ne!( - bob.wallet.db.spendable_nft(nft.info.launcher_id).await?, - None - ); + assert_eq!(alice.wallet.db.xch_balance().await?, 1000); + assert_ne!(bob.wallet.db.nft(nft.info.launcher_id).await?, None); Ok(()) } @@ -297,19 +288,13 @@ mod tests { bob.wait_for_coins().await; // Check balances - assert_eq!(alice.wallet.db.balance().await?, 1000); + assert_eq!(alice.wallet.db.xch_balance().await?, 1000); assert_ne!( - bob.wallet - .db - .spendable_nft(nft_id_first.info.launcher_id) - .await?, + bob.wallet.db.nft(nft_id_first.info.launcher_id).await?, None ); assert_ne!( - bob.wallet - .db - .spendable_nft(nft_id_second.info.launcher_id) - .await?, + bob.wallet.db.nft(nft_id_second.info.launcher_id).await?, None ); @@ -395,17 +380,11 @@ mod tests { // Check balances assert_eq!(alice.wallet.db.cat_balance(asset_id).await?, 1000); assert_ne!( - bob.wallet - .db - .spendable_nft(nft_id_first.info.launcher_id) - .await?, + bob.wallet.db.nft(nft_id_first.info.launcher_id).await?, None ); assert_ne!( - bob.wallet - .db - .spendable_nft(nft_id_second.info.launcher_id) - .await?, + bob.wallet.db.nft(nft_id_second.info.launcher_id).await?, None ); @@ -484,19 +463,13 @@ mod tests { bob.wait_for_coins().await; // Check balances - assert_eq!(alice.wallet.db.balance().await?, 1000); + assert_eq!(alice.wallet.db.xch_balance().await?, 1000); assert_ne!( - bob.wallet - .db - .spendable_nft(nft_id_first.info.launcher_id) - .await?, + bob.wallet.db.nft(nft_id_first.info.launcher_id).await?, None ); assert_ne!( - bob.wallet - .db - .spendable_nft(nft_id_second.info.launcher_id) - .await?, + bob.wallet.db.nft(nft_id_second.info.launcher_id).await?, None ); @@ -595,21 +568,9 @@ mod tests { bob.wait_for_coins().await; // Check balances - assert_eq!(alice.wallet.db.balance().await?, 1000); - assert_ne!( - bob.wallet - .db - .spendable_nft(nft_first.info.launcher_id) - .await?, - None - ); - assert_ne!( - bob.wallet - .db - .spendable_nft(nft_second.info.launcher_id) - .await?, - None - ); + assert_eq!(alice.wallet.db.xch_balance().await?, 1000); + assert_ne!(bob.wallet.db.nft(nft_first.info.launcher_id).await?, None); + assert_ne!(bob.wallet.db.nft(nft_second.info.launcher_id).await?, None); Ok(()) } @@ -648,7 +609,7 @@ mod tests { bob.wait_for_coins().await; // Check balances - assert_eq!(bob.wallet.db.balance().await?, 1000); + assert_eq!(bob.wallet.db.xch_balance().await?, 1000); Ok(()) } @@ -698,22 +659,8 @@ mod tests { #[test(tokio::test)] async fn test_offer_nft_single_sided() -> anyhow::Result<()> { - let options = default_test_options(); - let mut alice = TestWallet::new(2).await?; - let mut bob = alice - .next_with_options( - 0, - SyncOptions { - puzzle_batch_size_per_peer: 1, - timeouts: Timeouts { - puzzle_delay: Duration::from_secs(1), - ..options.timeouts - }, - ..options - }, - ) - .await?; + let mut bob = alice.next(0).await?; let (coin_spends, did) = alice.wallet.create_did(0).await?; alice.transact(coin_spends).await?; @@ -766,6 +713,24 @@ mod tests { // We need to wait for both wallets to sync in this case bob.wait_for_coins().await; + let nft = bob + .wallet + .db + .spendable_nft(nft.info.launcher_id) + .await? + .expect("NFT should be spendable"); + + let is_spent = bob + .sim + .lock() + .await + .coin_state(nft.coin.coin_id()) + .expect("coin should exist") + .spent_height + .is_some(); + + assert!(!is_spent); + // Resync to make sure the NFT is spendable even if it wasn't pending previously bob.resync().await?; bob.wait_for_puzzles().await; @@ -778,19 +743,11 @@ mod tests { .await? .expect("NFT should be spendable"); - let coin_id = bob - .wallet - .db - .nft_row(nft.info.launcher_id) - .await? - .expect("NFT should exist") - .coin_id; - let is_spent = bob - .wallet - .db - .coin_state(coin_id) - .await? + .sim + .lock() + .await + .coin_state(nft.coin.coin_id()) .expect("coin should exist") .spent_height .is_some(); diff --git a/crates/sage-wallet/src/wallet/offer/cancel_offer.rs b/crates/sage-wallet/src/wallet/offer/cancel_offer.rs index 58c7ef82f..08c7aa76a 100644 --- a/crates/sage-wallet/src/wallet/offer/cancel_offer.rs +++ b/crates/sage-wallet/src/wallet/offer/cancel_offer.rs @@ -1,6 +1,5 @@ use chia::protocol::{CoinSpend, SpendBundle}; use chia_wallet_sdk::driver::{Action, Offer, SpendContext}; -use sage_database::CoinKind; use crate::{Wallet, WalletError}; @@ -14,41 +13,26 @@ impl Wallet { let offer = Offer::from_spend_bundle(&mut ctx, &spend_bundle)?; - let mut coin_states = Vec::new(); + let mut coins = Vec::new(); - for coin_spend in &offer.spend_bundle().coin_spends { - let Some(row) = self.db.full_coin_state(coin_spend.coin.coin_id()).await? else { - continue; - }; + for coin_spend in offer.cancellable_coin_spends()? { + let coin_id = coin_spend.coin.coin_id(); - if row.coin_state.created_height.is_none() { + let Some(kind) = self.db.coin_kind(coin_id).await? else { continue; - } - - if row.coin_state.spent_height.is_some() { - return Err(WalletError::UncancellableOffer); - } - - match row.kind { - CoinKind::Xch | CoinKind::Cat | CoinKind::Did | CoinKind::Nft => {} - CoinKind::Unknown => continue, - } + }; - coin_states.push(row); + coins.push((kind, coin_id)); } - coin_states.sort_by_key(|row| row.kind); + coins.sort(); - let Some(row) = coin_states.first().copied() else { + let Some((_, coin_id)) = coins.first().copied() else { return Err(WalletError::UncancellableOffer); }; - self.spend( - &mut ctx, - vec![row.coin_state.coin.coin_id()], - &[Action::fee(fee)], - ) - .await?; + self.spend(&mut ctx, vec![coin_id], &[Action::fee(fee)]) + .await?; Ok(ctx.take()) } diff --git a/crates/sage-wallet/src/wallet/offer/make_offer.rs b/crates/sage-wallet/src/wallet/offer/make_offer.rs index 7120c8955..999d4a8be 100644 --- a/crates/sage-wallet/src/wallet/offer/make_offer.rs +++ b/crates/sage-wallet/src/wallet/offer/make_offer.rs @@ -1,6 +1,6 @@ use chia::{ bls::Signature, - protocol::{Bytes32, Program, SpendBundle}, + protocol::{Bytes32, SpendBundle}, puzzles::{ offer::{NotarizedPayment, Payment}, Memos, @@ -14,6 +14,7 @@ use chia_wallet_sdk::driver::{ }; use indexmap::IndexMap; use itertools::Itertools; +use sage_database::NftOfferInfo; use crate::{Wallet, WalletError}; @@ -30,15 +31,7 @@ pub struct Offered { pub struct Requested { pub xch: u64, pub cats: IndexMap, - pub nfts: IndexMap, -} - -#[derive(Debug, Clone)] -pub struct RequestedNft { - pub metadata: Program, - pub metadata_updater_puzzle_hash: Bytes32, - pub royalty_puzzle_hash: Bytes32, - pub royalty_basis_points: u16, + pub nfts: IndexMap, } impl Wallet { diff --git a/crates/sage-wallet/src/wallet/offer/offer_assets.rs b/crates/sage-wallet/src/wallet/offer/offer_assets.rs new file mode 100644 index 000000000..6cdb5f659 --- /dev/null +++ b/crates/sage-wallet/src/wallet/offer/offer_assets.rs @@ -0,0 +1,106 @@ +use std::{collections::HashMap, time::Duration}; + +use chia::{ + clvm_traits::{FromClvm, ToClvm}, + protocol::{Bytes32, CoinState, Program}, + puzzles::nft::NftMetadata, +}; +use chia_wallet_sdk::driver::{HashedPtr, Nft, Puzzle}; +use clvmr::Allocator; +use sage_database::NftOfferInfo; +use tokio::time::{sleep, timeout}; + +use crate::{fetch_nft_did, insert_nft, Wallet, WalletError, WalletPeer}; + +impl Wallet { + pub async fn fetch_offer_nft_info( + &self, + peer: Option<&WalletPeer>, + launcher_id: Bytes32, + ) -> Result, WalletError> { + let Some(peer) = peer else { + if let Some(row) = self.db.offer_nft_info(launcher_id).await? { + return Ok(Some(row)); + }; + + return Ok(None); + }; + + let minter_did = + fetch_nft_did(peer, self.genesis_challenge, launcher_id, &HashMap::new()).await?; + + let mut allocator = Allocator::new(); + let mut current_id = launcher_id; + let mut parent = None; + + let nft = loop { + let Some(child) = + timeout(Duration::from_secs(5), peer.try_fetch_child(current_id)).await?? + else { + return Ok(None); + }; + + if child.spent_height.is_some() { + parent = Some(child); + current_id = child.coin.coin_id(); + sleep(Duration::from_secs(1)).await; + continue; + } + + let parent = parent.expect("parent not found"); + + let (parent_puzzle_reveal, parent_solution) = timeout( + Duration::from_secs(15), + peer.fetch_puzzle_solution( + parent.coin.coin_id(), + parent.spent_height.ok_or(WalletError::PeerMisbehaved)?, + ), + ) + .await??; + + let parent_puzzle = parent_puzzle_reveal.to_clvm(&mut allocator)?; + let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle); + let parent_solution = parent_solution.to_clvm(&mut allocator)?; + + if let Some(nft) = Nft::::parse_child( + &mut allocator, + parent.coin, + parent_puzzle, + parent_solution, + ) + .ok() + .flatten() + { + break nft; + } + + return Ok(None); + }; + + let metadata = Program::from_clvm(&allocator, nft.info.metadata.ptr())?; + let parsed_metadata = NftMetadata::from_clvm(&allocator, nft.info.metadata.ptr()).ok(); + + let mut tx = self.db.tx().await?; + + insert_nft( + &mut tx, + CoinState::new(nft.coin, None, None), + None, + nft.info.with_metadata(metadata.clone()), + parsed_metadata, + minter_did, + ) + .await?; + + tx.commit().await?; + + let offer_details = Some(NftOfferInfo { + metadata, + metadata_updater_puzzle_hash: nft.info.metadata_updater_puzzle_hash, + royalty_puzzle_hash: nft.info.royalty_puzzle_hash, + royalty_basis_points: nft.info.royalty_basis_points, + }); + + Ok(offer_details) + } +} diff --git a/crates/sage-wallet/src/wallet/offer/take_offer.rs b/crates/sage-wallet/src/wallet/offer/take_offer.rs index 17782703f..e056a33b2 100644 --- a/crates/sage-wallet/src/wallet/offer/take_offer.rs +++ b/crates/sage-wallet/src/wallet/offer/take_offer.rs @@ -5,8 +5,9 @@ use chia_wallet_sdk::driver::{ }; use indexmap::IndexMap; use itertools::Itertools; +use sage_database::NftOfferInfo; -use crate::{RequestedNft, Wallet, WalletError}; +use crate::{Wallet, WalletError}; impl Wallet { pub async fn take_offer( @@ -30,7 +31,7 @@ impl Wallet { requested_nfts.insert( launcher_id, - RequestedNft { + NftOfferInfo { metadata, metadata_updater_puzzle_hash: nft.metadata_updater_puzzle_hash, royalty_puzzle_hash: nft.royalty_puzzle_hash, diff --git a/crates/sage-wallet/src/wallet/p2_send.rs b/crates/sage-wallet/src/wallet/p2_send.rs deleted file mode 100644 index 9c40c7880..000000000 --- a/crates/sage-wallet/src/wallet/p2_send.rs +++ /dev/null @@ -1,108 +0,0 @@ -use chia::protocol::{Bytes, Bytes32, CoinSpend}; -use chia_wallet_sdk::driver::{Action, Id, SpendContext}; - -use crate::{wallet::memos::calculate_memos, WalletError}; - -use super::Wallet; - -impl Wallet { - /// Sends the given amount of XCH to the given puzzle hash, minus the fee. - pub async fn send_xch( - &self, - amounts: Vec<(Bytes32, u64)>, - fee: u64, - memos: Option>, - ) -> Result, WalletError> { - let mut ctx = SpendContext::new(); - let mut actions = vec![Action::fee(fee)]; - - for (puzzle_hash, amount) in amounts { - let memos = calculate_memos(&mut ctx, puzzle_hash, false, memos.clone())?; - actions.push(Action::send(Id::Xch, puzzle_hash, amount, memos)); - } - - self.spend(&mut ctx, vec![], &actions).await?; - - Ok(ctx.take()) - } -} - -#[cfg(test)] -mod tests { - use test_log::test; - - use crate::TestWallet; - - #[test(tokio::test)] - async fn test_send_xch() -> anyhow::Result<()> { - let mut test = TestWallet::new(1000).await?; - - let coin_spends = test - .wallet - .send_xch(vec![(test.puzzle_hash, 1000)], 0, None) - .await?; - - assert_eq!(coin_spends.len(), 1); - - test.transact(coin_spends).await?; - test.wait_for_coins().await; - - assert_eq!(test.wallet.db.balance().await?, 1000); - assert_eq!(test.wallet.db.spendable_coins().await?.len(), 1); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_send_xch_change() -> anyhow::Result<()> { - let mut test = TestWallet::new(1000).await?; - - let coin_spends = test - .wallet - .send_xch(vec![(test.puzzle_hash, 250)], 250, None) - .await?; - - assert_eq!(coin_spends.len(), 1); - - test.transact(coin_spends).await?; - test.wait_for_coins().await; - - assert_eq!(test.wallet.db.balance().await?, 750); - assert_eq!(test.wallet.db.spendable_coins().await?.len(), 2); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_send_xch_hardened() -> anyhow::Result<()> { - let mut test = TestWallet::new(1000).await?; - - let coin_spends = test - .wallet - .send_xch(vec![(test.hardened_puzzle_hash, 1000)], 0, None) - .await?; - - assert_eq!(coin_spends.len(), 1); - - test.transact(coin_spends).await?; - test.wait_for_coins().await; - - assert_eq!(test.wallet.db.balance().await?, 1000); - assert_eq!(test.wallet.db.spendable_coins().await?.len(), 1); - - let coin_spends = test - .wallet - .send_xch(vec![(test.puzzle_hash, 1000)], 0, None) - .await?; - - assert_eq!(coin_spends.len(), 1); - - test.transact(coin_spends).await?; - test.wait_for_coins().await; - - assert_eq!(test.wallet.db.balance().await?, 1000); - assert_eq!(test.wallet.db.spendable_coins().await?.len(), 1); - - Ok(()) - } -} diff --git a/crates/sage-wallet/src/wallet/signing.rs b/crates/sage-wallet/src/wallet/signing.rs index cddf0f3e6..76ba60038 100644 --- a/crates/sage-wallet/src/wallet/signing.rs +++ b/crates/sage-wallet/src/wallet/signing.rs @@ -37,36 +37,35 @@ impl Wallet { agg_sig_constants, )?; - let mut indices = HashMap::new(); + let mut derivations = HashMap::new(); for required in &required_signatures { let RequiredSignature::Bls(required) = required else { return Err(WalletError::SecpNotSupported); }; - let pk = required.public_key; - let Some(info) = self.db.synthetic_key_info(pk).await? else { + let Some(derivation) = self.db.derivation(required.public_key).await? else { if partial { continue; } return Err(WalletError::UnknownPublicKey); }; - indices.insert(pk, info); + derivations.insert(required.public_key, derivation); } let unhardened_intermediate_sk = master_to_wallet_unhardened_intermediate(&master_sk); let hardened_intermediate_sk = master_to_wallet_hardened_intermediate(&master_sk); - let secret_keys: HashMap = indices + let secret_keys: HashMap = derivations .iter() - .map(|(pk, info)| { - let sk = if info.hardened { - hardened_intermediate_sk.derive_hardened(info.index) + .map(|(public_key, derivation)| { + let secret_key = if derivation.is_hardened { + hardened_intermediate_sk.derive_hardened(derivation.derivation_index) } else { - unhardened_intermediate_sk.derive_unhardened(info.index) + unhardened_intermediate_sk.derive_unhardened(derivation.derivation_index) } .derive_synthetic(); - (*pk, sk) + (*public_key, secret_key) }) .collect(); diff --git a/crates/sage-wallet/src/wallet/xch.rs b/crates/sage-wallet/src/wallet/xch.rs new file mode 100644 index 000000000..840258dd2 --- /dev/null +++ b/crates/sage-wallet/src/wallet/xch.rs @@ -0,0 +1,245 @@ +use chia::{ + clvm_utils::ToTreeHash, + protocol::{Bytes, Bytes32, CoinSpend}, +}; +use chia_wallet_sdk::driver::{Action, ClawbackV2, Id, SpendContext}; + +use crate::{ + wallet::memos::{calculate_memos, Hint}, + WalletError, +}; + +use super::Wallet; + +impl Wallet { + /// Sends the given amount of XCH to the given puzzle hash, minus the fee. + pub async fn send_xch( + &self, + amounts: Vec<(Bytes32, u64)>, + fee: u64, + memos: Vec, + clawback: Option, + ) -> Result, WalletError> { + let sender_puzzle_hash = self.p2_puzzle_hash(false, true).await?; + + let mut ctx = SpendContext::new(); + let mut actions = vec![Action::fee(fee)]; + + for (puzzle_hash, amount) in amounts { + let clawback = clawback.map(|seconds| { + ClawbackV2::new(sender_puzzle_hash, puzzle_hash, seconds, amount, false) + }); + + let memos = calculate_memos( + &mut ctx, + if let Some(clawback) = clawback { + Hint::Clawback(clawback) + } else { + Hint::None + }, + memos.clone(), + )?; + + let p2_puzzle_hash = if let Some(clawback) = clawback { + clawback.tree_hash().into() + } else { + puzzle_hash + }; + + actions.push(Action::send(Id::Xch, p2_puzzle_hash, amount, memos)); + } + + self.spend(&mut ctx, vec![], &actions).await?; + + Ok(ctx.take()) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use test_log::test; + use tokio::time::sleep; + + use crate::TestWallet; + + #[test(tokio::test)] + async fn test_send_xch() -> anyhow::Result<()> { + let mut test = TestWallet::new(1000).await?; + + let coin_spends = test + .wallet + .send_xch(vec![(test.puzzle_hash, 1000)], 0, vec![], None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.xch_balance().await?, 1000); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 1); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_send_xch_change() -> anyhow::Result<()> { + let mut test = TestWallet::new(1000).await?; + + let coin_spends = test + .wallet + .send_xch(vec![(test.puzzle_hash, 250)], 250, vec![], None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.xch_balance().await?, 750); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 2); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_send_xch_hardened() -> anyhow::Result<()> { + let mut test = TestWallet::new(1000).await?; + + let coin_spends = test + .wallet + .send_xch(vec![(test.hardened_puzzle_hash, 1000)], 0, vec![], None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.xch_balance().await?, 1000); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 1); + + let coin_spends = test + .wallet + .send_xch(vec![(test.puzzle_hash, 1000)], 0, vec![], None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.xch_balance().await?, 1000); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 1); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_send_xch_with_clawback_self() -> anyhow::Result<()> { + let mut test = TestWallet::new(1000).await?; + + let timestamp = test.new_block_with_current_time().await?; + + let coin_spends = test + .wallet + .send_xch( + vec![(test.puzzle_hash, 1000)], + 0, + vec![], + Some(timestamp + 5), + ) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.selectable_xch_balance().await?, 0); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 0); + + sleep(Duration::from_secs(6)).await; + test.new_block_with_current_time().await?; + + assert_eq!(test.wallet.db.selectable_xch_balance().await?, 1000); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 1); + + let coin_spends = test + .wallet + .send_xch(vec![(test.puzzle_hash, 1000)], 0, vec![], None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.xch_balance().await?, 1000); + assert_eq!(test.wallet.db.selectable_xch_coins().await?.len(), 1); + + Ok(()) + } + + #[test(tokio::test)] + async fn test_send_xch_with_clawback_external() -> anyhow::Result<()> { + let mut alice = TestWallet::new(1000).await?; + let mut bob = alice.next(0).await?; + + let timestamp = alice.new_block_with_current_time().await?; + + let coin_spends = alice + .wallet + .send_xch( + vec![(bob.puzzle_hash, 1000)], + 0, + vec![], + Some(timestamp + 5), + ) + .await?; + + assert_eq!(coin_spends.len(), 1); + + alice.transact(coin_spends).await?; + + alice.wait_for_coins().await; + + assert_eq!(alice.wallet.db.selectable_xch_balance().await?, 0); + assert_eq!(alice.wallet.db.selectable_xch_coins().await?.len(), 0); + + bob.wait_for_puzzles().await; + + assert_eq!(bob.wallet.db.selectable_xch_balance().await?, 0); + assert_eq!(bob.wallet.db.selectable_xch_coins().await?.len(), 0); + + sleep(Duration::from_secs(6)).await; + bob.new_block_with_current_time().await?; + + assert_eq!(alice.wallet.db.selectable_xch_balance().await?, 0); + assert_eq!(alice.wallet.db.selectable_xch_coins().await?.len(), 0); + + assert_eq!(bob.wallet.db.selectable_xch_balance().await?, 1000); + assert_eq!(bob.wallet.db.selectable_xch_coins().await?.len(), 1); + + let coin_spends = bob + .wallet + .send_xch(vec![(alice.puzzle_hash, 1000)], 0, vec![], None) + .await?; + + assert_eq!(coin_spends.len(), 1); + + bob.transact(coin_spends).await?; + bob.wait_for_coins().await; + alice.wait_for_coins().await; + + assert_eq!(alice.wallet.db.selectable_xch_balance().await?, 1000); + assert_eq!(alice.wallet.db.selectable_xch_coins().await?.len(), 1); + + assert_eq!(bob.wallet.db.selectable_xch_balance().await?, 0); + assert_eq!(bob.wallet.db.selectable_xch_coins().await?.len(), 0); + + Ok(()) + } +} diff --git a/crates/sage-wallet/src/wallet_peer.rs b/crates/sage-wallet/src/wallet_peer.rs index 22a2c4d61..c9396aff6 100644 --- a/crates/sage-wallet/src/wallet_peer.rs +++ b/crates/sage-wallet/src/wallet_peer.rs @@ -193,13 +193,21 @@ impl WalletPeer { Ok(()) } - pub async fn block_timestamp(&self, height: u32) -> Result, WalletError> { - Ok(self + pub async fn block_timestamp(&self, height: u32) -> Result<(Bytes32, u64), WalletError> { + let header_block = self .peer .request_infallible::(RequestBlockHeader::new(height)) .await? - .header_block + .header_block; + + let timestamp = header_block .foliage_transaction_block - .map(|block| block.timestamp)) + .as_ref() + .map(|block| block.timestamp); + + Ok(( + header_block.header_hash(), + timestamp.ok_or(WalletError::PeerMisbehaved)?, + )) } } diff --git a/crates/sage/Cargo.toml b/crates/sage/Cargo.toml index 17f5853d4..59bcb05ec 100644 --- a/crates/sage/Cargo.toml +++ b/crates/sage/Cargo.toml @@ -39,10 +39,8 @@ itertools = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } bech32 = { workspace = true } -hex-literal = { workspace = true } base64 = { workspace = true } clvmr = { workspace = true } serde = { workspace = true, features = ["derive"] } bincode = { workspace = true } serde_json = { workspace = true } -chrono = { workspace = true } diff --git a/crates/sage/src/endpoints/actions.rs b/crates/sage/src/endpoints/actions.rs index 0f0792d6e..e431ae31e 100644 --- a/crates/sage/src/endpoints/actions.rs +++ b/crates/sage/src/endpoints/actions.rs @@ -3,44 +3,67 @@ use chia::{ clvm_traits::{FromClvm, ToClvm}, puzzles::{nft::NftMetadata, standard::StandardArgs, DeriveSynthetic}, }; +use chia_wallet_sdk::types::TESTNET11_CONSTANTS; use clvmr::Allocator; use sage_api::{ IncreaseDerivationIndex, IncreaseDerivationIndexResponse, RedownloadNft, RedownloadNftResponse, - RemoveCat, RemoveCatResponse, UpdateCat, UpdateCatResponse, UpdateDid, UpdateDidResponse, + ResyncCat, ResyncCatResponse, UpdateCat, UpdateCatResponse, UpdateDid, UpdateDidResponse, UpdateNft, UpdateNftCollection, UpdateNftCollectionResponse, UpdateNftResponse, }; -use sage_database::{CatRow, DidRow}; +use sage_assets::DexieCat; +use sage_database::{Asset, AssetKind, Derivation}; use sage_wallet::SyncCommand; use crate::{parse_asset_id, parse_collection_id, parse_did_id, parse_nft_id, Error, Result, Sage}; impl Sage { - pub async fn remove_cat(&self, req: RemoveCat) -> Result { + pub async fn resync_cat(&self, req: ResyncCat) -> Result { let wallet = self.wallet()?; let asset_id = parse_asset_id(req.asset_id)?; - wallet.db.refetch_cat(asset_id).await?; + let testnet = self.network().genesis_challenge == TESTNET11_CONSTANTS.genesis_challenge; - Ok(RemoveCatResponse {}) + let cat = DexieCat::fetch(asset_id, testnet).await?; + + wallet + .db + .update_asset(Asset { + hash: asset_id, + name: cat.name, + ticker: cat.ticker, + precision: 3, + icon_url: cat.icon_url, + description: cat.description, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Token, + }) + .await?; + + Ok(ResyncCatResponse {}) } pub async fn update_cat(&self, req: UpdateCat) -> Result { let wallet = self.wallet()?; - let asset_id = parse_asset_id(req.record.asset_id)?; + let asset_id = req + .record + .asset_id + .map(parse_asset_id) + .transpose()? + .unwrap_or_default(); - wallet - .db - .update_cat(CatRow { - asset_id, - name: req.record.name, - description: req.record.description, - ticker: req.record.ticker, - icon: req.record.icon_url, - visible: req.record.visible, - fetched: true, - }) - .await?; + let Some(mut asset) = wallet.db.asset(asset_id).await? else { + return Err(Error::MissingCat(asset_id)); + }; + + asset.name = req.record.name; + asset.ticker = req.record.ticker; + asset.icon_url = req.record.icon_url; + asset.description = req.record.description; + asset.is_visible = req.record.visible; + + wallet.db.update_asset(asset).await?; Ok(UpdateCatResponse {}) } @@ -50,21 +73,14 @@ impl Sage { let did_id = parse_did_id(req.did_id)?; - let Some(row) = wallet.db.did_row(did_id).await? else { + let Some(mut asset) = wallet.db.asset(did_id).await? else { return Err(Error::MissingDid(did_id)); }; - wallet - .db - .insert_did(DidRow { - launcher_id: row.launcher_id, - coin_id: row.coin_id, - name: req.name, - is_owned: row.is_owned, - visible: req.visible, - created_height: row.created_height, - }) - .await?; + asset.name = req.name; + asset.is_visible = req.visible; + + wallet.db.update_asset(asset).await?; Ok(UpdateDidResponse {}) } @@ -73,7 +89,14 @@ impl Sage { let wallet = self.wallet()?; let nft_id = parse_nft_id(req.nft_id)?; - wallet.db.set_nft_visible(nft_id, req.visible).await?; + + let Some(mut asset) = wallet.db.asset(nft_id).await? else { + return Err(Error::MissingNft(nft_id)); + }; + + asset.is_visible = req.visible; + + wallet.db.update_asset(asset).await?; Ok(UpdateNftResponse {}) } @@ -120,20 +143,19 @@ impl Sage { ] .concat() { - tx.set_nft_uri_unchecked(uri).await?; + tx.set_uri_unchecked(uri).await?; } if let Some(hash) = metadata.data_hash { - tx.delete_nft_data(hash).await?; - tx.delete_nft_thumbnail(hash).await?; + tx.delete_file_data(hash).await?; } if let Some(hash) = metadata.metadata_hash { - tx.delete_nft_data(hash).await?; + tx.delete_file_data(hash).await?; } if let Some(hash) = metadata.license_hash { - tx.delete_nft_data(hash).await?; + tx.delete_file_data(hash).await?; } tx.commit().await?; @@ -173,8 +195,15 @@ impl Sage { let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); - tx.insert_derivation(p2_puzzle_hash, index, true, synthetic_key) - .await?; + tx.insert_custody_p2_puzzle( + p2_puzzle_hash, + synthetic_key, + Derivation { + derivation_index: index, + is_hardened: true, + }, + ) + .await?; derivations.push(p2_puzzle_hash); } diff --git a/crates/sage/src/endpoints/data.rs b/crates/sage/src/endpoints/data.rs index 465ea3dbe..20198fc87 100644 --- a/crates/sage/src/endpoints/data.rs +++ b/crates/sage/src/endpoints/data.rs @@ -1,37 +1,36 @@ -use std::collections::HashMap; - use crate::{ - parse_asset_id, parse_collection_id, parse_did_id, parse_nft_id, - utils::{to_bytes32_opt, to_u64}, - Error, Result, Sage, BURN_PUZZLE_HASH, + address_kind, encode_asset, parse_asset_id, parse_collection_id, parse_did_id, parse_nft_id, + Error, Result, Sage, }; use base64::{prelude::BASE64_STANDARD, Engine}; use chia::{ clvm_traits::{FromClvm, ToClvm}, - protocol::{Bytes32, Program}, + protocol::Bytes32, puzzles::nft::NftMetadata, }; -use chia_puzzles::{SETTLEMENT_PAYMENT_HASH, SINGLETON_LAUNCHER_HASH}; -use chia_wallet_sdk::{driver::Nft, utils::Address}; +use chia_wallet_sdk::{driver::BURN_PUZZLE_HASH, utils::Address}; use clvmr::Allocator; use sage_api::{ - AddressKind, Amount, AssetKind, CatRecord, CheckAddress, CheckAddressResponse, CoinRecord, - CoinSortMode as ApiCoinSortMode, DerivationRecord, DidRecord, GetAreCoinsSpendable, - GetAreCoinsSpendableResponse, GetCat, GetCatCoins, GetCatCoinsResponse, GetCatResponse, - GetCats, GetCatsResponse, GetCoinsByIds, GetCoinsByIdsResponse, GetDerivations, - GetDerivationsResponse, GetDids, GetDidsResponse, GetMinterDidIds, GetMinterDidIdsResponse, - GetNft, GetNftCollection, GetNftCollectionResponse, GetNftCollections, - GetNftCollectionsResponse, GetNftData, GetNftDataResponse, GetNftIcon, GetNftIconResponse, - GetNftResponse, GetNftThumbnail, GetNftThumbnailResponse, GetNfts, GetNftsResponse, - GetPendingTransactions, GetPendingTransactionsResponse, GetSpendableCoinCount, - GetSpendableCoinCountResponse, GetSyncStatus, GetSyncStatusResponse, GetTransactions, - GetTransactionsResponse, GetVersion, GetVersionResponse, GetXchCoins, GetXchCoinsResponse, - NftCollectionRecord, NftData, NftRecord, NftSortMode as ApiNftSortMode, - PendingTransactionRecord, TransactionCoin, TransactionRecord, + Amount, CheckAddress, CheckAddressResponse, CoinFilterMode as ApiCoinFilterMode, CoinRecord, + CoinSortMode as ApiCoinSortMode, DerivationRecord, DidRecord, GetAllCats, GetAllCatsResponse, + GetAreCoinsSpendable, GetAreCoinsSpendableResponse, GetCats, GetCatsResponse, GetCoins, + GetCoinsByIds, GetCoinsByIdsResponse, GetCoinsResponse, GetDatabaseStats, + GetDatabaseStatsResponse, GetDerivations, GetDerivationsResponse, GetDids, GetDidsResponse, + GetMinterDidIds, GetMinterDidIdsResponse, GetNft, GetNftCollection, GetNftCollectionResponse, + GetNftCollections, GetNftCollectionsResponse, GetNftData, GetNftDataResponse, GetNftIcon, + GetNftIconResponse, GetNftResponse, GetNftThumbnail, GetNftThumbnailResponse, GetNfts, + GetNftsResponse, GetPendingTransactions, GetPendingTransactionsResponse, GetSpendableCoinCount, + GetSpendableCoinCountResponse, GetSyncStatus, GetSyncStatusResponse, GetToken, + GetTokenResponse, GetTransaction, GetTransactionResponse, GetTransactions, + GetTransactionsResponse, GetVersion, GetVersionResponse, NftCollectionRecord, NftData, + NftRecord, NftSortMode as ApiNftSortMode, PendingTransactionRecord, PerformDatabaseMaintenance, + PerformDatabaseMaintenanceResponse, TokenRecord, TransactionCoinRecord, TransactionRecord, +}; +use sage_database::{ + AssetFilter, CoinFilterMode, CoinSortMode, NftGroupSearch, NftRow, NftSortMode, Transaction, + TransactionCoin, }; -use sage_database::{CoinKind, CoinSortMode, NftGroup, NftRow, NftSearchParams, NftSortMode}; use sage_wallet::WalletError; -use sqlx::{sqlite::SqliteRow, Row}; impl Sage { pub fn get_version(&self, _req: GetVersion) -> Result { @@ -40,10 +39,52 @@ impl Sage { }) } + pub async fn perform_database_maintenance( + &self, + req: PerformDatabaseMaintenance, + ) -> Result { + let wallet = self.wallet()?; + let stats = wallet + .db + .perform_sqlite_maintenance(req.force_vacuum) + .await?; + + let response = PerformDatabaseMaintenanceResponse { + vacuum_duration_ms: stats.vacuum_duration_ms, + analyze_duration_ms: stats.analyze_duration_ms, + wal_checkpoint_duration_ms: stats.wal_checkpoint_duration_ms, + total_duration_ms: stats.total_duration_ms, + pages_vacuumed: stats.pages_vacuumed, + wal_pages_checkpointed: stats.wal_pages_checkpointed, + }; + + Ok(response) + } + + pub async fn get_database_stats( + &self, + _req: GetDatabaseStats, + ) -> Result { + let wallet = self.wallet()?; + let stats = wallet.db.get_database_stats().await?; + + let response = GetDatabaseStatsResponse { + total_pages: stats.total_pages, + free_pages: stats.free_pages, + free_percentage: stats.free_percentage, + page_size: stats.page_size, + database_size_bytes: stats.database_size_bytes, + free_space_bytes: stats.free_space_bytes, + wal_pages: stats.wal_pages, + }; + + Ok(response) + } + pub async fn get_sync_status(&self, _req: GetSyncStatus) -> Result { let wallet = self.wallet()?; - let balance = wallet.db.balance().await?; + let balance = wallet.db.xch_balance().await?; let total_coins = wallet.db.total_coin_count().await?; let synced_coins = wallet.db.synced_coin_count().await?; @@ -69,12 +110,11 @@ impl Sage { total_coins, synced_coins, receive_address: receive_address.unwrap_or_default(), - burn_address: Address::new(BURN_PUZZLE_HASH.into(), self.network().prefix()) - .encode()?, - unhardened_derivation_index: wallet.db.derivation_index(false).await?, - hardened_derivation_index: wallet.db.derivation_index(true).await?, - checked_uris: wallet.db.checked_uris().await?, - total_uris: wallet.db.total_uris().await?, + burn_address: Address::new(BURN_PUZZLE_HASH, self.network().prefix()).encode()?, + unhardened_derivation_index: wallet.db.max_derivation_index(false).await?, + hardened_derivation_index: wallet.db.max_derivation_index(true).await?, + checked_files: wallet.db.checked_files().await?.try_into().unwrap_or(0), + total_files: wallet.db.total_files().await?.try_into().unwrap_or(0), database_size, }) } @@ -86,7 +126,10 @@ impl Sage { return Ok(CheckAddressResponse { valid: false }); }; - let is_valid = wallet.db.is_p2_puzzle_hash(address.puzzle_hash).await?; + let is_valid = wallet + .db + .is_custody_p2_puzzle_hash(address.puzzle_hash) + .await?; Ok(CheckAddressResponse { valid: is_valid }) } @@ -118,7 +161,7 @@ impl Sage { req: GetAreCoinsSpendable, ) -> Result { let wallet = self.wallet()?; - let spendable = wallet.db.get_are_coins_spendable(&req.coin_ids).await?; + let spendable = wallet.db.are_coins_spendable(&req.coin_ids).await?; Ok(GetAreCoinsSpendableResponse { spendable }) } @@ -128,12 +171,12 @@ impl Sage { req: GetSpendableCoinCount, ) -> Result { let wallet = self.wallet()?; - let count = if req.asset_id == "xch" { - wallet.db.spendable_p2_coin_count().await? - } else { - let asset_id = parse_asset_id(req.asset_id)?; - - wallet.db.spendable_cat_coin_count(asset_id).await? + let count = match req.asset_id { + None => wallet.db.selectable_xch_coin_count().await?, + Some(asset_id_str) => { + let asset_id = parse_asset_id(asset_id_str)?; + wallet.db.selectable_cat_coin_count(asset_id).await? + } }; Ok(GetSpendableCoinCountResponse { count }) @@ -141,129 +184,120 @@ impl Sage { pub async fn get_coins_by_ids(&self, req: GetCoinsByIds) -> Result { let wallet = self.wallet()?; - let rows = wallet.db.coin_states_by_ids(&req.coin_ids).await?; + let rows = wallet.db.coins_by_ids(&req.coin_ids).await?; let mut coins = Vec::new(); for row in rows { - let cs = row.base.coin_state; - coins.push(CoinRecord { - coin_id: hex::encode(cs.coin.coin_id()), - address: Address::new(cs.coin.puzzle_hash, self.network().prefix()).encode()?, - amount: Amount::u64(cs.coin.amount), - created_height: cs.created_height, - spent_height: cs.spent_height, - create_transaction_id: row.base.transaction_id.map(hex::encode), - spend_transaction_id: row.spend_transaction_id.map(hex::encode), - offer_id: row.offer_id.map(hex::encode), - created_timestamp: row.base.created_timestamp, - spent_timestamp: row.base.spent_timestamp, + coin_id: hex::encode(row.coin.coin_id()), + address: Address::new(row.coin.puzzle_hash, self.network().prefix()).encode()?, + amount: Amount::u64(row.coin.amount), + transaction_id: row.mempool_item_hash.map(hex::encode), + offer_id: row.offer_hash.map(hex::encode), + clawback_timestamp: row.clawback_timestamp, + created_height: row.created_height, + spent_height: row.spent_height, + created_timestamp: row.created_timestamp, + spent_timestamp: row.spent_timestamp, }); } Ok(GetCoinsByIdsResponse { coins }) } - pub async fn get_xch_coins(&self, req: GetXchCoins) -> Result { + pub async fn get_coins(&self, req: GetCoins) -> Result { let wallet = self.wallet()?; let sort_mode = match req.sort_mode { ApiCoinSortMode::CoinId => CoinSortMode::CoinId, ApiCoinSortMode::Amount => CoinSortMode::Amount, ApiCoinSortMode::CreatedHeight => CoinSortMode::CreatedHeight, ApiCoinSortMode::SpentHeight => CoinSortMode::SpentHeight, + ApiCoinSortMode::ClawbackTimestamp => CoinSortMode::ClawbackTimestamp, + }; + let filter_mode = match req.filter_mode { + ApiCoinFilterMode::All => CoinFilterMode::All, + ApiCoinFilterMode::Selectable => CoinFilterMode::Selectable, + ApiCoinFilterMode::Owned => CoinFilterMode::Owned, + ApiCoinFilterMode::Spent => CoinFilterMode::Spent, + ApiCoinFilterMode::Clawback => CoinFilterMode::Clawback, }; let mut coins = Vec::new(); let (rows, total) = wallet .db - .p2_coin_states( + .coin_records( + AssetFilter::Id( + req.asset_id + .map(parse_asset_id) + .transpose()? + .unwrap_or_default(), + ), req.limit, req.offset, sort_mode, req.ascending, - req.include_spent_coins, + filter_mode, ) .await?; for row in rows { - let cs = row.base.coin_state; - coins.push(CoinRecord { - coin_id: hex::encode(cs.coin.coin_id()), - address: Address::new(cs.coin.puzzle_hash, self.network().prefix()).encode()?, - amount: Amount::u64(cs.coin.amount), - created_height: cs.created_height, - spent_height: cs.spent_height, - create_transaction_id: row.base.transaction_id.map(hex::encode), - spend_transaction_id: row.spend_transaction_id.map(hex::encode), - offer_id: row.offer_id.map(hex::encode), - created_timestamp: row.base.created_timestamp, - spent_timestamp: row.base.spent_timestamp, + coin_id: hex::encode(row.coin.coin_id()), + address: Address::new(row.coin.puzzle_hash, self.network().prefix()).encode()?, + amount: Amount::u64(row.coin.amount), + transaction_id: row.mempool_item_hash.map(hex::encode), + offer_id: row.offer_hash.map(hex::encode), + clawback_timestamp: row.clawback_timestamp, + created_height: row.created_height, + spent_height: row.spent_height, + created_timestamp: row.created_timestamp, + spent_timestamp: row.spent_timestamp, }); } - Ok(GetXchCoinsResponse { coins, total }) + Ok(GetCoinsResponse { coins, total }) } - pub async fn get_cat_coins(&self, req: GetCatCoins) -> Result { + pub async fn get_all_cats(&self, _req: GetAllCats) -> Result { let wallet = self.wallet()?; - let asset_id = parse_asset_id(req.asset_id)?; - - let mut coins = Vec::new(); - - let sort_mode: CoinSortMode = match req.sort_mode { - ApiCoinSortMode::CoinId => CoinSortMode::CoinId, - ApiCoinSortMode::Amount => CoinSortMode::Amount, - ApiCoinSortMode::CreatedHeight => CoinSortMode::CreatedHeight, - ApiCoinSortMode::SpentHeight => CoinSortMode::SpentHeight, - }; - let (rows, total) = wallet - .db - .cat_coin_states( - asset_id, - req.limit, - req.offset, - sort_mode, - req.ascending, - req.include_spent_coins, - ) - .await?; + let cats = wallet.db.all_cats().await?; + let mut records = Vec::with_capacity(cats.len()); - for row in rows { - let cs = row.base.coin_state; + for cat in cats { + let balance = wallet.db.cat_balance(cat.hash).await?; - coins.push(CoinRecord { - coin_id: hex::encode(cs.coin.coin_id()), - address: Address::new(cs.coin.puzzle_hash, self.network().prefix()).encode()?, - amount: Amount::u64(cs.coin.amount), - created_height: cs.created_height, - spent_height: cs.spent_height, - create_transaction_id: row.base.transaction_id.map(hex::encode), - spend_transaction_id: row.spend_transaction_id.map(hex::encode), - offer_id: row.offer_id.map(hex::encode), - created_timestamp: row.base.created_timestamp, - spent_timestamp: row.base.spent_timestamp, + records.push(TokenRecord { + asset_id: (cat.hash != Bytes32::default()).then(|| hex::encode(cat.hash)), + name: cat.name, + ticker: cat.ticker, + precision: cat.precision, + description: cat.description, + icon_url: cat.icon_url, + visible: cat.is_visible, + balance: Amount::u128(balance), }); } - Ok(GetCatCoinsResponse { coins, total }) + Ok(GetAllCatsResponse { cats: records }) } pub async fn get_cats(&self, _req: GetCats) -> Result { let wallet = self.wallet()?; - let cats = wallet.db.cats_by_name().await?; + + let cats = wallet.db.owned_cats().await?; let mut records = Vec::with_capacity(cats.len()); for cat in cats { - let balance = wallet.db.cat_balance(cat.asset_id).await?; + let balance = wallet.db.cat_balance(cat.hash).await?; - records.push(CatRecord { - asset_id: hex::encode(cat.asset_id), + records.push(TokenRecord { + asset_id: (cat.hash != Bytes32::default()).then(|| hex::encode(cat.hash)), name: cat.name, ticker: cat.ticker, + precision: cat.precision, description: cat.description, - icon_url: cat.icon, - visible: cat.visible, + icon_url: cat.icon_url, + visible: cat.is_visible, balance: Amount::u128(balance), }); } @@ -271,28 +305,34 @@ impl Sage { Ok(GetCatsResponse { cats: records }) } - pub async fn get_cat(&self, req: GetCat) -> Result { + pub async fn get_token(&self, req: GetToken) -> Result { let wallet = self.wallet()?; - let asset_id = parse_asset_id(req.asset_id)?; - let cat = wallet.db.cat(asset_id).await?; + let asset_id = req + .asset_id + .map(parse_asset_id) + .transpose()? + .unwrap_or_default(); + let token = wallet.db.asset(asset_id).await?; + // TODO: Empty hash is xch even though it says cat. Is this confusing? let balance = wallet.db.cat_balance(asset_id).await?; - let cat = cat + let token = token .map(|cat| { - Result::Ok(CatRecord { - asset_id: hex::encode(cat.asset_id), + Result::Ok(TokenRecord { + asset_id: (cat.hash != Bytes32::default()).then(|| hex::encode(cat.hash)), name: cat.name, ticker: cat.ticker, + precision: cat.precision, description: cat.description, - icon_url: cat.icon, - visible: cat.visible, + icon_url: cat.icon_url, + visible: cat.is_visible, balance: Amount::u128(balance), }) }) .transpose()?; - Ok(GetCatResponse { cat }) + Ok(GetTokenResponse { token }) } pub async fn get_dids(&self, _req: GetDids) -> Result { @@ -300,21 +340,17 @@ impl Sage { let mut dids = Vec::new(); - for row in wallet.db.dids_by_name().await? { - let Some(did) = wallet.db.did_coin_info(row.coin_id).await? else { - continue; - }; - + for row in wallet.db.owned_dids().await? { dids.push(DidRecord { - launcher_id: Address::new(row.launcher_id, "did:chia:".to_string()).encode()?, - name: row.name, - visible: row.visible, - coin_id: hex::encode(did.coin_id), - address: Address::new(did.p2_puzzle_hash, self.network().prefix()).encode()?, - amount: Amount::u64(did.amount), - recovery_hash: did.recovery_list_hash.map(hex::encode), - created_height: did.created_height, - create_transaction_id: did.transaction_id.map(hex::encode), + launcher_id: Address::new(row.asset.hash, "did:chia:".to_string()).encode()?, + name: row.asset.name, + visible: row.asset.is_visible, + coin_id: hex::encode(row.coin_row.coin.coin_id()), + address: Address::new(row.coin_row.p2_puzzle_hash, self.network().prefix()) + .encode()?, + amount: Amount::u64(row.coin_row.coin.amount), + recovery_hash: row.did_info.recovery_list_hash.map(hex::encode), + created_height: row.coin_row.created_height, }); } @@ -334,8 +370,7 @@ impl Sage { let did_ids = dids .into_iter() - .filter_map(|did| did.map(|d| Address::new(d, "did:chia:".to_string()).encode().ok())) - .flatten() + .filter_map(|did| Address::new(did, "did:chia:".to_string()).encode().ok()) .collect(); Ok(GetMinterDidIdsResponse { did_ids, total }) @@ -349,15 +384,14 @@ impl Sage { let transactions = wallet .db - .transactions() + .mempool_items() .await? .into_iter() .map(|tx| { Result::Ok(PendingTransactionRecord { - transaction_id: hex::encode(tx.transaction_id), + transaction_id: hex::encode(tx.hash), fee: Amount::u64(tx.fee), - // TODO: Date format? - submitted_at: tx.submitted_at.map(|ts| ts.to_string()), + submitted_at: Some(tx.submitted_timestamp.to_string()), }) }) .collect::>>()?; @@ -365,42 +399,31 @@ impl Sage { Ok(GetPendingTransactionsResponse { transactions }) } - pub async fn get_transactions(&self, req: GetTransactions) -> Result { + pub async fn get_transaction(&self, req: GetTransaction) -> Result { let wallet = self.wallet()?; - let mut transactions = Vec::new(); - - let (transaction_coins, total) = wallet - .db - .get_transaction_coins(req.offset, req.limit, req.ascending, req.find_value) - .await?; + let transaction = wallet.db.transaction(req.height).await?; - // Group transaction coins by height - let mut heights = HashMap::new(); + let transaction = transaction + .map(|row| self.transaction_record(row)) + .transpose()?; - for row in transaction_coins { - let height: u32 = row.get::("height").try_into()?; - heights.entry(height).or_insert_with(Vec::new).push(row); - } + Ok(GetTransactionResponse { transaction }) + } - let mut grouped_coins = heights.into_iter().collect::>(); + pub async fn get_transactions(&self, req: GetTransactions) -> Result { + let wallet = self.wallet()?; - // Sort grouped_coins by height - if req.ascending { - grouped_coins.sort_by_key(|(height, _)| *height); - } else { - grouped_coins.sort_by_key(|(height, _)| std::cmp::Reverse(*height)); - } + let mut transactions = Vec::new(); - for (height, coins) in grouped_coins { - // Process each group by height - let timestamp: Option = coins - .first() - .map(|coin| coin.try_get("unixtime")) - .transpose()?; - let transaction_record = self.transaction_record(height, timestamp, coins)?; + let (transaction_records, total) = wallet + .db + .transactions(req.find_value, req.ascending, req.limit, req.offset) + .await?; - transactions.push(transaction_record); + for row in transaction_records { + let record = self.transaction_record(row)?; + transactions.push(record); } Ok(GetTransactionsResponse { @@ -414,27 +437,22 @@ impl Sage { req: GetNftCollections, ) -> Result { let wallet = self.wallet()?; - let include_hidden = req.include_hidden; - let (collections, total) = if include_hidden { - wallet.db.collections_named(req.limit, req.offset).await? - } else { - wallet - .db - .collections_visible_named(req.limit, req.offset) - .await? - }; + let (collections, total) = wallet + .db + .collections(req.limit, req.offset, req.include_hidden) + .await?; let records = collections .into_iter() .map(|row| { Ok(NftCollectionRecord { - collection_id: Address::new(row.collection_id, "col".to_string()).encode()?, - did_id: Address::new(row.did_id, "did:chia:".to_string()).encode()?, - metadata_collection_id: row.metadata_collection_id, + collection_id: Address::new(row.hash, "col".to_string()).encode()?, + did_id: Address::new(row.minter_hash, "did:chia:".to_string()).encode()?, + metadata_collection_id: row.uuid, name: row.name, - icon: row.icon, - visible: row.visible, + icon: row.icon_url, + visible: row.is_visible, }) }) .collect::>>()?; @@ -464,13 +482,12 @@ impl Sage { let record = if let Some(collection) = collection { NftCollectionRecord { - collection_id: Address::new(collection.collection_id, "col".to_string()) - .encode()?, - did_id: Address::new(collection.did_id, "did:chia:".to_string()).encode()?, - metadata_collection_id: collection.metadata_collection_id, - visible: collection.visible, + collection_id: Address::new(collection.hash, "col".to_string()).encode()?, + did_id: Address::new(collection.minter_hash, "did:chia:".to_string()).encode()?, + metadata_collection_id: collection.uuid, + visible: collection.is_visible, name: collection.name, - icon: collection.icon, + icon: collection.icon_url, } } else { NftCollectionRecord { @@ -496,55 +513,54 @@ impl Sage { let group = match (&req.collection_id, &req.minter_did_id, &req.owner_did_id) { (Some(collection_id), None, None) => { if collection_id == "none" { - Some(NftGroup::NoCollection) + Some(NftGroupSearch::NoCollection) } else { - Some(NftGroup::Collection(parse_collection_id( + Some(NftGroupSearch::Collection(parse_collection_id( collection_id.clone(), )?)) } } (None, Some(minter_did_id), None) => { if minter_did_id == "none" { - Some(NftGroup::NoMinterDid) + Some(NftGroupSearch::NoMinterDid) } else { - Some(NftGroup::MinterDid(parse_did_id(minter_did_id.clone())?)) + Some(NftGroupSearch::MinterDid(parse_did_id( + minter_did_id.clone(), + )?)) } } (None, None, Some(owner_did_id)) => { if owner_did_id == "none" { - Some(NftGroup::NoOwnerDid) + Some(NftGroupSearch::NoOwnerDid) } else { - Some(NftGroup::OwnerDid(parse_did_id(owner_did_id.clone())?)) + Some(NftGroupSearch::OwnerDid(parse_did_id( + owner_did_id.clone(), + )?)) } } (None, None, None) => None, _ => return Err(Error::InvalidGroup), }; - let params = NftSearchParams { - sort_mode: match req.sort_mode { - ApiNftSortMode::Recent => NftSortMode::Recent, - ApiNftSortMode::Name => NftSortMode::Name, - }, - include_hidden: req.include_hidden, - group, - name: req.name, + let sort_mode = match req.sort_mode { + ApiNftSortMode::Recent => NftSortMode::Recent, + ApiNftSortMode::Name => NftSortMode::Name, }; - let (nfts, total) = wallet.db.search_nfts(params, req.limit, req.offset).await?; - - for nft_row in nfts { - let Some(nft) = wallet.db.nft(nft_row.launcher_id).await? else { - continue; - }; - - let collection_name = if let Some(collection_id) = nft_row.collection_id { - wallet.db.collection_name(collection_id).await? - } else { - None - }; + let (nfts, total) = wallet + .db + .owned_nfts( + req.name, + group, + sort_mode, + req.include_hidden, + req.limit, + req.offset, + ) + .await?; - records.push(self.nft_record(nft_row, nft, collection_name)?); + for row in nfts { + records.push(self.nft_record(row)?); } Ok(GetNftsResponse { @@ -558,22 +574,12 @@ impl Sage { let nft_id = parse_nft_id(req.nft_id)?; - let Some(nft_row) = wallet.db.nft_row(nft_id).await? else { + let Some(row) = wallet.db.owned_nft(nft_id).await? else { return Ok(GetNftResponse { nft: None }); }; - let Some(nft) = wallet.db.nft(nft_id).await? else { - return Ok(GetNftResponse { nft: None }); - }; - - let collection_name = if let Some(collection_id) = nft_row.collection_id { - wallet.db.collection_name(collection_id).await? - } else { - None - }; - Ok(GetNftResponse { - nft: Some(self.nft_record(nft_row, nft, collection_name)?), + nft: Some(self.nft_record(row)?), }) } @@ -594,30 +600,30 @@ impl Sage { let metadata_hash = metadata.as_ref().and_then(|m| m.metadata_hash); let data = if let Some(hash) = data_hash { - wallet.db.fetch_nft_data(hash).await? + wallet.db.full_file_data(hash).await? } else { None }; let offchain_metadata = if let Some(hash) = metadata_hash { - wallet.db.fetch_nft_data(hash).await? + wallet.db.full_file_data(hash).await? } else { None }; - let hash_matches = data.as_ref().is_some_and(|data| data.hash_matches); + let hash_matches = data.as_ref().is_some_and(|data| data.is_hash_match); let metadata_hash_matches = offchain_metadata .as_ref() - .is_some_and(|offchain_metadata| offchain_metadata.hash_matches); + .is_some_and(|offchain_metadata| offchain_metadata.is_hash_match); Ok(GetNftDataResponse { data: Some(NftData { - blob: data.as_ref().map(|data| BASE64_STANDARD.encode(&data.blob)), + blob: data.as_ref().map(|data| BASE64_STANDARD.encode(&data.data)), mime_type: data.map(|data| data.mime_type), hash_matches, metadata_json: offchain_metadata.and_then(|offchain_metadata| { if offchain_metadata.mime_type == "application/json" { - String::from_utf8(offchain_metadata.blob).ok() + String::from_utf8(offchain_metadata.data).ok() } else { None } @@ -647,9 +653,9 @@ impl Sage { Ok(GetNftIconResponse { icon: wallet .db - .nft_icon(data_hash) + .icon(data_hash) .await? - .map(|icon| BASE64_STANDARD.encode(icon)), + .map(|icon| BASE64_STANDARD.encode(icon.data)), }) } @@ -673,20 +679,15 @@ impl Sage { Ok(GetNftThumbnailResponse { thumbnail: wallet .db - .nft_thumbnail(data_hash) + .thumbnail(data_hash) .await? - .map(|thumbnail| BASE64_STANDARD.encode(thumbnail)), + .map(|thumbnail| BASE64_STANDARD.encode(thumbnail.data)), }) } - fn nft_record( - &self, - nft_row: NftRow, - nft: Nft, - collection_name: Option, - ) -> Result { + fn nft_record(&self, row: NftRow) -> Result { let mut allocator = Allocator::new(); - let metadata_ptr = nft.info.metadata.to_clvm(&mut allocator)?; + let metadata_ptr = row.nft_info.metadata.to_clvm(&mut allocator)?; let metadata = NftMetadata::from_clvm(&allocator, metadata_ptr).ok(); let data_hash = metadata.as_ref().and_then(|m| m.data_hash); @@ -694,28 +695,32 @@ impl Sage { let license_hash = metadata.as_ref().and_then(|m| m.license_hash); Ok(NftRecord { - launcher_id: Address::new(nft_row.launcher_id, "nft".to_string()).encode()?, - collection_id: nft_row - .collection_id - .map(|col| Address::new(col, "col".to_string()).encode()) - .transpose()?, - collection_name, - minter_did: nft_row - .minter_did + launcher_id: Address::new(row.asset.hash, "nft".to_string()).encode()?, + collection_id: Some( + Address::new(row.nft_info.collection_hash, "col".to_string()).encode()?, + ), + collection_name: row.nft_info.collection_name, + minter_did: row + .nft_info + .minter_hash .map(|did| Address::new(did, "did:chia:".to_string()).encode()) .transpose()?, - owner_did: nft_row - .owner_did + owner_did: row + .nft_info + .owner_hash .map(|did| Address::new(did, "did:chia:".to_string()).encode()) .transpose()?, - visible: nft_row.visible, - name: nft_row.name, - sensitive_content: nft_row.sensitive_content, - coin_id: hex::encode(nft.coin.coin_id()), - address: Address::new(nft.info.p2_puzzle_hash, self.network().prefix()).encode()?, - royalty_address: Address::new(nft.info.royalty_puzzle_hash, self.network().prefix()) - .encode()?, - royalty_ten_thousandths: nft.info.royalty_basis_points, + visible: row.asset.is_visible, + name: row.asset.name, + sensitive_content: row.asset.is_sensitive_content, + coin_id: hex::encode(row.coin_row.coin.coin_id()), + address: Address::new(row.coin_row.p2_puzzle_hash, self.network().prefix()).encode()?, + royalty_address: Address::new( + row.nft_info.royalty_puzzle_hash, + self.network().prefix(), + ) + .encode()?, + royalty_ten_thousandths: row.nft_info.royalty_basis_points, data_uris: metadata .as_ref() .map(|m| m.data_uris.clone()) @@ -733,122 +738,42 @@ impl Sage { license_hash: license_hash.map(hex::encode), edition_number: metadata.as_ref().map(|m| m.edition_number as u32), edition_total: metadata.as_ref().map(|m| m.edition_total as u32), - created_height: nft_row.created_height, + created_height: row.coin_row.created_height, + icon_url: row.asset.icon_url, }) } - fn transaction_coin(&self, transaction_coin: SqliteRow) -> Result { - let coin_id: Option = to_bytes32_opt(transaction_coin.get("coin_id")); - let kind_int: i64 = transaction_coin.get("kind"); - let coin_kind = CoinKind::from_i64(kind_int); - let p2_puzzle_hash: Option = - to_bytes32_opt(transaction_coin.get("p2_puzzle_hash")); - let name: Option = transaction_coin.get("name"); - let item_id: Option = to_bytes32_opt(transaction_coin.get("item_id")); - let amount: Vec = transaction_coin.get("amount"); - - let kind = match coin_kind { - CoinKind::Unknown => AssetKind::Unknown, - CoinKind::Xch => AssetKind::Xch, - CoinKind::Cat => { - if let Some(item_id) = item_id { - AssetKind::Cat { - asset_id: hex::encode(item_id), - name, - ticker: transaction_coin.get("ticker"), - icon_url: transaction_coin.get("cat_icon_url"), - } - } else { - AssetKind::Unknown - } - } - CoinKind::Nft => { - if let Some(item_id) = item_id { - let icon: Option> = transaction_coin.get("nft_icon"); - - AssetKind::Nft { - launcher_id: Address::new(item_id, "nft".to_string()).encode()?, - name, - icon: icon.map(|icon| BASE64_STANDARD.encode(icon)), - } - } else { - AssetKind::Unknown - } - } - CoinKind::Did => { - if let Some(item_id) = item_id { - AssetKind::Did { - launcher_id: Address::new(item_id, "did:chia:".to_string()).encode()?, - name, - } - } else { - AssetKind::Unknown - } - } - }; - - let address_kind = if let Some(p2_puzzle_hash) = p2_puzzle_hash { - address_kind(transaction_coin, p2_puzzle_hash) - } else { - AddressKind::Unknown - }; - - Ok(TransactionCoin { - coin_id: coin_id.map_or_else(String::new, hex::encode), - address: p2_puzzle_hash + fn transaction_coin(&self, transaction_coin: TransactionCoin) -> Result { + Ok(TransactionCoinRecord { + coin_id: hex::encode(transaction_coin.coin.coin_id()), + address: transaction_coin + .p2_puzzle_hash .map(|p2_puzzle_hash| { Address::new(p2_puzzle_hash, self.network().prefix()).encode() }) .transpose()?, - address_kind, - amount: Amount::u64(to_u64(&amount)?), - kind, + address_kind: address_kind(transaction_coin.p2_puzzle_hash), + amount: Amount::u64(transaction_coin.coin.amount), + asset: encode_asset(transaction_coin.asset)?, }) } - fn transaction_record( - &self, - height: u32, - timestamp: Option, - coins: Vec, - ) -> Result { + fn transaction_record(&self, transaction: Transaction) -> Result { let mut spent = Vec::new(); let mut created = Vec::new(); - for coin in coins { - let action: String = coin.get("action_type"); - let transaction_coin = self.transaction_coin(coin)?; - - if action == "spent" { - spent.push(transaction_coin); - } else { - created.push(transaction_coin); - } + for coin in transaction.created { + created.push(self.transaction_coin(coin)?); + } + for coin in transaction.spent { + spent.push(self.transaction_coin(coin)?); } Ok(TransactionRecord { - height, - timestamp, + height: transaction.height, + timestamp: transaction.timestamp, spent, created, }) } } - -fn address_kind(transaction_coin: SqliteRow, p2_puzzle_hash: Bytes32) -> AddressKind { - if p2_puzzle_hash == BURN_PUZZLE_HASH.into() { - return AddressKind::Burn; - } else if p2_puzzle_hash == SINGLETON_LAUNCHER_HASH.into() { - return AddressKind::Launcher; - } else if p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() { - return AddressKind::Offer; - } - - let derivation_count: Option = transaction_coin.get("derivation_count"); - - if derivation_count.is_some_and(|count| count > 0) { - AddressKind::Own - } else { - AddressKind::External - } -} diff --git a/crates/sage/src/endpoints/keys.rs b/crates/sage/src/endpoints/keys.rs index db7e1a966..5135f6993 100644 --- a/crates/sage/src/endpoints/keys.rs +++ b/crates/sage/src/endpoints/keys.rs @@ -11,13 +11,14 @@ use chia::{ use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use sage_api::{ - DeleteKey, DeleteKeyResponse, GenerateMnemonic, GenerateMnemonicResponse, GetKey, - GetKeyResponse, GetKeys, GetKeysResponse, GetSecretKey, GetSecretKeyResponse, ImportKey, - ImportKeyResponse, KeyInfo, KeyKind, Login, LoginResponse, Logout, LogoutResponse, RenameKey, - RenameKeyResponse, Resync, ResyncResponse, SecretKeyInfo, + DeleteDatabase, DeleteDatabaseResponse, DeleteKey, DeleteKeyResponse, GenerateMnemonic, + GenerateMnemonicResponse, GetKey, GetKeyResponse, GetKeys, GetKeysResponse, GetSecretKey, + GetSecretKeyResponse, ImportKey, ImportKeyResponse, KeyInfo, KeyKind, Login, LoginResponse, + Logout, LogoutResponse, RenameKey, RenameKeyResponse, Resync, ResyncResponse, SecretKeyInfo, }; -use sage_config::{ChangeMode, DerivationMode, Wallet}; -use sage_database::Database; +use sage_config::Wallet; +use sage_database::{Database, Derivation}; +use sqlx::query; use crate::{Error, Result, Sage}; @@ -46,42 +47,53 @@ impl Sage { let pool = self.connect_to_database(req.fingerprint).await?; - sqlx::query!( + query!( " - DELETE FROM `coin_states`; - DELETE FROM `transactions`; - DELETE FROM `peaks`; - DELETE FROM `cats`; - DELETE FROM `future_did_names`; - DELETE FROM `collections`; - DELETE FROM `nft_data`; - DELETE FROM `nft_uris`; + DELETE FROM mempool_items; + UPDATE blocks SET is_peak = FALSE WHERE is_peak = TRUE; " ) .execute(&pool) .await?; - if req.delete_offer_files { - sqlx::query!("DELETE FROM `offers`").execute(&pool).await?; + if req.delete_coins { + query!("DELETE FROM coins").execute(&pool).await?; } - if req.delete_unhardened_derivations { - sqlx::query!("DELETE FROM `derivations` WHERE `hardened` = 0") - .execute(&pool) - .await?; + if req.delete_assets { + query!( + " + DELETE FROM assets WHERE id != 0; + DELETE FROM collections WHERE id != 0; + " + ) + .execute(&pool) + .await?; } - if req.delete_hardened_derivations { - sqlx::query!("DELETE FROM `derivations` WHERE `hardened` = 1") - .execute(&pool) - .await?; + if req.delete_files { + query!("DELETE FROM files").execute(&pool).await?; } - if req.delete_blockinfo { - sqlx::query!("DELETE FROM `blockinfo`") - .execute(&pool) - .await?; + + if req.delete_offers { + query!("DELETE FROM offers").execute(&pool).await?; + } + + if req.delete_addresses { + query!("DELETE FROM p2_puzzles").execute(&pool).await?; } + if req.delete_blocks { + query!("DELETE FROM blocks").execute(&pool).await?; + } + + // reclaim disk space after all those deletes + query("PRAGMA wal_checkpoint(TRUNCATE)") + .execute(&pool) + .await?; + query("VACUUM").execute(&pool).await?; + query("ANALYZE").execute(&pool).await?; + if login { self.config.global.fingerprint = Some(req.fingerprint); self.save_config()?; @@ -147,9 +159,7 @@ impl Sage { self.wallet_config.wallets.push(Wallet { name: req.name, fingerprint, - change: ChangeMode::Default, - derivation: DerivationMode::Default, - network: None, + ..Default::default() }); self.config.global.fingerprint = Some(fingerprint); @@ -168,8 +178,15 @@ impl Sage { .derive_unhardened(index) .derive_synthetic(); let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); - tx.insert_derivation(p2_puzzle_hash, index, false, synthetic_key) - .await?; + tx.insert_custody_p2_puzzle( + p2_puzzle_hash, + synthetic_key, + Derivation { + derivation_index: index, + is_hardened: false, + }, + ) + .await?; } if let Some(master_sk) = master_sk { @@ -181,8 +198,15 @@ impl Sage { .derive_synthetic() .public_key(); let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); - tx.insert_derivation(p2_puzzle_hash, index, true, synthetic_key) - .await?; + tx.insert_custody_p2_puzzle( + p2_puzzle_hash, + synthetic_key, + Derivation { + derivation_index: index, + is_hardened: true, + }, + ) + .await?; } } @@ -195,6 +219,20 @@ impl Sage { Ok(ImportKeyResponse { fingerprint }) } + pub fn delete_database(&mut self, req: DeleteDatabase) -> Result { + let path = self.path.join("wallets").join(req.fingerprint.to_string()); + + if path.try_exists()? { + // Delete the specific SQLite file for this network + let db_file = path.join(format!("{}.sqlite", req.network)); + if db_file.try_exists()? { + fs::remove_file(&db_file)?; + } + } + + Ok(DeleteDatabaseResponse {}) + } + pub fn delete_key(&mut self, req: DeleteKey) -> Result { self.keychain.remove(req.fingerprint); @@ -240,17 +278,9 @@ impl Sage { return Ok(GetKeyResponse { key: None }); }; - let wallet_config = self - .wallet_config - .wallets - .iter() - .find(|wallet| wallet.fingerprint == fingerprint); - - let name = wallet_config.map_or_else(Wallet::default_name, |wallet| wallet.name.clone()); + let wallet_config = self.wallet_config().cloned().unwrap_or_default(); - let network_id = wallet_config - .and_then(|wallet| wallet.network.clone()) - .unwrap_or_else(|| self.network_id()); + let network_id = wallet_config.network.unwrap_or_else(|| self.network_id()); let Some(master_pk) = self.keychain.extract_public_key(fingerprint)? else { return Ok(GetKeyResponse { key: None }); @@ -258,7 +288,7 @@ impl Sage { Ok(GetKeyResponse { key: Some(KeyInfo { - name, + name: wallet_config.name, fingerprint, public_key: hex::encode(master_pk.to_bytes()), kind: KeyKind::Bls, diff --git a/crates/sage/src/endpoints/offers.rs b/crates/sage/src/endpoints/offers.rs index 6b5d06d58..81acf62db 100644 --- a/crates/sage/src/endpoints/offers.rs +++ b/crates/sage/src/endpoints/offers.rs @@ -1,67 +1,79 @@ -use std::{ - collections::HashMap, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -use base64::{prelude::BASE64_STANDARD, Engine}; +use chia::protocol::Bytes32; use chia::puzzles::nft::NftMetadata; use chia_wallet_sdk::{ driver::{decode_offer, encode_offer, DriverError, Offer, SpendContext}, signer::AggSigConstants, utils::Address, }; -use chrono::{Local, TimeZone}; use indexmap::IndexMap; use itertools::Itertools; use sage_api::{ - Amount, CancelOffer, CancelOfferResponse, CancelOffers, CancelOffersResponse, CatAmount, - CombineOffers, CombineOffersResponse, DeleteOffer, DeleteOfferResponse, GetOffer, - GetOfferResponse, GetOffers, GetOffersResponse, ImportOffer, ImportOfferResponse, MakeOffer, - MakeOfferResponse, OfferAssets, OfferCat, OfferNft, OfferRecord, OfferRecordStatus, - OfferSummary, OfferXch, TakeOffer, TakeOfferResponse, ViewOffer, ViewOfferResponse, + Amount, CancelOffer, CancelOfferResponse, CancelOffers, CancelOffersResponse, CombineOffers, + CombineOffersResponse, DeleteOffer, DeleteOfferResponse, GetOffer, GetOfferResponse, GetOffers, + GetOffersResponse, ImportOffer, ImportOfferResponse, MakeOffer, MakeOfferResponse, NftRoyalty, + OfferAsset, OfferRecord, OfferRecordStatus, OfferSummary, TakeOffer, TakeOfferResponse, + ViewOffer, ViewOfferResponse, }; use sage_assets::fetch_uris_with_hash; -use sage_database::{OfferCatRow, OfferNftRow, OfferRow, OfferStatus, OfferXchRow}; +use sage_database::{AssetKind, OfferRow, OfferStatus, OfferedAsset}; use sage_wallet::{ - aggregate_offers, fetch_nft_offer_details, insert_transaction, sort_offer, Offered, Requested, - SyncCommand, Transaction, Wallet, + aggregate_offers, insert_transaction, sort_offer, Offered, Requested, SyncCommand, Transaction, + Wallet, WalletError, }; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::time::timeout; -use tracing::{debug, warn}; +use tracing::debug; use crate::{ - extract_nft_data, json_bundle, lookup_coin_creation, offer_expiration, parse_amount, - parse_asset_id, parse_nft_id, parse_offer_id, ConfirmationInfo, Error, ExtractedNftData, - Result, Sage, + encode_asset, extract_nft_data, json_bundle, offer_expiration, parse_amount, parse_asset_id, + parse_nft_id, parse_offer_id, ConfirmationInfo, Error, ExtractedNftData, Result, Sage, }; +#[derive(Debug, Clone)] +struct AssetToOffer { + offer_id: Bytes32, + asset_id: Bytes32, + amount: u64, + royalty: u64, + is_requested: bool, +} + impl Sage { pub async fn make_offer(&self, req: MakeOffer) -> Result { let wallet = self.wallet()?; - let offered_xch = parse_amount(req.offered_assets.xch)?; - - let mut offered_cats = IndexMap::new(); - - for CatAmount { asset_id, amount } in req.offered_assets.cats { - offered_cats.insert(parse_asset_id(asset_id)?, parse_amount(amount)?); - } - - let mut offered_nfts = Vec::new(); - - for nft_id in req.offered_assets.nfts { - offered_nfts.push(parse_nft_id(nft_id)?); - } - - let requested_xch = parse_amount(req.requested_assets.xch)?; - - let mut requested_cats = IndexMap::new(); + let offered = Offered { + xch: parse_amount(req.offered_assets.xch)?, + cats: req + .offered_assets + .cats + .into_iter() + .map(|cat| Ok((parse_asset_id(cat.asset_id)?, parse_amount(cat.amount)?))) + .collect::>()?, + nfts: req + .offered_assets + .nfts + .into_iter() + .map(parse_nft_id) + .collect::>()?, + fee: parse_amount(req.fee)?, + p2_puzzle_hash: req + .receive_address + .map(|address| self.parse_address(address)) + .transpose()?, + }; - for CatAmount { asset_id, amount } in req.requested_assets.cats { - requested_cats.insert(parse_asset_id(asset_id)?, parse_amount(amount)?); - } + let mut requested = Requested { + xch: parse_amount(req.requested_assets.xch)?, + cats: req + .requested_assets + .cats + .into_iter() + .map(|cat| Ok((parse_asset_id(cat.asset_id)?, parse_amount(cat.amount)?))) + .collect::>()?, + nfts: IndexMap::new(), + }; - let mut requested_nfts = IndexMap::new(); let mut peer = None; for nft_id in req.requested_assets.nfts { @@ -69,40 +81,18 @@ impl Sage { peer = self.peer_state.lock().await.acquire_peer(); } - let peer = peer.as_ref().ok_or(Error::NoPeers)?; - let nft_id = parse_nft_id(nft_id)?; - let Some(offer_details) = fetch_nft_offer_details(peer, nft_id).await? else { + let Some(requested_nft) = wallet.fetch_offer_nft_info(peer.as_ref(), nft_id).await? + else { return Err(Error::CouldNotFetchNft(nft_id)); }; - requested_nfts.insert(nft_id, offer_details); + requested.nfts.insert(nft_id, requested_nft); } - let fee = parse_amount(req.fee)?; - - let p2_puzzle_hash = req - .receive_address - .map(|address| self.parse_address(address)) - .transpose()?; - let unsigned = wallet - .make_offer( - Offered { - xch: offered_xch, - cats: offered_cats, - nfts: offered_nfts, - fee, - p2_puzzle_hash, - }, - Requested { - xch: requested_xch, - cats: requested_cats, - nfts: requested_nfts, - }, - req.expires_at_second, - ) + .make_offer(offered, requested, req.expires_at_second) .await?; let (_mnemonic, Some(master_sk)) = @@ -191,10 +181,6 @@ impl Sage { let json_bundle = json_bundle(&spend_bundle); let transaction_id = hex::encode(spend_bundle.name()); - if req.auto_import { - self.import_offer(ImportOffer { offer: req.offer }).await?; - } - Ok(TakeOfferResponse { summary: self .summarize(spend_bundle.coin_spends, ConfirmationInfo::default()) @@ -215,14 +201,12 @@ impl Sage { let spend_bundle = sort_offer(decode_offer(&req.offer)?); let offer_id = spend_bundle.name(); - if wallet.db.get_offer(offer_id).await?.is_some() { + if wallet.db.offer(offer_id).await?.is_some() { return Ok(ImportOfferResponse { offer_id: hex::encode(offer_id), }); } - let peer = self.peer_state.lock().await.acquire_peer(); - let mut ctx = SpendContext::new(); let offer = Offer::from_spend_bundle(&mut ctx, &spend_bundle)?; let coin_ids = offer @@ -231,15 +215,7 @@ impl Sage { .map(|cs| cs.coin.coin_id()) .collect_vec(); - let status = if let Some(peer) = peer { - let coin_creation = - lookup_coin_creation(&peer, coin_ids.clone(), self.network().genesis_challenge) - .await?; - offer_expiration(&mut ctx, &offer, &coin_creation)? - } else { - warn!("No peers available to fetch coin creation information, so skipping for now"); - offer_expiration(&mut ctx, &offer, &HashMap::new())? - }; + let status = offer_expiration(&mut ctx, &offer)?; let offered_amounts = offer.offered_coins().amounts(); let requested_amounts = offer.requested_payments().amounts(); @@ -250,25 +226,17 @@ impl Sage { let mut nft_rows = Vec::new(); for (asset_id, amount) in offered_amounts.cats { - let info = wallet.db.cat(asset_id).await?; - let name = info.as_ref().and_then(|info| info.name.clone()); - let ticker = info.as_ref().and_then(|info| info.ticker.clone()); - let icon = info.as_ref().and_then(|info| info.icon.clone()); - - cat_rows.push(OfferCatRow { + cat_rows.push(AssetToOffer { offer_id, - requested: false, + is_requested: false, asset_id, amount, - name: name.clone(), - ticker: ticker.clone(), - icon: icon.clone(), royalty: offered_royalties.cats.get(&asset_id).copied().unwrap_or(0), }); } for nft in offer.offered_coins().nfts.values() { - let info = if let Ok(metadata) = ctx.extract::(nft.info.metadata.ptr()) { + let _info = if let Ok(metadata) = ctx.extract::(nft.info.metadata.ptr()) { let mut confirmation_info = ConfirmationInfo::default(); if let Some(hash) = metadata.data_hash { @@ -298,32 +266,23 @@ impl Sage { ExtractedNftData::default() }; - nft_rows.push(OfferNftRow { + nft_rows.push(AssetToOffer { offer_id, - requested: false, - launcher_id: nft.info.launcher_id, - royalty_puzzle_hash: nft.info.royalty_puzzle_hash, - royalty_ten_thousandths: nft.info.royalty_basis_points, - name: info.name, - thumbnail: info.icon, - thumbnail_mime_type: Some("image/png".to_string()), + is_requested: false, + asset_id: nft.info.launcher_id, + amount: nft.coin.amount, + royalty: nft.info.royalty_basis_points as u64, }); } for (asset_id, amount) in requested_amounts.cats { - let info = wallet.db.cat(asset_id).await?; - let name = info.as_ref().and_then(|info| info.name.clone()); - let ticker = info.as_ref().and_then(|info| info.ticker.clone()); - let icon = info.as_ref().and_then(|info| info.icon.clone()); + self.cache_cat(asset_id).await?; - cat_rows.push(OfferCatRow { + cat_rows.push(AssetToOffer { offer_id, - requested: true, + is_requested: true, asset_id, amount, - name: name.clone(), - ticker: ticker.clone(), - icon: icon.clone(), royalty: requested_royalties .cats .get(&asset_id) @@ -338,7 +297,15 @@ impl Sage { .nft(launcher_id) .ok_or(DriverError::MissingAssetInfo)?; - let info = if let Ok(metadata) = ctx.extract::(nft.metadata.ptr()) { + self.cache_nft( + &ctx, + launcher_id, + nft.metadata.ptr(), + &mut ConfirmationInfo::default(), + ) + .await?; + + let _info = if let Ok(metadata) = ctx.extract::(nft.metadata.ptr()) { let mut confirmation_info = ConfirmationInfo::default(); if let Some(hash) = metadata.data_hash { @@ -368,21 +335,12 @@ impl Sage { ExtractedNftData::default() }; - let thumbnail_mime_type = if info.icon.is_some() { - Some("image/png".to_string()) - } else { - None - }; - - nft_rows.push(OfferNftRow { + nft_rows.push(AssetToOffer { offer_id, - requested: true, - launcher_id, - royalty_puzzle_hash: nft.royalty_puzzle_hash, - royalty_ten_thousandths: nft.royalty_basis_points, - name: info.name, - thumbnail: info.icon, - thumbnail_mime_type, + is_requested: true, + asset_id: launcher_id, + amount: 0, // TODO is this right? + royalty: nft.royalty_basis_points as u64, }); } @@ -405,35 +363,55 @@ impl Sage { .await?; for coin_id in coin_ids { + if !tx.is_known_coin(coin_id).await? { + return Err(Error::Wallet(WalletError::CannotImportOffer)); + } + tx.insert_offered_coin(offer_id, coin_id).await?; } if offered_amounts.xch > 0 || offered_royalties.xch > 0 { - tx.insert_offer_xch(OfferXchRow { + tx.insert_offer_asset( offer_id, - requested: false, - amount: offered_amounts.xch, - royalty: offered_royalties.xch, - }) + Bytes32::default(), + offered_amounts.xch, + offered_royalties.xch, + false, + ) .await?; } if requested_amounts.xch > 0 || requested_royalties.xch > 0 { - tx.insert_offer_xch(OfferXchRow { + tx.insert_offer_asset( offer_id, - requested: true, - amount: requested_amounts.xch, - royalty: requested_royalties.xch, - }) + Bytes32::default(), + requested_amounts.xch, + requested_royalties.xch, + true, + ) .await?; } for row in cat_rows { - tx.insert_offer_cat(row).await?; + tx.insert_offer_asset( + row.offer_id, + row.asset_id, + row.amount, + row.royalty, + row.is_requested, + ) + .await?; } for row in nft_rows { - tx.insert_offer_nft(row).await?; + tx.insert_offer_asset( + row.offer_id, + row.asset_id, + row.amount, + row.royalty, + row.is_requested, + ) + .await?; } tx.commit().await?; @@ -457,7 +435,7 @@ impl Sage { pub async fn get_offers(&self, _req: GetOffers) -> Result { let wallet = self.wallet()?; - let offers = wallet.db.get_offers().await?; + let offers = wallet.db.offers(None).await?; let mut records = Vec::new(); @@ -474,7 +452,7 @@ impl Sage { let offer_id = parse_offer_id(req.offer_id)?; let offer = wallet .db - .get_offer(offer_id) + .offer(offer_id) .await? .ok_or_else(|| Error::MissingOffer(offer_id))?; @@ -493,64 +471,48 @@ impl Sage { } async fn offer_record(&self, wallet: &Wallet, offer: OfferRow) -> Result { - let xch = wallet.db.offer_xch(offer.offer_id).await?; - let cats = wallet.db.offer_cats(offer.offer_id).await?; - let nfts = wallet.db.offer_nfts(offer.offer_id).await?; - - let mut maker_xch_amount = 0; - let mut maker_xch_royalty = 0; - let mut taker_xch_amount = 0; - let mut taker_xch_royalty = 0; - - for xch in xch { - if xch.requested { - taker_xch_amount += xch.amount; - taker_xch_royalty += xch.royalty; + let assets = wallet.db.offer_assets(offer.offer_id).await?; + + let mut maker = Vec::new(); + let mut taker = Vec::new(); + + for OfferedAsset { + asset, + amount, + royalty, + is_requested, + .. + } in assets + { + let nft_royalty = if asset.kind == AssetKind::Nft { + let info = wallet.db.offer_nft_info(asset.hash).await?; + + info.map(|info| { + Result::Ok(NftRoyalty { + royalty_address: Address::new( + info.royalty_puzzle_hash, + self.network().prefix(), + ) + .encode()?, + royalty_basis_points: info.royalty_basis_points, + }) + }) + .transpose()? } else { - maker_xch_amount += xch.amount; - maker_xch_royalty += xch.royalty; - } - } - - let mut maker_cats = IndexMap::new(); - let mut taker_cats = IndexMap::new(); - - for cat in cats { - let asset_id = hex::encode(cat.asset_id); - - let record = OfferCat { - amount: Amount::u64(cat.amount), - royalty: Amount::u64(cat.royalty), - name: cat.name, - ticker: cat.ticker, - icon_url: cat.icon, + None }; - if cat.requested { - taker_cats.insert(asset_id, record); - } else { - maker_cats.insert(asset_id, record); - } - } - - let mut maker_nfts = IndexMap::new(); - let mut taker_nfts = IndexMap::new(); - - for nft in nfts { - let nft_id = Address::new(nft.launcher_id, "nft".to_string()).encode()?; - - let record = OfferNft { - royalty_address: Address::new(nft.royalty_puzzle_hash, self.network().prefix()) - .encode()?, - royalty_ten_thousandths: nft.royalty_ten_thousandths, - name: nft.name, - icon: nft.thumbnail.map(|data| BASE64_STANDARD.encode(data)), + let asset = OfferAsset { + amount: Amount::u64(amount), + royalty: Amount::u64(royalty), + asset: encode_asset(asset)?, + nft_royalty, }; - if nft.requested { - taker_nfts.insert(nft_id, record); + if is_requested { + taker.push(asset); } else { - maker_nfts.insert(nft_id, record); + maker.push(asset); } } @@ -558,33 +520,16 @@ impl Sage { offer_id: hex::encode(offer.offer_id), offer: offer.encoded_offer, status: match offer.status { + OfferStatus::Pending => OfferRecordStatus::Pending, OfferStatus::Active => OfferRecordStatus::Active, OfferStatus::Completed => OfferRecordStatus::Completed, OfferStatus::Cancelled => OfferRecordStatus::Cancelled, OfferStatus::Expired => OfferRecordStatus::Expired, }, - creation_date: Local - .timestamp_opt(offer.inserted_timestamp.try_into()?, 0) - .unwrap() - .format("%b %d, %Y %r") - .to_string(), + creation_timestamp: offer.inserted_timestamp, summary: OfferSummary { - maker: OfferAssets { - xch: OfferXch { - amount: Amount::u64(maker_xch_amount), - royalty: Amount::u64(maker_xch_royalty), - }, - cats: maker_cats, - nfts: maker_nfts, - }, - taker: OfferAssets { - xch: OfferXch { - amount: Amount::u64(taker_xch_amount), - royalty: Amount::u64(taker_xch_royalty), - }, - cats: taker_cats, - nfts: taker_nfts, - }, + maker, + taker, fee: Amount::u64(offer.fee), expiration_height: offer.expiration_height, expiration_timestamp: offer.expiration_timestamp, @@ -597,7 +542,7 @@ impl Sage { let offer_id = parse_offer_id(req.offer_id)?; let fee = parse_amount(req.fee)?; - let Some(row) = wallet.db.get_offer(offer_id).await? else { + let Some(row) = wallet.db.offer(offer_id).await? else { return Err(Error::MissingOffer(offer_id)); }; @@ -620,7 +565,7 @@ impl Sage { let mut coin_spends = Vec::new(); for offer_id in offer_ids { - let Some(row) = wallet.db.get_offer(offer_id).await? else { + let Some(row) = wallet.db.offer(offer_id).await? else { return Err(Error::MissingOffer(offer_id)); }; diff --git a/crates/sage/src/endpoints/settings.rs b/crates/sage/src/endpoints/settings.rs index 65fce29aa..cf55b3bb3 100644 --- a/crates/sage/src/endpoints/settings.rs +++ b/crates/sage/src/endpoints/settings.rs @@ -4,6 +4,7 @@ use itertools::Itertools; use sage_api::{ AddPeer, AddPeerResponse, GetNetwork, GetNetworkResponse, GetNetworks, GetNetworksResponse, GetPeers, GetPeersResponse, NetworkKind, PeerRecord, RemovePeer, RemovePeerResponse, + SetDeltaSync, SetDeltaSyncOverride, SetDeltaSyncOverrideResponse, SetDeltaSyncResponse, SetDiscoverPeers, SetDiscoverPeersResponse, SetNetwork, SetNetworkOverride, SetNetworkOverrideResponse, SetNetworkResponse, SetTargetPeers, SetTargetPeersResponse, }; @@ -133,4 +134,27 @@ impl Sage { }, }) } + + pub fn set_delta_sync(&mut self, req: SetDeltaSync) -> Result { + self.wallet_config.defaults.delta_sync = req.delta_sync; + self.save_config()?; + Ok(SetDeltaSyncResponse {}) + } + + pub fn set_delta_sync_override( + &mut self, + req: SetDeltaSyncOverride, + ) -> Result { + let Some(wallet_config) = self + .wallet_config + .wallets + .iter_mut() + .find(|w| w.fingerprint == req.fingerprint) + else { + return Err(Error::UnknownFingerprint); + }; + wallet_config.delta_sync = req.delta_sync; + self.save_config()?; + Ok(SetDeltaSyncOverrideResponse {}) + } } diff --git a/crates/sage/src/endpoints/transactions.rs b/crates/sage/src/endpoints/transactions.rs index 14823a274..63c075527 100644 --- a/crates/sage/src/endpoints/transactions.rs +++ b/crates/sage/src/endpoints/transactions.rs @@ -1,21 +1,20 @@ use std::time::Duration; use chia::{ - protocol::{Bytes, Coin, CoinSpend}, + protocol::{Coin, CoinSpend}, puzzles::nft::NftMetadata, }; use chia_wallet_sdk::{driver::MetadataUpdate, utils::Address}; use itertools::Itertools; use sage_api::{ AddNftUri, AssignNftsToDid, AutoCombineCat, AutoCombineCatResponse, AutoCombineXch, - AutoCombineXchResponse, BulkMintNfts, BulkMintNftsResponse, BulkSendCat, BulkSendXch, - CombineCat, CombineXch, CreateDid, IssueCat, MultiSend, NftUriKind, NormalizeDids, SendCat, - SendXch, SignCoinSpends, SignCoinSpendsResponse, SplitCat, SplitXch, SubmitTransaction, - SubmitTransactionResponse, TransactionResponse, TransferDids, TransferNfts, ViewCoinSpends, - ViewCoinSpendsResponse, + AutoCombineXchResponse, BulkMintNfts, BulkMintNftsResponse, BulkSendCat, BulkSendXch, Combine, + CreateDid, IssueCat, MultiSend, NftUriKind, NormalizeDids, SendCat, SendXch, SignCoinSpends, + SignCoinSpendsResponse, Split, SubmitTransaction, SubmitTransactionResponse, + TransactionResponse, TransferDids, TransferNfts, ViewCoinSpends, ViewCoinSpendsResponse, }; use sage_assets::fetch_uris_without_hash; -use sage_database::CatRow; +use sage_database::{Asset, AssetKind}; use sage_wallet::{MultiSendPayment, WalletNftMint}; use tokio::time::timeout; @@ -33,7 +32,7 @@ impl Sage { let memos = parse_memos(req.memos)?; let coin_spends = wallet - .send_xch(vec![(puzzle_hash, amount)], fee, memos) + .send_xch(vec![(puzzle_hash, amount)], fee, memos, req.clawback) .await?; self.transact(coin_spends, req.auto_submit).await } @@ -52,11 +51,11 @@ impl Sage { let fee = parse_amount(req.fee)?; let memos = parse_memos(req.memos)?; - let coin_spends = wallet.send_xch(amounts, fee, memos).await?; + let coin_spends = wallet.send_xch(amounts, fee, memos, None).await?; self.transact(coin_spends, req.auto_submit).await } - pub async fn combine_xch(&self, req: CombineXch) -> Result { + pub async fn combine(&self, req: Combine) -> Result { let wallet = self.wallet()?; let fee = parse_amount(req.fee)?; let coin_ids = parse_coin_ids(req.coin_ids)?; @@ -72,7 +71,7 @@ impl Sage { let coins = wallet .db - .spendable_coins() + .selectable_xch_coins() .await? .into_iter() .filter(|coin| { @@ -102,7 +101,7 @@ impl Sage { }) } - pub async fn split_xch(&self, req: SplitXch) -> Result { + pub async fn split(&self, req: Split) -> Result { let wallet = self.wallet()?; let fee = parse_amount(req.fee)?; let coin_ids = parse_coin_ids(req.coin_ids)?; @@ -113,15 +112,6 @@ impl Sage { self.transact(coin_spends, req.auto_submit).await } - pub async fn combine_cat(&self, req: CombineCat) -> Result { - let wallet = self.wallet()?; - let fee = parse_amount(req.fee)?; - let coin_ids = parse_coin_ids(req.coin_ids)?; - - let coin_spends = wallet.combine(coin_ids, fee).await?; - self.transact(coin_spends, req.auto_submit).await - } - pub async fn auto_combine_cat(&self, req: AutoCombineCat) -> Result { let wallet = self.wallet()?; let fee = parse_amount(req.fee)?; @@ -130,7 +120,7 @@ impl Sage { let cats = wallet .db - .spendable_cat_coins(asset_id) + .selectable_cat_coins(asset_id) .await? .into_iter() .filter(|cat| { @@ -160,35 +150,27 @@ impl Sage { }) } - pub async fn split_cat(&self, req: SplitCat) -> Result { - let wallet = self.wallet()?; - let fee = parse_amount(req.fee)?; - let coin_ids = parse_coin_ids(req.coin_ids)?; - - let coin_spends = wallet - .split(coin_ids, req.output_count as usize, fee) - .await?; - self.transact(coin_spends, req.auto_submit).await - } - pub async fn issue_cat(&self, req: IssueCat) -> Result { let wallet = self.wallet()?; let amount = parse_amount(req.amount)?; let fee = parse_amount(req.fee)?; let (coin_spends, asset_id) = wallet.issue_cat(amount, fee, None).await?; - wallet - .db - .insert_cat(CatRow { - asset_id, - name: Some(req.name), - ticker: Some(req.ticker), - description: None, - icon: None, - visible: true, - fetched: true, - }) - .await?; + let mut tx = wallet.db.tx().await?; + + tx.insert_asset(Asset { + hash: asset_id, + name: Some(req.name), + ticker: Some(req.ticker), + precision: 3, + icon_url: None, + description: None, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Token, + }) + .await?; + tx.commit().await?; self.transact(coin_spends, req.auto_submit).await } @@ -208,6 +190,7 @@ impl Sage { fee, req.include_hint, memos, + req.clawback, ) .await?; self.transact(coin_spends, req.auto_submit).await @@ -229,7 +212,7 @@ impl Sage { let memos = parse_memos(req.memos)?; let coin_spends = wallet - .send_cat(asset_id, amounts, fee, req.include_hint, memos) + .send_cat(asset_id, amounts, fee, req.include_hint, memos, None) .await?; self.transact(coin_spends, req.auto_submit).await } @@ -247,15 +230,7 @@ impl Sage { }; let amount = parse_amount(payment.amount)?; let puzzle_hash = self.parse_address(payment.address)?; - let memos = if let Some(list) = payment.memos { - let mut memos = Vec::new(); - for memo in list { - memos.push(Bytes::from(hex::decode(memo)?)); - } - Some(memos) - } else { - None - }; + let memos = parse_memos(payment.memos)?; payments.push(MultiSendPayment { asset_id, @@ -276,14 +251,24 @@ impl Sage { let fee = parse_amount(req.fee)?; let (coin_spends, did) = wallet.create_did(fee).await?; + wallet .db - .set_future_did_name(did.info.launcher_id, req.name.clone()) + .insert_asset(Asset { + name: Some(req.name.clone()), + ticker: None, + precision: 1, + hash: did.info.launcher_id, + icon_url: None, + description: None, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Did, + }) .await?; - let mut info = ConfirmationInfo::default(); - info.did_names.insert(did.info.launcher_id, req.name); - self.transact_with(coin_spends, req.auto_submit, info).await + self.transact_with(coin_spends, req.auto_submit, ConfirmationInfo::default()) + .await } pub async fn bulk_mint_nfts(&self, req: BulkMintNfts) -> Result { @@ -405,7 +390,9 @@ impl Sage { let puzzle_hash = self.parse_address(req.address)?; let fee = parse_amount(req.fee)?; - let coin_spends = wallet.transfer_nfts(nft_ids, puzzle_hash, fee).await?; + let coin_spends = wallet + .transfer_nfts(nft_ids, puzzle_hash, fee, req.clawback) + .await?; self.transact(coin_spends, req.auto_submit).await } @@ -448,7 +435,9 @@ impl Sage { let puzzle_hash = self.parse_address(req.address)?; let fee = parse_amount(req.fee)?; - let coin_spends = wallet.transfer_dids(did_ids, puzzle_hash, fee).await?; + let coin_spends = wallet + .transfer_dids(did_ids, puzzle_hash, fee, req.clawback) + .await?; self.transact(coin_spends, req.auto_submit).await } diff --git a/crates/sage/src/endpoints/wallet_connect.rs b/crates/sage/src/endpoints/wallet_connect.rs index 0f95a7f15..1d0c680f1 100644 --- a/crates/sage/src/endpoints/wallet_connect.rs +++ b/crates/sage/src/endpoints/wallet_connect.rs @@ -1,16 +1,17 @@ use chia::{ bls::{master_to_wallet_hardened, master_to_wallet_unhardened, sign}, clvm_utils::ToTreeHash, - protocol::{Coin, CoinSpend, SpendBundle}, - puzzles::{cat::CatArgs, standard::StandardArgs, DeriveSynthetic, Proof}, + protocol::{Bytes32, Coin, CoinSpend, SpendBundle}, + puzzles::{DeriveSynthetic, Proof}, }; -use chia_wallet_sdk::driver::{Layer, SpendContext}; +use chia_wallet_sdk::driver::{ClawbackV2, Layer, SpendContext, StandardLayer}; use sage_api::wallet_connect::{ self, AssetCoinType, FilterUnlockedCoins, FilterUnlockedCoinsResponse, GetAssetCoins, GetAssetCoinsResponse, LineageProof, SendTransactionImmediately, SendTransactionImmediatelyResponse, SignMessageByAddress, SignMessageByAddressResponse, SignMessageWithPublicKey, SignMessageWithPublicKeyResponse, SpendableCoin, }; +use sage_database::{AssetFilter, CoinFilterMode, CoinSortMode, P2Puzzle}; use sage_wallet::{insert_transaction, submit_to_peers, Status, SyncCommand, Transaction}; use tracing::{debug, info, warn}; @@ -28,11 +29,7 @@ impl Sage { let mut coin_ids = Vec::new(); for coin_id in req.coin_ids { - if !wallet - .db - .is_coin_locked(parse_coin_id(coin_id.clone())?) - .await? - { + if wallet.db.are_coins_spendable(&[coin_id.clone()]).await? { coin_ids.push(coin_id); } } @@ -46,279 +43,105 @@ impl Sage { let mut items = Vec::new(); - if let Some(kind) = req.kind { - match kind { - AssetCoinType::Cat => { - let asset_id = parse_asset_id(req.asset_id.ok_or(Error::MissingAssetId)?)?; - - let rows = wallet - .db - .created_unspent_cat_coin_states( - asset_id, - req.limit.unwrap_or(10), - req.offset.unwrap_or(0), - ) - .await?; - - for row in rows { - let cs = row.coin_state; - - let in_transaction = wallet - .db - .coin_transaction_id(cs.coin.coin_id()) - .await? - .is_some(); - - if !include_locked && in_transaction { - continue; - } - - let is_offered = - wallet.db.coin_offer_id(cs.coin.coin_id()).await?.is_some(); - - if !include_locked && is_offered { - continue; - } - - let Some(cat) = wallet.db.cat_coin(cs.coin.coin_id()).await? else { - return Err(Error::MissingCatCoin(cs.coin.coin_id())); - }; - - let synthetic_key = - wallet.db.synthetic_key(cat.info.p2_puzzle_hash).await?; - - let mut ctx = SpendContext::new(); - let p2_puzzle = ctx.curry(StandardArgs::new(synthetic_key))?; - let cat_puzzle = ctx.curry(CatArgs::new(cat.info.asset_id, p2_puzzle))?; - - items.push(SpendableCoin { - coin: wallet_connect::Coin { - parent_coin_info: hex::encode(cs.coin.parent_coin_info), - puzzle_hash: hex::encode(cs.coin.puzzle_hash), - amount: cs.coin.amount, - }, - coin_name: hex::encode(cs.coin.coin_id()), - puzzle: hex::encode(ctx.serialize(&cat_puzzle)?), - confirmed_block_index: cs.created_height.expect("not created"), - locked: in_transaction || is_offered, - lineage_proof: cat.lineage_proof.map(|proof| LineageProof { - parent_name: Some(hex::encode(proof.parent_parent_coin_info)), - inner_puzzle_hash: Some(hex::encode( - proof.parent_inner_puzzle_hash, - )), - amount: Some(proof.parent_amount), - }), - }); + let (rows, _count) = wallet + .db + .coin_records( + match (req.kind, req.asset_id) { + (None, _) => AssetFilter::Id(Bytes32::default()), + (Some(AssetCoinType::Cat), None) => return Err(Error::MissingAssetId), + (Some(AssetCoinType::Cat), Some(asset_id)) => { + AssetFilter::Id(parse_asset_id(asset_id)?) } - } - AssetCoinType::Did => { - let asset_id = if let Some(asset_id) = req.asset_id { - Some(if let Ok(asset_id) = parse_asset_id(asset_id.clone()) { - asset_id - } else { - parse_did_id(asset_id)? - }) - } else { - None - }; - - let rows = if let Some(asset_id) = asset_id { - wallet.db.created_unspent_did_coin_state(asset_id).await? - } else { - wallet - .db - .created_unspent_did_coin_states( - req.limit.unwrap_or(10), - req.offset.unwrap_or(0), - ) - .await? - }; - - for row in rows { - let cs = row.coin_state; - - let in_transaction = wallet - .db - .coin_transaction_id(cs.coin.coin_id()) - .await? - .is_some(); - - if !include_locked && in_transaction { - continue; - } - - let is_offered = - wallet.db.coin_offer_id(cs.coin.coin_id()).await?.is_some(); - - if !include_locked && is_offered { - continue; - } - - let Some(did) = wallet.db.did_by_coin_id(cs.coin.coin_id()).await? else { - return Err(Error::MissingCoin(cs.coin.coin_id())); - }; - - let synthetic_key = - wallet.db.synthetic_key(did.info.p2_puzzle_hash).await?; - - let mut ctx = SpendContext::new(); - let p2_puzzle = ctx.curry(StandardArgs::new(synthetic_key))?; - let did_puzzle = - did.info.into_layers(p2_puzzle).construct_puzzle(&mut ctx)?; - - items.push(SpendableCoin { - coin: wallet_connect::Coin { - parent_coin_info: hex::encode(cs.coin.parent_coin_info), - puzzle_hash: hex::encode(cs.coin.puzzle_hash), - amount: cs.coin.amount, - }, - coin_name: hex::encode(cs.coin.coin_id()), - puzzle: hex::encode(ctx.serialize(&did_puzzle)?), - confirmed_block_index: cs.created_height.expect("not created"), - locked: in_transaction || is_offered, - lineage_proof: Some(match did.proof { - Proof::Lineage(proof) => LineageProof { - parent_name: Some(hex::encode(proof.parent_parent_coin_info)), - inner_puzzle_hash: Some(hex::encode( - proof.parent_inner_puzzle_hash, - )), - amount: Some(proof.parent_amount), - }, - Proof::Eve(proof) => LineageProof { - parent_name: Some(hex::encode(proof.parent_parent_coin_info)), - inner_puzzle_hash: None, - amount: Some(proof.parent_amount), - }, - }), - }); + (Some(AssetCoinType::Did), None) => AssetFilter::Dids, + (Some(AssetCoinType::Did), Some(asset_id)) => { + AssetFilter::Id(parse_did_id(asset_id)?) + } + (Some(AssetCoinType::Nft), None) => AssetFilter::Nfts, + (Some(AssetCoinType::Nft), Some(asset_id)) => { + AssetFilter::Id(parse_nft_id(asset_id)?) } + }, + req.limit.unwrap_or(10), + req.offset.unwrap_or(0), + CoinSortMode::CreatedHeight, + true, + if include_locked { + CoinFilterMode::Owned + } else { + CoinFilterMode::Selectable + }, + ) + .await?; + + for row in rows { + let mut ctx = SpendContext::new(); + + let p2_puzzle = match wallet.db.p2_puzzle(row.p2_puzzle_hash).await? { + P2Puzzle::PublicKey(key) => StandardLayer::new(key).construct_puzzle(&mut ctx)?, + P2Puzzle::Clawback(clawback) => { + let clawback = ClawbackV2::new( + clawback.sender_puzzle_hash, + clawback.receiver_puzzle_hash, + clawback.seconds, + row.coin.amount, + true, + ); + clawback.into_1_of_n().construct_puzzle(&mut ctx)? } - AssetCoinType::Nft => { - let asset_id = if let Some(asset_id) = req.asset_id { - Some(if let Ok(asset_id) = parse_asset_id(asset_id.clone()) { - asset_id - } else { - parse_nft_id(asset_id)? - }) - } else { - None - }; + P2Puzzle::OptionUnderlying(underlying) => { + underlying.option.into_1_of_n().construct_puzzle(&mut ctx)? + } + }; - let rows = if let Some(asset_id) = asset_id { - wallet.db.created_unspent_nft_coin_state(asset_id).await? - } else { - wallet - .db - .created_unspent_nft_coin_states( - req.limit.unwrap_or(10), - req.offset.unwrap_or(0), - ) - .await? + let (puzzle, proof) = match req.kind { + None => (p2_puzzle, None), + Some(AssetCoinType::Cat) => { + let Some(cat) = wallet.db.cat_coin(row.coin.coin_id()).await? else { + return Err(Error::MissingCatCoin(row.coin.coin_id())); }; - - for row in rows { - let cs = row.coin_state; - - let in_transaction = wallet - .db - .coin_transaction_id(cs.coin.coin_id()) - .await? - .is_some(); - - if !include_locked && in_transaction { - continue; - } - - let is_offered = - wallet.db.coin_offer_id(cs.coin.coin_id()).await?.is_some(); - - if !include_locked && is_offered { - continue; - } - - let Some(nft) = wallet.db.nft_by_coin_id(cs.coin.coin_id()).await? else { - return Err(Error::MissingCoin(cs.coin.coin_id())); - }; - - let synthetic_key = - wallet.db.synthetic_key(nft.info.p2_puzzle_hash).await?; - - let mut ctx = SpendContext::new(); - let p2_puzzle = ctx.curry(StandardArgs::new(synthetic_key))?; - let nft_puzzle = - nft.info.into_layers(p2_puzzle).construct_puzzle(&mut ctx)?; - - items.push(SpendableCoin { - coin: wallet_connect::Coin { - parent_coin_info: hex::encode(cs.coin.parent_coin_info), - puzzle_hash: hex::encode(cs.coin.puzzle_hash), - amount: cs.coin.amount, - }, - coin_name: hex::encode(cs.coin.coin_id()), - puzzle: hex::encode(ctx.serialize(&nft_puzzle)?), - confirmed_block_index: cs.created_height.expect("not created"), - locked: in_transaction || is_offered, - lineage_proof: Some(match nft.proof { - Proof::Lineage(proof) => LineageProof { - parent_name: Some(hex::encode(proof.parent_parent_coin_info)), - inner_puzzle_hash: Some(hex::encode( - proof.parent_inner_puzzle_hash, - )), - amount: Some(proof.parent_amount), - }, - Proof::Eve(proof) => LineageProof { - parent_name: Some(hex::encode(proof.parent_parent_coin_info)), - inner_puzzle_hash: None, - amount: Some(proof.parent_amount), - }, - }), - }); - } + let puzzle = cat.info.construct_puzzle(&mut ctx, p2_puzzle)?; + (puzzle, cat.lineage_proof.map(Proof::Lineage)) } - } - } else { - let rows = wallet - .db - .created_unspent_p2_coin_states(req.limit.unwrap_or(10), req.offset.unwrap_or(0)) - .await?; - - for row in rows { - let cs = row.coin_state; - - let in_transaction = wallet - .db - .coin_transaction_id(cs.coin.coin_id()) - .await? - .is_some(); - - if !include_locked && in_transaction { - continue; + Some(AssetCoinType::Did) => { + let Some(did) = wallet.db.did_coin(row.coin.coin_id()).await? else { + return Err(Error::MissingDidCoin(row.coin.coin_id())); + }; + let puzzle = did.info.into_layers(p2_puzzle).construct_puzzle(&mut ctx)?; + (puzzle, Some(did.proof)) } - - let is_offered = wallet.db.coin_offer_id(cs.coin.coin_id()).await?.is_some(); - - if !include_locked && is_offered { - continue; + Some(AssetCoinType::Nft) => { + let Some(nft) = wallet.db.nft_coin(row.coin.coin_id()).await? else { + return Err(Error::MissingNftCoin(row.coin.coin_id())); + }; + let puzzle = nft.info.into_layers(p2_puzzle).construct_puzzle(&mut ctx)?; + (puzzle, Some(nft.proof)) } - - let synthetic_key = wallet.db.synthetic_key(cs.coin.puzzle_hash).await?; - - let mut ctx = SpendContext::new(); - let puzzle = ctx.curry(StandardArgs::new(synthetic_key))?; - - items.push(SpendableCoin { - coin: wallet_connect::Coin { - parent_coin_info: hex::encode(cs.coin.parent_coin_info), - puzzle_hash: hex::encode(cs.coin.puzzle_hash), - amount: cs.coin.amount, - }, - coin_name: hex::encode(cs.coin.coin_id()), - puzzle: hex::encode(ctx.serialize(&puzzle)?), - confirmed_block_index: cs.created_height.expect("not created"), - locked: in_transaction || is_offered, - lineage_proof: None, - }); - } + }; + + items.push(SpendableCoin { + coin: wallet_connect::Coin { + parent_coin_info: hex::encode(row.coin.parent_coin_info), + puzzle_hash: hex::encode(row.coin.puzzle_hash), + amount: row.coin.amount, + }, + coin_name: hex::encode(row.coin.coin_id()), + puzzle: hex::encode(ctx.serialize(&puzzle)?), + confirmed_block_index: row.created_height.unwrap_or(0), + locked: row.mempool_item_hash.is_some() || row.offer_hash.is_some(), + lineage_proof: match proof { + None => None, + Some(Proof::Eve(proof)) => Some(LineageProof { + parent_name: Some(hex::encode(proof.parent_parent_coin_info)), + inner_puzzle_hash: None, + amount: Some(proof.parent_amount), + }), + Some(Proof::Lineage(proof)) => Some(LineageProof { + parent_name: Some(hex::encode(proof.parent_parent_coin_info)), + inner_puzzle_hash: Some(hex::encode(proof.parent_inner_puzzle_hash)), + amount: Some(proof.parent_amount), + }), + }, + }); } Ok(items) @@ -331,7 +154,7 @@ impl Sage { let wallet = self.wallet()?; let public_key = parse_public_key(req.public_key)?; - let Some(info) = wallet.db.synthetic_key_info(public_key).await? else { + let Some(info) = wallet.db.derivation(public_key).await? else { return Err(Error::InvalidKey); }; @@ -341,10 +164,10 @@ impl Sage { return Err(Error::NoSigningKey); }; - let secret_key = if info.hardened { - master_to_wallet_hardened(&master_sk, info.index) + let secret_key = if info.is_hardened { + master_to_wallet_hardened(&master_sk, info.derivation_index) } else { - master_to_wallet_unhardened(&master_sk, info.index) + master_to_wallet_unhardened(&master_sk, info.derivation_index) } .derive_synthetic(); @@ -366,9 +189,11 @@ impl Sage { let wallet = self.wallet()?; let p2_puzzle_hash = self.parse_address(req.address)?; - let public_key = wallet.db.synthetic_key(p2_puzzle_hash).await?; + let Some(public_key) = wallet.db.public_key(p2_puzzle_hash).await? else { + return Err(Error::InvalidKey); + }; - let Some(info) = wallet.db.synthetic_key_info(public_key).await? else { + let Some(info) = wallet.db.derivation(public_key).await? else { return Err(Error::InvalidKey); }; @@ -378,10 +203,10 @@ impl Sage { return Err(Error::NoSigningKey); }; - let secret_key = if info.hardened { - master_to_wallet_hardened(&master_sk, info.index) + let secret_key = if info.is_hardened { + master_to_wallet_hardened(&master_sk, info.derivation_index) } else { - master_to_wallet_unhardened(&master_sk, info.index) + master_to_wallet_unhardened(&master_sk, info.derivation_index) } .derive_synthetic(); @@ -411,15 +236,7 @@ impl Sage { let transaction_id = spend_bundle.name(); - match submit_to_peers(&peers, wallet.genesis_challenge, spend_bundle.clone()).await? { - Status::Success => { - info!("Transaction {transaction_id} has already been confirmed, not submitting again."); - - Ok(SendTransactionImmediatelyResponse { - status: 1, - error: None, - }) - } + match submit_to_peers(&peers, spend_bundle.clone()).await? { Status::Pending => { let peer = self .peer_state diff --git a/crates/sage/src/error.rs b/crates/sage/src/error.rs index a487860c9..acd53678b 100644 --- a/crates/sage/src/error.rs +++ b/crates/sage/src/error.rs @@ -169,6 +169,9 @@ pub enum Error { #[error("Missing coin: {0}")] MissingCoin(Bytes32), + #[error("Missing CAT: {0}")] + MissingCat(Bytes32), + #[error("Missing DID: {0}")] MissingDid(Bytes32), @@ -178,6 +181,12 @@ pub enum Error { #[error("Missing CAT coin: {0}")] MissingCatCoin(Bytes32), + #[error("Missing DID coin: {0}")] + MissingDidCoin(Bytes32), + + #[error("Missing NFT coin: {0}")] + MissingNftCoin(Bytes32), + #[error("Missing offer: {0}")] MissingOffer(Bytes32), @@ -199,6 +208,9 @@ pub enum Error { #[error("Missing asset id")] MissingAssetId, + #[error("Database version too old")] + DatabaseVersionTooOld, + #[error("Timeout")] Timeout(#[from] Elapsed), } @@ -217,7 +229,7 @@ impl Error { | KeychainError::Bip39(..) | KeychainError::Argon2(..) => ErrorKind::Internal, }, - Self::SqlxMigration(..) => ErrorKind::DatabaseMigration, + Self::SqlxMigration(..) | Self::DatabaseVersionTooOld => ErrorKind::DatabaseMigration, Self::Send(..) | Self::Io(..) | Self::Client(..) @@ -241,6 +253,9 @@ impl Error { | Self::UnknownNetwork | Self::MissingCoin(..) | Self::MissingCatCoin(..) + | Self::MissingDidCoin(..) + | Self::MissingNftCoin(..) + | Self::MissingCat(..) | Self::MissingDid(..) | Self::MissingNft(..) | Self::MissingOffer(..) => ErrorKind::NotFound, diff --git a/crates/sage/src/sage.rs b/crates/sage/src/sage.rs index b0610acb1..5c7291683 100644 --- a/crates/sage/src/sage.rs +++ b/crates/sage/src/sage.rs @@ -220,6 +220,11 @@ impl Sage { max_peer_age_seconds: 3600 * 8, dns_batch_size: 10, connection_batch_size: 30, + delta_sync: self + .wallet_config() + .cloned() + .unwrap_or_default() + .delta_sync(&self.wallet_config.defaults), puzzle_batch_size_per_peer: 5, timeouts: Timeouts::default(), testing: false, @@ -251,7 +256,10 @@ impl Sage { self.wallet = None; self.command_sender - .send(SyncCommand::SwitchWallet { wallet: None }) + .send(SyncCommand::SwitchWallet { + wallet: None, + delta_sync: self.wallet_config.defaults.delta_sync, + }) .await?; return Ok(()); @@ -265,7 +273,9 @@ impl Sage { let pool = self.connect_to_database(fingerprint).await?; let db = Database::new(pool); - db.run_rust_migrations().await?; + + db.run_rust_migrations(self.network().ticker.clone()) + .await?; let wallet = Arc::new(Wallet::new( db.clone(), @@ -284,6 +294,11 @@ impl Sage { self.command_sender .send(SyncCommand::SwitchWallet { wallet: Some(wallet), + delta_sync: self + .wallet_config() + .cloned() + .unwrap_or_default() + .delta_sync(&self.wallet_config.defaults), }) .await?; @@ -403,7 +418,9 @@ impl Sage { ) .await?; - sqlx::migrate!("../../migrations").run(&pool).await?; + if let Err(_error) = sqlx::migrate!("../../migrations").run(&pool).await { + return Err(Error::DatabaseVersionTooOld); + } Ok(pool) } @@ -427,20 +444,22 @@ impl Sage { Ok(path) } - pub fn network(&self) -> &Network { - if let Some(fingerprint) = self.config.global.fingerprint { - if let Some(wallet) = self - .wallet_config + pub fn wallet_config(&self) -> Option<&sage_config::Wallet> { + self.config.global.fingerprint.and_then(|fingerprint| { + self.wallet_config .wallets .iter() .find(|w| w.fingerprint == fingerprint) - { - if let Some(network) = &wallet.network { - return self - .network_list - .by_name(network) - .expect("network not found"); - } + }) + } + + pub fn network(&self) -> &Network { + if let Some(wallet) = self.wallet_config() { + if let Some(network) = &wallet.network { + return self + .network_list + .by_name(network) + .expect("network not found"); } } diff --git a/crates/sage/src/utils.rs b/crates/sage/src/utils.rs index cc0c5e15b..99cda691a 100644 --- a/crates/sage/src/utils.rs +++ b/crates/sage/src/utils.rs @@ -1,32 +1,12 @@ -mod coins; +mod cache; mod confirmation; +mod conversions; mod offer_status; mod offer_summary; mod parse; mod spends; -pub use coins::*; pub use confirmation::*; +pub use conversions::*; pub use offer_status::*; pub use parse::*; - -use crate::Error; -use chia::protocol::Bytes32; - -pub fn to_bytes(slice: &[u8]) -> Result<[u8; N], Error> { - slice - .try_into() - .map_err(|_| Error::Database(sage_database::DatabaseError::InvalidLength(slice.len(), N))) -} - -pub fn to_bytes32(slice: &[u8]) -> Result { - to_bytes(slice).map(Bytes32::new) -} - -pub fn to_u64(slice: &[u8]) -> Result { - Ok(u64::from_be_bytes(to_bytes::<8>(slice)?)) -} - -pub fn to_bytes32_opt(bytes: Option>) -> Option { - bytes.map(|b| to_bytes32(&b).unwrap_or_default()) -} diff --git a/crates/sage/src/utils/cache.rs b/crates/sage/src/utils/cache.rs new file mode 100644 index 000000000..04192906c --- /dev/null +++ b/crates/sage/src/utils/cache.rs @@ -0,0 +1,106 @@ +use std::{collections::hash_map::Entry, time::Duration}; + +use chia::{clvm_traits::FromClvm, protocol::Bytes32, puzzles::nft::NftMetadata}; +use chia_wallet_sdk::types::TESTNET11_CONSTANTS; +use clvmr::{Allocator, NodePtr}; +use sage_assets::{fetch_uris_with_hash, DexieCat}; +use sage_database::{Asset, AssetKind}; +use tokio::time::timeout; + +use crate::{extract_nft_data, ConfirmationInfo, ExtractedNftData, Result, Sage}; + +impl Sage { + pub async fn cache_cat(&self, asset_id: Bytes32) -> Result { + let wallet = self.wallet()?; + + if let Some(asset) = wallet.db.asset(asset_id).await? { + return Ok(asset); + } + + let testnet = self.network().genesis_challenge == TESTNET11_CONSTANTS.genesis_challenge; + + let asset = if let Ok(Ok(asset)) = + timeout(Duration::from_secs(5), DexieCat::fetch(asset_id, testnet)).await + { + Asset { + hash: asset_id, + name: asset.name, + ticker: asset.ticker, + precision: 3, + icon_url: asset.icon_url, + description: asset.description, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Token, + } + } else { + Asset::default_cat(asset_id) + }; + + wallet.db.insert_asset(asset.clone()).await?; + + Ok(asset) + } + + pub async fn cache_nft( + &self, + allocator: &Allocator, + launcher_id: Bytes32, + nft_metadata: NodePtr, + confirmation_info: &mut ConfirmationInfo, + ) -> Result { + let wallet = self.wallet()?; + + if let Some(asset) = wallet.db.asset(launcher_id).await? { + return Ok(asset); + } + + let info = if let Ok(metadata) = NftMetadata::from_clvm(allocator, nft_metadata) { + if let Some(hash) = metadata.data_hash { + if let Entry::Vacant(entry) = confirmation_info.nft_data.entry(hash) { + if let Ok(Some(data)) = timeout( + Duration::from_secs(10), + fetch_uris_with_hash(metadata.data_uris.clone(), hash), + ) + .await + { + entry.insert(data); + } + } + } + + if let Some(hash) = metadata.metadata_hash { + if let Entry::Vacant(entry) = confirmation_info.nft_data.entry(hash) { + if let Ok(Some(data)) = timeout( + Duration::from_secs(10), + fetch_uris_with_hash(metadata.metadata_uris.clone(), hash), + ) + .await + { + entry.insert(data); + } + } + } + + extract_nft_data(Some(&wallet.db), Some(metadata), confirmation_info).await? + } else { + ExtractedNftData::default() + }; + + let asset = Asset { + hash: launcher_id, + name: info.name, + ticker: None, + precision: 1, + icon_url: info.icon_url, + description: info.description, + is_sensitive_content: info.is_sensitive_content, + is_visible: true, + kind: AssetKind::Nft, + }; + + wallet.db.insert_asset(asset.clone()).await?; + + Ok(asset) + } +} diff --git a/crates/sage/src/utils/coins.rs b/crates/sage/src/utils/coins.rs deleted file mode 100644 index 3810b7b9d..000000000 --- a/crates/sage/src/utils/coins.rs +++ /dev/null @@ -1,4 +0,0 @@ -use hex_literal::hex; - -pub const BURN_PUZZLE_HASH: [u8; 32] = - hex!("000000000000000000000000000000000000000000000000000000000000dead"); diff --git a/crates/sage/src/utils/confirmation.rs b/crates/sage/src/utils/confirmation.rs index ea4946f69..e0fd21900 100644 --- a/crates/sage/src/utils/confirmation.rs +++ b/crates/sage/src/utils/confirmation.rs @@ -1,26 +1,26 @@ use std::collections::HashMap; -use base64::{prelude::BASE64_STANDARD, Engine}; use chia::{ + clvm_traits::ToClvm, protocol::{Bytes32, Coin, CoinSpend, SpendBundle}, puzzles::nft::NftMetadata, }; -use chia_wallet_sdk::utils::Address; +use chia_wallet_sdk::{driver::BURN_PUZZLE_HASH, utils::Address}; +use clvmr::Allocator; use sage_api::{ - Amount, AssetKind, CoinJson, CoinSpendJson, SpendBundleJson, TransactionInput, - TransactionOutput, TransactionSummary, + Amount, CoinJson, CoinSpendJson, SpendBundleJson, TransactionInput, TransactionOutput, + TransactionSummary, }; -use sage_assets::Data; -use sage_database::Database; -use sage_wallet::{compute_nft_info, ChildKind, CoinKind, Transaction}; +use sage_assets::{base64_data_uri, Data}; +use sage_database::{Asset, AssetKind, Database}; +use sage_wallet::{compute_nft_info, CoinKind, Transaction}; -use crate::{Error, Result, Sage}; +use crate::{encode_asset, Error, Result, Sage}; -use super::{parse_coin_id, parse_hash, parse_program, parse_signature, BURN_PUZZLE_HASH}; +use super::{parse_coin_id, parse_hash, parse_program, parse_signature}; #[derive(Debug, Default)] pub struct ConfirmationInfo { - pub did_names: HashMap, pub nft_data: HashMap, } @@ -28,7 +28,7 @@ impl Sage { pub(crate) async fn summarize( &self, coin_spends: Vec, - cache: ConfirmationInfo, + mut cache: ConfirmationInfo, ) -> Result { let wallet = self.wallet()?; @@ -39,54 +39,42 @@ impl Sage { for input in transaction.inputs { let coin = input.coin_spend.coin; - let (kind, p2_puzzle_hash) = match input.kind { + let mut p2_puzzle_hash = coin.puzzle_hash; + + let asset = match input.kind { + CoinKind::Launcher => None, CoinKind::Unknown => { - let kind = if wallet.db.is_p2_puzzle_hash(coin.puzzle_hash).await? { - AssetKind::Xch + if wallet.db.is_p2_puzzle_hash(coin.puzzle_hash).await? { + wallet.db.asset(Bytes32::default()).await? } else { - AssetKind::Unknown - }; - (kind, coin.puzzle_hash) + None + } } - CoinKind::Launcher => (AssetKind::Launcher, coin.puzzle_hash), - CoinKind::Cat { - asset_id, - p2_puzzle_hash, - } => { - let cat = wallet.db.cat(asset_id).await?; - let kind = AssetKind::Cat { - asset_id: hex::encode(asset_id), - name: cat.as_ref().and_then(|cat| cat.name.clone()), - ticker: cat.as_ref().and_then(|cat| cat.ticker.clone()), - icon_url: cat.as_ref().and_then(|cat| cat.icon.clone()), - }; - (kind, p2_puzzle_hash) + CoinKind::Cat { info } => { + p2_puzzle_hash = info.p2_puzzle_hash; + Some(self.cache_cat(info.asset_id).await?) } - CoinKind::Did { info } => { - let name = if let Some(name) = cache.did_names.get(&info.launcher_id).cloned() { - Some(name) - } else { - wallet.db.did_name(info.launcher_id).await? - }; - - let kind = AssetKind::Did { - launcher_id: Address::new(info.launcher_id, "did:chia:".to_string()) - .encode()?, - name, - }; - - (kind, info.p2_puzzle_hash) + CoinKind::Nft { info, .. } => { + let mut allocator = Allocator::new(); + let metadata = info.metadata.to_clvm(&mut allocator)?; + p2_puzzle_hash = info.p2_puzzle_hash; + Some( + self.cache_nft(&allocator, info.launcher_id, metadata, &mut cache) + .await?, + ) } - CoinKind::Nft { info, metadata } => { - let extracted = extract_nft_data(Some(&wallet.db), metadata, &cache).await?; - - let kind = AssetKind::Nft { - launcher_id: Address::new(info.launcher_id, "nft".to_string()).encode()?, - icon: extracted.icon.map(|icon| BASE64_STANDARD.encode(icon)), - name: extracted.name, - }; - - (kind, info.p2_puzzle_hash) + CoinKind::Did { info, .. } => { + Some(wallet.db.asset(info.launcher_id).await?.unwrap_or(Asset { + hash: info.launcher_id, + name: None, + ticker: None, + precision: 1, + icon_url: None, + description: None, + is_sensitive_content: false, + is_visible: true, + kind: AssetKind::Did, + })) } }; @@ -95,13 +83,10 @@ impl Sage { let mut outputs = Vec::new(); for output in input.outputs { - let p2_puzzle_hash = match output.kind { - ChildKind::Unknown { hint } => hint.unwrap_or(output.coin.puzzle_hash), - ChildKind::Launcher => output.coin.puzzle_hash, - ChildKind::Cat { p2_puzzle_hash, .. } => p2_puzzle_hash, - ChildKind::Did { info, .. } => info.p2_puzzle_hash, - ChildKind::Nft { info, .. } => info.p2_puzzle_hash, - }; + let p2_puzzle_hash = output + .kind + .receiver_custody_p2_puzzle_hash() + .unwrap_or(output.coin.puzzle_hash); let address = Address::new(p2_puzzle_hash, self.network().prefix()).encode()?; @@ -109,8 +94,8 @@ impl Sage { coin_id: hex::encode(output.coin.coin_id()), amount: Amount::u64(output.coin.amount), address, - receiving: wallet.db.is_p2_puzzle_hash(p2_puzzle_hash).await?, - burning: p2_puzzle_hash.to_bytes() == BURN_PUZZLE_HASH, + receiving: wallet.db.is_custody_p2_puzzle_hash(p2_puzzle_hash).await?, + burning: p2_puzzle_hash == BURN_PUZZLE_HASH, }); } @@ -118,7 +103,7 @@ impl Sage { coin_id: hex::encode(coin.coin_id()), amount: Amount::u64(coin.amount), address, - kind, + asset: asset.map(encode_asset).transpose()?, outputs, }); } @@ -132,8 +117,10 @@ impl Sage { #[derive(Debug, Default)] pub struct ExtractedNftData { - pub icon: Option>, pub name: Option, + pub description: Option, + pub icon_url: Option, + pub is_sensitive_content: bool, } pub async fn extract_nft_data( @@ -150,24 +137,30 @@ pub async fn extract_nft_data( if let Some(data_hash) = onchain_metadata.data_hash { if let Some(Data { thumbnail: Some(thumbnail), + mime_type, .. }) = cache.nft_data.get(&data_hash) { - result.icon = Some(thumbnail.icon.clone()); + result.icon_url = Some(base64_data_uri(&thumbnail.icon, mime_type)); } else if let Some(db) = &db { - if let Some(data) = db.nft_icon(data_hash).await? { - result.icon = Some(data); + if let Some(icon) = db.icon(data_hash).await? { + result.icon_url = Some(base64_data_uri( + &icon.data, + icon.mime_type.as_deref().unwrap_or("image/png"), + )); } } } if let Some(metadata_hash) = onchain_metadata.metadata_hash { if let Some(metadata) = cache.nft_data.get(&metadata_hash) { - let info = compute_nft_info(None, Some(&metadata.blob)); + let info = compute_nft_info(None, &metadata.blob); result.name = info.name; + result.description = info.description; + result.is_sensitive_content = info.sensitive_content; } else if let Some(db) = &db { - if let Some(metadata) = db.fetch_nft_data(metadata_hash).await? { - let info = compute_nft_info(None, Some(&metadata.blob)); + if let Some(metadata) = db.full_file_data(metadata_hash).await? { + let info = compute_nft_info(None, &metadata.data); result.name = info.name; } } diff --git a/crates/sage/src/utils/conversions.rs b/crates/sage/src/utils/conversions.rs new file mode 100644 index 000000000..571bc36ce --- /dev/null +++ b/crates/sage/src/utils/conversions.rs @@ -0,0 +1,61 @@ +use chia::protocol::Bytes32; +use chia_puzzles::{SETTLEMENT_PAYMENT_HASH, SINGLETON_LAUNCHER_HASH}; +use chia_wallet_sdk::{driver::BURN_PUZZLE_HASH, utils::Address}; +use sage_api::AddressKind; +use sage_database::{Asset, AssetKind}; + +use crate::Result; + +pub fn address_kind(p2_puzzle_hash: Option) -> AddressKind { + let Some(p2_puzzle_hash) = p2_puzzle_hash else { + return AddressKind::External; + }; + + if p2_puzzle_hash == BURN_PUZZLE_HASH { + return AddressKind::Burn; + } + if p2_puzzle_hash == SINGLETON_LAUNCHER_HASH.into() { + return AddressKind::Launcher; + } + if p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() { + return AddressKind::Offer; + } + + AddressKind::Own +} + +pub fn encode_asset(asset: Asset) -> Result { + Ok(sage_api::Asset { + asset_id: encode_asset_id(asset.hash, asset.kind)?, + name: asset.name, + ticker: asset.ticker, + precision: asset.precision, + icon_url: asset.icon_url, + description: asset.description, + is_sensitive_content: asset.is_sensitive_content, + is_visible: asset.is_visible, + kind: encode_asset_kind(asset.kind), + }) +} + +pub fn encode_asset_id(hash: Bytes32, kind: AssetKind) -> Result> { + Ok(if hash == Bytes32::default() { + None + } else { + Some(match kind { + AssetKind::Token => hex::encode(hash), + AssetKind::Nft => Address::new(hash, "nft".to_string()).encode()?, + AssetKind::Did => Address::new(hash, "did:chia:".to_string()).encode()?, + AssetKind::Option => Address::new(hash, "option".to_string()).encode()?, + }) + }) +} + +pub fn encode_asset_kind(kind: AssetKind) -> sage_api::AssetKind { + match kind { + AssetKind::Token => sage_api::AssetKind::Token, + AssetKind::Nft => sage_api::AssetKind::Nft, + AssetKind::Did => sage_api::AssetKind::Did, + AssetKind::Option => sage_api::AssetKind::Option, + } +} diff --git a/crates/sage/src/utils/offer_status.rs b/crates/sage/src/utils/offer_status.rs index 4c4ce5d84..030b2448c 100644 --- a/crates/sage/src/utils/offer_status.rs +++ b/crates/sage/src/utils/offer_status.rs @@ -1,17 +1,11 @@ -use std::collections::HashMap; - -use chia::{ - clvm_traits::{FromClvm, ToClvm}, - protocol::Bytes32, -}; +use chia::clvm_traits::{FromClvm, ToClvm}; use chia_wallet_sdk::{ driver::Offer, types::{run_puzzle, Condition}, }; use clvmr::{Allocator, NodePtr}; -use sage_wallet::WalletPeer; -use crate::{Error, Result}; +use crate::Result; #[derive(Debug, Clone)] pub struct OfferExpiration { @@ -19,57 +13,7 @@ pub struct OfferExpiration { pub expiration_timestamp: Option, } -#[derive(Debug, Clone, Copy)] -pub struct CoinCreation { - pub previous_height: u32, - pub previous_timestamp: u64, -} - -pub async fn lookup_coin_creation( - peer: &WalletPeer, - coin_ids: Vec, - genesis_challenge: Bytes32, -) -> Result> { - let coin_states_list = peer - .fetch_coins(coin_ids.clone(), genesis_challenge) - .await?; - - let mut coin_states = HashMap::new(); - - for coin_state in coin_states_list { - coin_states.insert(coin_state.coin.coin_id(), coin_state); - } - - let mut coin_creation = HashMap::new(); - - for coin_id in coin_ids { - let coin_state = coin_states - .get(&coin_id) - .ok_or(Error::MissingCoin(coin_id))?; - - let previous_height = coin_state - .created_height - .map_or(0, |height| height.saturating_sub(1)); - - let previous_timestamp = peer.block_timestamp(previous_height).await?.unwrap_or(0); - - coin_creation.insert( - coin_id, - CoinCreation { - previous_height, - previous_timestamp, - }, - ); - } - - Ok(coin_creation) -} - -pub fn offer_expiration( - allocator: &mut Allocator, - offer: &Offer, - coin_creation: &HashMap, -) -> Result { +pub fn offer_expiration(allocator: &mut Allocator, offer: &Offer) -> Result { let mut expiration_height = None::; let mut expiration_timestamp = None::; @@ -95,32 +39,6 @@ pub fn offer_expiration( Some(cond.seconds) }; } - Condition::AssertBeforeHeightRelative(cond) => { - let previous_height = coin_creation - .get(&coin_spend.coin.coin_id()) - .map_or(0, |state| state.previous_height); - - let height = previous_height + cond.height; - - expiration_height = if let Some(old_height) = expiration_height { - Some(old_height.min(height)) - } else { - Some(height) - }; - } - Condition::AssertBeforeSecondsRelative(cond) => { - let previous_timestamp = coin_creation - .get(&coin_spend.coin.coin_id()) - .map_or(0, |state| state.previous_timestamp); - - let timestamp = previous_timestamp + cond.seconds; - - expiration_timestamp = if let Some(old_timestamp) = expiration_timestamp { - Some(old_timestamp.min(timestamp)) - } else { - Some(timestamp) - }; - } _ => {} } } diff --git a/crates/sage/src/utils/offer_summary.rs b/crates/sage/src/utils/offer_summary.rs index a8dd4a332..c9a31443e 100644 --- a/crates/sage/src/utils/offer_summary.rs +++ b/crates/sage/src/utils/offer_summary.rs @@ -1,21 +1,13 @@ -use std::collections::HashMap; -use std::time::Duration; - -use base64::{prelude::BASE64_STANDARD, Engine}; -use chia::{protocol::SpendBundle, puzzles::nft::NftMetadata}; +use chia::protocol::Bytes32; +use chia::protocol::SpendBundle; use chia_wallet_sdk::driver::{DriverError, Offer}; use chia_wallet_sdk::{driver::SpendContext, utils::Address}; -use indexmap::IndexMap; -use itertools::Itertools; -use sage_api::{Amount, OfferAssets, OfferCat, OfferNft, OfferSummary, OfferXch}; -use sage_assets::fetch_uris_with_hash; -use tokio::time::timeout; -use tracing::warn; - -use crate::utils::offer_status::{lookup_coin_creation, offer_expiration}; -use crate::{Result, Sage}; +use sage_api::{Amount, NftRoyalty, OfferAsset, OfferSummary}; +use sage_wallet::WalletError; -use super::{extract_nft_data, ConfirmationInfo, ExtractedNftData}; +use crate::utils::offer_status::offer_expiration; +use crate::ConfirmationInfo; +use crate::{encode_asset, Error, Result, Sage}; impl Sage { pub(crate) async fn summarize_offer(&self, spend_bundle: SpendBundle) -> Result { @@ -24,155 +16,118 @@ impl Sage { let mut ctx = SpendContext::new(); let offer = Offer::from_spend_bundle(&mut ctx, &spend_bundle)?; - let coin_ids = offer - .cancellable_coin_spends()? - .into_iter() - .map(|cs| cs.coin.coin_id()) - .collect_vec(); // Get expiration information - let peer = self.peer_state.lock().await.acquire_peer(); - let status = if let Some(peer) = peer { - let coin_creation = - lookup_coin_creation(&peer, coin_ids.clone(), self.network().genesis_challenge) - .await?; - offer_expiration(&mut ctx, &offer, &coin_creation)? - } else { - warn!("No peers available to fetch coin creation information, so skipping for now"); - offer_expiration(&mut ctx, &offer, &HashMap::new())? - }; + let status = offer_expiration(&mut ctx, &offer)?; let offered_amounts = offer.offered_coins().amounts(); let requested_amounts = offer.requested_payments().amounts(); let offered_royalties = offer.offered_royalty_amounts(); let requested_royalties = offer.requested_royalty_amounts(); - let mut maker = OfferAssets { - xch: OfferXch { + let mut maker = Vec::new(); + + if offered_amounts.xch > 0 || offered_royalties.xch > 0 { + let Some(asset) = wallet.db.asset(Bytes32::default()).await? else { + return Err(Error::Wallet(WalletError::MissingAsset(Bytes32::default()))); + }; + + maker.push(OfferAsset { amount: Amount::u64(offered_amounts.xch), royalty: Amount::u64(offered_royalties.xch), - }, - cats: IndexMap::new(), - nfts: IndexMap::new(), - }; + asset: encode_asset(asset)?, + nft_royalty: None, + }); + } for (asset_id, amount) in offered_amounts.cats { - let cat = wallet.db.cat(asset_id).await?; - - maker.cats.insert( - hex::encode(asset_id), - OfferCat { - amount: Amount::u64(amount), - royalty: Amount::u64( - offered_royalties.cats.get(&asset_id).copied().unwrap_or(0), - ), - name: cat.as_ref().and_then(|cat| cat.name.clone()), - ticker: cat.as_ref().and_then(|cat| cat.ticker.clone()), - icon_url: cat.as_ref().and_then(|cat| cat.icon.clone()), - }, - ); + maker.push(OfferAsset { + amount: Amount::u64(amount), + royalty: Amount::u64(offered_royalties.cats.get(&asset_id).copied().unwrap_or(0)), + asset: encode_asset(self.cache_cat(asset_id).await?)?, + nft_royalty: None, + }); } for (&launcher_id, nft) in &offer.offered_coins().nfts { - let info = if let Ok(metadata) = ctx.extract::(nft.info.metadata.ptr()) { - let mut confirmation_info = ConfirmationInfo::default(); - - if let Some(hash) = metadata.data_hash { - if let Ok(Some(data)) = timeout( - Duration::from_secs(10), - fetch_uris_with_hash(metadata.data_uris.clone(), hash), - ) - .await - { - confirmation_info.nft_data.insert(hash, data); - } - } - - if let Some(hash) = metadata.metadata_hash { - if let Ok(Some(data)) = timeout( - Duration::from_secs(10), - fetch_uris_with_hash(metadata.metadata_uris.clone(), hash), - ) - .await - { - confirmation_info.nft_data.insert(hash, data); - } - } - - extract_nft_data(Some(&wallet.db), Some(metadata), &confirmation_info).await? - } else { - ExtractedNftData::default() - }; - - maker.nfts.insert( - Address::new(launcher_id, "nft".to_string()).encode()?, - OfferNft { - icon: info.icon.map(|icon| BASE64_STANDARD.encode(icon)), - name: info.name, - royalty_ten_thousandths: nft.info.royalty_basis_points, + let asset = self + .cache_nft( + &ctx, + launcher_id, + nft.info.metadata.ptr(), + &mut ConfirmationInfo::default(), + ) + .await?; + + maker.push(OfferAsset { + amount: Amount::u64(nft.coin.amount), + royalty: Amount::u64(0), + asset: encode_asset(asset)?, + nft_royalty: Some(NftRoyalty { royalty_address: Address::new( nft.info.royalty_puzzle_hash, - self.network().prefix().clone(), + self.network().prefix(), ) .encode()?, - }, - ); + royalty_basis_points: nft.info.royalty_basis_points, + }), + }); } - let mut taker = OfferAssets { - xch: OfferXch { + let mut taker = Vec::new(); + + if requested_amounts.xch > 0 || requested_royalties.xch > 0 { + let Some(asset) = wallet.db.asset(Bytes32::default()).await? else { + return Err(Error::Wallet(WalletError::MissingAsset(Bytes32::default()))); + }; + + taker.push(OfferAsset { amount: Amount::u64(requested_amounts.xch), royalty: Amount::u64(requested_royalties.xch), - }, - cats: IndexMap::new(), - nfts: IndexMap::new(), - }; + asset: encode_asset(asset)?, + nft_royalty: None, + }); + } for (asset_id, amount) in requested_amounts.cats { - let cat = wallet.db.cat(asset_id).await?; - - taker.cats.insert( - hex::encode(asset_id), - OfferCat { - amount: Amount::u64(amount), - royalty: Amount::u64( - requested_royalties - .cats - .get(&asset_id) - .copied() - .unwrap_or(0), - ), - name: cat.as_ref().and_then(|cat| cat.name.clone()), - ticker: cat.as_ref().and_then(|cat| cat.ticker.clone()), - icon_url: cat.as_ref().and_then(|cat| cat.icon.clone()), - }, - ); + taker.push(OfferAsset { + amount: Amount::u64(amount), + royalty: Amount::u64(*requested_royalties.cats.get(&asset_id).unwrap_or(&0)), + asset: encode_asset(self.cache_cat(asset_id).await?)?, + nft_royalty: None, + }); } - for &launcher_id in offer.requested_payments().nfts.keys() { + for (&launcher_id, payments) in &offer.requested_payments().nfts { + let amount = payments + .iter() + .map(|p| p.payments.iter().map(|p| p.amount).sum::()) + .sum::(); + let nft = offer .asset_info() .nft(launcher_id) .ok_or(DriverError::MissingAssetInfo)?; - let metadata = ctx.extract::(nft.metadata.ptr())?; - let info = extract_nft_data( - Some(&wallet.db), - Some(metadata), - &ConfirmationInfo::default(), - ) - .await?; - - taker.nfts.insert( - Address::new(launcher_id, "nft".to_string()).encode()?, - OfferNft { - icon: info.icon.map(|icon| BASE64_STANDARD.encode(icon)), - name: info.name, - royalty_ten_thousandths: nft.royalty_basis_points, + let asset = self + .cache_nft( + &ctx, + launcher_id, + nft.metadata.ptr(), + &mut ConfirmationInfo::default(), + ) + .await?; + + taker.push(OfferAsset { + amount: Amount::u64(amount), + royalty: Amount::u64(0), + asset: encode_asset(asset)?, + nft_royalty: Some(NftRoyalty { royalty_address: Address::new(nft.royalty_puzzle_hash, self.network().prefix()) .encode()?, - }, - ); + royalty_basis_points: nft.royalty_basis_points, + }), + }); } Ok(OfferSummary { diff --git a/crates/sage/src/utils/parse.rs b/crates/sage/src/utils/parse.rs index f9f49bbf5..3217b7173 100644 --- a/crates/sage/src/utils/parse.rs +++ b/crates/sage/src/utils/parse.rs @@ -147,16 +147,12 @@ pub fn parse_program(input: String) -> Result { Ok(hex::decode(stripped)?.into()) } -pub fn parse_memos(input: Option>) -> Result>> { - if let Some(list) = input { - let mut memos = Vec::new(); - for memo in list { - memos.push(Bytes::from(hex::decode(memo)?)); - } - Ok(Some(memos)) - } else { - Ok(None) +pub fn parse_memos(input: Vec) -> Result> { + let mut memos = Vec::new(); + for memo in input { + memos.push(Bytes::from(hex::decode(memo)?)); } + Ok(memos) } #[cfg(test)] diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..f2ba5d698 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,55 @@ +// @ts-check + +import eslint from '@eslint/js'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['target/**/*', 'src/bindings.ts'], + }, + { + files: ['src/**/*.ts', 'src/**/*.tsx'], + extends: [ + eslint.configs.recommended, + tseslint.configs.recommended, + tseslint.configs.strict, + tseslint.configs.stylistic, + ], + plugins: { + react, + 'react-hooks': reactHooks, + }, + rules: { + // React Hooks rules + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + // React rules + ...react.configs.recommended.rules, + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'react/no-unstable-nested-components': 'warn', + 'react/hook-use-state': 'warn', + 'react/button-has-type': 'warn', + 'react/default-props-match-prop-types': 'warn', + 'react/no-did-mount-set-state': 'warn', + 'react/no-did-update-set-state': 'warn', + 'react/no-invalid-html-attribute': 'warn', + 'react/no-unsafe': 'warn', + 'react/no-typos': 'warn', + 'react/no-array-index-key': 'warn', + 'react/no-danger': 'warn', + 'react/require-optimization': 'warn', + 'react/no-access-state-in-setstate': 'warn', + 'react/no-redundant-should-component-update': 'warn', + 'react/no-this-in-sfc': 'warn', + 'react/no-unused-state': 'warn', + }, + settings: { + react: { + version: 'detect', + }, + }, + }, +); diff --git a/migrations/0000_pragmas.sql b/migrations/0000_pragmas.sql new file mode 100644 index 000000000..a5243959d --- /dev/null +++ b/migrations/0000_pragmas.sql @@ -0,0 +1,2 @@ +PRAGMA journal_mode = WAL; +PRAGMA foreign_keys = ON; diff --git a/migrations/0001_setup.sql b/migrations/0001_setup.sql deleted file mode 100644 index 7990ff6cc..000000000 --- a/migrations/0001_setup.sql +++ /dev/null @@ -1,193 +0,0 @@ -CREATE TABLE `peaks` ( - `height` INTEGER NOT NULL PRIMARY KEY, - `header_hash` BLOB NOT NULL -); - -CREATE TABLE `derivations` ( - `p2_puzzle_hash` BLOB NOT NULL PRIMARY KEY, - `index` INTEGER NOT NULL, - `hardened` BOOLEAN NOT NULL, - `synthetic_key` BLOB NOT NULL -); - -CREATE INDEX `derivation_index` ON `derivations` (`index`, `hardened`); -CREATE INDEX `derivation_key` ON `derivations` (`synthetic_key`); - -CREATE TABLE `coin_states` ( - `coin_id` BLOB NOT NULL PRIMARY KEY, - `parent_coin_id` BLOB NOT NULL, - `puzzle_hash` BLOB NOT NULL, - `amount` BLOB NOT NULL, - `spent_height` INTEGER, - `created_height` INTEGER, - `hint` BLOB, - `synced` BOOLEAN NOT NULL, - `transaction_id` BLOB, - FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`transaction_id`) ON DELETE CASCADE -); - -CREATE INDEX `coin_puzzle_hash` ON `coin_states` (`puzzle_hash`); -CREATE INDEX `coin_hint` ON `coin_states` (`hint`); -CREATE INDEX `coin_spent` ON `coin_states` (`spent_height`); -CREATE INDEX `coin_created` ON `coin_states` (`created_height`); -CREATE INDEX `coin_synced` ON `coin_states` (`synced`); -CREATE INDEX `coin_height` ON `coin_states` (`spent_height` ASC, `created_height` DESC); -CREATE INDEX `coin_transaction` ON `coin_states` (`transaction_id`); - -CREATE TABLE `transactions` ( - `transaction_id` BLOB NOT NULL PRIMARY KEY, - `aggregated_signature` BLOB NOT NULL, - `fee` BLOB NOT NULL, - `submitted_at` INTEGER -); - -CREATE TABLE `transaction_spends` ( - `coin_id` BLOB NOT NULL PRIMARY KEY, - `index` INTEGER NOT NULL, - `transaction_id` BLOB NOT NULL, - `parent_coin_id` BLOB NOT NULL, - `puzzle_hash` BLOB NOT NULL, - `amount` BLOB NOT NULL, - `puzzle_reveal` BLOB NOT NULL, - `solution` BLOB NOT NULL, - FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`transaction_id`) ON DELETE CASCADE -); - -CREATE INDEX `indexed_spend` ON `transaction_spends` (`transaction_id`, `index` ASC); - -CREATE TABLE `p2_coins` ( - `coin_id` BLOB NOT NULL PRIMARY KEY, - FOREIGN KEY (`coin_id`) REFERENCES `coin_states` (`coin_id`) ON DELETE CASCADE -); - -CREATE TABLE `cats` ( - `asset_id` BLOB NOT NULL PRIMARY KEY, - `name` TEXT, - `ticker` TEXT, - `visible` BOOLEAN NOT NULL, - `icon` TEXT, - `description` TEXT, - `fetched` BOOLEAN NOT NULL, - `is_named` BOOLEAN GENERATED ALWAYS AS (`name` IS NOT NULL) STORED -); - -CREATE INDEX `cat_lookup` ON `cats` (`fetched`); -CREATE INDEX `cat_name` ON `cats` (`visible` DESC, `is_named` DESC, `name` ASC, `asset_id` ASC); - -CREATE TABLE `cat_coins` ( - `coin_id` BLOB NOT NULL PRIMARY KEY, - `parent_parent_coin_id` BLOB NOT NULL, - `parent_inner_puzzle_hash` BLOB NOT NULL, - `parent_amount` BLOB NOT NULL, - `p2_puzzle_hash` BLOB NOT NULL, - `asset_id` BLOB NOT NULL, - FOREIGN KEY (`coin_id`) REFERENCES `coin_states` (`coin_id`) ON DELETE CASCADE -); - -CREATE INDEX `cat_asset_id` ON `cat_coins` (`asset_id`); - -CREATE TABLE `dids` ( - `launcher_id` BLOB NOT NULL PRIMARY KEY, - `coin_id` BLOB NOT NULL, - `name` TEXT, - `visible` BOOLEAN NOT NULL, - `is_owned` BOOLEAN NOT NULL, - `is_named` BOOLEAN GENERATED ALWAYS AS (`name` IS NOT NULL) STORED, - `created_height` INTEGER, - `is_pending` BOOLEAN GENERATED ALWAYS AS (`created_height` IS NULL) STORED, - FOREIGN KEY (`coin_id`) REFERENCES `did_coins` (`coin_id`) ON DELETE CASCADE -); - -CREATE INDEX `did_coin_id` ON `dids` (`coin_id`); -CREATE INDEX `did_name` ON `dids` (`is_owned`, `visible` DESC, `is_pending` DESC, `is_named` DESC, `name` ASC, `launcher_id` ASC); - -CREATE TABLE `future_did_names` ( - `launcher_id` BLOB NOT NULL PRIMARY KEY, - `name` TEXT NOT NULL -); - -CREATE TABLE `did_coins` ( - `coin_id` BLOB NOT NULL PRIMARY KEY, - `parent_parent_coin_id` BLOB NOT NULL, - `parent_inner_puzzle_hash` BLOB NOT NULL, - `parent_amount` BLOB NOT NULL, - `launcher_id` BLOB NOT NULL, - `recovery_list_hash` BLOB, - `num_verifications_required` BLOB NOT NULL, - `metadata` BLOB NOT NULL, - `p2_puzzle_hash` BLOB NOT NULL, - FOREIGN KEY (`coin_id`) REFERENCES `coin_states` (`coin_id`) ON DELETE CASCADE -); - -CREATE INDEX `did_launcher_id` ON `did_coins` (`launcher_id`); - -CREATE TABLE `collections` ( - `collection_id` BLOB NOT NULL PRIMARY KEY, - `did_id` BLOB NOT NULL, - `metadata_collection_id` TEXT NOT NULL, - `name` TEXT, - `visible` BOOLEAN NOT NULL, - `icon` TEXT, - `is_named` BOOLEAN GENERATED ALWAYS AS (`name` IS NOT NULL) STORED -); - -CREATE INDEX `col_name` ON `collections` (`visible` DESC, `is_named` DESC, `name` ASC, `collection_id` ASC); - -CREATE TABLE `nfts` ( - `launcher_id` BLOB NOT NULL PRIMARY KEY, - `coin_id` BLOB NOT NULL, - `collection_id` BLOB, - `minter_did` BLOB, - `owner_did` BLOB, - `visible` BOOLEAN NOT NULL, - `sensitive_content` BOOLEAN NOT NULL, - `name` TEXT, - `is_owned` BOOLEAN NOT NULL, - `is_named` BOOLEAN GENERATED ALWAYS AS (`name` IS NOT NULL) STORED, - `created_height` INTEGER, - `is_pending` BOOLEAN GENERATED ALWAYS AS (`created_height` IS NULL) STORED, - `metadata_hash` BLOB, - FOREIGN KEY (`coin_id`) REFERENCES `nft_coins` (`coin_id`) ON DELETE CASCADE -); - -CREATE INDEX `nft_coin_id` ON `nfts` (`coin_id`); -CREATE INDEX `nft_metadata` ON `nfts` (`is_owned`, `metadata_hash`); -CREATE INDEX `nft_name` ON `nfts` (`is_owned`, `visible` DESC, `is_pending` DESC, `is_named` DESC, `name` ASC, `launcher_id` ASC); -CREATE INDEX `nft_recent` ON `nfts` (`is_owned`, `visible` DESC, `is_pending` DESC, `created_height` DESC, `launcher_id` ASC); -CREATE INDEX `nft_col_name` ON `nfts` (`is_owned`, `collection_id`, `visible` DESC, `is_pending` DESC, `is_named` DESC, `name` ASC, `launcher_id` ASC); -CREATE INDEX `nft_col_recent` ON `nfts` (`is_owned`, `collection_id`, `visible` DESC, `is_pending` DESC, `created_height` DESC, `launcher_id` ASC); - -CREATE TABLE `nft_coins` ( - `coin_id` BLOB NOT NULL PRIMARY KEY, - `parent_parent_coin_id` BLOB NOT NULL, - `parent_inner_puzzle_hash` BLOB NOT NULL, - `parent_amount` BLOB NOT NULL, - `launcher_id` BLOB NOT NULL, - `metadata` BLOB NOT NULL, - `metadata_updater_puzzle_hash` BLOB NOT NULL, - `current_owner` BLOB, - `royalty_puzzle_hash` BLOB NOT NULL, - `royalty_ten_thousandths` INTEGER NOT NULL, - `p2_puzzle_hash` BLOB NOT NULL, - `data_hash` BLOB, - `metadata_hash` BLOB, - `license_hash` BLOB, - FOREIGN KEY (`coin_id`) REFERENCES `coin_states` (`coin_id`) ON DELETE CASCADE -); - -CREATE INDEX `nft_launcher_id` ON `nft_coins` (`launcher_id`); - -CREATE TABLE `nft_data` ( - `hash` BLOB NOT NULL PRIMARY KEY, - `data` BLOB NOT NULL, - `mime_type` TEXT NOT NULL -); - -CREATE TABLE `nft_uris` ( - `uri` TEXT NOT NULL, - `hash` BLOB NOT NULL, - `checked` BOOLEAN NOT NULL, - PRIMARY KEY (`uri`, `hash`) -); - -CREATE INDEX `nft_uri_checked_hash` ON `nft_uris` (`checked`, `hash`); diff --git a/migrations/0001_tables.sql b/migrations/0001_tables.sql new file mode 100644 index 000000000..5b8eb5020 --- /dev/null +++ b/migrations/0001_tables.sql @@ -0,0 +1,298 @@ +/* + * The following conventions are used in tables: + * + * 1. All BOOLEAN columns are named is_ + * 2. All foreign keys are specified with FOREIGN KEY (and indexed) + * 3. All UNIX timestamps are INTEGER and named _timestamp + * 4. All tables have a surrogate or INTEGER NOT NULL PRIMARY KEY + * 5. All natural keys are specified as UNIQUE (which also creates an auto-index) +*/ + +/* + * A table with one row that represents changes made so far to the schema that involve + * more complex or parameterizable code than SQLite can handle. + */ +CREATE TABLE rust_migrations ( + version INTEGER NOT NULL PRIMARY KEY +); + +CREATE TABLE collections ( + id INTEGER NOT NULL PRIMARY KEY, + hash BLOB NOT NULL UNIQUE, + uuid TEXT NOT NULL, + minter_hash BLOB NOT NULL, + name TEXT, + icon_url TEXT, + banner_url TEXT, + description TEXT, + is_visible BOOLEAN NOT NULL +); + +/* + * A single table that represents all kinds of supported assets on the Chia blockchain: + * Token = 0 + * NFT = 1 + * DID = 2 + * Option = 3 + * + * The hash represents the asset's unique on-chain identifier (asset id or launcher id). + * Everything else is for display purposes only + * + * Note: For CATs, there isn't a defined created or spent height for the whole asset class, + * but for singletons it's possible to no longer own the asset, but still reference it in + * transaction history or offers. + */ +CREATE TABLE assets ( + id INTEGER NOT NULL PRIMARY KEY, + hash BLOB NOT NULL UNIQUE, + kind INTEGER NOT NULL, + name TEXT, + ticker TEXT, + precision INTEGER NOT NULL, + icon_url TEXT, + description TEXT, + is_sensitive_content BOOLEAN NOT NULL DEFAULT FALSE, + is_visible BOOLEAN NOT NULL +); + +CREATE TABLE nfts ( + id INTEGER NOT NULL PRIMARY KEY, + asset_id INTEGER NOT NULL UNIQUE, + collection_id INTEGER NOT NULL DEFAULT 0, + minter_hash BLOB, + owner_hash BLOB, + metadata BLOB NOT NULL, + metadata_updater_puzzle_hash BLOB NOT NULL, + royalty_puzzle_hash BLOB NOT NULL, + royalty_basis_points INTEGER NOT NULL, + data_hash BLOB, + metadata_hash BLOB, + license_hash BLOB, + edition_number INTEGER, + edition_total INTEGER, + FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE SET DEFAULT, + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE +); + +CREATE TABLE dids ( + id INTEGER NOT NULL PRIMARY KEY, + asset_id INTEGER NOT NULL UNIQUE, + metadata BLOB NOT NULL, + recovery_list_hash BLOB, + num_verifications_required INTEGER NOT NULL, + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE +); + +CREATE TABLE options ( + id INTEGER NOT NULL PRIMARY KEY, + asset_id INTEGER NOT NULL UNIQUE, + creator_puzzle_hash BLOB NOT NULL, + expiration_seconds INTEGER NOT NULL, + underlying_asset_id INTEGER NOT NULL, + underlying_amount BLOB NOT NULL, + underlying_coin_hash BLOB NOT NULL, + underlying_delegated_puzzle_hash BLOB NOT NULL, + strike_asset_id INTEGER NOT NULL, + strike_hidden_puzzle_hash BLOB, + strike_settlement_puzzle_hash BLOB, + strike_amount BLOB NOT NULL, + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE, + FOREIGN KEY (underlying_asset_id) REFERENCES assets(id) ON DELETE CASCADE, + FOREIGN KEY (strike_asset_id) REFERENCES assets(id) ON DELETE CASCADE +); + +/* + * This isn't a comprehensive history of the blockchain, but keeps track of blocks that + * have been synced. It's primarily used for identifying the timestamp in which coins + * were created or spent. + * + * However, its secondary use is for performing a rollback if the header hash of a + * previous block is changed (a fork). + */ +CREATE TABLE blocks ( + height INTEGER NOT NULL PRIMARY KEY, + header_hash BLOB, + timestamp INTEGER, + is_peak BOOLEAN NOT NULL +); + +/* + * A table of all p2 puzzle hashes that belong to the wallet, from kinds such as: + * Derivation = 0 + * Clawback = 1 + * OptionUnderlying = 2 + * + * However, outer puzzles such as the CAT or revocation layer are stored elsewhere. + */ +CREATE TABLE p2_puzzles ( + id INTEGER NOT NULL PRIMARY KEY, + hash BLOB NOT NULL UNIQUE, + kind INTEGER NOT NULL +); + +CREATE TABLE public_keys ( + id INTEGER NOT NULL PRIMARY KEY, + p2_puzzle_id INTEGER NOT NULL UNIQUE, + is_hardened BOOLEAN NOT NULL, + derivation_index INTEGER NOT NULL, + key BLOB NOT NULL, + FOREIGN KEY (p2_puzzle_id) REFERENCES p2_puzzles(id) ON DELETE CASCADE +); + +CREATE TABLE clawbacks ( + id INTEGER NOT NULL PRIMARY KEY, + p2_puzzle_id INTEGER NOT NULL UNIQUE, + sender_puzzle_hash BLOB NOT NULL, + receiver_puzzle_hash BLOB NOT NULL, + expiration_seconds INTEGER NOT NULL, + FOREIGN KEY (p2_puzzle_id) REFERENCES p2_puzzles(id) ON DELETE CASCADE +); + +CREATE TABLE p2_options ( + id INTEGER NOT NULL PRIMARY KEY, + p2_puzzle_id INTEGER NOT NULL UNIQUE, + option_asset_id INTEGER NULL, + FOREIGN KEY (p2_puzzle_id) REFERENCES p2_puzzles(id) ON DELETE CASCADE, + FOREIGN KEY (option_asset_id) REFERENCES assets(id) ON DELETE CASCADE +); + +/* + * This is all coins which have been synced from the blockchain, created in a transaction, + * or spent in a transaction. + * + * When a coin is discovered, and hasn't been synced yet, it's added to this table without + * an asset_id. This will put it into a queue for further processing, in which case we + * will lookup the coin on-chain and discover the asset. + */ +CREATE TABLE coins ( + id INTEGER NOT NULL PRIMARY KEY, + asset_id INTEGER, + hash BLOB NOT NULL UNIQUE, + parent_coin_hash BLOB NOT NULL, + puzzle_hash BLOB NOT NULL, + amount BLOB NOT NULL, + hidden_puzzle_hash BLOB, + p2_puzzle_id INTEGER, + created_height INTEGER, + spent_height INTEGER, + is_children_synced BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE, + FOREIGN KEY (p2_puzzle_id) REFERENCES p2_puzzles(id) ON DELETE SET NULL, + FOREIGN KEY (created_height) REFERENCES blocks(height) ON DELETE CASCADE, + FOREIGN KEY (spent_height) REFERENCES blocks(height) ON DELETE SET NULL +); + +CREATE TABLE lineage_proofs ( + id INTEGER NOT NULL PRIMARY KEY, + coin_id INTEGER NOT NULL UNIQUE, + parent_parent_coin_hash BLOB NOT NULL, + parent_inner_puzzle_hash BLOB NOT NULL, + parent_amount BLOB NOT NULL, + FOREIGN KEY (coin_id) REFERENCES coins(id) ON DELETE CASCADE +); + +/* + * Offer statuses + * + * Pending = 0 + * Active = 1 + * Completed = 2 + * Cancelled = 3 + * Expired = 4 + */ +CREATE TABLE offers ( + id INTEGER NOT NULL PRIMARY KEY, + hash BLOB NOT NULL UNIQUE, + encoded_offer TEXT NOT NULL, + fee BLOB NOT NULL, + status INTEGER NOT NULL, + expiration_height INTEGER, + expiration_timestamp INTEGER, + inserted_timestamp INTEGER NOT NULL DEFAULT (unixepoch()) +); + +CREATE TABLE offer_assets ( + id INTEGER NOT NULL PRIMARY KEY, + offer_id INTEGER NOT NULL, + asset_id INTEGER NOT NULL, + is_requested BOOLEAN NOT NULL, + amount BLOB NOT NULL, + royalty BLOB NOT NULL, + FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE, + FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE, + UNIQUE(offer_id, asset_id, is_requested) +); + +CREATE TABLE offer_coins ( + id INTEGER NOT NULL PRIMARY KEY, + offer_id INTEGER NOT NULL, + coin_id INTEGER NOT NULL, + FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE, + FOREIGN KEY (coin_id) REFERENCES coins(id) ON DELETE CASCADE, + UNIQUE(offer_id, coin_id) +); + +CREATE TABLE mempool_items ( + id INTEGER NOT NULL PRIMARY KEY, + hash BLOB NOT NULL UNIQUE, + aggregated_signature BLOB NOT NULL, + fee BLOB NOT NULL, + submitted_timestamp INTEGER +); + +CREATE TABLE mempool_coins ( + id INTEGER NOT NULL PRIMARY KEY, + mempool_item_id INTEGER NOT NULL, + coin_id INTEGER NOT NULL, + is_input BOOLEAN NOT NULL, + is_output BOOLEAN NOT NULL, + FOREIGN KEY (mempool_item_id) REFERENCES mempool_items(id) ON DELETE CASCADE, + FOREIGN KEY (coin_id) REFERENCES coins(id) ON DELETE CASCADE, + UNIQUE(mempool_item_id, coin_id) +); + +CREATE TABLE mempool_spends ( + id INTEGER NOT NULL PRIMARY KEY, + mempool_item_id INTEGER NOT NULL, + coin_hash BLOB NOT NULL, + parent_coin_hash BLOB NOT NULL, + puzzle_hash BLOB NOT NULL, + amount BLOB NOT NULL, + puzzle_reveal BLOB NOT NULL, + solution BLOB NOT NULL, + seq INTEGER NOT NULL, + FOREIGN KEY (mempool_item_id) REFERENCES mempool_items(id) ON DELETE CASCADE, + UNIQUE(mempool_item_id, coin_hash) +); + +CREATE TABLE files ( + id INTEGER NOT NULL PRIMARY KEY, + hash BLOB NOT NULL UNIQUE, + data BLOB, + mime_type TEXT, + is_hash_match BOOLEAN +); + +/* + * Resized images + * + * Icon = 0 + * Thumbnail = 1 + */ +CREATE TABLE resized_images ( + id INTEGER NOT NULL PRIMARY KEY, + file_id INTEGER NOT NULL, + kind INTEGER NOT NULL, + data BLOB NOT NULL, + FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE +); + +CREATE TABLE file_uris ( + id INTEGER NOT NULL PRIMARY KEY, + file_id INTEGER NOT NULL, + uri TEXT NOT NULL, + last_checked_timestamp INTEGER, + failed_attempts INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE, + UNIQUE(file_id, uri) +); diff --git a/migrations/0002_offers.sql b/migrations/0002_offers.sql deleted file mode 100644 index d26191a56..000000000 --- a/migrations/0002_offers.sql +++ /dev/null @@ -1,62 +0,0 @@ -CREATE TABLE `offers` ( - `offer_id` BLOB NOT NULL PRIMARY KEY, - `encoded_offer` TEXT NOT NULL, - `expiration_height` INTEGER, - `expiration_timestamp` BLOB, - `fee` BLOB NOT NULL, - `status` INTEGER NOT NULL, - `inserted_timestamp` BLOB NOT NULL -); - -CREATE INDEX `offer_status` ON `offers` (`status`); -CREATE INDEX `offer_timestamp` ON `offers` (`inserted_timestamp` DESC); - -CREATE TABLE `offered_coins` ( - `offer_id` BLOB NOT NULL, - `coin_id` BLOB NOT NULL, - PRIMARY KEY (`offer_id`, `coin_id`), - FOREIGN KEY (`offer_id`) REFERENCES `offers`(`offer_id`) ON DELETE CASCADE -); - -CREATE INDEX `offer_coin_id` ON `offered_coins` (`coin_id`); - -CREATE TABLE `offer_xch` ( - `offer_id` BLOB NOT NULL, - `requested` BOOLEAN NOT NULL, - `amount` BLOB NOT NULL, - `royalty` BLOB NOT NULL, - PRIMARY KEY (`offer_id`, `requested`), - FOREIGN KEY (`offer_id`) REFERENCES `offers`(`offer_id`) ON DELETE CASCADE -); - -CREATE INDEX `xch_offer_id` ON `offer_xch` (`offer_id`); - -CREATE TABLE `offer_nfts` ( - `offer_id` BLOB NOT NULL, - `requested` BOOLEAN NOT NULL, - `launcher_id` BLOB NOT NULL, - `royalty_puzzle_hash` BLOB NOT NULL, - `royalty_ten_thousandths` INTEGER NOT NULL, - `name` TEXT, - `thumbnail` BLOB, - `thumbnail_mime_type` TEXT, - PRIMARY KEY (`offer_id`, `requested`), - FOREIGN KEY (`offer_id`) REFERENCES `offers`(`offer_id`) ON DELETE CASCADE -); - -CREATE INDEX `nft_offer_id` ON `offer_nfts` (`offer_id`); - -CREATE TABLE `offer_cats` ( - `offer_id` BLOB NOT NULL, - `requested` BOOLEAN NOT NULL, - `asset_id` BLOB NOT NULL, - `amount` BLOB NOT NULL, - `royalty` BLOB NOT NULL, - `name` TEXT, - `ticker` TEXT, - `icon` TEXT, - PRIMARY KEY (`offer_id`, `requested`), - FOREIGN KEY (`offer_id`) REFERENCES `offers`(`offer_id`) ON DELETE CASCADE -); - -CREATE INDEX `cat_offer_id` ON `offer_cats` (`offer_id`); diff --git a/migrations/0002_seed_data.sql b/migrations/0002_seed_data.sql new file mode 100644 index 000000000..854dc0207 --- /dev/null +++ b/migrations/0002_seed_data.sql @@ -0,0 +1,7 @@ +INSERT INTO assets (id, kind, hash, name, ticker, precision, description, icon_url, is_visible) +VALUES (0, 0, x'0000000000000000000000000000000000000000000000000000000000000000', 'Chia', 'XCH', 12, 'The primary asset on the Chia blockchain.', 'https://icons.dexie.space/xch.webp', TRUE); + +INSERT INTO collections (id, name, hash, uuid, minter_hash, is_visible) +VALUES (0, 'No Collection', x'0000000000000000000000000000000000000000000000000000000000000000', '00000000-0000-0000-0000-000000000000', x'0000000000000000000000000000000000000000000000000000000000000000', TRUE); + +INSERT INTO rust_migrations (version) VALUES (0); diff --git a/migrations/0003_indices.sql b/migrations/0003_indices.sql new file mode 100644 index 000000000..75a073a2d --- /dev/null +++ b/migrations/0003_indices.sql @@ -0,0 +1,38 @@ +/* foreign key indices */ + +-- this index plays a dual role as an FK index and helps with the puzzle queue +CREATE INDEX idx_coins_asset_spent_children ON coins (asset_id, spent_height, is_children_synced); +CREATE INDEX idx_coins_p2_puzzle_id ON coins(p2_puzzle_id); +CREATE INDEX idx_coins_created_height ON coins(created_height); +CREATE INDEX idx_coins_spent_height ON coins(spent_height); +CREATE INDEX idx_file_uris_file_id ON file_uris(file_id); +CREATE INDEX idx_mempool_coins_coin_id ON mempool_coins(coin_id); +CREATE INDEX idx_mempool_coins_mempool_item_id ON mempool_coins(mempool_item_id); +CREATE INDEX idx_mempool_spends_mempool_item_id ON mempool_spends(mempool_item_id); +CREATE INDEX idx_nfts_data_hash ON nfts(data_hash); +CREATE INDEX idx_nfts_metadata_hash ON nfts(metadata_hash); +CREATE INDEX idx_nfts_license_hash ON nfts(license_hash); +CREATE INDEX idx_nfts_collection_id ON nfts(collection_id); +CREATE INDEX idx_offer_assets_offer_id ON offer_assets(offer_id); +CREATE INDEX idx_offer_assets_asset_id ON offer_assets(asset_id); +CREATE INDEX idx_offer_coins_offer_id ON offer_coins(offer_id); +CREATE INDEX idx_offer_coins_coin_id ON offer_coins(coin_id); +CREATE INDEX idx_options_underlying_asset_id ON options(underlying_asset_id); +CREATE INDEX idx_options_strike_asset_id ON options(strike_asset_id); +CREATE INDEX idx_p2_options_option_asset_id ON p2_options(option_asset_id); +CREATE INDEX idx_resized_images_file_id ON resized_images(file_id); +CREATE INDEX idx_peaks ON blocks(is_peak DESC, height DESC); + +/* search and ordering indices */ +CREATE INDEX idx_assets_name ON assets(name ASC); +CREATE INDEX idx_assets_ticker ON assets(ticker); +CREATE INDEX idx_clawbacks_sender_puzzle_hash ON clawbacks(sender_puzzle_hash); +CREATE INDEX idx_clawbacks_receiver_puzzle_hash ON clawbacks(receiver_puzzle_hash); +CREATE INDEX idx_clawbacks_expiration_seconds ON clawbacks(expiration_seconds); +CREATE INDEX idx_nfts_minter_hash ON nfts(minter_hash); +CREATE INDEX idx_nfts_owner_hash ON nfts(owner_hash); +CREATE INDEX idx_nfts_edition_number ON nfts(edition_number ASC); +CREATE INDEX idx_options_underlying_coin_hash ON options(underlying_coin_hash); +CREATE INDEX idx_public_keys_key ON public_keys(key); +CREATE INDEX idx_public_keys_derivation_index_hardened ON public_keys(derivation_index, is_hardened); + diff --git a/migrations/0003_offer_fix.sql b/migrations/0003_offer_fix.sql deleted file mode 100644 index a374661b8..000000000 --- a/migrations/0003_offer_fix.sql +++ /dev/null @@ -1,37 +0,0 @@ -CREATE TABLE `offer_nfts_2` ( - `offer_id` BLOB NOT NULL, - `requested` BOOLEAN NOT NULL, - `launcher_id` BLOB NOT NULL, - `royalty_puzzle_hash` BLOB NOT NULL, - `royalty_ten_thousandths` INTEGER NOT NULL, - `name` TEXT, - `thumbnail` BLOB, - `thumbnail_mime_type` TEXT, - PRIMARY KEY (`offer_id`, `launcher_id`, `requested`), - FOREIGN KEY (`offer_id`) REFERENCES `offers`(`offer_id`) ON DELETE CASCADE -); - -CREATE TABLE `offer_cats_2` ( - `offer_id` BLOB NOT NULL, - `requested` BOOLEAN NOT NULL, - `asset_id` BLOB NOT NULL, - `amount` BLOB NOT NULL, - `royalty` BLOB NOT NULL, - `name` TEXT, - `ticker` TEXT, - `icon` TEXT, - PRIMARY KEY (`offer_id`, `asset_id`, `requested`), - FOREIGN KEY (`offer_id`) REFERENCES `offers`(`offer_id`) ON DELETE CASCADE -); - -INSERT INTO `offer_nfts_2` SELECT * FROM `offer_nfts`; -INSERT INTO `offer_cats_2` SELECT * FROM `offer_cats`; - -DROP TABLE `offer_nfts`; -DROP TABLE `offer_cats`; - -ALTER TABLE `offer_nfts_2` RENAME TO `offer_nfts`; -ALTER TABLE `offer_cats_2` RENAME TO `offer_cats`; - -CREATE INDEX `nft_offer_id` ON `offer_nfts` (`offer_id`); -CREATE INDEX `cat_offer_id` ON `offer_cats` (`offer_id`); diff --git a/migrations/0004_transactions.sql b/migrations/0004_transactions.sql deleted file mode 100644 index 94c2b0286..000000000 --- a/migrations/0004_transactions.sql +++ /dev/null @@ -1,10 +0,0 @@ -ALTER TABLE `coin_states` ADD COLUMN `kind` INTEGER NOT NULL DEFAULT 0; -CREATE INDEX `coin_kind` ON `coin_states` (`kind`); -CREATE INDEX `coin_kind_spent` ON `coin_states` (`kind`, `spent_height` ASC); - -UPDATE `coin_states` SET `kind` = 1 WHERE `coin_id` IN (SELECT `coin_id` FROM `p2_coins`); -DROP TABLE `p2_coins`; - -UPDATE `coin_states` SET `kind` = 2 WHERE `coin_id` IN (SELECT `coin_id` FROM `cat_coins`); -UPDATE `coin_states` SET `kind` = 3 WHERE `coin_id` IN (SELECT `coin_id` FROM `nft_coins`); -UPDATE `coin_states` SET `kind` = 4 WHERE `coin_id` IN (SELECT `coin_id` FROM `did_coins`); diff --git a/migrations/0004_views.sql b/migrations/0004_views.sql new file mode 100644 index 000000000..956ec5aaf --- /dev/null +++ b/migrations/0004_views.sql @@ -0,0 +1,140 @@ +CREATE VIEW wallet_coins AS +SELECT + coins.id AS coin_id, + coins.hash AS coin_hash, + coins.asset_id AS asset_id, + coins.parent_coin_hash, + coins.puzzle_hash, + coins.amount, + coins.hidden_puzzle_hash, + coins.p2_puzzle_id, + coins.spent_height, + coins.created_height, + assets.hash AS asset_hash, + assets.kind AS asset_kind, + assets.name AS asset_name, + assets.ticker AS asset_ticker, + assets.precision AS asset_precision, + assets.icon_url AS asset_icon_url, + assets.description AS asset_description, + assets.is_visible AS asset_is_visible, + assets.is_sensitive_content AS asset_is_sensitive_content, + p2_puzzles.hash AS p2_puzzle_hash, + p2_puzzles.kind AS p2_puzzle_kind, + clawbacks.sender_puzzle_hash AS clawback_sender_puzzle_hash, + clawbacks.receiver_puzzle_hash AS clawback_receiver_puzzle_hash, + clawbacks.expiration_seconds AS clawback_expiration_seconds, + sender_p2_puzzle.id AS clawback_sender_p2_puzzle_id, + receiver_p2_puzzle.id AS clawback_receiver_p2_puzzle_id, + created_blocks.timestamp AS created_timestamp, + spent_blocks.timestamp AS spent_timestamp, + ( + SELECT hash FROM mempool_items + INNER JOIN mempool_coins ON mempool_coins.mempool_item_id = mempool_items.id + WHERE mempool_coins.coin_id = coins.id + AND mempool_coins.is_input = TRUE + LIMIT 1 + ) AS mempool_item_hash, + ( + SELECT hash FROM offers + INNER JOIN offer_coins ON offer_coins.offer_id = offers.id + WHERE offer_coins.coin_id = coins.id + AND offers.status <= 1 + LIMIT 1 + ) AS offer_hash +FROM coins + INNER JOIN assets ON assets.id = coins.asset_id + INNER JOIN p2_puzzles ON p2_puzzles.id = coins.p2_puzzle_id + LEFT JOIN clawbacks ON clawbacks.p2_puzzle_id = p2_puzzles.id + LEFT JOIN p2_puzzles AS sender_p2_puzzle ON sender_p2_puzzle.hash = clawbacks.sender_puzzle_hash + LEFT JOIN p2_puzzles AS receiver_p2_puzzle ON receiver_p2_puzzle.hash = clawbacks.receiver_puzzle_hash + LEFT JOIN blocks AS created_blocks ON created_blocks.height = coins.created_height + LEFT JOIN blocks AS spent_blocks ON spent_blocks.height = coins.spent_height; + +CREATE VIEW selectable_coins AS +SELECT * +FROM wallet_coins +WHERE 1=1 + AND created_height IS NOT NULL + AND spent_height IS NULL + AND mempool_item_hash IS NULL + AND offer_hash IS NULL + AND NOT EXISTS ( + SELECT 1 FROM mempool_coins + WHERE mempool_coins.coin_id = wallet_coins.coin_id + ) + AND ( + clawback_expiration_seconds IS NULL + OR (clawback_receiver_p2_puzzle_id IS NOT NULL AND unixepoch() >= clawback_expiration_seconds) + ); + +CREATE VIEW owned_coins AS +SELECT * +FROM wallet_coins +WHERE 1=1 + AND spent_height IS NULL + AND mempool_item_hash IS NULL + AND ( + clawback_expiration_seconds IS NULL + OR (clawback_receiver_p2_puzzle_id IS NOT NULL AND unixepoch() >= clawback_expiration_seconds) + ); + +CREATE VIEW spent_coins AS +SELECT * +FROM wallet_coins +WHERE spent_height IS NOT NULL OR mempool_item_hash IS NOT NULL; + +CREATE VIEW clawback_coins AS +SELECT * +FROM wallet_coins +WHERE 1=1 + AND spent_height IS NULL + AND unixepoch() < clawback_expiration_seconds; + +CREATE VIEW spendable_coins AS +SELECT * +FROM wallet_coins +WHERE 1=1 + AND spent_height IS NULL + AND NOT EXISTS ( + SELECT 1 FROM mempool_coins + WHERE mempool_coins.coin_id = wallet_coins.coin_id + ) + AND ( + clawback_expiration_seconds IS NULL + OR (clawback_receiver_p2_puzzle_id IS NOT NULL AND unixepoch() >= clawback_expiration_seconds) + OR (clawback_sender_p2_puzzle_id IS NOT NULL AND unixepoch() < clawback_expiration_seconds) + ); + +CREATE VIEW transaction_coins AS +SELECT + blocks.height, + blocks.timestamp, + coins.hash AS coin_id, + coins.puzzle_hash, + coins.parent_coin_hash, + coins.amount, + coins.created_height = blocks.height AS is_created_in_block, + coins.spent_height = blocks.height AS is_spent_in_block, + p2_puzzles.hash AS p2_puzzle_hash, + assets.hash AS asset_hash, + assets.name AS asset_name, + assets.ticker AS asset_ticker, + assets.precision AS asset_precision, + assets.icon_url AS asset_icon_url, + assets.kind AS asset_kind, + assets.description AS asset_description, + assets.is_visible AS asset_is_visible, + assets.is_sensitive_content AS asset_is_sensitive_content +FROM blocks +LEFT JOIN coins ON coins.created_height = blocks.height OR coins.spent_height = blocks.height +INNER JOIN assets ON assets.id = coins.asset_id +LEFT JOIN p2_puzzles ON p2_puzzles.id = coins.p2_puzzle_id; + +CREATE VIEW owned_nfts AS + SELECT + owned_coins.*, nfts.minter_hash, owner_hash, metadata, metadata_updater_puzzle_hash, + royalty_puzzle_hash, royalty_basis_points, data_hash, metadata_hash, license_hash, + edition_number, edition_total, nfts.collection_id + FROM owned_coins + INNER JOIN nfts ON nfts.asset_id = owned_coins.asset_id; diff --git a/migrations/0005_nft_hash.sql b/migrations/0005_nft_hash.sql deleted file mode 100644 index e6a1b78c2..000000000 --- a/migrations/0005_nft_hash.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE `nft_uris` ADD COLUMN `hash_matches` BOOLEAN; - -UPDATE `nft_uris` SET `hash_matches` = 1; - -ALTER TABLE `nft_data` ADD COLUMN `hash_matches` BOOLEAN NOT NULL DEFAULT 1; diff --git a/migrations/0005_triggers.sql b/migrations/0005_triggers.sql new file mode 100644 index 000000000..d581379ed --- /dev/null +++ b/migrations/0005_triggers.sql @@ -0,0 +1,15 @@ +CREATE TRIGGER trigger_prevent_delete_xch_asset + BEFORE DELETE ON assets + FOR EACH ROW + WHEN OLD.id = 0 +BEGIN + SELECT RAISE(ABORT, 'Cannot delete XCH asset'); +END; + +CREATE TRIGGER trigger_prevent_delete_default_collection + BEFORE DELETE ON collections + FOR EACH ROW + WHEN OLD.id = 0 +BEGIN + SELECT RAISE(ABORT, 'Cannot delete default collection'); +END; diff --git a/migrations/0006_nft_search_indices.sql b/migrations/0006_nft_search_indices.sql deleted file mode 100644 index cb8d7178e..000000000 --- a/migrations/0006_nft_search_indices.sql +++ /dev/null @@ -1,51 +0,0 @@ --- Add index for DID-based name searches -CREATE INDEX `nft_did_name` ON `nfts` ( - `is_owned`, - `minter_did`, - `visible` DESC, - `is_pending` DESC, - `is_named` DESC, - `name` ASC, - `launcher_id` ASC -); - --- Add index for DID-based recent searches -CREATE INDEX `nft_did_recent` ON `nfts` ( - `is_owned`, - `minter_did`, - `visible` DESC, - `is_pending` DESC, - `created_height` DESC, - `launcher_id` ASC -); - --- Create FTS5 virtual table for name searching -CREATE VIRTUAL TABLE `nft_name_fts` USING fts5( - name, - nft_rowid UNINDEXED, -- Store the nfts table rowid explicitly - launcher_id UNINDEXED -); - --- Populate FTS table with existing NFT names -INSERT INTO nft_name_fts(name, nft_rowid, launcher_id) -SELECT name, rowid, launcher_id -FROM nfts -WHERE name IS NOT NULL AND name != ''; - --- Modified triggers with additional safety checks -CREATE TRIGGER nfts_ai AFTER INSERT ON nfts BEGIN - INSERT INTO nft_name_fts(name, nft_rowid, launcher_id) - SELECT NEW.name, NEW.rowid, NEW.launcher_id - WHERE NEW.name IS NOT NULL AND NEW.name != ''; -END; - -CREATE TRIGGER nfts_ad AFTER DELETE ON nfts BEGIN - DELETE FROM nft_name_fts WHERE nft_rowid = OLD.rowid; -END; - -CREATE TRIGGER nfts_au AFTER UPDATE ON nfts BEGIN - DELETE FROM nft_name_fts WHERE nft_rowid = OLD.rowid; - INSERT INTO nft_name_fts(name, nft_rowid, launcher_id) - SELECT NEW.name, NEW.rowid, NEW.launcher_id - WHERE NEW.name IS NOT NULL AND NEW.name != ''; -END; diff --git a/migrations/0007_owner_did_indices.sql b/migrations/0007_owner_did_indices.sql deleted file mode 100644 index 85d6b8548..000000000 --- a/migrations/0007_owner_did_indices.sql +++ /dev/null @@ -1,40 +0,0 @@ -DROP INDEX `nft_did_name`; -DROP INDEX `nft_did_recent`; - -CREATE INDEX `nft_minter_did_name` ON `nfts` ( - `is_owned`, - `minter_did`, - `visible` DESC, - `is_pending` DESC, - `is_named` DESC, - `name` ASC, - `launcher_id` ASC -); - -CREATE INDEX `nft_minter_did_recent` ON `nfts` ( - `is_owned`, - `minter_did`, - `visible` DESC, - `is_pending` DESC, - `created_height` DESC, - `launcher_id` ASC -); - -CREATE INDEX `nft_owner_did_name` ON `nfts` ( - `is_owned`, - `owner_did`, - `visible` DESC, - `is_pending` DESC, - `is_named` DESC, - `name` ASC, - `launcher_id` ASC -); - -CREATE INDEX `nft_owner_did_recent` ON `nfts` ( - `is_owned`, - `owner_did`, - `visible` DESC, - `is_pending` DESC, - `created_height` DESC, - `launcher_id` ASC -); diff --git a/migrations/0008_duplicate_search_fix.sql b/migrations/0008_duplicate_search_fix.sql deleted file mode 100644 index 9683bdfd2..000000000 --- a/migrations/0008_duplicate_search_fix.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Drop existing triggers to recreate them -DROP TRIGGER IF EXISTS nfts_ai; -DROP TRIGGER IF EXISTS nfts_ad; -DROP TRIGGER IF EXISTS nfts_au; - --- Clear and rebuild FTS table -DELETE FROM nft_name_fts; - --- Repopulate FTS table with existing NFT names -INSERT INTO nft_name_fts(name, nft_rowid, launcher_id) -SELECT name, rowid, launcher_id -FROM nfts -WHERE name IS NOT NULL -AND name != ''; - --- Recreate triggers with duplicate prevention -CREATE TRIGGER nfts_ai AFTER INSERT ON nfts BEGIN - -- Remove any existing entry for this launcher_id first - DELETE FROM nft_name_fts - WHERE launcher_id = NEW.launcher_id; - - -- Then insert the new entry if it has a name - INSERT INTO nft_name_fts(name, nft_rowid, launcher_id) - SELECT NEW.name, NEW.rowid, NEW.launcher_id - WHERE NEW.name IS NOT NULL - AND NEW.name != ''; -END; - -CREATE TRIGGER nfts_ad AFTER DELETE ON nfts BEGIN - DELETE FROM nft_name_fts WHERE nft_rowid = OLD.rowid; -END; - -CREATE TRIGGER nfts_au AFTER UPDATE ON nfts BEGIN - -- Remove the old entry - DELETE FROM nft_name_fts WHERE launcher_id = NEW.launcher_id; - - -- Insert the updated entry if it has a name - INSERT INTO nft_name_fts(name, nft_rowid, launcher_id) - SELECT NEW.name, NEW.rowid, NEW.launcher_id - WHERE NEW.name IS NOT NULL - AND NEW.name != ''; -END; diff --git a/migrations/0009_rust_migrations.sql b/migrations/0009_rust_migrations.sql deleted file mode 100644 index 357c4a9f0..000000000 --- a/migrations/0009_rust_migrations.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE `rust_migrations` ( - `version` INTEGER PRIMARY KEY -); - -INSERT INTO `rust_migrations` (`version`) VALUES (0); diff --git a/migrations/0010_blockinfo.sql b/migrations/0010_blockinfo.sql deleted file mode 100644 index 0eaaccb3d..000000000 --- a/migrations/0010_blockinfo.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE IF NOT EXISTS `blockinfo` ( - `height` INTEGER NOT NULL PRIMARY KEY, - `unix_time` BIGINT NOT NULL -); - -CREATE INDEX `blockinfo_index` ON `blockinfo` (`height`); - -ALTER TABLE `coin_states` ADD COLUMN `spent_unixtime` BIGINT; -ALTER TABLE `coin_states` ADD COLUMN `created_unixtime` BIGINT; diff --git a/migrations/0011_thumbnails.sql b/migrations/0011_thumbnails.sql deleted file mode 100644 index 0bda98418..000000000 --- a/migrations/0011_thumbnails.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE `nft_thumbnails` ( - `hash` BLOB NOT NULL PRIMARY KEY, - `icon` BLOB NOT NULL, - `thumbnail` BLOB NOT NULL -); - -DELETE FROM `nft_data`; -UPDATE `nft_uris` SET `checked` = 0; diff --git a/migrations/0012_drop_fts_tables.sql b/migrations/0012_drop_fts_tables.sql deleted file mode 100644 index 170b770be..000000000 --- a/migrations/0012_drop_fts_tables.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Drop triggers first to prevent any issues during table deletion -DROP TRIGGER IF EXISTS nfts_ai; -DROP TRIGGER IF EXISTS nfts_ad; -DROP TRIGGER IF EXISTS nfts_au; - --- Drop the FTS virtual table and its shadow tables -DROP TABLE IF EXISTS nft_name_fts; diff --git a/migrations/0013_nft_edition.sql b/migrations/0013_nft_edition.sql deleted file mode 100644 index 33cf231ee..000000000 --- a/migrations/0013_nft_edition.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `nfts` ADD COLUMN `edition_number` INTEGER; -ALTER TABLE `nfts` ADD COLUMN `edition_total` INTEGER; \ No newline at end of file diff --git a/migrations/0014_nft_edition_indexes.sql b/migrations/0014_nft_edition_indexes.sql deleted file mode 100644 index 3a4770190..000000000 --- a/migrations/0014_nft_edition_indexes.sql +++ /dev/null @@ -1,31 +0,0 @@ --- Update existing name-based indexes to include edition_number for better sorting - --- Drop existing name-based indexes -DROP INDEX IF EXISTS `nft_name`; -DROP INDEX IF EXISTS `nft_col_name`; -DROP INDEX IF EXISTS `nft_minter_did_name`; -DROP INDEX IF EXISTS `nft_owner_did_name`; - --- Recreate them with edition_number included -CREATE INDEX `nft_name` ON `nfts` (`is_owned`, `visible` DESC, `is_pending` DESC, `is_named` DESC, `name` ASC, `edition_number` ASC, `launcher_id` ASC); -CREATE INDEX `nft_col_name` ON `nfts` (`is_owned`, `collection_id`, `visible` DESC, `is_pending` DESC, `is_named` DESC, `name` ASC, `edition_number` ASC, `launcher_id` ASC); -CREATE INDEX `nft_minter_did_name` ON `nfts` ( - `is_owned`, - `minter_did`, - `visible` DESC, - `is_pending` DESC, - `is_named` DESC, - `name` ASC, - `edition_number` ASC, - `launcher_id` ASC -); -CREATE INDEX `nft_owner_did_name` ON `nfts` ( - `is_owned`, - `owner_did`, - `visible` DESC, - `is_pending` DESC, - `is_named` DESC, - `name` ASC, - `edition_number` ASC, - `launcher_id` ASC -); \ No newline at end of file diff --git a/package.json b/package.json index 47d4e9e78..9f3162bcf 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,11 @@ "private": true, "version": "0.0.0", "type": "module", - "packageManager": "pnpm@10.11.0", + "packageManager": "pnpm@10.13.1", "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint .", "preview": "vite preview", "prettier": "prettier --write .", "prettier:check": "prettier --check .", @@ -39,6 +39,7 @@ "@radix-ui/react-tooltip": "^1.1.6", "@react-spring/web": "^9.7.5", "@tanstack/react-table": "^8.20.5", + "@tanstack/react-virtual": "^3.13.12", "@tauri-apps/api": "^2.3.0", "@tauri-apps/plugin-barcode-scanner": "^2.2.0", "@tauri-apps/plugin-biometric": "^2.2.1", @@ -75,6 +76,7 @@ "zustand": "^4.5.5" }, "devDependencies": { + "@eslint/js": "^9.31.0", "@lingui/cli": "^5.1.2", "@lingui/format-po": "^5.1.2", "@lingui/swc-plugin": "^5.0.1", @@ -89,12 +91,14 @@ "autoprefixer": "^10.4.20", "eslint": "^8.57.0", "eslint-plugin-lingui": "^0.9.0", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.9", "postcss": "^8.4.47", "prettier": "^3.3.3", "tailwindcss": "^3.4.12", "typescript": "^5.5.4", + "typescript-eslint": "^8.36.0", "vite": "^5.3.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9d32ecc9..70368c412 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: '@tanstack/react-table': specifier: ^8.20.5 version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': + specifier: ^3.13.12 + version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tauri-apps/api': specifier: ^2.3.0 version: 2.3.0 @@ -180,6 +183,9 @@ importers: specifier: ^4.5.5 version: 4.5.5(@types/react@18.3.3)(react@18.3.1) devDependencies: + '@eslint/js': + specifier: ^9.31.0 + version: 9.31.0 '@lingui/cli': specifier: ^5.1.2 version: 5.1.2(typescript@5.5.4) @@ -222,6 +228,9 @@ importers: eslint-plugin-lingui: specifier: ^0.9.0 version: 0.9.0(eslint@8.57.0)(typescript@5.5.4) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@8.57.0) eslint-plugin-react-hooks: specifier: ^4.6.2 version: 4.6.2(eslint@8.57.0) @@ -240,6 +249,9 @@ importers: typescript: specifier: ^5.5.4 version: 5.5.4 + typescript-eslint: + specifier: ^8.36.0 + version: 8.36.0(eslint@8.57.0)(typescript@5.5.4) vite: specifier: ^5.3.5 version: 5.3.5(@types/node@22.1.0) @@ -509,6 +521,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.11.0': resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -521,6 +539,10 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/js@9.31.0': + resolution: {integrity: sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ethersproject/abstract-provider@5.7.0': resolution: {integrity: sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==} @@ -1654,10 +1676,19 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/table-core@8.20.5': resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} engines: {node: '>=12'} + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tauri-apps/api@2.0.0-rc.0': resolution: {integrity: sha512-v454Qs3REHc3Za59U+/eSmBsdmF+3NE5+76+lFDaitVqN4ZglDHENDaMARYKGJVZuxiSkzyqG0SeG7lLQjVkPA==} engines: {node: '>= 18.18', npm: '>= 6.6.0', yarn: '>= 1.19.1'} @@ -1795,6 +1826,14 @@ packages: typescript: optional: true + '@typescript-eslint/eslint-plugin@8.36.0': + resolution: {integrity: sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.36.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/parser@7.18.0': resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1805,6 +1844,19 @@ packages: typescript: optional: true + '@typescript-eslint/parser@8.36.0': + resolution: {integrity: sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/project-service@8.36.0': + resolution: {integrity: sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/scope-manager@7.18.0': resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1813,6 +1865,16 @@ packages: resolution: {integrity: sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.36.0': + resolution: {integrity: sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.36.0': + resolution: {integrity: sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1823,6 +1885,13 @@ packages: typescript: optional: true + '@typescript-eslint/type-utils@8.36.0': + resolution: {integrity: sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/types@7.18.0': resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1831,6 +1900,10 @@ packages: resolution: {integrity: sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.36.0': + resolution: {integrity: sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1846,6 +1919,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.8.0' + '@typescript-eslint/typescript-estree@8.36.0': + resolution: {integrity: sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/utils@7.18.0': resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1859,6 +1938,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' + '@typescript-eslint/utils@8.36.0': + resolution: {integrity: sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/visitor-keys@7.18.0': resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1867,6 +1953,10 @@ packages: resolution: {integrity: sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.36.0': + resolution: {integrity: sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -2004,10 +2094,42 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -2019,6 +2141,10 @@ packages: peerDependencies: postcss: ^8.1.0 + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -2080,6 +2206,18 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -2224,6 +2362,18 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -2246,6 +2396,14 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -2273,10 +2431,18 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} @@ -2307,6 +2473,38 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -2341,6 +2539,12 @@ packages: peerDependencies: eslint: '>=7' + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2353,6 +2557,10 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2441,6 +2649,10 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -2473,10 +2685,21 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -2484,10 +2707,18 @@ packages: get-port-please@3.1.2: resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2517,16 +2748,28 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} h3@1.13.0: resolution: {integrity: sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -2535,6 +2778,21 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -2567,6 +2825,10 @@ packages: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2586,23 +2848,55 @@ packages: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} engines: {node: '>=8.0.0'} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.15.0: resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} engines: {node: '>= 0.4'} + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2612,10 +2906,18 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2629,6 +2931,18 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2637,14 +2951,50 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -2653,9 +3003,16 @@ packages: resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} engines: {node: '>=18'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -2714,6 +3071,10 @@ packages: engines: {node: '>=6'} hasBin: true + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2792,6 +3153,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2905,6 +3270,30 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + ofetch@1.4.1: resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} @@ -2937,6 +3326,10 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -3044,6 +3437,10 @@ packages: pofile@1.1.4: resolution: {integrity: sha512-r6Q21sKsY1AjTVVjOuU02VYKVNQGJNQHjTIvs4dEbeuuYfxgYk/DGD2mqqq4RDaVkwdSq0VEtmQUOPe/wH8X3g==} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -3105,6 +3502,9 @@ packages: process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + pseudolocale@2.1.0: resolution: {integrity: sha512-af5fsrRvVwD+MBasBJvuDChT0KDqT0nEwD9NTgbtHJ16FKomWac9ua0z6YVNB4G9x9IOaiGWym62aby6n4tFMA==} engines: {node: '>=16.0.0'} @@ -3148,6 +3548,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3239,9 +3642,17 @@ packages: resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} engines: {node: '>= 12.13.0'} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3250,6 +3661,10 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -3279,9 +3694,21 @@ packages: resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} engines: {npm: '>=2.0.0'} + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -3301,6 +3728,18 @@ packages: engines: {node: '>=10'} hasBin: true + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3309,6 +3748,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3348,6 +3803,10 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} @@ -3363,6 +3822,25 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -3455,6 +3933,12 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -3476,6 +3960,29 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.36.0: + resolution: {integrity: sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} @@ -3487,6 +3994,10 @@ packages: uint8arrays@3.1.0: resolution: {integrity: sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==} + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -3636,6 +4147,22 @@ packages: whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3938,7 +4465,12 @@ snapshots: eslint: 8.57.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.0': {} + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.0': {} '@eslint/eslintrc@2.1.4': dependencies: @@ -3956,6 +4488,8 @@ snapshots: '@eslint/js@8.57.0': {} + '@eslint/js@9.31.0': {} + '@ethersproject/abstract-provider@5.7.0': dependencies: '@ethersproject/bignumber': 5.7.0 @@ -5122,8 +5656,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@tanstack/table-core@8.20.5': {} + '@tanstack/virtual-core@3.13.12': {} + '@tauri-apps/api@2.0.0-rc.0': {} '@tauri-apps/api@2.3.0': {} @@ -5252,6 +5794,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 8.36.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/type-utils': 8.36.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.36.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.36.0 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 @@ -5265,6 +5824,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.36.0(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.36.0 + debug: 4.3.6 + eslint: 8.57.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.36.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.5.4) + '@typescript-eslint/types': 8.36.0 + debug: 4.3.6 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -5275,6 +5855,15 @@ snapshots: '@typescript-eslint/types': 8.18.2 '@typescript-eslint/visitor-keys': 8.18.2 + '@typescript-eslint/scope-manager@8.36.0': + dependencies: + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/visitor-keys': 8.36.0 + + '@typescript-eslint/tsconfig-utils@8.36.0(typescript@5.5.4)': + dependencies: + typescript: 5.5.4 + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) @@ -5287,10 +5876,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.36.0(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.36.0(eslint@8.57.0)(typescript@5.5.4) + debug: 4.3.6 + eslint: 8.57.0 + ts-api-utils: 2.1.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@7.18.0': {} '@typescript-eslint/types@8.18.2': {} + '@typescript-eslint/types@8.36.0': {} + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.5.4)': dependencies: '@typescript-eslint/types': 7.18.0 @@ -5320,6 +5922,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.36.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/project-service': 8.36.0(typescript@5.5.4) + '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.5.4) + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/visitor-keys': 8.36.0 + debug: 4.3.6 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 2.1.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@7.18.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) @@ -5342,6 +5960,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.36.0(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.5.4) + eslint: 8.57.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -5352,6 +5981,11 @@ snapshots: '@typescript-eslint/types': 8.18.2 eslint-visitor-keys: 4.2.0 + '@typescript-eslint/visitor-keys@8.36.0': + dependencies: + '@typescript-eslint/types': 8.36.0 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.2.0': {} '@use-gesture/core@10.3.1': {} @@ -5636,8 +6270,67 @@ snapshots: dependencies: tslib: 2.7.0 + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + array-union@2.1.0: {} + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + atomic-sleep@1.0.0: {} autoprefixer@10.4.20(postcss@8.4.47): @@ -5650,6 +6343,10 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.25.0 @@ -5716,6 +6413,23 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camelcase-css@2.0.1: {} @@ -5854,6 +6568,24 @@ snapshots: csstype@3.1.3: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + date-fns@3.6.0: {} debug@4.3.6: @@ -5868,6 +6600,18 @@ snapshots: dependencies: clone: 1.0.4 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + defu@6.1.4: {} destr@2.0.3: {} @@ -5886,10 +6630,20 @@ snapshots: dlv@1.1.3: {} + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + doctrine@3.0.0: dependencies: esutils: 2.0.3 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexify@4.1.3: dependencies: end-of-stream: 1.4.4 @@ -5935,6 +6689,107 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -5984,6 +6839,28 @@ snapshots: dependencies: eslint: 8.57.0 + eslint-plugin-react@7.37.5(eslint@8.57.0): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 8.57.0 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 @@ -5993,6 +6870,8 @@ snapshots: eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} + eslint@8.57.0: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) @@ -6125,6 +7004,10 @@ snapshots: flatted@3.3.1: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.3 @@ -6149,14 +7032,49 @@ snapshots: function-bind@1.1.2: {} + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-port-please@3.1.2: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@8.0.1: {} + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -6198,6 +7116,11 @@ snapshots: dependencies: type-fest: 0.20.2 + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -6207,6 +7130,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + graphemer@1.4.0: {} h3@1.13.0: @@ -6222,10 +7147,26 @@ snapshots: uncrypto: 0.1.3 unenv: 1.10.0 + has-bigints@1.1.0: {} + has-flag@3.0.0: {} has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hash.js@1.1.7: dependencies: inherits: 2.0.4 @@ -6255,6 +7196,8 @@ snapshots: ignore@5.3.1: {} + ignore@7.0.5: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -6285,28 +7228,81 @@ snapshots: strip-ansi: 6.0.1 through: 2.3.8 + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + invariant@2.2.4: dependencies: loose-envify: 1.4.0 iron-webcrypto@1.2.1: {} + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + is-core-module@2.15.0: dependencies: hasown: 2.0.2 + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-docker@3.0.0: {} is-extglob@2.1.1: {} + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -6317,14 +7313,62 @@ snapshots: is-interactive@1.0.0: {} + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-path-inside@3.0.3: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + is-stream@3.0.0: {} + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + is-unicode-supported@0.1.0: {} + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -6333,8 +7377,19 @@ snapshots: dependencies: system-architecture: 0.1.0 + isarray@2.0.5: {} + isexe@2.0.0: {} + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -6382,6 +7437,13 @@ snapshots: json5@2.2.3: {} + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6464,6 +7526,8 @@ snapshots: dependencies: react: 18.3.1 + math-intrinsics@1.1.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6550,6 +7614,40 @@ snapshots: object-hash@3.0.0: {} + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + ofetch@1.4.1: dependencies: destr: 2.0.3 @@ -6595,6 +7693,12 @@ snapshots: os-tmpdir@1.0.2: {} + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -6695,6 +7799,8 @@ snapshots: pofile@1.1.4: {} + possible-typed-array-names@1.1.0: {} + postcss-import@15.1.0(postcss@8.4.47): dependencies: postcss: 8.4.47 @@ -6746,6 +7852,12 @@ snapshots: process-warning@1.0.0: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + pseudolocale@2.1.0: dependencies: commander: 10.0.1 @@ -6783,6 +7895,8 @@ snapshots: dependencies: react: 18.3.1 + react-is@16.13.1: {} + react-is@18.3.1: {} react-number-format@5.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -6871,8 +7985,28 @@ snapshots: real-require@0.1.0: {} + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + resolve-from@4.0.0: {} resolve@1.22.8: @@ -6881,6 +8015,12 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.15.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -6924,8 +8064,27 @@ snapshots: dependencies: tslib: 1.14.1 + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -6938,12 +8097,62 @@ snapshots: semver@7.6.3: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -6972,6 +8181,11 @@ snapshots: std-env@3.7.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + stream-shift@1.0.3: {} strict-uri-encode@2.0.0: {} @@ -6988,6 +8202,50 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -7099,6 +8357,10 @@ snapshots: dependencies: typescript: 5.5.4 + ts-api-utils@2.1.0(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + ts-interface-checker@0.1.13: {} tslib@1.14.1: {} @@ -7113,6 +8375,49 @@ snapshots: type-fest@0.21.3: {} + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.36.0(eslint@8.57.0)(typescript@5.5.4): + dependencies: + '@typescript-eslint/eslint-plugin': 8.36.0(@typescript-eslint/parser@8.36.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.36.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.36.0(eslint@8.57.0)(typescript@5.5.4) + eslint: 8.57.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + typescript@5.5.4: {} ufo@1.5.4: {} @@ -7121,6 +8426,13 @@ snapshots: dependencies: multiformats: 9.9.0 + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + uncrypto@0.1.3: {} undici-types@6.13.0: {} @@ -7221,6 +8533,47 @@ snapshots: tr46: 1.0.1 webidl-conversions: 4.0.2 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src-tauri/gen/apple/sage-tauri.xcodeproj/project.pbxproj b/src-tauri/gen/apple/sage-tauri.xcodeproj/project.pbxproj index 351d9f0bb..7c3c39ec1 100644 --- a/src-tauri/gen/apple/sage-tauri.xcodeproj/project.pbxproj +++ b/src-tauri/gen/apple/sage-tauri.xcodeproj/project.pbxproj @@ -364,7 +364,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); PRODUCT_BUNDLE_IDENTIFIER = com.rigidnetwork.sage; - PRODUCT_NAME = Sage; + PRODUCT_NAME = "Sage"; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "arm64 arm64-sim"; @@ -481,7 +481,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); PRODUCT_BUNDLE_IDENTIFIER = com.rigidnetwork.sage; - PRODUCT_NAME = Sage; + PRODUCT_NAME = "Sage"; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "arm64 arm64-sim"; diff --git a/src-tauri/src/app_state.rs b/src-tauri/src/app_state.rs index 10550ee5a..b4616d226 100644 --- a/src-tauri/src/app_state.rs +++ b/src-tauri/src/app_state.rs @@ -22,20 +22,13 @@ pub async fn initialize(app_handle: AppHandle, sage: &mut Sage) -> Result<()> { SyncEvent::Stop => ApiEvent::Stop, SyncEvent::Subscribed => ApiEvent::Subscribed, SyncEvent::DerivationIndex { .. } => ApiEvent::Derivation, - SyncEvent::TransactionEnded { + SyncEvent::TransactionFailed { transaction_id, error, - success, - } => { - if success { - ApiEvent::CoinState - } else { - ApiEvent::TransactionFailed { - transaction_id: transaction_id.to_string(), - error, - } - } - } + } => ApiEvent::TransactionFailed { + transaction_id: transaction_id.to_string(), + error, + }, // TODO: New event? SyncEvent::CoinsUpdated { .. } | SyncEvent::TransactionUpdated { .. } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index b02548ddd..bac6154ea 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,14 +1,14 @@ -use std::time::Duration; +use std::{fs, time::Duration}; use chia_wallet_sdk::utils::Address; use reqwest::StatusCode; use sage::Error; use sage_api::{wallet_connect::*, *}; use sage_api_macro::impl_endpoints_tauri; -use sage_config::{NetworkConfig, Wallet}; +use sage_config::{NetworkConfig, Wallet, WalletDefaults}; use sage_rpc::start_rpc; use serde::{Deserialize, Serialize}; -use specta::specta; +use specta::{specta, Type}; use tauri::{command, AppHandle, State}; use tokio::time::sleep; use tracing::error; @@ -102,6 +102,12 @@ pub async fn wallet_config(state: State<'_, AppState>, fingerprint: u32) -> Resu .cloned()) } +#[command] +#[specta] +pub async fn default_wallet_config(state: State<'_, AppState>) -> Result { + Ok(state.lock().await.wallet_config.defaults) +} + #[command] #[specta] pub async fn is_rpc_running(rpc_task: State<'_, RpcTask>) -> Result { @@ -205,3 +211,34 @@ pub async fn download_cni_offercode(code: String) -> Result { Ok(response) } + +#[derive(Serialize, Type)] +pub struct LogFile { + name: String, + text: String, +} + +#[command] +#[specta] +pub async fn get_logs(state: State<'_, AppState>) -> Result> { + let state = state.lock().await; + let files = fs::read_dir(state.path.join("log"))?; + + let mut log_files = Vec::new(); + + for file in files { + let file = file?; + + let name = file.file_name().to_string_lossy().to_string(); + + if !name.starts_with("app.log") { + continue; + } + + let text = fs::read_to_string(file.path())?; + + log_files.push(LogFile { name, text }); + } + + Ok(log_files) +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 73701a89d..027fb32fa 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -34,6 +34,15 @@ impl From for Error { } } +impl From for Error { + fn from(error: std::io::Error) -> Self { + Self { + kind: ErrorKind::Internal, + reason: error.to_string(), + } + } +} + impl std::error::Error for Error {} pub type Result = std::result::Result; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 40586ca20..aba64fd96 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,20 +30,19 @@ pub fn run() { commands::generate_mnemonic, commands::import_key, commands::delete_key, + commands::delete_database, commands::rename_key, commands::get_keys, commands::get_key, commands::get_secret_key, commands::send_xch, commands::bulk_send_xch, - commands::combine_xch, + commands::combine, + commands::split, commands::auto_combine_xch, - commands::split_xch, commands::send_cat, commands::bulk_send_cat, - commands::combine_cat, commands::auto_combine_cat, - commands::split_cat, commands::issue_cat, commands::create_did, commands::bulk_mint_nfts, @@ -57,15 +56,17 @@ pub fn run() { commands::submit_transaction, commands::get_sync_status, commands::get_version, + commands::get_database_stats, + commands::perform_database_maintenance, commands::check_address, commands::get_derivations, commands::get_are_coins_spendable, commands::get_spendable_coin_count, commands::get_coins_by_ids, - commands::get_xch_coins, - commands::get_cat_coins, + commands::get_coins, commands::get_cats, - commands::get_cat, + commands::get_all_cats, + commands::get_token, commands::get_dids, commands::get_minter_did_ids, commands::get_nft_collections, @@ -76,6 +77,7 @@ pub fn run() { commands::get_nft_icon, commands::get_nft_thumbnail, commands::get_pending_transactions, + commands::get_transaction, commands::get_transactions, commands::validate_address, commands::make_offer, @@ -94,10 +96,13 @@ pub fn run() { commands::set_network, commands::set_network_override, commands::wallet_config, + commands::default_wallet_config, commands::get_networks, commands::get_network, + commands::set_delta_sync, + commands::set_delta_sync_override, commands::update_cat, - commands::remove_cat, + commands::resync_cat, commands::update_did, commands::update_nft, commands::update_nft_collection, @@ -119,6 +124,7 @@ pub fn run() { commands::switch_wallet, commands::move_key, commands::download_cni_offercode, + commands::get_logs, ]) .events(collect_events![SyncEvent]); diff --git a/src/bindings.ts b/src/bindings.ts index e98e94bf7..1e8a4c030 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -26,6 +26,9 @@ async importKey(req: ImportKey) : Promise { async deleteKey(req: DeleteKey) : Promise { return await TAURI_INVOKE("delete_key", { req }); }, +async deleteDatabase(req: DeleteDatabase) : Promise { + return await TAURI_INVOKE("delete_database", { req }); +}, async renameKey(req: RenameKey) : Promise { return await TAURI_INVOKE("rename_key", { req }); }, @@ -44,30 +47,24 @@ async sendXch(req: SendXch) : Promise { async bulkSendXch(req: BulkSendXch) : Promise { return await TAURI_INVOKE("bulk_send_xch", { req }); }, -async combineXch(req: CombineXch) : Promise { - return await TAURI_INVOKE("combine_xch", { req }); +async combine(req: Combine) : Promise { + return await TAURI_INVOKE("combine", { req }); +}, +async split(req: Split) : Promise { + return await TAURI_INVOKE("split", { req }); }, async autoCombineXch(req: AutoCombineXch) : Promise { return await TAURI_INVOKE("auto_combine_xch", { req }); }, -async splitXch(req: SplitXch) : Promise { - return await TAURI_INVOKE("split_xch", { req }); -}, async sendCat(req: SendCat) : Promise { return await TAURI_INVOKE("send_cat", { req }); }, async bulkSendCat(req: BulkSendCat) : Promise { return await TAURI_INVOKE("bulk_send_cat", { req }); }, -async combineCat(req: CombineCat) : Promise { - return await TAURI_INVOKE("combine_cat", { req }); -}, async autoCombineCat(req: AutoCombineCat) : Promise { return await TAURI_INVOKE("auto_combine_cat", { req }); }, -async splitCat(req: SplitCat) : Promise { - return await TAURI_INVOKE("split_cat", { req }); -}, async issueCat(req: IssueCat) : Promise { return await TAURI_INVOKE("issue_cat", { req }); }, @@ -107,6 +104,12 @@ async getSyncStatus(req: GetSyncStatus) : Promise { async getVersion(req: GetVersion) : Promise { return await TAURI_INVOKE("get_version", { req }); }, +async getDatabaseStats(req: GetDatabaseStats) : Promise { + return await TAURI_INVOKE("get_database_stats", { req }); +}, +async performDatabaseMaintenance(req: PerformDatabaseMaintenance) : Promise { + return await TAURI_INVOKE("perform_database_maintenance", { req }); +}, async checkAddress(req: CheckAddress) : Promise { return await TAURI_INVOKE("check_address", { req }); }, @@ -122,17 +125,17 @@ async getSpendableCoinCount(req: GetSpendableCoinCount) : Promise { return await TAURI_INVOKE("get_coins_by_ids", { req }); }, -async getXchCoins(req: GetXchCoins) : Promise { - return await TAURI_INVOKE("get_xch_coins", { req }); -}, -async getCatCoins(req: GetCatCoins) : Promise { - return await TAURI_INVOKE("get_cat_coins", { req }); +async getCoins(req: GetCoins) : Promise { + return await TAURI_INVOKE("get_coins", { req }); }, async getCats(req: GetCats) : Promise { return await TAURI_INVOKE("get_cats", { req }); }, -async getCat(req: GetCat) : Promise { - return await TAURI_INVOKE("get_cat", { req }); +async getAllCats(req: GetAllCats) : Promise { + return await TAURI_INVOKE("get_all_cats", { req }); +}, +async getToken(req: GetToken) : Promise { + return await TAURI_INVOKE("get_token", { req }); }, async getDids(req: GetDids) : Promise { return await TAURI_INVOKE("get_dids", { req }); @@ -164,6 +167,9 @@ async getNftThumbnail(req: GetNftThumbnail) : Promise { async getPendingTransactions(req: GetPendingTransactions) : Promise { return await TAURI_INVOKE("get_pending_transactions", { req }); }, +async getTransaction(req: GetTransaction) : Promise { + return await TAURI_INVOKE("get_transaction", { req }); +}, async getTransactions(req: GetTransactions) : Promise { return await TAURI_INVOKE("get_transactions", { req }); }, @@ -203,32 +209,41 @@ async cancelOffers(req: CancelOffers) : Promise { async networkConfig() : Promise { return await TAURI_INVOKE("network_config"); }, -async setDiscoverPeers(req: SetDiscoverPeers) : Promise { +async setDiscoverPeers(req: SetDiscoverPeers) : Promise { return await TAURI_INVOKE("set_discover_peers", { req }); }, -async setTargetPeers(req: SetTargetPeers) : Promise { +async setTargetPeers(req: SetTargetPeers) : Promise { return await TAURI_INVOKE("set_target_peers", { req }); }, -async setNetwork(req: SetNetwork) : Promise { +async setNetwork(req: SetNetwork) : Promise { return await TAURI_INVOKE("set_network", { req }); }, -async setNetworkOverride(req: SetNetworkOverride) : Promise { +async setNetworkOverride(req: SetNetworkOverride) : Promise { return await TAURI_INVOKE("set_network_override", { req }); }, async walletConfig(fingerprint: number) : Promise { return await TAURI_INVOKE("wallet_config", { fingerprint }); }, +async defaultWalletConfig() : Promise { + return await TAURI_INVOKE("default_wallet_config"); +}, async getNetworks(req: GetNetworks) : Promise { return await TAURI_INVOKE("get_networks", { req }); }, async getNetwork(req: GetNetwork) : Promise { return await TAURI_INVOKE("get_network", { req }); }, +async setDeltaSync(req: SetDeltaSync) : Promise { + return await TAURI_INVOKE("set_delta_sync", { req }); +}, +async setDeltaSyncOverride(req: SetDeltaSyncOverride) : Promise { + return await TAURI_INVOKE("set_delta_sync_override", { req }); +}, async updateCat(req: UpdateCat) : Promise { return await TAURI_INVOKE("update_cat", { req }); }, -async removeCat(req: RemoveCat) : Promise { - return await TAURI_INVOKE("remove_cat", { req }); +async resyncCat(req: ResyncCat) : Promise { + return await TAURI_INVOKE("resync_cat", { req }); }, async updateDid(req: UpdateDid) : Promise { return await TAURI_INVOKE("update_did", { req }); @@ -248,10 +263,10 @@ async increaseDerivationIndex(req: IncreaseDerivationIndex) : Promise { return await TAURI_INVOKE("get_peers", { req }); }, -async addPeer(req: AddPeer) : Promise { +async addPeer(req: AddPeer) : Promise { return await TAURI_INVOKE("add_peer", { req }); }, -async removePeer(req: RemovePeer) : Promise { +async removePeer(req: RemovePeer) : Promise { return await TAURI_INVOKE("remove_peer", { req }); }, async filterUnlockedCoins(req: FilterUnlockedCoins) : Promise { @@ -292,6 +307,9 @@ async moveKey(fingerprint: number, index: number) : Promise { }, async downloadCniOffercode(code: string) : Promise { return await TAURI_INVOKE("download_cni_offercode", { code }); +}, +async getLogs() : Promise { + return await TAURI_INVOKE("get_logs"); } } @@ -312,10 +330,11 @@ syncEvent: "sync-event" export type AddNftUri = { nft_id: string; uri: string; fee: Amount; kind: NftUriKind; auto_submit?: boolean } export type AddPeer = { ip: string } -export type AddPeerResponse = Record export type AddressKind = "own" | "burn" | "launcher" | "offer" | "external" | "unknown" export type Amount = string | number +export type Asset = { asset_id: string | null; name: string | null; ticker: string | null; precision: number; icon_url: string | null; description: string | null; is_sensitive_content: boolean; is_visible: boolean; kind: AssetKind } export type AssetCoinType = "cat" | "did" | "nft" +export type AssetKind = "token" | "nft" | "did" | "option" export type Assets = { xch: Amount; cats: CatAmount[]; nfts: string[] } export type AssignNftsToDid = { nft_ids: string[]; did_id: string | null; fee: Amount; auto_submit?: boolean } export type AutoCombineCat = { asset_id: string; max_coins: number; max_coin_amount: Amount | null; fee: Amount; auto_submit?: boolean } @@ -324,12 +343,11 @@ export type AutoCombineXch = { max_coins: number; max_coin_amount: Amount | null export type AutoCombineXchResponse = { coin_ids: string[]; summary: TransactionSummary; coin_spends: CoinSpendJson[] } export type BulkMintNfts = { mints: NftMint[]; did_id: string; fee: Amount; auto_submit?: boolean } export type BulkMintNftsResponse = { nft_ids: string[]; summary: TransactionSummary; coin_spends: CoinSpendJson[] } -export type BulkSendCat = { asset_id: string; addresses: string[]; amount: Amount; fee: Amount; include_hint?: boolean; memos?: string[] | null; auto_submit?: boolean } -export type BulkSendXch = { addresses: string[]; amount: Amount; fee: Amount; memos?: string[] | null; auto_submit?: boolean } +export type BulkSendCat = { asset_id: string; addresses: string[]; amount: Amount; fee: Amount; include_hint?: boolean; memos?: string[]; auto_submit?: boolean } +export type BulkSendXch = { addresses: string[]; amount: Amount; fee: Amount; memos?: string[]; auto_submit?: boolean } export type CancelOffer = { offer_id: string; fee: Amount; auto_submit?: boolean } export type CancelOffers = { offer_ids: string[]; fee: Amount; auto_submit?: boolean } export type CatAmount = { asset_id: string; amount: Amount } -export type CatRecord = { asset_id: string; name: string | null; ticker: string | null; description: string | null; icon_url: string | null; visible: boolean; balance: Amount } export type ChangeMode = { mode: "default" } | /** * Reuse the first address of coins involved in the transaction @@ -346,16 +364,18 @@ export type ChangeMode = { mode: "default" } | export type CheckAddress = { address: string } export type CheckAddressResponse = { valid: boolean } export type Coin = { parent_coin_info: string; puzzle_hash: string; amount: number } +export type CoinFilterMode = "all" | "selectable" | "owned" | "spent" | "clawback" export type CoinJson = { parent_coin_info: string; puzzle_hash: string; amount: Amount } -export type CoinRecord = { coin_id: string; address: string; amount: Amount; created_height: number | null; spent_height: number | null; create_transaction_id: string | null; spend_transaction_id: string | null; offer_id: string | null; spent_timestamp: number | null; created_timestamp: number | null } -export type CoinSortMode = "coin_id" | "amount" | "created_height" | "spent_height" +export type CoinRecord = { coin_id: string; address: string; amount: Amount; transaction_id: string | null; offer_id: string | null; clawback_timestamp: number | null; created_height: number | null; spent_height: number | null; spent_timestamp: number | null; created_timestamp: number | null } +export type CoinSortMode = "coin_id" | "amount" | "created_height" | "spent_height" | "clawback_timestamp" export type CoinSpend = { coin: Coin; puzzle_reveal: string; solution: string } export type CoinSpendJson = { coin: CoinJson; puzzle_reveal: string; solution: string } -export type CombineCat = { coin_ids: string[]; fee: Amount; auto_submit?: boolean } +export type Combine = { coin_ids: string[]; fee: Amount; auto_submit?: boolean } export type CombineOffers = { offers: string[] } export type CombineOffersResponse = { offer: string } -export type CombineXch = { coin_ids: string[]; fee: Amount; auto_submit?: boolean } export type CreateDid = { name: string; fee: Amount; auto_submit?: boolean } +export type DeleteDatabase = { fingerprint: number; network: string } +export type DeleteDatabaseResponse = Record export type DeleteKey = { fingerprint: number } export type DeleteKeyResponse = Record export type DeleteOffer = { offer_id: string } @@ -371,24 +391,27 @@ export type DerivationMode = { mode: "default" } | */ { mode: "static" } export type DerivationRecord = { index: number; public_key: string; address: string } -export type DidRecord = { launcher_id: string; name: string | null; visible: boolean; coin_id: string; address: string; amount: Amount; recovery_hash: string | null; created_height: number | null; create_transaction_id: string | null } +export type DidRecord = { launcher_id: string; name: string | null; visible: boolean; coin_id: string; address: string; amount: Amount; recovery_hash: string | null; created_height: number | null } +export type EmptyResponse = Record export type Error = { kind: ErrorKind; reason: string } export type ErrorKind = "wallet" | "api" | "not_found" | "unauthorized" | "internal" | "database_migration" | "nfc" export type FilterUnlockedCoins = { coin_ids: string[] } export type FilterUnlockedCoinsResponse = { coin_ids: string[] } export type GenerateMnemonic = { use_24_words: boolean } export type GenerateMnemonicResponse = { mnemonic: string } +export type GetAllCats = Record +export type GetAllCatsResponse = { cats: TokenRecord[] } export type GetAreCoinsSpendable = { coin_ids: string[] } export type GetAreCoinsSpendableResponse = { spendable: boolean } export type GetAssetCoins = { type?: AssetCoinType | null; assetId?: string | null; includedLocked?: boolean | null; offset?: number | null; limit?: number | null } -export type GetCat = { asset_id: string } -export type GetCatCoins = { asset_id: string; offset: number; limit: number; sort_mode?: CoinSortMode; ascending?: boolean; include_spent_coins?: boolean } -export type GetCatCoinsResponse = { coins: CoinRecord[]; total: number } -export type GetCatResponse = { cat: CatRecord | null } export type GetCats = Record -export type GetCatsResponse = { cats: CatRecord[] } +export type GetCatsResponse = { cats: TokenRecord[] } +export type GetCoins = { asset_id?: string | null; offset: number; limit: number; sort_mode?: CoinSortMode; filter_mode?: CoinFilterMode; ascending?: boolean } export type GetCoinsByIds = { coin_ids: string[] } export type GetCoinsByIdsResponse = { coins: CoinRecord[] } +export type GetCoinsResponse = { coins: CoinRecord[]; total: number } +export type GetDatabaseStats = Record +export type GetDatabaseStatsResponse = { total_pages: number; free_pages: number; free_percentage: number; page_size: number; database_size_bytes: number; free_space_bytes: number; wal_pages: number } export type GetDerivations = { hardened?: boolean; offset: number; limit: number } export type GetDerivationsResponse = { derivations: DerivationRecord[]; total: number } export type GetDids = Record @@ -426,16 +449,18 @@ export type GetPendingTransactions = Record export type GetPendingTransactionsResponse = { transactions: PendingTransactionRecord[] } export type GetSecretKey = { fingerprint: number } export type GetSecretKeyResponse = { secrets: SecretKeyInfo | null } -export type GetSpendableCoinCount = { asset_id: string } +export type GetSpendableCoinCount = { asset_id: string | null } export type GetSpendableCoinCountResponse = { count: number } export type GetSyncStatus = Record -export type GetSyncStatusResponse = { balance: Amount; unit: Unit; synced_coins: number; total_coins: number; receive_address: string; burn_address: string; unhardened_derivation_index: number; hardened_derivation_index: number; checked_uris: number; total_uris: number; database_size: number } +export type GetSyncStatusResponse = { balance: Amount; unit: Unit; synced_coins: number; total_coins: number; receive_address: string; burn_address: string; unhardened_derivation_index: number; hardened_derivation_index: number; checked_files: number; total_files: number; database_size: number } +export type GetToken = { asset_id: string | null } +export type GetTokenResponse = { token: TokenRecord | null } +export type GetTransaction = { height: number } +export type GetTransactionResponse = { transaction: TransactionRecord | null } export type GetTransactions = { offset: number; limit: number; ascending: boolean; find_value: string | null } export type GetTransactionsResponse = { transactions: TransactionRecord[]; total: number } export type GetVersion = Record export type GetVersionResponse = { version: string } -export type GetXchCoins = { offset: number; limit: number; sort_mode?: CoinSortMode; ascending?: boolean; include_spent_coins?: boolean } -export type GetXchCoinsResponse = { coins: CoinRecord[]; total: number } export type ImportKey = { name: string; key: string; derivation_index?: number; save_secrets?: boolean; login?: boolean } export type ImportKeyResponse = { fingerprint: number } export type ImportOffer = { offer: string } @@ -447,6 +472,7 @@ export type IssueCat = { name: string; ticker: string; amount: Amount; fee: Amou export type KeyInfo = { name: string; fingerprint: number; public_key: string; kind: KeyKind; has_secrets: boolean; network_id: string } export type KeyKind = "bls" export type LineageProof = { parentName: string | null; innerPuzzleHash: string | null; amount: number | null } +export type LogFile = { name: string; text: string } export type Login = { fingerprint: number } export type LoginResponse = Record export type Logout = Record @@ -460,42 +486,39 @@ export type NetworkList = { networks: Network[] } export type NftCollectionRecord = { collection_id: string; did_id: string; metadata_collection_id: string; visible: boolean; name: string | null; icon: string | null } export type NftData = { blob: string | null; mime_type: string | null; hash_matches: boolean; metadata_json: string | null; metadata_hash_matches: boolean } export type NftMint = { address?: string | null; edition_number?: number | null; edition_total?: number | null; data_hash?: string | null; data_uris?: string[]; metadata_hash?: string | null; metadata_uris?: string[]; license_hash?: string | null; license_uris?: string[]; royalty_address?: string | null; royalty_ten_thousandths?: number } -export type NftRecord = { launcher_id: string; collection_id: string | null; collection_name: string | null; minter_did: string | null; owner_did: string | null; visible: boolean; sensitive_content: boolean; name: string | null; created_height: number | null; coin_id: string; address: string; royalty_address: string; royalty_ten_thousandths: number; data_uris: string[]; data_hash: string | null; metadata_uris: string[]; metadata_hash: string | null; license_uris: string[]; license_hash: string | null; edition_number: number | null; edition_total: number | null } +export type NftRecord = { launcher_id: string; collection_id: string | null; collection_name: string | null; minter_did: string | null; owner_did: string | null; visible: boolean; sensitive_content: boolean; name: string | null; created_height: number | null; coin_id: string; address: string; royalty_address: string; royalty_ten_thousandths: number; data_uris: string[]; data_hash: string | null; metadata_uris: string[]; metadata_hash: string | null; license_uris: string[]; license_hash: string | null; edition_number: number | null; edition_total: number | null; icon_url: string | null } +export type NftRoyalty = { royalty_address: string; royalty_basis_points: number } export type NftSortMode = "name" | "recent" export type NftUriKind = "data" | "metadata" | "license" export type NormalizeDids = { did_ids: string[]; fee: Amount; auto_submit?: boolean } -export type OfferAssets = { xch: OfferXch; cats: { [key in string]: OfferCat }; nfts: { [key in string]: OfferNft } } -export type OfferCat = { amount: Amount; royalty: Amount; name: string | null; ticker: string | null; icon_url: string | null } -export type OfferNft = { icon: string | null; name: string | null; royalty_ten_thousandths: number; royalty_address: string } -export type OfferRecord = { offer_id: string; offer: string; status: OfferRecordStatus; creation_date: string; summary: OfferSummary } -export type OfferRecordStatus = "active" | "completed" | "cancelled" | "expired" -export type OfferSummary = { fee: Amount; maker: OfferAssets; taker: OfferAssets; expiration_height: number | null; expiration_timestamp: number | null } -export type OfferXch = { amount: Amount; royalty: Amount } +export type OfferAsset = { asset: Asset; amount: Amount; royalty: Amount; nft_royalty: NftRoyalty | null } +export type OfferRecord = { offer_id: string; offer: string; status: OfferRecordStatus; creation_timestamp: number; summary: OfferSummary } +export type OfferRecordStatus = "pending" | "active" | "completed" | "cancelled" | "expired" +export type OfferSummary = { fee: Amount; maker: OfferAsset[]; taker: OfferAsset[]; expiration_height: number | null; expiration_timestamp: number | null } export type PeerRecord = { ip_addr: string; port: number; peak_height: number; user_managed: boolean } export type PendingTransactionRecord = { transaction_id: string; fee: Amount; submitted_at: string | null } +export type PerformDatabaseMaintenance = { force_vacuum: boolean } +export type PerformDatabaseMaintenanceResponse = { vacuum_duration_ms: number; analyze_duration_ms: number; wal_checkpoint_duration_ms: number; total_duration_ms: number; pages_vacuumed: number; wal_pages_checkpointed: number } export type RedownloadNft = { nft_id: string } export type RedownloadNftResponse = Record -export type RemoveCat = { asset_id: string } -export type RemoveCatResponse = Record export type RemovePeer = { ip: string; ban: boolean } -export type RemovePeerResponse = Record export type RenameKey = { fingerprint: number; name: string } export type RenameKeyResponse = Record -export type Resync = { fingerprint: number; delete_offer_files?: boolean; delete_unhardened_derivations?: boolean; delete_hardened_derivations?: boolean; delete_blockinfo?: boolean } +export type Resync = { fingerprint: number; delete_coins?: boolean; delete_assets?: boolean; delete_files?: boolean; delete_offers?: boolean; delete_addresses?: boolean; delete_blocks?: boolean } +export type ResyncCat = { asset_id: string } +export type ResyncCatResponse = Record export type ResyncResponse = Record export type SecretKeyInfo = { mnemonic: string | null; secret_key: string } -export type SendCat = { asset_id: string; address: string; amount: Amount; fee: Amount; include_hint?: boolean; memos?: string[] | null; auto_submit?: boolean } +export type SendCat = { asset_id: string; address: string; amount: Amount; fee: Amount; include_hint?: boolean; memos?: string[]; clawback?: number | null; auto_submit?: boolean } export type SendTransactionImmediately = { spend_bundle: SpendBundle } export type SendTransactionImmediatelyResponse = { status: number; error: string | null } -export type SendXch = { address: string; amount: Amount; fee: Amount; memos?: string[] | null; auto_submit?: boolean } +export type SendXch = { address: string; amount: Amount; fee: Amount; memos?: string[]; clawback?: number | null; auto_submit?: boolean } +export type SetDeltaSync = { delta_sync: boolean } +export type SetDeltaSyncOverride = { fingerprint: number; delta_sync: boolean | null } export type SetDiscoverPeers = { discover_peers: boolean } -export type SetDiscoverPeersResponse = Record export type SetNetwork = { name: string } export type SetNetworkOverride = { fingerprint: number; name: string | null } -export type SetNetworkOverrideResponse = Record -export type SetNetworkResponse = Record export type SetTargetPeers = { target_peers: number } -export type SetTargetPeersResponse = Record export type SignCoinSpends = { coin_spends: CoinSpendJson[]; auto_submit?: boolean; partial?: boolean } export type SignCoinSpendsResponse = { spend_bundle: SpendBundleJson } export type SignMessageByAddress = { message: string; address: string } @@ -505,23 +528,23 @@ export type SignMessageWithPublicKeyResponse = { signature: string } export type SpendBundle = { coin_spends: CoinSpend[]; aggregated_signature: string } export type SpendBundleJson = { coin_spends: CoinSpendJson[]; aggregated_signature: string } export type SpendableCoin = { coin: Coin; coinName: string; puzzle: string; confirmedBlockIndex: number; locked: boolean; lineageProof: LineageProof | null } -export type SplitCat = { coin_ids: string[]; output_count: number; fee: Amount; auto_submit?: boolean } -export type SplitXch = { coin_ids: string[]; output_count: number; fee: Amount; auto_submit?: boolean } +export type Split = { coin_ids: string[]; output_count: number; fee: Amount; auto_submit?: boolean } export type SubmitTransaction = { spend_bundle: SpendBundleJson } export type SubmitTransactionResponse = Record export type SyncEvent = { type: "start"; ip: string } | { type: "stop" } | { type: "subscribed" } | { type: "derivation" } | { type: "coin_state" } | { type: "transaction_failed"; transaction_id: string; error: string | null } | { type: "puzzle_batch_synced" } | { type: "cat_info" } | { type: "did_info" } | { type: "nft_data" } -export type TakeOffer = { offer: string; fee: Amount; auto_submit?: boolean; auto_import?: boolean } +export type TakeOffer = { offer: string; fee: Amount; auto_submit?: boolean } export type TakeOfferResponse = { summary: TransactionSummary; spend_bundle: SpendBundleJson; transaction_id: string } -export type TransactionCoin = ({ type: "unknown" } | { type: "xch" } | { type: "launcher" } | { type: "cat"; asset_id: string; name: string | null; ticker: string | null; icon_url: string | null } | { type: "did"; launcher_id: string; name: string | null } | { type: "nft"; launcher_id: string; icon: string | null; name: string | null }) & { coin_id: string; amount: Amount; address: string | null; address_kind: AddressKind } -export type TransactionInput = ({ type: "unknown" } | { type: "xch" } | { type: "launcher" } | { type: "cat"; asset_id: string; name: string | null; ticker: string | null; icon_url: string | null } | { type: "did"; launcher_id: string; name: string | null } | { type: "nft"; launcher_id: string; icon: string | null; name: string | null }) & { coin_id: string; amount: Amount; address: string; outputs: TransactionOutput[] } +export type TokenRecord = { asset_id: string | null; name: string | null; ticker: string | null; precision: number; description: string | null; icon_url: string | null; visible: boolean; balance: Amount } +export type TransactionCoinRecord = { coin_id: string; amount: Amount; address: string | null; address_kind: AddressKind; asset: Asset } +export type TransactionInput = { coin_id: string; amount: Amount; address: string; asset: Asset | null; outputs: TransactionOutput[] } export type TransactionOutput = { coin_id: string; amount: Amount; address: string; receiving: boolean; burning: boolean } -export type TransactionRecord = { height: number; timestamp: number | null; spent: TransactionCoin[]; created: TransactionCoin[] } +export type TransactionRecord = { height: number; timestamp: number | null; spent: TransactionCoinRecord[]; created: TransactionCoinRecord[] } export type TransactionResponse = { summary: TransactionSummary; coin_spends: CoinSpendJson[] } export type TransactionSummary = { fee: Amount; inputs: TransactionInput[] } -export type TransferDids = { did_ids: string[]; address: string; fee: Amount; auto_submit?: boolean } -export type TransferNfts = { nft_ids: string[]; address: string; fee: Amount; auto_submit?: boolean } +export type TransferDids = { did_ids: string[]; address: string; fee: Amount; clawback?: number | null; auto_submit?: boolean } +export type TransferNfts = { nft_ids: string[]; address: string; fee: Amount; clawback?: number | null; auto_submit?: boolean } export type Unit = { ticker: string; decimals: number } -export type UpdateCat = { record: CatRecord } +export type UpdateCat = { record: TokenRecord } export type UpdateCatResponse = Record export type UpdateDid = { did_id: string; name: string | null; visible: boolean } export type UpdateDidResponse = Record @@ -533,7 +556,8 @@ export type ViewCoinSpends = { coin_spends: CoinSpendJson[] } export type ViewCoinSpendsResponse = { summary: TransactionSummary } export type ViewOffer = { offer: string } export type ViewOfferResponse = { offer: OfferSummary } -export type Wallet = { name?: string; fingerprint: number; change: ChangeMode; derivation: DerivationMode; network?: string | null } +export type Wallet = { name: string; fingerprint: number; change: ChangeMode; derivation: DerivationMode; network?: string | null; delta_sync: boolean | null } +export type WalletDefaults = { change: ChangeMode; derivation: DerivationMode; delta_sync: boolean } /** tauri-specta globals **/ diff --git a/src/components/AddressList.tsx b/src/components/AddressList.tsx index 19eaafe6e..3789e09dd 100644 --- a/src/components/AddressList.tsx +++ b/src/components/AddressList.tsx @@ -1,6 +1,7 @@ import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import { ColumnDef } from '@tanstack/react-table'; +import { ColumnDef, Row } from '@tanstack/react-table'; +import { useMemo } from 'react'; import { toast } from 'react-toastify'; import { DerivationRecord } from '../bindings'; import { CopyButton } from './CopyButton'; @@ -8,6 +9,56 @@ import { FormattedAddress } from './FormattedAddress'; import { SimplePagination } from './SimplePagination'; import { DataTable } from './ui/data-table'; +// Extract column header and cell components +const IndexHeader = () => Index; + +const IndexCell = ({ row }: { row: Row }) => + row.original.index; + +const AddressHeader = () => Address; + +const AddressCell = ({ row }: { row: Row }) => { + const address = row.getValue('address') as string; + return ( +
+
+ +
+
+ { + toast.success(t`Address copied to clipboard`); + }} + /> +
+
+ ); +}; + +const PublicKeyHeader = () => Public Key; + +const PublicKeyCell = ({ row }: { row: Row }) => { + const publicKey = row.getValue('public_key') as string; + return ( +
+
+ {publicKey} +
+
+ { + toast.success(t`Public key copied to clipboard`); + }} + /> +
+
+ ); +}; + export interface AddressListProps { derivations: DerivationRecord[]; currentPage: number; @@ -25,63 +76,30 @@ export default function AddressList(props: AddressListProps) { totalDerivations, } = props; - const columns: ColumnDef[] = [ - { - id: 'index', - accessorFn: (row) => row.index, - size: 40, - header: () => Index, - cell: (info) => info.row.original.index, - }, - { - id: 'address', - accessorFn: (row) => row.address, - header: () => Address, - cell: (info) => { - const address = info.getValue() as string; - return ( -
-
- -
-
- { - toast.success(t`Address copied to clipboard`); - }} - /> -
-
- ); + const columns: ColumnDef[] = useMemo( + () => [ + { + id: 'index', + accessorFn: (row) => row.index, + size: 40, + header: IndexHeader, + cell: IndexCell, }, - }, - { - id: 'public_key', - accessorFn: (row) => row.public_key, - header: () => Public Key, - cell: (info) => { - const publicKey = info.getValue() as string; - return ( -
-
- {publicKey} -
-
- { - toast.success(t`Public key copied to clipboard`); - }} - /> -
-
- ); + { + id: 'address', + accessorFn: (row) => row.address, + header: AddressHeader, + cell: AddressCell, }, - }, - ]; + { + id: 'public_key', + accessorFn: (row) => row.public_key, + header: PublicKeyHeader, + cell: PublicKeyCell, + }, + ], + [], + ); return (
diff --git a/src/components/AdvancedTransactionSummary.tsx b/src/components/AdvancedTransactionSummary.tsx index 2fceba36a..95aed771f 100644 --- a/src/components/AdvancedTransactionSummary.tsx +++ b/src/components/AdvancedTransactionSummary.tsx @@ -1,12 +1,12 @@ -import { toDecimal, fromMojos } from '@/lib/utils'; +import { fromMojos, toDecimal } from '@/lib/utils'; import { useWalletState } from '@/state'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import BigNumber from 'bignumber.js'; import { BadgeMinus, BadgePlus, BoxIcon, ForwardIcon } from 'lucide-react'; import { TransactionSummary, Unit } from '../bindings'; -import { Badge } from './ui/badge'; import { formatNumber } from '../i18n'; +import { Badge } from './ui/badge'; interface SpentCoin { sort: number; @@ -50,9 +50,9 @@ export function AdvancedTransactionSummary({
{spent .sort((a, b) => a.sort - b.sort) - .map((spent, i) => ( + .map((spent) => (
@@ -94,9 +94,9 @@ export function AdvancedTransactionSummary({ )} {created .sort((a, b) => a.sort - b.sort) - .map((created, i) => ( + .map((created) => (
@@ -132,7 +132,7 @@ export function calculateTransaction( const created: CreatedCoin[] = []; for (const input of summary.inputs || []) { - if (input.type === 'xch') { + if (input.asset && !input.asset.asset_id) { spent.push({ badge: 'Chia', label: `${formatNumber({ @@ -166,11 +166,11 @@ export function calculateTransaction( } } - if (input.type === 'cat') { - const ticker = input.ticker || 'CAT'; + if (input.asset?.kind === 'token' && input.asset.asset_id) { + const ticker = input.asset.ticker || 'CAT'; spent.push({ - badge: `CAT ${input.name || input.asset_id}`, + badge: `CAT ${input.asset.name ?? input.asset.ticker ?? input.asset.asset_id}`, label: `${formatNumber({ value: fromMojos(input.amount, 3), minimumFractionDigits: 0, @@ -186,7 +186,7 @@ export function calculateTransaction( } created.push({ - badge: `CAT ${input.name || input.asset_id}`, + badge: `CAT ${input.asset.name ?? input.asset.ticker ?? input.asset.asset_id}`, label: `${formatNumber({ value: fromMojos(output.amount, 3), minimumFractionDigits: 0, @@ -202,7 +202,7 @@ export function calculateTransaction( } } - if (input.type === 'did') { + if (input.asset?.kind === 'did') { if ( !summary.inputs .map((i) => i.outputs) @@ -211,7 +211,7 @@ export function calculateTransaction( ) { spent.push({ badge: t`Profile`, - label: input.name || t`Unnamed`, + label: input.asset.name ?? t`Unnamed`, coinId: input.coin_id, sort: 3, }); @@ -225,7 +225,7 @@ export function calculateTransaction( if (BigNumber(output.amount).mod(2).eq(1)) { created.push({ badge: t`Profile`, - label: input.name || t`Unnamed`, + label: input.asset.name ?? t`Unnamed`, address: output.burning ? t`Permanently Burned` : output.receiving @@ -237,7 +237,7 @@ export function calculateTransaction( } } - if (input.type === 'nft') { + if (input.asset?.kind === 'nft') { if ( !summary.inputs .map((i) => i.outputs) @@ -246,7 +246,7 @@ export function calculateTransaction( ) { spent.push({ badge: 'NFT', - label: input.name || t`Unknown`, + label: input.asset.name ?? t`Unknown`, coinId: input.coin_id, sort: 4, }); @@ -260,7 +260,7 @@ export function calculateTransaction( if (BigNumber(output.amount).mod(2).isEqualTo(1)) { created.push({ badge: 'NFT', - label: input.name || t`Unknown`, + label: input.asset.name ?? t`Unknown`, address: output.burning ? t`Permanently Burned` : output.receiving diff --git a/src/components/AmountCell.tsx b/src/components/AmountCell.tsx index 67d65a9a4..72a0e7091 100644 --- a/src/components/AmountCell.tsx +++ b/src/components/AmountCell.tsx @@ -1,18 +1,16 @@ -import { Amount, AssetCoinType } from '@/bindings'; +import { Amount, AssetKind } from '@/bindings'; import { fromMojos } from '@/lib/utils'; -import { useWalletState } from '@/state'; import BigNumber from 'bignumber.js'; import { NumberFormat } from './NumberFormat'; interface AmountCellProps { amount: Amount; - type: AssetCoinType; + assetKind: AssetKind; + precision: number; } -export function AmountCell({ amount, type }: AmountCellProps) { - const walletState = useWalletState(); +export function AmountCell({ amount, assetKind, precision }: AmountCellProps) { const amountNum = BigNumber(amount); - const decimals = type === 'cat' ? 3 : walletState.sync.unit.decimals; return (
@@ -24,9 +22,9 @@ export function AmountCell({ amount, type }: AmountCellProps) { ? 'text-green-600' : 'text-red-600' } - aria-label={`${fromMojos(amount, decimals)} ${type.toUpperCase()}`} + aria-label={`${fromMojos(amount, precision)} ${assetKind}`} > - {type === 'nft' || type === 'did' ? ( + {assetKind === 'nft' || assetKind === 'did' ? ( amountNum.eq(0) ? ( 'Edited' ) : amountNum.gt(0) ? ( @@ -36,9 +34,9 @@ export function AmountCell({ amount, type }: AmountCellProps) { ) ) : ( )} diff --git a/src/components/AssetIcon.tsx b/src/components/AssetIcon.tsx new file mode 100644 index 000000000..42f3ab730 --- /dev/null +++ b/src/components/AssetIcon.tsx @@ -0,0 +1,47 @@ +import { AssetKind } from '@/bindings'; +import { Coins, Image, User } from 'lucide-react'; + +export interface AssetIconProps { + iconUrl?: string | null; + name?: string | null; + kind: AssetKind; + size?: 'sm' | 'md' | 'lg'; + className?: string; +} + +const sizeClasses = { + sm: 'w-6 h-6', + md: 'w-8 h-8', + lg: 'w-10 h-10', +}; + +export function AssetIcon({ + iconUrl, + kind, + size = 'sm', + className = '', +}: AssetIconProps) { + const sizeClass = sizeClasses[size]; + const imgClasses = `${sizeClass} rounded object-cover`; + + if (iconUrl) { + return ( + + ); + } + + const iconClasses = `${sizeClass} rounded stroke-1`; + return kind === 'token' ? ( +