JS `Substrate` / `Polkadot` `Wasm Smart Contracts` 的前端交互

各位观众,欢迎来到今天的 "Substrate/Polkadot Wasm Smart Contracts 前端交互" 讲座!今天咱们不整那些虚头巴脑的,直接上干货,让大家伙儿能听懂、能上手、能回家就能开撸代码。

开场白:为啥要搞前端交互?

咱们先聊聊为啥要搞前端交互。想象一下,你辛辛苦苦用 Rust 写了个牛逼哄哄的 Wasm 合约,能发 token、能搞 NFT、甚至能玩 DeFi,但是呢,用户只能通过命令行或者 Polkadot JS Apps 这种工具才能用你的合约,是不是感觉有点…憋屈?

这就好比你做了个香气扑鼻的红烧肉,但是别人只能用筷子尖儿戳一下闻闻味儿,不能大快朵颐,是不是很可惜?

所以,前端交互就是要把你这个“红烧肉”端到用户面前,让他们能用鼠标点点、手指划划,就能轻松调用你的合约,体验你的 DApp 的魅力。

第一部分:准备工作,磨刀不误砍柴工

在开始撸代码之前,咱们得先准备好家伙什儿。这就像做饭之前要先洗菜、切菜一样,不能省略。

  1. 环境搭建:

    • Node.js 和 npm/yarn: 这是前端开发的基石,没有它啥也玩不转。确保你的电脑上安装了 Node.js 和 npm (或者 yarn)。

    • Polkadot JS API: 这是咱们跟 Substrate/Polkadot 链交互的桥梁。用 npm 或者 yarn 安装:

      npm install @polkadot/api @polkadot/extension-dapp
      # 或者
      yarn add @polkadot/api @polkadot/extension-dapp
    • 一个 Wasm 合约: 这个嘛… 你得自己写一个,或者找个现成的。 咱们假设你已经有一个编译好的 .wasm 文件 和 .contract 文件 (包含了 ABI)。

  2. Polkadot{.js} Extension (插件):

    这个插件是用户授权你的 DApp 访问他们的 Polkadot 账户的关键。 确保用户安装并配置好这个插件。

第二部分:核心代码,手把手教你撸

好了,家伙什儿都齐了,咱们开始撸代码! 这里我们使用 React 作为一个例子,因为现在 React 用的人多,比较主流。

import React, { useState, useEffect } from 'react';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { Keyring } from '@polkadot/keyring';
import { ContractPromise } from '@polkadot/api-contract';
import { web3Accounts, web3Enable, web3FromSource } from '@polkadot/extension-dapp';

const CONTRACT_ADDRESS = '你的合约地址'; // 替换成你的合约地址
const CONTRACT_ABI = require('./你的合约ABI.json'); // 替换成你的合约ABI文件

function App() {
  const [api, setApi] = useState(null);
  const [accounts, setAccounts] = useState([]);
  const [contract, setContract] = useState(null);
  const [greeting, setGreeting] = useState('');
  const [status, setStatus] = useState('');
  const [currentAccount, setCurrentAccount] = useState(null);

  useEffect(() => {
    const init = async () => {
      setStatus('Connecting to the blockchain...');

      // 连接到 Substrate/Polkadot 节点
      const wsProvider = new WsProvider('ws://127.0.0.1:9944'); // 替换成你的节点地址
      const api = await ApiPromise.create({ provider: wsProvider });
      setApi(api);

      // 获取用户账户
      const extensions = await web3Enable('你的 DApp 名称'); // 替换成你的 DApp 名称
      if (extensions.length === 0) {
        setStatus('No extension installed, or you did not allow access');
        return;
      }
      const accounts = await web3Accounts();
      setAccounts(accounts);

      if (accounts.length > 0) {
        setCurrentAccount(accounts[0].address);
      }

      // 创建合约实例
      const contract = new ContractPromise(api, CONTRACT_ABI, CONTRACT_ADDRESS);
      setContract(contract);

      setStatus('Ready');
    };

    init();
  }, []);

  const getGreeting = async () => {
    if (!contract || !currentAccount) {
      setStatus('Contract not initialized or no account selected.');
      return;
    }

    setStatus('Calling get_greeting...');

    try {
      const { output } = await contract.query.getGreeting(currentAccount, { gasLimit: 10000000000 }, currentAccount); // 替换成你的合约方法名
      setGreeting(output.toString());
      setStatus('');
    } catch (error) {
      console.error('Error calling get_greeting:', error);
      setStatus(`Error: ${error.message}`);
    }
  };

  const setGreetingHandler = async (newGreeting) => {
      if (!contract || !currentAccount) {
          setStatus('Contract not initialized or no account selected.');
          return;
      }

      setStatus('Setting new greeting...');

      try {
          const injector = await web3FromSource(accounts.find(a => a.address === currentAccount).meta.source);
          const gasLimit = 10000000000;
          const tx = contract.tx.setGreeting( { gasLimit, value: 0 }, newGreeting); // 替换成你的合约方法名

          tx.signAndSend(currentAccount, { signer: injector.signer }, ({ status }) => {
              if (status.isInBlock) {
                  setStatus(`Completed at block hash #${status.asInBlock.toString()}`);
              } else if (status.isFinalized) {
                  setStatus(`Finalized block hash #${status.asFinalized.toString()}`);
              } else {
                  setStatus(`Current Status: ${status.type}`);
              }
          }).catch(error => {
              console.error('Error setting greeting:', error);
              setStatus(`Error: ${error.message}`);
          });
      } catch (error) {
          console.error('Error setting greeting:', error);
          setStatus(`Error: ${error.message}`);
      }
  };

  const handleAccountChange = (event) => {
    setCurrentAccount(event.target.value);
  };

  return (
    <div>
      <h1>Substrate/Polkadot Wasm Contract Demo</h1>
      <p>Status: {status}</p>

      {accounts.length > 0 ? (
        <div>
          <label>Select Account:</label>
          <select value={currentAccount} onChange={handleAccountChange}>
            {accounts.map((account) => (
              <option key={account.address} value={account.address}>
                {account.meta.name} ({account.address})
              </option>
            ))}
          </select>
        </div>
      ) : (
        <p>No accounts found. Please install and configure the Polkadot{.js} extension.</p>
      )}

      <button onClick={getGreeting} disabled={!contract || !currentAccount}>Get Greeting</button>
      <p>Greeting: {greeting}</p>

      <button onClick={() => {
          const newGreeting = prompt('Enter new greeting:');
          if (newGreeting) {
              setGreetingHandler(newGreeting);
          }
      }} disabled={!contract || !currentAccount}>Set Greeting</button>

    </div>
  );
}

export default App;

代码解释:

  1. 引入依赖: 引入必要的库,包括 @polkadot/api, @polkadot/extension-dapp, 和 @polkadot/api-contract
  2. 状态管理: 使用 useState 管理组件的状态,包括 API 连接、账户信息、合约实例、greeting 信息和状态信息。
  3. useEffect 钩子: 在组件加载时初始化 API 连接、获取账户信息、创建合约实例。
  4. 连接到 Substrate 节点: 使用 ApiPromise.create 连接到指定的 Substrate 节点。 记住替换 ws://127.0.0.1:9944 为你自己的节点地址。
  5. 获取用户账户: 使用 web3Enable 启用 Polkadot{.js} 扩展,然后使用 web3Accounts 获取用户账户。 替换 '你的 DApp 名称' 为你自己的 DApp 名称。
  6. 创建合约实例: 使用 ContractPromise 创建合约实例,需要传入 API 对象、合约 ABI 和合约地址。 记住替换 你的合约地址你的合约ABI.json 为你自己的合约地址和 ABI 文件。
  7. 调用合约方法 (query): 使用 contract.query.getGreeting 调用合约的 get_greeting 方法。 这是一个只读方法,不需要签名。
  8. 调用合约方法 (tx): 使用 contract.tx.setGreeting 调用合约的 set_greeting 方法。 这是一个需要签名的方法。
    • 首先,你需要使用 web3FromSource 获取一个 injector 对象,用于签名交易。
    • 然后,你需要使用 tx.signAndSend 签名并发送交易。
    • signAndSend 接收两个参数: 账户地址 和 一个包含 signer 的对象。
    • signAndSend 还会返回一个 unsubscribe 函数,用于取消订阅交易状态。
  9. UI 渲染: 使用 JSX 渲染 UI,包括状态显示、账户选择、调用合约方法的按钮和 greeting 信息。

第三部分:部署和运行,让你的 DApp 飞起来

  1. 编译前端代码: 使用你喜欢的构建工具 (比如 webpack, parcel, vite) 编译前端代码。
  2. 部署前端代码: 将编译好的前端代码部署到你喜欢的服务器 (比如 Netlify, Vercel, GitHub Pages)。
  3. 配置 CORS: 确保你的 Substrate 节点配置了正确的 CORS 策略,允许你的前端应用访问。
  4. 运行 DApp: 在浏览器中打开你的 DApp,连接到你的 Substrate 节点,选择你的账户,然后就可以开始体验你的 Wasm 合约了!

第四部分:常见问题和注意事项,避坑指南

  • CORS 问题: 这是前端开发中最常见的问题之一。 确保你的 Substrate 节点配置了正确的 CORS 策略,允许你的前端应用访问。
  • Gas Limit: 调用合约方法时,需要设置合适的 Gas Limit。 如果 Gas Limit 设置得太小,交易会失败。 如果 Gas Limit 设置得太大,你会浪费 Gas。
  • 错误处理: 在调用合约方法时,一定要进行错误处理。 如果交易失败,你需要向用户显示错误信息。
  • 账户权限: 用户需要授权你的 DApp 访问他们的 Polkadot 账户。 如果用户没有授权,你的 DApp 将无法调用合约方法。
  • ABI 文件: ABI 文件描述了合约的接口。 确保你使用的是正确的 ABI 文件。

第五部分:进阶技巧,更上一层楼

  • 使用 Redux 或者 Zustand 管理状态: 如果你的 DApp 比较复杂,可以使用 Redux 或者 Zustand 等状态管理库来管理状态。
  • 使用 Typescript: 使用 Typescript 可以提高代码的可读性和可维护性。
  • 使用 GraphQL: 使用 GraphQL 可以更高效地查询链上的数据。
  • 使用 Substrate API Sidecar: Substrate API Sidecar 是一个 REST API 服务,可以简化与 Substrate 链的交互。

总结:

今天我们一起学习了如何使用前端技术与 Substrate/Polkadot Wasm 智能合约进行交互。 希望大家能够掌握这些知识,开发出更多有趣、有用的 DApp。 记住,实践是检验真理的唯一标准。 赶紧动手撸代码吧!

代码表格:

代码片段 描述
npm install @polkadot/api @polkadot/extension-dapp 安装 Polkadot JS API 和 Polkadot{.js} 扩展 DApp 接口。
const wsProvider = new WsProvider('ws://127.0.0.1:9944'); 创建一个 WebSocket Provider 连接到 Substrate 节点。
const api = await ApiPromise.create({ provider: wsProvider }); 使用 WebSocket Provider 创建一个 ApiPromise 实例,用于与 Substrate 链交互。
const extensions = await web3Enable('你的 DApp 名称'); 启用 Polkadot{.js} 扩展,并请求用户授权你的 DApp 访问他们的账户。
const accounts = await web3Accounts(); 获取用户授权的 Polkadot 账户列表。
const contract = new ContractPromise(api, CONTRACT_ABI, CONTRACT_ADDRESS); 创建一个 ContractPromise 实例,用于与 Wasm 合约交互。
contract.query.getGreeting(currentAccount, { gasLimit: 10000000000 }, currentAccount) 调用合约的只读方法 (query),获取 greeting 信息。
contract.tx.setGreeting( { gasLimit, value: 0 }, newGreeting) 调用合约的交易方法 (tx),设置 greeting 信息。
tx.signAndSend(currentAccount, { signer: injector.signer }, ({ status }) => { ... }); 签名并发送交易,并监听交易状态。

友情提示:

  • 代码仅供参考,请根据你的实际情况进行修改。
  • 在生产环境中使用时,请务必进行安全审计。
  • 遇到问题不要慌,Google 一下,Stack Overflow 一下,实在不行,问问我!

感谢大家的观看! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注