├── .gitattributes ├── .gitignore ├── README.md ├── assets ├── WeChat-FlowChainOfficial.png └── brands-background.jpg ├── lesson-1 ├── README.md └── src │ └── metaElement.cdc ├── lesson-2 ├── README.md └── src │ ├── entity │ ├── entity.contract.cdc │ └── generate.transaction.cdc │ └── hello │ ├── contract.cdc │ ├── sayHi.script.cdc │ └── sayHi.transaction.cdc ├── lesson-3 ├── README.md └── cadence │ ├── contracts │ ├── Entity.cdc │ └── standard │ │ ├── MetadataViews.cdc │ │ └── NonFungibleToken.cdc │ └── transactions │ └── .gitkeep ├── lesson-4 └── README.md ├── lesson-5 └── README.md └── lesson-6 └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | *.jpg filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Local 107 | **/example/ 108 | flow.json 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👋 Flow DApp 开发入门课程 2 | 3 | ![BrandsBg](/assets/brands-background.jpg "Flow blockchain") 4 | 5 | 欢迎来到 Flow DApp 开发入门课程,你可以在本代码仓库中找到全部的课程资料,包括讲义内容、代码资源和相关链接。 6 | 7 | ## 📚 整个课程学习周期的大概是怎么样的? 8 | 9 | - **Flow DApp 开发入门课程** 一共6节课,共持续一个月时间。 10 | - **作业任务将与课程内容配套出现**,课程习题主要将通过 github classroom 进行。 11 | - **每节课程的视频时间大约为50~80分钟**,每节课花在课程习题上的时间大约为0.5-2个小时。 12 | 13 | ## ⚡️ 提交作业与答疑 14 | 15 | 每节课结束后,都会收到一份包含了该节课课程习题的问卷表单,同时还可以在通过该表单递交 github 代码库链接。 16 | 17 | 在课程中,我们每周会安排答疑时间对上节课的习题进行一定的回顾。 18 | 在答疑时间内你可以对本周课程的任意内容进行提问。 19 | 20 | ## 🙌 欢迎来到 FlowFans 中文社区开发者频道 21 | 22 | 欢迎加入我们社区成为我们的一员! 23 | 我们在 Discord 上开设了专属开发者频道 24 | 在 #身份获取 中领取开发者身份后即可看到开发者相关的频道。 25 | 26 | 我们在微信也有一个开发者社区群,加入可以添加「Flow官方小助手」并注明:Cadence课程学习。 27 | 可以扫描下列二维码,或添加微信号 FlowChainOfficial。 28 | 29 | ![QRCode](/assets/WeChat-FlowChainOfficial.png#pic_right "WeChat QR Code") 30 | 31 | 更多开发者相关的活动,我们也会在社区中公布。 32 | -------------------------------------------------------------------------------- /assets/WeChat-FlowChainOfficial.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:fa0438b16b3f561c8ff3119c922fb2c2149a8b3840c49d26b7a2903f14012d0d 3 | size 89880 4 | -------------------------------------------------------------------------------- /assets/brands-background.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ef8185b9b64aa3486c8fbddb2858a43e6a70c05f1bbba6bd46c1a5a80301040b 3 | size 300918 4 | -------------------------------------------------------------------------------- /lesson-1/README.md: -------------------------------------------------------------------------------- 1 | # 第一讲 初识 Cadence - Cadence基础与Playground 2 | 3 | ## Flow 链和它的智能合约语言 4 | 5 | Flow 是一个高效、快速、可靠且为数字藏品而生的区块链,在现实产品数字化、藏品化上有着其独到的优势。 6 | 它充分尊重了开发者的开发体验,并提供了大量的工具和资源以帮助开发者流畅地进行 DApp 的开发。 7 | 8 | Flow 具有非常创新的技术架构(分离共识和计算的流水线节点设计),可以访问 查看文档了解更多信息。 9 | 但从应用开发角度来说,我们只需要了解如何与 Flow 区块链交互,因此,我们不会深入研究它的工作原理。 10 | 11 | 那么现在,我们就从 Flow 的开发环境搭建开始,逐步进入其智能合约语言 Cadence 的世界吧。 12 | 13 | ## 开发环境和命令行安装 14 | 15 | > 官方文档:[安装flow-cli][1] 16 | 17 | ### Linux/macOS 18 | 19 | 执行单行命令 20 | 21 | ```sh 22 | sh -ci "$(curl -fsSL https://storage.googleapis.com/flow-cli/install.sh)" 23 | ``` 24 | 25 | 安装完成后,请确保在您的 `$PATH` 环境变量中包含 `flow`。 26 | 27 | 注意: 如果是 M1 芯片的 mac 只能通过 homebrew 进行安装,该脚本只支持 x86 芯片。 28 | 29 | ```sh 30 | brew install flow-cli 31 | ``` 32 | 33 | ### Windows 34 | 35 | 同样,按照 [安装flow-cli][1] 文档,确保您的 Windows 版本上有 **PowerShell**。 36 | 搜索并在打开 *"PowerShell"* 后运行下面的命令。 37 | 38 | ```sh 39 | iex "& { $(irm 'https://storage.googleapis.com/flow-cli/install.ps1') }" 40 | ``` 41 | 42 | ### 运行测试 43 | 44 | 成功安装 `flow-cli` 后,可以尝试运行 version 命令。 45 | 46 | ```sh 47 | flow version 48 | // Version: v0.28.3 49 | // Commit: ff1f8b186bf26e922ab6abe845849fb9e6e6d729 50 | ``` 51 | 52 | 然后让我们执行我们的第一个 `Cadence` 命令。 53 | 54 | ```sh 55 | flow cadence 56 | ``` 57 | 58 | 首先会出现的是命令行提示符。 59 | 60 | ``` 61 | Welcome to Cadence v0.18.0! 62 | Type '.help' for assistance. 63 | 64 | 1> 65 | ``` 66 | 67 | 然后向 `Cadence` 世界问好吧! 68 | 69 | ```cadence 70 | log("Hello, World!") 71 | ``` 72 | 73 | 命令行的回复应该是: 74 | 75 | ``` 76 | "Hello, World!" 77 | () 78 | ``` 79 | 80 | 后续我们会使用 **VS Code** 进行开发,值得高兴的是 Flow 团队为 VS Code 制作了一款`Cadence`插件,它支持语法高亮、类型检查等。 81 | 具体参见 [VS Code插件文档][2],可以在 VS Code 扩展中搜索`Cadence`,或者在本地运行此命令安装它: 82 | 83 | ```sh 84 | flow cadence install-vscode-extension 85 | ``` 86 | 87 | 现在我们已经设置好了开发环境,我们可以更深入地研究`Cadence`,**Flow** 的智能合约编程语言。 88 | 89 | ## Cadence 语言 90 | 91 | > 官方文档:[Cadence语言][3] 92 | 93 | **Cadence** 是一种面向资源的编程语言,您将使用它为 **Flow** 区块链编写智能合约 —— 在区块链上执行的应用程序。 94 | 95 | 由于`Cadence`是解释型语言,因此我们可以使用 Cadence 语言服务器(一个 REPL shell)开始执行 Cadence 代码,我们之前用它来打印过 `"Hello, World!"`。 96 | 相同的命令也可用于执行整个程序文件,稍后会解释。 97 | 98 | ```sh 99 | flow cadence [filename] 100 | ``` 101 | 102 | 现在我们就从基本语法开始,对 `Cadence` 进行深入得了解吧。 103 | 104 | ### 基本语法 105 | 106 | > 官方文档: 107 | 108 | ```cadence 109 | // 单行注释 110 | /* 大块的 /* 嵌套的 */ 注释 */ 111 | ``` 112 | 113 | 与大多数其他编程语言一样,在命名变量时,可以用大写或小写字母“A-Z、a-z”或下划线“_”开头,后面才也可以包含数字“0-9”。 114 | 115 | ```cadence 116 | test1234 // 正确 117 | 1234test // 错误 118 | (-_-) // 错误 119 | ``` 120 | 121 | 分号`;` 是可选的,除非你在同一行中放置了两个或多个声明时,必须用分号分割。 122 | 123 | 变量与常量 124 | 125 | - 变量:用 `var` 声明。可以不初始化。 126 | - 常量:用 `let` 声明。当声明一个变量时,你必须初始化它。 127 | 128 | ```cadence 129 | // 变量的声明 130 | var bad 131 | var counter = 10 132 | // 变量的赋值 133 | counter = 11 134 | // 常量的声明 135 | let name = "Morgan" 136 | ``` 137 | 138 | `Cadence` 是一门强类型的语言,一切事物都有一个类型,推断得到或显式声明。 139 | 140 | ```cadence 141 | var isGood: Bool = false 142 | isGood = true 143 | isGood = 42 // 炸! 144 | ``` 145 | 146 | ### 基本类型 147 | 148 | `Cadence` 有许多有用的基本类型。 149 | 150 | #### 整型 151 | 152 | ```cadence 153 | 123 154 | 0b1111 // 二进制 155 | 0o17 // 八进制的 17 156 | 0xff // 十六进制 157 | 1_000_000_000 // 便于阅读的 十亿 158 | ``` 159 | 160 | 所有这些整数都被推断为`Int`,它们可以表示任意大的有符号整数。 161 | 如果你想更具体的表达,你可以使用`Int8`、`Int16`等。所有`Int`和`UInt`类型都有溢出检查。 162 | 163 | ```cadence 164 | var tiny: Int8 = 126 165 | tiny = tiny + 1 // 正常 166 | tiny = tiny + 1 // 报错! 167 | ``` 168 | 169 | `Cadence` 不允许为整数分配超出其范围的值, 这可以保护开发者免受代价高昂的程序溢出错误。 170 | 171 | 同时, 整型还有一些有用的方法。 172 | 173 | ```cadence 174 | let million = 1_000_000 175 | million.toString() // "1000000" 176 | million.toBigEndianBytes() // [15, 66, 64] 177 | ``` 178 | 179 | #### 定点数 180 | 181 | `Cadence` 使用 `Fix64` 和 `UFix64` 来表示小数,但它们本质上是带有缩放因子的整数,其缩放因子为`8`。 182 | 183 | ```cadence 184 | let fractional: Fix64 = 10.5 185 | ``` 186 | 187 | #### 地址 188 | 189 | `Cadence` 中当需要用与账户进行交互时,可以使用“地址”类型引用它们。 190 | 191 | ```cadence 192 | let myAddress: Address = 0x96462d76b0a776b1 193 | ``` 194 | 195 | #### 字符串 196 | 197 | Unicode 字符的不可变集合。 198 | 199 | ```cadence 200 | let name = "Hello" 201 | ``` 202 | 203 | 字符串方法和字段。 204 | 205 | ```cadence 206 | name.length // 5 207 | name.utf8 // [72, 101, 108, 108, 111] 208 | name.concat(" World") // "Hello World" 209 | name.slice(from: 0, upTo: 1) // "H" 210 | ``` 211 | 212 | #### 可选类型 213 | 214 | 可以通过在类型后添加`?`表示可选,那么这些字段将可以被设置为`nil`以表示**空值**。 215 | 216 | ```cadence 217 | var inbox: String? = nil 218 | inbox = "Says hi!" 219 | inbox = nil 220 | ``` 221 | 222 | #### 数组 223 | 224 | `Cadence` 数组是可变的,可以有固定或可变的长度。 225 | 数组元素必须是相同的类型`T`或属于`T`的子类型。 226 | 227 | ```cadence 228 | let days = ["Monday", "Tuesday"] 229 | days 230 | days[0] 231 | days[2] 232 | ``` 233 | 234 | 数组类型拥有的方法和字段。 235 | 236 | ```candence 237 | days.length // 2 238 | days.concat(["Wednesday"]) // ["Monday", "Tuesday", "Wednesday"] 239 | days.contains("Friday") // false 240 | days.append("Wednesday") 241 | days // ["Monday", "Tuesday", "Wednesday"] 242 | days.appendAll(["Thursday", "Friday"]) 243 | days.remove(at: 0) // "Monday" 244 | days // ["Tuesday", "Wednesday", "Thursday", "Friday"] 245 | days.insert(at: 0, "Monday") 246 | days // ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] 247 | days.removeFirst() // "Monday" 248 | days.removeLast() // "Friday" 249 | ``` 250 | 251 | #### 字典 252 | 253 | 字典是可变的、无序的键值对集合。 254 | 其键值必须是可散列和可比较大小的,大多数内置类型都符合这些要求。 255 | 256 | ```cadence 257 | {} // 空字典 258 | let capitals = {"Japan": "Tokyo", "France": "Paris"} 259 | capitals["Japan"] // "Tokyo" of type String? 260 | capitals["England"] = "London" 261 | capitals 262 | ``` 263 | 264 | 字典类型拥有的方法和字段。 265 | 266 | ```cadence 267 | capitals.keys // ["Japan", "France", "England"] 268 | capitals.values // ["Tokyo", "Paris", "London"] 269 | capitals.containsKey("USA") // false 270 | capitals.remove(key: "France") // "London" 271 | ``` 272 | 273 | ### 函数与闭包 274 | 275 | > 参考文档:[函数说明][3.1] 276 | 277 | Cadence 的函数与其他语言中的函数非常相似,尤其是 Swift 语言。它们是值类型,这意味着您可以将它们分配给变量,并将它们作为参数传递给其他函数。 278 | 279 | 到目前为止,我们一直在使用 `flow cadence` 的 **REPL** 功能。为了学习函数,现在我们将通过向解释器发送程序文件的方式,来执行我们的代码。 280 | 281 | ```sh 282 | flow cadence test.cdc 283 | ``` 284 | 285 | 命令行通过 **REPL** 功能执行与 `.cdc` 文件的唯一区别是:通过文件来执行的方式中,你必须声明一个程序开始执行的入口点。 286 | 您需要通过声明一个名为 `main()` 的函数来实现。 287 | 288 | ```cadence 289 | pub fun main() { 290 | log("Hi!") 291 | } 292 | ``` 293 | 294 | `fun` 之前的关键字 `pub` 是一个访问修饰符,它定义了对值的 *public* 访问。我们稍后会讨论它,现在,在声明 `main()` 函数之外的任何内容时,只需使用 `pub`。 295 | 296 | ```cadence 297 | pub fun sayHi(to name: String) { 298 | log("Hi, ".concat(name)) 299 | } 300 | pub fun main() { 301 | sayHi(to: "Cadence") 302 | } 303 | ``` 304 | 305 | 和具有闭包特性的其他语言一样,闭包可以在函数内定义,并可以读写闭包外部的一些变量。 306 | 307 | ```cadence 308 | // 注意这个函数返回的结果同样是一个函数定义 309 | fun makeCounter(): ((): Int) { 310 | var count = 0 311 | return fun (): Int { 312 | // 读写 count 变量 313 | count = count + 1 314 | return count 315 | } 316 | } 317 | 318 | let test = makeCounter() 319 | test() // is `1` 320 | test() // is `2` 321 | ``` 322 | 323 | ### 控制循环 324 | 325 | > 参考文档:[控制循环][3.2] 326 | 327 | #### 条件 328 | 329 | `Cadence`的条件语法和其他语言一样,以 `if..else..` 为基础。 330 | 331 | ```cadence 332 | let a = 0 333 | var b = 0 334 | 335 | if a == 1 { 336 | b = 1 337 | } else { 338 | b = 2 339 | } 340 | // `b` is `2` 341 | ``` 342 | 343 | 值得一提的是在可选类型上,`Cadence`支持可选绑定的`if let`语法结构。 344 | 345 | ```cadence 346 | let maybeNumber: Int? = 1 347 | 348 | if let number = maybeNumber { 349 | // 因为 maybeNumber 目前有值所以走这条路线,并为 number 常量设置为 1 和 Int 类型 350 | } else { 351 | // 这条分支将不会被走到,因为 maybeNumber 不是 nil 352 | } 353 | ``` 354 | 355 | 在 `switch` 语法的使用上和其他类型的语言是比较类似,最大的不同是不可向下传递,每个 `case` 后必须跟着有效代码。 356 | 357 | ```cadence 358 | fun words(_ n: Int): [String] { 359 | // 定义一个保存字符串的数组 360 | let arr: [String] = [] 361 | 362 | // 测试参数 n 的值 363 | switch n { 364 | case 1: 365 | // 如果 n 为 1 将 "one" 加入数组 366 | arr.append("one") 367 | case 2: 368 | // 如果 n 为 2 将 "two" 加入数组 369 | arr.append("two") 370 | default: 371 | // 如果 n 不是 1 或者 2 将 "other" 加入数组 372 | arr.append("other") 373 | } 374 | return arr 375 | } 376 | 377 | words(1) // 返回 `["one"]` 378 | words(2) // 返回 `["two"]` 379 | words(3) // 返回 `["other"]` 380 | words(4) // 返回 `["other"]` 381 | ``` 382 | 383 | #### 循环 384 | 385 | `while` 表述. 386 | 387 | ```cadence 388 | var a = 0 389 | while a < 5 { 390 | a = a + 1 391 | } 392 | // `a` is `5` 393 | ``` 394 | 395 | `for..in` 表述,用于常见的数组或者字典结构。 396 | 397 | ```cadence 398 | let array = ["Hello", "World", "Foo", "Bar"] 399 | 400 | for element in array { 401 | log(element) 402 | } 403 | // "Hello" 404 | // "World" 405 | // "Foo" 406 | // "Bar" 407 | ``` 408 | 409 | 循环支持 `continue`, `break` 等常见关键词。 410 | 411 | ### 组合类型 412 | 413 | 我们现在有了开始组装更复杂结构的知识基础。在 `Cadence` 中,您有两种复合类型。 414 | 415 | 1. 结构 `struct` - 值类型(可复制的) 416 | 2. 资源 `resource` - 线性类型(可移动的,不可复制的,只能存在一次) 417 | 418 | #### 定义声明 419 | 420 | 声明结构和资源的方式几乎相同,每个都可以有字段、函数和初始化函数。 421 | 每个字段都必须在 `init()` 函数中初始化。 422 | 423 | ```cadence 424 | // 结构的关键词是 struct 425 | pub struct Rectangle { 426 | pub let width: Int 427 | pub let height: Int 428 | 429 | init(width: Int, height: Int) { 430 | self.width = width 431 | self.height = height 432 | } 433 | } 434 | 435 | // 资源的关键词是 resource 436 | pub resource Wallet { 437 | pub var dollars: UInt 438 | 439 | init(dollars: UInt) { 440 | self.dollars = dollars 441 | } 442 | } 443 | ``` 444 | 445 | #### 实例化 446 | 447 | *结构*像常规类型一样进行初始化。同样的,它和其他常规类型一样,允许垃圾回收器隐式处理释放。 448 | 449 | ```cadence 450 | let square = Rectangle(width: 10, height: 10) 451 | ``` 452 | 453 | *资源*是不同的,我们使用 `<-` 代替 `=` 来表示我们正在将资源从一个地方移动到另一个地方。 454 | 而且我们**必须**显式使用 `create` 和 `destroy` 来明确标记我们的资源的初始化和释放的过程。**必须**将资源明确分配给位于给定范围之外的变量或字段,否则必须将其销毁。 455 | 456 | ```cadence 457 | let myWallet <- create Wallet(dollars: 10) 458 | destroy myWallet 459 | ``` 460 | 461 | 我们也可以结合函数来进行*资源*的创建,要注意的是`<-`将始终用于资源对象的转移。 462 | 463 | ```cadence 464 | pub fun createWallet (_ dollars: UInt): @Wallet { 465 | return <- create Wallet(dollars: dollars) 466 | } 467 | 468 | pub fun main() { 469 | let myWallet <- createWallet(10) 470 | destroy myWallet 471 | } 472 | ``` 473 | 474 | 这里有个注意点,为表示函数返回的类型是一个资源,我们需要在返回类型前增加`@`符号。 475 | 更多相关信息,可以查看 [复合类型][3.3] 文档 476 | 477 | #### 代码实践 478 | 479 | 现在我们已经对 `Cadence` 有比较基础的了解,可以开始一定构建 DApp 了。 480 | 首先我们从最基本的结构和功能开始,最终逐步构建成一个完整的应用程序,可以根据你输入的文本创造一个独特的元资产(Meta Asset)NFT。 481 | 482 | 我们的目标是构建一个有趣的元资产,它需要足够地简单方可被更多的其他NFT进行组合与赋能,那我们就先从最直接的 `bytes` 和可选的 `raw` 开始。 483 | 我们可以创建一个 Cadence 结构来储存我们的元资产数据,目前只需要定义最少的元素。 484 | 485 | ```cadence 486 | // 元特征定义 487 | pub struct MetaFeature { 488 | pub let bytes: [UInt8] 489 | // 可选,原始数据 490 | pub let raw: String? 491 | 492 | init(bytes: [UInt8], raw: String?) { 493 | self.bytes = bytes 494 | self.raw = raw 495 | } 496 | } 497 | ``` 498 | 499 | 现在我们可以实例化一个简单的 MetaFeature 结构 500 | 501 | ```cadence 502 | pub fun main() { 503 | let raw = "Hello World" 504 | let bytes = raw.utf8 505 | // 创建元特征结构 506 | let feature = MetaFeature(bytes: bytes, raw: raw) 507 | } 508 | ``` 509 | 510 | 现在我们将创建一个具有特定所有权的实体,这个实体必须要附属于某个特定实体,因此现在我们需要使用**资源**类型。 511 | 为此,我们只需要将 `MetaFeature` 结构包装在一个 `Element` 资源中。 512 | 513 | ```cadence 514 | // 元要素 515 | pub resource Element { 516 | // 实体中的元要素特征 517 | pub let feature: MetaFeature 518 | 519 | init(feature: MetaFeature) { 520 | self.feature = feature 521 | } 522 | } 523 | ``` 524 | 525 | 现在,我们可以将这个 `Element` 资源与 `MetaFeature` 一起使用。 526 | 527 | ```cadence 528 | // 创建资源并打印出来 529 | pub fun createAndLog(raw: String) { 530 | let bytes = raw.utf8 531 | // 创建元特征结构 532 | let feature = MetaFeature(bytes: bytes, raw: raw) 533 | // 创建实体 534 | let entity <- create Element(feature: feature) 535 | log(entity.feature) 536 | // 必须销毁实体,否则会报错 537 | destroy entity 538 | } 539 | 540 | // 入口函数 541 | pub fun main() { 542 | createAndLog(raw: "Hello World") 543 | } 544 | ``` 545 | 546 | 执行 `flow cadence metaElement.cdc`,我们便可以看到打印出来的结构信息。 547 | 548 | ## Playground 549 | 550 | 我们刚才使用 `flow cadence` 进行了第一次 Cadence 的代码实践,它是一个编程语言解释器,也是一个很好的入门方式。 551 | 然而,DApp开发不仅仅是解释并执行代码,它们还需要与区块链的全局状态交互。 552 | 553 | **Flow** 为我们提供了很多开发工具和入门的材料。 554 | 555 | - 一个代码演练场(Playground) 556 | - 一个独立的本地 Flow 模拟器 557 | - 一个公共的测试网 558 | 559 | 现在我们将使用 **Playground** 继续完成基础的 `Cadence` 教学,另外两个部分将在下一节课进行介绍。 560 | 561 | ### 运行环境 562 | 563 | 启动浏览器并打开 564 | 565 | ![代码演练场截图](https://github.com/decentology/fast-floward-1/blob/main/week1/day2/images/playground.jpg?raw=true) 566 | 567 | Playground 界面有5个关键部分,让我们来逐一看看每一个。 568 | 569 | ### Cadence 编辑器 570 | 571 | 这是您存储 `Cadence` 代码的地方。由于 Playground 模拟了 Flow 区块链,因此 Playground 有一些 Cadence REPL 中不存在的特殊限制。 572 | 573 | - 您只能在 **合约** 编辑器中定义 `contract`、`struct` 和 `resource` 类型,您可以通过从左侧窗格中选择任何 **account** 来打开该编辑器。 574 | - 同样的限制适用于 Cadence `事件 event`类型。 575 | 576 | 准备好部署合约后,点击绿色的 **Deploy** 按钮。重新点击该按钮会再一次部署合约。 577 | 578 | Flow Playground 允许您更新现有合约,但是有时更新可能会失败。 579 | 如果您遇到了一些异常问题,请尝试打开一个新的 Playground 并在那里部署您的合约。 580 | 581 | ![Cadence 编辑器](https://github.com/decentology/fast-floward-1/blob/main/week1/day2/images/editor.jpg?raw=true) 582 | 583 | ### 账号 584 | 585 | 在 Flow 中一切都是在**帐户**内存储的,包括智能合约。因此无论做什么都需要访问一个或多个帐户,而 Playground 为我们提供了 5 个自动生成的帐户。 586 | 587 | 目前 Playground 的一个限制是每个账户只能部署一个合约,当然实际在 Flow 区块链上每个账户内是可以部署多个合约的。 588 | 589 | ![账户](https://github.com/decentology/fast-floward-1/blob/main/week1/day2/images/accounts.jpg?raw=true) 590 | 591 | ### 交易 592 | 593 | **交易**是定义 Flow 交易的地方。交易通常用于改变区块链的状态,因此需要由涉及的每一个参与方签名。 594 | 与其他区块链一样,Flow 交易必须使用私钥进行加密签名,以对交易数据进行编码。 当然 Playground 对此也进行了抽象,签署交易只需一键即可完成。 595 | 596 | ![交易](https://github.com/decentology/fast-floward-1/blob/main/week1/day2/images/transactions.jpg?raw=true) 597 | 598 | ### 脚本 599 | 600 | **脚本** 面板是您定义 Flow 脚本的地方,这些脚本是不需要任何区块链改变状态的*只读*代码。 601 | 因此与交易不同,它们不会产生 gas 费用(即使 Playground 没有任何手续费),并且它们不需要任何帐户的签名授权。 602 | 603 | ![脚本](https://github.com/decentology/fast-floward-1/blob/main/week1/day2/images/scripts.jpg?raw=true) 604 | 605 | ### 日志和存储 606 | 607 | Cadence 为开发人员提供了非常方便的日志功能 - `log()`。您可以记录变量,查看程序执行时状态如何变化,我们已经通过 `flow cadence` 体验到了这一点。 608 | Playground 也有一个让你看到你的 `log()` 输出的地方。 609 | 610 | 一旦您开始使用帐户存储数据,您还会在底部窗格中找到帐户的存储信息。 611 | 612 | ![日志和存储](https://github.com/decentology/fast-floward-1/blob/main/week1/day2/images/logAndStorage.jpg?raw=true) 613 | 614 | ## Playgound实践 615 | 616 | 我们就用上面的例子,继续实践一下Playground。 617 | 618 | ### 合约部署 619 | 620 | 让我们创建我们的第一个 Cadence 智能合约! 621 | 622 | 1. 从 **账号(Accounts)** 窗格中选择 `0x01`。 623 | 2. 将我们刚才的例子代码,更新到合约代码中去(可以直接复制下面的代码)。 624 | 625 | ```cadence 626 | pub contract Entity { 627 | // 元特征 628 | pub struct MetaFeature { 629 | // 哈希值 630 | pub let bytes: [UInt8] 631 | // 原始数据 632 | pub let raw: String? 633 | 634 | init(bytes: [UInt8], raw: String?) { 635 | self.bytes = bytes 636 | self.raw = raw 637 | } 638 | } 639 | 640 | // 元要素 641 | pub resource Element { 642 | // 实体中的元要素特征 643 | pub let feature: MetaFeature 644 | 645 | init(feature: MetaFeature) { 646 | self.feature = feature 647 | } 648 | } 649 | 650 | pub fun createAndLog(raw: String) { 651 | let bytes = raw.utf8 652 | // 创建元特征结构 653 | let feature = MetaFeature(bytes: bytes, raw: raw) 654 | // 创建实体 655 | let entity <- create Element(feature: feature) 656 | log(entity.feature) 657 | // 必须销毁实体,否则会报错 658 | destroy entity 659 | } 660 | } 661 | ``` 662 | 663 | 3. 点击 **Deploy** 664 | 4. 从日志中获取确认信息。 665 | 666 | ``` 667 | 11:11:11 Deployment > [1] > Deployed Contract To: 0x01 668 | ``` 669 | 670 | 此时在**帐户**窗格中的`0x01`帐户下看到您部署的合约的名称。在这种情况下,名称直接取自源代码,但在 Playground 之外,您可以使用 `name: String` 和合约源代码部署合约。 因此,每个帐户可以在不同名称下拥有同一合约的多个实例。 但如前所述,目前 Playground 帐户每个仅支持一份合约。 671 | 672 | 现在,我们已经成功部署了一个区块链程序,我们试着完成一次交互吧! 673 | 674 | ### 脚本查询 675 | 676 | 现在点击**Script**窗格,让我们编写一些代码。 677 | 678 | 我们从拥有合约的账户地址**导入**我们想要与之交互的合约,可以将这个视为从包管理器的库中导入类这样的情况。在 Flow 上所有内容都存储在账户之中,其中自然包括所有的合约。 679 | 680 | ```cadence 681 | import Entity from 0x01 682 | 683 | pub fun main() { 684 | Entity.createAndLog(raw: "Hello World") 685 | } 686 | ``` 687 | 688 | 单击 **Execute**,您将在 **Log** 窗格中看到如下的两行。 689 | 690 | ``` 691 | 11:11:11 Script > [1] > A.0000000000000001.Entity.MetaFeature(bytes: [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100], raw: "Hello World") 692 | 11:11:11 Script > [2] Result > {"type":"Void"} 693 | ``` 694 | 695 | 通常使用脚本获取有关公共状态的信息,因此预计它们会“返回”某种值。 696 | 在我们的例子中,我们没有显式返回任何东西,类似于 JavaScript中,当函数返回 undefined 时,在没有显式 return 语句的 Cadence 函数中返回 Void 类型。 697 | 698 | 到现在为止,我们已经完成了一次将一个简单的合约部署到调用的全部实践! 699 | 在下节课我们会更详细地介绍智能合约、交易事务,现在就尝试一下这些作业和任务吧。 700 | 701 | ## 课后作业 702 | 703 | 习题问卷表单将通过我们的社交群组向学员推送。 704 | 705 | ### 编程题 706 | 707 | **注:** 请在表单中提交保存后的 Playground URL。 708 | 709 | - `Q1` - 打印输出 710 | 711 | 在合约中编写一个输出元要素 特征的函数,将 bytes 的每两位作为一个特征代码进行输出。并在 Script 中使用该函数进行输出显示。 712 | 713 | ```cadence 714 | pub fun display(feature: MetaFeature) 715 | ``` 716 | 717 | ``` 718 | "Code[0]: 72, 101" 719 | "Code[1]: 108, 108" 720 | "Code[2]: 111, 32" 721 | ``` 722 | 723 | - `Q2` - 唯一特征 724 | 725 | 创建一个可以生成“特性”信息的资源,但对于每个独特的 `bytes` 只创建一次元素资源。 726 | 727 | ```cadence 728 | pub resource Generator { 729 | pub fun generate(feature: MetaFeature): @Element? 730 | } 731 | ``` 732 | 733 | ### 问答题 734 | 735 | 现在你们已经对 Cadence 有了初步的了解。 736 | Cadence 是一门面向资源编程范式的语言,说说你对“面向资源”的理解,简述其特点。 737 | 738 | ### 挑战题 739 | 740 | **注:** 请在表单中提交保存后的 Playground URL。 741 | 742 | 根据描述,定义资源并编写脚本执行。 743 | 744 | 定义以下资源: 745 | 746 | - `Book`, 带有 `title: String` 字段; 747 | - `Bookshelf`, 带有 `books: @[Book]` 字段(私有)及以下函数: 748 | - `add(book: @Book)` 向 `books` 末尾添加一本 `Book`; 749 | - `getBooksCount(): UInt` 返回 `books` 中的元素个数; 750 | 751 | 编写脚本(Script)执行: 752 | 753 | - 创建 2 个 `Book` 实例及 1 个 `Bookshelf` 实例,使得 `Bookshelf` 拥有 2 本 Book 754 | 755 | ## 参考致谢 756 | 757 | 本课程部分摘选自 758 | 以及其中文翻译 759 | 760 | [1]: https://docs.onflow.org/flow-cli/install/ 761 | [2]: https://docs.onflow.org/vscode-extension/ 762 | [3]: https://docs.onflow.org/cadence/language/ 763 | [3.1]: https://docs.onflow.org/cadence/language/functions 764 | [3.2]: https://docs.onflow.org/cadence/language/control-flow 765 | [3.3]: https://docs.onflow.org/cadence/language/composite-types/ 766 | -------------------------------------------------------------------------------- /lesson-1/src/metaElement.cdc: -------------------------------------------------------------------------------- 1 | // 元特征 2 | pub struct MetaFeature { 3 | // 哈希值 4 | pub let bytes: [UInt8] 5 | // 原始数据 6 | pub let raw: String? 7 | 8 | init(bytes: [UInt8], raw: String?) { 9 | self.bytes = bytes 10 | self.raw = raw 11 | } 12 | } 13 | 14 | // 元要素 15 | pub resource Element { 16 | // 实体中的元要素特征 17 | pub let feature: MetaFeature 18 | 19 | init(feature: MetaFeature) { 20 | self.feature = feature 21 | } 22 | } 23 | 24 | pub fun createAndLog(raw: String) { 25 | let bytes = raw.utf8 26 | // 创建元特征结构 27 | let feature = MetaFeature(bytes: bytes, raw: raw) 28 | // 创建实体 29 | let entity <- create Element(feature: feature) 30 | log(entity.feature) 31 | // 必须销毁实体,否则会报错 32 | destroy entity 33 | } 34 | pub fun main() { 35 | createAndLog(raw: "Hello World") 36 | } 37 | -------------------------------------------------------------------------------- /lesson-2/README.md: -------------------------------------------------------------------------------- 1 | # 第二讲 初识 Cadence - 账户、交易、模拟器与本地开发 2 | 3 | 本节课我们将继续补充 Cadence 的重要知识点,学习 Flow 账户存储模型,学习使用**交易**和**脚本**与智能合约进行交互。 4 | 更重要的是,我们将进一步了解 Flow 模拟器、本地配置、本地密钥等,开启 Cadence 项目的工程化进程。 5 | 6 | ## 账户存储空间与资源保存 7 | 8 | > 账户文档: 9 | 10 | 我们先继续完善一下上节课的拓展任务,它是我们了解的第一个智能合约,现在我们将对它进行一定的补充。 11 | 12 | ```cadence 13 | pub contract Entity { 14 | // 元特征 15 | pub struct MetaFeature { 16 | 17 | pub let bytes: [UInt8] 18 | pub let raw: String? 19 | 20 | init(bytes: [UInt8], raw: String?) { 21 | self.bytes = bytes 22 | self.raw = raw 23 | } 24 | } 25 | 26 | // 元要素 27 | pub resource Element { 28 | 29 | pub let feature: MetaFeature 30 | 31 | init(feature: MetaFeature) { 32 | self.feature = feature 33 | } 34 | } 35 | 36 | // 特征收集器 37 | pub resource Generator { 38 | 39 | pub let features: {String: MetaFeature} 40 | 41 | init() { 42 | self.features = {} 43 | } 44 | 45 | pub fun generate(feature: MetaFeature): @Element? { 46 | // 只收集唯一的 bytes 47 | let hex = String.encodeHex(feature.bytes) 48 | 49 | if self.features.containsKey(hex) == false { 50 | let element <- create Element(feature: feature) 51 | self.features[hex] = feature 52 | return <- element 53 | } else { 54 | return nil 55 | } 56 | } 57 | } 58 | 59 | init() { 60 | // 保存到存储空间 61 | self.account.save( 62 | <- create Generator(), 63 | to: /storage/ElementGenerator 64 | ) 65 | // 链接到公有空间 66 | self.account.link<&Generator>( 67 | /public/ElementGenerator, // 共有空间 68 | target: /storage/ElementGenerator // 目标路径 69 | ) 70 | } 71 | } 72 | ``` 73 | 74 | 大部分代码你应该很熟悉。我们额外添加了一些新内容。 75 | 所有的代码都包含在 `contract` 的关键字中,在 Flow 里写智能合约时,您不能在 `contract` 关键字之外声明任何内容。 76 | 根据我们上节课的拓展任务,我们实现了 `Generator` 资源和 `generate` 函数,我们使用字典 `features` 来记录特征。同时我们为合约增加了 `init()` 初始化方法,这是为合约设置一些初始化操作的地方。 77 | 接下来我们来深入了解一下账户的存储和访问权限相关的知识点。 78 | 79 | ### 存储空间 80 | 81 | 想要在 Flow 区块链上持久化的资源,就必须用一个账户来存储。 82 | save 函数的作用就是将一个特定的资源,保存到一个唯一的存储位置。 83 | 84 | ```cadence 85 | fun save(_ value: T, to: StoragePath) 86 | ``` 87 | 88 | 在上面的例子中,我们将 `Generator` 资源保存到了 `/storage/ElementGenerator`。 89 | 90 | ```cadence 91 | // 保存到存储空间 92 | self.account.save( 93 | <- create Generator(), 94 | to: /storage/ElementGenerator 95 | ) 96 | ``` 97 | 98 | 账户中的存储空间存在三个可用的域。 99 | 100 | 1. storage - 资源和值的实际保存位置,只能通过 `save()` 进行保存。 101 | 2. public - 可以通过 `PublicAccount` 访问的作用域。 102 | 3. private - 必须通过 `AuthAccount` 方可进行授权访问的作用域。 103 | 104 | ### 资源链接 105 | 106 | Cadence 采用基于能力`Capability`的访问控制,允许智能合约将其部分存储资源公开给其他账户。 107 | 通过调用 `link` 方法,我们创建了一个 `Capability`。 108 | 109 | ```cadence 110 | fun link(_ newCapabilityPath: CapabilityPath, target: Path): Capability? 111 | ``` 112 | 113 | 我们在上文的例子中,将 `Generator` 放置到了 `/public/ElementGenerator` 位置。 114 | 115 | ```cadence 116 | // 链接到公有空间 117 | self.account.link<&Generator>( 118 | /public/ElementGenerator, // 共有空间 119 | target: /storage/ElementGenerator // 目标路径 120 | ) 121 | ``` 122 | 123 | 这里我们需要额外说明一下 `&` 符号的用途,它代表的是对某个对象都是引用。 124 | 125 | ### 资源引用 126 | 127 | > 引用文档: 128 | 129 | 我们可以创建对资源和结构的引用,引用使我们能够访问所引用对象的字段和函数。 130 | 131 | 如下面的代码例子。 132 | 133 | ```cadence 134 | let name = "Cadence" 135 | let nameRef: &String = &name as &String 136 | ``` 137 | 138 | 或者您可以从账户的链接域中借用它们。 139 | 140 | ```cadence 141 | let generatorRef = getAccount(0x01) 142 | .getCapability<&Entity.Generator>(/public/ElementGenerator) 143 | .borrow() 144 | ?? panic("Couldn't borrow reference to Generator") 145 | ``` 146 | 147 | ## 交易机制 148 | 149 | > 交易模型文档: 150 | 151 | Cadence 的交易不同于其他的智能合约,它同样是一段代码片段。 152 | 现在我们来看一个简单的交易示例。 153 | 154 | ```cadence 155 | import Hello from 0x01 156 | 157 | transaction { 158 | 159 | let name: String 160 | 161 | prepare(account: AuthAccount) { 162 | self.name = account.address.toString() 163 | } 164 | 165 | execute { 166 | log(Hello.sayHi(to: self.name)) 167 | } 168 | } 169 | ``` 170 | 171 | 交易体需要包含在 `transaction` 的声明内,可以定义局部变量,同时有几个固定的交易阶段。 172 | 173 | ### 交易阶段 174 | 175 | 每个交易都有4个顺序执行的步骤,当然有些是可选的。 176 | 177 | ```cadence 178 | transaction(parameter: String) { 179 | let localVariable: Int 180 | prepare(signer: AuthAccount) {} 181 | pre {} 182 | execute {} 183 | post {} 184 | } 185 | ``` 186 | 187 | 如果想要在4个阶段之间共享数据,可以在 `transaction` 主体中声明局部变量,不需要访问修饰符。 188 | 其中 `pre` 和 `post` 阶段,与函数中作用相同,通常语言前置条件和后置条件的检测,在以后会有更多的介绍。 189 | 现在,我们只需要 `prepare` 和 `execute` 阶段。 190 | 191 | #### 准备阶段 prepare 192 | 193 | `prepare` 准备阶段是能够访问到 `AuthAccount` 实例的唯一阶段,只有在该阶段可以访问到账户存储和其他私有功能。 194 | 在上面的简单例子中,我们只是获取了一下 `address` 字段。 195 | 196 | ```cadence 197 | prepare(account: AuthAccount) { 198 | self.name = account.address.toString() 199 | } 200 | ``` 201 | 202 | 因为只有这个阶段可以访问 `AuthAccount`,因此要将后续需要使用到的值(例如这里的 `account.address`)保存到临时变量,以供在 `execute` 阶段访问。 203 | 204 | #### 执行阶段 execute 205 | 206 | 需要在这个阶段,执行交易的主要业务逻辑。而且此时也**不能**访问 `AuthAccount` 的私有对象。 207 | 208 | 在 `execute` 阶段,我们调用 `Hello` 合约的 `sayHi` 函数,使用签名账户地址作为 `name` 参数的值。 209 | 210 | ```cadence 211 | execute { 212 | log(Hello.sayHi(to: self.name)) 213 | } 214 | ``` 215 | 216 | ### 多账户交易 AuthAccount 217 | 218 | 我们注意到交易的prepare阶段, `AuthAccount` 是以参数的形式传入的。 219 | 实际上,Cadence的交易是原生支持多个 `AuthAccount` 同时进行参与的。 220 | 221 | ```cadence 222 | prepare(acc1: AuthAccount, acc2: AuthAccount) { 223 | self.message = acc1.address.toString().concat(" + ").concat(acc2.address.toString()) 224 | } 225 | ``` 226 | 227 | ### 复杂交易 228 | 229 | 现在我们使用之前部署的合约 `Entity`,执行一个相对复杂的交易。 230 | 注意:交易中是无法使用 `create` 关键字创建资源的,所有资源必须在合约中生成。 231 | 232 | ```cadence 233 | import Entity from 0x02 234 | 235 | transaction(entityAddress: Address) { 236 | 237 | let message: String 238 | let element: @Entity.Element? 239 | 240 | prepare(account: AuthAccount) { 241 | // use get PublicAccount instance by address 242 | let generatorRef = getAccount(entityAddress) 243 | .getCapability<&Entity.Generator>(/public/ElementGenerator) 244 | .borrow() 245 | ?? panic("Couldn't borrow printer reference.") 246 | 247 | self.message = "Hello World" 248 | let feature = Entity.MetaFeature( 249 | bytes: self.message.utf8, 250 | raw: self.message 251 | ) 252 | 253 | // save resource 254 | self.element <- generatorRef.generate(feature: feature) 255 | } 256 | 257 | execute { 258 | if self.element == nil { 259 | log("Element of feature<".concat(self.message).concat("> already exists!")) 260 | } else { 261 | log("Element generated") 262 | } 263 | 264 | destroy self.element 265 | } 266 | } 267 | ``` 268 | 269 | > 请注意,目前能使用的 entityAddress 变量,还只有 0x02,也就是目前合约部署的地址。 请思考一下为什么? 270 | > 请思考一下,上面这个 transaction 中的 entityAddress 和 account 分别起到了什么作用? 271 | 272 | 该交易执行后,会生成一个具有特定特征码的元素,然后会将其销毁。 273 | 如果我们执行该交易两次,将会得到下列日志: 274 | 275 | ```cadence 276 | 11:11:11 New Transaction > [1] > "Element generated!" 277 | 11:11:12 New Transaction > [2] > "Element of feature already exists!" 278 | ``` 279 | 280 | 我们在 Playground 执行后,可以去账户的 Storage 窗格中查看变更。 281 | 282 | ## 工程项目初始化和环境配置 283 | 284 | 我们已经在浏览器中借助 Flow Playground 进行了合约部署、运行脚本、执行交易等操作。 285 | 现在我们将回归 VS Code,在代码编辑器中,工程化得重新实现这些步骤。 286 | 287 | ### 工程初始化和配置 288 | 289 | > 初始化文档: 290 | 291 | #### 项目配置 292 | 293 | 打开终端(terminal)并运行这个命令 294 | 295 | ```sh 296 | flow init 297 | ``` 298 | 299 | 它将会在当前的目录下创建一个配置文件,`flow.json`。 300 | 它主要的目的是将你的本地环境与合约,账户的的别名(aliases)连接起来,以及组织你的合约部署。 301 | 302 | #### 全局配置 303 | 304 | Flow 也支持使用一个全局配置 `flow.json`,这样你可以将一些通用的配置内容设置在其中,但它相对于本地工程目录下的 `flow.json` 优先级更低。 305 | 生成全局配置只需要添加一个 `--global` 标记。 306 | 307 | ```sh 308 | flow init --global 309 | ``` 310 | 311 | 全局配置文件保存的位置在: 312 | 313 | - macOS: `~/flow.json` 314 | - Linux: `~/flow.json` 315 | - Windows: `C:\Users\$USER\flow.json` 316 | 317 | ### 环境配置 318 | 319 | > 配置文档: 320 | 321 | 现在我们一起来看一下 `flow.json` 中的几个部分。 322 | 323 | #### networks 网络配置 324 | 325 | 这里是配置不同 Flow 链环境的节点地址。 326 | 当发布交易或执行脚本时,需要使用 networks 下的环境名称,比如 `emulator` (模拟器), `testnet` (测试网), `mainnet` (主网)。 327 | **如果不作指定,那么默认是 `emulator` 环境。** 328 | 329 | ```json 330 | { 331 | "networks": { 332 | "emulator": "127.0.0.1:3569", 333 | "mainnet": "access.mainnet.nodes.onflow.org:9000", 334 | "testnet": "access.devnet.nodes.onflow.org:9000" 335 | } 336 | } 337 | ``` 338 | 339 | 通常我们会先在模拟器环境中进行开发。 340 | 341 | #### accounts 账户配置 342 | 343 | 在这里你可以给在开发过程中用到的不同的账户起个别名。 344 | 每个 `flow.json` 配置文件的第一部分都是 `emulator-account` (模拟器账户),或者可以理解成 `service account` (服务账户)。 345 | 你可以添加其他账户用以模拟你的 DApp 中会遇到的各式情况,或者用来部署合约等等。 346 | 347 | ```json 348 | { 349 | "accounts": { 350 | "emulator-account": { 351 | "address": "f8d6e0586b0a20c7", 352 | "key": "bcbd7e16179f286eeb805e06482ac45657d1dface4a775511abcaf8e4b6d4373" 353 | } 354 | } 355 | } 356 | ``` 357 | 358 | #### contracts 合约配置 359 | 360 | 在使用 Flow Playground 的时候,已经体会过通过从其他账户地址 `import` 合约来与之交互。 361 | 但是,如果你在Cadence脚本中明确账户地址的话,你将只能将合约部署到有这个地址的环境中。这是因为账户地址在模拟器,测试网,主网的不同环境中是不通用的。 362 | 363 | 为了解决这个问题,其实只需要在 `*.cdc` 文件中使用从文件引入`import Contract from "path/to/contract.cdc"` 这样的形式。 364 | 之后,如果在 `flow.json` 中已经定义明确的别名,flow-cli 就会在后续过程中接手替换成对应的账户地址。 365 | 366 | ```json 367 | { 368 | "contracts": { 369 | "Hello": "./hello/contract.cdc" 370 | } 371 | } 372 | ``` 373 | 374 | #### deployments 部署配置 375 | 376 | 这部分就是对上述那些内容的合并使用了,即明确环境、账户、合约的相互关系。 377 | 以 网络 network > 账户 account > 合约 contract 的方式表示: 378 | 在什么网络环境中,再什么账户地址上,部署哪些个合约。 379 | 380 | ```json 381 | { 382 | "deployments": { 383 | "emulator": { 384 | "emulator-account": [ 385 | "Hello" 386 | ] 387 | } 388 | } 389 | } 390 | ``` 391 | 392 | ## 模拟器、密钥对和账户管理 393 | 394 | Flow 模拟器的功能是 `flow-cli` 的一部分,安装后即可使用。 395 | 396 | ### Flow 模拟器 397 | 398 | > 模拟器文档: 399 | 400 | 如何启动一个Flow模拟器?非常简单,只需要一行命令: 401 | 402 | ```sh 403 | flow emulator start 404 | ``` 405 | 406 | **注意** : 如果你看到一个系统弹窗要求网络许可,请允许。 407 | 408 | 你将会看到 4 个 `INFO` 信息, 最后的两条是用来确认 `gRPC` 和 `HTTP` 服务器正常启动, 说明一切正常. 409 | 只要这个进程一直运行,模拟器就也会一直运行。你可以通过使用 `SIGINT` 终端信号停止进程 (macOS的终端是 `CTRL + C`). 410 | 411 | 默认状态下,模拟器生成的数据将会在模拟器进程关闭后丢失。如果需要持久化的话,你可以通过添加 `--persist` 标记要求模拟器保留数据。 412 | 413 | ```sh 414 | flow emulator start --persist 415 | ``` 416 | 417 | 它会为模拟器创建一个 `flowdb` 文件夹来存储它的状态。当然,你可以随时删除这个 `flowdb` 文件夹进行状态重置。 418 | 419 | ### 密钥对 420 | 421 | > 密钥文档: 422 | 423 | Flow 是账户地址模型而不是公钥地址模型,即地址关联到一个或多个公钥。 424 | `flow-cli` 提供了一个非常便捷的方法创建公钥,而且可以指定签名算法。下面这个命令设置的是使用 `ECDSA_secp256k1` 算法。 425 | 426 | ```sh 427 | flow keys generate\ 428 | --sig-algo=ECDSA_secp256k1 429 | ``` 430 | 431 | 它将打印并返回一对公钥和私钥。 432 | 433 | ### Flow 账户 434 | 435 | > 账户创建文档: 436 | 437 | 创建账户需要在某个指定的环境中进行,如果是模拟器环境的话,需要确保 `flow emulator` 处于进程运行的状态。 438 | 如果不明确指定网络环境 `--network` 的话,默认是使用模拟器环境。 439 | 如果不明确算法 `--sig-algo` 的话,添加的账户公钥默认为 `ECDSA_P256` 。 440 | 441 | 同时由于Flow账户需要在区块链上进行注册,因此需要一个现有的账号发起一笔交易方可创建。 442 | 因此在模拟器环境中,我们可以使用已经预设生成好的 `emulator-account`。 443 | 444 | ```sh 445 | flow accounts create \ 446 | --key "$PUBLIC_KEY" \ 447 | --sig-algo "ECDSA_secp256k1" \ 448 | --signer "emulator-account" 449 | ``` 450 | 451 | 当上述命令执行成功后,你将得到一个新的账户地址。 452 | 和公钥地址模型的区块链不同的是,该地址是区块链生成的,它可以对应多个不同的公钥,也意味着它也无法从公钥直接推导出来。 453 | 454 | ```console 455 | Transaction ID: 1234...abcd 456 | 457 | Address 0xabcd...0123 458 | Balance 0.00100000 459 | Keys 1 460 | ``` 461 | 462 | 同时可以通过下列命令查看刚才的生成的账户信息。 463 | 464 | ```sh 465 | flow accounts get 0xabcd...0123 466 | ``` 467 | 468 | 最后,我们可以更新 `flow.json`,把刚才的新生成的账户添加进来。 469 | 470 | ```json 471 | { 472 | "accounts": { 473 | "the-creator": { 474 | "address": "abcd...0123", 475 | "key": { 476 | "type": "hex", 477 | "index": 0, 478 | "signatureAlgorithm": "ECDSA_secp256k1", 479 | "hashAlgorithm": "SHA3_256", 480 | "privateKey": "$PRIVATE_KEY" 481 | } 482 | } 483 | } 484 | } 485 | ``` 486 | 487 | ## 使用模拟器进行合约开发 488 | 489 | 现在我们将要使用模拟器,对项目进行工程化的开发。在这部分我们使用 `hello` 文件夹中的简单合约为例子。 490 | 491 | ### 事件与合约部署 492 | 493 | > 事件文档: 494 | 495 | 现在先来看下我们的合约。 496 | 497 | ```cadence 498 | pub contract Hello { 499 | pub event IssuedGreeting(greeting: String) 500 | 501 | pub fun sayHi(to name: String): String { 502 | let greeting = "Hi, ".concat(name) 503 | 504 | emit IssuedGreeting(greeting: greeting) 505 | 506 | return greeting 507 | } 508 | } 509 | ``` 510 | 511 | 这里要注意下,我们没有使用 `log()` 输出内容,因为这个日志一般只在 Flow Playground 和 Cadence REPL shell 中使用。 512 | 所以我们也将引入事件 `Event` 机制,当与一个合约进行交互时,通过 `Event` 来显示什么时间发生了什么事是非常有帮助的。在模拟器中,交易不会返回值也不会打印日志,所以更加需要事件来确定具体的合约执行情况。 513 | 514 | > 部署文档: 515 | 516 | 部署合约时,先需要修改 `flow.json` 来个这个合约起一个名字,并且说明源文件的路径。 517 | 518 | ```json 519 | { 520 | "contracts": { 521 | "Hello": "./hello/contract.cdc" 522 | } 523 | } 524 | ``` 525 | 526 | 然后我们定义合约部署的目标。 527 | 528 | ```json 529 | { 530 | "deployments": { 531 | "emulator": { 532 | "the-creator": [ 533 | "Hello" 534 | ] 535 | } 536 | } 537 | } 538 | ``` 539 | 540 | 然后下一步是部署这个合约。 541 | 542 | ```sh 543 | flow project deploy 544 | ``` 545 | 546 | 一切完成后,我们应该会看到下面这个输出。 547 | 548 | ```console 549 | Deploying 1 contracts for accounts: the-creator 550 | ``` 551 | 552 | ### 脚本查询 553 | 554 | 现在我们已经将第一个合约部署到了一个账户之中了。现在我们来执行一个简单的脚本。 555 | 556 | 注意:由于事件 `Event` 是不可以在脚本中执行的,所以需要注释掉 `emit event` 方法。 557 | 558 | ```cadence 559 | import Hello from "./contract.cdc" 560 | 561 | pub fun main(name: String): String { 562 | return Hello.sayHi(to: name) 563 | } 564 | ``` 565 | 566 | 然后我们使用 `flow scripts execute` 子命令运行脚本。 567 | 568 | ```sh 569 | flow scripts execute hello/sayHi.script.cdc "Cadence" 570 | ``` 571 | 572 | 我们也可以使用 `--args-json` 的方式引入多个变量。 573 | 我们可以用`JSON`来编码所有的变量,且必须是一个数组`[]`。 574 | 575 | ```sh 576 | flow scripts execute hello/sayHi.script.cdc \ 577 | --args-json='[{"type": "String", "value": "Cadence"}]' 578 | ``` 579 | 580 | **注意:**如果你使用的是 `Windows PowerShell` 或者其他非 `Unix` 终端的话,只能使用 `--arg` 来设置变量。 581 | 582 | ### 交易签名与发送 583 | 584 | > 交易文档: 585 | 586 | 然后来到了最重要的阶段,交易的签名与发送。 587 | 588 | Flow交易将经历这样的流程: 589 | 590 | 1. 构建:用 `RLP`(Recursive Length Prefix) 编码规则进行交易体编码。 591 | 2. 签名:对刚才编码过的交易体,根据交易需要的签名数量进行签名。 592 | 3. 发送:将签名结果和交易体发送到Flow节点上。 593 | 594 | #### 分步发送 595 | 596 | 现在,我们命令行一步一步完成范例交易的签名发送过程。 597 | 598 | ```sh 599 | flow transactions build ./hello/sayHi.transaction.cdc \ 600 | --authorizer the-creator \ 601 | --proposer the-creator \ 602 | --payer the-creator \ 603 | --filter payload \ 604 | --save transaction.build.rlp 605 | ``` 606 | 607 | 这里将生成一个 `transaction.build.rlp` 文件,它保存着待签名交易以及需要签名的信息。 608 | 可以注意到,`authorizer`/`proposer`/`payer` 这里都设置为 `the-creator` 时,仅需要使用该账户签名一次。反之则需要根据实际情况进行多次签名。 609 | 610 | ```sh 611 | flow transactions sign ./transaction.build.rlp \ 612 | --signer the-creator \ 613 | --filter payload \ 614 | --save transaction.signed.rlp \ 615 | -y 616 | ``` 617 | 618 | 签名后将生成一个 `transaction.signed.rlp` 文件,此时我们就可以发送到模拟器让其执行。 619 | 620 | ```sh 621 | flow transactions send-signed ./transaction.signed.rlp 622 | ``` 623 | 624 | 如果一切顺利的话,我们应该会在返回中看到之前声明的事件。 625 | 626 | #### 简易发送 627 | 628 | 当然,我们也有相对简易快捷的方式实现交易的发送过程。 629 | 630 | ```sh 631 | flow transactions send ./hello/sayHi.transaction.cdc \ 632 | --signer the-creator 633 | ``` 634 | 635 | ## 课后作业 636 | 637 | 习题问卷表单将通过我们的社交群组向学员推送。 638 | 639 | **注1:** 本次编程题将通过 进行作业递交 640 | 641 | **注2:** 以下所有编程题必须在模拟器中运行。 642 | 643 | ### 问答题 644 | 645 | Cadence中设计Storage的意义是什么? 646 | 相比以太坊 Balance Sheet 的资产管理方式,Cadence的Storage有哪些优势? 647 | 简述一下 `storage`, `private`, `public` 三类存储空间各自的作用。 648 | 649 | ### 编程题 650 | 651 | - Q1 事件添加 652 | 653 | 修改 `entity.contract.cdc` 合约,添加两个事件,并在合适的地方进行发送。 654 | 655 | ```text 656 | pub event ElementGenerateSuccess(hex: String) 657 | pub event ElementGenerateFailure(hex: String) 658 | ``` 659 | 660 | - Q2 收藏包 661 | 662 | 修改 `entity.contract.cdc` 为我们的 `Entity` 合约创建一个 `Collection` 资源,允许保存生成的 `Element` 资源。 663 | 注意:必须在合约中创建资源。 664 | 665 | ```cadence 666 | // 实现一个新的资源 667 | pub resource Collection { 668 | pub fun deposit(element: @Element) 669 | } 670 | // 实现一个创建方法 671 | pub fun createCollection(): @Collection 672 | ``` 673 | 674 | 新增一个交易 `createCollection.transactions.cdc`,创建 `Collection` 资源并保存到指定的路径。 675 | 676 | ```cadence 677 | import Entity from "./entity.contract.cdc" 678 | 679 | // 为交易的发起者创建一个 Element 收藏包 680 | transaction {} 681 | ``` 682 | 683 | - Q3 收集和展示 684 | 685 | 修改 `generate.transaction.cdc` 合约,需要实现以下功能: 686 | 687 | - 检测交易发起人是否有 `Element` 的 `Collection`,若不存在则创建一个。 688 | - 将生成的 `Element` 保存到交易发起人的收藏包中。 689 | - 若无法生成 `Element`,交易正常结束。 690 | 691 | 创建 `displayCollection.script.cdc`, 需要实现以下功能: 692 | 693 | - 返回特定账户地址收藏包中 `Element` 的列表(以合适的 `Struct` 来进行返回) 694 | - 若没有收藏包,则返回 nil 695 | 696 | ### 挑战题 697 | 698 | 根据描述,补充资源定义并编写交易。 699 | 700 | 补充 `Collection` 资源定义: 701 | 702 | - 在 `Collection` 资源中实现 `withdraw` 方法,传入参数 `hex: String`, 返回指定的 `Element` 703 | 704 | 实现交易: 705 | 706 | - 实现 `transfer` 交易,从 A 账户的 `Collection` 中提取 `Element` 转移到 B 账户 707 | 708 | ## 参考致谢 709 | 710 | 本课程部分摘选自 711 | 以及其中文翻译 712 | -------------------------------------------------------------------------------- /lesson-2/src/entity/entity.contract.cdc: -------------------------------------------------------------------------------- 1 | pub contract Entity { 2 | // 元特征 3 | pub struct MetaFeature { 4 | 5 | pub let bytes: [UInt8] 6 | pub let raw: String? 7 | 8 | init(bytes: [UInt8], raw: String?) { 9 | self.bytes = bytes 10 | self.raw = raw 11 | } 12 | } 13 | 14 | // 元要素 15 | pub resource Element { 16 | 17 | pub let feature: MetaFeature 18 | 19 | init(feature: MetaFeature) { 20 | self.feature = feature 21 | } 22 | } 23 | 24 | // 特征收集器 25 | pub resource Generator { 26 | 27 | pub let features: {String: MetaFeature} 28 | 29 | init() { 30 | self.features = {} 31 | } 32 | 33 | pub fun generate(feature: MetaFeature): @Element? { 34 | // 只收集唯一的 bytes 35 | let hex = String.encodeHex(feature.bytes) 36 | 37 | if self.features.containsKey(hex) == false { 38 | let element <- create Element(feature: feature) 39 | self.features[hex] = feature 40 | return <- element 41 | } else { 42 | return nil 43 | } 44 | } 45 | } 46 | 47 | init() { 48 | // 保存到存储空间 49 | self.account.save( 50 | <- create Generator(), 51 | to: /storage/ElementGenerator 52 | ) 53 | // 链接到公有空间 54 | self.account.link<&Generator>( 55 | /public/ElementGenerator, // 共有空间 56 | target: /storage/ElementGenerator // 目标路径 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lesson-2/src/entity/generate.transaction.cdc: -------------------------------------------------------------------------------- 1 | import Entity from "./entity.contract.cdc" 2 | 3 | transaction(entityAddress: Address) { 4 | 5 | let message: String 6 | let element: @Entity.Element? 7 | 8 | prepare(account: AuthAccount) { 9 | // use get PublicAccount instance by address 10 | let generatorRef = getAccount(entityAddress) 11 | .getCapability<&Entity.Generator>(/public/ElementGenerator) 12 | .borrow() 13 | ?? panic("Couldn't borrow printer reference.") 14 | 15 | self.message = "Hello World" 16 | let feature = Entity.MetaFeature( 17 | bytes: self.message.utf8, 18 | raw: self.message 19 | ) 20 | 21 | // save resource 22 | self.element <- generatorRef.generate(feature: feature) 23 | } 24 | 25 | execute { 26 | if self.element == nil { 27 | log("Element of feature<".concat(self.message).concat("> already exists!")) 28 | } else { 29 | log("Element generated") 30 | } 31 | 32 | destroy self.element 33 | } 34 | } -------------------------------------------------------------------------------- /lesson-2/src/hello/contract.cdc: -------------------------------------------------------------------------------- 1 | pub contract Hello { 2 | pub event IssuedGreeting(greeting: String) 3 | 4 | pub fun sayHi(to name: String): String { 5 | let greeting = "Hi, ".concat(name) 6 | 7 | emit IssuedGreeting(greeting: greeting) 8 | 9 | return greeting 10 | } 11 | } -------------------------------------------------------------------------------- /lesson-2/src/hello/sayHi.script.cdc: -------------------------------------------------------------------------------- 1 | import Hello from "./contract.cdc" 2 | 3 | pub fun main(name: String): String { 4 | return Hello.sayHi(to: name) 5 | } -------------------------------------------------------------------------------- /lesson-2/src/hello/sayHi.transaction.cdc: -------------------------------------------------------------------------------- 1 | import Hello from "./contract.cdc" 2 | 3 | transaction { 4 | 5 | let name: String 6 | 7 | prepare(account: AuthAccount) { 8 | self.name = account.address.toString() 9 | } 10 | 11 | execute { 12 | Hello.sayHi(to: self.name) 13 | } 14 | } -------------------------------------------------------------------------------- /lesson-3/README.md: -------------------------------------------------------------------------------- 1 | # 第三讲 初识 Cadence - Cadence 开发最佳实践 2 | 3 | 本节课上,我们将继续深入学习 Cadence 的语法,并学习 Cadence 中的一些最佳实践。 4 | 除此之外,我们也将学习的 `Flow FT` 和 `Flow NFT` 的标准协议,以及最新 `NFT Metadata` 标准。 5 | 6 | ## 接口与 Capability 访问控制的实践 7 | 8 | > 官方文档: 9 | > 10 | > - [基于capability的访问控制](https://docs.onflow.org/cadence/language/capability-based-access-control/) 11 | > - [Interface接口文档](https://docs.onflow.org/cadence/language/interfaces/) 12 | 13 | 在上节课中,我们已经了解到了 `Capability` 机制和账户的 `link` 方法,例如: 14 | 15 | ```cadence 16 | // 链接到公有空间 17 | self.account.link<&Generator>( 18 | /public/ElementGenerator, // 共有空间 19 | target: /storage/ElementGenerator // 目标路径 20 | ) 21 | ``` 22 | 23 | 当然,由于 `generator` 的方法是所有人都可以调用的,这样的写法无可厚非。 24 | 但我们再看一下我们上节课作业中实现的 `Collection` 资源类。 25 | 26 | ```cadence 27 | pub resource Collection { 28 | pub let elements: @[Element] 29 | 30 | pub fun deposit(element: @Element) { 31 | let hex = String.encodeHex(element.feature.bytes) 32 | self.elements.append(<- element) 33 | emit ElementDeposit(hex: hex) 34 | } 35 | 36 | pub fun withdraw(hex: String): @Element? { 37 | var index = 0 38 | while index < self.elements.length { 39 | let currentHex = String.encodeHex(self.elements[index].feature.bytes) 40 | if currentHex == hex { 41 | return <- self.elements.remove(at: index) 42 | } 43 | index = index + 1 44 | } 45 | 46 | return nil 47 | } 48 | 49 | pub fun getFeatures(): [MetaFeature] { 50 | var features: [MetaFeature] = [] 51 | var index = 0 52 | while index < self.elements.length { 53 | features.append( 54 | self.elements[index].feature 55 | ) 56 | index = index + 1 57 | } 58 | return features; 59 | } 60 | 61 | init() { 62 | self.elements <- [] 63 | } 64 | 65 | destroy() { 66 | destroy self.elements 67 | } 68 | } 69 | ``` 70 | 71 | 在这里我们同样使用了 `link` 的方法公开了 `Capability`。 72 | 73 | ```cadence 74 | self.account.link<&Collection>( 75 | /public/LocalEntityCollection, 76 | target: /storage/LocalEntityCollection 77 | ) 78 | ``` 79 | 80 | 但如果这样写的话,会带来一个问题:所有人都可以通过调用 `withdraw` 方法,将 `Element` 从 `Collection` 中提取出来。 81 | 所以我们就必须使用到 Capability 提供的一个功能:通过链接接口 `interface` 的方式自由选择开放哪些功能。 82 | 83 | 首先我们创建一个定义了一组字段和函数的资源接口(接口分为资源接口、结构接口和合约接口),然后修改我们的资源并实现接口。 84 | 85 | ```cadence 86 | pub resource interface Receiver { 87 | pub fun deposit(element: @Element) 88 | pub fun getFeatures(): [MetaFeature] 89 | } 90 | 91 | pub resource Collection: Receiver { 92 | // ... 93 | pub fun deposit(element: @Element) { /* ... */ } 94 | pub fun getFeatures(): [MetaFeature] { /* ... */ } 95 | // ... 96 | } 97 | ``` 98 | 99 | 完成这个之后,我们就可以通过接口的方式(注意写法上的区别)创建我们的公共 `Capability` 并安全地使用 `Receiver` 的接口功能了。 100 | 101 | ```cadence 102 | self.account.link<&{Receiver}>( 103 | /public/LocalEntityReceiver, 104 | target: /storage/LocalEntityCollection 105 | ) 106 | ``` 107 | 108 | 现在获取到 `/public/LocalEntityReceiver` 的 `Capability` 后,就只能对 `deposit()` 和 `getFeatures()` 方法进行访问。从而我们就可以确保只有该账户的所有者才能从 Collection 中提取 element 了。 109 | 110 | ## 方法中的前置和后置条件 111 | 112 | > 官方文档: 113 | > 114 | > - [接口中的前后置条件](https://docs.onflow.org/cadence/language/interfaces) 115 | > - [方法中的前后置条件](https://docs.onflow.org/cadence/language/functions/#function-preconditions-and-postconditions) 116 | > - [交易中的前后置条件](https://docs.onflow.org/cadence/language/transactions/#pre-phase) 117 | 118 | `Cadence` 在语法层面上是不支持继承的,接口是描述复合数据结构功能的唯一手段。而接口也提供了通用性的实现内容,那就是前置后和后置条件。 119 | 例如我们可以在 `Receiver` 的接口定义中设置 `feature.raw` 必须不为 `nil`。 120 | 121 | ```cadence 122 | pub resource interface Receiver { 123 | pub fun deposit(element: @Element) { 124 | pre { 125 | element.feature.raw != nil: 126 | "RawData should not be nil" 127 | } 128 | } 129 | 130 | pub fun getFeatures(): [MetaFeature] 131 | } 132 | ``` 133 | 134 | 需要注意的是,interface 是不能包含实现代码的,因此只能包含 `pre` 和 `post` 的代码块,用于实现代码执行前和执行后的 assert 判断。 135 | 代码块中格式为 `expression : "panic message"`(前者为验证为 `true` 的表达式,后者为报错信息)。 136 | 当表达式的结果为 `false` 时,代码的执行将中断并以 `panic` 的形式输出报错信息。 137 | 138 | 当然除了在接口中,普通方法中、交易中都有可以设置前置和后置条件。 139 | 140 | ```cadence 141 | // 交易脚本中的前后置条件 142 | transaction { 143 | prepare(signer: AuthAccount) { /** 准备阶段 */ } 144 | 145 | pre { /** 前置条件,语法相同 */ } 146 | 147 | execute { /** 执行阶段 */ } 148 | 149 | post { /** 后置条件,语法相同 */ } 150 | } 151 | ``` 152 | 153 | 可以通过前后置条件的实现,在执行前后对参数进行更多更详细的判断和验证。 154 | 155 | ## 可升级合约的最佳实践 156 | 157 | > 官方文档: [合约的可升级规范](https://docs.onflow.org/cadence/language/contract-updatability/) 158 | 159 | `Cadence` 合约是可以进行更新代码的,但在更新时为了确保不会导致已存储的数据和运行时产生冲突,在更新中会有一系列的检查验证。 160 | 验证会遵循一些规则,否则会更新失败。 161 | 162 | - 更新验证可以确保 163 | - 更新合约时,存储的数据不会更改其含义。 164 | - 解码和使用存储的数据不会导致运行时崩溃。 165 | - 但不能确保的事也是有的 166 | - 不能确保 `import` 合约的代码程序都能继续生效。 167 | 168 | 为了确保你的合约代码能在以后可以进行正常的更新,在初期设计时需要遵循一些规则。 169 | 更详细的说明可以查看官方文档,这里主要列举一些最常见的规范: 170 | 171 | - 合约 172 | - 可以添加新合约/合约接口 173 | - 可以删除不含Enum定义的合约/合约接口,反之不可以。 174 | - 字段(归属于合约、结构、资源、接口的任意字段) 175 | - 移除一个字段是**合法**的,现有存储数据中若存在这个字段的数据仅仅是无用而已,不会造成运行时崩溃。 176 | - 新增一个字段是**不合法**的,因为其一 `init` 函数只会运行一次无法进行再次初始化,其二是原存储数据不包含新字段,在解码时会造成运行时崩溃。 177 | - 修改权限修饰符(access modifier)是**合法**的。 178 | - 修改字段类型是**不合法**的。 179 | - 结构、资源、接口 180 | - 新增结构、资源、接口的定义是**合法**的 181 | - 为结构、资源任意更换实现的接口是**合法**的,因为存储数据只保存实际的类型和值。 182 | - 删除或重命名一个现有的定义是**不合法**的,因为已经被存储数据使用了。 183 | - 枚举类型 184 | - 添加新的枚举定义是**合法**的,而且必须加在最后,插入同样不合法。 185 | - 删改老的枚举定义是**不合法**的。 186 | - 函数方法 187 | - 任意修改都是**合法**的,因为他们没有保存在存储数据中。 188 | - Imports 导入合约 189 | - 这里需要注意,`Cadence` 只在开发时有代码检查。若被导入的合约有所更新,需要自行修改。 190 | 191 | ## 补充资料:Flow FT 和 Flow NFT标准 192 | 193 | > 官方文档和工程: 194 | > 195 | > - Flow FT [合约信息](https://docs.onflow.org/core-contracts/fungible-token/) 196 | > - Flow NFT [合约信息](https://docs.onflow.org/core-contracts/non-fungible-token/) 197 | 198 | 首先 Flow FT 和 Flow NFT 都有官方定义的接口在公链上,发行自己的 FT 或者 NFT 都必须实现相对应的接口。 199 | 然后我们主要说一下他们与其他公链的最大区别。 200 | 201 | Flow 的 Fungible Token 本质上依然是一个 Resource 资源。 202 | 203 | ```cadence 204 | // Flow FT 205 | pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance { 206 | /// The total balance of this vault 207 | pub var balance: UFix64 208 | 209 | // initialize the balance at resource creation time 210 | init(balance: UFix64) { 211 | self.balance = balance 212 | } 213 | 214 | /// withdraw 215 | /// 216 | pub fun withdraw(amount: UFix64): @FungibleToken.Vault { 217 | self.balance = self.balance - amount 218 | emit TokensWithdrawn(amount: amount, from: self.owner?.address) 219 | return <-create Vault(balance: amount) 220 | } 221 | 222 | /// deposit 223 | /// 224 | pub fun deposit(from: @FungibleToken.Vault) { 225 | let vault <- from as! @ExampleToken.Vault 226 | self.balance = self.balance + vault.balance 227 | emit TokensDeposited(amount: vault.balance, to: self.owner?.address) 228 | vault.balance = 0.0 229 | destroy vault 230 | } 231 | 232 | destroy() { 233 | ExampleToken.totalSupply = ExampleToken.totalSupply - self.balance 234 | } 235 | } 236 | ``` 237 | 238 | 因此我们会发现他需要有一个空的 Vault,才能进行充值。 239 | 240 | ```cadence 241 | // Flow FT 242 | pub fun createEmptyVault(): @Vault { 243 | return <-create Vault(balance: 0.0) 244 | } 245 | ``` 246 | 247 | 同样的,在NFT上我们也存在 Collection 的机制,需要有一个空的 Collection,才能放入该类型的NFT。 248 | 249 | ```cadence 250 | // Flow NFT 251 | // public function that anyone can call to create a new empty collection 252 | pub fun createEmptyCollection(): @NonFungibleToken.Collection { 253 | return <- create Collection() 254 | } 255 | ``` 256 | 257 | 因此我们在对 FT 以及 NFT 进行操作时,首先要保证的是在账户信息中已经存在相对应的资源对象。 258 | 259 | 此外更多细节,可以查看我们的官方文档。 260 | 261 | ## 补充资料:NFT Metadata标准的实现 262 | 263 | > 官方文档:[NFT Metadata合约](https://docs.onflow.org/core-contracts/nft-metadata/) 264 | > FLIP提案:[NFT Metadata标准](https://github.com/onflow/flow/blob/master/flips/20210916-nft-metadata.md) 265 | 266 | 在 Flow NFT 的接口合约中,已经更新了 Flow 最新的 Metadata 实现标准。 267 | 其核心理念即:通过强类型的方式更加结构化语义化,代码中获取元数据的信息。 268 | 269 | 抽象层上最为重要的是两个接口: 270 | 271 | ```cadence 272 | // A Resolver provides access to a set of metadata views. 273 | // 274 | // A struct or resource (e.g. an NFT) can implement this interface 275 | // to provide access to the views that it supports. 276 | // 277 | pub resource interface Resolver { 278 | pub fun getViews(): [Type] 279 | pub fun resolveView(_ view: Type): AnyStruct? 280 | } 281 | 282 | // A ResolverCollection is a group of view resolvers index by ID. 283 | // 284 | pub resource interface ResolverCollection { 285 | pub fun borrowViewResolver(id: UInt64): &{Resolver} 286 | pub fun getIDs(): [UInt64] 287 | } 288 | ``` 289 | 290 | 其中 `Resolver` 接口由 NFT 资源实现,而 `ResolverCollection` 接口由 Collection 资源实现。 291 | `ResolverCollection` 很好理解,是获取到某个 NFT 的 `Resolver`,重点在于 `Resolver` 要实现的目的: 292 | 293 | - `getViews` 获取到该 NFT 支持的 metadata 显示格式(或者说类型)。 294 | - `resolveView` 则需要传入指定的类型,获取这个 NFT 的实际 metadata。 295 | 296 | 这样的设计好处在于,不仅以结构化的方式返回了 metadata 的信息,同时一个 NFT 还能兼容多种 metadata 的呈现方式。 297 | 298 | 更多细节可以参考,Flow 技术大使 Caos 为此专门写的文章:[Cadence NFT 新标准 MetadataViews 介绍](https://caos.me/cadence-nft-metadataviews) 299 | 300 | ## 课后作业 301 | 302 | 习题问卷表单将通过我们的社交群组向学员推送。 303 | 304 | **注:** 本次编程题将通过 进行作业递交 305 | 306 | ### 编程题 307 | 308 | - Q: 使用标准 NFT 接口实现改造 entity 309 | 310 | 修改 `contracts/Entity.cdc` 合约,使 `Entity` 兼容并实现 Flow 标准 `NonFungibleToken` 接口。 311 | 即 `Element` 实现为标准 NFT,`Collection` 实现为标准 NFT Collection。 312 | 注:原 `withdraw` 方法可修改为 `withdrawByHex` 313 | 314 | ### 问答题 315 | 316 | Playground 中有个 Marketplace 317 | 请问这个 Marketplace 能够服务于任何种类的NFT吗?同时尝试梳理并解答一下通用型 Marketplace 的工作流程。 318 | 319 | ### 挑战题 320 | 321 | 继续改造 `Entity` : 322 | 323 | 1. 实现 `MetadataViews` 的 `Resolver` `ResolverCollection` 等相关接口。 324 | 2. 实现诸如 NFT 铸造、转移、销毁等交易脚本。 325 | -------------------------------------------------------------------------------- /lesson-3/cadence/contracts/Entity.cdc: -------------------------------------------------------------------------------- 1 | pub contract Entity { 2 | 3 | pub event GeneratorCreated() 4 | pub event ElementGenerateSuccess(hex: String) 5 | pub event ElementGenerateFailure(hex: String) 6 | pub event ElementDeposit(hex: String) 7 | pub event CollectionCreated() 8 | 9 | // 元特征 10 | pub struct MetaFeature { 11 | 12 | pub let bytes: [UInt8] 13 | pub let raw: String? 14 | 15 | init(bytes: [UInt8], raw: String?) { 16 | self.bytes = bytes 17 | self.raw = raw 18 | } 19 | } 20 | 21 | // 元要素 22 | pub resource Element { 23 | pub let feature: MetaFeature 24 | 25 | init(feature: MetaFeature) { 26 | self.feature = feature 27 | } 28 | } 29 | 30 | pub resource Collection { 31 | pub let elements: @[Element] 32 | 33 | pub fun deposit(element: @Element) { 34 | let hex = String.encodeHex(element.feature.bytes) 35 | self.elements.append(<- element) 36 | emit ElementDeposit(hex: hex) 37 | } 38 | 39 | pub fun withdraw(hex: String): @Element? { 40 | var index = 0 41 | while index < self.elements.length { 42 | let currentHex = String.encodeHex(self.elements[index].feature.bytes) 43 | if currentHex == hex { 44 | return <- self.elements.remove(at: index) 45 | } 46 | index = index + 1 47 | } 48 | 49 | return nil 50 | } 51 | 52 | pub fun getFeatures(): [MetaFeature] { 53 | var features: [MetaFeature] = [] 54 | var index = 0 55 | while index < self.elements.length { 56 | features.append( 57 | self.elements[index].feature 58 | ) 59 | index = index + 1 60 | } 61 | return features; 62 | } 63 | 64 | init() { 65 | self.elements <- [] 66 | } 67 | destroy() { 68 | destroy self.elements 69 | } 70 | } 71 | 72 | pub fun createCollection(): @Collection { 73 | emit CollectionCreated() 74 | return <- create Collection() 75 | } 76 | 77 | // 特征收集器 78 | pub resource Generator { 79 | 80 | pub let features: {String: MetaFeature} 81 | 82 | init() { 83 | self.features = {} 84 | } 85 | 86 | pub fun generate(feature: MetaFeature): @Element? { 87 | // 只收集唯一的 bytes 88 | let hex = String.encodeHex(feature.bytes) 89 | 90 | if self.features.containsKey(hex) == false { 91 | let element <- create Element(feature: feature) 92 | self.features[hex] = feature 93 | 94 | emit ElementGenerateSuccess(hex: hex) 95 | return <- element 96 | } else { 97 | emit ElementGenerateFailure(hex: hex) 98 | return nil 99 | } 100 | } 101 | } 102 | 103 | init() { 104 | // 保存到存储空间 105 | self.account.save( 106 | <- create Generator(), 107 | to: /storage/ElementGenerator 108 | ) 109 | emit GeneratorCreated() 110 | 111 | // 链接到公有空间 112 | self.account.link<&Generator>( 113 | /public/ElementGenerator, // 共有空间 114 | target: /storage/ElementGenerator // 目标路径 115 | ) 116 | 117 | // collection setup 118 | self.account.save( 119 | <- self.createCollection(), 120 | to: /storage/LocalEntityCollection 121 | ) 122 | self.account.link<&Collection>( 123 | /public/LocalEntityCollection, 124 | target: /storage/LocalEntityCollection 125 | ) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lesson-3/cadence/contracts/standard/MetadataViews.cdc: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | This contract implements the metadata standard proposed 4 | in FLIP-0636. 5 | 6 | Ref: https://github.com/onflow/flow/blob/master/flips/20210916-nft-metadata.md 7 | 8 | Structs and resources can implement one or more 9 | metadata types, called views. Each view type represents 10 | a different kind of metadata, such as a creator biography 11 | or a JPEG image file. 12 | */ 13 | 14 | pub contract MetadataViews { 15 | 16 | // A Resolver provides access to a set of metadata views. 17 | // 18 | // A struct or resource (e.g. an NFT) can implement this interface 19 | // to provide access to the views that it supports. 20 | // 21 | pub resource interface Resolver { 22 | pub fun getViews(): [Type] 23 | pub fun resolveView(_ view: Type): AnyStruct? 24 | } 25 | 26 | // A ResolverCollection is a group of view resolvers index by ID. 27 | // 28 | pub resource interface ResolverCollection { 29 | pub fun borrowViewResolver(id: UInt64): &{Resolver} 30 | pub fun getIDs(): [UInt64] 31 | } 32 | 33 | // Display is a basic view that includes the name, description and 34 | // thumbnail for an object. Most objects should implement this view. 35 | // 36 | pub struct Display { 37 | 38 | // The name of the object. 39 | // 40 | // This field will be displayed in lists and therefore should 41 | // be short an concise. 42 | // 43 | pub let name: String 44 | 45 | // A written description of the object. 46 | // 47 | // This field will be displayed in a detailed view of the object, 48 | // so can be more verbose (e.g. a paragraph instead of a single line). 49 | // 50 | pub let description: String 51 | 52 | // A small thumbnail representation of the object. 53 | // 54 | // This field should be a web-friendly file (i.e JPEG, PNG) 55 | // that can be displayed in lists, link previews, etc. 56 | // 57 | pub let thumbnail: AnyStruct{File} 58 | 59 | init( 60 | name: String, 61 | description: String, 62 | thumbnail: AnyStruct{File} 63 | ) { 64 | self.name = name 65 | self.description = description 66 | self.thumbnail = thumbnail 67 | } 68 | } 69 | 70 | // File is a generic interface that represents a file stored on or off chain. 71 | // 72 | // Files can be used to references images, videos and other media. 73 | // 74 | pub struct interface File { 75 | pub fun uri(): String 76 | } 77 | 78 | // HTTPFile is a file that is accessible at an HTTP (or HTTPS) URL. 79 | // 80 | pub struct HTTPFile: File { 81 | pub let url: String 82 | 83 | init(url: String) { 84 | self.url = url 85 | } 86 | 87 | pub fun uri(): String { 88 | return self.url 89 | } 90 | } 91 | 92 | // IPFSThumbnail returns a thumbnail image for an object 93 | // stored as an image file in IPFS. 94 | // 95 | // IPFS images are referenced by their content identifier (CID) 96 | // rather than a direct URI. A client application can use this CID 97 | // to find and load the image via an IPFS gateway. 98 | // 99 | pub struct IPFSFile: File { 100 | 101 | // CID is the content identifier for this IPFS file. 102 | // 103 | // Ref: https://docs.ipfs.io/concepts/content-addressing/ 104 | // 105 | pub let cid: String 106 | 107 | // Path is an optional path to the file resource in an IPFS directory. 108 | // 109 | // This field is only needed if the file is inside a directory. 110 | // 111 | // Ref: https://docs.ipfs.io/concepts/file-systems/ 112 | // 113 | pub let path: String? 114 | 115 | init(cid: String, path: String?) { 116 | self.cid = cid 117 | self.path = path 118 | } 119 | 120 | // This function returns the IPFS native URL for this file. 121 | // 122 | // Ref: https://docs.ipfs.io/how-to/address-ipfs-on-web/#native-urls 123 | // 124 | pub fun uri(): String { 125 | if let path = self.path { 126 | return "ipfs://".concat(self.cid).concat("/").concat(path) 127 | } 128 | 129 | return "ipfs://".concat(self.cid) 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /lesson-3/cadence/contracts/standard/NonFungibleToken.cdc: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | ## The Flow Non-Fungible Token standard 4 | 5 | ## `NonFungibleToken` contract interface 6 | 7 | The interface that all non-fungible token contracts could conform to. 8 | If a user wants to deploy a new nft contract, their contract would need 9 | to implement the NonFungibleToken interface. 10 | 11 | Their contract would have to follow all the rules and naming 12 | that the interface specifies. 13 | 14 | ## `NFT` resource 15 | 16 | The core resource type that represents an NFT in the smart contract. 17 | 18 | ## `Collection` Resource 19 | 20 | The resource that stores a user's NFT collection. 21 | It includes a few functions to allow the owner to easily 22 | move tokens in and out of the collection. 23 | 24 | ## `Provider` and `Receiver` resource interfaces 25 | 26 | These interfaces declare functions with some pre and post conditions 27 | that require the Collection to follow certain naming and behavior standards. 28 | 29 | They are separate because it gives the user the ability to share a reference 30 | to their Collection that only exposes the fields and functions in one or more 31 | of the interfaces. It also gives users the ability to make custom resources 32 | that implement these interfaces to do various things with the tokens. 33 | 34 | By using resources and interfaces, users of NFT smart contracts can send 35 | and receive tokens peer-to-peer, without having to interact with a central ledger 36 | smart contract. 37 | 38 | To send an NFT to another user, a user would simply withdraw the NFT 39 | from their Collection, then call the deposit function on another user's 40 | Collection to complete the transfer. 41 | 42 | */ 43 | 44 | // The main NFT contract interface. Other NFT contracts will 45 | // import and implement this interface 46 | // 47 | pub contract interface NonFungibleToken { 48 | 49 | // The total number of tokens of this type in existence 50 | pub var totalSupply: UInt64 51 | 52 | // Event that emitted when the NFT contract is initialized 53 | // 54 | pub event ContractInitialized() 55 | 56 | // Event that is emitted when a token is withdrawn, 57 | // indicating the owner of the collection that it was withdrawn from. 58 | // 59 | // If the collection is not in an account's storage, `from` will be `nil`. 60 | // 61 | pub event Withdraw(id: UInt64, from: Address?) 62 | 63 | // Event that emitted when a token is deposited to a collection. 64 | // 65 | // It indicates the owner of the collection that it was deposited to. 66 | // 67 | pub event Deposit(id: UInt64, to: Address?) 68 | 69 | // Interface that the NFTs have to conform to 70 | // 71 | pub resource interface INFT { 72 | // The unique ID that each NFT has 73 | pub let id: UInt64 74 | } 75 | 76 | // Requirement that all conforming NFT smart contracts have 77 | // to define a resource called NFT that conforms to INFT 78 | pub resource NFT: INFT { 79 | pub let id: UInt64 80 | } 81 | 82 | // Interface to mediate withdraws from the Collection 83 | // 84 | pub resource interface Provider { 85 | // withdraw removes an NFT from the collection and moves it to the caller 86 | pub fun withdraw(withdrawID: UInt64): @NFT { 87 | post { 88 | result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" 89 | } 90 | } 91 | } 92 | 93 | // Interface to mediate deposits to the Collection 94 | // 95 | pub resource interface Receiver { 96 | 97 | // deposit takes an NFT as an argument and adds it to the Collection 98 | // 99 | pub fun deposit(token: @NFT) 100 | } 101 | 102 | // Interface that an account would commonly 103 | // publish for their collection 104 | pub resource interface CollectionPublic { 105 | pub fun deposit(token: @NFT) 106 | pub fun getIDs(): [UInt64] 107 | pub fun borrowNFT(id: UInt64): &NFT 108 | } 109 | 110 | // Requirement for the the concrete resource type 111 | // to be declared in the implementing contract 112 | // 113 | pub resource Collection: Provider, Receiver, CollectionPublic { 114 | 115 | // Dictionary to hold the NFTs in the Collection 116 | pub var ownedNFTs: @{UInt64: NFT} 117 | 118 | // withdraw removes an NFT from the collection and moves it to the caller 119 | pub fun withdraw(withdrawID: UInt64): @NFT 120 | 121 | // deposit takes a NFT and adds it to the collections dictionary 122 | // and adds the ID to the id array 123 | pub fun deposit(token: @NFT) 124 | 125 | // getIDs returns an array of the IDs that are in the collection 126 | pub fun getIDs(): [UInt64] 127 | 128 | // Returns a borrowed reference to an NFT in the collection 129 | // so that the caller can read data and call methods from it 130 | pub fun borrowNFT(id: UInt64): &NFT { 131 | pre { 132 | self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" 133 | } 134 | } 135 | } 136 | 137 | // createEmptyCollection creates an empty Collection 138 | // and returns it to the caller so that they can own NFTs 139 | pub fun createEmptyCollection(): @Collection { 140 | post { 141 | result.getIDs().length == 0: "The created collection must be empty!" 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /lesson-3/cadence/transactions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlowFans-Learning/flow-dapp-courses-2022/260358e3f28d2a8b9b42b5eceae6a286e6d0f8b6/lesson-3/cadence/transactions/.gitkeep -------------------------------------------------------------------------------- /lesson-4/README.md: -------------------------------------------------------------------------------- 1 | # 第四讲 实战:上手FCL(Flow Client Library) 2 | 3 | 从这节课开始,我们已经完成了 `Cadence` 上主要内容的学习,现在我们需要通过实践案例来深入学习 Flow Client Library(FCL)了。 4 | 5 | 请放心,课程中不需要懂太多 JavaScript 的知识,客户端代码脚手架已经初始化完成。 6 | 在课程的学习时我们只需要关心 `Cadence`, `FCL`, `Flow 测试网` 这几个最主要的知识点。 7 | 8 | ## Flow Client Library - FCL 9 | 10 | > FCL 官方文档: 11 | 12 | 上节课中,我们学习了交易的发送,也知道需要使用私钥进行签名,作为开发者我们已经对这套流程非常熟悉了。 13 | 我们开发过程中使用 `flow.json` 文件作为我们的钥匙串,保存着私钥并关联的账户,同时也能够使用 `flow-cli` 进行一系列操作。 14 | 15 | 但当其他普通用户使用你的 DApp 时,你不可能获得到别人的私钥,这时候我们就需要通过用户钱包的方式来进行交易的签名。 16 | 我们将构建好的交易发送到用户钱包,它们确认交易的内容并由用户选择确认或者拒绝这笔交易。 17 | 18 | 为了让钱包供应商与应用供应商在Flow生态中能更方便得整合对接,FCL(Flow Client Library)应运而生。 19 | 20 | 可以参考这篇官方博客文章 [深入Flow: FCL 简洁的力量](https://www.onflow.org/post/inside-flow-the-power-of-simplicity-with-fcl),这里对 FCL 做了一个非常不错的示例介绍。 21 | 22 | ## 测试网和部署 23 | 24 | 我们之前是通过模拟器进行本地实例的交互,今天我们打算使用测试网进行操作。 25 | 部署的过程和部署到模拟器完全相同,唯一需要注意的是,我们不能在没有帐户的情况下创建帐户,因此我们需要使用一下水龙头。 26 | 27 | ### 测试网水龙头 28 | 29 | > 水龙头:[Flow faucet](https://testnet-faucet.onflow.org/) 30 | 31 | 我们可以在测试网上使用水龙头来创建一个账户。因为我们在上节课中已经学会了使用 `flow keys generate` 创建密钥对,我们可以获取一个全新的密钥对,并前往水龙头创建一个账户,这个过程大概需要几分钟时间。 32 | 33 | 然后在 `flow.json` 中使用我们刚得到的所有相关信息更新它。 34 | 35 | ```json 36 | { 37 | "contracts": { 38 | "Contract": "./contract.cdc" 39 | }, 40 | "accounts": { 41 | "testnet-depolyer": { 42 | "address": "abcd...0123", 43 | "key": { 44 | "type": "hex", 45 | "index": 0, 46 | "signatureAlgorithm": "ECDSA_secp256k1", 47 | "hashAlgorithm": "SHA3_256", 48 | "privateKey": "$PRIVATE_KEY" 49 | } 50 | } 51 | }, 52 | "deployments": { 53 | "testnet": { 54 | "testnet-depolyer": [ 55 | "Contract" 56 | ] 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | ### 测试网合约部署 63 | 64 | > Flow 账户查询工具:[flow-view-source.com](https://flow-view-source.com/testnet/account/0xda65073324040264) ,需要手动修改地址。 65 | 66 | 好了,现在开始将合约 `Entity` 部署到测试网。 67 | 68 | ```sh 69 | flow project deploy --network=testnet 70 | ``` 71 | 72 | 部署后,我们可以通过 `flow-view-source.com` 对该账户的进行查询,可以在浏览器中直接看到已经完成部署的合约。 73 | 74 | 当然,在使用 FCL 之前我们也可以通过 `flow-cli` 对我们部署的合约进行测试。 75 | 76 | ## 实战课程:CryptoDappy 77 | 78 | > CryptoDappy 原版教程参考: 79 | > 80 | > - [Onboarding上手简介](https://www.cryptodappy.com/missions/mission-0) 81 | > - [Mission#1 任务1](https://www.cryptodappy.com/missions/mission-1) 82 | > - [Mission#2 任务2](https://www.cryptodappy.com/missions/mission-2) 83 | 84 | 本节课开始将主要以实战视频为主。 85 | 将陆续播出第二期 Flow 技术大使制作 CryptoDappy 课程中文版。 86 | 87 | > **视频内使用到的代码模板,可以从作业链接中获取到** 88 | 89 | 本期实战课程为: 90 | 91 | 1. CryptoDappy 实战 1 - FCL 入门介绍与授权认证 by Caos 92 | 2. CryptoDappy 实战 2 - 通过 FCL 使用 Script 查询链上数据 by Lsy 93 | 94 | ### 补充说明 95 | 96 | 这里引入了一个 JS 中对[模板字符串](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals#%E5%B8%A6%E6%A0%87%E7%AD%BE%E7%9A%84%E6%A8%A1%E6%9D%BF%E5%AD%97%E7%AC%A6%E4%B8%B2)的特殊用法:**用函数解析模板字符串**。 97 | 函数名后直接跟着模版字符串,即代表通过函数对模版字符串以 `function(strings, ...keys)` 的方式进行解析。 98 | 99 | 在 FCL 中 `fcl.script` 和 `fcl.transaction` 等,即为这样的使用方式。 100 | 101 | ## 课后作业 102 | 103 | 习题问卷表单,将通过我们的社交群组向学员推送。 104 | 105 | **注:** 本次编程题将通过 进行作业递交 106 | 107 | ### 编程题 108 | 109 | 根据视频内容完成下列操作: 110 | 111 | - 添加 fcl 库 112 | - 初始化 fcl 配置 113 | - 添加 fcl 的授权登陆 114 | - 在配置中添加Dappy合约 115 | - 添加 list-dappy-templates 查询脚本 116 | - 执行该脚本的查询,并在页面上进行显示。 117 | 118 | ### 问答题 119 | 120 | 请尝试分析对比一下 fcl 的实现和 MetaMask 等插件钱包的优势和劣势。 121 | 122 | ### 挑战题 123 | 124 | 修改查询脚本,尝试使用 Scripts 实现一个分页查询的功能和与逻辑。 125 | 126 | 注:该题可以在作业工程中补充实现。 127 | -------------------------------------------------------------------------------- /lesson-5/README.md: -------------------------------------------------------------------------------- 1 | # 第五讲 实战:使用FCL进行链上交互 2 | 3 | 这节课我们讲继续通过实战案例来学习 Flow Client Library(FCL)。 4 | 5 | ## 回顾与补充 6 | 7 | > FCL 官方文档: 8 | 9 | 上节课我们通过 CryptoDappy 的实战课程完成了 FCL 的基础使用。 10 | 由于主要是基于测试网环境,这里补充一下模拟器环境的初始化方法,当然具体依然可以在官方文档中找到。 11 | 12 | ### 如何用 FCL 完成模拟器环境的登录授权 13 | 14 | > 【参考】模拟器开发钱包: 15 | 16 | 如果使用模拟器环境的话,配置部分需要修改为: 17 | 18 | ```ts 19 | import * as fcl from "@onflow/fcl" 20 | 21 | fcl.config() 22 | .put("accessNode.api", "http://localhost:8080") 23 | .put("discovery.wallet", "http://localhost:8701/fcl/authn") 24 | ``` 25 | 26 | 关于模拟器环境开发钱包的使用,可以参考 `fcl-dev-wallet` 的相关文档。 27 | 28 | ## 实战课程:CryptoDappy 29 | 30 | > CryptoDappy 原版教程参考: 31 | > 32 | > - [Mission#3 任务3](https://www.cryptodappy.com/missions/mission-3) 33 | > - [Mission#4 任务4](https://www.cryptodappy.com/missions/mission-4) 34 | 35 | 本节课继续播出由第二期 Flow 技术大使制作 CryptoDappy 课程中文版。 36 | 37 | > **视频内使用到的代码模板,可以从作业链接中获取到** 38 | 39 | 本期实战课程为: 40 | 41 | 1. CryptoDappy 实战 3 - 使用 Transactions 与智能合约进行交互 by WhiteMatrix 42 | 2. CryptoDappy 实战 4 - 将 FUSD 运用到 Dappy 的支付当中 by Fou 43 | 44 | ### 如何获取测试网上的测试币 45 | 46 | 视频中有介绍如何获取测试币,这里放上需要的链接地址: 47 | 48 | **获取 $FLOW** - 49 | **获取 $FUSD** - 50 | 51 | ## 课后作业 52 | 53 | 习题问卷表单将通过我们的社交群组向学员推送。 54 | 55 | **注:** 本次编程题将通过 进行作业递交 56 | 57 | ### 问答题 58 | 59 | 请描述 fcl 与钱包服务之间的通信与签名授权的流程和步骤。 60 | 61 | ### 编程题 62 | 63 | 根据视频内容完成下列操作: 64 | 65 | - 添加 create-collection 的交易代码 66 | - 添加 delete-colleciton 的交易代码 67 | - 添加 check-collection 的查询脚本 68 | - 修改 hooks/use-collection.hook.js 实现collection的基础操作 69 | - 在配置中添加 FUSD 和 FungibleToken 合约 70 | - 添加 create-fusd-vault 的交易代码 71 | - 添加 get-fusd-balance 的查询脚本 72 | - 修改 hooks/use-fusd.hook.js 实现fusd 73 | - 修改 providers/UserProvider.js 完成所有操作 74 | 75 | ### 挑战题 76 | 77 | 在截止日期前完成本节课的编程题即可。 78 | -------------------------------------------------------------------------------- /lesson-6/README.md: -------------------------------------------------------------------------------- 1 | # 第六讲 实战:NFT的铸造与销售 2 | 3 | 这节课我们讲继续通过实战案例来学习 Flow Client Library(FCL)。 4 | 此外我们还将学习在 Flow 生态上进行 NFT 发售的不同策略,并通过 CryptoDappy 的课程实现其中一个策略。 5 | 6 | ## NFT 发售的不同策略 7 | 8 | 当项目方计划进行 NFT 发售时,我们可以考虑通过不同的执行策略和技术解决方案进行发售。 9 | 在这里我们总结了 6 种不同的执行策略: 10 | 11 | - 预铸造,链上销售 12 | - 预铸造,链下销售 13 | - 预售 NFT 14 | - 按需铸造,链上销售 15 | - 按需铸造,链下销售 16 | - 凭证驱动,链上销售 17 | 18 | 当然我们还可以设计更多的一级市场发售的策略,比如盲盒、打包销售等玩法。 19 | 20 | ## 实战课程:CryptoDappy 21 | 22 | > CryptoDappy 原版教程参考: 23 | > 24 | > - [Mission#5 任务5](https://www.cryptodappy.com/missions/mission-5) 25 | > - [Mission#6 任务6](https://www.cryptodappy.com/missions/mission-6) 26 | 27 | 本节课继续播出由第二期 Flow 技术大使制作的 CryptoDappy 课程中文版。 28 | 29 | > **视频内使用到的代码模板,可以从作业链接中获取到** 30 | 31 | 本期实战课程为: 32 | 33 | 1. CryptoDappy 实战 5 - 如何在 Flow 链上铸造你的 Dappy by Raye 34 | 2. CryptoDappy 实战 6 - 如何实现 Dappy Packs 的功能 by Frank 35 | 36 | ## 课后作业 37 | 38 | 习题问卷表单将通过我们的社交群组向学员推送。 39 | 40 | **注:** 本次编程题将通过 进行作业递交 41 | 42 | ### 问答题 43 | 44 | 在一些应用场景中需要预铸造NFT,然后面向用户投放或发售NFT,如何实现这个需求?如何尽可能的保证安全? 45 | 46 | ### 编程题 47 | 48 | 根据视频内容完成下列操作: 49 | 50 | - 添加 mint-dappy 的交易代码 51 | - 添加 list-user-dappies 的查询脚本 52 | - 修改 hooks/use-user-dappies.hook.js 完成相关操作 53 | - 添加 get-packs 和 list-packs 的查询脚本 54 | - 添加 list-dappies-in-pack 的查询脚本 55 | - 添加 mint-dappies-from-pack 的交易代码 56 | - 修改 hooks/use-dappy-packs.hook.js 完成相关操作 57 | 58 | ### 挑战题 59 | 60 | 在截止日期前完成本节课的编程题即可。 61 | --------------------------------------------------------------------------------