create-dubhe 1.2.0-pre.98 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +16 -16
  4. package/templates/101/sui-template/docker-compose.testnet.yml +73 -0
  5. package/templates/101/sui-template/packages/client/src/app/page.tsx +545 -41
  6. package/templates/101/sui-template/packages/client/src/app/providers.tsx +12 -4
  7. package/templates/101/sui-template/packages/contracts/Pub.localnet.toml +4 -4
  8. package/templates/101/sui-template/packages/contracts/counter.lock.json +20 -0
  9. package/templates/101/sui-template/packages/contracts/deployment.ts +8 -3
  10. package/templates/101/sui-template/packages/contracts/dubhe.config.json +59 -13
  11. package/templates/101/sui-template/packages/contracts/dubhe.config.ts +1 -4
  12. package/templates/101/sui-template/packages/contracts/metadata.json +1015 -68
  13. package/templates/101/sui-template/packages/contracts/package.json +7 -4
  14. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/dapp_key.move +45 -31
  15. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/error.move +8 -0
  16. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/genesis.move +30 -13
  17. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/init_test.move +19 -14
  18. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2.move +198 -0
  19. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2withkey.move +90 -0
  20. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/value.move +30 -20
  21. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
  22. package/templates/101/sui-template/packages/contracts/src/counter/sources/scripts/deploy_hook.move +6 -11
  23. package/templates/101/sui-template/packages/contracts/src/counter/sources/systems/counter.move +23 -11
  24. package/templates/101/sui-template/packages/contracts/src/counter/sources/tests/counter.move +15 -14
  25. package/templates/101/sui-template/packages/contracts/src/dubhe/Move.lock +33 -30
  26. package/templates/101/sui-template/packages/contracts/src/dubhe/Move.toml +8 -3
  27. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/dapp_key.move +45 -31
  28. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/error.move +170 -0
  29. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/genesis.move +16 -13
  30. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/init_test.move +8 -14
  31. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/account_key.move +1 -3
  32. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/dapp_service.move +1776 -137
  33. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/data_key.move +1 -20
  34. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/events.move +864 -25
  35. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/scripts/deploy_hook.move +15 -16
  36. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/scripts/migrate.move +0 -3
  37. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/address_system.move +25 -17
  38. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/dapp_system.move +2802 -343
  39. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/utils.move +1 -1
  40. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/address.move +173 -55
  41. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/dapp_test.move +769 -0
  42. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/fee_test.move +2965 -0
  43. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/integration_test.move +303 -0
  44. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/listing_test.move +1422 -0
  45. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/scene_session_test.move +269 -0
  46. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/session_test.move +624 -0
  47. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/storage_test.move +700 -0
  48. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/typed_object_test.move +177 -0
  49. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/typed_scene_test.move +452 -0
  50. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/user_storage_test.move +160 -0
  51. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/utils/math.move +4 -3
  52. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/utils/type_info.move +2 -2
  53. package/templates/contract/sui-template/dubhe.config.ts +0 -1
  54. package/templates/contract/sui-template/package.json +3 -3
  55. package/templates/contract/sui-template/src/dubhe/Move.lock +33 -30
  56. package/templates/contract/sui-template/src/dubhe/Move.toml +8 -3
  57. package/templates/contract/sui-template/src/dubhe/sources/codegen/dapp_key.move +45 -31
  58. package/templates/contract/sui-template/src/dubhe/sources/codegen/error.move +170 -0
  59. package/templates/contract/sui-template/src/dubhe/sources/codegen/genesis.move +16 -13
  60. package/templates/contract/sui-template/src/dubhe/sources/codegen/init_test.move +8 -14
  61. package/templates/contract/sui-template/src/dubhe/sources/core/account_key.move +1 -3
  62. package/templates/contract/sui-template/src/dubhe/sources/core/dapp_service.move +1776 -137
  63. package/templates/contract/sui-template/src/dubhe/sources/core/data_key.move +1 -20
  64. package/templates/contract/sui-template/src/dubhe/sources/core/events.move +864 -25
  65. package/templates/contract/sui-template/src/dubhe/sources/scripts/deploy_hook.move +15 -16
  66. package/templates/contract/sui-template/src/dubhe/sources/scripts/migrate.move +0 -3
  67. package/templates/contract/sui-template/src/dubhe/sources/systems/address_system.move +25 -17
  68. package/templates/contract/sui-template/src/dubhe/sources/systems/dapp_system.move +2802 -343
  69. package/templates/contract/sui-template/src/dubhe/sources/systems/utils.move +1 -1
  70. package/templates/contract/sui-template/src/dubhe/sources/tests/address.move +173 -55
  71. package/templates/contract/sui-template/src/dubhe/sources/tests/dapp_test.move +769 -0
  72. package/templates/contract/sui-template/src/dubhe/sources/tests/fee_test.move +2965 -0
  73. package/templates/contract/sui-template/src/dubhe/sources/tests/integration_test.move +303 -0
  74. package/templates/contract/sui-template/src/dubhe/sources/tests/listing_test.move +1422 -0
  75. package/templates/contract/sui-template/src/dubhe/sources/tests/scene_session_test.move +269 -0
  76. package/templates/contract/sui-template/src/dubhe/sources/tests/session_test.move +624 -0
  77. package/templates/contract/sui-template/src/dubhe/sources/tests/storage_test.move +700 -0
  78. package/templates/contract/sui-template/src/dubhe/sources/tests/typed_object_test.move +177 -0
  79. package/templates/contract/sui-template/src/dubhe/sources/tests/typed_scene_test.move +452 -0
  80. package/templates/contract/sui-template/src/dubhe/sources/tests/user_storage_test.move +160 -0
  81. package/templates/contract/sui-template/src/dubhe/sources/utils/math.move +4 -3
  82. package/templates/contract/sui-template/src/dubhe/sources/utils/type_info.move +2 -2
  83. package/templates/contract/sui-template/src/template/Move.toml +12 -0
  84. package/templates/contract/sui-template/src/template/sources/codegen/dapp_key.move +47 -0
  85. package/templates/contract/sui-template/src/template/sources/codegen/genesis.move +34 -0
  86. package/templates/contract/sui-template/src/template/sources/codegen/init_test.move +23 -0
  87. package/templates/contract/sui-template/src/template/sources/codegen/user_storage_init.move +27 -0
  88. package/templates/contract/sui-template/src/template/sources/scripts/deploy_hook.move +13 -0
  89. package/templates/contract/sui-template/src/template/sources/scripts/migrate.move +7 -0
  90. package/templates/nextjs/sui-card-duel/.gitignore_ +51 -0
  91. package/templates/nextjs/sui-card-duel/README.md +213 -0
  92. package/templates/nextjs/sui-card-duel/docker-compose.yml +34 -0
  93. package/templates/nextjs/sui-card-duel/mprocs.yaml +17 -0
  94. package/templates/nextjs/sui-card-duel/package.json +43 -0
  95. package/templates/nextjs/sui-card-duel/packages/client/.gitignore_ +48 -0
  96. package/templates/nextjs/sui-card-duel/packages/client/.prettierignore +1 -0
  97. package/templates/nextjs/sui-card-duel/packages/client/.prettierrc.json +8 -0
  98. package/templates/nextjs/sui-card-duel/packages/client/eslint.config.mjs +22 -0
  99. package/templates/nextjs/sui-card-duel/packages/client/next.config.mjs +8 -0
  100. package/templates/nextjs/sui-card-duel/packages/client/package.json +47 -0
  101. package/templates/nextjs/sui-card-duel/packages/client/postcss.config.mjs +8 -0
  102. package/templates/nextjs/sui-card-duel/packages/client/src/app/admin/ArenaPanel.tsx +307 -0
  103. package/templates/nextjs/sui-card-duel/packages/client/src/app/admin/page.tsx +622 -0
  104. package/templates/nextjs/sui-card-duel/packages/client/src/app/brawl/[id]/page.tsx +559 -0
  105. package/templates/nextjs/sui-card-duel/packages/client/src/app/collection/page.tsx +202 -0
  106. package/templates/nextjs/sui-card-duel/packages/client/src/app/components/CardView.tsx +66 -0
  107. package/templates/nextjs/sui-card-duel/packages/client/src/app/components/GameShell.tsx +294 -0
  108. package/templates/nextjs/sui-card-duel/packages/client/src/app/components/icons.tsx +101 -0
  109. package/templates/nextjs/sui-card-duel/packages/client/src/app/duel/[id]/page.tsx +477 -0
  110. package/templates/nextjs/sui-card-duel/packages/client/src/app/favicon.ico +0 -0
  111. package/templates/nextjs/sui-card-duel/packages/client/src/app/globals.css +85 -0
  112. package/templates/nextjs/sui-card-duel/packages/client/src/app/hooks/useGame.ts +313 -0
  113. package/templates/nextjs/sui-card-duel/packages/client/src/app/layout.tsx +24 -0
  114. package/templates/nextjs/sui-card-duel/packages/client/src/app/leaderboard/page.tsx +123 -0
  115. package/templates/nextjs/sui-card-duel/packages/client/src/app/lib/game.ts +77 -0
  116. package/templates/nextjs/sui-card-duel/packages/client/src/app/lib/scenes.ts +226 -0
  117. package/templates/nextjs/sui-card-duel/packages/client/src/app/market/page.tsx +418 -0
  118. package/templates/nextjs/sui-card-duel/packages/client/src/app/page.tsx +427 -0
  119. package/templates/nextjs/sui-card-duel/packages/client/src/app/providers.tsx +69 -0
  120. package/templates/nextjs/sui-card-duel/packages/client/tailwind.config.ts +21 -0
  121. package/templates/nextjs/sui-card-duel/packages/client/tsconfig.json +30 -0
  122. package/templates/nextjs/sui-card-duel/packages/contracts/.gitignore_ +51 -0
  123. package/templates/nextjs/sui-card-duel/packages/contracts/.prettierignore +1 -0
  124. package/templates/nextjs/sui-card-duel/packages/contracts/.prettierrc.json +8 -0
  125. package/templates/nextjs/sui-card-duel/packages/contracts/Dockerfile +5 -0
  126. package/templates/nextjs/sui-card-duel/packages/contracts/Pub.localnet.toml +13 -0
  127. package/templates/nextjs/sui-card-duel/packages/contracts/card_duel.lock.json +66 -0
  128. package/templates/nextjs/sui-card-duel/packages/contracts/docker-compose.yml +34 -0
  129. package/templates/nextjs/sui-card-duel/packages/contracts/dubhe.config.ts +150 -0
  130. package/templates/nextjs/sui-card-duel/packages/contracts/logs/combined.log +31383 -0
  131. package/templates/nextjs/sui-card-duel/packages/contracts/package.json +49 -0
  132. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/config.ts +17 -0
  133. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-brawl.ts +270 -0
  134. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-duel.ts +272 -0
  135. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-lobby.ts +177 -0
  136. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-market.ts +219 -0
  137. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-session.ts +342 -0
  138. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/query-dapp-revenue.ts +95 -0
  139. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/seed.ts +92 -0
  140. package/templates/nextjs/sui-card-duel/packages/contracts/scripts/withdraw-dapp-revenue.ts +56 -0
  141. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/Move.toml +15 -0
  142. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/dapp_key.move +46 -0
  143. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/card_kind.move +47 -0
  144. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/match_state.move +37 -0
  145. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/rarity.move +37 -0
  146. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/error.move +126 -0
  147. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/genesis.move +33 -0
  148. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/init_test.move +23 -0
  149. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/objects/arena.move +137 -0
  150. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/permits/brawl_permit.move +125 -0
  151. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/permits/duel_permit.move +125 -0
  152. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/battle_state.move +255 -0
  153. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/card.move +282 -0
  154. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/deck.move +64 -0
  155. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/game_config.move +267 -0
  156. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/gold.move +205 -0
  157. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/profile.move +255 -0
  158. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/scenes/brawl.move +313 -0
  159. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/scenes/duel.move +313 -0
  160. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/user_storage_init.move +26 -0
  161. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/scripts/deploy_hook.move +32 -0
  162. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/scripts/migrate.move +7 -0
  163. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/arena_system.move +57 -0
  164. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/brawl_system.move +559 -0
  165. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/card_system.move +146 -0
  166. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/duel_system.move +515 -0
  167. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/market_system.move +115 -0
  168. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/player_system.move +37 -0
  169. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/brawl_tests.move +385 -0
  170. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/duel_tests.move +656 -0
  171. package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/test_helpers.move +31 -0
  172. package/templates/nextjs/sui-card-duel/packages/contracts/sui-indexer-schema.graphql +5973 -0
  173. package/templates/nextjs/sui-card-duel/pnpm-workspace.yaml +2 -0
  174. package/templates/nextjs/sui-farm/.gitignore_ +51 -0
  175. package/templates/nextjs/sui-farm/README.md +15 -0
  176. package/templates/nextjs/sui-farm/UPGRADE_NOTES.md +97 -0
  177. package/templates/nextjs/sui-farm/docker-compose.yml +34 -0
  178. package/templates/nextjs/sui-farm/mprocs.yaml +17 -0
  179. package/templates/nextjs/sui-farm/package.json +43 -0
  180. package/templates/nextjs/sui-farm/packages/client/.gitignore_ +48 -0
  181. package/templates/nextjs/sui-farm/packages/client/.prettierignore +1 -0
  182. package/templates/nextjs/sui-farm/packages/client/.prettierrc.json +8 -0
  183. package/templates/nextjs/sui-farm/packages/client/eslint.config.mjs +22 -0
  184. package/templates/nextjs/sui-farm/packages/client/next.config.mjs +8 -0
  185. package/templates/nextjs/sui-farm/packages/client/package.json +47 -0
  186. package/templates/nextjs/sui-farm/packages/client/postcss.config.mjs +8 -0
  187. package/templates/nextjs/sui-farm/packages/client/src/app/admin/page.tsx +618 -0
  188. package/templates/nextjs/sui-farm/packages/client/src/app/components/FarmLand.tsx +526 -0
  189. package/templates/nextjs/sui-farm/packages/client/src/app/components/FarmPlotCard.tsx +215 -0
  190. package/templates/nextjs/sui-farm/packages/client/src/app/components/PetAvatar.tsx +868 -0
  191. package/templates/nextjs/sui-farm/packages/client/src/app/components/PetPanel.tsx +1360 -0
  192. package/templates/nextjs/sui-farm/packages/client/src/app/components/PlantModal.tsx +147 -0
  193. package/templates/nextjs/sui-farm/packages/client/src/app/components/ProxyCard.tsx +684 -0
  194. package/templates/nextjs/sui-farm/packages/client/src/app/components/RanchLand.tsx +822 -0
  195. package/templates/nextjs/sui-farm/packages/client/src/app/components/ResourceHUD.tsx +125 -0
  196. package/templates/nextjs/sui-farm/packages/client/src/app/components/ShopPanel.tsx +232 -0
  197. package/templates/nextjs/sui-farm/packages/client/src/app/components/icons/GameIcons.tsx +574 -0
  198. package/templates/nextjs/sui-farm/packages/client/src/app/favicon.ico +0 -0
  199. package/templates/nextjs/sui-farm/packages/client/src/app/globals.css +423 -0
  200. package/templates/nextjs/sui-farm/packages/client/src/app/hooks/useWorldPermitId.ts +66 -0
  201. package/templates/nextjs/sui-farm/packages/client/src/app/layout.tsx +23 -0
  202. package/templates/nextjs/sui-farm/packages/client/src/app/leaderboard/page.tsx +185 -0
  203. package/templates/nextjs/sui-farm/packages/client/src/app/lib/crops.ts +97 -0
  204. package/templates/nextjs/sui-farm/packages/client/src/app/market/page.tsx +763 -0
  205. package/templates/nextjs/sui-farm/packages/client/src/app/page.tsx +1173 -0
  206. package/templates/nextjs/sui-farm/packages/client/src/app/providers.tsx +69 -0
  207. package/templates/nextjs/sui-farm/packages/client/src/app/state/index.tsx +5 -0
  208. package/templates/nextjs/sui-farm/packages/client/tailwind.config.ts +25 -0
  209. package/templates/nextjs/sui-farm/packages/client/tsconfig.json +30 -0
  210. package/templates/nextjs/sui-farm/packages/contracts/.gitignore_ +51 -0
  211. package/templates/nextjs/sui-farm/packages/contracts/.prettierignore +1 -0
  212. package/templates/nextjs/sui-farm/packages/contracts/.prettierrc.json +8 -0
  213. package/templates/nextjs/sui-farm/packages/contracts/Dockerfile +5 -0
  214. package/templates/nextjs/sui-farm/packages/contracts/Pub.localnet.toml +13 -0
  215. package/templates/nextjs/sui-farm/packages/contracts/counter.lock.json +14 -0
  216. package/templates/nextjs/sui-farm/packages/contracts/docker-compose.yml +34 -0
  217. package/templates/nextjs/sui-farm/packages/contracts/dubhe.config.ts +178 -0
  218. package/templates/nextjs/sui-farm/packages/contracts/harvest.lock.json +120 -0
  219. package/templates/nextjs/sui-farm/packages/contracts/package.json +48 -0
  220. package/templates/nextjs/sui-farm/packages/contracts/scripts/config.ts +17 -0
  221. package/templates/nextjs/sui-farm/packages/contracts/scripts/query-dapp-revenue.ts +95 -0
  222. package/templates/nextjs/sui-farm/packages/contracts/scripts/read-permit.ts +72 -0
  223. package/templates/nextjs/sui-farm/packages/contracts/scripts/seed.ts +85 -0
  224. package/templates/nextjs/sui-farm/packages/contracts/scripts/withdraw-dapp-revenue.ts +56 -0
  225. package/templates/nextjs/sui-farm/packages/contracts/src/counter/Move.toml +14 -0
  226. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/dapp_key.move +31 -0
  227. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/error.move +10 -0
  228. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/genesis.move +33 -0
  229. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/init_test.move +23 -0
  230. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/resources/counter1.move +64 -0
  231. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/resources/counter2.move +70 -0
  232. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
  233. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/scripts/deploy_hook.move +8 -0
  234. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/scripts/migrate.move +8 -0
  235. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/systems/counter.move +13 -0
  236. package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/tests/counter.move +28 -0
  237. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/Move.toml +14 -0
  238. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/dapp_key.move +46 -0
  239. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/crop_type.move +47 -0
  240. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/egg_type.move +42 -0
  241. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/pet_rarity.move +37 -0
  242. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/pet_species.move +52 -0
  243. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/error.move +110 -0
  244. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/genesis.move +33 -0
  245. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/init_test.move +23 -0
  246. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/permits/world.move +125 -0
  247. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/carrot.move +136 -0
  248. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/carrot_seed.move +80 -0
  249. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/common_egg.move +136 -0
  250. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/corn.move +136 -0
  251. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/corn_seed.move +80 -0
  252. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/crow_charges.move +162 -0
  253. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/crow_damage.move +207 -0
  254. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/farm_plot.move +261 -0
  255. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/gold.move +80 -0
  256. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet.move +467 -0
  257. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_config.move +372 -0
  258. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_hatch.move +162 -0
  259. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_slot_index.move +83 -0
  260. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_slots.move +64 -0
  261. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/profile.move +162 -0
  262. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pumpkin.move +136 -0
  263. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pumpkin_seed.move +80 -0
  264. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/rare_egg.move +136 -0
  265. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/scarecrow.move +64 -0
  266. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/season_config.move +197 -0
  267. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/season_stats.move +64 -0
  268. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/seasonal_egg.move +136 -0
  269. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/shop_config.move +267 -0
  270. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/trophy.move +245 -0
  271. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/wheat.move +136 -0
  272. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/wheat_seed.move +80 -0
  273. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/world_permit_id.move +64 -0
  274. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/user_storage_init.move +26 -0
  275. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/scripts/deploy_hook.move +45 -0
  276. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/scripts/migrate.move +7 -0
  277. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/crow_system.move +96 -0
  278. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/farm_system.move +182 -0
  279. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/market_system.move +172 -0
  280. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/pet_system.move +495 -0
  281. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/season_system.move +70 -0
  282. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/shop_system.move +112 -0
  283. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/world_system.move +34 -0
  284. package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/tests/farm_tests.move +1192 -0
  285. package/templates/nextjs/sui-farm/pnpm-workspace.yaml +2 -0
  286. package/templates/nextjs/sui-template/packages/client/src/app/components/ProxyCard.tsx +683 -0
  287. package/templates/nextjs/sui-template/packages/client/src/app/page.tsx +510 -5
  288. package/templates/nextjs/sui-template/packages/client/src/app/providers.tsx +13 -5
  289. package/templates/nextjs/sui-template/packages/contracts/Pub.localnet.toml +4 -4
  290. package/templates/nextjs/sui-template/packages/contracts/counter.lock.json +14 -0
  291. package/templates/nextjs/sui-template/packages/contracts/deployment.ts +8 -3
  292. package/templates/nextjs/sui-template/packages/contracts/dubhe.config.json +9 -9
  293. package/templates/nextjs/sui-template/packages/contracts/dubhe.config.ts +0 -4
  294. package/templates/nextjs/sui-template/packages/contracts/metadata.json +180 -124
  295. package/templates/nextjs/sui-template/packages/contracts/package.json +8 -4
  296. package/templates/nextjs/sui-template/packages/contracts/src/counter/Move.toml +4 -0
  297. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/dapp_key.move +45 -31
  298. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/error.move +10 -0
  299. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/genesis.move +30 -13
  300. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/init_test.move +19 -14
  301. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter1.move +30 -20
  302. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2.move +35 -24
  303. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
  304. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/scripts/deploy_hook.move +6 -10
  305. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/systems/counter.move +7 -20
  306. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/tests/counter.move +14 -27
  307. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/Move.lock +33 -30
  308. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/Move.toml +8 -3
  309. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/dapp_key.move +45 -31
  310. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/error.move +170 -0
  311. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/genesis.move +16 -13
  312. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/init_test.move +8 -14
  313. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/account_key.move +1 -3
  314. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/dapp_service.move +1776 -137
  315. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/data_key.move +1 -20
  316. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/events.move +864 -25
  317. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/scripts/deploy_hook.move +15 -16
  318. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/scripts/migrate.move +0 -3
  319. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/address_system.move +25 -17
  320. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/dapp_system.move +2802 -343
  321. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/utils.move +1 -1
  322. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/address.move +173 -55
  323. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/dapp_test.move +769 -0
  324. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/fee_test.move +2965 -0
  325. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/integration_test.move +303 -0
  326. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/listing_test.move +1422 -0
  327. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/scene_session_test.move +269 -0
  328. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/session_test.move +624 -0
  329. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/storage_test.move +700 -0
  330. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/typed_object_test.move +177 -0
  331. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/typed_scene_test.move +452 -0
  332. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/user_storage_test.move +160 -0
  333. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/utils/math.move +4 -3
  334. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/utils/type_info.move +2 -2
  335. package/templates/101/sui-template/packages/contracts/src/counter/Move.lock +0 -41
  336. package/templates/101/sui-template/packages/contracts/src/counter/Published.toml +0 -9
  337. package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/errors.move +0 -12
  338. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/errors.move +0 -140
  339. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
  340. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
  341. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
  342. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
  343. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/dapp_store.move +0 -29
  344. package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/session_system.move +0 -44
  345. package/templates/contract/sui-template/src/dubhe/sources/codegen/errors.move +0 -140
  346. package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
  347. package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
  348. package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
  349. package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
  350. package/templates/contract/sui-template/src/dubhe/sources/core/dapp_store.move +0 -29
  351. package/templates/contract/sui-template/src/dubhe/sources/systems/session_system.move +0 -44
  352. package/templates/nextjs/sui-template/packages/contracts/src/counter/Move.lock +0 -47
  353. package/templates/nextjs/sui-template/packages/contracts/src/counter/Published.toml +0 -9
  354. package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/errors.move +0 -12
  355. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/errors.move +0 -140
  356. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
  357. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
  358. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
  359. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
  360. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/dapp_store.move +0 -29
  361. package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/session_system.move +0 -44
@@ -1,406 +1,2865 @@
1
+ #[allow(unused_use)]
1
2
  module dubhe::dapp_system;
2
- use dubhe::dapp_service::DappHub;
3
- use dubhe::dapp_service;
4
- use dubhe::type_info;
5
- use dubhe::dapp_key;
6
- use sui::clock::Clock;
7
- use sui::clock;
8
- use dubhe::dapp_metadata;
9
- use dubhe::errors::{
10
- no_permission_error,
11
- not_latest_version_error,
12
- dapp_already_paused_error,
13
- invalid_package_id_error,
14
- invalid_version_error,
15
- insufficient_credit_error,
16
- dapp_not_been_delegated_error,
17
- dapp_already_delegated_error
3
+
4
+ use dubhe::dapp_service::{
5
+ Self,
6
+ DappHub,
7
+ DappStorage,
8
+ UserStorage,
9
+ PermitMetadata,
10
+ ObjectStorage,
11
+ ScenePermit,
12
+ SceneStorage,
18
13
  };
19
- use dubhe::dapp_fee_state;
20
- use sui::bag::Bag;
21
- use dubhe::dapp_fee_config;
22
- use std::ascii::String;
23
- use std::ascii::string;
14
+ use dubhe::dubhe_events;
15
+ use dubhe::type_info;
16
+ use dubhe::error;
17
+ use sui::clock::{Self, Clock};
18
+ use sui::coin::{Self, Coin};
19
+ use sui::balance;
20
+ use sui::bcs;
21
+ use sui::sui::SUI;
22
+ use sui::transfer;
23
+ use std::ascii::{String, string};
24
24
  use std::type_name;
25
25
 
26
- /// Set a record
26
+ // ─── Framework constants ──────────────────────────────────────────────────────
27
+
28
+ /// Current framework version. Lifecycle functions gate on this constant.
29
+ /// After a package upgrade, call bump_framework_version(dh) in migrate() to
30
+ /// increment DappHub.version to match; old package calls will then abort.
31
+ const FRAMEWORK_VERSION: u64 = 1;
32
+
33
+ /// Minimum session key validity duration (1 minute in milliseconds).
34
+ const MIN_SESSION_DURATION_MS: u64 = 60_000;
35
+
36
+ /// Maximum session key validity duration (7 days in milliseconds).
37
+ const MAX_SESSION_DURATION_MS: u64 = 7 * 24 * 60 * 60 * 1_000;
38
+
39
+ /// Minimum fee increase delay (48 hours in milliseconds).
40
+ const MIN_FEE_INCREASE_DELAY_MS: u64 = 48 * 60 * 60 * 1_000;
41
+
42
+ // ─── Settlement mode constants ────────────────────────────────────────────────
43
+
44
+ /// DApp subsidizes user write costs from its credit_pool (existing behaviour).
45
+ const SETTLEMENT_DAPP: u8 = 0;
46
+ /// User pre-pays; revenue is split between framework treasury and DApp admin at deposit time.
47
+ const SETTLEMENT_USER: u8 = 1;
48
+
49
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
50
+
51
+ /// Assert that DappHub.version matches FRAMEWORK_VERSION.
52
+ /// Lifecycle functions call this to block calls from old package IDs after
53
+ /// a framework upgrade (once migrate() bumps DappHub.version).
54
+ fun assert_framework_version(dh: &DappHub) {
55
+ error::not_latest_version(dapp_service::framework_version(dh) == FRAMEWORK_VERSION);
56
+ }
57
+
58
+ // ─── DApp lifecycle ───────────────────────────────────────────────────────────
59
+
60
+ /// Create a new DApp: produce a DappStorage object with initialised metadata.
61
+ ///
62
+ /// `dapp_hub` is required so the framework can enforce a one-shot guard:
63
+ /// each DappKey type may only produce ONE DappStorage ever. The check is
64
+ /// performed here inside the framework — not in the codegen-generated
65
+ /// genesis.move — so it cannot be bypassed even if the developer modifies
66
+ /// their genesis module.
67
+ ///
68
+ /// Returns the `DappStorage` so the caller can:
69
+ /// 1. Run deploy_hook to set up initial state.
70
+ /// 2. Call `transfer::public_share_object(ds)` to publish it.
71
+ ///
72
+ /// Typical usage in `genesis::run`:
73
+ /// ```
74
+ /// let mut ds = dapp_system::create_dapp<DappKey>(dapp_key, dapp_hub, ...);
75
+ /// my_package::deploy_hook::run(&mut ds, ctx);
76
+ /// transfer::public_share_object(ds);
77
+ /// ```
78
+ public fun create_dapp<DappKey: copy + drop>(
79
+ _dapp_key: DappKey,
80
+ dapp_hub: &mut DappHub,
81
+ name: String,
82
+ description: String,
83
+ initial_mode: u8,
84
+ clock: &Clock,
85
+ ctx: &mut TxContext,
86
+ ): DappStorage {
87
+ assert_framework_version(dapp_hub);
88
+ error::wrong_settlement_mode(initial_mode == 0 || initial_mode == 1);
89
+
90
+ // One-shot guard enforced by the framework: a given DappKey type can only
91
+ // ever produce one DappStorage, regardless of what genesis.move does.
92
+ error::dapp_already_initialized(!dapp_service::is_dapp_genesis_done<DappKey>(dapp_hub));
93
+
94
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
95
+
96
+ // Read default free credit from framework config and apply to the new DApp.
97
+ let cfg = dapp_service::get_config(dapp_hub);
98
+ let free_amount = dapp_service::default_free_credit(cfg);
99
+ let duration_ms = dapp_service::default_free_credit_duration_ms(cfg);
100
+ let created_at = clock::timestamp_ms(clock);
101
+ let expires_at = if (duration_ms > 0) { created_at + duration_ms } else { 0 };
102
+ let default_revenue_share = dapp_service::default_write_fee_dapp_share_bps(cfg);
103
+
104
+ let admin = ctx.sender();
105
+ let package_ids = vector[type_info::get_package_id<DappKey>()];
106
+
107
+ // Copy the current effective fee rates from DappHub into the new DappStorage.
108
+ // These become the per-DApp rates used by settle_writes.
109
+ let (default_base, default_bytes) = get_effective_fees(dapp_hub);
110
+
111
+ let ds = dapp_service::new_dapp_storage<DappKey>(
112
+ name,
113
+ description,
114
+ package_ids,
115
+ created_at,
116
+ admin,
117
+ free_amount,
118
+ expires_at,
119
+ default_base,
120
+ default_bytes,
121
+ initial_mode,
122
+ default_revenue_share,
123
+ ctx,
124
+ );
125
+
126
+ // Register genesis as complete. Any future call to create_dapp with the
127
+ // same DappKey type will abort with dapp_already_initialized_error.
128
+ dapp_service::set_dapp_genesis_done<DappKey>(dapp_hub);
129
+
130
+ dubhe_events::emit_dapp_created(
131
+ dapp_key_str,
132
+ admin,
133
+ created_at,
134
+ sui::object::uid_to_address(dapp_service::dapp_storage_id(&ds)),
135
+ );
136
+ dapp_service::emit_fee_state_record<DappKey>(&ds);
137
+ ds
138
+ }
139
+
140
+ /// Create a UserStorage for the transaction sender within a DApp.
141
+ /// Aborts if the DApp is paused.
142
+ ///
143
+ /// Each address may only create ONE UserStorage per DApp. A second call from
144
+ /// the same address aborts with `user_storage_already_exists_error`, preventing
145
+ /// users from discarding a debt-laden UserStorage and starting fresh with a
146
+ /// zero write_count. During an active proxy the registration persists, so the
147
+ /// canonical owner also cannot create a duplicate while their storage is held
148
+ /// by the proxy.
149
+ public fun create_user_storage<DappKey: copy + drop>(
150
+ _auth: DappKey,
151
+ dapp_hub: &DappHub,
152
+ dapp_storage: &mut DappStorage,
153
+ ctx: &mut TxContext,
154
+ ) {
155
+ assert_framework_version(dapp_hub);
156
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
157
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
158
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
159
+ let sender = ctx.sender();
160
+ error::user_storage_already_exists(!dapp_service::has_registered_user_storage(dapp_storage, sender));
161
+ dapp_service::register_user_storage(dapp_storage, sender);
162
+ let write_limit = dapp_service::framework_max_write_limit(dapp_service::get_config(dapp_hub));
163
+ let us = dapp_service::new_user_storage<DappKey>(sender, write_limit, ctx);
164
+ dubhe_events::emit_user_storage_created(
165
+ dapp_key_str,
166
+ sender,
167
+ sui::object::uid_to_address(dapp_service::user_storage_id(&us)),
168
+ );
169
+ dapp_service::share_user_storage(us);
170
+ }
171
+
172
+ // ─── Hot-path: user writes to UserStorage ─────────────────────────────────────
173
+
174
+ /// Write a full record to the caller's UserStorage.
175
+ ///
176
+ /// Requirements:
177
+ /// - `_auth` must be an instance of the DApp's DappKey type. Because DappKey::new()
178
+ /// is public(package), only code inside the DApp's own package can supply this value.
179
+ /// This prevents external packages from calling this function directly.
180
+ /// - `user_storage` must belong to the correct DApp (dapp_key must match).
181
+ /// - Caller must be the current owner (canonical_owner or active session key).
182
+ /// - Unsettled write count must be below the DApp's configured write_limit.
27
183
  public fun set_record<DappKey: copy + drop>(
28
- dh: &mut DappHub,
29
- dapp_key: DappKey,
30
- key: vector<vector<u8>>,
31
- value: vector<vector<u8>>,
32
- resource_address: String,
33
- offchain: bool,
34
- ctx: &mut TxContext
35
- ) {
36
- dapp_service::set_record<DappKey>(
37
- dh,
38
- dapp_key,
39
- key,
40
- value,
41
- resource_address,
42
- offchain,
43
- ctx
44
- );
45
- let dapp_key = type_info::get_type_name_string<DappKey>();
46
- // let (_, enabled) = dapp_proxy::get(dh, dapp_key);
47
- // dapp_already_delegated_error(!enabled);
48
- charge_fee(dh, dapp_key, key, value, 1, ctx);
49
- }
50
-
51
- /// Set a field
184
+ _auth: DappKey,
185
+ user_storage: &mut UserStorage,
186
+ key: vector<vector<u8>>,
187
+ field_names: vector<vector<u8>>,
188
+ values: vector<vector<u8>>,
189
+ offchain: bool,
190
+ ctx: &mut TxContext,
191
+ ) {
192
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
193
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
194
+
195
+ // Only canonical owner or active session key may write.
196
+ error::no_permission(dapp_service::is_write_authorized(
197
+ user_storage, ctx.sender(), ctx.epoch_timestamp_ms()
198
+ ));
199
+
200
+ // Enforce per-user write count ceiling. Settlement is required once the
201
+ // unsettled write count reaches the DApp's configured write_limit.
202
+ // Using a pure count avoids reading fee rates at write time.
203
+ error::user_debt_limit_exceeded(dapp_service::unsettled_count(user_storage) < dapp_service::user_write_limit(user_storage));
204
+
205
+ // Accumulate write metrics. write_count is incremented for ALL writes
206
+ // (offchain and onchain) because the framework was used regardless.
207
+ // write_bytes is incremented for onchain writes only (offchain writes
208
+ // emit an event but store nothing on-chain, so bytes_fee does not apply).
209
+ dapp_service::increment_write_count(user_storage);
210
+ if (!offchain) {
211
+ let data_bytes = compute_values_bytes(&values);
212
+ dapp_service::add_write_bytes(user_storage, data_bytes);
213
+ };
214
+
215
+ dapp_service::set_user_record<DappKey>(user_storage, key, field_names, values, offchain);
216
+ }
217
+
218
+ /// Update a single named field within an existing UserStorage record.
219
+ /// `_auth` enforces that only the DApp's own package code can invoke this function.
220
+ /// Unsettled write count must be below the DApp's configured write_limit.
52
221
  public fun set_field<DappKey: copy + drop>(
53
- dh: &mut DappHub,
54
- dapp_key: DappKey,
55
- resource_address: String,
56
- key: vector<vector<u8>>,
57
- field_index: u8,
58
- value: vector<u8>,
59
- ctx: &mut TxContext
60
- ) {
61
- dapp_service::set_field(
62
- dh,
63
- dapp_key,
64
- key,
65
- field_index,
66
- value,
67
- resource_address,
68
- );
69
- let dapp_key = type_info::get_type_name_string<DappKey>();
70
- // let (_, enabled) = dapp_proxy::get(dh, dapp_key);
71
- // dapp_already_delegated_error(!enabled);
72
- charge_fee(dh, dapp_key, key, vector[value], 1, ctx);
222
+ _auth: DappKey,
223
+ user_storage: &mut UserStorage,
224
+ key: vector<vector<u8>>,
225
+ field_name: vector<u8>,
226
+ field_value: vector<u8>,
227
+ ctx: &mut TxContext,
228
+ ) {
229
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
230
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
231
+
232
+ error::no_permission(dapp_service::is_write_authorized(
233
+ user_storage, ctx.sender(), ctx.epoch_timestamp_ms()
234
+ ));
235
+
236
+ error::user_debt_limit_exceeded(dapp_service::unsettled_count(user_storage) < dapp_service::user_write_limit(user_storage));
237
+
238
+ dapp_service::set_user_field<DappKey>(user_storage, key, field_name, field_value);
239
+ dapp_service::increment_write_count(user_storage);
240
+ dapp_service::add_write_bytes(user_storage, (field_value.length() as u256));
73
241
  }
74
242
 
243
+ /// Delete a record and all its named fields from the caller's UserStorage (no fee on delete).
244
+ /// `_auth` enforces that only the DApp's own package code can invoke this function.
75
245
  public fun delete_record<DappKey: copy + drop>(
76
- dh: &mut DappHub,
77
- dapp_key: DappKey,
78
- key: vector<vector<u8>>,
79
- resource_address: String,
80
- ) {
81
- dapp_service::delete_record(
82
- dh,
83
- dapp_key,
84
- key,
85
- resource_address,
86
- );
87
- let dapp_key = type_info::get_type_name_string<DappKey>();
88
- }
89
-
90
- /// Get a record
91
- public fun get_record<DappKey: copy + drop>(
92
- dh: &DappHub,
93
- resource_address: String,
94
- key: vector<vector<u8>>
95
- ): vector<u8> {
96
- dapp_service::get_record<DappKey>(dh, resource_address, key)
246
+ _auth: DappKey,
247
+ user_storage: &mut UserStorage,
248
+ key: vector<vector<u8>>,
249
+ field_names: vector<vector<u8>>,
250
+ ctx: &TxContext,
251
+ ) {
252
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
253
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
254
+ error::no_permission(dapp_service::is_write_authorized(
255
+ user_storage, ctx.sender(), ctx.epoch_timestamp_ms()
256
+ ));
257
+ dapp_service::delete_user_record<DappKey>(user_storage, key, field_names);
97
258
  }
98
259
 
99
- /// Get a field
100
- public fun get_field<DappKey: copy + drop>(
101
- dh: &DappHub,
102
- resource_address: String,
103
- key: vector<vector<u8>>,
104
- field_index: u8
105
- ): vector<u8> {
106
- dapp_service::get_field<DappKey>(dh, resource_address, key, field_index)
260
+ /// Delete a single named field from the caller's UserStorage.
261
+ /// `_auth` enforces that only the DApp's own package code can invoke this function.
262
+ public fun delete_field<DappKey: copy + drop>(
263
+ _auth: DappKey,
264
+ user_storage: &mut UserStorage,
265
+ key: vector<vector<u8>>,
266
+ field_name: vector<u8>,
267
+ ctx: &TxContext,
268
+ ) {
269
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
270
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
271
+ error::no_permission(dapp_service::is_write_authorized(
272
+ user_storage, ctx.sender(), ctx.epoch_timestamp_ms()
273
+ ));
274
+ dapp_service::delete_user_field<DappKey>(user_storage, key, field_name);
107
275
  }
108
276
 
277
+ // ─── Reactive writes (cross-user writes via PermitMetadata) ────────────────────
278
+ //
279
+ // Reactive writes allow one participant to modify another participant's UserStorage
280
+ // within a shared scene context. Four-layer security:
281
+ // 1. ctx.sender() must be authorized to write `from` (canonical owner or its
282
+ // active session key — sessions are delegated proxies for in-scene actions)
283
+ // 2. from's canonical owner must be a registered scene participant
284
+ // 3. target's canonical owner must be a registered scene participant
285
+ // 4. scene must be active (not expired)
286
+ //
287
+ // Write fees are charged to the initiator (`from`) under the initiator-pays model.
109
288
 
110
- public fun has_record<DappKey: copy + drop>(
111
- dh: &DappHub,
112
- resource_address: String,
113
- key: vector<vector<u8>>
289
+ /// Write a full record to another user's UserStorage, authorized by a ScenePermit.
290
+ public fun set_record_reactive<DappKey: copy + drop, PermType>(
291
+ _auth: DappKey,
292
+ permit: &ScenePermit<PermType>,
293
+ from: &mut UserStorage,
294
+ target: &mut UserStorage,
295
+ key: vector<vector<u8>>,
296
+ field_names: vector<vector<u8>>,
297
+ values: vector<vector<u8>>,
298
+ ctx: &mut TxContext,
299
+ ) {
300
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
301
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(from) == dapp_key_str);
302
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(target) == dapp_key_str);
303
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_key_str);
304
+
305
+ let scene_id = dapp_service::scene_permit_id(permit);
306
+ let meta = dapp_service::scene_permit_meta(permit);
307
+
308
+ // 1. Sender must be authorized to write `from` (canonical owner or active session key).
309
+ error::no_permission(dapp_service::is_write_authorized(from, ctx.sender(), ctx.epoch_timestamp_ms()));
310
+ // 2. Initiator must be a scene participant (O(1) DF lookup).
311
+ error::not_scene_participant(dapp_service::is_scene_participant(scene_id, dapp_service::canonical_owner(from)));
312
+ // 3. Target must be a scene participant (O(1) DF lookup).
313
+ error::not_scene_participant(dapp_service::is_scene_participant(scene_id, dapp_service::canonical_owner(target)));
314
+ // 4. Scene must not have expired.
315
+ error::scene_expired(dapp_service::is_scene_active(meta, ctx.epoch_timestamp_ms()));
316
+
317
+ error::user_debt_limit_exceeded(dapp_service::unsettled_count(from) < dapp_service::user_write_limit(from));
318
+
319
+ dapp_service::increment_write_count(from);
320
+ if (!values.is_empty()) {
321
+ let data_bytes = compute_values_bytes(&values);
322
+ dapp_service::add_write_bytes(from, data_bytes);
323
+ };
324
+
325
+ dapp_service::set_user_record<DappKey>(target, key, field_names, values, false);
326
+ }
327
+
328
+ /// Update a single named field in another user's UserStorage, authorized by a ScenePermit.
329
+ public fun set_field_reactive<DappKey: copy + drop, PermType>(
330
+ _auth: DappKey,
331
+ permit: &ScenePermit<PermType>,
332
+ from: &mut UserStorage,
333
+ target: &mut UserStorage,
334
+ key: vector<vector<u8>>,
335
+ field_name: vector<u8>,
336
+ field_value: vector<u8>,
337
+ ctx: &mut TxContext,
338
+ ) {
339
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
340
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(from) == dapp_key_str);
341
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(target) == dapp_key_str);
342
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_key_str);
343
+
344
+ let scene_id = dapp_service::scene_permit_id(permit);
345
+ let meta = dapp_service::scene_permit_meta(permit);
346
+
347
+ error::no_permission(dapp_service::is_write_authorized(from, ctx.sender(), ctx.epoch_timestamp_ms()));
348
+ error::not_scene_participant(dapp_service::is_scene_participant(scene_id, dapp_service::canonical_owner(from)));
349
+ error::not_scene_participant(dapp_service::is_scene_participant(scene_id, dapp_service::canonical_owner(target)));
350
+ error::scene_expired(dapp_service::is_scene_active(meta, ctx.epoch_timestamp_ms()));
351
+
352
+ error::user_debt_limit_exceeded(dapp_service::unsettled_count(from) < dapp_service::user_write_limit(from));
353
+
354
+ dapp_service::set_user_field<DappKey>(target, key, field_name, field_value);
355
+ dapp_service::increment_write_count(from);
356
+ dapp_service::add_write_bytes(from, (field_value.length() as u256));
357
+ }
358
+
359
+ // ─── Typed Object management primitives ──────────────────────────────────────
360
+ //
361
+ // Production path: create_and_share_typed_object / destroy_typed_object (used by codegen).
362
+ // entity_id uniqueness is scoped per (DApp, type_tag) — a guild and a boss
363
+ // can share the same entity_id bytes without collision.
364
+
365
+ /// Low-level UID primitive — test-only. Production code uses create_and_share_typed_object.
366
+ #[test_only]
367
+ public fun create_object<DappKey: copy + drop>(
368
+ _auth: DappKey,
369
+ dapp_storage: &mut DappStorage,
370
+ type_tag: vector<u8>,
371
+ entity_id: vector<u8>,
372
+ ctx: &mut TxContext,
373
+ ): sui::object::UID {
374
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
375
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
376
+
377
+ let uid = sui::object::new(ctx);
378
+ let object_id = sui::object::uid_to_address(&uid);
379
+ dapp_service::register_object_entity_id(dapp_storage, type_tag, entity_id, object_id);
380
+ uid
381
+ }
382
+
383
+ /// Unregister a typed object's entity_id from DappStorage and delete its UID.
384
+ #[test_only]
385
+ public fun destroy_object<DappKey: copy + drop>(
386
+ _auth: DappKey,
387
+ dapp_storage: &mut DappStorage,
388
+ type_tag: vector<u8>,
389
+ entity_id: vector<u8>,
390
+ uid: sui::object::UID,
391
+ ) {
392
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
393
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
394
+
395
+ dapp_service::unregister_object_entity_id(dapp_storage, type_tag, entity_id);
396
+ sui::object::delete(uid);
397
+ }
398
+
399
+ // ─── Framework-controlled ObjectStorage CRUD ─────────────────────────────────
400
+ //
401
+ // These functions replace the old DApp-side Bag manipulation pattern.
402
+ // The phantom ObjType parameter (a DApp-package-local struct, e.g. `Guild`)
403
+ // distinguishes GuildStorage from BossStorage at the Move compiler level.
404
+ //
405
+ // All writes:
406
+ // 1. Verify that the caller's DappKey matches the storage's recorded dapp_key.
407
+ // 2. Write the field as BCS bytes into the Bag.
408
+ // 3. Emit a Dubhe_Object_SetField event for off-chain indexing.
409
+ //
410
+ // Reads are unauthenticated — any caller can inspect ObjectStorage fields.
411
+
412
+ /// Create a new Framework-owned ObjectStorage<ObjType>, register entity_id uniqueness,
413
+ /// and share the object. Called from DApp-generated create_<obj> entry functions.
414
+ /// Because transfer::share_object must be called from the defining package (dubhe),
415
+ /// the framework handles the share here instead of returning the object to the caller.
416
+ public fun create_and_share_typed_object<DappKey: copy + drop, ObjType>(
417
+ _auth: DappKey,
418
+ dapp_storage: &mut DappStorage,
419
+ object_type: vector<u8>,
420
+ entity_id: vector<u8>,
421
+ ctx: &mut TxContext,
422
+ ) {
423
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
424
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
425
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
426
+
427
+ let storage = dapp_service::new_object_storage<ObjType>(
428
+ dapp_key_str,
429
+ object_type,
430
+ entity_id,
431
+ ctx,
432
+ );
433
+ let object_id = sui::object::uid_to_address(dapp_service::object_storage_id(&storage));
434
+ dapp_service::register_object_entity_id(
435
+ dapp_storage,
436
+ *dapp_service::object_storage_type(&storage),
437
+ *dapp_service::object_storage_entity_id(&storage),
438
+ object_id,
439
+ );
440
+ dapp_service::share_object_storage(storage);
441
+ }
442
+
443
+ /// Write a native-typed field into an ObjectStorage Bag and emit an indexing event.
444
+ /// The field value is stored as its native Move type in the Bag; BCS bytes are
445
+ /// computed only for the event payload (no serialization overhead for reads).
446
+ public fun set_object_field<DappKey: copy + drop, ObjType, T: store + copy + drop>(
447
+ _auth: DappKey,
448
+ storage: &mut ObjectStorage<ObjType>,
449
+ field_name: vector<u8>,
450
+ value: T,
451
+ ) {
452
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
453
+ error::dapp_key_mismatch(dapp_service::object_storage_dapp_key(storage) == dapp_key_str);
454
+
455
+ let event_bytes = sui::bcs::to_bytes(&value);
456
+ dapp_service::set_object_field(storage, field_name, value);
457
+
458
+ let object_id = sui::object::uid_to_address(dapp_service::object_storage_id(storage));
459
+ dubhe_events::emit_object_set_field(
460
+ dapp_key_str,
461
+ *dapp_service::object_storage_type(storage), // copies &vector<u8> → vector<u8>
462
+ object_id,
463
+ field_name,
464
+ event_bytes,
465
+ );
466
+ }
467
+
468
+ /// Read a native-typed field from an ObjectStorage Bag. Aborts if field not present.
469
+ public fun get_object_field<ObjType, T: store + copy + drop>(
470
+ storage: &ObjectStorage<ObjType>,
471
+ field_name: vector<u8>,
472
+ ): T {
473
+ dapp_service::get_object_field<ObjType, T>(storage, field_name)
474
+ }
475
+
476
+ /// Returns true if the typed field exists in the ObjectStorage Bag.
477
+ public fun has_object_field<ObjType, T: store + copy + drop>(
478
+ storage: &ObjectStorage<ObjType>,
479
+ field_name: vector<u8>,
114
480
  ): bool {
115
- dapp_service::has_record<DappKey>(dh, resource_address, key)
481
+ dapp_service::has_object_field<ObjType, T>(storage, field_name)
116
482
  }
117
483
 
118
- public fun ensure_has_record<DappKey: copy + drop>(
119
- dh: &DappHub,
120
- resource_address: String,
121
- key: vector<vector<u8>>
484
+ /// Remove and return a native-typed field from an ObjectStorage Bag.
485
+ /// Requires DappKey auth to prevent unauthorized field removal.
486
+ /// Emits a Dubhe_Object_DeleteField event so off-chain indexers stay in sync.
487
+ public fun remove_object_field<DappKey: copy + drop, ObjType, T: store + copy + drop>(
488
+ _auth: DappKey,
489
+ storage: &mut ObjectStorage<ObjType>,
490
+ field_name: vector<u8>,
491
+ ): T {
492
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
493
+ error::dapp_key_mismatch(dapp_service::object_storage_dapp_key(storage) == dapp_key_str);
494
+
495
+ // Capture identifiers before removal (the borrow ends when these copies are taken).
496
+ let object_type = *dapp_service::object_storage_type(storage);
497
+ let object_id = sui::object::uid_to_address(dapp_service::object_storage_id(storage));
498
+
499
+ let value = dapp_service::remove_object_field<ObjType, T>(storage, field_name);
500
+
501
+ dubhe_events::emit_object_delete_field(dapp_key_str, object_type, object_id, field_name);
502
+
503
+ value
504
+ }
505
+
506
+ /// Remove and return a native-typed field from a permit-bound SceneStorage Bag.
507
+ /// Emits a Dubhe_Scene_DeleteField event so off-chain indexers stay in sync.
508
+ /// The caller identifies as `user_storage`'s canonical owner; the transaction may be
509
+ /// signed by the canonical owner or its active session key (is_write_authorized).
510
+ public fun remove_scene_field<DappKey: copy + drop, PermType, SceneType, T: store + copy + drop>(
511
+ _auth: DappKey,
512
+ permit: &ScenePermit<PermType>,
513
+ storage: &mut SceneStorage<SceneType>,
514
+ user_storage: &UserStorage,
515
+ field_name: vector<u8>,
516
+ ctx: &TxContext,
517
+ ): T {
518
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
519
+ error::dapp_key_mismatch(dapp_service::scene_storage_dapp_key(storage) == dapp_key_str);
520
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_key_str);
521
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
522
+ assert_scene_storage_bound_to_permit(permit, storage);
523
+ let now_ms = ctx.epoch_timestamp_ms();
524
+ error::scene_expired(dapp_service::is_scene_active(dapp_service::scene_permit_meta(permit), now_ms));
525
+ error::no_permission(dapp_service::is_write_authorized(user_storage, ctx.sender(), now_ms));
526
+ error::not_scene_participant(dapp_service::is_participant_in_scene_permit(
527
+ permit, dapp_service::canonical_owner(user_storage)
528
+ ));
529
+
530
+ let scene_type = *dapp_service::scene_storage_type(storage);
531
+ let scene_id = sui::object::uid_to_address(dapp_service::scene_storage_id(storage));
532
+ let value = dapp_service::remove_scene_field<SceneType, T>(storage, field_name);
533
+ dubhe_events::emit_scene_delete_field(dapp_key_str, scene_type, scene_id, field_name);
534
+
535
+ value
536
+ }
537
+
538
+ /// System maintenance remove for cleanup/migration. DApp system modules must wrap
539
+ /// this with their own operator/admin checks before exposing it publicly.
540
+ public fun remove_scene_field_system_maintenance<DappKey: copy + drop, SceneType, T: store + copy + drop>(
541
+ _auth: DappKey,
542
+ storage: &mut SceneStorage<SceneType>,
543
+ field_name: vector<u8>,
544
+ ): T {
545
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
546
+ error::dapp_key_mismatch(dapp_service::scene_storage_dapp_key(storage) == dapp_key_str);
547
+
548
+ let scene_type = *dapp_service::scene_storage_type(storage);
549
+ let scene_id = sui::object::uid_to_address(dapp_service::scene_storage_id(storage));
550
+ let value = dapp_service::remove_scene_field<SceneType, T>(storage, field_name);
551
+ dubhe_events::emit_scene_delete_field(dapp_key_str, scene_type, scene_id, field_name);
552
+
553
+ value
554
+ }
555
+
556
+ /// Unregister entity_id from DappStorage and consume the ObjectStorage object.
557
+ /// The Bag must be empty before calling this; use remove_object_field first.
558
+ public fun destroy_typed_object<DappKey: copy + drop, ObjType>(
559
+ _auth: DappKey,
560
+ dapp_storage: &mut DappStorage,
561
+ storage: ObjectStorage<ObjType>,
122
562
  ) {
123
- dapp_service::ensure_has_record<DappKey>(dh, resource_address, key)
563
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
564
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
565
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
566
+
567
+ let type_tag = *dapp_service::object_storage_type(&storage); // &vector<u8> → copy
568
+ let entity_id = *dapp_service::object_storage_entity_id(&storage); // same
569
+ let object_id = sui::object::uid_to_address(dapp_service::object_storage_id(&storage));
570
+ dapp_service::unregister_object_entity_id(dapp_storage, type_tag, entity_id);
571
+ dubhe_events::emit_object_destroyed(dapp_key_str, type_tag, object_id, entity_id);
572
+ dapp_service::destroy_object_storage(storage);
124
573
  }
125
574
 
126
- public fun ensure_has_not_record<DappKey: copy + drop>(
127
- dh: &DappHub,
128
- resource_address: String,
129
- key: vector<vector<u8>>
575
+ // ─── Framework-controlled ScenePermit / SceneStorage CRUD ────────────────────
576
+
577
+ fun assert_scene_storage_bound_to_permit<PermType, SceneType>(
578
+ permit: &ScenePermit<PermType>,
579
+ storage: &SceneStorage<SceneType>,
130
580
  ) {
131
- dapp_service::ensure_has_not_record<DappKey>(dh, resource_address, key)
581
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_service::scene_storage_dapp_key(storage));
582
+
583
+ let auth_id = *dapp_service::scene_storage_authorized_permit_id(storage);
584
+ error::invalid_key(option::is_some(&auth_id));
585
+ error::invalid_key(*option::borrow(&auth_id) == sui::object::uid_to_address(dapp_service::scene_permit_id(permit)));
132
586
  }
133
587
 
134
- // public fun get_mut_dapp_objects<DappKey: copy + drop>(
135
- // dh: &mut DappHub,
136
- // dapp_key: DappKey,
137
- // ): &mut Bag {
138
- // dapp_service::get_mut_dapp_objects(dh, dapp_key)
139
- // }
588
+ /// Create an owned ScenePermit<PermType> with participants. The caller may create
589
+ /// multiple SceneStorage objects from it before sharing the whole session.
590
+ public fun new_scene_permit<DappKey: copy + drop, PermType>(
591
+ _auth: DappKey,
592
+ dapp_storage: &DappStorage,
593
+ permit_type: vector<u8>,
594
+ participants: vector<address>,
595
+ expires_at: std::option::Option<u64>,
596
+ max_participants: std::option::Option<u64>,
597
+ ctx: &mut TxContext,
598
+ ): ScenePermit<PermType> {
599
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
600
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
601
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
140
602
 
141
- public fun create_dapp<DappKey: copy + drop>(
142
- dh: &mut DappHub,
143
- dapp_key: DappKey,
144
- name: String,
145
- description: String,
146
- clock: &Clock,
147
- ctx: &mut TxContext
148
- ) {
149
- let dubhe_dapp_key = dapp_key::new();
150
- if(!dapp_key::eq(&dapp_key, &dubhe_dapp_key)) {
151
- initialize_metadata<DappKey>(dh, name, description, clock, ctx);
152
- initialize_fee_state<DappKey>(dh, ctx);
153
- // initialize_dapp_proxy<DappKey>(dh, ctx);
154
- };
155
- }
156
-
157
- public(package) fun initialize_metadata<DappKey: copy + drop>(
158
- dh: &mut DappHub,
159
- name: String,
160
- description: String,
161
- clock: &Clock,
162
- ctx: &mut TxContext
163
- ) {
164
- let dapp_key = type_info::get_type_name_string<DappKey>();
165
- let created_at = clock::timestamp_ms(clock);
166
- let admin = ctx.sender();
167
- let version = 1;
168
- let pausable = false;
169
- let package_ids = vector[type_info::get_package_id<DappKey>()];
170
- let cover_url = vector::empty();
171
- let website_url = string(b"");
172
- let partners = vector::empty();
173
- dapp_metadata::set(
174
- dh,
175
- dapp_key,
176
- name,
177
- description,
178
- website_url,
179
- cover_url,
180
- partners,
181
- package_ids,
182
- created_at,
183
- admin,
184
- version,
185
- pausable,
186
- ctx
187
- );
188
- }
189
-
190
- public(package) fun initialize_fee_state<DappKey: copy + drop>(
191
- dh: &mut DappHub,
192
- ctx: &mut TxContext
193
- ) {
194
- let dapp_key = type_info::get_type_name_string<DappKey>();
195
- let (free_credit, base_fee, byte_fee) = dapp_fee_config::get(dh);
196
- dapp_fee_state::set(
197
- dh,
198
- dapp_key,
199
- base_fee,
200
- byte_fee,
201
- free_credit,
202
- 0,
203
- 0,
204
- 0,
205
- 0,
206
- ctx
207
- );
208
- }
209
-
210
- public(package) fun initialize_dapp_proxy<DappKey: copy + drop>(
211
- dh: &mut DappHub,
212
- ctx: &mut TxContext
213
- ) {
214
- let dapp_key = type_info::get_type_name_string<DappKey>();
215
- // dapp_proxy::set(dh, dapp_key, @0x0, false);
216
- }
217
-
218
- public(package) fun upgrade_dapp<DappKey: copy + drop>(
219
- dh: &mut DappHub,
220
- new_package_id: address,
221
- new_version: u32,
222
- ctx: &mut TxContext
223
- ) {
224
- let dapp_key = type_info::get_type_name_string<DappKey>();
225
- dapp_metadata::ensure_has(dh, dapp_key);
226
- no_permission_error(dapp_metadata::get_admin(dh, dapp_key) == ctx.sender());
227
- let mut package_ids = dapp_metadata::get_package_ids(dh, dapp_key);
228
- invalid_package_id_error(!package_ids.contains(&new_package_id));
229
- package_ids.push_back(new_package_id);
230
- invalid_version_error(new_version > dapp_metadata::get_version(dh, dapp_key));
231
- dapp_metadata::set_package_ids(dh, dapp_key, package_ids);
232
- dapp_metadata::set_version(dh, dapp_key, new_version);
233
- }
603
+ dapp_service::new_scene_permit_with_participants<PermType>(
604
+ dapp_key_str, permit_type, participants, expires_at, max_participants, ctx
605
+ )
606
+ }
234
607
 
235
- public fun set_pausable(
236
- dh: &mut DappHub,
237
- dapp_key: String,
238
- pausable: bool,
239
- ctx: &mut TxContext
608
+ /// Create an owned ScenePermit<PermType> with invitees.
609
+ public fun new_scene_permit_with_invitations<DappKey: copy + drop, PermType>(
610
+ _auth: DappKey,
611
+ dapp_storage: &DappStorage,
612
+ permit_type: vector<u8>,
613
+ invitees: vector<address>,
614
+ invites_expire_at: std::option::Option<u64>,
615
+ scene_expires_at: std::option::Option<u64>,
616
+ max_participants: std::option::Option<u64>,
617
+ ctx: &mut TxContext,
618
+ ): ScenePermit<PermType> {
619
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
620
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
621
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
622
+
623
+ dapp_service::new_scene_permit_with_invitations<PermType>(
624
+ dapp_key_str, permit_type, invitees, invites_expire_at, scene_expires_at, max_participants, ctx
625
+ )
626
+ }
627
+
628
+ /// Create and share a ScenePermit<PermType> with participants.
629
+ public fun create_and_share_scene_permit<DappKey: copy + drop, PermType>(
630
+ _auth: DappKey,
631
+ dapp_storage: &DappStorage,
632
+ permit_type: vector<u8>,
633
+ participants: vector<address>,
634
+ expires_at: std::option::Option<u64>,
635
+ max_participants: std::option::Option<u64>,
636
+ ctx: &mut TxContext,
240
637
  ) {
241
- dapp_metadata::ensure_has(dh, dapp_key);
242
- no_permission_error(dapp_metadata::get_admin(dh, dapp_key) == ctx.sender());
243
- dapp_metadata::set_pausable(dh, dapp_key, pausable);
638
+ let permit = new_scene_permit<DappKey, PermType>(
639
+ _auth, dapp_storage, permit_type, participants, expires_at, max_participants, ctx
640
+ );
641
+ dapp_service::share_scene_permit(permit);
244
642
  }
245
643
 
246
- public fun transfer_ownership(
247
- dh: &mut DappHub,
248
- dapp_key: String,
249
- new_admin: address,
250
- ctx: &mut TxContext
251
- ) {
252
- dapp_metadata::ensure_has(dh, dapp_key);
253
- no_permission_error(dapp_metadata::get_admin(dh, dapp_key) == ctx.sender());
254
- dapp_metadata::set_admin(dh, dapp_key, new_admin);
255
- }
256
-
257
- public fun set_metadata(
258
- dh: &mut DappHub,
259
- dapp_key: String,
260
- name: String,
261
- description: String,
262
- website_url: String,
263
- cover_url: vector<String>,
264
- partners: vector<String>,
265
- ctx: &mut TxContext
266
- ) {
267
- dapp_metadata::ensure_has(dh, dapp_key);
268
- no_permission_error(dapp_metadata::get_admin(dh, dapp_key) == ctx.sender());
269
- let mut dapp_metadata = dapp_metadata::get_struct(dh, dapp_key);
270
- dapp_metadata.update_name(name);
271
- dapp_metadata.update_description(description);
272
- dapp_metadata.update_website_url(website_url);
273
- dapp_metadata.update_cover_url(cover_url);
274
- dapp_metadata.update_partners(partners);
275
- dapp_metadata::set_struct(dh, dapp_key, dapp_metadata, ctx);
276
- }
277
-
278
- public(package) fun calculate_bytes_size_and_fee(dh: &DappHub, dapp_key: String, key_tuple: vector<vector<u8>>, value_tuple: vector<vector<u8>>, count: u256): (u256, u256) {
279
- let fee_state = dapp_fee_state::get_struct(dh, dapp_key);
280
- let mut total_bytes_size = 0;
644
+ /// Create and share a ScenePermit<PermType> with invitees.
645
+ public fun create_and_share_scene_permit_with_invitations<DappKey: copy + drop, PermType>(
646
+ _auth: DappKey,
647
+ dapp_storage: &DappStorage,
648
+ permit_type: vector<u8>,
649
+ invitees: vector<address>,
650
+ invites_expire_at: std::option::Option<u64>,
651
+ scene_expires_at: std::option::Option<u64>,
652
+ max_participants: std::option::Option<u64>,
653
+ ctx: &mut TxContext,
654
+ ) {
655
+ let permit = new_scene_permit_with_invitations<DappKey, PermType>(
656
+ _auth, dapp_storage, permit_type, invitees, invites_expire_at, scene_expires_at, max_participants, ctx
657
+ );
658
+ dapp_service::share_scene_permit(permit);
659
+ }
281
660
 
282
- let mut i = 0;
283
- while (i < key_tuple.length()) {
284
- let key_bytes_size = key_tuple[i].length();
285
- total_bytes_size = total_bytes_size + key_bytes_size;
286
- i = i + 1;
661
+ /// Share an owned ScenePermit created by new_scene_permit*.
662
+ public fun share_scene_permit<DappKey: copy + drop, PermType>(
663
+ _auth: DappKey,
664
+ permit: ScenePermit<PermType>,
665
+ ) {
666
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
667
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(&permit) == dapp_key_str);
668
+ dapp_service::share_scene_permit(permit);
669
+ }
670
+
671
+ /// Create an owned system SceneStorage with no permit binding.
672
+ public fun new_typed_scene_system<DappKey: copy + drop, SceneType>(
673
+ _auth: DappKey,
674
+ dapp_storage: &DappStorage,
675
+ scene_type: vector<u8>,
676
+ ctx: &mut TxContext,
677
+ ): SceneStorage<SceneType> {
678
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
679
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
680
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
681
+
682
+ dapp_service::new_scene_storage_system<SceneType>(dapp_key_str, scene_type, ctx)
683
+ }
684
+
685
+ /// Create an owned SceneStorage bound to a concrete ScenePermit object.
686
+ public fun new_typed_scene_with_permit<DappKey: copy + drop, PermType, SceneType>(
687
+ _auth: DappKey,
688
+ dapp_storage: &DappStorage,
689
+ permit: &ScenePermit<PermType>,
690
+ scene_type: vector<u8>,
691
+ ctx: &mut TxContext,
692
+ ): SceneStorage<SceneType> {
693
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
694
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
695
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_key_str);
696
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
697
+
698
+ dapp_service::new_scene_storage_with_permit<PermType, SceneType>(
699
+ dapp_key_str, scene_type, permit, ctx
700
+ )
701
+ }
702
+
703
+ /// Create and share a system SceneStorage.
704
+ public fun create_and_share_typed_scene_system<DappKey: copy + drop, SceneType>(
705
+ _auth: DappKey,
706
+ dapp_storage: &DappStorage,
707
+ scene_type: vector<u8>,
708
+ ctx: &mut TxContext,
709
+ ) {
710
+ let storage = new_typed_scene_system<DappKey, SceneType>(
711
+ _auth, dapp_storage, scene_type, ctx
712
+ );
713
+ dapp_service::share_scene_storage(storage);
714
+ }
715
+
716
+ /// Create and share a SceneStorage bound to a concrete ScenePermit.
717
+ public fun create_and_share_typed_scene_with_permit<DappKey: copy + drop, PermType, SceneType>(
718
+ _auth: DappKey,
719
+ dapp_storage: &DappStorage,
720
+ permit: &ScenePermit<PermType>,
721
+ scene_type: vector<u8>,
722
+ ctx: &mut TxContext,
723
+ ) {
724
+ let storage = new_typed_scene_with_permit<DappKey, PermType, SceneType>(
725
+ _auth, dapp_storage, permit, scene_type, ctx
726
+ );
727
+ dapp_service::share_scene_storage(storage);
728
+ }
729
+
730
+ /// Share an owned SceneStorage created by new_typed_scene_*.
731
+ public fun share_scene_storage<DappKey: copy + drop, SceneType>(
732
+ _auth: DappKey,
733
+ storage: SceneStorage<SceneType>,
734
+ ) {
735
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
736
+ error::dapp_key_mismatch(dapp_service::scene_storage_dapp_key(&storage) == dapp_key_str);
737
+ dapp_service::share_scene_storage(storage);
738
+ }
739
+
740
+ /// Write a native-typed field into a permit-bound SceneStorage Bag and emit an event.
741
+ /// The caller identifies as `user_storage`'s canonical owner; the transaction may be
742
+ /// signed by the canonical owner or its active session key (is_write_authorized).
743
+ public fun set_scene_field<DappKey: copy + drop, PermType, SceneType, T: store + copy + drop>(
744
+ _auth: DappKey,
745
+ permit: &ScenePermit<PermType>,
746
+ storage: &mut SceneStorage<SceneType>,
747
+ user_storage: &UserStorage,
748
+ field_name: vector<u8>,
749
+ value: T,
750
+ ctx: &TxContext,
751
+ ) {
752
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
753
+ error::dapp_key_mismatch(dapp_service::scene_storage_dapp_key(storage) == dapp_key_str);
754
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_key_str);
755
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
756
+ assert_scene_storage_bound_to_permit(permit, storage);
757
+ let now_ms = ctx.epoch_timestamp_ms();
758
+ error::scene_expired(dapp_service::is_scene_active(dapp_service::scene_permit_meta(permit), now_ms));
759
+ error::no_permission(dapp_service::is_write_authorized(user_storage, ctx.sender(), now_ms));
760
+ error::not_scene_participant(dapp_service::is_participant_in_scene_permit(
761
+ permit, dapp_service::canonical_owner(user_storage)
762
+ ));
763
+
764
+ let event_bytes = sui::bcs::to_bytes(&value);
765
+ dapp_service::set_scene_field(storage, field_name, value);
766
+
767
+ let scene_id = sui::object::uid_to_address(dapp_service::scene_storage_id(storage));
768
+ dubhe_events::emit_scene_set_field(
769
+ dapp_key_str,
770
+ *dapp_service::scene_storage_type(storage),
771
+ scene_id,
772
+ field_name,
773
+ event_bytes,
774
+ );
775
+ }
776
+
777
+ /// Write a native-typed field into a system SceneStorage Bag and emit an event.
778
+ public fun set_scene_field_system<DappKey: copy + drop, SceneType, T: store + copy + drop>(
779
+ _auth: DappKey,
780
+ storage: &mut SceneStorage<SceneType>,
781
+ field_name: vector<u8>,
782
+ value: T,
783
+ ) {
784
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
785
+ error::dapp_key_mismatch(dapp_service::scene_storage_dapp_key(storage) == dapp_key_str);
786
+ error::invalid_key(option::is_none(dapp_service::scene_storage_authorized_permit_id(storage)));
787
+
788
+ let event_bytes = sui::bcs::to_bytes(&value);
789
+ dapp_service::set_scene_field(storage, field_name, value);
790
+
791
+ let scene_id = sui::object::uid_to_address(dapp_service::scene_storage_id(storage));
792
+ dubhe_events::emit_scene_set_field(
793
+ dapp_key_str,
794
+ *dapp_service::scene_storage_type(storage),
795
+ scene_id,
796
+ field_name,
797
+ event_bytes,
798
+ );
799
+ }
800
+
801
+ /// Read a native-typed field from a SceneStorage Bag. Aborts if field not present.
802
+ public fun get_scene_field<SceneType, T: store + copy + drop>(
803
+ storage: &SceneStorage<SceneType>,
804
+ field_name: vector<u8>,
805
+ ): T {
806
+ dapp_service::get_scene_field<SceneType, T>(storage, field_name)
807
+ }
808
+
809
+ /// Returns true if the typed field exists in the SceneStorage Bag.
810
+ public fun has_scene_field<SceneType, T: store + copy + drop>(
811
+ storage: &SceneStorage<SceneType>,
812
+ field_name: vector<u8>,
813
+ ): bool {
814
+ dapp_service::has_scene_field<SceneType, T>(storage, field_name)
815
+ }
816
+
817
+ /// Consume the SceneStorage object. The Bag must be empty.
818
+ /// Scenes are not registered in the entity_id registry, so no unregistration needed.
819
+ public fun destroy_typed_scene<DappKey: copy + drop, SceneType>(
820
+ _auth: DappKey,
821
+ storage: SceneStorage<SceneType>,
822
+ ) {
823
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
824
+ error::dapp_key_mismatch(dapp_service::scene_storage_dapp_key(&storage) == dapp_key_str);
825
+ let scene_type = *dapp_service::scene_storage_type(&storage);
826
+ let scene_id = sui::object::uid_to_address(dapp_service::scene_storage_id(&storage));
827
+ let authorized_permit_id = *dapp_service::scene_storage_authorized_permit_id(&storage);
828
+ dubhe_events::emit_scene_destroyed(dapp_key_str, scene_type, scene_id, authorized_permit_id);
829
+ dapp_service::destroy_scene_storage(storage);
830
+ }
831
+
832
+ /// Consume the ScenePermit object. All participant DFs must have been removed.
833
+ public fun destroy_scene_permit<DappKey: copy + drop, PermType>(
834
+ _auth: DappKey,
835
+ permit: ScenePermit<PermType>,
836
+ ) {
837
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
838
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(&permit) == dapp_key_str);
839
+
840
+ let permit_type = *dapp_service::scene_permit_type(&permit);
841
+ let permit_id = sui::object::uid_to_address(dapp_service::scene_permit_id(&permit));
842
+ let participant_count = dapp_service::scene_participant_count(dapp_service::scene_permit_meta(&permit));
843
+ error::participants_still_present(participant_count == 0);
844
+ dubhe_events::emit_scene_permit_expire(dapp_key_str, permit_type, permit_id);
845
+ dapp_service::destroy_scene_permit(permit);
846
+ }
847
+
848
+ /// Helper: accept a scene invitation for a ScenePermit-backed scene.
849
+ /// Moves the caller's canonical owner from the invitees list to confirmed
850
+ /// participants. The transaction may be signed by the canonical owner or its
851
+ /// active session key (is_write_authorized) — the registered identity is
852
+ /// always the canonical owner address.
853
+ /// Guards: scene must be active AND the invitation window must not have expired.
854
+ public fun accept_scene_permit_invitation<DappKey: copy + drop, PermType>(
855
+ _auth: DappKey,
856
+ permit: &mut ScenePermit<PermType>,
857
+ user_storage: &UserStorage,
858
+ ctx: &TxContext,
859
+ ) {
860
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
861
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_key_str);
862
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
863
+
864
+ // Check both permit activity and invitation window before mutating.
865
+ // The immutable borrow of `meta` is released at the end of this block.
866
+ let now_ms = ctx.epoch_timestamp_ms();
867
+ error::no_permission(dapp_service::is_write_authorized(user_storage, ctx.sender(), now_ms));
868
+ {
869
+ let meta = dapp_service::scene_permit_meta(permit);
870
+ error::scene_expired(dapp_service::is_scene_active(meta, now_ms));
871
+ let expire_opt = dapp_service::scene_invites_expire_at(meta);
872
+ if (option::is_some(&expire_opt)) {
873
+ error::invitation_expired(now_ms <= *option::borrow(&expire_opt));
874
+ };
287
875
  };
288
876
 
289
- let mut j = 0;
290
- while (j < value_tuple.length()) {
291
- let value_bytes_size = value_tuple[j].length();
292
- total_bytes_size = total_bytes_size + value_bytes_size;
293
- j = j + 1;
877
+ let owner = dapp_service::canonical_owner(user_storage);
878
+ dapp_service::accept_invitation_in_scene_permit(permit, owner);
879
+ dubhe_events::emit_scene_permit_accept(
880
+ dapp_key_str,
881
+ *dapp_service::scene_permit_type(permit),
882
+ sui::object::uid_to_address(dapp_service::scene_permit_id(permit)),
883
+ owner,
884
+ );
885
+ }
886
+
887
+ /// Helper: add the caller's canonical owner as a confirmed participant in a
888
+ /// ScenePermit. The transaction may be signed by the canonical owner or its
889
+ /// active session key (is_write_authorized) — the registered identity is
890
+ /// always the canonical owner address.
891
+ /// The permit must still be active — joining an expired permit is meaningless and
892
+ /// wastes gas on a DF write that can never be used for reactive writes.
893
+ public fun join_scene_permit<DappKey: copy + drop, PermType>(
894
+ _auth: DappKey,
895
+ permit: &mut ScenePermit<PermType>,
896
+ user_storage: &UserStorage,
897
+ ctx: &TxContext,
898
+ ) {
899
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
900
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_key_str);
901
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
902
+ let now_ms = ctx.epoch_timestamp_ms();
903
+ error::scene_expired(dapp_service::is_scene_active(dapp_service::scene_permit_meta(permit), now_ms));
904
+ error::no_permission(dapp_service::is_write_authorized(user_storage, ctx.sender(), now_ms));
905
+ let owner = dapp_service::canonical_owner(user_storage);
906
+ let was_participant = dapp_service::is_participant_in_scene_permit(permit, owner);
907
+ dapp_service::add_participant_in_scene_permit(permit, owner);
908
+ if (!was_participant) {
909
+ dubhe_events::emit_scene_permit_join(
910
+ dapp_key_str,
911
+ *dapp_service::scene_permit_type(permit),
912
+ sui::object::uid_to_address(dapp_service::scene_permit_id(permit)),
913
+ owner,
914
+ );
294
915
  };
916
+ }
295
917
 
296
- let total_bytes_size_u256 = (total_bytes_size as u256) * count;
297
- let total_fee = (total_bytes_size_u256 * fee_state.byte_fee() + fee_state.base_fee()) * count;
918
+ /// Helper: remove the caller's canonical owner from participants in a
919
+ /// SceneStorage scene. The transaction may be signed by the canonical owner or
920
+ /// its active session key (is_write_authorized).
921
+ public fun leave_scene_permit<DappKey: copy + drop, PermType>(
922
+ _auth: DappKey,
923
+ permit: &mut ScenePermit<PermType>,
924
+ user_storage: &UserStorage,
925
+ ctx: &TxContext,
926
+ ) {
927
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
928
+ error::dapp_key_mismatch(dapp_service::scene_permit_dapp_key(permit) == dapp_key_str);
929
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
930
+ error::no_permission(dapp_service::is_write_authorized(
931
+ user_storage, ctx.sender(), ctx.epoch_timestamp_ms()
932
+ ));
933
+ let owner = dapp_service::canonical_owner(user_storage);
934
+ let was_participant = dapp_service::is_participant_in_scene_permit(permit, owner);
935
+ dapp_service::remove_participant_in_scene_permit(permit, owner);
936
+ if (was_participant) {
937
+ dubhe_events::emit_scene_permit_leave(
938
+ dapp_key_str,
939
+ *dapp_service::scene_permit_type(permit),
940
+ sui::object::uid_to_address(dapp_service::scene_permit_id(permit)),
941
+ owner,
942
+ );
943
+ };
944
+ }
298
945
 
299
- (total_bytes_size_u256, total_fee)
946
+ /// Helper: check if addr is a participant in a ScenePermit.
947
+ public fun is_scene_permit_participant<PermType>(
948
+ permit: &ScenePermit<PermType>,
949
+ addr: address,
950
+ ): bool {
951
+ dapp_service::is_participant_in_scene_permit(permit, addr)
300
952
  }
301
953
 
302
- public(package) fun charge_fee(dh: &mut DappHub, dapp_key: String, key: vector<vector<u8>>, value: vector<vector<u8>>, count: u256, ctx: &mut TxContext) {
303
- let ( bytes_size, fee ) = calculate_bytes_size_and_fee(dh, dapp_key, key, value, count);
304
- let mut fee_state = dapp_fee_state::get_struct(dh, dapp_key);
305
- let total_bytes_size = fee_state.total_bytes_size();
306
- let total_paid = fee_state.total_paid();
307
- let total_recharged = fee_state.total_recharged();
308
- let free_credit = fee_state.free_credit();
309
- let total_set_count = fee_state.total_set_count();
310
- if(free_credit >= fee) {
311
- fee_state.update_free_credit(free_credit - fee);
312
- } else {
313
- insufficient_credit_error(total_recharged >= fee);
314
- fee_state.update_total_recharged(total_recharged - fee);
315
- };
316
- fee_state.update_total_bytes_size(total_bytes_size + bytes_size);
317
- fee_state.update_total_paid(total_paid + fee);
318
- fee_state.update_total_set_count(total_set_count + count);
319
- dapp_fee_state::set_struct(dh, dapp_key, fee_state, ctx);
954
+ /// Write a global record into DappStorage (admin / protocol-level data).
955
+ /// `_auth` enforces that only the DApp's own package code can invoke this function.
956
+ /// Global writes are free in both settlement modes (no credit deduction).
957
+ public fun set_global_record<DappKey: copy + drop>(
958
+ _auth: DappKey,
959
+ dapp_storage: &mut DappStorage,
960
+ key: vector<vector<u8>>,
961
+ field_names: vector<vector<u8>>,
962
+ values: vector<vector<u8>>,
963
+ offchain: bool,
964
+ ) {
965
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
966
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
967
+
968
+ dapp_service::set_global_record<DappKey>(dapp_storage, key, field_names, values, offchain);
969
+ dapp_service::emit_fee_state_record<DappKey>(dapp_storage);
320
970
  }
321
971
 
322
- public fun ensure_dapp_admin<DappKey: copy + drop>(
323
- dh: &DappHub,
324
- admin: address
972
+ /// Update a single named field within a DappStorage global record.
973
+ /// `_auth` enforces that only the DApp's own package code can invoke this function.
974
+ /// Global writes are free in both settlement modes (no credit deduction).
975
+ public fun set_global_field<DappKey: copy + drop>(
976
+ _auth: DappKey,
977
+ dapp_storage: &mut DappStorage,
978
+ key: vector<vector<u8>>,
979
+ field_name: vector<u8>,
980
+ field_value: vector<u8>,
325
981
  ) {
326
- let dapp_key = type_info::get_type_name_string<DappKey>();
327
- dapp_metadata::ensure_has(dh, dapp_key);
328
- no_permission_error(dapp_metadata::get_admin(dh, dapp_key) == admin);
982
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
983
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
984
+
985
+ dapp_service::set_global_field<DappKey>(dapp_storage, key, field_name, field_value);
986
+ dapp_service::emit_fee_state_record<DappKey>(dapp_storage);
329
987
  }
330
988
 
331
- public fun ensure_latest_version<DappKey: copy + drop>(
332
- dh: &DappHub,
333
- version: u32
989
+ /// Delete a global record and all its named fields from DappStorage.
990
+ /// `_auth` enforces that only the DApp's own package code can invoke this function.
991
+ public fun delete_global_record<DappKey: copy + drop>(
992
+ _auth: DappKey,
993
+ dapp_storage: &mut DappStorage,
994
+ key: vector<vector<u8>>,
995
+ field_names: vector<vector<u8>>,
334
996
  ) {
335
- let dapp_key = type_info::get_type_name_string<DappKey>();
336
- dapp_metadata::ensure_has(dh, dapp_key);
337
- not_latest_version_error(dapp_metadata::get_version(dh, dapp_key) == version);
997
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
998
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
999
+ dapp_service::delete_global_record<DappKey>(dapp_storage, key, field_names);
338
1000
  }
339
1001
 
340
- public fun ensure_not_pausable<DappKey: copy + drop>(
341
- dh: &DappHub
1002
+ /// Delete a single named field from DappStorage.
1003
+ /// `_auth` enforces that only the DApp's own package code can invoke this function.
1004
+ public fun delete_global_field<DappKey: copy + drop>(
1005
+ _auth: DappKey,
1006
+ dapp_storage: &mut DappStorage,
1007
+ key: vector<vector<u8>>,
1008
+ field_name: vector<u8>,
342
1009
  ) {
343
- let dapp_key = type_info::get_type_name_string<DappKey>();
344
- dapp_metadata::ensure_has(dh, dapp_key);
345
- dapp_already_paused_error(!dapp_metadata::get_pausable(dh, dapp_key));
1010
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1011
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
1012
+ dapp_service::delete_global_field<DappKey>(dapp_storage, key, field_name);
1013
+ }
1014
+
1015
+ // ─── Read-only helpers ────────────────────────────────────────────────────────
1016
+
1017
+ public fun get_field<DappKey: copy + drop>(
1018
+ user_storage: &UserStorage,
1019
+ key: vector<vector<u8>>,
1020
+ field_name: vector<u8>,
1021
+ ): vector<u8> {
1022
+ dapp_service::get_user_field<DappKey>(user_storage, key, field_name)
1023
+ }
1024
+
1025
+ public fun has_record<DappKey: copy + drop>(
1026
+ user_storage: &UserStorage,
1027
+ key: vector<vector<u8>>,
1028
+ ): bool {
1029
+ dapp_service::has_user_record<DappKey>(user_storage, key)
346
1030
  }
347
1031
 
348
- public fun delegate<DappKey: copy + drop>(
349
- dh: &mut DappHub,
350
- delegator: address,
351
- ctx: &mut TxContext
1032
+ public fun ensure_has_record<DappKey: copy + drop>(
1033
+ user_storage: &UserStorage,
1034
+ key: vector<vector<u8>>,
352
1035
  ) {
353
- let dapp_key = type_info::get_type_name_string<DappKey>();
354
- dapp_metadata::ensure_has(dh, dapp_key);
355
- no_permission_error(dapp_metadata::get_admin(dh, dapp_key) == ctx.sender());
356
- // dapp_proxy::set(dh, dapp_key, delegator, true);
1036
+ dapp_service::ensure_has_user_record<DappKey>(user_storage, key)
357
1037
  }
358
1038
 
359
- public fun undelegate<DappKey: copy + drop>(
360
- dh: &mut DappHub,
361
- ctx: &mut TxContext
1039
+ public fun ensure_has_not_record<DappKey: copy + drop>(
1040
+ user_storage: &UserStorage,
1041
+ key: vector<vector<u8>>,
362
1042
  ) {
363
- let dapp_key = type_info::get_type_name_string<DappKey>();
364
- dapp_metadata::ensure_has(dh, dapp_key);
365
- no_permission_error(dapp_metadata::get_admin(dh, dapp_key) == ctx.sender());
366
- // dapp_proxy::set(dh, dapp_key, @0x0, false);
1043
+ dapp_service::ensure_has_not_user_record<DappKey>(user_storage, key)
367
1044
  }
368
1045
 
369
- public fun is_delegated<DappKey: copy + drop>(
370
- dh: &DappHub
1046
+ public fun get_global_field<DappKey: copy + drop>(
1047
+ dapp_storage: &DappStorage,
1048
+ key: vector<vector<u8>>,
1049
+ field_name: vector<u8>,
1050
+ ): vector<u8> {
1051
+ dapp_service::get_global_field<DappKey>(dapp_storage, key, field_name)
1052
+ }
1053
+
1054
+ public fun has_global_record<DappKey: copy + drop>(
1055
+ dapp_storage: &DappStorage,
1056
+ key: vector<vector<u8>>,
371
1057
  ): bool {
372
- let dapp_key = type_info::get_type_name_string<DappKey>();
373
- // dapp_proxy::get_enabled(dh, dapp_key)
374
- false
1058
+ dapp_service::has_global_record<DappKey>(dapp_storage, key)
375
1059
  }
376
1060
 
377
- public fun set_storage<DappKey: copy + drop>(
378
- dh: &mut DappHub,
379
- table_id: String,
380
- key_tuple: vector<vector<u8>>,
381
- value_tuple: vector<vector<u8>>,
382
- count: u256,
383
- ctx: &mut TxContext
1061
+ public fun ensure_has_global_record<DappKey: copy + drop>(
1062
+ dapp_storage: &DappStorage,
1063
+ key: vector<vector<u8>>,
384
1064
  ) {
385
- // let dapp_key = type_info::get_type_name_string<DappKey>();
386
- // dapp_proxy::ensure_has(dh, dapp_key);
387
- // let (delegator, enabled) = dapp_proxy::get(dh, dapp_key);
388
- // dapp_not_been_delegated_error(enabled);
389
- // no_permission_error(delegator == ctx.sender());
390
- // charge_fee(dh, dapp_key, key_tuple, value_tuple, count);
391
- // dapp_service::set_record_internal(dh, dapp_key, table_id, key_tuple, value_tuple);
1065
+ dapp_service::ensure_has_global_record<DappKey>(dapp_storage, key)
392
1066
  }
393
1067
 
394
- public fun dapp_key<DappKey: copy + drop>(): String {
395
- type_name::get<DappKey>().into_string()
1068
+ public fun ensure_has_not_global_record<DappKey: copy + drop>(
1069
+ dapp_storage: &DappStorage,
1070
+ key: vector<vector<u8>>,
1071
+ ) {
1072
+ dapp_service::ensure_has_not_global_record<DappKey>(dapp_storage, key)
396
1073
  }
397
1074
 
398
- #[test_only]
399
- public fun create_dapp_hub_for_testing(ctx: &mut TxContext): DappHub {
400
- dapp_service::create_dapp_hub_for_testing(ctx)
1075
+ // ─── Listing market protocol ─────────────────────────────────────────────────
1076
+ //
1077
+ // `take_record` atomically removes an item record from a UserStorage and wraps
1078
+ // it in a shared Listing object. `restore_record` unwraps the Listing back
1079
+ // into a UserStorage. The take → share → restore/buy lifecycle guarantees
1080
+ // single-source-of-truth with no data duplication (Move linear types).
1081
+
1082
+ use dubhe::dapp_service::Listing;
1083
+
1084
+ /// Take an item record out of UserStorage and create a shared Listing.
1085
+ /// The item is removed from the seller's storage atomically.
1086
+ ///
1087
+ /// Security:
1088
+ /// - Only the CANONICAL OWNER of `user_storage` may create a listing.
1089
+ /// Session keys are intentionally excluded: listing = asset ownership transfer,
1090
+ /// which requires the wallet owner's direct authorization.
1091
+ /// - Aborts if `listed_until` is in the past (already expired at list time).
1092
+ public fun take_record<DappKey: copy + drop, CoinType>(
1093
+ _auth: DappKey,
1094
+ user_storage: &mut UserStorage,
1095
+ record_type: vector<u8>,
1096
+ record_key: vector<vector<u8>>,
1097
+ field_names: vector<vector<u8>>,
1098
+ price: u64,
1099
+ listed_until: Option<u64>,
1100
+ ctx: &mut TxContext,
1101
+ ) {
1102
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1103
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
1104
+ // Listing creation requires the canonical owner — session keys are not permitted.
1105
+ error::no_permission(ctx.sender() == dapp_service::canonical_owner(user_storage));
1106
+
1107
+ // Validate listed_until is in the future if provided.
1108
+ if (option::is_some(&listed_until)) {
1109
+ let expiry = *option::borrow(&listed_until);
1110
+ error::scene_expired(ctx.epoch_timestamp_ms() < expiry);
1111
+ };
1112
+
1113
+ // Read current field values to embed in the Listing.
1114
+ let num_fields = field_names.length();
1115
+ let mut record_values: vector<vector<u8>> = vector::empty();
1116
+ let mut i = 0;
1117
+ while (i < num_fields) {
1118
+ let fname = *field_names.borrow(i);
1119
+ let val = dapp_service::get_user_field<DappKey>(user_storage, record_key, fname);
1120
+ record_values.push_back(val);
1121
+ i = i + 1;
1122
+ };
1123
+
1124
+ // BCS-encode each field value individually; record_data is vector<vector<u8>>.
1125
+ // (No outer bcs::to_bytes wrapper needed — the type changed to vector<vector<u8>>.)
1126
+
1127
+ // Remove the record from the user's storage.
1128
+ dapp_service::delete_user_record<DappKey>(user_storage, record_key, field_names);
1129
+
1130
+ let seller = dapp_service::canonical_owner(user_storage);
1131
+ let listing = dapp_service::new_listing<CoinType>(
1132
+ record_values,
1133
+ record_type,
1134
+ record_key,
1135
+ field_names,
1136
+ seller,
1137
+ price,
1138
+ listed_until,
1139
+ dapp_key_str,
1140
+ false, // is_fungible = false for unique items
1141
+ ctx,
1142
+ );
1143
+ let listing_id = sui::object::uid_to_address(dapp_service::listing_id(&listing));
1144
+ let coin_type_str = type_info::get_type_name_string<CoinType>();
1145
+ let ev_rec_type = *dapp_service::listing_record_type(&listing);
1146
+ let ev_record_key = *dapp_service::listing_record_key(&listing);
1147
+ let ev_field_names = *dapp_service::listing_field_names(&listing);
1148
+ let ev_record_data = *dapp_service::listing_record_data(&listing);
1149
+ dapp_service::share_listing(listing);
1150
+ dubhe_events::emit_item_listed(
1151
+ dapp_key_str,
1152
+ listing_id,
1153
+ seller,
1154
+ ev_rec_type,
1155
+ ev_record_key,
1156
+ ev_field_names,
1157
+ ev_record_data,
1158
+ price,
1159
+ coin_type_str,
1160
+ false,
1161
+ listed_until,
1162
+ );
401
1163
  }
402
1164
 
403
- #[test_only]
404
- public fun destroy(dh: DappHub) {
405
- dapp_service::destroy(dh)
406
- }
1165
+ /// Restore a Listing's item record back into a UserStorage (cancel listing).
1166
+ /// Only the original seller may cancel.
1167
+ ///
1168
+ /// NOTE: This function is for unique (non-fungible) items only. Calling it
1169
+ /// on a fungible listing will abort. Fungible listings must use
1170
+ /// cancel_fungible_listing instead (which does additive-merge semantics).
1171
+ public fun restore_record<DappKey: copy + drop, CoinType>(
1172
+ _auth: DappKey,
1173
+ listing: Listing<CoinType>,
1174
+ user_storage: &mut UserStorage,
1175
+ ctx: &TxContext,
1176
+ ) {
1177
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1178
+ error::dapp_key_mismatch(dapp_service::listing_dapp_key(&listing) == dapp_key_str);
1179
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
1180
+
1181
+ // Fungible listings must go through cancel_fungible_listing (additive merge).
1182
+ error::no_permission(!dapp_service::listing_is_fungible(&listing));
1183
+
1184
+ let seller = dapp_service::listing_seller(&listing);
1185
+ error::no_permission(ctx.sender() == seller);
1186
+ error::no_permission(seller == dapp_service::canonical_owner(user_storage));
1187
+
1188
+ let record_key = *dapp_service::listing_record_key(&listing);
1189
+ let field_names = *dapp_service::listing_field_names(&listing);
1190
+ let record_values = *dapp_service::listing_record_data(&listing);
1191
+
1192
+ // Capture event data before consuming the listing.
1193
+ let listing_id = sui::object::uid_to_address(dapp_service::listing_id(&listing));
1194
+
1195
+ dapp_service::set_user_record<DappKey>(
1196
+ user_storage, record_key, field_names, record_values, false
1197
+ );
1198
+
1199
+ let (_, _, _, _, _, _, _, _) = dapp_service::destroy_listing(listing);
1200
+ dubhe_events::emit_listing_cancelled(dapp_key_str, listing_id, seller, false);
1201
+ }
1202
+
1203
+ /// Take a specific `amount` from a fungible record and create a shared Listing.
1204
+ ///
1205
+ /// Unlike `take_record` (which removes the entire record), this function
1206
+ /// subtracts `amount` from the caller's current balance, or deletes the record
1207
+ /// entirely if the balance would reach zero. A Listing is created that
1208
+ /// contains only the listed amount.
1209
+ ///
1210
+ /// Security:
1211
+ /// - Only the CANONICAL OWNER may list. Session keys are intentionally excluded.
1212
+ /// - `amount` must be > 0 and ≤ current balance (aborts with insufficient_balance).
1213
+ /// - Aborts if `listed_until` is already in the past.
1214
+ public fun take_fungible_record<DappKey: copy + drop, CoinType>(
1215
+ _auth: DappKey,
1216
+ user_storage: &mut UserStorage,
1217
+ record_type: vector<u8>,
1218
+ record_key: vector<vector<u8>>,
1219
+ field_name: vector<u8>,
1220
+ amount: u64,
1221
+ price: u64,
1222
+ listed_until: Option<u64>,
1223
+ ctx: &mut TxContext,
1224
+ ) {
1225
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1226
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
1227
+ // Listing creation requires the canonical owner — session keys are not permitted.
1228
+ error::no_permission(ctx.sender() == dapp_service::canonical_owner(user_storage));
1229
+ if (option::is_some(&listed_until)) {
1230
+ let expiry = *option::borrow(&listed_until);
1231
+ error::scene_expired(ctx.epoch_timestamp_ms() < expiry);
1232
+ };
1233
+
1234
+ // Read current balance (single BCS-encoded u64 field).
1235
+ let current_bytes = dapp_service::get_user_field<DappKey>(user_storage, record_key, field_name);
1236
+ let mut bcs_cur = sui::bcs::new(current_bytes);
1237
+ let current = bcs_cur.peel_u64();
1238
+
1239
+ // amount must be positive and within the available balance.
1240
+ error::insufficient_balance(amount > 0 && amount <= current);
1241
+
1242
+ let remaining = current - amount;
1243
+ if (remaining == 0) {
1244
+ dapp_service::delete_user_record<DappKey>(user_storage, record_key, vector[field_name]);
1245
+ } else {
1246
+ dapp_service::set_user_field<DappKey>(
1247
+ user_storage, record_key, field_name, sui::bcs::to_bytes(&remaining)
1248
+ );
1249
+ };
1250
+
1251
+ // Build Listing with only the listed amount.
1252
+ // record_values is vector<vector<u8>>; each element is BCS-encoded field value.
1253
+ let record_values = vector[sui::bcs::to_bytes(&amount)];
1254
+ let seller = dapp_service::canonical_owner(user_storage);
1255
+ let listing = dapp_service::new_listing<CoinType>(
1256
+ record_values,
1257
+ record_type,
1258
+ record_key,
1259
+ vector[field_name],
1260
+ seller,
1261
+ price,
1262
+ listed_until,
1263
+ dapp_key_str,
1264
+ true, // is_fungible = true
1265
+ ctx,
1266
+ );
1267
+ let listing_id = sui::object::uid_to_address(dapp_service::listing_id(&listing));
1268
+ let coin_type_str = type_info::get_type_name_string<CoinType>();
1269
+ let ev_rec_type = *dapp_service::listing_record_type(&listing);
1270
+ let ev_record_key = *dapp_service::listing_record_key(&listing);
1271
+ let ev_field_names = *dapp_service::listing_field_names(&listing);
1272
+ let ev_record_data = *dapp_service::listing_record_data(&listing);
1273
+ dapp_service::share_listing(listing);
1274
+ dubhe_events::emit_item_listed(
1275
+ dapp_key_str,
1276
+ listing_id,
1277
+ seller,
1278
+ ev_rec_type,
1279
+ ev_record_key,
1280
+ ev_field_names,
1281
+ ev_record_data,
1282
+ price,
1283
+ coin_type_str,
1284
+ true,
1285
+ listed_until,
1286
+ );
1287
+ }
1288
+
1289
+ /// Purchase a unique-item Listing and write the item into the buyer's UserStorage.
1290
+ ///
1291
+ /// Payment is enforced at the framework level — callers MUST supply at least
1292
+ /// `listing.price` in `payment`. The function:
1293
+ /// 1. Transfers `price - fee` to the seller immediately.
1294
+ /// 2. Calls `settle_marketplace_fee` to split the fee between framework and DApp.
1295
+ /// 3. Writes the item data into `buyer_storage`.
1296
+ /// 4. Returns any change (payment surplus) to the caller.
1297
+ ///
1298
+ /// Security:
1299
+ /// - ctx.sender() must be canonical_owner(buyer_storage).
1300
+ /// - listing and buyer_storage must belong to the same DApp.
1301
+ /// - Buyer must not be the seller (prevents self-trade).
1302
+ /// - Listing must not have expired.
1303
+ /// - payment must cover the full listing price (EInsufficientPayment).
1304
+ public fun buy_record<DappKey: copy + drop, CoinType>(
1305
+ _auth: DappKey,
1306
+ dh: &DappHub,
1307
+ dapp_storage: &mut DappStorage,
1308
+ listing: Listing<CoinType>,
1309
+ buyer_storage: &mut UserStorage,
1310
+ mut payment: Coin<CoinType>,
1311
+ ctx: &mut TxContext,
1312
+ ): Coin<CoinType> {
1313
+ assert_framework_version(dh);
1314
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1315
+ error::dapp_key_mismatch(dapp_service::listing_dapp_key(&listing) == dapp_key_str);
1316
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(buyer_storage) == dapp_key_str);
1317
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
1318
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
1319
+
1320
+ // Buyer must own buyer_storage.
1321
+ error::no_permission(ctx.sender() == dapp_service::canonical_owner(buyer_storage));
1322
+ // Buyer must not be the seller (prevents self-trade exploits).
1323
+ error::no_permission(dapp_service::canonical_owner(buyer_storage) != dapp_service::listing_seller(&listing));
1324
+ // Listing must not have expired.
1325
+ error::scene_expired(!dapp_service::is_listing_expired(&listing, ctx.epoch_timestamp_ms()));
1326
+
1327
+ let price = dapp_service::listing_price(&listing);
1328
+ let seller = dapp_service::listing_seller(&listing);
1329
+ let fee_bps = dapp_service::marketplace_fee_bps(dapp_service::get_config(dh));
1330
+ let fee_amount = ((price as u256) * (fee_bps as u256) / 10_000u256) as u64;
1331
+ let seller_amount = price - fee_amount;
1332
+
1333
+ // Payment must cover the full listing price — enforced at the framework level.
1334
+ error::insufficient_payment(coin::value(&payment) >= price);
1335
+
1336
+ // Extract listing data before any mutations so existence check can run first.
1337
+ let listing_id = sui::object::uid_to_address(dapp_service::listing_id(&listing));
1338
+ let record_key = *dapp_service::listing_record_key(&listing);
1339
+ let field_names = *dapp_service::listing_field_names(&listing);
1340
+ let record_values = *dapp_service::listing_record_data(&listing);
1341
+ let ev_rec_type = *dapp_service::listing_record_type(&listing);
1342
+ let coin_type_str = type_info::get_type_name_string<CoinType>();
1343
+
1344
+ // ── All checks must pass before any payment mutations ────────────────────
1345
+ // Prevent silent overwrite: buyer must not already own a record with this key.
1346
+ error::item_already_owned(!dapp_service::has_user_record<DappKey>(buyer_storage, record_key));
1347
+
1348
+ if (seller_amount > 0) {
1349
+ let seller_coin = coin::split(&mut payment, seller_amount, ctx);
1350
+ transfer::public_transfer(seller_coin, seller);
1351
+ };
1352
+ if (fee_amount > 0) {
1353
+ let fee_coin = coin::split(&mut payment, fee_amount, ctx);
1354
+ settle_marketplace_fee<DappKey, CoinType>(
1355
+ _auth, dh, dapp_storage, fee_coin, listing_id, ctx
1356
+ );
1357
+ };
1358
+
1359
+ dapp_service::set_user_record<DappKey>(
1360
+ buyer_storage, record_key, field_names, record_values, false
1361
+ );
1362
+
1363
+ let (_, _, _, _, _, _, _, _) = dapp_service::destroy_listing(listing);
1364
+ dubhe_events::emit_item_sold(dapp_key_str, listing_id, ctx.sender(), seller, ev_rec_type, price, coin_type_str, false);
1365
+
1366
+ payment // return change
1367
+ }
1368
+
1369
+ /// Purchase a fungible Listing and ADD the listed amount to the buyer's existing balance.
1370
+ ///
1371
+ /// Payment is enforced at the framework level — callers MUST supply at least
1372
+ /// `listing.price` in `payment`. Identical payment split logic to `buy_record`.
1373
+ /// If the buyer already holds some of this resource the amounts are merged; if not,
1374
+ /// a new record is created.
1375
+ ///
1376
+ /// Security:
1377
+ /// - ctx.sender() must be canonical_owner(buyer_storage).
1378
+ /// - listing and buyer_storage must belong to the same DApp.
1379
+ /// - Buyer must not be the original seller (no self-trade).
1380
+ /// - Listing must not have expired.
1381
+ /// - payment must cover the full listing price (EInsufficientPayment).
1382
+ /// - field_name is read from the listing itself (not caller-supplied) to prevent mismatch.
1383
+ public fun buy_fungible_record<DappKey: copy + drop, CoinType>(
1384
+ _auth: DappKey,
1385
+ dh: &DappHub,
1386
+ dapp_storage: &mut DappStorage,
1387
+ listing: Listing<CoinType>,
1388
+ buyer_storage: &mut UserStorage,
1389
+ mut payment: Coin<CoinType>,
1390
+ ctx: &mut TxContext,
1391
+ ): Coin<CoinType> {
1392
+ assert_framework_version(dh);
1393
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1394
+ error::dapp_key_mismatch(dapp_service::listing_dapp_key(&listing) == dapp_key_str);
1395
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(buyer_storage) == dapp_key_str);
1396
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
1397
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
1398
+
1399
+ error::no_permission(ctx.sender() == dapp_service::canonical_owner(buyer_storage));
1400
+ // Buyer must not be the seller (prevents self-trade exploits).
1401
+ error::no_permission(dapp_service::canonical_owner(buyer_storage) != dapp_service::listing_seller(&listing));
1402
+ error::scene_expired(!dapp_service::is_listing_expired(&listing, ctx.epoch_timestamp_ms()));
1403
+
1404
+ let price = dapp_service::listing_price(&listing);
1405
+ let seller = dapp_service::listing_seller(&listing);
1406
+ let fee_bps = dapp_service::marketplace_fee_bps(dapp_service::get_config(dh));
1407
+ let fee_amount = ((price as u256) * (fee_bps as u256) / 10_000u256) as u64;
1408
+ let seller_amount = price - fee_amount;
1409
+
1410
+ // Payment must cover the full listing price — enforced at the framework level.
1411
+ error::insufficient_payment(coin::value(&payment) >= price);
1412
+
1413
+ // Extract listing_id early so it can be forwarded to settle_marketplace_fee
1414
+ // for the MarketplaceFeeSettled event.
1415
+ let listing_id = sui::object::uid_to_address(dapp_service::listing_id(&listing));
1416
+ let ev_rec_type = *dapp_service::listing_record_type(&listing);
1417
+ let coin_type_str = type_info::get_type_name_string<CoinType>();
1418
+
1419
+ if (seller_amount > 0) {
1420
+ let seller_coin = coin::split(&mut payment, seller_amount, ctx);
1421
+ transfer::public_transfer(seller_coin, seller);
1422
+ };
1423
+ if (fee_amount > 0) {
1424
+ let fee_coin = coin::split(&mut payment, fee_amount, ctx);
1425
+ settle_marketplace_fee<DappKey, CoinType>(
1426
+ _auth, dh, dapp_storage, fee_coin, listing_id, ctx
1427
+ );
1428
+ };
1429
+
1430
+ // Read listed amount directly from record_data (now vector<vector<u8>>).
1431
+ let record_values = *dapp_service::listing_record_data(&listing);
1432
+ let listed_amount_bytes = *record_values.borrow(0);
1433
+ let mut bcs2 = sui::bcs::new(listed_amount_bytes);
1434
+ let listed_amount = bcs2.peel_u64();
1435
+
1436
+ let record_key = *dapp_service::listing_record_key(&listing);
1437
+ // Use the field name stored in the listing (set at listing creation time) to prevent
1438
+ // caller-supplied field_name mismatch attacks.
1439
+ let field_name = *dapp_service::listing_field_names(&listing).borrow(0);
1440
+ let field_names = vector[field_name];
1441
+
1442
+ // Read buyer's current balance (0 if no record exists yet).
1443
+ let buyer_current = if (dapp_service::has_user_record<DappKey>(buyer_storage, record_key)) {
1444
+ let current_bytes = dapp_service::get_user_field<DappKey>(buyer_storage, record_key, field_name);
1445
+ let mut bcs3 = sui::bcs::new(current_bytes);
1446
+ bcs3.peel_u64()
1447
+ } else {
1448
+ 0u64
1449
+ };
1450
+
1451
+ // Use u256 arithmetic to detect u64 overflow before casting back.
1452
+ let new_amount_u256 = (buyer_current as u256) + (listed_amount as u256);
1453
+ error::math_overflow(new_amount_u256 <= 18_446_744_073_709_551_615u256);
1454
+ let new_amount = new_amount_u256 as u64;
1455
+ let new_bytes = sui::bcs::to_bytes(&new_amount);
1456
+
1457
+ dapp_service::set_user_record<DappKey>(
1458
+ buyer_storage, record_key, field_names, vector[new_bytes], false
1459
+ );
1460
+
1461
+ let (_, _, _, _, _, _, _, _) = dapp_service::destroy_listing(listing);
1462
+ dubhe_events::emit_item_sold(dapp_key_str, listing_id, ctx.sender(), seller, ev_rec_type, price, coin_type_str, true);
1463
+
1464
+ payment // return change
1465
+ }
1466
+
1467
+ /// Expire a Listing that has passed its `listed_until` deadline (unique item).
1468
+ /// The item is restored to the original seller's storage.
1469
+ public fun expire_listing<DappKey: copy + drop, CoinType>(
1470
+ _auth: DappKey,
1471
+ listing: Listing<CoinType>,
1472
+ seller_storage: &mut UserStorage,
1473
+ ctx: &TxContext,
1474
+ ) {
1475
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1476
+ error::dapp_key_mismatch(dapp_service::listing_dapp_key(&listing) == dapp_key_str);
1477
+ // seller_storage must belong to the same DApp as the listing (prevents cross-DApp writes).
1478
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(seller_storage) == dapp_key_str);
1479
+
1480
+ // Must actually be expired.
1481
+ error::scene_expired(dapp_service::is_listing_expired(&listing, ctx.epoch_timestamp_ms()));
1482
+
1483
+ let seller = dapp_service::listing_seller(&listing);
1484
+ error::no_permission(seller == dapp_service::canonical_owner(seller_storage));
1485
+
1486
+ let record_key = *dapp_service::listing_record_key(&listing);
1487
+ let field_names = *dapp_service::listing_field_names(&listing);
1488
+ let record_values = *dapp_service::listing_record_data(&listing);
1489
+
1490
+ // Capture event data before consuming the listing.
1491
+ let listing_id = sui::object::uid_to_address(dapp_service::listing_id(&listing));
1492
+
1493
+ dapp_service::set_user_record<DappKey>(
1494
+ seller_storage, record_key, field_names, record_values, false
1495
+ );
1496
+
1497
+ let (_, _, _, _, _, _, _, _) = dapp_service::destroy_listing(listing);
1498
+ dubhe_events::emit_listing_expired(dapp_key_str, listing_id, seller, false);
1499
+ }
1500
+
1501
+ /// Cancel a fungible Listing — ADDS the listed amount back to the seller's balance.
1502
+ ///
1503
+ /// Unlike `restore_record` (which overwrites), this function reads the seller's
1504
+ /// current balance and merges the listed amount on top, preventing the overwrite
1505
+ /// bug when the seller has accumulated more of the same resource since listing.
1506
+ ///
1507
+ /// Security:
1508
+ /// - Only the original seller may cancel.
1509
+ /// - listing and seller_storage must belong to the same DApp.
1510
+ /// - field_name is read from the listing itself (not caller-supplied) to prevent mismatch.
1511
+ public fun cancel_fungible_listing<DappKey: copy + drop, CoinType>(
1512
+ _auth: DappKey,
1513
+ listing: Listing<CoinType>,
1514
+ seller_storage: &mut UserStorage,
1515
+ ctx: &TxContext,
1516
+ ) {
1517
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1518
+ error::dapp_key_mismatch(dapp_service::listing_dapp_key(&listing) == dapp_key_str);
1519
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(seller_storage) == dapp_key_str);
1520
+
1521
+ let seller = dapp_service::listing_seller(&listing);
1522
+ error::no_permission(ctx.sender() == seller);
1523
+ error::no_permission(seller == dapp_service::canonical_owner(seller_storage));
1524
+
1525
+ let record_key = *dapp_service::listing_record_key(&listing);
1526
+ // Use the field name stored in the listing to prevent caller-supplied field_name mismatch.
1527
+ let field_name = *dapp_service::listing_field_names(&listing).borrow(0);
1528
+ let field_names = vector[field_name];
1529
+
1530
+ // Capture event data before consuming the listing.
1531
+ let listing_id = sui::object::uid_to_address(dapp_service::listing_id(&listing));
1532
+
1533
+ // Read listed amount directly from record_data (now vector<vector<u8>>).
1534
+ let record_values = *dapp_service::listing_record_data(&listing);
1535
+ let listed_amount_bytes = *record_values.borrow(0);
1536
+ let mut bcs2 = sui::bcs::new(listed_amount_bytes);
1537
+ let listed_amount = bcs2.peel_u64();
1538
+
1539
+ // Read seller's current balance (0 if record deleted while listing was live).
1540
+ let current = if (dapp_service::has_user_record<DappKey>(seller_storage, record_key)) {
1541
+ let current_bytes = dapp_service::get_user_field<DappKey>(seller_storage, record_key, field_name);
1542
+ let mut bcs3 = sui::bcs::new(current_bytes);
1543
+ bcs3.peel_u64()
1544
+ } else {
1545
+ 0u64
1546
+ };
1547
+
1548
+ // Use u256 arithmetic to detect u64 overflow before casting back.
1549
+ let new_amount_u256 = (current as u256) + (listed_amount as u256);
1550
+ error::math_overflow(new_amount_u256 <= 18_446_744_073_709_551_615u256);
1551
+ let new_amount = new_amount_u256 as u64;
1552
+ dapp_service::set_user_record<DappKey>(
1553
+ seller_storage, record_key, field_names, vector[sui::bcs::to_bytes(&new_amount)], false
1554
+ );
1555
+
1556
+ let (_, _, _, _, _, _, _, _) = dapp_service::destroy_listing(listing);
1557
+ dubhe_events::emit_listing_cancelled(dapp_key_str, listing_id, seller, true);
1558
+ }
1559
+
1560
+ /// Expire a fungible Listing — ADDS the listed amount back to the seller's balance.
1561
+ ///
1562
+ /// Anyone may call this for a listing that has passed its `listed_until` deadline.
1563
+ /// Uses additive merge (not overwrite) to prevent balance corruption when the seller
1564
+ /// has acquired more of the same resource since the listing was created.
1565
+ ///
1566
+ /// Security:
1567
+ /// - Listing must have actually expired.
1568
+ /// - seller_storage must belong to the same DApp as the listing (prevents cross-DApp writes).
1569
+ /// - seller_storage must belong to the original seller.
1570
+ /// - field_name is read from the listing itself (not caller-supplied) to prevent mismatch.
1571
+ public fun expire_fungible_listing<DappKey: copy + drop, CoinType>(
1572
+ _auth: DappKey,
1573
+ listing: Listing<CoinType>,
1574
+ seller_storage: &mut UserStorage,
1575
+ ctx: &TxContext,
1576
+ ) {
1577
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1578
+ error::dapp_key_mismatch(dapp_service::listing_dapp_key(&listing) == dapp_key_str);
1579
+ // seller_storage must belong to the same DApp as the listing (prevents cross-DApp writes).
1580
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(seller_storage) == dapp_key_str);
1581
+
1582
+ // Must actually be expired.
1583
+ error::scene_expired(dapp_service::is_listing_expired(&listing, ctx.epoch_timestamp_ms()));
1584
+
1585
+ let seller = dapp_service::listing_seller(&listing);
1586
+ error::no_permission(seller == dapp_service::canonical_owner(seller_storage));
1587
+
1588
+ let record_key = *dapp_service::listing_record_key(&listing);
1589
+ // Use the field name stored in the listing to prevent caller-supplied field_name mismatch.
1590
+ let field_name = *dapp_service::listing_field_names(&listing).borrow(0);
1591
+ let field_names = vector[field_name];
1592
+
1593
+ // Capture event data before consuming the listing.
1594
+ let listing_id = sui::object::uid_to_address(dapp_service::listing_id(&listing));
1595
+
1596
+ // Read listed amount directly from record_data (now vector<vector<u8>>).
1597
+ let record_values = *dapp_service::listing_record_data(&listing);
1598
+ let listed_amount_bytes = *record_values.borrow(0);
1599
+ let mut bcs2 = sui::bcs::new(listed_amount_bytes);
1600
+ let listed_amount = bcs2.peel_u64();
1601
+
1602
+ // Read seller's current balance (0 if record deleted while listing was live).
1603
+ let current = if (dapp_service::has_user_record<DappKey>(seller_storage, record_key)) {
1604
+ let current_bytes = dapp_service::get_user_field<DappKey>(seller_storage, record_key, field_name);
1605
+ let mut bcs3 = sui::bcs::new(current_bytes);
1606
+ bcs3.peel_u64()
1607
+ } else {
1608
+ 0u64
1609
+ };
1610
+
1611
+ // Use u256 arithmetic to detect u64 overflow before casting back.
1612
+ let new_amount_u256 = (current as u256) + (listed_amount as u256);
1613
+ error::math_overflow(new_amount_u256 <= 18_446_744_073_709_551_615u256);
1614
+ let new_amount = new_amount_u256 as u64;
1615
+ dapp_service::set_user_record<DappKey>(
1616
+ seller_storage, record_key, field_names, vector[sui::bcs::to_bytes(&new_amount)], false
1617
+ );
1618
+
1619
+ let (_, _, _, _, _, _, _, _) = dapp_service::destroy_listing(listing);
1620
+ dubhe_events::emit_listing_expired(dapp_key_str, listing_id, seller, true);
1621
+ }
1622
+
1623
+ // ─── Lazy Settlement ──────────────────────────────────────────────────────────
1624
+
1625
+ /// Settle accumulated write debt for a user.
1626
+ ///
1627
+ /// Uses the per-DApp fee rates stored in DappStorage (synced from DappHub via
1628
+ /// sync_dapp_fee). Pending DappHub fee changes do not apply until sync_dapp_fee
1629
+ /// is called after update_framework_fee has committed them.
1630
+ ///
1631
+ /// Behaviour when credit is insufficient:
1632
+ /// - Full balance available → full settlement (settled_count = write_count).
1633
+ /// - Partial balance, makes progress → partial settlement (proportional advance).
1634
+ /// - Partial balance, rounds to zero → skip (credit preserved, emit SettlementSkipped).
1635
+ /// - Zero balance / free fee → silent skip, emit SettlementSkipped event.
1636
+ ///
1637
+ /// This function NEVER aborts due to insufficient credit so it is safe to
1638
+ /// insert at the start of any PTB without risking user-transaction rollback.
1639
+ public fun settle_writes<DappKey: copy + drop>(
1640
+ dh: &DappHub,
1641
+ dapp_storage: &mut DappStorage,
1642
+ user_storage: &mut UserStorage,
1643
+ ctx: &TxContext,
1644
+ ) {
1645
+ assert_framework_version(dh);
1646
+
1647
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1648
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
1649
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
1650
+
1651
+ let unsettled_writes = dapp_service::unsettled_count(user_storage);
1652
+ let unsettled_bytes = dapp_service::unsettled_bytes(user_storage);
1653
+ if (unsettled_writes == 0 && unsettled_bytes == 0) { return };
1654
+
1655
+ // In USER_PAYS mode the user must provide a Coin via settle_writes_user_pays.
1656
+ error::wrong_settlement_mode(dapp_service::settlement_mode(dapp_storage) == SETTLEMENT_DAPP);
1657
+
1658
+ let now_ms = ctx.epoch_timestamp_ms();
1659
+ // Read per-DApp fee rates from DappStorage (synced via sync_dapp_fee from DappHub defaults).
1660
+ let base_fee = dapp_service::dapp_base_fee_per_write(dapp_storage);
1661
+ let bytes_fee = dapp_service::dapp_bytes_fee_per_byte(dapp_storage);
1662
+ let account = dapp_service::canonical_owner(user_storage);
1663
+
1664
+ // Free-tier: both fees are zero — mark everything settled at no cost.
1665
+ if (base_fee == 0 && bytes_fee == 0) {
1666
+ dapp_service::set_settled_to_write(user_storage);
1667
+ dubhe_events::emit_writes_settled(dapp_key_str, account, unsettled_writes, unsettled_bytes, 0, 0);
1668
+ return
1669
+ };
1670
+
1671
+ let total_cost = base_fee * (unsettled_writes as u256) + bytes_fee * unsettled_bytes;
1672
+ // DApp only owes the framework's write-fee share; the DApp-developer portion is not charged here.
1673
+ // This aligns DAPP_SUBSIDIZES with USER_PAYS: in both modes the framework collects only its cut.
1674
+ let share_bps = dapp_service::dapp_write_fee_share_bps(dapp_storage) as u256;
1675
+ let framework_cost = total_cost * (10000 - share_bps) / 10000;
1676
+
1677
+ // If the framework's share is zero (e.g. share_bps == 10000), writes cost nothing.
1678
+ if (framework_cost == 0) {
1679
+ dapp_service::set_settled_to_write(user_storage);
1680
+ dubhe_events::emit_writes_settled(dapp_key_str, account, unsettled_writes, unsettled_bytes, 0, 0);
1681
+ return
1682
+ };
1683
+
1684
+ // Effective free credit (0 if expired).
1685
+ let eff_free = dapp_service::effective_free_credit(dapp_storage, now_ms);
1686
+ // Total budget: free credit consumed first, then paid credit.
1687
+ let total_available = eff_free + dapp_service::credit_pool(dapp_storage);
1688
+
1689
+ if (total_available == 0) {
1690
+ dubhe_events::emit_settlement_skipped(
1691
+ dapp_key_str, account, unsettled_writes, unsettled_bytes,
1692
+ );
1693
+ return
1694
+ };
1695
+
1696
+ if (total_available >= framework_cost) {
1697
+ // Full settlement: exact cost deducted, all debt cleared.
1698
+ let free_used = if (eff_free >= framework_cost) { framework_cost } else { eff_free };
1699
+ let paid_used = framework_cost - free_used;
1700
+
1701
+ if (free_used > 0) { dapp_service::deduct_free_credit(dapp_storage, free_used); };
1702
+ if (paid_used > 0) {
1703
+ dapp_service::deduct_credit(dapp_storage, paid_used);
1704
+ dapp_service::add_total_settled(dapp_storage, paid_used);
1705
+ };
1706
+
1707
+ dapp_service::set_settled_to_write(user_storage);
1708
+ dubhe_events::emit_writes_settled(
1709
+ dapp_key_str, account, unsettled_writes, unsettled_bytes, free_used, paid_used,
1710
+ );
1711
+ dapp_service::emit_fee_state_record<DappKey>(dapp_storage);
1712
+ } else {
1713
+ // Partial settlement: compute proportional progress first.
1714
+ //
1715
+ // settled_writes = floor(total_available × unsettled_writes / framework_cost)
1716
+ // settled_bytes = floor(total_available × unsettled_bytes / framework_cost)
1717
+ //
1718
+ // If both round to zero the available credit is insufficient to retire even one
1719
+ // write unit. Deducting it anyway would consume DApp funds without making any
1720
+ // measurable progress. Treat this as a skip and preserve the credit.
1721
+ let settled_writes = ((total_available * (unsettled_writes as u256)) / framework_cost) as u64;
1722
+ let settled_bytes = (total_available * unsettled_bytes) / framework_cost;
1723
+
1724
+ if (settled_writes == 0 && settled_bytes == 0) {
1725
+ dubhe_events::emit_settlement_skipped(
1726
+ dapp_key_str, account, unsettled_writes, unsettled_bytes,
1727
+ );
1728
+ return
1729
+ };
1730
+
1731
+ // Compute exact cost for the proportionally settled portion only.
1732
+ // The DApp owes only the framework's revenue share (same ratio as full settlement):
1733
+ // exact_cost = settled_total_cost × (10000 − share_bps) / 10000
1734
+ // Using settled_total_cost × framework_ratio (instead of the raw total) ensures
1735
+ // exact_cost ≤ total_available and prevents arithmetic underflow in deduct_credit.
1736
+ let settled_total_cost = base_fee * (settled_writes as u256) + bytes_fee * settled_bytes;
1737
+ let exact_cost = settled_total_cost * (10000 - share_bps) / 10000;
1738
+ let free_used = if (eff_free >= exact_cost) { exact_cost } else { eff_free };
1739
+ let paid_used = exact_cost - free_used;
1740
+
1741
+ if (free_used > 0) { dapp_service::deduct_free_credit(dapp_storage, free_used); };
1742
+ if (paid_used > 0) {
1743
+ dapp_service::deduct_credit(dapp_storage, paid_used);
1744
+ dapp_service::add_total_settled(dapp_storage, paid_used);
1745
+ };
1746
+
1747
+ dapp_service::add_settled_count(user_storage, settled_writes);
1748
+ dapp_service::add_settled_bytes(user_storage, settled_bytes);
1749
+
1750
+ dubhe_events::emit_settlement_partial(
1751
+ dapp_key_str, account,
1752
+ settled_writes,
1753
+ settled_bytes,
1754
+ unsettled_writes - settled_writes,
1755
+ unsettled_bytes - settled_bytes,
1756
+ free_used,
1757
+ paid_used,
1758
+ );
1759
+ dapp_service::emit_fee_state_record<DappKey>(dapp_storage);
1760
+ };
1761
+ }
1762
+
1763
+ // ─── Session key management ────────────────────────────────────────────────────
1764
+ //
1765
+ // A "session key" is an ephemeral keypair generated by the game frontend.
1766
+ // The canonical owner authorises it once; the session key can then sign game
1767
+ // transactions without requiring the main wallet for every action.
1768
+ //
1769
+ // Unlike the old proxy model, UserStorage is NOT transferred. It stays as a
1770
+ // shared object reachable by both parties. The canonical owner can revoke
1771
+ // the session at any time, and a lost session key is never a lockout risk.
1772
+
1773
+ /// Authorise an ephemeral session key to write on behalf of the canonical owner.
1774
+ ///
1775
+ /// - Only the canonical owner may call this.
1776
+ /// - `session_wallet` must differ from the caller and must not be @0x0.
1777
+ /// - `duration_ms` controls expiry (1 min – 7 days). The expiry timestamp is
1778
+ /// stored as a Clock millisecond value, but write-path checks use
1779
+ /// ctx.epoch_timestamp_ms() (≈ 24 h granularity on mainnet/testnet, ≈ 1 h on
1780
+ /// devnet). Treat the deadline as a soft bound: the session may remain valid
1781
+ /// for up to one epoch after the Clock expiry. The canonical owner can always
1782
+ /// revoke early via deactivate_session.
1783
+ /// - Calling activate_session while a session is already active replaces it
1784
+ /// immediately — no need to deactivate first.
1785
+ public fun activate_session<DappKey: copy + drop>(
1786
+ dh: &DappHub,
1787
+ user_storage: &mut UserStorage,
1788
+ session_wallet: address,
1789
+ duration_ms: u64,
1790
+ clock: &Clock,
1791
+ ctx: &mut TxContext,
1792
+ ) {
1793
+ assert_framework_version(dh);
1794
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1795
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
1796
+
1797
+ let canonical = dapp_service::canonical_owner(user_storage);
1798
+ error::not_canonical_owner(canonical == ctx.sender());
1799
+
1800
+ error::invalid_session_key(session_wallet != @0x0);
1801
+ error::invalid_session_key(session_wallet != ctx.sender());
1802
+ error::invalid_session_duration(duration_ms >= MIN_SESSION_DURATION_MS);
1803
+ error::invalid_session_duration(duration_ms <= MAX_SESSION_DURATION_MS);
1804
+
1805
+ let expires_at = clock::timestamp_ms(clock) + duration_ms;
1806
+ dapp_service::set_session_key(user_storage, session_wallet);
1807
+ dapp_service::set_session_expires_at(user_storage, expires_at);
1808
+
1809
+ dubhe_events::emit_session_activated(dapp_key_str, canonical, session_wallet, expires_at);
1810
+ }
1811
+
1812
+ /// Deactivate the current session key.
1813
+ ///
1814
+ /// Allowed callers:
1815
+ /// - The canonical owner (revoke at any time, e.g. on browser refresh).
1816
+ /// - The session key itself (voluntary sign-out at end of game session).
1817
+ /// - Anyone, once the session has expired (cleanup).
1818
+ public fun deactivate_session<DappKey: copy + drop>(
1819
+ dh: &DappHub,
1820
+ user_storage: &mut UserStorage,
1821
+ ctx: &mut TxContext,
1822
+ ) {
1823
+ assert_framework_version(dh);
1824
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1825
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
1826
+
1827
+ // Must have an active session to deactivate.
1828
+ error::no_active_session(dapp_service::session_key(user_storage) != @0x0);
1829
+
1830
+ let sender = ctx.sender();
1831
+ let canonical = dapp_service::canonical_owner(user_storage);
1832
+ let sk = dapp_service::session_key(user_storage);
1833
+ let expires = dapp_service::session_expires_at(user_storage);
1834
+ let expired = expires > 0 && ctx.epoch_timestamp_ms() >= expires;
1835
+
1836
+ // Canonical owner may always deactivate; session key may deactivate itself;
1837
+ // anyone may clean up after natural expiry.
1838
+ error::no_permission(sender == canonical || sender == sk || expired);
1839
+
1840
+ dapp_service::clear_session(user_storage);
1841
+ dubhe_events::emit_session_deactivated(dapp_key_str, canonical, sk);
1842
+ }
1843
+
1844
+ // ─── Credit management ────────────────────────────────────────────────────────
1845
+
1846
+ /// Recharge a DApp's credit pool by paying with the framework's accepted coin type.
1847
+ /// Any account may call this — no admin restriction.
1848
+ /// Payment is forwarded to the framework treasury.
1849
+ /// Credits added at 1 base-unit = 1 credit unit (e.g. 1 MIST = 1 credit for SUI).
1850
+ /// The accepted coin type is stored in DappHub and can be changed by the treasury
1851
+ /// via propose_coin_type / accept_coin_type (requires a 48-hour delay).
1852
+ public fun recharge_credit<DappKey: copy + drop, CoinType>(
1853
+ dh: &DappHub,
1854
+ dapp_storage: &mut DappStorage,
1855
+ payment: Coin<CoinType>,
1856
+ ctx: &mut TxContext,
1857
+ ) {
1858
+ assert_framework_version(dh);
1859
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1860
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
1861
+
1862
+ // recharge_credit only makes sense in DAPP_SUBSIDIZES mode; in USER_PAYS mode
1863
+ // the credit_pool is not consumed by settlement, so depositing would be misleading.
1864
+ error::wrong_settlement_mode(
1865
+ dapp_service::settlement_mode(dapp_storage) == SETTLEMENT_DAPP
1866
+ );
1867
+
1868
+ // Verify the caller is paying with the currently accepted coin type.
1869
+ // type_name::with_defining_ids<CoinType>() is VM-generated from the actual type parameter and
1870
+ // includes the full package ID, so it cannot be spoofed via string manipulation.
1871
+ let cfg = dapp_service::get_fee_config(dh);
1872
+ let accepted = dapp_service::accepted_coin_type(cfg);
1873
+ error::wrong_payment_coin_type(
1874
+ option::is_some(accepted) && *option::borrow(accepted) == type_name::with_defining_ids<CoinType>()
1875
+ );
1876
+
1877
+ let amount = coin::value(&payment) as u256;
1878
+ error::insufficient_credit(amount > 0);
1879
+ let treasury = dapp_service::treasury(cfg);
1880
+ transfer::public_transfer(payment, treasury);
1881
+
1882
+ dapp_service::add_credit(dapp_storage, amount);
1883
+
1884
+ dubhe_events::emit_credit_recharged(
1885
+ dapp_key_str,
1886
+ ctx.sender(),
1887
+ type_name::with_defining_ids<CoinType>().into_string(),
1888
+ amount,
1889
+ );
1890
+ dapp_service::emit_fee_state_record<DappKey>(dapp_storage);
1891
+ }
1892
+
1893
+ // ─── USER_PAYS mode — settlement with payment and DApp revenue withdrawal ────────
1894
+
1895
+ /// Settle accumulated write debt in USER_PAYS mode by providing a Coin payment.
1896
+ ///
1897
+ /// The caller passes a coin (may be larger than needed — excess is returned as change).
1898
+ /// The exact cost is computed on-chain, split between framework treasury and DApp revenue,
1899
+ /// and writes are marked as fully settled.
1900
+ ///
1901
+ /// Aborts if:
1902
+ /// - DApp is not in USER_PAYS mode (wrong_settlement_mode)
1903
+ /// - CoinType does not match accepted type (wrong_payment_coin_type)
1904
+ /// - payment.value < total_cost (insufficient_credit)
1905
+ ///
1906
+ /// When there is nothing to settle, the payment is returned to the sender unchanged.
1907
+ /// When fee rates are zero, settlement is free and payment is returned unchanged.
1908
+ public fun settle_writes_user_pays<DappKey: copy + drop, CoinType>(
1909
+ dh: &DappHub,
1910
+ dapp_storage: &mut DappStorage,
1911
+ user_storage: &mut UserStorage,
1912
+ mut payment: Coin<CoinType>,
1913
+ ctx: &mut TxContext,
1914
+ ): Coin<CoinType> {
1915
+ assert_framework_version(dh);
1916
+
1917
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
1918
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
1919
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
1920
+
1921
+ // Must be in USER_PAYS mode.
1922
+ error::wrong_settlement_mode(dapp_service::settlement_mode(dapp_storage) == SETTLEMENT_USER);
1923
+
1924
+ // Validate coin type.
1925
+ let cfg = dapp_service::get_fee_config(dh);
1926
+ let accepted = dapp_service::accepted_coin_type(cfg);
1927
+ error::wrong_payment_coin_type(
1928
+ option::is_some(accepted) && *option::borrow(accepted) == type_name::with_defining_ids<CoinType>()
1929
+ );
1930
+
1931
+ let unsettled_writes = dapp_service::unsettled_count(user_storage);
1932
+ let unsettled_bytes = dapp_service::unsettled_bytes(user_storage);
1933
+ let account = dapp_service::canonical_owner(user_storage);
1934
+
1935
+ // Nothing to settle — return payment unchanged to the caller.
1936
+ if (unsettled_writes == 0 && unsettled_bytes == 0) {
1937
+ return payment
1938
+ };
1939
+
1940
+ let base_fee = dapp_service::dapp_base_fee_per_write(dapp_storage);
1941
+ let bytes_fee = dapp_service::dapp_bytes_fee_per_byte(dapp_storage);
1942
+
1943
+ // Free-tier — settle at no cost, return payment unchanged.
1944
+ if (base_fee == 0 && bytes_fee == 0) {
1945
+ dapp_service::set_settled_to_write(user_storage);
1946
+ dubhe_events::emit_writes_settled(dapp_key_str, account, unsettled_writes, unsettled_bytes, 0, 0);
1947
+ return payment
1948
+ };
1949
+
1950
+ let total_cost = base_fee * (unsettled_writes as u256) + bytes_fee * unsettled_bytes;
1951
+
1952
+ // Guard: total_cost must fit in u64.
1953
+ // Coin<T>.value() is bounded by u64::MAX, so if total_cost exceeds it
1954
+ // no payment can ever satisfy the debt — abort with a clear error rather
1955
+ // than letting the as u64 cast silently truncate.
1956
+ error::insufficient_credit(total_cost <= 18_446_744_073_709_551_615u256);
1957
+
1958
+ // Abort if payment is insufficient.
1959
+ error::insufficient_credit((coin::value(&payment) as u256) >= total_cost);
1960
+
1961
+ // Split exact cost out of payment; `payment` now holds the change to return.
1962
+ let mut exact_coin = coin::split(&mut payment, total_cost as u64, ctx);
1963
+
1964
+ // Split between framework treasury and DApp revenue.
1965
+ let share_bps = dapp_service::dapp_write_fee_share_bps(dapp_storage) as u256;
1966
+ let dapp_amount = (total_cost * share_bps / 10000) as u64;
1967
+ let fw_amount = total_cost as u64 - dapp_amount;
1968
+ let treasury = dapp_service::treasury(cfg);
1969
+
1970
+ // Only transfer to treasury when fw_amount > 0 (share_bps == 10000 means
1971
+ // 100% goes to the DApp; no zero-value Coin should be sent to treasury).
1972
+ if (fw_amount > 0) {
1973
+ let fw_coin = coin::split(&mut exact_coin, fw_amount, ctx);
1974
+ transfer::public_transfer(fw_coin, treasury);
1975
+ };
1976
+
1977
+ let dapp_bal = coin::into_balance(exact_coin);
1978
+ if (balance::value(&dapp_bal) > 0) {
1979
+ dapp_service::add_dapp_revenue<CoinType>(dapp_storage, dapp_bal);
1980
+ } else {
1981
+ balance::destroy_zero(dapp_bal);
1982
+ };
1983
+
1984
+ // Mark all writes as settled and update DApp-level accounting.
1985
+ dapp_service::set_settled_to_write(user_storage);
1986
+ dapp_service::add_total_settled(dapp_storage, total_cost);
1987
+
1988
+ dubhe_events::emit_writes_settled(
1989
+ dapp_key_str, account, unsettled_writes, unsettled_bytes, 0, total_cost,
1990
+ );
1991
+ dapp_service::emit_fee_state_record<DappKey>(dapp_storage);
1992
+ dapp_service::emit_revenue_state_record<DappKey, CoinType>(dapp_storage);
1993
+
1994
+ // Return the change coin to the caller (the PTB decides where it goes).
1995
+ payment
1996
+ }
1997
+
1998
+ /// Anyone can call this to flush accumulated DApp revenue to the DApp admin wallet.
1999
+ /// The coin is always sent to the stored `dapp_admin` address — the caller only
2000
+ /// pays gas and cannot redirect funds anywhere else.
2001
+ /// Aborts if there is no revenue to withdraw.
2002
+ ///
2003
+ /// Version-gated: after a framework upgrade this function must be called via the
2004
+ /// new package version to prevent stale code from touching DappStorage state.
2005
+ public fun withdraw_dapp_revenue<DappKey: copy + drop, CoinType>(
2006
+ dh: &DappHub,
2007
+ dapp_storage: &mut DappStorage,
2008
+ ctx: &mut TxContext,
2009
+ ) {
2010
+ assert_framework_version(dh);
2011
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2012
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2013
+
2014
+ let bal = dapp_service::take_dapp_revenue<CoinType>(dapp_storage);
2015
+ let amount = balance::value(&bal);
2016
+ error::no_revenue_to_withdraw(amount > 0);
2017
+
2018
+ let admin = dapp_service::dapp_admin(dapp_storage);
2019
+
2020
+ dubhe_events::emit_dapp_revenue_withdrawn(
2021
+ dapp_key_str,
2022
+ admin,
2023
+ type_name::with_defining_ids<CoinType>().into_string(),
2024
+ amount,
2025
+ );
2026
+
2027
+ transfer::public_transfer(coin::from_balance(bal, ctx), admin);
2028
+ }
2029
+
2030
+ /// DApp admin: switch settlement mode.
2031
+ ///
2032
+ /// Both directions are allowed:
2033
+ /// DAPP_SUBSIDIZES(0) → USER_PAYS(1): credit_pool is kept but becomes inactive —
2034
+ /// it cannot be consumed for settlement after the switch.
2035
+ /// Any unsettled user debt that existed before the switch must be paid by users
2036
+ /// via settle_writes_user_pays; the remaining credit_pool is NOT automatically
2037
+ /// refunded. DApp admin can withdraw any remaining balance manually if desired.
2038
+ /// USER_PAYS(1) → DAPP_SUBSIDIZES(0): DappStorage Revenue Balance is kept (withdrawable).
2039
+ /// Write-fee DApp share (write_fee_dapp_share_bps) is set exclusively by the framework admin
2040
+ /// via set_dapp_write_fee_share; DApp admin only controls the mode.
2041
+ public fun set_dapp_settlement_config<DappKey: copy + drop>(
2042
+ dh: &DappHub,
2043
+ dapp_storage: &mut DappStorage,
2044
+ mode: u8,
2045
+ ctx: &TxContext,
2046
+ ) {
2047
+ assert_framework_version(dh);
2048
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2049
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2050
+ error::no_permission(dapp_service::dapp_admin(dapp_storage) == ctx.sender());
2051
+ error::wrong_settlement_mode(mode == SETTLEMENT_DAPP || mode == SETTLEMENT_USER);
2052
+
2053
+ let old_mode = dapp_service::settlement_mode(dapp_storage);
2054
+ if (old_mode == mode) { return };
2055
+ dapp_service::set_settlement_mode(dapp_storage, mode);
2056
+
2057
+ dubhe_events::emit_settlement_mode_changed(dapp_key_str, old_mode, mode);
2058
+ }
2059
+
2060
+ /// Framework admin: set the write-fee DApp share for a specific DApp (immediate effect).
2061
+ ///
2062
+ /// `new_bps` is the percentage of write-fee settlement revenue allocated to the
2063
+ /// DApp developer. e.g. 3000 = 30% to DApp; 70% to framework treasury.
2064
+ /// Valid range: 0 – 10000 (0% – 100%).
2065
+ /// Takes effect on the next settle_writes / settle_writes_user_pays call for this DApp.
2066
+ public fun set_dapp_write_fee_share<DappKey: copy + drop>(
2067
+ dh: &DappHub,
2068
+ dapp_storage: &mut DappStorage,
2069
+ new_bps: u64,
2070
+ ctx: &TxContext,
2071
+ ) {
2072
+ assert_framework_version(dh);
2073
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2074
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2075
+ error::no_permission(
2076
+ dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender()
2077
+ );
2078
+ error::revenue_share_exceeds_max(new_bps <= 10_000);
2079
+
2080
+ dapp_service::set_write_fee_dapp_share_bps(dapp_storage, new_bps);
2081
+ dubhe_events::emit_dapp_revenue_share_set(dapp_key_str, new_bps);
2082
+ }
2083
+
2084
+ /// Framework admin: update the default write-fee DApp share for future newly created DApps.
2085
+ ///
2086
+ /// This does NOT retroactively affect existing DApps. Use set_dapp_write_fee_share
2087
+ /// to update individual DApps.
2088
+ /// Valid range: 0 – 10000.
2089
+ public fun update_default_revenue_share(
2090
+ dh: &mut DappHub,
2091
+ new_bps: u64,
2092
+ ctx: &TxContext,
2093
+ ) {
2094
+ assert_framework_version(dh);
2095
+ error::no_permission(
2096
+ dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender()
2097
+ );
2098
+ error::revenue_share_exceeds_max(new_bps <= 10_000);
2099
+
2100
+ dapp_service::set_default_write_fee_dapp_share_bps(dapp_service::get_config_mut(dh), new_bps);
2101
+ dubhe_events::emit_default_revenue_share_updated(new_bps);
2102
+ }
2103
+
2104
+ // ─── Marketplace fee management ───────────────────────────────────────────────
2105
+
2106
+ /// Framework admin: update the global marketplace fee rate.
2107
+ /// Maximum allowed value is 10000 (100%).
2108
+ public fun update_marketplace_fee(
2109
+ dh: &mut DappHub,
2110
+ fee_bps: u64,
2111
+ ctx: &TxContext,
2112
+ ) {
2113
+ assert_framework_version(dh);
2114
+ error::no_permission(
2115
+ dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender()
2116
+ );
2117
+ error::marketplace_fee_exceeds_max(fee_bps <= 10_000);
2118
+ dapp_service::set_marketplace_fee_bps(dapp_service::get_config_mut(dh), fee_bps);
2119
+ dubhe_events::emit_marketplace_fee_updated(fee_bps);
2120
+ }
2121
+
2122
+ /// Return the current global marketplace fee rate (basis points).
2123
+ ///
2124
+ /// Used by generated DApp buy functions to compute the fee to charge buyers.
2125
+ /// All DApps share the same global rate; there is no per-DApp override.
2126
+ public fun marketplace_fee_bps(dh: &DappHub): u64 {
2127
+ assert_framework_version(dh);
2128
+ dapp_service::marketplace_fee_bps(dapp_service::get_config(dh))
2129
+ }
2130
+
2131
+ /// Framework admin: update the DApp's share of the marketplace fee (basis points).
2132
+ ///
2133
+ /// e.g. 5000 = 50% of total fee goes to the DApp; remainder to framework treasury.
2134
+ /// Maximum allowed value is 10000 (100% to DApp, 0% to framework).
2135
+ public fun update_marketplace_dapp_share(
2136
+ dh: &mut DappHub,
2137
+ share_bps: u64,
2138
+ ctx: &TxContext,
2139
+ ) {
2140
+ assert_framework_version(dh);
2141
+ error::no_permission(
2142
+ dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender()
2143
+ );
2144
+ error::marketplace_fee_exceeds_max(share_bps <= 10_000);
2145
+ dapp_service::set_marketplace_dapp_share_bps(dapp_service::get_config_mut(dh), share_bps);
2146
+ }
2147
+
2148
+ /// Settle a marketplace fee coin: split into framework and DApp portions.
2149
+ ///
2150
+ /// Called from generated DApp `buy` functions after the seller has received their
2151
+ /// exact listing price. The fee coin is consumed entirely — no value is returned.
2152
+ /// - Framework portion is transferred to the framework treasury.
2153
+ /// - DApp portion is credited to DappStorage's revenue balance.
2154
+ public fun settle_marketplace_fee<DappKey: copy + drop, CoinType>(
2155
+ _auth: DappKey,
2156
+ dh: &DappHub,
2157
+ dapp_storage: &mut DappStorage,
2158
+ mut fee_coin: Coin<CoinType>,
2159
+ listing_id: address,
2160
+ ctx: &mut TxContext,
2161
+ ) {
2162
+ assert_framework_version(dh);
2163
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2164
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2165
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
2166
+
2167
+ let total_fee = coin::value(&fee_coin);
2168
+ if (total_fee == 0) {
2169
+ coin::destroy_zero(fee_coin);
2170
+ return
2171
+ };
2172
+
2173
+ let share_bps = dapp_service::marketplace_dapp_share_bps(dapp_service::get_config(dh)) as u256;
2174
+ let dapp_amount = ((total_fee as u256) * share_bps / 10_000) as u64;
2175
+ let fw_amount = total_fee - dapp_amount;
2176
+ let cfg = dapp_service::get_fee_config(dh);
2177
+ let treasury = dapp_service::treasury(cfg);
2178
+
2179
+ if (fw_amount > 0) {
2180
+ let fw_coin = coin::split(&mut fee_coin, fw_amount, ctx);
2181
+ transfer::public_transfer(fw_coin, treasury);
2182
+ };
2183
+
2184
+ if (dapp_amount > 0) {
2185
+ let dapp_bal = coin::into_balance(fee_coin);
2186
+ dapp_service::add_dapp_revenue<CoinType>(dapp_storage, dapp_bal);
2187
+ } else {
2188
+ coin::destroy_zero(fee_coin);
2189
+ };
2190
+
2191
+ let coin_type_str = type_info::get_type_name_string<CoinType>();
2192
+ dubhe_events::emit_marketplace_fee_settled(
2193
+ dapp_key_str, listing_id, coin_type_str,
2194
+ total_fee, fw_amount, dapp_amount,
2195
+ );
2196
+ dapp_service::emit_revenue_state_record<DappKey, CoinType>(dapp_storage);
2197
+ }
2198
+
2199
+ // ─── Write limit management ───────────────────────────────────────────────────
2200
+
2201
+ /// Sync the framework's current write limit into a UserStorage.
2202
+ ///
2203
+ /// Call this after `set_framework_max_write_limit` to propagate the new limit
2204
+ /// to specific users. The client can compare `user_write_limit(us)` with
2205
+ /// `framework_max_write_limit(get_config(dh))` to detect whether a sync is
2206
+ /// needed before the user starts playing.
2207
+ ///
2208
+ /// Requirements:
2209
+ /// - DappKey type must match the UserStorage's dapp_key.
2210
+ public fun sync_user_write_limit<DappKey: copy + drop>(
2211
+ dapp_hub: &DappHub,
2212
+ user_storage: &mut UserStorage,
2213
+ ) {
2214
+ assert_framework_version(dapp_hub);
2215
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2216
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
2217
+ let new_limit = dapp_service::framework_max_write_limit(dapp_service::get_config(dapp_hub));
2218
+ dapp_service::set_user_write_limit(user_storage, new_limit);
2219
+ let owner = dapp_service::canonical_owner(user_storage);
2220
+ dubhe_events::emit_user_write_limit_synced(dapp_key_str, owner, new_limit);
2221
+ }
2222
+
2223
+ /// Framework admin: set the absolute ceiling on per-user unsettled writes.
2224
+ ///
2225
+ /// This is the single source of truth for write limits. New UserStorage objects
2226
+ /// snapshot this value at creation time. Existing UserStorage objects update via
2227
+ /// sync_user_write_limit. Constraint: max >= 1.
2228
+ public fun set_framework_max_write_limit(
2229
+ dh: &mut DappHub,
2230
+ max: u64,
2231
+ ctx: &TxContext,
2232
+ ) {
2233
+ assert_framework_version(dh);
2234
+ error::no_permission(
2235
+ dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender()
2236
+ );
2237
+ error::write_limit_out_of_range(max >= 1);
2238
+ dapp_service::set_framework_max_write_limit_cfg(dapp_service::get_config_mut(dh), max);
2239
+ dubhe_events::emit_framework_max_write_limit_updated(max, ctx.sender());
2240
+ }
2241
+
2242
+ // ─── Free credit management ───────────────────────────────────────────────────
2243
+ //
2244
+ // Framework admin controls the virtual free credit pool for each DApp.
2245
+ // Free credit has no SUI backing — it is a promotional subsidy paid by the
2246
+ // framework operator. It is consumed before the DApp's paid credit_pool.
2247
+
2248
+ /// Framework admin: grant (or override) virtual free credit to a DApp.
2249
+ ///
2250
+ /// This is an override operation: the existing free_credit balance and expiry
2251
+ /// are completely replaced. To extend time only, use extend_free_credit.
2252
+ ///
2253
+ /// - `amount`: new free credit in MIST (25 SUI = 25_000_000_000).
2254
+ /// - `expires_at`: epoch ms after which this credit is void; 0 = never expires.
2255
+ public fun grant_free_credit<DappKey: copy + drop>(
2256
+ dh: &DappHub,
2257
+ dapp_storage: &mut DappStorage,
2258
+ amount: u256,
2259
+ expires_at: u64,
2260
+ ctx: &mut TxContext,
2261
+ ) {
2262
+ assert_framework_version(dh);
2263
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2264
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2265
+ error::no_permission(dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender());
2266
+
2267
+ dapp_service::set_free_credit(dapp_storage, amount, expires_at);
2268
+ dubhe_events::emit_free_credit_granted(dapp_key_str, amount, expires_at, ctx.sender());
2269
+ dapp_service::emit_fee_state_record<DappKey>(dapp_storage);
2270
+ }
2271
+
2272
+ /// Framework admin: revoke all remaining free credit from a DApp immediately.
2273
+ public fun revoke_free_credit<DappKey: copy + drop>(
2274
+ dh: &DappHub,
2275
+ dapp_storage: &mut DappStorage,
2276
+ ctx: &mut TxContext,
2277
+ ) {
2278
+ assert_framework_version(dh);
2279
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2280
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2281
+ error::no_permission(dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender());
2282
+
2283
+ let remaining = dapp_service::free_credit(dapp_storage);
2284
+ if (remaining == 0) { return };
2285
+ dapp_service::set_free_credit(dapp_storage, 0, 0);
2286
+ dubhe_events::emit_free_credit_revoked(dapp_key_str, remaining, ctx.sender());
2287
+ dapp_service::emit_fee_state_record<DappKey>(dapp_storage);
2288
+ }
2289
+
2290
+ /// Framework admin: extend (or shorten) the expiry of a DApp's free credit.
2291
+ /// Does not change the amount. Use grant_free_credit to change the amount.
2292
+ ///
2293
+ /// - `new_expires_at`: new expiry in epoch ms; 0 = never expires.
2294
+ public fun extend_free_credit<DappKey: copy + drop>(
2295
+ dh: &DappHub,
2296
+ dapp_storage: &mut DappStorage,
2297
+ new_expires_at: u64,
2298
+ ctx: &mut TxContext,
2299
+ ) {
2300
+ assert_framework_version(dh);
2301
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2302
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2303
+ error::no_permission(dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender());
2304
+
2305
+ let current_amount = dapp_service::free_credit(dapp_storage);
2306
+ dapp_service::set_free_credit(dapp_storage, current_amount, new_expires_at);
2307
+ dubhe_events::emit_free_credit_extended(dapp_key_str, new_expires_at, ctx.sender());
2308
+ }
2309
+
2310
+ /// Framework admin: update the default free credit granted to future new DApps.
2311
+ ///
2312
+ /// - `new_amount`: MIST to grant; 0 disables auto-grant.
2313
+ /// - `new_duration_ms`: validity window in ms; 0 = never expires.
2314
+ /// 6 months ≈ 15_778_800_000 ms.
2315
+ public fun update_default_free_credit(
2316
+ dh: &mut DappHub,
2317
+ new_amount: u256,
2318
+ new_duration_ms: u64,
2319
+ ctx: &mut TxContext,
2320
+ ) {
2321
+ assert_framework_version(dh);
2322
+ error::no_permission(dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender());
2323
+ dapp_service::set_default_free_credit(dapp_service::get_config_mut(dh), new_amount, new_duration_ms);
2324
+ dubhe_events::emit_default_free_credit_updated(new_amount, new_duration_ms, ctx.sender());
2325
+ }
2326
+
2327
+ // ─── Framework config management ─────────────────────────────────────────────
2328
+ //
2329
+ // The framework admin manages operational parameters stored in DappHub.config.
2330
+ // This is separate from the treasury which manages financial operations.
2331
+
2332
+ /// Step 1: Current framework admin proposes a new admin address.
2333
+ /// Only the current framework admin can call this.
2334
+ /// Propose @0x0 to cancel a pending proposal.
2335
+ public fun propose_framework_admin(
2336
+ dh: &mut DappHub,
2337
+ new_admin: address,
2338
+ ctx: &TxContext,
2339
+ ) {
2340
+ assert_framework_version(dh);
2341
+ let cfg = dapp_service::get_config_mut(dh);
2342
+ error::no_permission(dapp_service::framework_admin(cfg) == ctx.sender());
2343
+ dapp_service::set_pending_framework_admin(cfg, new_admin);
2344
+ }
2345
+
2346
+ /// Step 2: Pending framework admin accepts, completing the rotation.
2347
+ /// Only the pending framework admin can call this.
2348
+ public fun accept_framework_admin(
2349
+ dh: &mut DappHub,
2350
+ ctx: &TxContext,
2351
+ ) {
2352
+ assert_framework_version(dh);
2353
+ let cfg = dapp_service::get_config_mut(dh);
2354
+ let pending = dapp_service::pending_framework_admin(cfg);
2355
+ error::no_pending_ownership_transfer(pending != @0x0);
2356
+ error::no_permission(pending == ctx.sender());
2357
+ dapp_service::set_framework_admin(cfg, pending);
2358
+ dapp_service::set_pending_framework_admin(cfg, @0x0);
2359
+ }
2360
+
2361
+ // ─── Framework version management ────────────────────────────────────────────
2362
+
2363
+ /// Bump DappHub.version to the current FRAMEWORK_VERSION.
2364
+ /// Called from migrate() after a package upgrade to enable version-gated
2365
+ /// lifecycle functions from the new package while blocking the old package.
2366
+ ///
2367
+ /// MONOTONIC: the version only ever increases. This prevents an attack where
2368
+ /// a caller invokes an older package's migrate::run after a new package has
2369
+ /// already bumped the version, which would otherwise reset DappHub.version to
2370
+ /// the old constant and re-enable old clients.
2371
+ ///
2372
+ /// `public(package)` restricts this to the genesis/migrate modules within the
2373
+ /// same package — external callers cannot bump the version.
2374
+ public(package) fun bump_framework_version(dh: &mut DappHub) {
2375
+ let current = dapp_service::framework_version(dh);
2376
+ if (FRAMEWORK_VERSION > current) {
2377
+ dapp_service::set_framework_version(dh, FRAMEWORK_VERSION);
2378
+ };
2379
+ }
2380
+
2381
+ // ─── Framework fee management ─────────────────────────────────────────────────
2382
+
2383
+ /// Initialise the FrameworkFeeConfig (called once from deploy_hook).
2384
+ /// Silently skips if already initialised.
2385
+ public(package) fun initialize_framework_fee<CoinType>(
2386
+ dh: &mut DappHub,
2387
+ base_fee: u256,
2388
+ bytes_fee: u256,
2389
+ treasury: address,
2390
+ revenue_share_bps: u64,
2391
+ _ctx: &mut TxContext,
2392
+ ) {
2393
+ if (dapp_service::is_fee_config_initialized(dh)) { return };
2394
+
2395
+ let cfg = dapp_service::get_fee_config_mut(dh);
2396
+ dapp_service::set_base_fee_per_write(cfg, base_fee);
2397
+ dapp_service::set_bytes_fee_per_byte(cfg, bytes_fee);
2398
+ dapp_service::set_treasury(cfg, treasury);
2399
+ dapp_service::set_accepted_coin_type(cfg, type_name::with_defining_ids<CoinType>());
2400
+
2401
+ // Also initialise the settlement defaults — both are one-shot and share
2402
+ // the same idempotency guard (is_fee_config_initialized).
2403
+ let scfg = dapp_service::get_config_mut(dh);
2404
+ dapp_service::set_default_write_fee_dapp_share_bps(scfg, revenue_share_bps);
2405
+ }
2406
+
2407
+ /// Update both fee components atomically.
2408
+ ///
2409
+ /// All fee changes (increases and decreases) are scheduled with a 48-hour
2410
+ /// delay before taking effect. This gives DApps and users consistent advance
2411
+ /// notice regardless of direction.
2412
+ ///
2413
+ /// No-op if the requested fees are identical to the current committed fees.
2414
+ /// Calling again before the delay has elapsed replaces the pending change
2415
+ /// and resets the 48-hour timer.
2416
+ /// Caller must be the framework admin address.
2417
+ public fun update_framework_fee(
2418
+ dh: &mut DappHub,
2419
+ new_base_fee: u256,
2420
+ new_bytes_fee: u256,
2421
+ clock: &Clock,
2422
+ ctx: &mut TxContext,
2423
+ ) {
2424
+ assert_framework_version(dh);
2425
+
2426
+ let admin = dapp_service::framework_admin(dapp_service::get_config(dh));
2427
+ error::no_permission(admin == ctx.sender());
2428
+
2429
+ let now = clock::timestamp_ms(clock);
2430
+ let cfg = dapp_service::get_fee_config_mut(dh);
2431
+
2432
+ // Commit any matured pending fees first.
2433
+ let effective_at = dapp_service::fee_effective_at_ms(cfg);
2434
+ if (effective_at > 0 && now >= effective_at) {
2435
+ let pb = dapp_service::pending_base_fee(cfg);
2436
+ let py = dapp_service::pending_bytes_fee(cfg);
2437
+ dapp_service::set_base_fee_per_write(cfg, pb);
2438
+ dapp_service::set_bytes_fee_per_byte(cfg, py);
2439
+ dapp_service::push_fee_history(cfg, pb, py, effective_at);
2440
+ dapp_service::set_pending_base_fee(cfg, 0);
2441
+ dapp_service::set_pending_bytes_fee(cfg, 0);
2442
+ dapp_service::set_fee_effective_at_ms(cfg, 0);
2443
+ dubhe_events::emit_fee_updated(pb, py, effective_at);
2444
+ };
2445
+
2446
+ // No-op if the committed fees are already at the requested values.
2447
+ let cur_base = dapp_service::base_fee_per_write(cfg);
2448
+ let cur_bytes = dapp_service::bytes_fee_per_byte(cfg);
2449
+ if (new_base_fee == cur_base && new_bytes_fee == cur_bytes) { return };
2450
+
2451
+ // Schedule with a 48-hour delay regardless of direction.
2452
+ let effective_at_ms = now + MIN_FEE_INCREASE_DELAY_MS;
2453
+ dapp_service::set_pending_base_fee(cfg, new_base_fee);
2454
+ dapp_service::set_pending_bytes_fee(cfg, new_bytes_fee);
2455
+ dapp_service::set_fee_effective_at_ms(cfg, effective_at_ms);
2456
+ dubhe_events::emit_fee_update_scheduled(new_base_fee, new_bytes_fee, effective_at_ms);
2457
+ }
2458
+
2459
+ /// Return the currently effective (base_fee, bytes_fee) pair at `now_ms`.
2460
+ ///
2461
+ /// If the pending fees have matured (now_ms >= fee_effective_at_ms), the
2462
+ /// pending values are returned. The pending fees are NOT committed to storage
2463
+ /// here; that happens on the next call to update_framework_fee.
2464
+ ///
2465
+ /// Note: settle_writes reads per-DApp snapshot rates from DappStorage (set via
2466
+ /// sync_dapp_fee), not from this function. Use sync_dapp_fee after a pending fee
2467
+ /// change has been committed to propagate the new rates to each DApp.
2468
+ public fun get_effective_fees_at(dh: &DappHub, now_ms: u64): (u256, u256) {
2469
+ assert_framework_version(dh);
2470
+ let cfg = dapp_service::get_fee_config(dh);
2471
+ let effective_at = dapp_service::fee_effective_at_ms(cfg);
2472
+ let pb = dapp_service::pending_base_fee(cfg);
2473
+ let py = dapp_service::pending_bytes_fee(cfg);
2474
+ if (effective_at > 0 && now_ms >= effective_at) {
2475
+ (pb, py)
2476
+ } else {
2477
+ (
2478
+ dapp_service::base_fee_per_write(cfg),
2479
+ dapp_service::bytes_fee_per_byte(cfg),
2480
+ )
2481
+ }
2482
+ }
2483
+
2484
+ // ─── Revenue-share cap helpers ────────────────────────────────────────────────
2485
+
2486
+ /// Return the current base-fee and bytes-fee without accounting for pending increases.
2487
+ public fun get_effective_fees(dh: &DappHub): (u256, u256) {
2488
+ assert_framework_version(dh);
2489
+ let cfg = dapp_service::get_fee_config(dh);
2490
+ (
2491
+ dapp_service::base_fee_per_write(cfg),
2492
+ dapp_service::bytes_fee_per_byte(cfg),
2493
+ )
2494
+ }
2495
+
2496
+ // ─── Framework treasury rotation ─────────────────────────────────────────────
2497
+ //
2498
+ // Two-step treasury transfer (mirrors DApp Ownable2Step pattern):
2499
+ // Step 1: Current treasury proposes a new treasury address.
2500
+ // Step 2: New treasury accepts, completing the rotation.
2501
+ // Either party can cancel by proposing @0x0 (step 1) or simply ignoring.
2502
+
2503
+ /// Step 1: Current treasury proposes a new treasury address.
2504
+ /// Only the current treasury can call this.
2505
+ public fun propose_treasury(
2506
+ dh: &mut DappHub,
2507
+ new_treasury: address,
2508
+ ctx: &TxContext,
2509
+ ) {
2510
+ assert_framework_version(dh);
2511
+ let cfg = dapp_service::get_fee_config_mut(dh);
2512
+ error::no_permission(dapp_service::treasury(cfg) == ctx.sender());
2513
+ dapp_service::set_pending_treasury(cfg, new_treasury);
2514
+ }
2515
+
2516
+ /// Step 2: New treasury accepts, completing the rotation.
2517
+ /// Only the pending treasury can call this.
2518
+ public fun accept_treasury(
2519
+ dh: &mut DappHub,
2520
+ ctx: &TxContext,
2521
+ ) {
2522
+ assert_framework_version(dh);
2523
+ let cfg = dapp_service::get_fee_config_mut(dh);
2524
+ let pending = dapp_service::pending_treasury(cfg);
2525
+ error::no_pending_ownership_transfer(pending != @0x0);
2526
+ error::no_permission(pending == ctx.sender());
2527
+ dapp_service::set_treasury(cfg, pending);
2528
+ dapp_service::set_pending_treasury(cfg, @0x0);
2529
+ }
2530
+
2531
+ // ─── Payment coin type management (framework admin) ──────────────────────────
2532
+ //
2533
+ // The accepted payment coin type (CoinType) can be changed by the framework admin
2534
+ // with a mandatory 48-hour notice period, giving DApp operators time to update
2535
+ // their recharge flows before the old coin type stops being accepted.
2536
+ //
2537
+ // Step 1 framework admin calls propose_coin_type<NewCoinType> — schedules the change.
2538
+ // Step 2 framework admin calls accept_coin_type after the delay — commits the change.
2539
+ //
2540
+ // Coin-type management belongs to the framework admin because it is a
2541
+ // protocol-level decision (what token the entire platform accepts), not a
2542
+ // treasury wallet concern.
2543
+
2544
+ /// Step 1: Framework admin schedules a payment coin type change with a 48-hour delay.
2545
+ /// Emits CoinTypeChangeProposed so off-chain systems can prepare.
2546
+ /// Calling again before the delay has elapsed replaces the pending change.
2547
+ public fun propose_coin_type<NewCoinType>(
2548
+ dh: &mut DappHub,
2549
+ clock: &Clock,
2550
+ ctx: &TxContext,
2551
+ ) {
2552
+ assert_framework_version(dh);
2553
+
2554
+ error::no_permission(
2555
+ dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender()
2556
+ );
2557
+
2558
+ let cfg = dapp_service::get_fee_config_mut(dh);
2559
+ let effective_at_ms = clock::timestamp_ms(clock) + MIN_FEE_INCREASE_DELAY_MS;
2560
+ dapp_service::set_pending_coin_type(cfg, option::some(type_name::with_defining_ids<NewCoinType>()));
2561
+ dapp_service::set_coin_type_effective_at_ms(cfg, effective_at_ms);
2562
+
2563
+ dubhe_events::emit_coin_type_change_proposed(
2564
+ type_name::with_defining_ids<NewCoinType>().into_string(),
2565
+ effective_at_ms,
2566
+ );
2567
+ }
2568
+
2569
+ /// Step 2: Framework admin commits the pending coin type change after the delay.
2570
+ /// Aborts if there is no pending change or the delay has not elapsed yet.
2571
+ public fun accept_coin_type(
2572
+ dh: &mut DappHub,
2573
+ clock: &Clock,
2574
+ ctx: &TxContext,
2575
+ ) {
2576
+ assert_framework_version(dh);
2577
+
2578
+ error::no_permission(
2579
+ dapp_service::framework_admin(dapp_service::get_config(dh)) == ctx.sender()
2580
+ );
2581
+
2582
+ let cfg = dapp_service::get_fee_config_mut(dh);
2583
+ let pending = dapp_service::pending_coin_type(cfg);
2584
+ error::no_pending_coin_type_change(option::is_some(pending));
2585
+
2586
+ let effective_at = dapp_service::coin_type_effective_at_ms(cfg);
2587
+ error::coin_type_change_not_ready(clock::timestamp_ms(clock) >= effective_at);
2588
+
2589
+ let new_type = *option::borrow(pending);
2590
+ dapp_service::set_accepted_coin_type(cfg, new_type);
2591
+ dapp_service::set_pending_coin_type(cfg, option::none());
2592
+ dapp_service::set_coin_type_effective_at_ms(cfg, 0);
2593
+
2594
+ dubhe_events::emit_coin_type_changed(new_type.into_string());
2595
+ }
2596
+
2597
+ // ─── DApp metadata management ────────────────────────────────────────────────
2598
+
2599
+ // ─── Per-DApp fee rate management ────────────────────────────────────────────
2600
+
2601
+ /// Pull the current DappHub effective fee rates into a DappStorage.
2602
+ /// Permissionless: any caller may trigger a sync to keep a DApp's rates
2603
+ /// aligned with the latest framework defaults after update_framework_fee.
2604
+ ///
2605
+ /// Because update_framework_fee schedules all changes with a 48-hour delay,
2606
+ /// the typical flow is:
2607
+ /// 1. update_framework_fee(dh, new_fees, clock, ctx) — schedules the pending change.
2608
+ /// 2. Wait 48 hours.
2609
+ /// 3. update_framework_fee(dh, new_fees, clock, ctx) — triggers commit of the matured pending.
2610
+ /// 4. sync_dapp_fee(dh, ds) — propagates the newly committed rates to each DApp.
2611
+ ///
2612
+ /// sync_dapp_fee reads the committed (base_fee_per_write / bytes_fee_per_byte) fields,
2613
+ /// not the pending values. Committed rates are updated only by step 3 above.
2614
+ public fun sync_dapp_fee<DappKey: copy + drop>(
2615
+ dh: &DappHub,
2616
+ ds: &mut DappStorage,
2617
+ ) {
2618
+ assert_framework_version(dh);
2619
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2620
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(ds) == dapp_key_str);
2621
+ let (base_fee, bytes_fee) = get_effective_fees(dh);
2622
+ dapp_service::set_dapp_base_fee_per_write(ds, base_fee);
2623
+ dapp_service::set_dapp_bytes_fee_per_byte(ds, bytes_fee);
2624
+ dapp_service::emit_fee_state_record<DappKey>(ds);
2625
+ }
2626
+
2627
+
2628
+ /// Update the DApp's display metadata.
2629
+ /// Only the current DApp admin may call this.
2630
+ public fun set_metadata<DappKey: copy + drop>(
2631
+ dh: &DappHub,
2632
+ dapp_storage: &mut DappStorage,
2633
+ name: String,
2634
+ description: String,
2635
+ website_url: String,
2636
+ cover_url: vector<String>,
2637
+ partners: vector<String>,
2638
+ ctx: &mut TxContext,
2639
+ ) {
2640
+ assert_framework_version(dh);
2641
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2642
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2643
+ error::no_permission(dapp_service::dapp_admin(dapp_storage) == ctx.sender());
2644
+
2645
+ dapp_service::set_dapp_name(dapp_storage, name);
2646
+ dapp_service::set_dapp_description(dapp_storage, description);
2647
+ dapp_service::set_dapp_website_url(dapp_storage, website_url);
2648
+ dapp_service::set_dapp_cover_url(dapp_storage, cover_url);
2649
+ dapp_service::set_dapp_partners(dapp_storage, partners);
2650
+ }
2651
+
2652
+ /// Step 1 of the two-step DApp admin transfer.
2653
+ public fun propose_ownership<DappKey: copy + drop>(
2654
+ dh: &DappHub,
2655
+ dapp_storage: &mut DappStorage,
2656
+ new_admin: address,
2657
+ ctx: &mut TxContext,
2658
+ ) {
2659
+ assert_framework_version(dh);
2660
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2661
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2662
+ error::no_permission(dapp_service::dapp_admin(dapp_storage) == ctx.sender());
2663
+ dapp_service::set_dapp_pending_admin(dapp_storage, new_admin);
2664
+ }
2665
+
2666
+ /// Step 2 of the two-step DApp admin transfer.
2667
+ public fun accept_ownership<DappKey: copy + drop>(
2668
+ dh: &DappHub,
2669
+ dapp_storage: &mut DappStorage,
2670
+ ctx: &mut TxContext,
2671
+ ) {
2672
+ assert_framework_version(dh);
2673
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2674
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2675
+ let pending = dapp_service::dapp_pending_admin(dapp_storage);
2676
+ error::no_pending_ownership_transfer(pending != @0x0);
2677
+ error::no_permission(pending == ctx.sender());
2678
+ dapp_service::set_dapp_admin(dapp_storage, pending);
2679
+ dapp_service::set_dapp_pending_admin(dapp_storage, @0x0);
2680
+ }
2681
+
2682
+ /// DApp admin: update the registered package IDs and version (called during upgrade).
2683
+ ///
2684
+ /// DappKey must come from a package already registered in this DApp's package_ids list,
2685
+ /// OR from the new package being registered (caller_pkg == new_package_id). This allows
2686
+ /// migrate_to_vN in the newly upgraded package to call upgrade_dapp without the type-name
2687
+ /// mismatch that would occur if we compared the full type string (which embeds the package
2688
+ /// address and changes on every upgrade).
2689
+ public fun upgrade_dapp<DappKey: copy + drop>(
2690
+ dh: &DappHub,
2691
+ dapp_storage: &mut DappStorage,
2692
+ new_package_id: address,
2693
+ new_version: u32,
2694
+ ctx: &mut TxContext,
2695
+ ) {
2696
+ assert_framework_version(dh);
2697
+ let caller_pkg = type_info::get_package_id<DappKey>();
2698
+ let existing = dapp_service::dapp_package_ids(dapp_storage);
2699
+ error::dapp_key_mismatch(existing.contains(&caller_pkg) || caller_pkg == new_package_id);
2700
+ error::no_permission(dapp_service::dapp_admin(dapp_storage) == ctx.sender());
2701
+ let mut package_ids = dapp_service::dapp_package_ids(dapp_storage);
2702
+ error::invalid_package_id(!package_ids.contains(&new_package_id));
2703
+ package_ids.push_back(new_package_id);
2704
+ error::invalid_version(new_version > dapp_service::dapp_version(dapp_storage));
2705
+ dapp_service::set_dapp_package_ids(dapp_storage, package_ids);
2706
+ dapp_service::set_dapp_version(dapp_storage, new_version);
2707
+ let dapp_key_str = dapp_service::dapp_storage_dapp_key(dapp_storage);
2708
+ dubhe_events::emit_dapp_upgraded(dapp_key_str, new_package_id, new_version, ctx.sender());
2709
+ }
2710
+
2711
+ /// DApp admin: toggle the paused flag.
2712
+ /// When paused == true, ensure_not_paused aborts and the DApp is effectively halted.
2713
+ public fun set_paused<DappKey: copy + drop>(
2714
+ dh: &DappHub,
2715
+ dapp_storage: &mut DappStorage,
2716
+ paused: bool,
2717
+ ctx: &mut TxContext,
2718
+ ) {
2719
+ assert_framework_version(dh);
2720
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2721
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2722
+ error::no_permission(dapp_service::dapp_admin(dapp_storage) == ctx.sender());
2723
+ dapp_service::set_dapp_paused(dapp_storage, paused);
2724
+ dubhe_events::emit_dapp_paused_changed(dapp_key_str, paused, ctx.sender());
2725
+ }
2726
+
2727
+ // ─── Guards ───────────────────────────────────────────────────────────────────
2728
+
2729
+ public fun ensure_dapp_admin<DappKey: copy + drop>(
2730
+ dapp_storage: &DappStorage,
2731
+ admin: address,
2732
+ ) {
2733
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2734
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2735
+ error::no_permission(dapp_service::dapp_admin(dapp_storage) == admin);
2736
+ }
2737
+
2738
+ public fun ensure_latest_version<DappKey: copy + drop>(
2739
+ dapp_storage: &DappStorage,
2740
+ version: u32,
2741
+ ) {
2742
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2743
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2744
+ error::not_latest_version(dapp_service::dapp_version(dapp_storage) == version);
2745
+ }
2746
+
2747
+ public fun ensure_not_paused<DappKey: copy + drop>(
2748
+ dapp_storage: &DappStorage,
2749
+ ) {
2750
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2751
+ error::dapp_key_mismatch(dapp_service::dapp_storage_dapp_key(dapp_storage) == dapp_key_str);
2752
+ error::dapp_paused(!dapp_service::dapp_paused(dapp_storage));
2753
+ }
2754
+
2755
+ // ─── Utility ─────────────────────────────────────────────────────────────────
2756
+
2757
+ /// Returns the canonical dapp key string for a given DappKey type.
2758
+ /// Convenience helper for DApp developers who need to reference their key string
2759
+ /// (e.g. for off-chain indexing or event filtering).
2760
+ public fun dapp_key<DappKey: copy + drop>(): String {
2761
+ type_name::with_defining_ids<DappKey>().into_string()
2762
+ }
2763
+
2764
+ /// Returns the current framework version constant.
2765
+ public fun framework_version(): u64 { FRAMEWORK_VERSION }
2766
+
2767
+ // ─── Internal fee helpers ──────────────────────────────────────────────────────
2768
+
2769
+ /// Sum all value byte lengths. Used to compute the bytes portion of write charges.
2770
+ fun compute_values_bytes(values: &vector<vector<u8>>): u256 {
2771
+ let len = values.length();
2772
+ let mut i = 0u64;
2773
+ let mut total = 0u256;
2774
+ while (i < len) {
2775
+ total = total + (values[i].length() as u256);
2776
+ i = i + 1;
2777
+ };
2778
+ total
2779
+ }
2780
+
2781
+ // ─── Test helpers ─────────────────────────────────────────────────────────────
2782
+
2783
+ #[test_only]
2784
+ public fun create_dapp_hub_for_testing(ctx: &mut TxContext): DappHub {
2785
+ dapp_service::create_dapp_hub_for_testing(ctx)
2786
+ }
2787
+
2788
+ #[test_only]
2789
+ public fun create_dapp_storage_for_testing<DappKey: copy + drop>(ctx: &mut TxContext): DappStorage {
2790
+ // free_credit=0, expires_at=0, base_fee=0, bytes_fee=0 so tests are not affected
2791
+ // by free-credit or fee logic unless explicitly set.
2792
+ dapp_service::new_dapp_storage<DappKey>(
2793
+ string(b"Test DApp"),
2794
+ string(b""),
2795
+ vector[type_info::get_package_id<DappKey>()],
2796
+ 0,
2797
+ ctx.sender(),
2798
+ 0,
2799
+ 0,
2800
+ 0,
2801
+ 0,
2802
+ 0,
2803
+ 0,
2804
+ ctx,
2805
+ )
2806
+ }
2807
+
2808
+ #[test_only]
2809
+ public fun create_user_storage_for_testing<DappKey: copy + drop>(
2810
+ owner: address,
2811
+ ctx: &mut TxContext,
2812
+ ): UserStorage {
2813
+ dapp_service::new_user_storage<DappKey>(owner, 1_000, ctx)
2814
+ }
2815
+
2816
+ #[test_only]
2817
+ public fun min_session_duration_ms(): u64 { MIN_SESSION_DURATION_MS }
2818
+
2819
+ #[test_only]
2820
+ public fun max_session_duration_ms(): u64 { MAX_SESSION_DURATION_MS }
2821
+
2822
+ #[test_only]
2823
+ public fun destroy_dapp_hub(dh: DappHub) {
2824
+ dapp_service::destroy(dh)
2825
+ }
2826
+
2827
+ #[test_only]
2828
+ public fun destroy_dapp_storage(ds: DappStorage) {
2829
+ dapp_service::destroy_dapp_storage(ds);
2830
+ }
2831
+
2832
+ #[test_only]
2833
+ public fun destroy_user_storage(us: UserStorage) {
2834
+ dapp_service::destroy_user_storage(us);
2835
+ }
2836
+
2837
+ /// Deactivate a session with an explicit `now_ms` instead of `ctx.epoch_timestamp_ms()`.
2838
+ ///
2839
+ /// `deactivate_session` uses `ctx.epoch_timestamp_ms()` which stays at 0 in
2840
+ /// `test_scenario` and cannot be advanced without real epoch progression.
2841
+ /// This helper accepts an explicit `now_ms` so the "expired session can be
2842
+ /// cleaned up by anyone" code path is exercisable from unit tests.
2843
+ ///
2844
+ /// Permission rules are identical to the production `deactivate_session`:
2845
+ /// - canonical owner may always deactivate
2846
+ /// - session key may deactivate itself
2847
+ /// - any `sender` may deactivate once `now_ms >= session_expires_at` (expired cleanup)
2848
+ #[test_only]
2849
+ public fun deactivate_session_with_now_ms_for_testing<DappKey: copy + drop>(
2850
+ user_storage: &mut UserStorage,
2851
+ sender: address,
2852
+ now_ms: u64,
2853
+ ) {
2854
+ let dapp_key_str = type_info::get_type_name_string<DappKey>();
2855
+ error::dapp_key_mismatch(dapp_service::user_storage_dapp_key(user_storage) == dapp_key_str);
2856
+ error::no_active_session(dapp_service::session_key(user_storage) != @0x0);
2857
+
2858
+ let canonical = dapp_service::canonical_owner(user_storage);
2859
+ let sk = dapp_service::session_key(user_storage);
2860
+ let expires = dapp_service::session_expires_at(user_storage);
2861
+ let expired = expires > 0 && now_ms >= expires;
2862
+
2863
+ error::no_permission(sender == canonical || sender == sk || expired);
2864
+ dapp_service::clear_session(user_storage);
2865
+ }