使用 JS-SDK 与 FLOW 交互

本文假设读者是熟悉 JavaScript 和 React 的开发者,对 Flow 有着一定的了解,或者熟悉 Flow 智能合约语言 Cadence 相关的概念。

我们将通过本文熟悉并搭建本地开发环境,使用 JavaScript 根据现有的 JS-SDK 完成对链的调用与交互。

包含以下内容:

  • 搭建本地开发模拟环境
  • 部署开发版钱包服务
  • 使用 Dev Wallet 创建本地账户
  • 查询账户信息
  • 执行 Cadence 脚本
  • 部署 Cadence 合约并与之交互

教程内容参照了原文 flow-js-sdk quick start 内容根据最新的代码和示例略有增补。

初始化仓库和开发环境

为了方便读者理解,我们直接使用 flow-js-sdk 官方提供的代码库作为基础,并针对原有的示例略有一些调整,请参照 fork 的仓库 react-fcl-demo 来完成部署和演示

git clone https://github.com/caosbad/react-fcl-demo.git
cd react-fcl-demo
yarn

首先将远程仓库克隆到本地,然后在实例项目中安装依赖,yarn 会将 package.json 文件中的项目依赖 @onflow/fcl @onflow/sdk @onflow/six-set-code @onflow/dev-wallet 等下载。

在这之前,我们还需要初始化 Flow 的本地模拟器启动 wallet-dev 服务

安装 & 启动模拟器

模拟器是帮助我们在本机启动一个本地的 Flow 网络,类似于以太坊的 ganache,模拟器的下载安装步骤可以参考这里 instructions.

// Linux and macOS
sh -ci "$(curl -fsSL https://storage.googleapis.com/flow-cli/install.sh)"

// Windows
iex "& { $(irm 'https://storage.googleapis.com/flow-cli/install.ps1') }"
// --init 参数是在第一次启动的时候添加,如果已经初始化过,就直接执行 start 命令
flow emulator start --init

在示例项目的目录里执行 init 命令后,我们会发现目录下多出了一个 flow.json 文件,类似于以下的结构:

{
    "accounts": {
        "service": {
            "address": "f8d6e0586b0a20c7",
            "privateKey": "84f82df6790f07b281adb5bbc848bd6298a2de67f94bdfac7a400d5a1b893de5",
            "sigAlgorithm": "ECDSA_P256",
            "hashAlgorithm": "SHA3_256"
        }
    }
}

模拟器启动之后你会看到启动成功的日志,模拟器提供了 gRPC 和 http 通信的接口

 

 

接下来在新的终端启动 Dev wallet

启动 Dev wallet 服务

在 package.json 文件中,我们会看到 scripts 的配置项中有名为 dev-wallet 和 dev-wallet-win 两个脚本,现在把我们上一步模拟初始化生成的 privateKey 覆盖现有的配置。

然后执行 yarn run dev-wallet 或 yarn run dev-wallet-win

成功之后,将会看到以下的日志:

 

 

这里启动了多个服务,同时注意 Service Address 和 Private Key 与模拟器生成的一致。

环境已经配置成功,接下来就是启动示例项目:

启动示例项目

yarn start

确保模拟器和 Dev wallet 也在启动的状态,我们可以看到页面上的一些示例操作,下面我们从代码层面了解一些交互的细节

获取最新区块

// src/demo/GetLatestBlock.tsx
import { decode, send, getLatestBlock } from "@onflow/fcl"

const GetLatestBlock = () => {
  const [data, setData] = useState(null)

  const runGetLatestBlock = async (event: any) => {
    event.preventDefault()

    const response = await send([
      getLatestBlock(),
    ])

    setData(await decode(response)) // 解码返回的数据,并更新 state
  }
// 返回结果
{
  "id": "de37aabaf1ce314da4a6e2189d9584b71a7f302844b4ed5fb1ca3042afbad3d0", // 区块的 id
  "parentId": "1ae736bdea1065a98262348d5a7a2141d2b21a76ac2184b3e1181088de430255",  // 上一个区块的 id
  "height": 2,
  "timestamp": {
    "wrappers_": null,
    "arrayIndexOffset_": -1,
    "array": [
      1607256408,
      195959000
    ],
    "pivot_": 1.7976931348623157e+308,
    "convertedPrimitiveFields_": {}
  },
  "collectionGuarantees": [
    {
      "collectionId": "49e27fcf465075e6afd9009478788ba801fefa85a919d48df740e541cc514497",
      "signatures": [
        {}
      ]
    }
  ],
  "blockSeals": [],
  "signatures": [
    {}
  ]
}

查询用户信息

这里需要我们输入用户地址来完成查询,

// src/demo/GetAccount.tsx
  const runGetAccount = async (event: any) => {
    const response = await fcl.send([
      fcl.getAccount(addr),            // 通过地址获取用户信息
    ])

    setData(await fcl.decode(response))
  }
{
  "address": "01cf0e2f2f715450",      // 地址
  "balance": 0,
  "code": {},
  "keys": [
    {
      "index": 0,
      "publicKey": "7b3f982ebf0e87073831aa47543d7c2a375f99156e3d0cff8c3638bb8d3f166fd0db7c858b4b77709bf25c07815cf15d7b2b7014f3f31c2efa9b5c7fdac5064d",  // 公钥
      "signAlgo": 2,
      "hashAlgo": 3,
      "weight": 1000,
      "sequenceNumber": 1
    }
  ]
}

执行脚本

执行脚本我们可以理解为是一种无需用户授权的查询操作

// src/demo/ScriptOne.tsx

const scriptOne = `\
pub fun main(): Int {
  return 42 + 6
}
`

const runScript = async (event: any) => {
    const response = await fcl.send([
      fcl.script(scriptOne),
    ])
    setData(await fcl.decode(response)) // 48
  }

用定义的结构解析脚本运行的结果

这里我们可以看到在智能合约里可以定义复杂的数据结构, 并且通过 typescript 的类型进行数据的解构,能够将复杂的数据与前端的应用层友好的关联。

// src/model/Point.ts 这里定义了结构数据的类型
class Point {
  public x: number;
  public y: number;

  constructor (p: Point) {
    this.x = p.x
    this.y = p.y
  }
}

export default Point;

// src/demo/ScriptTwo.tsx
const scriptTwo = `
pub struct SomeStruct {
  pub var x: Int
  pub var y: Int

  init(x: Int, y: Int) {
    self.x = x
    self.y = y
  }
}

pub fun main(): [SomeStruct] {
  return [SomeStruct(x: 1, y: 2), SomeStruct(x: 3, y: 4)]
}
`;

fcl.config()
  .put("decoder.SomeStruct", (data: Point) => new Point(data)) // 这里定义了 fcl 对数据的解构方式

  const runScript = async (event: any) => {
    event.preventDefault()

    const response = await fcl.send([   // 脚本的执行可以认为是一种读操作,不需要用户授权
      fcl.script(scriptTwo),
    ])

    setData(await fcl.decode(response))
  }

// class 中的 public 和 脚本中的 pub 替换

这里需要注意几点:

  • config 中 decoder.SomeStruct 名称要与脚本中的 SomeStruct 类型名称对应
  • 回调函数中的 data 要指定对应的类型,也就是负责解构的 Point 类型
  • 解构的类型,需要有自己的 constructor 函数
// 输出结果
Point 0
{
  "x": 1,
  "y": 2
}
--
Point 1
{
  "x": 3,
  "y": 4
}
--

登入(创建账户)登出

确保我们本地运行了 Dev wallet 服务

在 demo 的页面点击 Sign In/Up Dev wallet 将会弹出授权页面:

 

 

 

接着点击授权,会进入到更新 profile 的界面

 

 

 

保存并应用之后,Dev wallet 会将 profile 的信息存入数据库中,订阅函数将会执行回调,将 user 的信息作为参数传递回来

// src/demo/Authenticate.tsx
const signInOrOut = async (event: any) => {
    event.preventDefault()

    if (loggedIn) {
      fcl.unauthenticate() // logout
    } else {
      fcl.authenticate() // sign in or sign up ,这里会呼出 Dev wallet 的窗口
    }
  }

// line:38
fcl.currentUser().subscribe((user: any) => setUser({...user})) // fcl.currentUser() 这里提供了监听方法,并动态获取 User 数据

对应用开发者来说,fcl 帮助我们管理用户的登录状态和所需要的授权操作,会在下文发送交易的章节详述。

// user 返回值
{
  "VERSION": "0.2.0",
  "addr": "179b6b1cb6755e31",  // 用户的地址
  "cid": "did:fcl:179b6b1cb6755e31",
  "loggedIn": true,            // 登录状态
  "services": [                // 服务数据
    {
      "type": "authz",
      "keyId": 0,
      "id": "asdf8701#authz-http-post",
      "addr": "179b6b1cb6755e31",
      "method": "HTTP/POST",
      "endpoint": "http://localhost:8701/flow/authorize",
      "params": {
        "userId": "37b92714-2713-41b0-9749-fc08b3fdd827"
      }
    },
    {
      "type": "authn",
      "id": "wallet-provider#authn",
      "pid": "37b92714-2713-41b0-9749-fc08b3fdd827",
      "addr": "asdf8701",
      "name": "FCL Dev Wallet",
      "icon": "https://avatars.onflow/avatar/asdf8701.svg",
      "authn": "http://localhost:8701/flow/authenticate"
    }
  ]
}

Dev wallet 会将 profile 的数据存储到 GraphQL 的数据服务中,供第二次登陆时由 Dev wallet 调用展示

发送交易

 // src/demo/SendTransaction.tsx

const simpleTransaction = `
transaction {
  execute {
    log("A transaction happened")
  }
}
`
const { transactionId } = await fcl.send([
   fcl.transaction(simpleTransaction),
   fcl.proposer(fcl.currentUser().authorization), // 交易触发者
   fcl.payer(fcl.currentUser().authorization),    // 费用支付者
  ])

    setStatus("Transaction sent, waiting for confirmation")

 const unsub = fcl
    .tx({ transactionId })
    .subscribe((transaction: any) => {
       setTransaction(transaction)  // 更新 state

       if (fcl.tx.isSealed(transaction)) {
          setStatus("Transaction is Sealed")
          unsub()
          }
        })

与执行脚本不同的是,这里的 transaction 调用需要发起交易,相当于执行链上的交易操作,虽然这里只进执行了 log ,但仍需要指定交易发起者和费用支付者。

// 脚本运行成功的返回值
{
  "status": 4,
  "statusCode": 0,
  "errorMessage": "",
  "events": []     // 触发的事件列表
}

由于这里 Cadence 脚本只单纯的只执行了 log ,events 里没有数据返回

部署合约

这里我们定义一个示例的合约,并声明一个公开可调用的函数来通过外部传入的参数触发合约的事件,这里并未对合约的变量进行改变

// src/demo/DeployContract.tsx
// 需要部署的合约脚本,这里为了测试方便我添加了 access(all) 的访问权限声明
const simpleContract = `
access(all) contract HelloWorld {
  pub let greeting: String
  pub event HelloEvent(message: String)

  init() {
    self.greeting = "Hello, World!"
  }

  pub fun hello(message: String): String {
    emit HelloEvent(message: message)
    return self.greeting
  }
}
`

// 为账户部署合约的脚本
const deployScript = `
transaction(code: String) {
  prepare(acct: AuthAccount) {
      acct.contracts.add(name: "HelloWorld", code: code.decodeHex())
  }
}
`


const runTransaction = async (event: any) => {
    const result = await Send(simpleContract, deployScript);  // 这里的 send 是一个封装函数
    setTransaction(result);  // 更新 state
  }


// src/helper/fcl-deployer.ts
export async function Send(code: string, deployScript: string) {
    const response = await fcl.send([
        setCode({
            proposer: fcl.currentUser().authorization,
            authorization: fcl.currentUser().authorization,
            payer: fcl.currentUser().authorization,
            code: code,
            deployScript: deployScript
        })
    ])

    try {
      return await fcl.tx(response).onceExecuted()  // 返回执行结果
    } catch (error) {
      return error;
    }
}
  • Send 函数封装了当前用户的签名信息
  • 部署合约的脚本和合约自身都是 Cadence 脚本
  • fcl.tx(res).onceExecuted 可以用作交易执行的监听函数
  • acct.contracts.add(name: "HelloWorld", code: code.decodeHex()) 其中 add 函数的 name 参数需要与合约脚本中声明的名称一致
  • 同样名字的合约在一个账户下只能有一个
// 部署合约返回值
{
  "status": 4,
  "statusCode": 0,
  "errorMessage": "",
  "events": [
    {
      "type": "flow.AccountContractAdded",    // 类型
      "transactionId": "8ba62635f73f7f5d3e1a73d5fd860ea7369662109556e510b4af904761944e2a",  // trx id
      "transactionIndex": 1,
      "eventIndex": 0,
      "data": {
        "address": "0x179b6b1cb6755e31",  // 地址
        "codeHash": [...],               // 编码之后的合约 code ,此处有省略
        "contract": "HelloWorld"        // 合约名称
      }
    }
  ]
}

与合约交互

在界面上我们需要输入之前部署合约账户的地址,才能够成功的导入合约并调用其公开的函数,注意调用的交易体中(由 transaction 包裹,execute 中执行合约代码的调用),传入 massage 作为合约方法的参数

// src/demo/ScriptOne.tsx
// 这里的 addr 是我们部署合约的地址
const simpleTransaction = (address: string | null) => `\
  import HelloWorld from 0x${address}

  transaction {
    execute {
      HelloWorld.hello(message: "Hello from visitor")
    }
  }
`
  const runTransaction = async (event: any) => {
    try {
      // 通过 transactionId 获得交易监听
      const { transactionId } = await fcl.send([
        fcl.transaction(simpleTransaction(addr)),
        fcl.proposer(fcl.currentUser().authorization),
        fcl.payer(fcl.currentUser().authorization),
      ])
      // 交易的监听函数定义,返回值是取消监听的函数
      const unsub = fcl
        .tx({
          transactionId,  // 解构出交易 id
        })
        .subscribe((transaction: any) => {
          setTransaction(transaction)    // 更新 state

          if (fcl.tx.isSealed(transaction)) {
            unsub()    // 取消监听
          }
        })
    } catch (error) {
      setStatus("Transaction failed")
    }
  }
{
  "status": 4,
  "statusCode": 0,
  "errorMessage": "",
  "events": [
    {
      "type": "A.179b6b1cb6755e31.HelloWorld.HelloEvent",     // 调用的合约的事件类型
      "transactionId": "28ec7c9c0eecb4408dfc3b7b23720a6038a8379721eb7b532747cfc016a3b1cc",
      "transactionIndex": 1,
      "eventIndex": 0,
      "data": {                                         // 数据
        "message": "Hello from visitor"                 // 事件监听的参数
      }
    }
  ]
}
  • unsub = fcl.tx({transactionId}).subscribe(func) 是交易结果监听的方式,等价于 fcl.tx(response).onceExecuted()
  • 事件的 type 以 A. 用户地址 . 合约名称 . 事件名称 规则来命名
  • fcl 已经将获取到的数据进行了解码操作,可以直接看到返回的结果

最后合约的监听函数可以获得合约触发的事件,并将其通过回调函数返回给我们。
 

最后

现在我们已经熟悉了如何通过 fcl 与 flow 链的交互,我们已经具备了在 Flow 链上开发 DApp 的最小知识,接下来可以继续根据现有的 demo 做一些测试,或者深入探索有关 Dev wallet 、flow-sdk 或 Cadence 相关的代码与服务,相信会有更多的收获。

 关注Flow 

我们欢迎越来越多的小伙伴加入Flow星球,为星球增添色彩!

 

Flow 官网:https://zh.onflow.org/

Flow 论坛: https://forum.onflow.org/

Flow Discord:

https://discord.com/invite/flow

Flow CN Telegram: https://t.me/flow_zh

Flow B站:https://space.bilibili.com/1002168058

Flow 微博: 

https://weibo.com/7610419699

Flow CSDN:

https://blog.csdn.net/weixin_57551966?spm=1010.2135.3001.5343

 

扫码添加Flow官方账号微信号,加入Flow生态群

微信号 : FlowChainOfficial

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值