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.
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/package.json +16 -16
- package/templates/101/sui-template/docker-compose.testnet.yml +73 -0
- package/templates/101/sui-template/packages/client/src/app/page.tsx +545 -41
- package/templates/101/sui-template/packages/client/src/app/providers.tsx +12 -4
- package/templates/101/sui-template/packages/contracts/Pub.localnet.toml +4 -4
- package/templates/101/sui-template/packages/contracts/counter.lock.json +20 -0
- package/templates/101/sui-template/packages/contracts/deployment.ts +8 -3
- package/templates/101/sui-template/packages/contracts/dubhe.config.json +59 -13
- package/templates/101/sui-template/packages/contracts/dubhe.config.ts +1 -4
- package/templates/101/sui-template/packages/contracts/metadata.json +1015 -68
- package/templates/101/sui-template/packages/contracts/package.json +7 -4
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/dapp_key.move +45 -31
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/error.move +8 -0
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/genesis.move +30 -13
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/init_test.move +19 -14
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2.move +198 -0
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2withkey.move +90 -0
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/resources/value.move +30 -20
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
- package/templates/101/sui-template/packages/contracts/src/counter/sources/scripts/deploy_hook.move +6 -11
- package/templates/101/sui-template/packages/contracts/src/counter/sources/systems/counter.move +23 -11
- package/templates/101/sui-template/packages/contracts/src/counter/sources/tests/counter.move +15 -14
- package/templates/101/sui-template/packages/contracts/src/dubhe/Move.lock +33 -30
- package/templates/101/sui-template/packages/contracts/src/dubhe/Move.toml +8 -3
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/dapp_key.move +45 -31
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/error.move +170 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/genesis.move +16 -13
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/init_test.move +8 -14
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/account_key.move +1 -3
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/dapp_service.move +1776 -137
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/data_key.move +1 -20
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/events.move +864 -25
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/scripts/deploy_hook.move +15 -16
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/scripts/migrate.move +0 -3
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/address_system.move +25 -17
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/dapp_system.move +2802 -343
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/utils.move +1 -1
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/address.move +173 -55
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/dapp_test.move +769 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/fee_test.move +2965 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/integration_test.move +303 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/listing_test.move +1422 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/scene_session_test.move +269 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/session_test.move +624 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/storage_test.move +700 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/typed_object_test.move +177 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/typed_scene_test.move +452 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/tests/user_storage_test.move +160 -0
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/utils/math.move +4 -3
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/utils/type_info.move +2 -2
- package/templates/contract/sui-template/dubhe.config.ts +0 -1
- package/templates/contract/sui-template/package.json +3 -3
- package/templates/contract/sui-template/src/dubhe/Move.lock +33 -30
- package/templates/contract/sui-template/src/dubhe/Move.toml +8 -3
- package/templates/contract/sui-template/src/dubhe/sources/codegen/dapp_key.move +45 -31
- package/templates/contract/sui-template/src/dubhe/sources/codegen/error.move +170 -0
- package/templates/contract/sui-template/src/dubhe/sources/codegen/genesis.move +16 -13
- package/templates/contract/sui-template/src/dubhe/sources/codegen/init_test.move +8 -14
- package/templates/contract/sui-template/src/dubhe/sources/core/account_key.move +1 -3
- package/templates/contract/sui-template/src/dubhe/sources/core/dapp_service.move +1776 -137
- package/templates/contract/sui-template/src/dubhe/sources/core/data_key.move +1 -20
- package/templates/contract/sui-template/src/dubhe/sources/core/events.move +864 -25
- package/templates/contract/sui-template/src/dubhe/sources/scripts/deploy_hook.move +15 -16
- package/templates/contract/sui-template/src/dubhe/sources/scripts/migrate.move +0 -3
- package/templates/contract/sui-template/src/dubhe/sources/systems/address_system.move +25 -17
- package/templates/contract/sui-template/src/dubhe/sources/systems/dapp_system.move +2802 -343
- package/templates/contract/sui-template/src/dubhe/sources/systems/utils.move +1 -1
- package/templates/contract/sui-template/src/dubhe/sources/tests/address.move +173 -55
- package/templates/contract/sui-template/src/dubhe/sources/tests/dapp_test.move +769 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/fee_test.move +2965 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/integration_test.move +303 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/listing_test.move +1422 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/scene_session_test.move +269 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/session_test.move +624 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/storage_test.move +700 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/typed_object_test.move +177 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/typed_scene_test.move +452 -0
- package/templates/contract/sui-template/src/dubhe/sources/tests/user_storage_test.move +160 -0
- package/templates/contract/sui-template/src/dubhe/sources/utils/math.move +4 -3
- package/templates/contract/sui-template/src/dubhe/sources/utils/type_info.move +2 -2
- package/templates/contract/sui-template/src/template/Move.toml +12 -0
- package/templates/contract/sui-template/src/template/sources/codegen/dapp_key.move +47 -0
- package/templates/contract/sui-template/src/template/sources/codegen/genesis.move +34 -0
- package/templates/contract/sui-template/src/template/sources/codegen/init_test.move +23 -0
- package/templates/contract/sui-template/src/template/sources/codegen/user_storage_init.move +27 -0
- package/templates/contract/sui-template/src/template/sources/scripts/deploy_hook.move +13 -0
- package/templates/contract/sui-template/src/template/sources/scripts/migrate.move +7 -0
- package/templates/nextjs/sui-card-duel/.gitignore_ +51 -0
- package/templates/nextjs/sui-card-duel/README.md +213 -0
- package/templates/nextjs/sui-card-duel/docker-compose.yml +34 -0
- package/templates/nextjs/sui-card-duel/mprocs.yaml +17 -0
- package/templates/nextjs/sui-card-duel/package.json +43 -0
- package/templates/nextjs/sui-card-duel/packages/client/.gitignore_ +48 -0
- package/templates/nextjs/sui-card-duel/packages/client/.prettierignore +1 -0
- package/templates/nextjs/sui-card-duel/packages/client/.prettierrc.json +8 -0
- package/templates/nextjs/sui-card-duel/packages/client/eslint.config.mjs +22 -0
- package/templates/nextjs/sui-card-duel/packages/client/next.config.mjs +8 -0
- package/templates/nextjs/sui-card-duel/packages/client/package.json +47 -0
- package/templates/nextjs/sui-card-duel/packages/client/postcss.config.mjs +8 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/admin/ArenaPanel.tsx +307 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/admin/page.tsx +622 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/brawl/[id]/page.tsx +559 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/collection/page.tsx +202 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/components/CardView.tsx +66 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/components/GameShell.tsx +294 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/components/icons.tsx +101 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/duel/[id]/page.tsx +477 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/favicon.ico +0 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/globals.css +85 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/hooks/useGame.ts +313 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/layout.tsx +24 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/leaderboard/page.tsx +123 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/lib/game.ts +77 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/lib/scenes.ts +226 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/market/page.tsx +418 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/page.tsx +427 -0
- package/templates/nextjs/sui-card-duel/packages/client/src/app/providers.tsx +69 -0
- package/templates/nextjs/sui-card-duel/packages/client/tailwind.config.ts +21 -0
- package/templates/nextjs/sui-card-duel/packages/client/tsconfig.json +30 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/.gitignore_ +51 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/.prettierignore +1 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/.prettierrc.json +8 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/Dockerfile +5 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/Pub.localnet.toml +13 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/card_duel.lock.json +66 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/docker-compose.yml +34 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/dubhe.config.ts +150 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/logs/combined.log +31383 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/package.json +49 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/config.ts +17 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-brawl.ts +270 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-duel.ts +272 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-lobby.ts +177 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-market.ts +219 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/e2e-session.ts +342 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/query-dapp-revenue.ts +95 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/seed.ts +92 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/scripts/withdraw-dapp-revenue.ts +56 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/Move.toml +15 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/dapp_key.move +46 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/card_kind.move +47 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/match_state.move +37 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/enums/rarity.move +37 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/error.move +126 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/genesis.move +33 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/init_test.move +23 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/objects/arena.move +137 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/permits/brawl_permit.move +125 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/permits/duel_permit.move +125 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/battle_state.move +255 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/card.move +282 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/deck.move +64 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/game_config.move +267 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/gold.move +205 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/resources/profile.move +255 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/scenes/brawl.move +313 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/scenes/duel.move +313 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/codegen/user_storage_init.move +26 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/scripts/deploy_hook.move +32 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/scripts/migrate.move +7 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/arena_system.move +57 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/brawl_system.move +559 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/card_system.move +146 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/duel_system.move +515 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/market_system.move +115 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/systems/player_system.move +37 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/brawl_tests.move +385 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/duel_tests.move +656 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/src/card_duel/sources/tests/test_helpers.move +31 -0
- package/templates/nextjs/sui-card-duel/packages/contracts/sui-indexer-schema.graphql +5973 -0
- package/templates/nextjs/sui-card-duel/pnpm-workspace.yaml +2 -0
- package/templates/nextjs/sui-farm/.gitignore_ +51 -0
- package/templates/nextjs/sui-farm/README.md +15 -0
- package/templates/nextjs/sui-farm/UPGRADE_NOTES.md +97 -0
- package/templates/nextjs/sui-farm/docker-compose.yml +34 -0
- package/templates/nextjs/sui-farm/mprocs.yaml +17 -0
- package/templates/nextjs/sui-farm/package.json +43 -0
- package/templates/nextjs/sui-farm/packages/client/.gitignore_ +48 -0
- package/templates/nextjs/sui-farm/packages/client/.prettierignore +1 -0
- package/templates/nextjs/sui-farm/packages/client/.prettierrc.json +8 -0
- package/templates/nextjs/sui-farm/packages/client/eslint.config.mjs +22 -0
- package/templates/nextjs/sui-farm/packages/client/next.config.mjs +8 -0
- package/templates/nextjs/sui-farm/packages/client/package.json +47 -0
- package/templates/nextjs/sui-farm/packages/client/postcss.config.mjs +8 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/admin/page.tsx +618 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/FarmLand.tsx +526 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/FarmPlotCard.tsx +215 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/PetAvatar.tsx +868 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/PetPanel.tsx +1360 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/PlantModal.tsx +147 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/ProxyCard.tsx +684 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/RanchLand.tsx +822 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/ResourceHUD.tsx +125 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/ShopPanel.tsx +232 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/components/icons/GameIcons.tsx +574 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/favicon.ico +0 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/globals.css +423 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/hooks/useWorldPermitId.ts +66 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/layout.tsx +23 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/leaderboard/page.tsx +185 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/lib/crops.ts +97 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/market/page.tsx +763 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/page.tsx +1173 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/providers.tsx +69 -0
- package/templates/nextjs/sui-farm/packages/client/src/app/state/index.tsx +5 -0
- package/templates/nextjs/sui-farm/packages/client/tailwind.config.ts +25 -0
- package/templates/nextjs/sui-farm/packages/client/tsconfig.json +30 -0
- package/templates/nextjs/sui-farm/packages/contracts/.gitignore_ +51 -0
- package/templates/nextjs/sui-farm/packages/contracts/.prettierignore +1 -0
- package/templates/nextjs/sui-farm/packages/contracts/.prettierrc.json +8 -0
- package/templates/nextjs/sui-farm/packages/contracts/Dockerfile +5 -0
- package/templates/nextjs/sui-farm/packages/contracts/Pub.localnet.toml +13 -0
- package/templates/nextjs/sui-farm/packages/contracts/counter.lock.json +14 -0
- package/templates/nextjs/sui-farm/packages/contracts/docker-compose.yml +34 -0
- package/templates/nextjs/sui-farm/packages/contracts/dubhe.config.ts +178 -0
- package/templates/nextjs/sui-farm/packages/contracts/harvest.lock.json +120 -0
- package/templates/nextjs/sui-farm/packages/contracts/package.json +48 -0
- package/templates/nextjs/sui-farm/packages/contracts/scripts/config.ts +17 -0
- package/templates/nextjs/sui-farm/packages/contracts/scripts/query-dapp-revenue.ts +95 -0
- package/templates/nextjs/sui-farm/packages/contracts/scripts/read-permit.ts +72 -0
- package/templates/nextjs/sui-farm/packages/contracts/scripts/seed.ts +85 -0
- package/templates/nextjs/sui-farm/packages/contracts/scripts/withdraw-dapp-revenue.ts +56 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/Move.toml +14 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/dapp_key.move +31 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/error.move +10 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/genesis.move +33 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/init_test.move +23 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/resources/counter1.move +64 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/resources/counter2.move +70 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/scripts/deploy_hook.move +8 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/scripts/migrate.move +8 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/systems/counter.move +13 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/counter/sources/tests/counter.move +28 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/Move.toml +14 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/dapp_key.move +46 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/crop_type.move +47 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/egg_type.move +42 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/pet_rarity.move +37 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/enums/pet_species.move +52 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/error.move +110 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/genesis.move +33 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/init_test.move +23 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/permits/world.move +125 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/carrot.move +136 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/carrot_seed.move +80 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/common_egg.move +136 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/corn.move +136 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/corn_seed.move +80 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/crow_charges.move +162 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/crow_damage.move +207 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/farm_plot.move +261 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/gold.move +80 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet.move +467 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_config.move +372 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_hatch.move +162 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_slot_index.move +83 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pet_slots.move +64 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/profile.move +162 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pumpkin.move +136 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/pumpkin_seed.move +80 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/rare_egg.move +136 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/scarecrow.move +64 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/season_config.move +197 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/season_stats.move +64 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/seasonal_egg.move +136 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/shop_config.move +267 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/trophy.move +245 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/wheat.move +136 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/wheat_seed.move +80 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/resources/world_permit_id.move +64 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/codegen/user_storage_init.move +26 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/scripts/deploy_hook.move +45 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/scripts/migrate.move +7 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/crow_system.move +96 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/farm_system.move +182 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/market_system.move +172 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/pet_system.move +495 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/season_system.move +70 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/shop_system.move +112 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/systems/world_system.move +34 -0
- package/templates/nextjs/sui-farm/packages/contracts/src/harvest/sources/tests/farm_tests.move +1192 -0
- package/templates/nextjs/sui-farm/pnpm-workspace.yaml +2 -0
- package/templates/nextjs/sui-template/packages/client/src/app/components/ProxyCard.tsx +683 -0
- package/templates/nextjs/sui-template/packages/client/src/app/page.tsx +510 -5
- package/templates/nextjs/sui-template/packages/client/src/app/providers.tsx +13 -5
- package/templates/nextjs/sui-template/packages/contracts/Pub.localnet.toml +4 -4
- package/templates/nextjs/sui-template/packages/contracts/counter.lock.json +14 -0
- package/templates/nextjs/sui-template/packages/contracts/deployment.ts +8 -3
- package/templates/nextjs/sui-template/packages/contracts/dubhe.config.json +9 -9
- package/templates/nextjs/sui-template/packages/contracts/dubhe.config.ts +0 -4
- package/templates/nextjs/sui-template/packages/contracts/metadata.json +180 -124
- package/templates/nextjs/sui-template/packages/contracts/package.json +8 -4
- package/templates/nextjs/sui-template/packages/contracts/src/counter/Move.toml +4 -0
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/dapp_key.move +45 -31
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/error.move +10 -0
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/genesis.move +30 -13
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/init_test.move +19 -14
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter1.move +30 -20
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/resources/counter2.move +35 -24
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/user_storage_init.move +26 -0
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/scripts/deploy_hook.move +6 -10
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/systems/counter.move +7 -20
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/tests/counter.move +14 -27
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/Move.lock +33 -30
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/Move.toml +8 -3
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/dapp_key.move +45 -31
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/error.move +170 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/genesis.move +16 -13
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/init_test.move +8 -14
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/account_key.move +1 -3
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/dapp_service.move +1776 -137
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/data_key.move +1 -20
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/events.move +864 -25
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/scripts/deploy_hook.move +15 -16
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/scripts/migrate.move +0 -3
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/address_system.move +25 -17
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/dapp_system.move +2802 -343
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/utils.move +1 -1
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/address.move +173 -55
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/dapp_test.move +769 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/fee_test.move +2965 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/integration_test.move +303 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/listing_test.move +1422 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/scene_session_test.move +269 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/session_test.move +624 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/storage_test.move +700 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/typed_object_test.move +177 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/typed_scene_test.move +452 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/tests/user_storage_test.move +160 -0
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/utils/math.move +4 -3
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/utils/type_info.move +2 -2
- package/templates/101/sui-template/packages/contracts/src/counter/Move.lock +0 -41
- package/templates/101/sui-template/packages/contracts/src/counter/Published.toml +0 -9
- package/templates/101/sui-template/packages/contracts/src/counter/sources/codegen/errors.move +0 -12
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/errors.move +0 -140
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/core/dapp_store.move +0 -29
- package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/session_system.move +0 -44
- package/templates/contract/sui-template/src/dubhe/sources/codegen/errors.move +0 -140
- package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
- package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
- package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
- package/templates/contract/sui-template/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
- package/templates/contract/sui-template/src/dubhe/sources/core/dapp_store.move +0 -29
- package/templates/contract/sui-template/src/dubhe/sources/systems/session_system.move +0 -44
- package/templates/nextjs/sui-template/packages/contracts/src/counter/Move.lock +0 -47
- package/templates/nextjs/sui-template/packages/contracts/src/counter/Published.toml +0 -9
- package/templates/nextjs/sui-template/packages/contracts/src/counter/sources/codegen/errors.move +0 -12
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/errors.move +0 -140
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_config.move +0 -186
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_fee_state.move +0 -306
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dapp_metadata.move +0 -396
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/codegen/resources/dubhe_config.move +0 -246
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/core/dapp_store.move +0 -29
- package/templates/nextjs/sui-template/packages/contracts/src/dubhe/sources/systems/session_system.move +0 -44
package/templates/101/sui-template/packages/contracts/src/dubhe/sources/systems/dapp_system.move
CHANGED
|
@@ -1,406 +1,2865 @@
|
|
|
1
|
+
#[allow(unused_use)]
|
|
1
2
|
module dubhe::dapp_system;
|
|
2
|
-
|
|
3
|
-
use dubhe::dapp_service
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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::
|
|
20
|
-
use
|
|
21
|
-
use dubhe::
|
|
22
|
-
use
|
|
23
|
-
use
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
key
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
///
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
481
|
+
dapp_service::has_object_field<ObjType, T>(storage, field_name)
|
|
116
482
|
}
|
|
117
483
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
1039
|
+
public fun ensure_has_not_record<DappKey: copy + drop>(
|
|
1040
|
+
user_storage: &UserStorage,
|
|
1041
|
+
key: vector<vector<u8>>,
|
|
362
1042
|
) {
|
|
363
|
-
|
|
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
|
|
370
|
-
|
|
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
|
-
|
|
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
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
|
395
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
+
}
|