{"content":{"title":"如何开发一个简单的 dApp（一）","body":"## 后端合约\r\n\r\n### 合约升级\r\n\r\n​\t在开始合约编写之前，先一起来了解一下**合约升级**，顾名思义可以将已经发布的合约进行升级，在**dApp**遇到漏洞或需要添加新功能时可以及时更正，并且能防止原有数据的丢失\r\n\r\n​\t需要注意的是进行合约升级需要在**共享**的`obejct`中添加版本属性，这样能防止在给共享`object`添加动态属性后，旧合约仍对共享`object`进行操作从而引发的兼容问题，关于具体的操作可以参考[这里](https://intro-zh.sui-book.com/advanced-topics/upgrade_packages/lessons/1_%E5%90%88%E7%BA%A6%E5%8D%87%E7%BA%A7.html)。\r\n\r\n​\t执行 `sui move new resource_manage` 创建一个包，接着就可以开始编写合约了\r\n\r\n首先设置一个常量`VERSION`代表当前版本号\r\n\r\n```rust\r\nconst VERSION: u64 = 1;\r\n```\r\n\r\n创建一个`AdminCap`用来将数据转移操作成为专有，只允许使用`AdminCap`调用\r\n\r\n```rust\r\npublic struct AdminCap has key {\r\n    id: UID,\r\n}\r\n```\r\n\r\n定义两个 `Struct` 用来创建 `Profile` 并记录在 `State` 中，在 **object** `State`和`Profile`中添加 `version` 属性来记录当前**object**的版本信息\r\n\r\n```rust\r\npublic struct State has key {\r\n    id: UID,\r\n    users: Table<address, address>,\r\n    admin: ID,\r\n    version: u64,\r\n}\r\n\r\npublic struct Profile has key {\r\n    id: UID,\r\n    name: String,\r\n    description: String,\r\n    version: u64,\r\n}\r\n```\r\n\r\n接着创建一个事件来记录`Profile`的创建过程\r\n\r\n```rust\r\npublic struct ProfileCreated has copy, drop {\r\n    profile: address,\r\n    owner: address,\r\n}\r\n```\r\n\r\n下一步初始化合约将  `State` 共享，并把`AdminCap`的所有权转移给合约的发布者\r\n\r\n```rust\r\nfun init(ctx: &mut TxContext) {\r\n    let admin = AdminCap {\r\n        id: object::new(ctx),\r\n    };\r\n    transfer::share_object(State{\r\n        id: object::new(ctx), \r\n        users: table::new(ctx),\r\n        admin: object::id(&admin),\r\n        version: VERSION,\r\n    });\r\n    transfer::transfer(admin, tx_context::sender(ctx));\r\n}\r\n```\r\n\r\n下面开始定义一个创建 `Profile` 的函数，注意要判断`version`的值\r\n\r\n```rust\r\nconst EWrongVersion: u64 = 0;\r\nconst EProfileExit: u64 = 1;\r\n\r\npublic entry fun create_profile(name: String, description: String, state: &mut State, ctx: &mut TxContext) {\r\n    // 判断 State 的版本\r\n    assert!(state.version == VERSION, EWrongVersion);\r\n    let owner = tx_context::sender(ctx);\r\n    assert!(!table::contains(&state.users, owner), EProfileExit);\r\n    let uid = object::new(ctx);\r\n    let id = object::uid_to_inner(&uid);\r\n\r\n    let new_profile = Profile {\r\n        id: uid,\r\n        name: name,\r\n        description: description,\r\n        version: VERSION,\r\n    };\r\n\r\n    transfer::transfer(new_profile, owner);\r\n    table::add(&mut state.users, owner, object::id_to_address(&id));\r\n\r\n    event::emit(ProfileCreated { \r\n        profile: object::id_to_address(&id), \r\n        owner,\r\n    });\r\n}\r\n```\r\n\r\n​\t函数的大致逻辑就是先从 `State.users` 中判断交易者是否已经记录过，如果没有则创建一个 `Profile` 并将所有权转移给交易者，并将交易者的 `address` 和 `Profile` 的 `id` 转换成 `address` 类型后一并记录到 `State` 中去，最后触发事件记录资源的创建\r\n\r\n接着定义一个检查资源是否存在的函数\r\n\r\n```rust\r\npublic fun check_has_profile(state: &State, user: address): Option<address> {\r\n    // 判断State的版本\r\n    assert!(state.version == VERSION, EWrongVersion);\r\n    if(table::contains(&state.users, user)) {\r\n        option::some(*table::borrow(&state.users, user))\r\n    }else {\r\n        option::none()\r\n    }\r\n}\r\n```\r\n\r\n> 这里 * 号的作用是“解引用”，table::borrow() 返回的是一个引用，但是 option::some() 函数需要接收一个值\r\n\r\n到此我们的合约就编写完成了，最后修改 `Move.toml` 文件的格式\r\n\r\n```toml\r\n[package]\r\nname = \"resource_manage\"\r\nedition = \"2024.beta\" # edition = \"legacy\" to use legacy (pre-2024) Move\r\nversion = \"0.0.1\"\r\n\r\n[addresses]\r\nresource_manage = \"0x0\"\r\n```\r\n\r\n```\r\n执行 sui move build 编译\r\n\r\n执行 sui client publish 部署\r\n```\r\n\r\n![image-20250111190251615](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501121334238.png)\r\n\r\n```\r\nPackageID: 0xcb764601ba2573e1e31fc11f5770897753ee2934ed11e8c86612b8ca5c73e9af \r\nState: 0x185d432938a81bec3945417806c11ef739c962a7d99b5aa55292d894d31651dd\r\nAdminCap: 0x88e4a163b42ead5b524a66b47ae06f5da39063d7b557b66617b73f8bdbeb34a6\r\nUpgradeCap: 0xd44c6de28d105a9a4fe581f701f1f3aa43585797fbdfffca1cc018103026167b\r\n```\r\n\r\n通过**Sui Cli**调用**create_profile**函数\r\n\r\n```sh\r\nsui client call --package <PACKAGE-ID> --module resource_manage --function create_profile --args \"file_1\" \"version = 1\" <STATE-ID>\r\n```\r\n\r\n可以获取到**ProfileID**，使用`sui client object <Profile-ID>`查看详细信息\r\n\r\n![image-20250111190533618](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501121336682.png)\r\n\r\n## 前端调用\r\n\r\n执行命令 `npm create @mysten/dapp` 开始初始化项目\r\n\r\n![image-20241112202825435](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501051754789.png)\r\n\r\n这里选择第一个生成一个简单的 `dapp` 模板，第二个是携带 Move 代码的计算器示例\r\n\r\n接着进入项目内执行 `npm install` 下载依赖包，这时可能会提示 `eslint` 版本不兼容\r\n\r\n![image-20250104130647602](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501051754468.png)\r\n\r\n我们需要在 `package.json` 中将 `eslint` 设置为 `^8.56.0`\r\n\r\n![image-20250104132129994](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501051754790.png)\r\n\r\n重新执行 `npm install` 即可成功\r\n\r\n> 注意：如果安装不成功通过执行  `npm install -g npm` 来升级版本，以获得更好的兼容\r\n\r\n可以在项目根目录执行 `npm run dev` 来检验项目是否部署成功\r\n\r\n![image-20250104132515706](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501051754470.png)\r\n\r\n如果浏览器出现此界面就代表成功了\r\n\r\n\r\n\r\n往下我们先学习 **Sui SDK** 一些基本用法\r\n\r\n1. `ConnectButton`：就是用于连接钱包的按钮\r\n\r\n   ```typescript\r\n   import { ConnectButton } from '@mysten/dapp-kit';\r\n    \r\n   export function App() {\r\n   \treturn <ConnectButton />;\r\n   }\r\n   ```\r\n\r\n2. `useCurrentAccount `：检索当前选择的钱包账户\r\n\r\n   ```ts\r\n   import { ConnectButton, useCurrentAccount } from '@mysten/dapp-kit';\r\n    \r\n   function MyComponent() {\r\n   \tconst account = useCurrentAccount();\r\n    \r\n   \treturn (\r\n   \t\t<div>\r\n   \t\t\t<ConnectButton />\r\n   \t\t\t{!account && <div>No account connected</div>}\r\n   \t\t\t{account && (\r\n   \t\t\t\t<div>\r\n   \t\t\t\t\t<h2>Current account:</h2>\r\n   \t\t\t\t\t<div>Address: {account.address}</div>\r\n   \t\t\t\t</div>\r\n   \t\t\t)}\r\n   \t\t</div>\r\n   \t);\r\n   }\r\n   ```\r\n\r\n3. `SuiClient`：用来连接到网络并进行交互\r\n\r\n   ```ts\r\n   import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';\r\n   \r\n   const suiClient = new SuiClient({\r\n   \turl: getFullnodeUrl('testnet'),\r\n   });\r\n   ```\r\n\r\n4. `Transaction`：用来打包交易块，可以使用 **PTB**\r\n\r\n   ```ts\r\n   import { Transaction } from \"@mysten/sui/transactions\";\r\n   \r\n   export const Test = async(name: string, description: string) => {\r\n       const tx = new Transaction();\r\n       tx.moveCall({\r\n           package: packageID,\r\n           module: \"module_name\",\r\n           function: \"fun_name\",\r\n           arguments: [\r\n               tx.pure.string(name),\r\n               tx.pure.string(description),\r\n               tx.object(state),\r\n           ],\r\n       });\r\n   \r\n       return tx;\r\n   }\r\n   ```\r\n\r\n5. `useSignAndExecuteTransaction`：进行签名并交易\r\n\r\n   ```tsx\r\n   import { useSignAndExecuteTransaction } from \"@mysten/dapp-kit\";\r\n   \r\n   const {mutate: signAndExecute} = useSignAndExecuteTransaction();\r\n   \r\n   const tx = await Test(name, description);\r\n   signAndExecute({\r\n     transaction: tx\r\n   }, {\r\n     onSuccess: () => {\r\n       console.log(\"success!\");\r\n     },\r\n     onError: (error) => {\r\n       console.log(error);\r\n     }\r\n   });\r\n   ```\r\n\r\n完整代码：https://github.com/Ch1hiro4002/Sui_Frontend_Study/tree/main/week_1\r\n\r\n展示效果：\r\n\r\n![](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501051754324.png)\r\n\r\n![image-20250107194519474](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501071933939.png)\r\n\r\n![](https://oss-of-ch1hiro.oss-cn-beijing.aliyuncs.com/imgs/202501051754046.png)"},"author":{"user":"https://learnblockchain.cn/people/23714","address":null},"history":"bafkreidwbl2s7h7ihr2texggqbrhbstvpnjxntxkn5gr5kbaf3fiii5z7a","timestamp":1736660545,"version":1}