From 442412915a488e0123e65d0137bd9f3e916bc845 Mon Sep 17 00:00:00 2001 From: Joshua <62268199+minimalsm@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:15:44 +0000 Subject: [PATCH] i18n(zh-tw): translation import part 07 of 13 (23 files) --- .../docs/standards/tokens/erc-721/index.md | 255 +++ .../docs/standards/tokens/erc-777/index.md | 45 + .../developers/docs/standards/tokens/index.md | 41 + .../zh-tw/developers/docs/storage/index.md | 120 +- .../developers/docs/transactions/index.md | 124 +- .../developers/docs/web2-vs-web3/index.md | 18 +- .../developers/docs/wrapped-eth/index.md | 8 +- .../index.md | 300 ++++ .../tutorials/all-you-can-cache/index.md | 864 +++++++++ .../developers/tutorials/app-plasma/index.md | 1255 ++++++++++++++ .../index.md | 131 ++ .../index.md | 585 +++++++ .../index.md | 95 + .../index.md | 363 ++++ .../index.md | 144 ++ .../index.md | 123 ++ .../erc-721-vyper-annotated-code/index.md | 646 +++++++ .../tutorials/erc20-annotated-code/index.md | 803 +++++++++ .../erc20-with-safety-rails/index.md | 217 +++ .../tutorials/ethereum-for-web2-auth/index.md | 886 ++++++++++ .../index.md | 149 ++ .../index.md | 102 ++ .../index.md | 1540 +++++++++++++++++ 23 files changed, 8683 insertions(+), 131 deletions(-) create mode 100644 public/content/translations/zh-tw/developers/docs/standards/tokens/erc-721/index.md create mode 100644 public/content/translations/zh-tw/developers/docs/standards/tokens/erc-777/index.md create mode 100644 public/content/translations/zh-tw/developers/docs/standards/tokens/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/a-developers-guide-to-ethereum-part-one/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/all-you-can-cache/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/app-plasma/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/calling-a-smart-contract-from-javascript/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/deploying-your-first-smart-contract/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/develop-and-test-dapps-with-a-multi-client-local-eth-testnet/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/eip-1271-smart-contract-signatures/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/erc-721-vyper-annotated-code/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/erc20-annotated-code/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/erc20-with-safety-rails/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/ethereum-for-web2-auth/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/getting-started-with-ethereum-development-using-alchemy/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/guide-to-smart-contract-security-tools/index.md create mode 100644 public/content/translations/zh-tw/developers/tutorials/hello-world-smart-contract-fullstack/index.md diff --git a/public/content/translations/zh-tw/developers/docs/standards/tokens/erc-721/index.md b/public/content/translations/zh-tw/developers/docs/standards/tokens/erc-721/index.md new file mode 100644 index 00000000000..e50d8f199fb --- /dev/null +++ b/public/content/translations/zh-tw/developers/docs/standards/tokens/erc-721/index.md @@ -0,0 +1,255 @@ +--- +title: "ERC-721 非同質化代幣標準" +description: "了解 ERC-721,這是以太坊上代表獨特數位資產的非同質化代幣 (NFT) 標準。" +lang: zh-tw +--- + +## 介紹 {#introduction} + +**什麼是非同質化代幣?** + +非同質化代幣 (NFT) 用於以獨一無二的方式來識別某物或某人。 這類型的代幣非常適合在提供收藏品、密鑰、彩票、音樂會和體育比賽的編號座位等平台上使用。 這種特殊類型的代幣具有驚人潛力,因此它應得一個適當標準,而 ERC-721 正是來解決這個問題! + +**什麼是 ERC-721?** + +ERC-721 引入了非同質化代幣標準,換句話說,這類型的代幣是獨一無二,並且可以與來自同一智慧型合約的另一種代幣有不同的價值,這可能是由於其存在時間、稀有性甚至是視覺觀感等其他原因。 +等一下,視覺觀感? + +是的! 所有 NFT 都有一個名為 `tokenId` 的 `uint256` 變數,因此對於任何 ERC-721 合約, +`contract address, uint256 tokenId` 這組配對都必須是全域唯一的。 話雖如此,一個去中心化應用程式可以有一個 "轉換器", +它使用 `tokenId` 作為輸入,並輸出一些很酷的東西的圖片,像是殭屍、武器、技能或超讚的貓咪! + +## 先決條件 {#prerequisites} + +- [賬戶](/developers/docs/accounts/) +- [智能合約](/developers/docs/smart-contracts/) +- [代幣標準](/developers/docs/standards/tokens/) + +## 主旨 {#body} + +ERC-721(以太坊意見請求 721)由 William Entriken、Dieter Shirley、Jacob Evans、Nastassia Sachs 於 2018 年 1 月提出,是一種非同質化代幣標準,在智慧型合約中實作代幣應用程式介面。 + +它提供的功能包括將代幣從一個帳戶轉移到另一個帳戶、獲取帳戶當前的代幣餘額、獲取特定代幣的所有者以及網路上可用代幣的總供應量。 +此外它還有一些其他功能,例如批准帳戶中一定數量的代幣可以被第三方帳戶轉移。 + +如果智慧型合約實作以下方法和事件,則可以將其稱為 ERC-721 非同質化代幣合約。一旦部署,它將負責追蹤以太坊上創建的代幣。 + +來自 [EIP-721](https://eips.ethereum.org/EIPS/eip-721): + +### 方法 {#methods} + +```solidity + function balanceOf(address _owner) external view returns (uint256); + function ownerOf(uint256 _tokenId) external view returns (address); + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; + function approve(address _approved, uint256 _tokenId) external payable; + function setApprovalForAll(address _operator, bool _approved) external; + function getApproved(uint256 _tokenId) external view returns (address); + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +``` + +### Events {#events} + +```solidity + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); +``` + +### 範例 {#web3py-example} + +讓我們看看為何標準如此重要,去讓我們檢查以太坊上的任何 ERC-721 代幣合約變得簡單。 +我們只需要合約應用程式二進位介面 (ABI) 來創建任何 ERC-721 代幣的介面。 如下所示,我們將使用簡化的 ABI,使其成為一個低門檻的範例。 + +#### Web3.py 範例 {#web3py-example} + +首先,請確認您已安裝 [Web3.py](https://web3py.readthedocs.io/en/stable/quickstart.html#installation) Python 函式庫: + +``` +pip install web3 +``` + +```python +from web3 import Web3 +from web3._utils.events import get_event_data + + +w3 = Web3(Web3.HTTPProvider("https://cloudflare-eth.com")) + +ck_token_addr = "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d" # CryptoKitties 合約 + +acc_address = "0xb1690C08E213a35Ed9bAb7B318DE14420FB57d8C" # CryptoKitties 銷售拍賣 + +# 這是 ERC-721 NFT 合約的簡化版合約應用程式二進位介面 (ABI)。 +# 它只會公開以下方法:balanceOf(address)、name()、ownerOf(tokenId)、symbol()、totalSupply() +simplified_abi = [ + { + 'inputs': [{'internalType': 'address', 'name': 'owner', 'type': 'address'}], + 'name': 'balanceOf', + 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], + 'payable': False, 'stateMutability': 'view', 'type': 'function', 'constant': True + }, + { + 'inputs': [], + 'name': 'name', + 'outputs': [{'internalType': 'string', 'name': '', 'type': 'string'}], + 'stateMutability': 'view', 'type': 'function', 'constant': True + }, + { + 'inputs': [{'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256'}], + 'name': 'ownerOf', + 'outputs': [{'internalType': 'address', 'name': '', 'type': 'address'}], + 'payable': False, 'stateMutability': 'view', 'type': 'function', 'constant': True + }, + { + 'inputs': [], + 'name': 'symbol', + 'outputs': [{'internalType': 'string', 'name': '', 'type': 'string'}], + 'stateMutability': 'view', 'type': 'function', 'constant': True + }, + { + 'inputs': [], + 'name': 'totalSupply', + 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], + 'stateMutability': 'view', 'type': 'function', 'constant': True + }, +] + +ck_extra_abi = [ + { + 'inputs': [], + 'name': 'pregnantKitties', + 'outputs': [{'name': '', 'type': 'uint256'}], + 'payable': False, 'stateMutability': 'view', 'type': 'function', 'constant': True + }, + { + 'inputs': [{'name': '_kittyId', 'type': 'uint256'}], + 'name': 'isPregnant', + 'outputs': [{'name': '', 'type': 'bool'}], + 'payable': False, 'stateMutability': 'view', 'type': 'function', 'constant': True + } +] + +ck_contract = w3.eth.contract(address=w3.to_checksum_address(ck_token_addr), abi=simplified_abi+ck_extra_abi) +name = ck_contract.functions.name().call() +symbol = ck_contract.functions.symbol().call() +kitties_auctions = ck_contract.functions.balanceOf(acc_address).call() +print(f"{name} [{symbol}] 拍賣中的 NFT:{kitties_auctions}") + +pregnant_kitties = ck_contract.functions.pregnantKitties().call() +print(f"{name} [{symbol}] 懷孕中的 NFT:{pregnant_kitties}") + +# 使用 Transfer 事件 ABI 來取得已轉移貓咪的資訊。 +tx_event_abi = { + 'anonymous': False, + 'inputs': [ + {'indexed': False, 'name': 'from', 'type': 'address'}, + {'indexed': False, 'name': 'to', 'type': 'address'}, + {'indexed': False, 'name': 'tokenId', 'type': 'uint256'}], + 'name': 'Transfer', + 'type': 'event' +} + +# 我們需要事件的簽章來篩選日誌 +event_signature = w3.keccak(text="Transfer(address,address,uint256)").hex() + +logs = w3.eth.get_logs({ + "fromBlock": w3.eth.block_number - 120, + "address": w3.to_checksum_address(ck_token_addr), + "topics": [event_signature] +}) + +# 注意: +# - 如果沒有回傳任何 Transfer 事件,請增加 120 這個區塊數量。 +# - 如果找不到任何 Transfer 事件,您也可以嘗試在此處取得 tokenId: +# https://etherscan.io/address/0x06012c8cf97BEaD5deAe237070F9587f8E7A266d#events +# 按一下以展開事件的日誌,並複製其「tokenId」引數 +recent_tx = [get_event_data(w3.codec, tx_event_abi, log)["args"] for log in logs] + +if recent_tx: + kitty_id = recent_tx[0]['tokenId'] # 從上面的連結將「tokenId」貼在此處 + is_pregnant = ck_contract.functions.isPregnant(kitty_id).call() + print(f"{name} [{symbol}] NFT {kitty_id} 是否懷孕:{is_pregnant}") +``` + +除了標準事件外,謎戀貓合約還有一些有趣的事件。 + +讓我們檢查其中兩個:`Pregnant` 和 `Birth`。 + +```python +# 使用 Pregnant 和 Birth 事件的 ABI 來取得新貓咪的資訊。 +ck_extra_events_abi = [ + { + 'anonymous': False, + 'inputs': [ + {'indexed': False, 'name': 'owner', 'type': 'address'}, + {'indexed': False, 'name': 'matronId', 'type': 'uint256'}, + {'indexed': False, 'name': 'sireId', 'type': 'uint256'}, + {'indexed': False, 'name': 'cooldownEndBlock', 'type': 'uint256'}], + 'name': 'Pregnant', + 'type': 'event' + }, + { + 'anonymous': False, + 'inputs': [ + {'indexed': False, 'name': 'owner', 'type': 'address'}, + {'indexed': False, 'name': 'kittyId', 'type': 'uint256'}, + {'indexed': False, 'name': 'matronId', 'type': 'uint256'}, + {'indexed': False, 'name': 'sireId', 'type': 'uint256'}, + {'indexed': False, 'name': 'genes', 'type': 'uint256'}], + 'name': 'Birth', + 'type': 'event' + }] + +# 我們需要事件的簽章來篩選日誌 +ck_event_signatures = [ + w3.keccak(text="Pregnant(address,uint256,uint256,uint256)").hex(), + w3.keccak(text="Birth(address,uint256,uint256,uint256,uint256)").hex(), +] + +# 這是一個 Pregnant 事件: +# - https://etherscan.io/tx/0xc97eb514a41004acc447ac9d0d6a27ea6da305ac8b877dff37e49db42e1f8cef#eventlog +pregnant_logs = w3.eth.get_logs({ + "fromBlock": w3.eth.block_number - 120, + "address": w3.to_checksum_address(ck_token_addr), + "topics": [ck_event_signatures[0]] +}) + +recent_pregnants = [get_event_data(w3.codec, ck_extra_events_abi[0], log)["args"] for log in pregnant_logs] + +# 這是一個 Birth 事件: +# - https://etherscan.io/tx/0x3978028e08a25bb4c44f7877eb3573b9644309c044bf087e335397f16356340a +birth_logs = w3.eth.get_logs({ + "fromBlock": w3.eth.block_number - 120, + "address": w3.to_checksum_address(ck_token_addr), + "topics": [ck_event_signatures[1]] +}) + +recent_births = [get_event_data(w3.codec, ck_extra_events_abi[1], log)["args"] for log in birth_logs] +``` + +## 熱門 NFT {#popular-nfts} + +- [Etherscan NFT Tracker](https://etherscan.io/nft-top-contracts) 根據轉帳量列出以太坊上的頂級 NFT。 +- [CryptoKitties](https://www.cryptokitties.co/) 是一款遊戲,主題圍繞著一種我們稱之為「謎戀貓」的 + 可繁殖、可收藏且非常可愛的生物。 +- [Sorare](https://sorare.com/) 是一款全球夢幻足球遊戲,你可以在其中收集限量版收藏品、 + 管理你的球隊並參與競爭以贏得獎品。 +- [以太坊域名服務 (ENS)](https://ens.domains/) 提供一種安全且去中心化的方式,可使用簡單易讀的名稱來定址 + 鏈上和鏈下的資源。 +- [POAP](https://poap.xyz) 會向參加活動或完成特定行動的人們免費發放 NFT。 建立和分發 POAP 是免費的。 +- [Unstoppable Domains](https://unstoppabledomains.com/) 是一家總部位於舊金山的公司,專門在 + 區塊鏈上建立網域。 區塊鏈網域以人類可讀的名稱取代加密貨幣地址,並可用於啟用 + 抗審查的網站。 +- [Gods Unchained Cards](https://godsunchained.com/) 是以太坊區塊鏈上的一款集換式卡牌遊戲 (TCG),它使用 NFT 為 + 遊戲內資產帶來真正的所有權。 +- [Bored Ape Yacht Club](https://boredapeyachtclub.com) 是一個包含 10,000 個獨特 NFT 的收藏品,它既是一件可證明為稀有的藝術品,也同時是俱樂部的會員代幣,能為會員提供福利,而這些福利會隨著社群的努力與時俱進。 + +## 延伸閱讀 {#further-reading} + +- [EIP-721:ERC-721 非同質化代幣標準](https://eips.ethereum.org/EIPS/eip-721) +- [OpenZeppelin - ERC-721 文件](https://docs.openzeppelin.com/contracts/3.x/erc721) +- [OpenZeppelin - ERC-721 實作](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol) +- [Alchemy NFT API](https://www.alchemy.com/docs/reference/nft-api-quickstart) diff --git a/public/content/translations/zh-tw/developers/docs/standards/tokens/erc-777/index.md b/public/content/translations/zh-tw/developers/docs/standards/tokens/erc-777/index.md new file mode 100644 index 00000000000..b623419752f --- /dev/null +++ b/public/content/translations/zh-tw/developers/docs/standards/tokens/erc-777/index.md @@ -0,0 +1,45 @@ +--- +title: "ERC-777 代幣標準" +description: "了解 ERC-777,這是一種帶有掛鉤 (hooks) 的改良版同質化代幣標準,但基於安全考量,建議使用 ERC-20。" +lang: zh-tw +--- + +## 警告 {#warning} + +\*\*ERC-777 由於[容易受到不同種類的網絡攻擊], 因此難以正確實行 (https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2620)。 為此建議使用 [ERC-20](/developers/docs/standards/tokens/erc-20/) 。\*\*本頁保留作歷史檔案。 + +## 簡介? {#introduction} + +ERC-777是一種同質化代幣標準, 改良於現有 [ERC-20](/developers/docs/standards/tokens/erc-20/) 標準上。 + +## 先決條件 {#prerequisites} + +為能更好地理解本頁內容, 我們推薦你先閱讀有關 [ERC-20](/developers/docs/standards/tokens/erc-20/)。 + +## ERC-777 相對於 ERC-20 提出了哪些改進? {#-erc-777-vs-erc-20} + +ERC-777 相對於 ERC-20 進行了以下改進。 + +### 挂鈎 {#hooks} + +Hooks為一功能函數於智慧型合約程式中. 其能被調用當代幣被接發經由一智慧型合約. 其使智慧型合約能與進發之代幣互動. + +挂鈎是使用 [ERC-1820](https://eips.ethereum.org/EIPS/eip-1820) 標準注冊和發現的。 + +#### 為何掛鉤棒棒? {#why-are-hooks-great} + +1. 挂鈎允許在單筆交易中向合約傳送代幣並通知合約,而 [ERC-20](https://eips.ethereum.org/EIPS/eip-20) 則需要進行雙重調用 (`approve`/`transferFrom`) 來達成同樣的操作。 +2. 合約無登記之掛鉤為非完整ERC-777. 傳送合約將終止交易當接收合約無法登記一掛鉤. 此預防意外傳送至一ERC-777智慧型合約. +3. 掛鉤能脫退中止合約. + +### 小數 {#decimals} + +此標準還解決了 ERC-20 中引起的 `decimals` 混亂。 這種明確度改善了開發者的體驗。 + +### 向後兼容 ERC-20 {#backwards-compatibility-with-erc-20} + +ERC-777 合約可以像 ERC-20 合約一樣進行互動。 + +## 延伸閱讀 {#further-reading} + +[EIP-777: 代幣標準](https://eips.ethereum.org/EIPS/eip-777) diff --git a/public/content/translations/zh-tw/developers/docs/standards/tokens/index.md b/public/content/translations/zh-tw/developers/docs/standards/tokens/index.md new file mode 100644 index 00000000000..05d5dc57064 --- /dev/null +++ b/public/content/translations/zh-tw/developers/docs/standards/tokens/index.md @@ -0,0 +1,41 @@ +--- +title: "代幣標準" +description: "探索以太坊代幣標準,包含用於同質化與非同質化代幣的 ERC-20、ERC-721 和 ERC-1155。" +lang: zh-tw +incomplete: true +--- + +## 介紹 {#introduction} + +許多以太坊開發標準都專注於代幣介面。 這些標準有助於確保智能合約保持可組合性,因此當新專案發行代幣時,它能與現有的去中心化交易所和應用程式保持相容。 + +代幣標準定義了代幣在整個以太坊生態系統中的行為和互動方式。 它們讓開發者更容易建構,無需重造輪子,並確保代幣可與錢包、交易所及 DeFi 平台無縫運作。 無論是在遊戲、管理體系還是其他使用案例中,這些標準都提供了一致性,並讓以太坊更加互聯互通。 + +## 先決條件 {#prerequisites} + +- [以太坊開發標準](/developers/docs/standards/) +- [智能合約](/developers/docs/smart-contracts/) + +## 代幣標準 {#token-standards} + +以下是以太坊上一些最受歡迎的代幣標準: + +- [ERC-20](/developers/docs/standards/tokens/erc-20/) - 一種適用於同質化(可互換)代幣的標準介面,例如投票代幣、質押代幣或虛擬貨幣。 + +### NFT 標準 {#nft-standards} + +- [ERC-721](/developers/docs/standards/tokens/erc-721/) - 一種非同質化代幣的標準介面,例如藝術品或歌曲的契約。 +- [ERC-1155](/developers/docs/standards/tokens/erc-1155/) - ERC-1155 允許更有效率的交易和交易捆綁 – 從而節省成本。 該代幣標準允許創建實用代幣(例如 $BNB 或 $BAT)和非同質化代幣如 CryptoPunks。 + +完整的 [ERC](https://eips.ethereum.org/erc) 提案清單。 + +## 延伸閱讀 {#further-reading} + +_知道一個曾經幫助你學習更多社區或社團資源? 歡迎在本頁自由編輯或添加內容!_ + +## 相關教學 {#related-tutorials} + +- [代幣整合檢查清單](/developers/tutorials/token-integration-checklist/) _– 一份在與代幣互動時的注意事項清單。_ +- [了解 ERC20 代幣智能合約](/developers/tutorials/understand-the-erc-20-token-smart-contract/) _– 在以太坊測試網上部署您第一個智能合約的簡介。_ +- [從 Solidity 智能合約轉帳及授權 ERC20 代幣](/developers/tutorials/transfers-and-approval-of-erc-20-tokens-from-a-solidity-smart-contract/) _– 如何使用 Solidity 語言,透過智能合約與代幣互動。_ +- [實作 ERC721 市場 [操作指南]](/developers/tutorials/how-to-implement-an-erc721-market/) _– 如何在去中心化的分類廣告板上出售代幣化物品。_ diff --git a/public/content/translations/zh-tw/developers/docs/storage/index.md b/public/content/translations/zh-tw/developers/docs/storage/index.md index 8fc5397730a..49717e97026 100644 --- a/public/content/translations/zh-tw/developers/docs/storage/index.md +++ b/public/content/translations/zh-tw/developers/docs/storage/index.md @@ -1,12 +1,12 @@ --- -title: 去中心化存儲 -description: 去中心化存儲及可以將該存儲整合到去中心化應用程式的可用工具概觀 +title: "去中心化存儲" +description: "去中心化存儲及可以將該存儲整合到去中心化應用程式的可用工具概觀" lang: zh-tw --- 與單一公司或組織運作的中心化伺服器不同,去中心化存儲系統由持有全部資料中部分資料的使用者和營運者的點對點網路組成,建立了一個彈性文件存儲共用系統。 這些存儲系統可以位於基於區塊鏈的應用程式或任何點對點網路中。 -以太坊本身可以用作去中心化存儲系統,所有智慧型合約中的程式碼存儲都是如此。 然而,當涉及大量資料時,就不符合以太坊的設計目的。 該鏈正在穩步增長,但在撰寫本文時,以太坊鏈約為 500GB - 1TB([取決於用戶端](https://etherscan.io/chartsync/chaindefault)),網路上的每個節點都需要能夠存儲所有資料。 如果鏈上資料量擴大(例如 5TB),所有節點都無法繼續運作。 此外,由於[燃料](/developers/docs/gas)費用,將這麼多資料部署到主網的成本將非常昂貴。 +以太坊本身可以用作去中心化存儲系統,所有智慧型合約中的程式碼存儲都是如此。 然而,當涉及大量資料時,就不符合以太坊的設計目的。 該鏈正在穩步增長,但在撰寫本文時,以太坊鏈的大小約為 500GB 至 1TB([取決于用戶端](https://etherscan.io/chartsync/chaindefault)),網路上的每個節點都需要能夠存儲所有資料。 如果鏈上資料量擴大(例如 5TB),所有節點都無法繼續運作。 此外,由於 [gas](/developers/docs/gas) 費用,將這麼多資料部署到主網的成本將會高得嚇人。 由於這些限制,我們需要不同的鏈或方法來以去中心化方式存儲大量資料。 @@ -17,15 +17,15 @@ lang: zh-tw - 去中心化 - 共識 -## 持續機制 / 誘因架構 {#persistence-mechanism} +## 持久性機制 / 激勵結構 {#persistence-mechanism} ### 基於區塊鏈 {#blockchain-based} 為了讓一段資料永久保存,我們需要使用持久性機制。 例如,在以太坊上,持久性機制意味著運行一個節點時需要考慮整條鏈。 新的資料被新增至鏈的末端,並且鏈會繼續增長,並要求每個節點複製所有內嵌的資料。 -這稱為**基於區塊鏈**的持久性。 +這就是所謂的 **基於區塊鏈** 的持久性。 -基於區塊鏈的持久性會出現區塊鏈過大,無法維護和存儲所有資料的問題(例如[許多機構](https://healthit.com.au/how-big-is-the-internet-and-how-do-we-measure-it/)預測整條區塊鏈網路需要 40ZB 的存儲容量)。 +基於區塊鏈的持久性的問題在於,鏈可能會變得過於龐大,以至於無法實際地維護和存儲所有資料(例如,[許多來源](https://healthit.com.au/how-big-is-the-internet-and-how-do-we-measure-it/)估計網際網路需要超過 40 ZB 的存儲容量)。 區塊鏈也必須具有某種類型的激勵結構。 為實現基於區塊鏈的持久性,需要向驗證者支付費用。 資料被新增到鏈上後,向驗證者支付以讓其繼續添加資料。 @@ -36,14 +36,14 @@ lang: zh-tw ### 基於合約 {#contract-based} -我們能直觀地感受到,**基於合約**的持久性使得資料不能被每個節點複製並永久存儲,而必須根據合約協定進行維護。 這些是與多個節點簽訂的協定,承諾在一段時間內保存一段資料。 一旦時間結束,就必須向節點續費,以保持資料的持久性。 +**基於合約**的持久性認為,資料無法由每個節點複製並永久存儲,而必須透過合約協議來維護。 這些是與多個節點簽訂的協定,承諾在一段時間內保存一段資料。 一旦時間結束,就必須向節點續費,以保持資料的持久性。 -在大多數情況下,不會將所有資料存儲在鏈上,而是存儲資料在鏈上位置的雜湊值。 這樣,就不需要擴充整個鏈來保留所有資料。 +在大多數情況下,不會將所有資料存儲在鏈上,而是存儲資料在鏈上位置的哈希。 這樣,就不需要擴充整個鏈來保留所有資料。 具有基於合約的持久性的平台: -- [Filecoin](https://docs.filecoin.io/about-filecoin/what-is-filecoin/) -- [Skynet](https://siasky.net/) +- [Filecoin](https://docs.filecoin.io/basics/what-is-filecoin) +- [Skynet](https://sia.tech/) - [Storj](https://storj.io/) - [Züs](https://zus.network/) - [Crust Network](https://crust.network) @@ -54,14 +54,14 @@ lang: zh-tw 星際檔案系統是一個儲存和存取檔案、網站、應用程式和資料的分散式系統。 雖然它沒有內建激勵計劃,但可以與上述任何基於合約的激勵解決方案一起使用,以獲得更長期的持久性。 另一個將資料持久儲存在星際檔案系統上的方法是與某項固定服務(表示將你的資料固定在某處)一起使用。 你甚至可以運行自己的星際檔案系統節點來為該網路做出貢獻,從而將你和/或他人的資料免費且持久地儲存在星際檔案系統上。 -- [星際檔案系統](https://docs.ipfs.io/concepts/what-is-ipfs/) -- [Pinata](https://www.pinata.cloud/)_(星際檔案系統固定服務)_ -- [web3.storage](https://web3.storage/)_(星際檔案系統/菲樂幣固定服務)_ -- [Infura](https://infura.io/product/ipfs)_(星際檔案系統固定服務)_ -- [IPFS Scan](https://ipfs-scan.io) _(星際檔案系統固定瀏覽器)_ -- -- [Filebase](https://filebase.com)_(星際檔案系統固定服務)_ -- [Spheron Network](https://spheron.network/) _(星際檔案系統/菲樂幣固定服務)_ +- [IPFS](https://docs.ipfs.io/concepts/what-is-ipfs/) +- [Pinata](https://www.pinata.cloud/) _(IPFS 釘選服務)_ +- [web3.storage](https://web3.storage/) _(IPFS/Filecoin 釘選服務)_ +- [Infura](https://infura.io/product/ipfs) _(IPFS 釘選服務)_ +- [IPFS Scan](https://ipfs-scan.io) _(IPFS 釘選瀏覽器)_ +- [4EVERLAND](https://www.4everland.org/)_(IPFS 釘選服務)_ +- [Filebase](https://filebase.com) _(IPFS 釘選服務)_ +- [Spheron Network](https://spheron.network/) _(IPFS/Filecoin 釘選服務)_ SWARM 是一種去中心化的資料儲存和分發技術,具有儲存激勵系統和儲存空間租金價格預測機。 @@ -69,7 +69,7 @@ SWARM 是一種去中心化的資料儲存和分發技術,具有儲存激勵 為了保留資料,系統必須有某種機制來確保已保留資料。 -### 質詢機制 {#challenge-mechanism} +### 挑戰機制 {#challenge-mechanism} 確保已保留資料的最常用方法之一是使用向節點發出的某種類型的加密質詢以確保節點仍然擁有資料。 一個簡單的例子就是查看Arweave的存取證明。 它們向節點發出質詢,看看節點是否擁有最新區塊和過去隨機區塊的資料。 如果節點無法給出答案,就會受到處罰。 @@ -82,7 +82,7 @@ SWARM 是一種去中心化的資料儲存和分發技術,具有儲存激勵 - Crust Network - 4EVERLAND -### 去中央化性 {#decentrality} +### 去中心化 {#decentrality} 沒有很好的工具來衡量平台的去中心化程度,但一般來說,你會希望使用不具有某種形式的「認識客戶」的工具來提供平台並非中心化的證據。 @@ -91,14 +91,14 @@ SWARM 是一種去中心化的資料儲存和分發技術,具有儲存激勵 - Skynet - Arweave - Filecoin -- IPFS -- Ethereum +- 星際檔案系統 +- 以太坊 - Crust Network - 4EVERLAND ### 共識 {#consensus} -這些工具中的大多數都有自己的[共識機制](/developers/docs/consensus-mechanisms/)版本,但通常它們基於[**工作量證明 (PoW)**](/developers/docs/consensus-mechanisms/pow/) 或[**權益證明 (PoS)**](/developers/docs/consensus-mechanisms/pos/)。 +這些工具大多有自己的[共識機制](/developers/docs/consensus-mechanisms/)版本,但通常是基於 [**工作量證明 (PoW)**](/developers/docs/consensus-mechanisms/pow/) 或 [**權益證明 (PoS)**](/developers/docs/consensus-mechanisms/pos/)。 基於工作量證明: @@ -114,103 +114,103 @@ SWARM 是一種去中心化的資料儲存和分發技術,具有儲存激勵 ## 相關工具 {#related-tools} -**IPFS - _星際檔案系統是以太坊的去中心化存儲和檔案引用系統。_** +**IPFS - _星際檔案系統 (InterPlanetary File System) 是一種給以太坊使用的去中心化存儲與檔案參考系統。_** - [Ipfs.io](https://ipfs.io/) - [文件](https://docs.ipfs.io/) -- [Github](https://github.com/ipfs/ipfs) +- [GitHub](https://github.com/ipfs/ipfs) -**Storj DCS - _為開發者提供安全、私有且相容 S3 的去中心化雲端物件存儲。_** +**Storj DCS - _為開發者設計的安全、私有且與 S3 相容的去中心化雲端物件存儲。_** - [Storj.io](https://storj.io/) - [文件](https://docs.storj.io/) - [GitHub](https://github.com/storj/storj) -**Skynet - _Skynet 是一個致力於去中心化網路的去中心化工作量證明鏈。_** +**Sia - _利用密碼學建立無需信任的雲端存儲市集,讓買賣雙方可以直接交易。_** -- [Skynet.net](https://siasky.net/) -- [文件](https://siasky.net/docs/) -- [Github](https://github.com/SkynetLabs/) +- [Skynet.net](https://sia.tech/) +- [文件](https://docs.sia.tech/) +- [GitHub](https://github.com/SiaFoundation/) -**Filecoin - _Filecoin 是由星際檔案系統背後的同一團隊建立的。 它是星際檔案系統概念之上的激勵層。_** +**Filecoin - _Filecoin 由 IPFS 背後的同一個團隊所創建。 此添增一誘因層面於IPFS想法之上._** - [Filecoin.io](https://filecoin.io/) - [文件](https://docs.filecoin.io/) -- [Github](https://github.com/filecoin-project/) +- [GitHub](https://github.com/filecoin-project/) -**Arweave - _Arweave 是一個用於存儲資料的去中心化存儲平台。_** +**Arweave - _Arweave 是一個用來存儲資料的去中心化存儲平台。_** - [Arweave.org](https://www.arweave.org/) - [文件](https://docs.arweave.org/info/) - [Arweave](https://github.com/ArweaveTeam/arweave/) -**Züs - _Züs 是具有分片和 Blobber 的權益證明去中心化存儲平台。_** +**Züs - _Züs 是一個具有分片和 blobber 的權益證明去中心化存儲平台。_** - [zus.network](https://zus.network/) -- [文件](https://0chaindocs.gitbook.io/zus-docs) -- [Github](https://github.com/0chain/) +- [文件](https://docs.zus.network/zus-docs/) +- [GitHub](https://github.com/0chain/) -**Crust Network - _Crust 是基於星際檔案系統的去中心化存儲平台。_** +**Crust Network - _Crust 是建構於 IPFS 之上的去中心化存儲平台。_** - [Crust.network](https://crust.network) - [文件](https://wiki.crust.network) - [GitHub](https://github.com/crustio) -**Swarm - _用於以太坊 web3 堆疊的分佈式存儲平台和內容分發服務。_** +**Swarm - _為以太坊 web3 堆疊設計的分散式存儲平台與內容分發服務。_** - [EthSwarm.org](https://www.ethswarm.org/) -- [文件](https://docs.ethswarm.org/docs/) -- [Github](https://github.com/ethersphere/) +- [文件](https://docs.ethswarm.org/) +- [GitHub](https://github.com/ethersphere/) -**OrbitDB - _基於星際檔案系統的去中心化點對點資料庫。_** +**OrbitDB - _建構於 IPFS 之上的去中心化點對點資料庫。_** - [OrbitDB.org](https://orbitdb.org/) - [文件](https://github.com/orbitdb/field-manual/) -- [Github](https://github.com/orbitdb/orbit-db/) +- [GitHub](https://github.com/orbitdb/orbit-db/) -**Aleph.im - _去中心化雲端專案(資料庫、檔案存儲、運算和去中心化身分)。 鏈下和鏈上點對點技術的獨特融合。 星際檔案系統和多鏈相容性。_** +**Aleph.im - _去中心化雲端專案(資料庫、檔案存儲、運算與 DID)。 鏈下和鏈上點對點技術的獨特融合。 IPFS及跨鏈組合性._** -- [Aleph.im](https://aleph.im/) -- [文件](https://aleph.im/#/developers/) -- [Github](https://github.com/aleph-im/) +- [Aleph.im](https://aleph.cloud/) +- [文件](https://docs.aleph.cloud/) +- [GitHub](https://github.com/aleph-im/) -**Ceramic - _使用者控制的星際檔案系統資料庫存儲,用於資料豐富且引人入勝的應用程式。_** +**Ceramic - _為資料豐富且具吸引力的應用程式設計,由使用者控制的 IPFS 資料庫存儲。_** - [Ceramic.network](https://ceramic.network/) -- [文件](https://developers.ceramic.network/learn/welcome/) -- [Github](https://github.com/ceramicnetwork/js-ceramic/) +- [文件](https://developers.ceramic.network/) +- [GitHub](https://github.com/ceramicnetwork/js-ceramic/) -**Filebase - _ S3 相容的去中心化存儲和異地備援星際檔案系統固定服務。 所有透過 Filebase 上傳到星際檔案系統的檔案,都會自動被固定到 Filebase 基礎設施,並在全球複製 3 份。_** +**Filebase - _與 S3 相容的去中心化存儲和異地備援 IPFS 釘選服務。 所有透過 Filebase 上傳到 IPFS 的檔案都會自動釘選到 Filebase 基礎設施,並在全球複製 3 份。_** - [Filebase.com](https://filebase.com/) -- [文檔](https://docs.filebase.com/) -- [Github](https://github.com/filebase) +- [文件](https://docs.filebase.com/) +- [GitHub](https://github.com/filebase) -**4EVERLAND - _Web 3.0 雲端運算平台,集存儲、運算和網路核心能力於一身,相容於 S3 並在星際檔案系統和 Arweave 等去中心化存儲網路上提供同步資料存儲。_** +**4EVERLAND - _一個 Web 3.0 雲端運算平台,整合了存儲、運算和網路核心能力,與 S3 相容,並在 IPFS 和 Arweave 等去中心化存儲網路上提供同步資料存儲。_** - [4everland.org](https://www.4everland.org/) - [文件](https://docs.4everland.org/) - [GitHub](https://github.com/4everland) -**Kaleido - _區塊鏈即服務平台,具有點擊按鈕的星際檔案系統節點_** +**Kaleido - _一個提供可一鍵部署 IPFS 節點的區塊鏈即服務平台_** - [Kaleido](https://kaleido.io/) - [文件](https://docs.kaleido.io/kaleido-services/ipfs/) - [GitHub](https://github.com/kaleido-io) -**Spheron Network - _Spheron 是一個平台即服務 (PaaS),專為希望在去中心化基礎設施上以最佳效能啟動其應用程式的去中心化應用程式而設計。 它提供開箱即用的運算、去中心化存儲、內容傳遞網路和網頁寄存。_** +**Spheron Network - _Spheron 是一個平台即服務 (PaaS),專為希望在去中心化基礎設施上以最佳效能啟動其應用程式的去中心化應用程式而設計。 它提供開箱即用的運算、去中心化存儲、CDN 與網頁代管服務。_** - [spheron.network](https://spheron.network/) - [文件](https://docs.spheron.network/) - [GitHub](https://github.com/spheronFdn) -## 衍生閱讀 {#further-reading} +## 延伸閱讀 {#further-reading} -- [什麼是去中心化存儲?](https://coinmarketcap.com/alexandria/article/what-is-decentralized-storage-a-deep-dive-by-filecoin) - _CoinMarketCap_ -- [打破關於去中心化存儲的五個常見誤解](https://www.storj.io/blog/busting-five-common-myths-about-decentralized-storage) - _Storj_ +- [什麼是去中心化存儲?](https://coinmarketcap.com/academy/article/what-is-decentralized-storage-a-deep-dive-by-filecoin) - _CoinMarketCap_ +- [破解關於去中心化存儲的五個常見迷思](https://www.storj.io/blog/busting-five-common-myths-about-decentralized-storage) - _Storj_ -_知道一個曾經幫助你學習更多社區或社團資源? 歡迎在本頁自由編輯或添加內容!!_ +_知道一個曾經幫助你學習更多社區或社團資源? 歡迎在本頁自由編輯或添加內容!_ ## 相關主題 {#related-topics} -- [開發架構](/developers/docs/frameworks/) +- [開發框架](/developers/docs/frameworks/) diff --git a/public/content/translations/zh-tw/developers/docs/transactions/index.md b/public/content/translations/zh-tw/developers/docs/transactions/index.md index b233bf5bf44..8cf2d797733 100644 --- a/public/content/translations/zh-tw/developers/docs/transactions/index.md +++ b/public/content/translations/zh-tw/developers/docs/transactions/index.md @@ -1,20 +1,21 @@ --- -title: 交易 -description: 以太坊交易概觀 – 運作原理、資料結構以及如何透過應用程式發送。 +title: "交易" +description: "以太坊交易概觀 – 運作原理、資料結構以及如何透過應用程式發送。" lang: zh-tw --- 交易是帳戶發出的帶密碼學簽章的指令。 帳戶將發起交易以更新以太坊網路的狀態。 最簡單的交易是將以太幣從一個帳戶轉帳到另一個帳戶。 -## 前置要求 {#prerequisites} +## 先決條件 {#prerequisites} -為了讓你更容易理解本頁,建議你先閱讀[帳戶](/developers/docs/accounts/)及我們的[以太坊介紹](/developers/docs/intro-to-ethereum/)。 +為協助您更了解本頁,建議您先閱讀 [帳戶](/developers/docs/accounts/) 和我們的 [以太坊簡介](/developers/docs/intro-to-ethereum/)。 ## 什麼是交易? {#whats-a-transaction} 以太坊交易是指由外部帳戶發起的操作,換句話說,此帳戶是由人而不是智慧型合約管理的帳戶。 例如,如果 Bob 發送給 Alice 1 以太幣,Bob 的帳戶必須扣除,Alice 的帳戶必須存入。 此更改狀態的操作發生在交易中。 -![顯示交易導致狀態變化的圖表](./tx.png) _此圖源於[以太坊EVM圖解](https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf)_ +![顯示交易導致狀態變更的圖表](./tx.png) +_圖表改編自 [Ethereum EVM 圖解](https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf)_ 交易會改變以太坊虛擬機的狀態,須廣播至整個網路。 任何節點都可以廣播要在以太坊虛擬機上執行的交易請求;之後驗證者將執行交易並將引起的狀態變化傳播到網路上的其他節點。 @@ -22,17 +23,17 @@ lang: zh-tw 提交的交易包括下列資訊: -- `from` – 發送者(簽署交易者)的地址。 這將是一個外部擁有的帳戶,因爲合約帳戶無法傳送交易 -- `to` – 接收地址(若為外部帳戶,交易將會轉移金額。 如果為合約帳戶,交易將執行合約程式碼) +- `from` – 發送者的地址,將會用來簽署交易。 這將是一個外部擁有的帳戶,因爲合約帳戶無法傳送交易 +- `to` – 接收地址 (若是外部擁有的帳戶,此交易將會轉移價值。 如果為合約帳戶,交易將執行合約程式碼) - `signature` – 發送者的識別碼。 當發送者以私密金鑰簽署交易並確認發送者已授權此交易时,就會產生此簽章。 -- `nonce` - 用來表示帳戶中交易編號的按順序遞增計數器 -- `value` – 發送者轉帳至接收者的以太幣數量(面額為 WEI,1 以太幣等於 10 的 18 次方 wei) -- `input data` –可選欄位,可填入任意資料 -- `gasLimit` – 交易可以使用的最大燃料單位數量。 [以太坊虛擬機](/developers/docs/evm/opcodes)指定了每個計算步驟的所需燃料單位 -- `maxPriorityFeePerGas` - 已使用燃料的最高價格,可作為給驗證者的小費 -- `maxFeePerGas` - 願意為交易支付的每單位燃料的最高費用(包含 `baseFeePerGas` 和 `maxPriorityFeePerGas`) +- `nonce` - 一個循序遞增的計數器,代表來自該帳戶的交易編號 +- `value` – 從發送者轉移到接收者的 ETH 數量 (以 WEI 為單位,其中 1 ETH 等於 1e+18 wei) +- `input data` – 選填欄位,可包含任意資料 +- `gasLimit` – 交易可消耗的 Gas 單位上限。 [EVM](/developers/docs/evm/opcodes) 指定了每個運算步驟所需的 Gas 單位 +- `maxPriorityFeePerGas` - 可作為給驗證程式小費的每單位 Gas 最高價格 +- `maxFeePerGas` - 願意為此交易支付的每單位 Gas 最高費用 (包含 `baseFeePerGas` 與 `maxPriorityFeePerGas`) -燃料指請驗證者處理交易所需的計算。 使用者必須為計算支付費用。 `gasLimit` 和 `maxPriorityFeePerGas` 決定支付給驗證者的最高交易費。 [更多燃料相關資訊](/developers/docs/gas/)。 +燃料指請驗證者處理交易所需的計算。 使用者必須為計算支付費用。 `gasLimit` 和 `maxPriorityFeePerGas` 決定支付給驗證程式的最高交易費用。 [更多關於 Gas 的資訊](/developers/docs/gas/)。 交易物件看起來有些像以下內容: @@ -41,10 +42,10 @@ lang: zh-tw from: "0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8", to: "0xac03bb73b6a9e108530aff4df5077c2b3d481e5a", gasLimit: "21000", - maxFeePerGas: "300" - maxPriorityFeePerGas: "10" + maxFeePerGas: "300", + maxPriorityFeePerGas: "10", nonce: "0", - value: "10000000000", + value: "10000000000" } ``` @@ -52,7 +53,7 @@ lang: zh-tw Geth 之類的以太坊用戶端將處理此簽署過程。 -[JSON-RPC](/developers/docs/apis/json-rpc) 呼叫範例: +範例 [JSON-RPC](/developers/docs/apis/json-rpc) 呼叫: ```json { @@ -99,22 +100,26 @@ Geth 之類的以太坊用戶端將處理此簽署過程。 } ``` -- `raw` 是[遞迴長度前綴 (RLP)](/developers/docs/data-structures-and-encoding/rlp) 編碼形式的已簽署交易 +- `raw` 是以 [遞迴長度前綴 (RLP)](/developers/docs/data-structures-and-encoding/rlp) 編碼形式呈現的已簽署交易 - `tx` 是 JSON 形式的已簽署交易 交易具備簽章雜湊值,因此可通過加密技術證明交易來自發送者並提交至網路。 ### 資料欄位 {#the-data-field} -大多數交易從外部帳戶存取合約。 大部分合約都用 Solidity 寫成,並根據[應用程式介面 (ABI)](/glossary/#abi) 解譯其資料欄位。 +大多數交易從外部帳戶存取合約。 +大多數合約以 Solidity 撰寫,並根據 [應用程式二進位介面 (ABI)](/glossary/#abi) 解譯其資料欄位。 -前四個字節位元組使用函式名稱及參數的雜湊值指定要呼叫的函式。 有時候可以從使用[此資料庫](https://www.4byte.directory/signatures/)識別選擇器中的函式。 +前四個字節位元組使用函式名稱及參數的雜湊值指定要呼叫的函式。 +有時您可以使用[此資料庫](https://www.4byte.directory/signatures/) 來從選擇器識別函式。 -calldata 的剩餘部分是參數,[依據 ABI 規範中的說明編碼](https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding)。 +calldata 的其餘部分是引數,[根據 ABI 規格中的指定方式進行編碼](https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding)。 -例如,我們來看下[這筆交易](https://etherscan.io/tx/0xd0dcbe007569fcfa1902dae0ab8b4e078efe42e231786312289b1eee5590f6a1)。 使用 **Click to see More** 檢視 calldata。 +例如,我們來看看[這筆交易](https://etherscan.io/tx/0xd0dcbe007569fcfa1902dae0ab8b4e078efe42e231786312289b1eee5590f6a1)。 +使用 **Click to see More** 檢視 calldata。 -函式選擇器為 `0xa9059cbb`。 一些[已知的函式具有此簽章](https://www.4byte.directory/signatures/?bytes4_signature=0xa9059cbb)。 在這個例子中,[合約的原始程式碼](https://etherscan.io/address/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#code)已經上傳到 Etherscan,所以我們知道函式是 `transfer(address,uint256)`。 +函式選擇器是 `0xa9059cbb`。 有數個[已知函式具有此簽章](https://www.4byte.directory/signatures/?bytes4_signature=0xa9059cbb)。 +在本案例中,[合約原始碼](https://etherscan.io/address/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#code)已上傳至 Etherscan,因此我們知道函式為 `transfer(address,uint256)`。 其餘資料如下所示: @@ -123,7 +128,9 @@ calldata 的剩餘部分是參數,[依據 ABI 規範中的說明編碼](https: 000000000000000000000000000000000000000000000000000000003b0559f4 ``` -根據 ABI 規範,應用程式介面中的整數值(例如地址,20 字節位元組的整數)顯示為 32 字節位元組的字,並且前面用 0 來填補。 所以我們知道 `to` 地址為 [`4f6742badb049791cd9a37ea913f2bac38d01279`](https://etherscan.io/address/0x4f6742badb049791cd9a37ea913f2bac38d01279)。 `value` 為 0x3b0559f4 = 990206452。 +根據 ABI 規範,應用程式介面中的整數值(例如地址,20 字節位元組的整數)顯示為 32 字節位元組的字,並且前面用 0 來填補。 +因此我們知道 `to` 地址是 [`4f6742badb049791cd9a37ea913f2bac38d01279`](https://etherscan.io/address/0x4f6742badb049791cd9a37ea913f2bac38d01279)。 +`value` 為 0x3b0559f4 = 990206452。 ## 交易類型 {#types-of-transactions} @@ -133,11 +140,11 @@ calldata 的剩餘部分是參數,[依據 ABI 規範中的說明編碼](https: - 合約部署交易:沒有「to」地址的交易,其資料欄供合約程式碼所用。 - 合約執行:與部署的智慧型合約互動的交易。 在本例中,「to」地址為智慧型合約的地址。 -### 關於燃料 {#on-gas} +### 關於 Gas {#on-gas} -如上所述,執行交易需要花費[燃料](/developers/docs/gas/)。 簡單的轉帳交易需要 21000 單位燃料。 +如前所述,執行交易需要花費 [Gas](/developers/docs/gas/)。 簡單的轉帳交易需要 21000 單位燃料。 -所以,若 Bob 要以 190 gwei 的 `baseFeePerGas` 還有 10 gwei 的 `maxPriorityFeePerGas` 給 Alice 發送 1 以太幣,Bob 將需要支付以下費用: +因此,如果 Bob 要在 `baseFeePerGas` 為 190 gwei 且 `maxPriorityFeePerGas` 為 10 gwei 的情況下,傳送 1 ETH 給 Alice,Bob 將需要支付下列費用: ``` (190 + 10) * 21000 = 4,200,000 gwei @@ -145,77 +152,82 @@ calldata 的剩餘部分是參數,[依據 ABI 規範中的說明編碼](https: 0.0042 以太幣 ``` -Bob 的帳戶會被扣除 **-1.0042 以太幣**(1 以太幣給 Alice + 0.0042 以太幣用來支付燃料費) +Bob 的帳戶將會被扣款 **-1.0042 ETH** (1 ETH 給 Alice + 0.0042 ETH 的 Gas 費用) -Alice 的帳戶將存入 **+1.0 以太幣** +Alice 的帳戶將會存入 **+1.0 ETH** -基本費用將銷毀 **-0.00399 以太幣** +基本費用將被銷毀 **-0.00399 ETH** -驗證者將保留 **+0.000210 以太幣**的小費 +驗證程式保留小費 **+0.000210 ETH** - -![顯示如何退還未使用燃料的圖表](./gas-tx.png) _此圖源於[以太坊EVM圖解](https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf)_ +![顯示未使用 Gas 如何退款的圖表](./gas-tx.png) +_圖表改編自 [Ethereum EVM 圖解](https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf)_ 任何交易中未使用的燃料都會退還給使用者帳戶。 -### 智慧型合約互動 {#smart-contract-interactions} +### 智慧合約互動 {#smart-contract-interactions} 任何涉及智慧型合約的交易都需要燃料。 -智慧型合約也可以包含稱為 [`view`](https://docs.soliditylang.org/en/latest/contracts.html#view-functions) 或 [`pure`](https://docs.soliditylang.org/en/latest/contracts.html#pure-functions) 的函數,而不會改變合約的狀態。 因此,從外部帳戶調用這些函數不需要任何燃料。 此場景的底層遠端程序呼叫為 [`eth_call`](/developers/docs/apis/json-rpc#eth_call)。 +智慧合約也可以包含 [`view`](https://docs.soliditylang.org/en/latest/contracts.html#view-functions) 或 [`pure`](https://docs.soliditylang.org/en/latest/contracts.html#pure-functions) 函式,這些函式不會改變合約的狀態。 因此,從外部帳戶調用這些函數不需要任何燃料。 此情境的底層 RPC 呼叫是 [`eth_call`](/developers/docs/apis/json-rpc#eth_call)。 -與使用 `eth_call` 存取不同,這些 `view` 或 `pure` 函數也通常被內部調用(即從合約本身調用或從另一個合約調用),這會消耗燃料。 +與使用 `eth_call` 存取不同,這些 `view` 或 `pure` 函式也常在內部被呼叫 (即從合約本身或從另一個合約呼叫),這確實會花費 Gas。 -## 交易的生命週期 {#transaction-lifecycle} +## 交易生命週期 {#transaction-lifecycle} 一旦交易被提交,就會發生以下情況: -1. 透過加密方式生成交易雜湊值: `0x97d99bc7729211111a21b12c933c949d4f31684f1d6954ff477d0477538ff017` +1. 交易哈希是以密碼學方式產生的: + `0x97d99bc7729211111a21b12c933c949d4f31684f1d6954ff477d0477538ff017` 2. 然後該交易會廣播到網路並添加到交易池中,交易池中包含了其他所有等待中的網路交易。 3. 為了要驗證交易並使交易「成功」,驗證者必須選擇你的交易並將它打包進區塊中。 -4. 隨著時間推移,含有你交易的區塊會被升級為「已證明」,然後是「最終化」。 這些升級進一步確定 你的交易已經成功且永遠不會被更改。 當區塊「最終化」後,就僅可能被網路層級的攻擊變更, 此類攻擊需要花費數十億美元。 +4. 隨著時間推移,含有你交易的區塊會被升級為「已證明」,然後是「最終化」。 這些升級能讓您更加 + 確定交易成功,且永遠不會被更改。 一旦區塊「最終確認」,就只能透過 + 耗資數十億美元的網路層級攻擊才能更改。 -## 視訊示範 {#a-visual-demo} +## 視覺化示範 {#a-visual-demo} 觀看 Austin 為你講解交易、燃料和挖礦。 -## Typed Transaction Envelope 交易 {#typed-transaction-envelope} +## 類型化交易封包 {#typed-transaction-envelope} -以太坊最初有一種形式的交易。 每筆交易都包含 nonce、gas price、gas limit、to address、value、data、v、r 與 s。 這些欄位均為 [RLP 編碼](/developers/docs/data-structures-and-encoding/rlp/),看上去像是以下內容: +以太坊最初有一種形式的交易。 每筆交易都包含 nonce、gas price、gas limit、to address、value、data、v、r 與 s。 這些欄位經過 [RLP 編碼](/developers/docs/data-structures-and-encoding/rlp/)後,看起來像這樣: `RLP([nonce, gasPrice, gasLimit, to, value, data, v, r, s])` -以太坊不斷演進以支援多種交易類型,以便在實作存取清單和 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) 等新功能時不會影響原有交易形式。 +以太坊已演進到支援多種類型的交易,以便在實作存取清單和 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) 等新功能時,不會影響舊有交易格式。 -[EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) 是支援此類行為的協議。 交易的解釋如下: +[EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) 實現了這種行為。 交易的解釋如下: `TransactionType || TransactionPayload` 其欄位定義如下: -- `TransactionType` - 介於 0 和 0x7f 之間的數字,代表總計 128 種可能的交易類型。 -- `TransactionPayload` - 由交易類型定義的任意字節位元組陣列。 +- `TransactionType` - 一個介於 0 和 0x7f 之間的數字,總共有 128 種可能的交易類型。 +- `TransactionPayload` - 由交易類型定義的任意位元組陣列。 -根據 `TransactionType` 值,交易可以分類為: +根據 `TransactionType` 的值,交易可分類為: -1. **類型 0(傳統)交易:**自以太坊推出以來使用的原始交易格式。 它們不包括 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) 的功能,例如動態燃料費計算或智慧型合約的存取清單。 傳統交易缺少在序列化形式中指示交易類型的特定前綴,在使用[遞迴長度前綴 (RLP)](/developers/docs/data-structures-and-encoding/rlp) 編碼時,該前綴以位元組 `0xf8` 開始。 這些交易的 TransactionType 值為 `0x0`。 +1. **類型 0 (傳統) 交易:** 自以太坊推出以來使用的原始交易格式。 它們不包含 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) 的功能,例如動態 Gas 費用計算或智慧合約的存取清單。 傳統交易在其序列化形式中缺少指示其類型的特定前綴,在使用[遞迴長度前綴 (RLP)](/developers/docs/data-structures-and-encoding/rlp) 編碼時以位元組 `0xf8` 開頭。 這些交易的 TransactionType 值為 `0x0`。 -2. **類型 1 交易:**在 [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) 中引入作為以太坊[柏林升級](/ethereum-forks/#berlin)的一部分,這些交易包含一個 `accessList` 參數。 此清單指定了交易期望存取的地址和儲存金鑰,有助於潛在降低涉及智慧型合約的複雜交易的[燃料](/developers/docs/gas/)成本。 EIP-1559 的費用市場變化不會包含在類型 1 交易中。 類型 1 交易也包含一個 `yParity` 參數,該參數可以是 `0x0` 或 `0x1`,表示 secp256k1 簽章的 y 值的奇偶性。 此類交易透過開頭的位元組 `0x01` 開頭辨識,其 TransactionType 值為 `0x1`。 +2. **類型 1 交易:** 在 [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) 中作為以太坊[柏林升級](/ethereum-forks/#berlin) 的一部分引入,這些交易包含一個 `accessList` 參數。 此清單指定了交易預期存取的地址和儲存金鑰,有助於潛在降低涉及智慧合約的複雜交易的 [Gas](/developers/docs/gas/) 成本。 EIP-1559 的費用市場變化不會包含在類型 1 交易中。 類型 1 交易也包含一個 `yParity` 參數,可以是 `0x0` 或 `0x1`,表示 secp256k1 簽章的 y 值的奇偶性。 它們以位元組 `0x01` 開頭來識別,其 TransactionType 值為 `0x1`。 -3. **類型 2 交易**,通常稱為 EIP-1559 交易,是以太坊[倫敦升級](/ethereum-forks/#london)裡 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) 中引入的交易。 這類交易已成為以太坊網路上的標準交易類型。 這些交易引入了一種新的費用市場機制,透過將交易費用分為基本費用和優先費來提高可預測性。 這些交易的開頭為位元組 `0x02`,並包含 `maxPriorityFeePerGas` 和 `maxFeePerGas` 等欄位。 類型 2 交易因其靈活性和效率而成為預設交易,在網路高度擁塞期間尤其受到青睞,因為它們能夠幫助使用者更好地預測及管理交易費用。 這些交易的 TransactionType 值為 `0x2`。 +3. **類型 2 交易**,通常稱為 EIP-1559 交易,是在以太坊[倫敦升級](/ethereum-forks/#london) 的 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) 中引入的交易。 這類交易已成為以太坊網路上的標準交易類型。 這些交易引入了一種新的費用市場機制,透過將交易費用分為基本費用和優先費來提高可預測性。 它們以位元組 `0x02` 開頭,並包含 `maxPriorityFeePerGas` 和 `maxFeePerGas` 等欄位。 類型 2 交易因其靈活性和效率而成為預設交易,在網路高度擁塞期間尤其受到青睞,因為它們能夠幫助使用者更好地預測及管理交易費用。 這些交易的 TransactionType 值為 `0x2`。 +4. **類型 3 (Blob) 交易**在[EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) 中作為以太坊 [Dencun 升級](/ethereum-forks/#dencun) 的一部分被引入。 這些交易旨在更高效地處理「blob」資料 (二進位大型物件),它們提供了一種以更低成本將資料發佈到以太坊網路的方法,尤其有利於二層網路卷軸。 Blob 交易包含額外欄位,例如 `blobVersionedHashes`、`maxFeePerBlobGas` 和 `blobGasPrice`。 它們以位元組 `0x03` 開頭,其 TransactionType 值為 `0x3`。 Blob 交易代表了以太坊在資料可用性和可擴張性方面的重大改進。 +5. **類型 4 交易**是在[EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) 中作為以太坊 [Pectra 升級](/roadmap/pectra/) 的一部分引入的。 這些交易被設計為與帳戶抽象化向前相容。 它們允許 EOA 暫時表現得像智慧合約帳戶,而不會影響其原始功能。 它們包含一個 `authorization_list` 參數,用於指定 EOA 將其權限委派給哪個智慧合約。 交易後,EOA 的程式碼欄位將包含被委派的智慧合約地址。 -## 衍生閱讀 {#further-reading} +## 延伸閱讀 {#further-reading} -- [EIP-2718:Typed Transaction Envelope 交易](https://eips.ethereum.org/EIPS/eip-2718) +- [EIP-2718:類型化交易封包](https://eips.ethereum.org/EIPS/eip-2718) -_認識社區或社團資源能幫助大家學習更多? 歡迎自由編輯或添加於本頁!!_ +_知道一個曾經幫助你學習更多社區或社團資源? 歡迎在本頁自由編輯或添加內容!_ ## 相關主題 {#related-topics} -- [帳戶](/developers/docs/accounts/) -- [以太坊虛擬機](/developers/docs/evm/) +- [賬戶](/developers/docs/accounts/) +- [以太坊虛擬機 (EVM)](/developers/docs/evm/) - [Gas](/developers/docs/gas/) diff --git a/public/content/translations/zh-tw/developers/docs/web2-vs-web3/index.md b/public/content/translations/zh-tw/developers/docs/web2-vs-web3/index.md index d98309a961b..13f27fdc3ef 100644 --- a/public/content/translations/zh-tw/developers/docs/web2-vs-web3/index.md +++ b/public/content/translations/zh-tw/developers/docs/web2-vs-web3/index.md @@ -1,6 +1,6 @@ --- -title: Web2 vs Web3 -description: +title: "Web2 與 Web3" +description: "比較中心化 Web2 服務與建立在以太坊區塊鏈技術上的去中心化 Web3 應用程式。" lang: zh-tw --- @@ -8,7 +8,7 @@ Web2 指的是目前我們大多人熟知的網際網路。 網際網路由各 正在找尋找更適合初學者的資源? 請參閱我們的 [web3 簡介](/web3/)。 -## Web3 優點 {#web3-benefits} +## Web3 的優點 {#web3-benefits} 很多 Web3 開發者選擇建立去中心化應用程式,是因為以太坊固有的去中心化優點: @@ -17,7 +17,7 @@ Web2 指的是目前我們大多人熟知的網際網路。 網際網路由各 - 支付是透過原生代幣以太幣建立的。 - 以太坊是圖靈完備的,這表示你可以在上面寫許多程式。 -## 實務對比 {#practical-comparisons} +## 實際比較 {#practical-comparisons} | Web2 | Web3 | | ------------------------- | --------------------------------------- | @@ -52,11 +52,11 @@ Web3 目前的一些限制: 注意,這些概況可能並不適用於每個網路。 此外實際當中,網路的中心化與去中心化程度是一個範圍;沒有任何一個網路是完全中心化或完全去中心化的。 -## 衍生閱讀 {#further-reading} +## 延伸閱讀 {#further-reading} - [什麼是 Web3?](/web3/) - _ethereum.org_ - [Web 3.0 應用程式的架構](https://www.preethikasireddy.com/post/the-architecture-of-a-web-3-0-application) - _Preethi Kasireddy_ -- [去中心化的意義](https://medium.com/@VitalikButerin/the-meaning-of-decentralization-a0c92b76a274) _2017 年 2 月 6日 - Vitalik Buterin_ -- [去中心化的重要性](https://medium.com/s/story/why-decentralization-matters-5e3f79f7638e) _2018 年 2 月 18 日 - Chris Dixon_ -- [什麼是 Web 3.0?它為什麼重要?](https://medium.com/fabric-ventures/what-is-web-3-0-why-it-matters-934eb07f3d2b) _2019 年 12 月 31 日 - Max Mersch 和 Richard Muirhead_ -- [為何我們需要 Web 3.0](https://medium.com/@gavofyork/why-we-need-web-3-0-5da4f2bf95ab) _2018 年 9 月 12 日 - Gavin Wood_ +- [去中心化的意義](https://medium.com/@VitalikButerin/the-meaning-of-decentralization-a0c92b76a274) _2017 年 2 月 6 日 - Vitalik Buterin_ +- [為什麼去中心化很重要](https://onezero.medium.com/why-decentralization-matters-5e3f79f7638e) _2018 年 2 月 18 日 - Chris Dixon_ +- [什麼是 Web 3.0,以及它為何重要](https://medium.com/fabric-ventures/what-is-web-3-0-why-it-matters-934eb07f3d2b) _2019 年 12 月 31 日 - Max Mersch 和 Richard Muirhead_ +- [為什麼我們需要 Web 3.0](https://gavofyork.medium.com/why-we-need-web-3-0-5da4f2bf95ab) _2018 年 9 月 12 日 - Gavin Wood_ diff --git a/public/content/translations/zh-tw/developers/docs/wrapped-eth/index.md b/public/content/translations/zh-tw/developers/docs/wrapped-eth/index.md index defbdf83881..412c113c089 100644 --- a/public/content/translations/zh-tw/developers/docs/wrapped-eth/index.md +++ b/public/content/translations/zh-tw/developers/docs/wrapped-eth/index.md @@ -1,6 +1,6 @@ --- -title: 甚麼是包裝以太幣 (WETH) -description: 包裝以太幣 (WETH) 簡介 — 以太幣 (ETH) 的一種 ERC20 相容包裝函式。 +title: "甚麼是包裝以太幣 (WETH)" +description: "包裝以太幣 (WETH) 簡介 — 以太幣 (ETH) 的一種 ERC20 相容包裝函式。" lang: zh-tw --- @@ -35,19 +35,16 @@ lang: zh-tw 你需要支付燃料費來使用包裝以太幣智慧型合約來兌換或贖回以太幣。 - 包裝以太幣通常被認為是安全的,因為它基於一個簡單且經過實證的智慧型合約。 包裝以太幣合約也已經經過正式驗證,這是以太坊上智慧型合約的最高安全標準。 - 除了本頁描述的 [包裝以太幣的規範化實作](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2)外,還有其他變體存在於市場中。 這些可能是由應用程式開發者建立的自訂代幣,或在其他區塊鏈上發行的版本,可能會有不同的行為或具有不同的安全屬性。 **始終仔細檢查代幣資訊,以確認你正在與哪一種包裝以太幣實作進行互動。** - @@ -55,7 +52,6 @@ lang: zh-tw - [以太坊主網](https://etherscan.io/token/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) - [Arbitrum](https://arbiscan.io/token/0x82af49447d8a07e3bd95bd0d56f35241523fbab1) - [Optimism](https://optimistic.etherscan.io/token/0x4200000000000000000000000000000000000006) - ## 延伸閱讀 {#further-reading} diff --git a/public/content/translations/zh-tw/developers/tutorials/a-developers-guide-to-ethereum-part-one/index.md b/public/content/translations/zh-tw/developers/tutorials/a-developers-guide-to-ethereum-part-one/index.md new file mode 100644 index 00000000000..0b581c791fb --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/a-developers-guide-to-ethereum-part-one/index.md @@ -0,0 +1,300 @@ +--- +title: "給 Python 開發者的以太坊介紹,第一部分" +description: "以太坊開發介紹,特別適合了解 Python 程式語言的開發人員。" +author: Marc Garreau +lang: zh-tw +tags: [ "python", "web3.py" ] +skill: beginner +published: 2020-09-08 +source: Snake charmers +sourceUrl: https://snakecharmers.ethereum.org/a-developers-guide-to-ethereum-pt-1/ +--- + +所以,您已經聽說過以太坊,並準備好要深入探索了嗎? 本文將快速介紹一些區塊鏈基礎知識,然後讓您與一個模擬的以太坊節點互動——讀取區塊資料、檢查帳戶餘額以及傳送交易。 在此過程中,我們將強調傳統的應用程式建置方式與這種新的去中心化範式之間的差異。 + +## (非強制)先決條件 {#soft-prerequisites} + +本文旨在讓廣大開發人員都能輕鬆理解。 [Python 工具](/developers/docs/programming-languages/python/)將會被使用,但它們只是用來傳達概念的載體——如果您不是 Python 開發人員也沒問題。 不過,我會先假設您已經具備一些基礎知識,這樣我們就可以快速進入以太坊專屬的部分。 + +假設: + +- 您能夠操作終端機, +- 您寫過幾行 Python 程式碼, +- 您的電腦上已安裝 Python 3.6 或更高版本(強烈建議使用[虛擬環境](https://realpython.com/effective-python-environment/#virtual-environments)),以及 +- 您使用過 `pip`,Python 的套件安裝程式。 + 再次強調,即使您不符合上述任何條件,或者不打算重現本文中的程式碼,您仍然可以順利地跟上進度。 + +## 區塊鏈簡介 {#blockchains-briefly} + +描述以太坊的方式有很多種,但其核心是一個區塊鏈。 區塊鏈由一系列的區塊組成,所以讓我們從這裡開始。 簡單來說,以太坊區塊鏈上的每個區塊只包含一些元資料和一個交易列表。 在 JSON 格式中,它看起來像這樣: + +```json +{ + "number": 1234567, + "hash": "0xabc123...", + "parentHash": "0xdef456...", + ..., + "transactions": [...] +} +``` + +每個[區塊](/developers/docs/blocks/)都包含對前一個區塊的引用;`parentHash` 就是前一個區塊的哈希。 + +注意:以太坊經常使用哈希函數來產生固定大小的值(「哈希」)。 哈希在以太坊中扮演著重要角色,但目前您可以放心地將它們視為唯一的識別碼。 + +![一個描述區塊鏈的圖表,包含每個區塊內的資料](./blockchain-diagram.png) + +_區塊鏈本質上是一個鏈結串列;每個區塊都包含對前一個區塊的引用。_ + +這種資料結構本身並不是什麼新奇的東西,但管理網路的規則(即點對點協定)卻是全新的。 沒有中心化的權威機構;網路中的對等節點必須合作以維持網路運作,並透過競爭來決定下一個區塊要包含哪些交易。 所以,當您想寄錢給朋友時,您需要將該交易廣播到網路上,然後等待它被包含在即將產生的區塊中。 + +區塊鏈要驗證金錢確實從一位使用者傳送給另一位使用者,唯一的方法是使用該區塊鏈的原生貨幣(即由該區塊鏈創造和管理的貨幣)。 在以太坊中,這種貨幣稱為以太幣 (ether),而以太坊區塊鏈是唯一包含帳戶餘額官方記錄的地方。 + +## 新範式 {#a-new-paradigm} + +這個新的去中心化技術堆疊催生了新的開發者工具。 這類工具存在於許多程式語言中,但我們將從 Python 的角度來探討。 重申一次:即使 Python 不是您偏好的語言,跟上本文的內容應該也不會太困難。 + +想要與以太坊互動的 Python 開發人員很可能會使用 [Web3.py](https://web3py.readthedocs.io/)。 Web3.py 是一個函式庫,它能大幅簡化您連接到以太坊節點,以及從節點傳送和接收資料的方式。 + +注意:「以太坊節點」和「以太坊用戶端」這兩個詞可以互換使用。 無論是哪個詞,指的都是以太坊網路參與者執行的軟體。 這個軟體可以讀取區塊資料、在新區塊加入鏈上時接收更新、廣播新交易等等。 技術上來說,用戶端是軟體,節點是執行軟體的電腦。 + +[以太坊用戶端](/developers/docs/nodes-and-clients/)可以設定為透過 [IPC](https://wikipedia.org/wiki/Inter-process_communication)、HTTP 或 Websocket 進行連線,因此 Web3.py 需要對應此設定。 Web3.py 將這些連線選項稱為**提供者**。 您需要從這三種提供者中選擇一種,將 Web3.py 執行個體與您的節點連結。 + +![一個圖表,顯示 web3.py 如何使用 IPC 將您的應用程式連接到以太坊節點](./web3py-and-nodes.png) + +_設定以太坊節點和 Web3.py 使用相同的協定進行通訊,例如此圖中的 IPC。_ + +一旦 Web3.py 設定正確,您就可以開始與區塊鏈互動。 以下是一些 Web3.py 的使用範例,作為後續內容的預覽: + +```python +# 讀取區塊資料: +w3.eth.get_block('latest') + +# 傳送一筆交易: +w3.eth.send_transaction({'from': ..., 'to': ..., 'value': ...}) +``` + +## 安裝 {#installation} + +在這份逐步教學中,我們只會在 Python 直譯器中操作。 我們不會建立任何目錄、檔案、類別或函式。 + +注意:在下面的範例中,以 `$` 開頭的指令是用於在終端機中執行的。 (請勿輸入 `$`,它只是表示一行的開始。) + +首先,安裝 [IPython](https://ipython.org/),這是一個方便探索的使用者友好環境。 IPython 提供了 tab 鍵自動完成等功能,讓您更容易了解 Web3.py 的各種可能性。 + +```bash +pip install ipython +``` + +Web3.py 是以 `web3` 的名稱發布的。 安裝方式如下: + +```bash +pip install web3 +``` + +還有一件事——我們稍後將模擬一個區塊鏈,這需要額外安裝幾個相依套件。 您可以透過以下方式安裝: + +```bash +pip install 'web3[tester]' +``` + +您已準備就緒! + +注意:`web3[tester]` 套件支援到 Python 3.10.xx 版本。 + +## 啟動沙箱 {#spin-up-a-sandbox} + +在終端機中執行 `ipython` 來開啟一個新的 Python 環境。 這和執行 `python` 類似,但提供了更多附加功能。 + +```bash +ipython +``` + +這會印出您正在執行的 Python 和 IPython 版本資訊,然後您會看到一個等待輸入的提示符號: + +```python +In [1]: +``` + +您現在看到的是一個互動式 Python shell。 基本上,這是一個可以讓您盡情實驗的沙箱。 如果您已經進行到這一步,是時候匯入 Web3.py 了: + +```python +In [1]: from web3 import Web3 +``` + +## Web3 模組介紹 {#introducing-the-web3-module} + +除了作為通往以太坊的閘道,[Web3](https://web3py.readthedocs.io/en/stable/overview.html#base-api) 模組還提供了一些便捷函式。 讓我們來探索其中幾個。 + +在以太坊應用程式中,您通常需要轉換貨幣單位。 Web3 模組為此提供了幾個輔助方法:[from_wei](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.from_wei) 和 [to_wei](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.to_wei)。 + + +注意:眾所周知,電腦不擅長處理小數運算。 為了解決這個問題,開發人員通常以「分」為單位來儲存美元金額。 例如,價格為 5.99 美元的商品在資料庫中可能會被儲存為 599。 + +在處理以太幣交易時也使用類似的模式。 然而,以太幣不是兩位小數點,而是 18 位! 以太幣的最小單位稱為 wei,因此在傳送交易時指定的是這個數值。 + +1 以太幣 = 1000000000000000000 wei + +1 wei = 0.000000000000000001 以太幣 + + + +試著在 wei 與其他單位之間轉換一些數值。 請注意,在以太幣和 wei 之間[還有許多單位的名稱](https://web3py.readthedocs.io/en/stable/troubleshooting.html#how-do-i-convert-currency-denominations)。 其中比較有名的是 **gwei**,因為交易費用通常用它來表示。 + +```python +In [2]: Web3.to_wei(1, 'ether') +Out[2]: 1000000000000000000 + +In [3]: Web3.from_wei(500000000, 'gwei') +Out[3]: Decimal('0.5') +``` + +Web3 模組上的其他工具方法包括資料格式轉換器(例如 [`toHex`](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.toHex))、位址輔助工具(例如 [`isAddress`](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.isAddress))和哈希函數(例如 [`keccak`](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.keccak))。 本系列文章的後續部分將會介紹其中許多內容。 若要查看所有可用的方法和屬性,可以輸入 `Web3` 來利用 IPython 的自動完成功能。 並在句點後按兩次 tab 鍵。 + +## 與鏈互動 {#talk-to-the-chain} + +便捷方法很棒,但讓我們繼續來談談區塊鏈。 下一步是設定 Web3.py 與以太坊節點進行通訊。 在這裡,我們可以選擇使用 IPC、HTTP 或 Websocket 提供者。 + +我們不會走這條路,但使用 HTTP 提供者的完整工作流程範例如下: + +- 下載一個以太坊節點,例如 [Geth](https://geth.ethereum.org/)。 +- 在一個終端機視窗中啟動 Geth,並等待它同步網路。 預設的 HTTP 連接埠是 `8545`,但可以自行設定。 +- 讓 Web3.py 透過 HTTP 連接到 `localhost:8545` 上的節點。 + `w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))` +- 使用 `w3` 執行個體與節點互動。 + +雖然這是一種「真實」的作法,但同步過程需要數小時,而且如果您只想要一個開發環境,這並非必要。 為此,Web3.py 提供了第四種提供者:**EthereumTesterProvider**。 這個測試提供者會連結到一個模擬的以太坊節點,它有較寬鬆的權限和可供使用的假貨幣。 + +![一個圖表,顯示 EthereumTesterProvider 將您的 web3.py 應用程式連結到一個模擬的以太坊節點](./ethereumtesterprovider.png) + +_EthereumTesterProvider 會連接到一個模擬節點,對於快速建立開發環境來說非常方便。_ + +那個模擬節點稱為 [eth-tester](https://github.com/ethereum/eth-tester),我們在執行 `pip install web3[tester]` 指令時已經將它安裝。 設定 Web3.py 使用此測試提供者非常簡單: + +```python +In [4]: w3 = Web3(Web3.EthereumTesterProvider()) +``` + +現在您準備好在鏈上遨遊了! 這不是大家會說的話。 我剛才亂編的。 讓我們快速導覽一下。 + +## 快速導覽 {#the-quick-tour} + +首先,做個基本功能檢查: + +```python +In [5]: w3.is_connected() +Out[5]: True +``` + +因為我們使用的是測試提供者,這個測試不是很有價值,但如果它失敗了,很可能是您在實例化 `w3` 變數時打錯了字。 再次檢查您是否包含了內層的括號,即 `Web3.EthereumTesterProvider()`。 + +## 導覽第一站:[帳戶](/developers/docs/accounts/) {#tour-stop-1-accounts} + +為了方便,測試提供者建立了一些帳戶,並預先存入了測試以太幣。 + +首先,讓我們看看這些帳戶的列表: + +```python +In [6]: w3.eth.accounts +Out[6]: ['0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', + '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', + '0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69', ...] +``` + +如果您執行這個指令,您應該會看到一個包含十個以 `0x` 開頭的字串列表。 每個都是一個**公開位址**,在某些方面,類似於支票帳戶的帳號。 您可以將此位址提供給想傳送以太幣給您的人。 + +如前所述,測試提供者已為每個帳戶預先存入一些測試以太幣。 讓我們來看看第一個帳戶裡有多少錢: + +```python +In [7]: w3.eth.get_balance(w3.eth.accounts[0]) +Out[7]: 1000000000000000000000000 +``` + +好多個零! 在您笑著把錢存入假銀行之前,回想一下前面關於貨幣單位的課程。 以太幣的數值是以最小單位 wei 來表示的。 將它轉換成以太幣: + +```python +In [8]: w3.from_wei(1000000000000000000000000, 'ether') +Out[8]: Decimal('1000000') +``` + +一百萬測試以太幣——還不賴。 + +## 導覽第二站:區塊資料 {#tour-stop-2-block-data} + +讓我們來看看這個模擬區塊鏈的狀態: + +```python +In [9]: w3.eth.get_block('latest') +Out[9]: AttributeDict({ + 'number': 0, + 'hash': HexBytes('0x9469878...'), + 'parentHash': HexBytes('0x0000000...'), + ... + 'transactions': [] +}) +``` + +一個區塊會傳回很多資訊,但這裡只指出幾點: + +- 區塊編號是零——無論您多久之前設定測試提供者都一樣。 與真實的以太坊網路每 12 秒新增一個新區塊不同,這個模擬會等到您給它一些工作才會有動作。 +- `transactions` 是一個空列表,原因相同:我們還沒有做任何事。 第一個區塊是**空區塊**,只是為了啟動這條鏈。 +- 請注意,`parentHash` 只是一堆空位元組。 這表示它是鏈中的第一個區塊,也稱為**創世區塊**。 + +## 導覽第三站:[交易](/developers/docs/transactions/) {#tour-stop-3-transactions} + +在有待處理的交易之前,我們會一直停在區塊 0,所以讓我們來建立一筆交易。 從一個帳戶傳送幾枚測試以太幣到另一個帳戶: + +```python +In [10]: tx_hash = w3.eth.send_transaction({ + 'from': w3.eth.accounts[0], + 'to': w3.eth.accounts[1], + 'value': w3.to_wei(3, 'ether'), + 'gas': 21000 +}) +``` + +通常這時候您需要等待幾秒鐘,讓您的交易被包含在新區塊中。 完整的過程大致如下: + +1. 提交一筆交易並保留交易哈希。 在包含該交易的區塊被建立和廣播之前,該交易是「待處理」狀態。 + `tx_hash = w3.eth.send_transaction({ … })` +2. 等待交易被包含在一個區塊中: + `w3.eth.wait_for_transaction_receipt(tx_hash)` +3. 繼續執行應用程式邏輯。 查看成功的交易: + `w3.eth.get_transaction(tx_hash)` + +我們的模擬環境會立即將交易新增到新區塊中,因此我們可以立即查看該交易: + +```python +In [11]: w3.eth.get_transaction(tx_hash) +Out[11]: AttributeDict({ + 'hash': HexBytes('0x15e9fb95dc39...'), + 'blockNumber': 1, + 'transactionIndex': 0, + 'from': '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', + 'to': '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', + 'value': 3000000000000000000, + ... +}) +``` + +您會在這裡看到一些熟悉的詳細資訊:`from`、`to` 和 `value` 欄位應與我們 `send_transaction` 呼叫的輸入相符。 另一個讓人安心的地方是,這筆交易被包含在區塊編號 1 中,作為第一筆交易 (`'transactionIndex': 0`)。 + +我們也可以透過檢查兩個相關帳戶的餘額來輕鬆驗證這筆交易是否成功。 三枚以太幣應該已經從一個帳戶轉移到另一個帳戶。 + +```python +In [12]: w3.eth.get_balance(w3.eth.accounts[0]) +Out[12]: 999996999979000000000000 + +In [13]: w3.eth.get_balance(w3.eth.accounts[1]) +Out[13]: 1000003000000000000000000 +``` + +後者看起來沒錯! 餘額從 1,000,000 以太幣增加到 1,000,003 以太幣。 但第一個帳戶發生了什麼事? 它似乎損失了比三枚以太幣多一點的金額。 唉,天下沒有白吃的午餐,使用以太坊公有網路需要您補償其他對等節點所提供的支援。 提交交易的帳戶被扣除了一筆小額交易費用——這筆費用是消耗的 gas 量(一次 ETH 轉帳為 21000 單位 gas)乘以根據網路活動變動的基本費用,再加上給予將交易包含在區塊中的驗證者的小費。 + +更多關於 [gas](/developers/docs/gas/#post-london) 的資訊 + +注意:在公有網路上,交易費用會根據網路需求以及您希望交易處理的速度而變動。 如果您對費用計算的詳細說明感興趣,請參閱我之前關於交易如何被包含在區塊中的文章。 + +## 喘口氣 {#and-breathe} + +我們已經進行了一段時間,這裡似乎是個休息的好時機。 兔子洞還很深,我們將在本系列的第二部分繼續探索。 即將介紹的概念:連接到真實節點、智慧合約和代幣。 還有其他問題嗎? 讓我知道! 您的回饋將影響我們接下來的方向。 歡迎透過 [Twitter](https://twitter.com/wolovim) 提出請求。 diff --git a/public/content/translations/zh-tw/developers/tutorials/all-you-can-cache/index.md b/public/content/translations/zh-tw/developers/tutorials/all-you-can-cache/index.md new file mode 100644 index 00000000000..9dbaedf4296 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/all-you-can-cache/index.md @@ -0,0 +1,864 @@ +--- +title: "任你快取" +description: "學習如何創建和使用快取合約,以降低卷軸交易的成本" +author: Ori Pomerantz +tags: [ "Layer 2", "快取", "儲存" ] +skill: intermediate +published: 2022-09-15 +lang: zh-tw +--- + +使用卷軸時,交易中一個位元組的成本遠高於一個儲存時隙的成本。 因此,盡可能在鏈上快取資訊是合理的。 + +在本文中,您將學習如何創建和使用快取合約,讓任何可能被多次使用的參數值都被快取,並在(首次使用後)能以更少的位元組來取用,以及如何撰寫使用此快取的鏈外程式碼。 + +如果您想跳過文章,直接查看原始程式碼,[請點擊這裡](https://github.com/qbzzt/20220915-all-you-can-cache)。 開發堆疊為 [Foundry](https://getfoundry.sh/introduction/installation/)。 + +## 總體設計 {#overall-design} + +為求簡單,我們假設所有交易參數都是 `uint256`,長度為 32 位元組。 當我們收到一筆交易時,我們會像這樣解析每個參數: + +1. 如果第一個位元組是 `0xFF`,則將接下來的 32 個位元組作為參數值並寫入快取。 + +2. 如果第一個位元組是 `0xFE`,則將接下來的 32 個位元組作為參數值,但_不_寫入快取。 + +3. 對於任何其他值,將前四個位元作為附加位元組的數量,後四個位元作為快取鍵的最高有效位。 下面有些範例: + + | calldata 中的位元組 | 快取鍵 | + | :-------------- | -------: | + | 0x0F | 0x0F | + | 0x10,0x10 | 0x10 | + | 0x12,0xAC | 0x02AC | + | 0x2D,0xEA, 0xD6 | 0x0DEAD6 | + +## 快取操作 {#cache-manipulation} + +快取在 [`Cache.sol`](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/src/Cache.sol) 中實現。 讓我們逐行檢視它。 + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + + +contract Cache { + + bytes1 public constant INTO_CACHE = 0xFF; + bytes1 public constant DONT_CACHE = 0xFE; +``` + +這些常數用於解釋特殊情況,即我們提供所有資訊,並選擇是否要將其寫入快取。 寫入快取需要對先前未使用的儲存時隙進行兩次 [`SSTORE`](https://www.evm.codes/#55) 操作,每次成本為 22100 Gas,因此我們將其設為可選。 + +```solidity + + mapping(uint => uint) public val2key; +``` + +值與其鍵之間的 [映射](https://www.geeksforgeeks.org/solidity/solidity-mappings/)。 在您送出交易之前,此資訊是編碼值所必需的。 + +```solidity + // 位置 n 儲存鍵 n+1 的值,因為我們需要保留 + // 零作為「不在快取中」的標記。 + uint[] public key2val; +``` + +我們可以使用陣列來進行從鍵到值的映射,因為我們是自己指派鍵,而且為求簡單,我們會循序指派。 + +```solidity + function cacheRead(uint _key) public view returns (uint) { + require(_key <= key2val.length, "Reading uninitialize cache entry"); + return key2val[_key-1]; + } // cacheRead +``` + +從快取中讀取一個值。 + +```solidity + // 如果快取中尚無此值,則將其寫入 + // 設為 public 僅為了讓測試能運作 + function cacheWrite(uint _value) public returns (uint) { + // 如果該值已在快取中,則回傳目前的鍵 + if (val2key[_value] != 0) { + return val2key[_value]; + } +``` + +將相同的值多次放入快取中是沒有意義的。 如果值已經存在,只需回傳現有的鍵即可。 + +```solidity + // 因為 0xFE 是特殊情況,所以快取能容納的最大鍵 + // 是 0x0D 後面跟著 15 個 0xFF。如果快取長度已達 + // 這麼大,就失敗。 + // 1 2 3 4 5 6 7 8 9 A B C D E F + require(key2val.length+1 < 0x0DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + "cache overflow"); +``` + +我不認為我們會有這麼大的快取 (約 1.8\*1037 個項目,需要約 1027 TB 來儲存)。 然而,我的年紀足以記得 ["640kB 永遠夠用"](https://quoteinvestigator.com/2011/09/08/640k-enough/)這句話。 這個測試的成本非常低。 + +```solidity + // 使用下一個鍵寫入值 + val2key[_value] = key2val.length+1; +``` + +新增反向查找 (從值到鍵)。 + +```solidity + key2val.push(_value); +``` + +新增正向查找 (從鍵到值)。 因為我們是循序指派值,所以可以直接將其加在陣列最後一個值的後面。 + +```solidity + return key2val.length; + } // cacheWrite +``` + +回傳 `key2val` 的新長度,也就是新值儲存的儲存格。 + +```solidity + function _calldataVal(uint startByte, uint length) + private pure returns (uint) +``` + +此函數從 calldata 讀取任意長度 (最多 32 位元組,即一個字組的大小) 的值。 + +```solidity + { + uint _retVal; + + require(length < 0x21, + "_calldataVal length limit is 32 bytes"); + require(length + startByte <= msg.data.length, + "_calldataVal trying to read beyond calldatasize"); +``` + +這個函數是內部的,所以如果其餘的程式碼都撰寫正確,這些測試就不是必需的。 不過,它們的成本不高,所以不妨保留。 + +```solidity + assembly { + _retVal := calldataload(startByte) + } +``` + +此程式碼使用 [Yul](https://docs.soliditylang.org/en/v0.8.16/yul.html) 撰寫。 它從 calldata 讀取一個 32 位元組的值。 即使 calldata 在 `startByte+32` 之前就結束了,這段程式碼也能運作,因為 EVM 中未初始化的空間會被視為零。 + +```solidity + _retVal = _retVal >> (256-length*8); +``` + +我們不一定需要一個 32 位元組的值。 這會移除多餘的位元組。 + +```solidity + return _retVal; + } // _calldataVal + + + // 從 calldata 讀取單一參數,從 _fromByte 開始 + function _readParam(uint _fromByte) internal + returns (uint _nextByte, uint _parameterValue) + { +``` + +從 calldata 讀取單一參數。 請注意,我們不僅需要回傳讀取的值,還需要回傳下一個位元組的位置,因為參數的長度可能從 1 位元組到 33 位元組不等。 + +```solidity + // 第一個位元組告訴我們如何解釋其餘的部分 + uint8 _firstByte; + + _firstByte = uint8(_calldataVal(_fromByte, 1)); +``` + +Solidity 藉由禁止潛在危險的[隱含類型轉換](https://docs.soliditylang.org/en/v0.8.16/types.html#implicit-conversions),試圖減少錯誤的數量。 降級,例如從 256 位元降到 8 位元,需要明確指定。 + +```solidity + + // 讀取值,但不寫入快取 + if (_firstByte == uint8(DONT_CACHE)) + return(_fromByte+33, _calldataVal(_fromByte+1, 32)); + + // 讀取值,並將其寫入快取 + if (_firstByte == uint8(INTO_CACHE)) { + uint _param = _calldataVal(_fromByte+1, 32); + cacheWrite(_param); + return(_fromByte+33, _param); + } + + // 如果執行到這裡,表示我們需要從快取中讀取 + + // 要讀取的額外位元組數 + uint8 _extraBytes = _firstByte / 16; +``` + +取較低的[半位元組 (nibble)](https://en.wikipedia.org/wiki/Nibble) 並將其與其他位元組組合,以從快取中讀取值。 + +```solidity + uint _key = (uint256(_firstByte & 0x0F) << (8*_extraBytes)) + + _calldataVal(_fromByte+1, _extraBytes); + + return (_fromByte+_extraBytes+1, cacheRead(_key)); + + } // _readParam + + + // 讀取 n 個參數 (函數知道它們預期有多少個參數) + function _readParams(uint _paramNum) internal returns (uint[] memory) { +``` + +我們可以從 calldata 本身取得參數數量,但是呼叫我們的函數知道它們預期有多少個參數。 讓它們告訴我們比較容易。 + +```solidity + // 我們讀取的參數 + uint[] memory params = new uint[](_paramNum); + + // 參數從第 4 個位元組開始,之前是函數簽章 + uint _atByte = 4; + + for(uint i=0; i<_paramNum; i++) { + (_atByte, params[i]) = _readParam(_atByte); + } +``` + +讀取參數,直到您取得所需的數量。 如果我們超出 calldata 的結尾,`_readParams` 將會還原呼叫。 + +```solidity + + return(params); + } // readParams + + // 用於測試 _readParams,測試讀取四個參數 + function fourParam() public + returns (uint256,uint256,uint256,uint256) + { + uint[] memory params; + params = _readParams(4); + return (params[0], params[1], params[2], params[3]); + } // fourParam +``` + +Foundry 的一個大優點是它允許用 Solidity 撰寫測試 ([見下文的測試快取](#testing-the-cache))。 這讓單元測試變得容易得多。 這是一個讀取四個參數並回傳它們的函數,以便測試可以驗證它們是否正確。 + +```solidity + // 取得一個值,回傳將其編碼的位元組 (如果可能,使用快取) + function encodeVal(uint _val) public view returns(bytes memory) { +``` + +`encodeVal` 是一個由鏈外程式碼呼叫的函數,用來幫助創建使用快取的 calldata。 它接收單一值並回傳對其編碼的位元組。 此函數是 `view` 函數,所以不需要交易,且從外部呼叫時不消耗任何 Gas。 + +```solidity + uint _key = val2key[_val]; + + // 該值尚不在快取中,將其加入 + if (_key == 0) + return bytes.concat(INTO_CACHE, bytes32(_val)); +``` + +在 [EVM](/developers/docs/evm/) 中,所有未初始化的儲存空間都假設為零。 所以,如果我們查找一個不存在的值的鍵,我們會得到零。 在這種情況下,編碼它的位元組是 `INTO_CACHE` (這樣下次就會被快取),後面跟著實際的值。 + +```solidity + // 如果鍵 <0x10,則以單一位元組回傳 + if (_key < 0x10) + return bytes.concat(bytes1(uint8(_key))); +``` + +單一位元組是最簡單的。 我們只需使用 [`bytes.concat`](https://docs.soliditylang.org/en/v0.8.16/types.html#the-functions-bytes-concat-and-string-concat) 將 `bytes` 類型轉換為任意長度的位元組陣列。 儘管有這個名稱,但當只提供一個參數時,它也能正常運作。 + +```solidity + // 兩位元組值,編碼為 0x1vvv + if (_key < 0x1000) + return bytes.concat(bytes2(uint16(_key) | 0x1000)); +``` + +當我們的鍵小於 163 時,我們可以用兩個位元組來表示它。 我們首先將 256 位元值的 `_key` 轉換為 16 位元值,並使用邏輯「或」將額外位元組的數量加到第一個位元組上。 然後我們將其轉換為 `bytes2` 值,該值可以轉換為 `bytes`。 + +```solidity + // 可能有更聰明的方法以迴圈方式處理以下幾行, + // 但這是一個 view 函數,所以我為了節省程式員時間和簡化而進行優化。 + + if (_key < 16*256**2) + return bytes.concat(bytes3(uint24(_key) | (0x2 * 16 * 256**2))); + if (_key < 16*256**3) + return bytes.concat(bytes4(uint32(_key) | (0x3 * 16 * 256**3))); + . + . + . + if (_key < 16*256**14) + return bytes.concat(bytes15(uint120(_key) | (0xE * 16 * 256**14))); + if (_key < 16*256**15) + return bytes.concat(bytes16(uint128(_key) | (0xF * 16 * 256**15))); +``` + +其他值 (3 位元組、4 位元組等) 以相同的方式處理,只是欄位大小不同。 + +```solidity + // 如果執行到這裡,表示出了問題。 + revert("Error in encodeVal, should not happen"); +``` + +如果我們執行到這裡,表示我們得到了一個不小於 16\*25615 的鍵。 但是 `cacheWrite` 限制了鍵的範圍,所以我們甚至無法達到 14\*25616 (其第一個位元組會是 0xFE,看起來就像 `DONT_CACHE`)。 但是,為了防止未來的程式員引入錯誤,增加一個測試並不會花費太多成本。 + +```solidity + } // encodeVal + +} // Cache +``` + +### 測試快取 {#testing-the-cache} + +Foundry 的優點之一是 [它允許您用 Solidity 撰寫測試](https://getfoundry.sh/forge/tests/overview/),這讓撰寫單元測試變得更容易。 `Cache` 類別的測試在[這裡](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/test/Cache.t.sol)。 因為測試程式碼通常是重複的,所以本文只解釋有趣的部分。 + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + + +// 需要執行 `forge test -vv` 才能使用主控台。 +import "forge-std/console.sol"; +``` + +這只是使用測試套件和 `console.log` 所需的樣板程式碼。 + +```solidity +import "src/Cache.sol"; +``` + +我們需要知道我們正在測試的合約。 + +```solidity +contract CacheTest is Test { + Cache cache; + + function setUp() public { + cache = new Cache(); + } +``` + +`setUp` 函數在每次測試前被呼叫。 在這種情況下,我們只是創建一個新的快取,這樣我們的測試就不會互相影響。 + +```solidity + function testCaching() public { +``` + +測試是以 `test` 開頭的函數。 此函數檢查基本的快取功能,即寫入值並再次讀取它們。 + +```solidity + for(uint i=1; i<5000; i++) { + cache.cacheWrite(i*i); + } + + for(uint i=1; i<5000; i++) { + assertEq(cache.cacheRead(i), i*i); +``` + +這就是您如何使用 [`assert...` 函數](https://getfoundry.sh/reference/forge-std/std-assertions/) 進行實際測試的方式。 在這種情況下,我們檢查我們寫入的值是否與我們讀取的值相同。 我們可以捨棄 `cache.cacheWrite` 的結果,因為我們知道快取鍵是線性指派的。 + +```solidity + } + } // testCaching + + + // 將相同的值多次快取,確保鍵保持不變 + // the same + function testRepeatCaching() public { + for(uint i=1; i<100; i++) { + uint _key1 = cache.cacheWrite(i); + uint _key2 = cache.cacheWrite(i); + assertEq(_key1, _key2); + } +``` + +首先,我們將每個值寫入快取兩次,並確保鍵是相同的 (表示第二次寫入沒有真正發生)。 + +```solidity + for(uint i=1; i<100; i+=3) { + uint _key = cache.cacheWrite(i); + assertEq(_key, i); + } + } // testRepeatCaching +``` + +理論上,可能存在一個不影響連續快取寫入的錯誤。 所以這裡我們做一些非連續的寫入,看看值是否仍然沒有被重寫。 + +```solidity + // 從記憶體緩衝區讀取 uint (以確保我們取回我們送出的參數) + function toUint256(bytes memory _bytes, uint256 _start) internal pure + returns (uint256) +``` + +從 `bytes memory` 緩衝區讀取一個 256 位元字組。 這個實用功能快鍵讓我們可以驗證當我們執行使用快取的函數呼叫時,是否收到正確的結果。 + +```solidity + { + require(_bytes.length >= _start + 32, "toUint256_outOfBounds"); + uint256 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x20), _start)) + } +``` + +Yul 不支援 `uint256` 以外的數據結構,所以當您引用更複雜的數據結構時,例如記憶體緩衝區 `_bytes`,您會得到該結構的地址。 Solidity 將 `bytes memory` 值儲存為一個 32 位元組字組,其中包含長度,後面跟著實際的位元組,所以要取得位元組編號 `_start`,我們需要計算 `_bytes+32+_start`。 + +```solidity + + return tempUint; + } // toUint256 + + // fourParams() 的函數簽章,由 + // https://www.4byte.directory/signatures/?bytes4_signature=0x3edc1e6d 提供 + bytes4 constant FOUR_PARAMS = 0x3edc1e6d; + + // 只是一些常數值,用來查看我們是否取回正確的值 + uint256 constant VAL_A = 0xDEAD60A7; + uint256 constant VAL_B = 0xBEEF; + uint256 constant VAL_C = 0x600D; + uint256 constant VAL_D = 0x600D60A7; +``` + +一些我們測試所需的常數。 + +```solidity + function testReadParam() public { +``` + +呼叫 `fourParams()`,一個使用 `readParams` 的函數,來測試我們是否能正確讀取參數。 + +```solidity + address _cacheAddr = address(cache); + bool _success; + bytes memory _callInput; + bytes memory _callOutput; +``` + +我們不能使用正常的 ABI 機制來呼叫使用快取的函數,所以我們需要使用低階的 [`
.call()`](https://docs.soliditylang.org/en/v0.8.16/types.html#members-of-addresses) 機制。 該機制接受一個 `bytes memory` 作為輸入,並回傳它 (以及一個布林值) 作為輸出。 + +```solidity + // 第一次呼叫,快取是空的 + _callInput = bytes.concat( + FOUR_PARAMS, +``` + +讓同一個合約同時支援快取函數 (用於直接從交易呼叫) 和非快取函數 (用於從其他智能合約呼叫) 是很有用的。 為此,我們需要繼續依賴 Solidity 的機制來呼叫正確的函數,而不是將所有東西都放在[一個 `fallback` 函數](https://docs.soliditylang.org/en/v0.8.16/contracts.html#fallback-function)中。 這樣做使得可組合性變得容易得多。 在大多數情況下,單一位元組就足以識別函數,所以我們浪費了三個位元組 (16\*3=48 Gas)。 然而,在我寫這篇文章的時候,那 48 Gas 的成本是 0.07 美分,對於更簡單、更少錯誤的程式碼來說,這是一個合理的成本。 + +```solidity + // 第一個值,將它加入快取 + cache.INTO_CACHE(), + bytes32(VAL_A), +``` + +第一個值:一個旗標,表示這是一個需要寫入快取的完整值,後面跟著值的 32 個位元組。 其他三個值是相似的,除了 `VAL_B` 沒有寫入快取,而 `VAL_C` 既是第三個參數也是第四個參數。 + +```solidity + . + . + . + ); + (_success, _callOutput) = _cacheAddr.call(_callInput); +``` + +這就是我們實際呼叫 `Cache` 合約的地方。 + +```solidity + assertEq(_success, true); +``` + +我們預期呼叫會成功。 + +```solidity + assertEq(cache.cacheRead(1), VAL_A); + assertEq(cache.cacheRead(2), VAL_C); +``` + +我們從一個空的快取開始,然後加入 `VAL_A`,接著是 `VAL_C`。 我們預期第一個的鍵是 1,第二個是 2。 + +``` + assertEq(toUint256(_callOutput,0), VAL_A); + assertEq(toUint256(_callOutput,32), VAL_B); + assertEq(toUint256(_callOutput,64), VAL_C); + assertEq(toUint256(_callOutput,96), VAL_C); +``` + +輸出是四個參數。 這裡我們驗證它是正確的。 + +```solidity + // 第二次呼叫,我們可以使用快取 + _callInput = bytes.concat( + FOUR_PARAMS, + + // 快取中的第一個值 + bytes1(0x01), +``` + +小於 16 的快取鍵只有一個位元組。 + +```solidity + // 第二個值,不要將它加入快取 + cache.DONT_CACHE(), + bytes32(VAL_B), + + // 第三和第四個值,相同的值 + bytes1(0x02), + bytes1(0x02) + ); + . + . + . + } // testReadParam +``` + +呼叫後的測試與第一次呼叫後的測試相同。 + +```solidity + function testEncodeVal() public { +``` + +這個函數與 `testReadParam` 相似,只是我們不顯式地寫入參數,而是使用 `encodeVal()`。 + +```solidity + . + . + . + _callInput = bytes.concat( + FOUR_PARAMS, + cache.encodeVal(VAL_A), + cache.encodeVal(VAL_B), + cache.encodeVal(VAL_C), + cache.encodeVal(VAL_D) + ); + . + . + . + assertEq(_callInput.length, 4+1*4); + } // testEncodeVal +``` + +在 `testEncodeVal()` 中唯一的額外測試是驗證 `_callInput` 的長度是否正確。 對於第一次呼叫,它是 4+33\*4。 對於第二次,其中每個值都已在快取中,它是 4+1\*4。 + +```solidity + // 當鍵超過一個位元組時測試 encodeVal + // 最多三個位元組,因為填滿快取到四個位元組需要太長時間。 + function testEncodeValBig() public { + // 將一些值放入快取。 + // 為保持簡單,對值 n 使用鍵 n。 + for(uint i=1; i<0x1FFF; i++) { + cache.cacheWrite(i); + } +``` + +上面的 `testEncodeVal` 函數只向快取中寫入四個值,因此 [處理多位元組值的函數部分](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/src/Cache.sol#L144-L171) 沒有被檢查到。 但那段程式碼很複雜且容易出錯。 + +這個函數的第一部分是一個迴圈,它按順序將從 1 到 0x1FFF 的所有值寫入快取,這樣我們就能夠編碼這些值並知道它們的位置。 + +```solidity + . + . + . + + _callInput = bytes.concat( + FOUR_PARAMS, + cache.encodeVal(0x000F), // 一個位元組 0x0F + cache.encodeVal(0x0010), // 兩個位元組 0x1010 + cache.encodeVal(0x0100), // 兩個位元組 0x1100 + cache.encodeVal(0x1000) // 三個位元組 0x201000 + ); +``` + +測試一個位元組、兩個位元組和三個位元組的值。 我們沒有測試超過這個範圍,因為寫入足夠多的堆疊項目 (至少 0x10000000,大約二十五億) 會花費太長時間。 + +```solidity + . + . + . + . + } // testEncodeValBig + + + // 測試當緩衝區過小時,我們會得到一個 revert + function testShortCalldata() public { +``` + +測試在參數不足的異常情況下會發生什麼。 + +```solidity + . + . + . + (_success, _callOutput) = _cacheAddr.call(_callInput); + assertEq(_success, false); + } // testShortCalldata +``` + +由於它會還原,我們應該得到的結果是 `false`。 + +``` + // 使用不存在的快取鍵呼叫 + function testNoCacheKey() public { + . + . + . + _callInput = bytes.concat( + FOUR_PARAMS, + + // 第一個值,將它加入快取 + cache.INTO_CACHE(), + bytes32(VAL_A), + + // 第二個值 + bytes1(0x0F), + bytes2(0x1234), + bytes11(0xA10102030405060708090A) + ); +``` + +這個函數得到四個完全合法的參數,但快取是空的,所以沒有值可以讀取。 + +```solidity + . + . + . + // 測試當緩衝區過長時一切都能正常運作 + function testLongCalldata() public { + address _cacheAddr = address(cache); + bool _success; + bytes memory _callInput; + bytes memory _callOutput; + + // 第一次呼叫,快取是空的 + _callInput = bytes.concat( + FOUR_PARAMS, + + // 第一個值,將它加入快取 + cache.INTO_CACHE(), bytes32(VAL_A), + + // 第二個值,將它加入快取 + cache.INTO_CACHE(), bytes32(VAL_B), + + // 第三個值,將它加入快取 + cache.INTO_CACHE(), bytes32(VAL_C), + + // 第四個值,將它加入快取 + cache.INTO_CACHE(), bytes32(VAL_D), + + // 再加上一個值來「祝好運」 + bytes4(0x31112233) + ); +``` + +此函數傳送五個值。 我們知道第五個值被忽略了,因為它不是一個有效的快取項目,如果它被包含進去,就會導致還原。 + +```solidity + (_success, _callOutput) = _cacheAddr.call(_callInput); + assertEq(_success, true); + . + . + . + } // testLongCalldata + +} // CacheTest + +``` + +## 一個範例應用程式 {#a-sample-app} + +用 Solidity 寫測試固然很好,但終究去中心化應用程式需要能夠處理來自鏈外的請求才能派上用場。 本文示範如何在一個名為 `WORM` (意指「寫一次,讀多次」) 的去中心化應用程式中使用快取。 如果一個鍵尚未寫入,您可以將一個值寫入其中。 如果鍵已經寫入,您會得到一個還原。 + +### 合約 {#the-contract} + +[這是合約](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/src/WORM.sol)。 它大部分重複了我們已經用 `Cache` 和 `CacheTest` 做過的事情,所以我們只涵蓋有趣的部分。 + +```solidity +import "./Cache.sol"; + +contract WORM is Cache { +``` + +使用 `Cache` 最簡單的方法是在我們自己的合約中繼承它。 + +```solidity + function writeEntryCached() external { + uint[] memory params = _readParams(2); + writeEntry(params[0], params[1]); + } // writeEntryCached +``` + +這個函數與上面 `CacheTest` 中的 `fourParam` 相似。 因為我們不遵循 ABI 規範,最好不要在函數中聲明任何參數。 + +```solidity + // 讓我們更容易被呼叫 + // writeEntryCached() 的函數簽章,由 + // https://www.4byte.directory/signatures/?bytes4_signature=0xe4e4f2d3 提供 + bytes4 constant public WRITE_ENTRY_CACHED = 0xe4e4f2d3; +``` + +呼叫 `writeEntryCached` 的外部程式碼將需要手動建構 calldata,而不是使用 `worm.writeEntryCached`,因為我們不遵循 ABI 規範。 有這個常數值只是為了讓撰寫更容易。 + +請注意,即使我們將 `WRITE_ENTRY_CACHED` 定義為狀態變數,要從外部讀取它,也必須使用它的 getter 函數,即 `worm.WRITE_ENTRY_CACHED()`。 + +```solidity + function readEntry(uint key) public view + returns (uint _value, address _writtenBy, uint _writtenAtBlock) +``` + +讀取函數是 `view` 函數,所以它不需要交易,也不消耗 Gas。 因此,對參數使用快取沒有任何好處。 對於 view 函數,最好使用更簡單的標準機制。 + +### 測試程式碼 {#the-testing-code} + +[這是合約的測試程式碼](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/test/WORM.t.sol)。 同樣,我們只看有趣的部分。 + +```solidity + function testWReadWrite() public { + worm.writeEntry(0xDEAD, 0x60A7); + + vm.expectRevert(bytes("entry already written")); + worm.writeEntry(0xDEAD, 0xBEEF); +``` + +[這個 (`vm.expectRevert`)](https://book.getfoundry.sh/cheatcodes/expect-revert#expectrevert) 是我們在 Foundry 測試中指定下一個呼叫應該失敗,以及失敗報告原因的方式。 這適用於我們使用語法 `.()` 而不是建構 calldata 並使用低階介面 (`.call()` 等) 呼叫合約的情況。 + +```solidity + function testReadWriteCached() public { + uint cacheGoat = worm.cacheWrite(0x60A7); +``` + +這裡我們利用 `cacheWrite` 回傳快取鍵的特性。 這不是我們期望在生產環境中使用的,因為 `cacheWrite` 會改變狀態,因此只能在交易中呼叫。 交易沒有回傳值,如果它們有結果,這些結果應該以事件的形式發出。 所以 `cacheWrite` 的回傳值只能從鏈上程式碼存取,而鏈上程式碼不需要參數快取。 + +```solidity + (_success,) = address(worm).call(_callInput); +``` + +這就是我們告訴 Solidity,雖然 `.call()` 有兩個回傳值,但我們只關心第一個。 + +```solidity + (_success,) = address(worm).call(_callInput); + assertEq(_success, false); +``` + +因為我們使用低階的 `
.call()` 函數,所以不能使用 `vm.expectRevert()`,而必須查看從呼叫中得到的布林成功值。 + +```solidity + event EntryWritten(uint indexed key, uint indexed value); + + . + . + . + + _callInput = bytes.concat( + worm.WRITE_ENTRY_CACHED(), worm.encodeVal(a), worm.encodeVal(b)); + vm.expectEmit(true, true, false, false); + emit EntryWritten(a, b); + (_success,) = address(worm).call(_callInput); +``` + +這是在 Foundry 中驗證程式碼 [正確發出事件](https://getfoundry.sh/reference/cheatcodes/expect-emit/) 的方式。 + +### 用戶端 {#the-client} + +用 Solidity 測試得不到的一件事,就是可以複製貼上到您自己應用程式中的 JavaScript 程式碼。 為了撰寫這段程式碼,我將 WORM 部署到了 [Optimism Goerli](https://community.optimism.io/docs/useful-tools/networks/#optimism-goerli),這是 [Optimism](https://www.optimism.io/) 的新測試網。 它的地址是 [`0xd34335b1d818cee54e3323d3246bd31d94e6a78a`](https://goerli-optimism.etherscan.io/address/0xd34335b1d818cee54e3323d3246bd31d94e6a78a)。 + +[您可以在這裡看到用戶端的 JavaScript 程式碼](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/javascript/index.js)。 如何使用它: + +1. 複製 git 儲存庫: + + ```sh + git clone https://github.com/qbzzt/20220915-all-you-can-cache.git + ``` + +2. 安裝必要的套件: + + ```sh + cd javascript + yarn + ``` + +3. 複製設定檔: + + ```sh + cp .env.example .env + ``` + +4. 編輯 `.env` 以符合您的設定: + + | 參數 | 數值 | + | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | + | MNEMONIC | 一個擁有足夠 ETH 支付交易費用的帳戶的助記詞。 [您可以在這裡免費獲得 Optimism Goerli 網路的 ETH](https://optimismfaucet.xyz/)。 | + | OPTIMISM_GOERLI_URL | Optimism Goerli 的 URL。 公共端點 `https://goerli.optimism.io` 有速率限制,但足以滿足我們在這裡的需求 | + +5. 執行 `index.js`。 + + ```sh + node index.js + ``` + + 這個範例應用程式首先向 WORM 寫入一個項目,顯示 calldata 和 Etherscan 上交易的連結。 然後它會讀回該項目,並顯示它使用的鍵以及項目中的值 (值、區塊號和作者)。 + +用戶端大部分是正常的去中心化應用程式 JavaScript。 所以我們同樣只會看有趣的部分。 + +```javascript +. +. +. +const main = async () => { + const func = await worm.WRITE_ENTRY_CACHED() + + // 每次都需要一個新的鍵 + const key = await worm.encodeVal(Number(new Date())) +``` + +一個給定的時隙只能寫入一次,所以我們使用時間戳來確保我們不會重複使用時隙。 + +```javascript +const val = await worm.encodeVal("0x600D") + +// 寫入一個項目 +const calldata = func + key.slice(2) + val.slice(2) +``` + +Ethers 期望呼叫資料是一個十六進制字串,即 `0x` 後面跟著偶數個十六進制數字。 由於 `key` 和 `val` 都以 `0x` 開頭,我們需要移除這些標頭。 + +```javascript +const tx = await worm.populateTransaction.writeEntryCached() +tx.data = calldata + +sentTx = await wallet.sendTransaction(tx) +``` + +與 Solidity 測試程式碼一樣,我們不能正常呼叫快取函數。 相反,我們需要使用更低階的機制。 + +```javascript + . + . + . + // 讀取剛寫入的項目 + const realKey = '0x' + key.slice(4) // 移除 FF 旗標 + const entryRead = await worm.readEntry(realKey) + . + . + . +``` + +對於讀取項目,我們可以使用正常的機制。 對於 `view` 函數,不需要使用參數快取。 + +## 結論 {#conclusion} + +本文中的程式碼是一個概念驗證,目的是讓這個想法更容易理解。 對於一個生產就緒的系統,您可能需要實現一些額外的功能: + +- 處理非 `uint256` 的值。 例如,字串。 +- 與其使用全域快取,或許可以建立使用者與快取之間的映射。 不同的使用者使用不同的值。 +- 用於地址的值與用於其他目的的值是不同的。 單獨為地址建立一個快取可能是有意義的。 +- 目前,快取鍵採用「先到先得,鍵值最小」的演算法。 前十六個值可以作為單一位元組傳送。 接下來的 4080 個值可以作為兩個位元組傳送。 接下來大約一百萬個值是三個位元組,依此類推。 一個生產系統應該對快取項目保留使用計數器,並重新組織它們,以便十六個_最常用_的值是一個位元組,接下來的 4080 個最常用值是兩個位元組,依此類推。 + + 然而,這是一個潛在危險的操作。 想像以下事件序列: + + 1. 天真的諾姆 (Noam Naive) 呼叫 `encodeVal` 來編碼他想傳送代幣的地址。 該地址是應用程式上最早使用的地址之一,所以編碼後的值是 0x06。 這是一個 `view` 函數,不是一個交易,所以它只發生在諾姆和他使用的節點之間,沒有其他人知道。 + + 2. 擁有者歐文 (Owen Owner) 執行快取重新排序操作。 很少有人真正使用那個地址,所以它現在被編碼為 0x201122。 一個不同的值,1018,被指派為 0x06。 + + 3. 天真的諾姆將他的代幣傳送到 0x06。 它們被送到地址 `0x0000000000000000000000000de0b6b3a7640000`,而且由於沒有人知道該地址的私密金鑰,它們就卡在那裡了。 諾姆_非常不開心_。 + + 有辦法解決這個問題,以及在快取重新排序期間交易在記憶體池中的相關問題,但您必須意識到這一點。 + +我在這裡用 Optimism 來示範快取,因為我是 Optimism 的員工,這是我最了解的 rollup。 但它應該適用於任何對內部處理收取最低成本的 rollup,這樣相比之下,將交易資料寫入 L1 就成了主要開銷。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 + diff --git a/public/content/translations/zh-tw/developers/tutorials/app-plasma/index.md b/public/content/translations/zh-tw/developers/tutorials/app-plasma/index.md new file mode 100644 index 00000000000..6463ea6fdda --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/app-plasma/index.md @@ -0,0 +1,1255 @@ +--- +title: "編寫一個保護隱私的特定應用程式 plasma" +description: "在本使用教學中,我們將為存款建立一個半祕密的銀行。 此銀行為中心化元件;它知道每位使用者的餘額。 然而,此資訊不會儲存在鏈上。 銀行會改為張貼狀態的雜湊值。 每當交易發生時,銀行就會張貼新的雜湊值,以及證明其擁有將雜湊狀態變更為新狀態的簽署交易之零知識證明。 閱讀本使用教學後,您不僅將瞭解如何使用零知識證明,還會瞭解為何要使用以及如何安全地使用。" +author: Ori Pomerantz +tags: [ "零知識", "伺服器", "鏈下", "隱私" ] +skill: advanced +lang: zh-tw +published: 2025-10-15 +--- + +## 介紹 {#introduction} + +與 [rollups](/developers/docs/scaling/zk-rollups/) 相反,[plasma](/developers/docs/scaling/plasma) 使用以太坊主網來確保完整性,而非可用性。 在本文中,我們將編寫一個行為類似 plasma 的應用程式,由以太坊保證完整性 (無未經授權的變更),但不保證可用性 (中心化元件可能故障並停用整個系統)。 + +我們在此處編寫的應用程式是保留隱私權的銀行。 不同的地址擁有含餘額的帳戶,且可將資金 (ETH) 傳送至其他帳戶。 銀行會張貼狀態 (帳戶及其餘額) 和交易的雜湊值,但會將實際餘額保留在鏈下,以維持其隱私。 + +## 設計 {#design} + +這不是可供生產的系統,而是教學工具。 因此,它是基於幾個簡化的假設編寫的。 + +- 固定的帳戶池。 帳戶有特定數量,且每個帳戶都屬於預先決定的地址。 這使得系統更加簡單,因為在零知識證明中處理可變大小的資料結構很困難。 對於可供生產的系統,我們可以使用 [Merkle 根](/developers/tutorials/merkle-proofs-for-offline-data-integrity/) 作為狀態雜湊值,並為必要的餘額提供 Merkle 證明。 + +- 記憶體儲存。 在生產系統上,我們需要將所有帳戶餘額寫入磁碟,以在重新啟動時保留它們。 在此,即使資訊遺失也無妨。 + +- 僅限傳送。 生產系統需要一種將資產存入銀行並提款的方式。 但這裡的目的只是為了說明概念,所以此銀行僅限於傳送。 + +### 零知識證明 {#zero-knowledge-proofs} + +在基礎層面上,零知識證明顯示證明者知道一些資料 _Dataprivate_,使得一些公開資料 _Datapublic_ 和 _Dataprivate_ 之間存在關係 _Relationship_。 驗證者知道 _Relationship_ 和 _Datapublic_。 + +為了保護隱私,我們需要將狀態和交易設為私密。 但為了確保完整性,我們需要將狀態的 [密碼學雜湊值](https://en.wikipedia.org/wiki/Cryptographic_hash_function) 設為公開。 為了向提交交易的人證明這些交易確實發生了,我們還需要張貼交易雜湊值。 + +在大多數情況下,_Dataprivate_ 是零知識證明程式的輸入,而 _Datapublic_ 是輸出。 + +_Dataprivate_ 中的這些欄位: + +- _Staten_,舊的狀態 +- _Staten+1_,新的狀態 +- _Transaction_,將舊狀態變更為新狀態的交易。 此交易需要包含以下欄位: + - _目的地地址_,接收傳送的地址 + - 正在傳送的 _金額_ + - _Nonce_,確保每個交易只能處理一次。 + 來源地址不需要在交易中,因為它可以從簽章中恢復。 +- _簽章_,一個經授權執行交易的簽章。 在我們的案例中,唯一被授權執行交易的地址是來源地址。 由於我們的零知識系統的運作方式,除了以太坊簽章外,我們還需要帳戶的公鑰。 + +_Datapublic_ 中的這些欄位: + +- _Hash(Staten)_,舊狀態的雜湊值 +- _Hash(Staten+1)_,新狀態的雜湊值 +- _Hash(Transaction)_,將狀態從 _Staten_ 變更為 _Staten+1_ 的交易雜湊值。 + +此關係會檢查幾個條件: + +- 公開的雜湊值確實是私密欄位的正確雜湊值。 +- 交易應用於舊狀態時,會產生新狀態。 +- 簽章來自交易的來源地址。 + +由於密碼學雜湊函數的特性,證明這些條件就足以確保完整性。 + +### 數據結構 {#data-structures} + +主要資料結構是伺服器持有的狀態。 對於每個帳戶,伺服器都會追蹤帳戶餘額和一個 [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce),用於防止 [重放攻擊](https://en.wikipedia.org/wiki/Replay_attack)。 + +### 元件 {#components} + +此系統需要兩個元件: + +- _伺服器_,接收交易、處理交易,並將雜湊值與零知識證明一起發布到鏈上。 +- 一個 _智能合約_,儲存雜湊值並驗證零知識證明,以確保狀態轉換是合法的。 + +### 資料和控制流 {#flows} + +這些是各種元件之間溝通,以將資金從一個帳戶傳送到另一個帳戶的方式。 + +1. 網頁瀏覽器提交一份簽署的交易,要求從簽署者的帳戶傳送到另一個不同的帳戶。 + +2. 伺服器驗證該交易是否有效: + + - 簽署者在銀行中有一個餘額充足的帳戶。 + - 收款人在銀行中有一個帳戶。 + +3. 伺服器透過從簽署者的餘額中減去傳送的金額,並將其加到收款人的餘額中,來計算新的狀態。 + +4. 伺服器計算一個零知識證明,證明狀態變更是有效的。 + +5. 伺服器向以太坊提交一筆包含以下內容的交易: + + - 新狀態的雜湊值 + - 交易雜湊值 (以便交易發送者可以知道交易已處理) + - 證明轉換到新狀態是有效的零知識證明 + +6. 智能合約驗證零知識證明。 + +7. 如果零知識證明檢查通過,智能合約將執行以下操作: + - 將當前狀態雜湊值更新為新狀態雜湊值 + - 發出一個包含新狀態雜湊值和交易雜湊值的日誌項目 + +### 工具 {#tools} + +對於用戶端程式碼,我們將使用 [Vite](https://vite.dev/)、[React](https://react.dev/)、[Viem](https://viem.sh/) 和 [Wagmi](https://wagmi.sh/)。 這些是業界標準的工具;如果您不熟悉,可以使用[此教學](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/)。 + +伺服器的主要部分是使用 [Node](https://nodejs.org/en) 以 JavaScript 編寫的。 零知識部分是用 [Noir](https://noir-lang.org/) 編寫的。 我們需要 `1.0.0-beta.10` 版本,因此在您 [按照指示安裝 Noir](https://noir-lang.org/docs/getting_started/quick_start) 後,請執行: + +``` +noirup -v 1.0.0-beta.10 +``` + +我們使用的區塊鏈是 `anvil`,一個本地測試區塊鏈,是 [Foundry](https://getfoundry.sh/introduction/installation) 的一部分。 + +## 實作 {#implementation} + +由於這是一個複雜的系統,我們將分階段實作。 + +### 第 1 階段 - 手動零知識 {#stage-1} + +在第一階段,我們將在瀏覽器中簽署一筆交易,然後手動提供資訊給零知識證明。 零知識程式碼預期會在 `server/noir/Prover.toml` 中取得該資訊 (文件記錄於 [此處](https://noir-lang.org/docs/getting_started/project_breakdown#provertoml-1))。 + +實際操作如下: + +1. 請確保您已安裝 [Node](https://nodejs.org/en/download) 和 [Noir](https://noir-lang.org/install)。 最好在 UNIX 系統上安裝它們,例如 macOS、Linux 或 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install)。 + +2. 下載第 1 階段的程式碼並啟動網頁伺服器以提供用戶端程式碼。 + + ```sh + git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk + cd 250911-zk-bank + cd client + npm install + npm run dev + ``` + + 您需要網頁伺服器的原因是,為了防止某些類型的詐騙,許多錢包 (例如 MetaMask) 不接受直接從磁碟提供的檔案。 + +3. 打開帶有錢包的瀏覽器。 + +4. 在錢包中,輸入新的密碼。 請注意,這將刪除您現有的密碼,所以_請確保您有備份_。 + + 密碼是 `test test test test test test test test test test test junk`,這是 anvil 的預設測試密碼。 + +5. 瀏覽至 [用戶端程式碼](http://localhost:5173/)。 + +6. 連接到錢包並選擇您的目標帳戶和金額。 + +7. 按一下 **Sign** 並簽署交易。 + +8. 在 **Prover.toml** 標題下,您會找到文字。 將 `server/noir/Prover.toml` 替換為該文字。 + +9. 執行零知識證明。 + + ```sh + cd ../server/noir + nargo execute + ``` + + 輸出應類似於 + + ``` + ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute + + [zkBank] Circuit witness successfully solved + [zkBank] Witness saved to target/zkBank.gz + [zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85) + ``` + +10. 將最後兩個值與您在網頁瀏覽器上看到的雜湊值進行比較,以查看訊息是否已正確雜湊。 + +#### `server/noir/Prover.toml` {#server-noir-prover-toml} + +[此檔案](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/Prover.toml) 顯示 Noir 預期的資訊格式。 + +```toml +message="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0 " +``` + +此訊息採用文字格式,方便使用者理解 (這在簽署時是必要的),也方便 Noir 程式碼解析。 金額以 finney 報價,一方面可以進行部分傳送,另一方面也易於閱讀。 最後一個數字是 [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce)。 + +該字串長度為 100 個字元。 零知識證明不善於處理可變大小的資料,因此通常需要填充資料。 + +```toml +pubKeyX=["0x83",...,"0x75"] +pubKeyY=["0x35",...,"0xa5"] +signature=["0xb1",...,"0x0d"] +``` + +這三個參數是固定大小的位元組陣列。 + +```toml +[[accounts]] +address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +balance=100_000 +nonce=0 + +[[accounts]] +address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +balance=100_000 +nonce=0 +``` + +這是指定結構陣列的方式。 對於每個項目,我們指定地址、餘額 (以 milliETH,又稱為 [finney](https://cryptovalleyjournal.com/glossary/finney/)),以及下一個 nonce 值。 + +#### `client/src/Transfer.tsx` {#client-src-transfer-tsx} + +[此檔案](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/client/src/Transfer.tsx) 會實作用戶端的處理,並產生 `server/noir/Prover.toml` 檔案 (包含零知識參數的檔案)。 + +以下是較有趣部分的說明。 + +```tsx +export default attrs => { +``` + +此函式會建立 `Transfer` React 元件,其他檔案可以匯入此元件。 + +```tsx + const accounts = [ + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x90F79bf6EB2c4f870365E785982E1f101E93b906", + "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", + ] +``` + +這些是帳戶地址,也就是 `test ...` 建立的地址。 test junk` 密碼。 如果您想使用自己的地址,只需修改此定義即可。 + +```tsx + const account = useAccount() + const wallet = createWalletClient({ + transport: custom(window.ethereum!) + }) +``` + +這些 [Wagmi hooks](https://wagmi.sh/react/api/hooks) 讓我們能夠存取 [viem](https://viem.sh/) 庫和錢包。 + +```tsx + const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ") +``` + +這是以空格填補的訊息。 每當 [`useState`](https://react.dev/reference/react/useState) 變數變更時,元件就會重新繪製,而 `message` 也會更新。 + +```tsx + const sign = async () => { +``` + +此函式會在使用者按一下 **Sign** 按鈕時呼叫。 訊息會自動更新,但簽章需要使用者在錢包中核准,除非必要,否則我們不想要求核准。 + +```tsx + const signature = await wallet.signMessage({ + account: fromAccount, + message, + }) +``` + +要求錢包 [簽署訊息](https://viem.sh/docs/accounts/local/signMessage)。 + +```tsx + const hash = hashMessage(message) +``` + +取得訊息雜湊值。 提供給使用者以供偵錯 (Noir 程式碼) 會很有幫助。 + +```tsx + const pubKey = await recoverPublicKey({ + hash, + signature + }) +``` + +[取得公鑰](https://viem.sh/docs/utilities/recoverPublicKey)。 這是 [Noir `ecrecover`](https://github.com/colinnielsen/ecrecover-noir) 函式所需的。 + +```tsx + setSignature(signature) + setHash(hash) + setPubKey(pubKey) +``` + +設定狀態變數。 這樣做會在 `sign` 函式結束後重新繪製元件,並向使用者顯示更新後的值。 + +```tsx + let proverToml = `" +``` + +`Prover.toml` 的文字。 + +```tsx +message="${message}" + +pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))} +pubKeyY=${hexToArray(pubKey.slice(4+2*32))} +``` + +Viem 提供我們一個 65 位元組的十六進位字串作為公鑰。 第一個位元組是 `0x04`,一個版本標記。 接下來是 32 位元組的公鑰 `x`,然後是 32 位元組的公鑰 `y`。 + +然而,Noir 預期會以兩個位元組陣列的形式取得此資訊,一個用於 `x`,一個用於 `y`。 在用戶端解析比在零知識證明中解析更容易。 + +請注意,這通常是零知識中的良好作法。 零知識證明中的程式碼成本很高,因此任何可以在零知識證明之外完成的處理都_應該_在零知識證明之外完成。 + +```tsx +signature=${hexToArray(signature.slice(2,-2))} +``` + +簽章也以 65 位元組的十六進位字串提供。 然而,最後一個位元組僅用於恢復公鑰。 由於公鑰已經提供給 Noir 程式碼,我們不需要它來驗證簽章,Noir 程式碼也不需要它。 + +```tsx +${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")} +` +``` + +提供帳戶。 + +```tsx + setProverToml(proverToml) + } + + return ( + <> +

Transfer

+``` + +這是元件的 HTML (更準確地說,是 [JSX](https://react.dev/learn/writing-markup-with-jsx)) 格式。 + +#### `server/noir/src/main.nr` {#server-noir-src-main-nr} + +[此檔案](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/src/main.nr) 是實際的零知識程式碼。 + +``` +use std::hash::pedersen_hash; +``` + +[Pedersen 雜湊](https://rya-sge.github.io/access-denied/2024/05/07/pedersen-hash-function/) 由 [Noir 標準庫](https://noir-lang.org/docs/noir/standard_library/cryptographic_primitives/hashes#pedersen_hash) 提供。 零知識證明通常使用此雜湊函數。 與標準雜湊函數相比,在 [算術電路](https://rareskills.io/post/arithmetic-circuit) 內計算要容易得多。 + +``` +use keccak256::keccak256; +use dep::ecrecover; +``` + +這兩個函式是外部庫,定義在 [`Nargo.toml`](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/Nargo.toml) 中。 它們的名稱恰如其分,一個是計算 [keccak256 雜湊](https://emn178.github.io/online-tools/keccak_256.html) 的函式,另一個是驗證以太坊簽章並恢復簽署者的以太坊地址的函式。 + +``` +global ACCOUNT_NUMBER : u32 = 5; +``` + +Noir 的靈感來自 [Rust](https://www.rust-lang.org/)。 變數預設是常數。 這是我們定義全域組態常數的方式。 具體來說,`ACCOUNT_NUMBER` 是我們儲存的帳戶數量。 + +名為 `u` 的資料類型是該位元數的無符號整數。 唯一支援的類型是 `u8`、`u16`、`u32`、`u64` 和 `u128`。 + +``` +global FLAT_ACCOUNT_FIELDS : u32 = 2; +``` + +此變數用於帳戶的 Pedersen 雜湊,如下所述。 + +``` +global MESSAGE_LENGTH : u32 = 100; +``` + +如上所述,訊息長度是固定的。 在此處指定。 + +``` +global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30]; +global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH; +``` + +[EIP-191 簽章](https://eips.ethereum.org/EIPS/eip-191) 需要一個緩衝區,其中包含 26 位元組的前綴,後接 ASCII 格式的訊息長度,最後是訊息本身。 + +``` +struct Account { + balance: u128, + address: Field, + nonce: u32, +} +``` + +我們儲存的帳戶資訊。 [`Field`](https://noir-lang.org/docs/noir/concepts/data_types/fields) 是一個數字,通常最多 253 位元,可直接用於實作零知識證明的 [算術電路](https://rareskills.io/post/arithmetic-circuit)。 在此,我們使用 `Field` 來儲存 160 位元的以太坊地址。 + +``` +struct TransferTxn { + from: Field, + to: Field, + amount: u128, + nonce: u32 +} +``` + +我們儲存的傳送交易資訊。 + +``` +fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] { +``` + +函式定義。 參數是 `Account` 資訊。 結果是一個 `Field` 變數陣列,長度為 `FLAT_ACCOUNT_FIELDS` + +``` + let flat = [ + account.address, + ((account.balance << 32) + account.nonce.into()).into(), + ]; +``` + +陣列中的第一個值是帳戶地址。 第二個值包括餘額和 nonce。 `.into()` 呼叫會將數字變更為其所需的資料類型。 `account.nonce` 是一個 `u32` 值,但若要將其新增至 `account.balance « 32` (一個 `u128` 值),它需要是 `u128`。 這是第一個 `.into()`。 第二個 `.into()` 將 `u128` 結果轉換為 `Field`,使其符合陣列。 + +``` + flat +} +``` + +在 Noir 中,函式只能在結尾傳回一個值 (沒有提早傳回)。 若要指定傳回值,您只需在函式的右括號前評估它即可。 + +``` +fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] { +``` + +此函式將帳戶陣列轉換為 `Field` 陣列,可作為 Petersen 雜湊的輸入。 + +``` + let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER]; +``` + +這是指定可變變數的方式,也就是_不是_常數。 Noir 中的變數必須始終有值,因此我們將此變數初始化為全零。 + +``` + for i in 0..ACCOUNT_NUMBER { +``` + +這是 `for` 迴圈。 請注意,邊界是常數。 Noir 迴圈的邊界必須在編譯時已知。 原因是算術電路不支援流程控制。 在處理 `for` 迴圈時,編譯器會簡單地將其中的程式碼多次放置,每次迭代一次。 + +``` + let fields = flatten_account(accounts[i]); + for j in 0..FLAT_ACCOUNT_FIELDS { + flat[i*FLAT_ACCOUNT_FIELDS + j] = fields[j]; + } + } + + flat +} + +fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field { + pedersen_hash(flatten_accounts(accounts)) +} +``` + +最後,我們來到雜湊帳戶陣列的函式。 + +``` +fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 { + let mut account : u32 = ACCOUNT_NUMBER; + + for i in 0..ACCOUNT_NUMBER { + if accounts[i].address == address { + account = i; + } + } +``` + +此函式會尋找具有特定地址的帳戶。 此函式在標準程式碼中效率極低,因為它會迭代所有帳戶,即使在找到地址後也是如此。 + +然而,在零知識證明中,沒有流程控制。 如果我們需要檢查條件,我們必須每次都檢查它。 + +`if` 陳述式也會發生類似的情況。 上述迴圈中的 `if` 陳述式會轉換為這些數學陳述式。 + +_conditionresult = accounts[i].address == address_ // 如果相等則為 1,否則為 0 + +_accountnew = conditionresult\*i + (1-conditionresult)\*accountold_ + +```rust + assert (account < ACCOUNT_NUMBER, f"{address} does not have an account"); + + account +} +``` + +如果斷言為假,[`assert`](https://noir-lang.org/docs/dev/noir/concepts/assert) 函式會導致零知識證明崩潰。 在這種情況下,如果我們找不到具有相關地址的帳戶。 若要報告地址,我們使用 [格式字串](https://noir-lang.org/docs/noir/concepts/data_types/strings#format-strings)。 + +```rust +fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] { +``` + +此函式會套用一筆傳送交易,並傳回新的帳戶陣列。 + +```rust + let from = find_account(accounts, txn.from); + let to = find_account(accounts, txn.to); + + let (txnFrom, txnAmount, txnNonce, accountNonce) = + (txn.from, txn.amount, txn.nonce, accounts[from].nonce); +``` + +我們無法在 Noir 的格式字串內存取結構元素,因此我們建立了一個可用的副本。 + +```rust + assert (accounts[from].balance >= txn.amount, + f"{txnFrom} 沒有 {txnAmount} finney"); + + assert (accounts[from].nonce == txn.nonce, + f"交易的 nonce 為 {txnNonce},但帳戶應使用 {accountNonce}"); +``` + +這是兩個可能使交易無效的條件。 + +```rust + let mut newAccounts = accounts; + + newAccounts[from].balance -= txn.amount; + newAccounts[from].nonce += 1; + newAccounts[to].balance += txn.amount; + + newAccounts +} +``` + +建立新的帳戶陣列,然後傳回它。 + +```rust +fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field +``` + +此函式從訊息中讀取地址。 + +```rust +{ + let mut result : Field = 0; + + for i in 7..47 { +``` + +地址總是 20 位元組 (又稱為 40 個十六進位數字) 長,並從字元 #7 開始。 + +```rust + result *= 0x10; + if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9 + result += (messageBytes[i]-48).into(); + } + if messageBytes[i] >= 65 & messageBytes[i] <= 70 { // A-F + result += (messageBytes[i]-65+10).into() + } + if messageBytes[i] >= 97 & messageBytes[i] <= 102 { // a-f + result += (messageBytes[i]-97+10).into() + } + } + + result +} + +fn readAmountAndNonce(messageBytes: [u8; MESSAGE_LENGTH]) -> (u128, u32) +``` + +從訊息中讀取金額和 nonce。 + +```rust +{ + let mut amount : u128 = 0; + let mut nonce: u32 = 0; + let mut stillReadingAmount: bool = true; + let mut lookingForNonce: bool = false; + let mut stillReadingNonce: bool = false; +``` + +在訊息中,地址後的第一個數字是要傳送的 finney (又稱為 千分之一 ETH) 金額。 第二個數字是 nonce。 兩者之間的任何文字都會被忽略。 + +```rust + for i in 48..MESSAGE_LENGTH { + if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9 + let digit = (messageBytes[i]-48); + + if stillReadingAmount { + amount = amount*10 + digit.into(); + } + + if lookingForNonce { // We just found it + stillReadingNonce = true; + lookingForNonce = false; + } + + if stillReadingNonce { + nonce = nonce*10 + digit.into(); + } + } else { + if stillReadingAmount { + stillReadingAmount = false; + lookingForNonce = true; + } + if stillReadingNonce { + stillReadingNonce = false; + } + } + } + + (amount, nonce) +} +``` + +傳回 [元組](https://noir-lang.org/docs/noir/concepts/data_types/tuples) 是 Noir 從函式傳回多個值的方式。 + +```rust +fn readTransferTxn(message: str) -> TransferTxn +{ + let mut txn: TransferTxn = TransferTxn { from: 0, to: 0, amount:0, nonce:0 }; + let messageBytes = message.as_bytes(); + + txn.to = readAddress(messageBytes); + let (amount, nonce) = readAmountAndNonce(messageBytes); + txn.amount = amount; + txn.nonce = nonce; + + txn +} +``` + +此函式將訊息轉換為位元組,然後將金額轉換為 `TransferTxn`。 + +```rust +// The equivalent to Viem's hashMessage +// https://viem.sh/docs/utilities/hashMessage#hashmessage +fn hashMessage(message: str) -> [u8;32] { +``` + +我們能夠對帳戶使用 Pedersen 雜湊,因為它們只在零知識證明內雜湊。 然而,在此程式碼中,我們需要檢查訊息的簽章,這是由瀏覽器產生的。 為此,我們需要遵循 [EIP 191](https://eips.ethereum.org/EIPS/eip-191) 中的以太坊簽署格式。 這表示我們需要建立一個組合緩衝區,其中包含標準前綴、ASCII 格式的訊息長度以及訊息本身,並使用以太坊標準 keccak256 進行雜湊。 + +```rust + // ASCII prefix + let prefix_bytes = [ + 0x19, // \x19 + 0x45, // 'E' + 0x74, // 't' + 0x68, // 'h' + 0x65, // 'e' + 0x72, // 'r' + 0x65, // 'e' + 0x75, // 'u' + 0x6D, // 'm' + 0x20, // ' ' + 0x53, // 'S' + 0x69, // 'i' + 0x67, // 'g' + 0x6E, // 'n' + 0x65, // 'e' + 0x64, // 'd' + 0x20, // ' ' + 0x4D, // 'M' + 0x65, // 'e' + 0x73, // 's' + 0x73, // 's' + 0x61, // 'a' + 0x67, // 'g' + 0x65, // 'e' + 0x3A, // ':' + 0x0A // '\n' + ]; +``` + +為避免應用程式要求使用者簽署可用作交易或其他用途的訊息,EIP 191 指定所有簽署的訊息都以字元 0x19 (非有效 ASCII 字元) 開頭,後接 `Ethereum Signed Message:` 和換行符。 + +```rust + let mut buffer: [u8; HASH_BUFFER_SIZE] = [0u8; HASH_BUFFER_SIZE]; + for i in 0..26 { + buffer[i] = prefix_bytes[i]; + } + + let messageBytes : [u8; MESSAGE_LENGTH] = message.as_bytes(); + + if MESSAGE_LENGTH <= 9 { + for i in 0..1 { + buffer[i+26] = ASCII_MESSAGE_LENGTH[i]; + } + + for i in 0..MESSAGE_LENGTH { + buffer[i+26+1] = messageBytes[i]; + } + } + + if MESSAGE_LENGTH >= 10 & MESSAGE_LENGTH <= 99 { + for i in 0..2 { + buffer[i+26] = ASCII_MESSAGE_LENGTH[i]; + } + + for i in 0..MESSAGE_LENGTH { + buffer[i+26+2] = messageBytes[i]; + } + } + + if MESSAGE_LENGTH >= 100 { + for i in 0..3 { + buffer[i+26] = ASCII_MESSAGE_LENGTH[i]; + } + + for i in 0..MESSAGE_LENGTH { + buffer[i+26+3] = messageBytes[i]; + } + } + + assert(MESSAGE_LENGTH < 1000, "不支援長度超過三位數的訊息"); +``` + +處理長度達 999 的訊息,如果超過則失敗。 我新增了這段程式碼,即使訊息長度是常數,因為這樣更容易變更。 在生產系統上,為了更好的效能,您可能會假設 `MESSAGE_LENGTH` 不會變更。 + +```rust + keccak256::keccak256(buffer, HASH_BUFFER_SIZE) +} +``` + +使用以太坊標準的 `keccak256` 函式。 + +```rust +fn signatureToAddressAndHash( + message: str, + pubKeyX: [u8; 32], + pubKeyY: [u8; 32], + signature: [u8; 64] + ) -> (Field, Field, Field) // address, first 16 bytes of hash, last 16 bytes of hash +{ +``` + +此函式會驗證簽章,這需要訊息雜湊值。 然後,它會提供我們簽署它的地址和訊息雜湊值。 訊息雜湊值以兩個 `Field` 值提供,因為它們比位元組陣列在程式的其餘部分更容易使用。 + +我們需要使用兩個 `Field` 值,因為欄位計算是使用 [模數](https://en.wikipedia.org/wiki/Modulo) 一個大數來完成的,但該數字通常小於 256 位元 (否則在 EVM 中執行這些計算會很困難)。 + +```rust + let hash = hashMessage(message); + + let mut (hash1, hash2) = (0,0); + + for i in 0..16 { + hash1 = hash1*256 + hash[31-i].into(); + hash2 = hash2*256 + hash[15-i].into(); + } +``` + +將 `hash1` 和 `hash2` 指定為可變變數,並逐位元組將雜湊寫入其中。 + +```rust + ( + ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash), +``` + +這類似於 [Solidity 的 `ecrecover`](https://docs.soliditylang.org/en/v0.8.30/cheatsheet.html#mathematical-and-cryptographic-functions),但有兩個重要的差異: + +- 如果簽章無效,呼叫會失敗一個 `assert`,程式會被中止。 +- 雖然公鑰可以從簽章和雜湊中恢復,但這項處理可以在外部完成,因此不值得在零知識證明內進行。 如果有人在此試圖欺騙我們,簽章驗證將會失敗。 + +```rust + hash1, + hash2 + ) +} + +fn main( + accounts: [Account; ACCOUNT_NUMBER], + message: str, + pubKeyX: [u8; 32], + pubKeyY: [u8; 32], + signature: [u8; 64], + ) -> pub ( + Field, // Hash of old accounts array + Field, // Hash of new accounts array + Field, // First 16 bytes of message hash + Field, // Last 16 bytes of message hash + ) +``` + +最後,我們來到 `main` 函式。 我們需要證明我們有一個交易,該交易有效地將帳戶的雜湊從舊值變更為新值。 我們還需要證明它具有此特定的交易雜湊,以便發送它的人知道他們的交易已處理。 + +```rust +{ + let mut txn = readTransferTxn(message); +``` + +我們需要 `txn` 是可變的,因為我們不是從訊息中讀取 `from` 地址,而是從簽章中讀取。 + +```rust + let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash( + message, + pubKeyX, + pubKeyY, + signature); + + txn.from = fromAddress; + + let newAccounts = apply_transfer_txn(accounts, txn); + + ( + hash_accounts(accounts), + hash_accounts(newAccounts), + txnHash1, + txnHash2 + ) +} +``` + +### 第 2 階段 - 新增伺服器 {#stage-2} + +在第二階段,我們新增一個伺服器,接收並實作來自瀏覽器的傳送交易。 + +實際操作如下: + +1. 如果 Vite 正在執行,請停止它。 + +2. 下載包含伺服器的分支,並確保您擁有所有必要的模組。 + + ```sh + git checkout 02-add-server + cd client + npm install + cd ../server + npm install + ``` + + 無需編譯 Noir 程式碼,它與您用於第 1 階段的程式碼相同。 + +3. 啟動伺服器。 + + ```sh + npm run start + ``` + +4. 在個別的命令列視窗中,執行 Vite 以提供瀏覽器程式碼。 + + ```sh + cd client + npm run dev + ``` + +5. 瀏覽至 [http://localhost:5173](http://localhost:5173) 的用戶端程式碼 + +6. 在發出交易之前,您需要知道 nonce 以及可以傳送的金額。 若要取得此資訊,請按一下 **Update account data** 並簽署訊息。 + + 我們在此遇到一個兩難的局面。 一方面,我們不希望簽署可重複使用的訊息 ([重放攻擊](https://en.wikipedia.org/wiki/Replay_attack)),這就是我們一開始需要 nonce 的原因。 然而,我們還沒有 nonce。 解決方案是選擇一個只能使用一次且雙方都已有的 nonce,例如目前時間。 + + 此解決方案的問題是時間可能無法完全同步。 因此,我們改為簽署一個每分鐘變更一次的值。 這表示我們遭受重放攻擊的漏洞窗口最多為一分鐘。 考量到在生產環境中,已簽署的請求將受到 TLS 保護,而且通道的另一端—伺服器—已經可以揭露餘額和 nonce (它必須知道這些才能運作),這是一個可接受的風險。 + +7. 瀏覽器取回餘額和 nonce 後,會顯示傳送表單。 選擇目的地地址和金額,然後按一下 **Transfer**。 簽署此請求。 + +8. 若要查看傳送,請 **更新帳戶資料** 或查看您執行伺服器的視窗。 伺服器每次變更狀態時都會記錄狀態。 + + ``` + ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start + + > server@1.0.0 start + > node --experimental-json-modules index.mjs + + Listening on port 3000 + Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 processed + New state: + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 64000 (1) + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 100000 (0) + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) + 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) + 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) + Txn send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 7200 finney (milliEth) 1 processed + New state: + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 56800 (2) + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) + 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) + 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) + Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 3000 finney (milliEth) 2 processed + New state: + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 53800 (3) + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) + 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 139000 (0) + 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) + ``` + +#### `server/index.mjs` {#server-index-mjs-1} + +[此檔案](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/index.mjs) 包含伺服器程序,並與 [`main.nr`](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/noir/src/main.nr) 的 Noir 程式碼互動。 以下是有趣部分的說明。 + +```js +import { Noir } from '@noir-lang/noir_js' +``` + +[noir.js](https://www.npmjs.com/package/@noir-lang/noir_js) 庫在 JavaScript 程式碼和 Noir 程式碼之間提供介面。 + +```js +const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json")) +const noir = new Noir(circuit) +``` + +載入算術電路—我們在上一階段建立的已編譯 Noir 程式—並準備執行它。 + +```js +// We only provide account information in return to a signed request +const accountInformation = async signature => { + const fromAddress = await recoverAddress({ + hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)), + signature + }) +``` + +若要提供帳戶資訊,我們只需要簽章。 原因是我們已經知道訊息會是什麼,因此也知道訊息雜湊值。 + +```js +const processMessage = async (message, signature) => { +``` + +處理訊息並執行其編碼的交易。 + +```js + // Get the public key + const pubKey = await recoverPublicKey({ + hash, + signature + }) +``` + +現在我們在伺服器上執行 JavaScript,我們可以在那裡而不是在用戶端上擷取公鑰。 + +```js + let noirResult + try { + noirResult = await noir.execute({ + message, + signature: signature.slice(2,-2).match(/.{2}/g).map(x => `0x${x}`), + pubKeyX, + pubKeyY, + accounts: Accounts + }) +``` + +`noir.execute` 會執行 Noir 程式。 參數相當於 [`Prover.toml`](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/Prover.toml) 中提供的參數。 請注意,長值是作為十六進位字串陣列 (`["0x60", "0xA7"]`) 提供的,而不是像 Viem 那樣作為單一十六進位值 (`0x60A7`)。 + +```js + } catch (err) { + console.log(`Noir 錯誤:${err}`) + throw Error("交易無效,未處理") + } +``` + +如果有錯誤,請將其攔截,然後將簡化版本轉送給用戶端。 + +```js + Accounts[fromAccountNumber].nonce++ + Accounts[fromAccountNumber].balance -= amount + Accounts[toAccountNumber].balance += amount +``` + +套用交易。 我們已經在 Noir 程式碼中完成了,但在這裡再做一次比從那裡擷取結果更容易。 + +```js +let Accounts = [ + { + address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + balance: 5000, + nonce: 0, + }, +``` + +初始 `Accounts` 結構。 + +### 第 3 階段 - 以太坊智能合約 {#stage-3} + +1. 停止伺服器和用戶端程序。 + +2. 下載包含智能合約的分支,並確保您擁有所有必要的模組。 + + ```sh + git checkout 03-smart-contracts + cd client + npm install + cd ../server + npm install + ``` + +3. 在個別的命令列視窗中執行 `anvil`。 + +4. 產生驗證金鑰和 solidity 驗證器,然後將驗證器程式碼複製到 Solidity 專案。 + + ```sh + cd noir + bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak + bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol + cp target/Verifier.sol ../../smart-contracts/src + ``` + +5. 前往智能合約並設定環境變數以使用 `anvil` 區塊鏈。 + + ```sh + cd ../../smart-contracts + export ETH_RPC_URL=http://localhost:8545 + ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + ``` + +6. 部署 `Verifier.sol` 並將地址儲存在環境變數中。 + + ```sh + VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'` + echo $VERIFIER_ADDRESS + ``` + +7. 部署 `ZkBank` 合約。 + + ```sh + ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'` + echo $ZKBANK_ADDRESS + ``` + + `0x199..67b` 值是 `Accounts` 初始狀態的 Pederson 雜湊。 如果您在 `server/index.mjs` 中修改此初始狀態,您可以執行一個交易,以查看零知識證明報告的初始雜湊。 + +8. 執行伺服器。 + + ```sh + cd ../server + npm run start + ``` + +9. 在不同的命令列視窗中執行用戶端。 + + ```sh + cd client + npm run dev + ``` + +10. 執行一些交易。 + +11. 若要驗證狀態已在鏈上變更,請重新啟動伺服器程序。 查看 `ZkBank` 是否不再接受交易,因為交易中的原始雜湊值與鏈上儲存的雜湊值不同。 + + 這是預期的錯誤類型。 + + ``` + ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start + + > server@1.0.0 start + > node --experimental-json-modules index.mjs + + 正在接聽連接埠 3000 + 驗證錯誤:ContractFunctionExecutionError: 合約函數 "processTransaction" 因以下原因而還原: + 舊狀態雜湊值錯誤 + + 合約呼叫: + 地址: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 + 函數: processTransaction(bytes _proof, bytes32[] _publicInputs) + 參數: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000 + ``` + +#### `server/index.mjs` {#server-index-mjs-2} + +此檔案中的變更主要與建立實際證明並在鏈上提交有關。 + +```js +import { exec } from 'child_process' +import util from 'util' + +const execPromise = util.promisify(exec) +``` + +我們需要使用 [Barretenberg 套件](https://github.com/AztecProtocol/aztec-packages/tree/next/barretenberg) 來建立要傳送到鏈上的實際證明。 我們可以使用此套件,方法是執行命令列介面 (`bb`) 或使用 [JavaScript 庫 `bb.js`](https://www.npmjs.com/package/@aztec/bb.js)。 JavaScript 庫比原生執行程式碼慢得多,因此我們在此使用 [`exec`](https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback) 來使用命令列。 + +請注意,如果您決定使用 `bb.js`,您需要使用與您正在使用的 Noir 版本相容的版本。 在撰寫本文時,目前的 Noir 版本 (1.0.0-beta.11) 使用 `bb.js` 版本 0.87。 + +```js +const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" +``` + +此處的地址是您從乾淨的 `anvil` 開始並遵循上述說明時取得的地址。 + +```js +const walletClient = createWalletClient({ + chain: anvil, + transport: http(), + account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6") +}) +``` + +此私密金鑰是 `anvil` 中預先資助的預設帳戶之一。 + +```js +const generateProof = async (witness, fileID) => { +``` + +使用 `bb` 可執行檔產生證明。 + +```js + const fname = `witness-${fileID}.gz` + await fs.writeFile(fname, witness) +``` + +將見證寫入檔案。 + +```js + await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`) +``` + +實際建立證明。 此步驟也會建立一個包含公開變數的檔案,但我們不需要該檔案。 我們已經從 `noir.execute` 取得這些變數。 + +```js + const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "") +``` + +證明是一個 JSON 陣列,其中包含 `Field` 值,每個值都以十六進位值表示。 然而,我們需要將它作為單一的 `bytes` 值在交易中傳送,Viem 會以一個大的十六進位字串表示。 在此,我們透過串接所有值、移除所有 `0x`,然後在結尾加上一個來變更格式。 + +```js + await execPromise(`rm -r ${fname} ${fileID}`) + + return proof +} +``` + +清理並傳回證明。 + +```js +const processMessage = async (message, signature) => { + . + . + . + + const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0")) +``` + +公開欄位需要是 32 位元組值的陣列。 然而,由於我們需要將交易雜湊值分割到兩個 `Field` 值之間,因此它顯示為 16 位元組值。 在此,我們加上零,讓 Viem 了解它實際上是 32 位元組。 + +```js + const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`) +``` + +每個地址只會使用每個 nonce 一次,因此我們可以使用 `fromAddress` 和 `nonce` 的組合作為見證檔案和輸出目錄的唯一識別碼。 + +```js + try { + await zkBank.write.processTransaction([ + proof, publicFields]) + } catch (err) { + console.log(`驗證錯誤:${err}`) + throw Error("無法在鏈上驗證交易") + } + . + . + . +} +``` + +將交易傳送到鏈上。 + +#### `smart-contracts/src/ZkBank.sol` {#smart-contracts-src-zkbank-sol} + +這是接收交易的鏈上程式碼。 + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.21; + +import {HonkVerifier} from "./Verifier.sol"; + +contract ZkBank { + HonkVerifier immutable myVerifier; + bytes32 currentStateHash; + + constructor(address _verifierAddress, bytes32 _initialStateHash) { + currentStateHash = _initialStateHash; + myVerifier = HonkVerifier(_verifierAddress); + } +``` + +鏈上程式碼需要追蹤兩個變數:驗證器 (由 `nargo` 建立的獨立合約) 和目前的狀態雜湊。 + +```solidity + event TransactionProcessed( + bytes32 indexed transactionHash, + bytes32 oldStateHash, + bytes32 newStateHash + ); +``` + +每當狀態變更時,我們就會發出 `TransactionProcessed` 事件。 + +```solidity + function processTransaction( + bytes calldata _proof, + bytes32[] calldata _publicFields + ) public { +``` + +此函式會處理交易。 它會以驗證器所需的格式取得證明 (作為 `bytes`) 和公開輸入 (作為 `bytes32` 陣列),以最小化鏈上處理並因此降低 gas 成本。 + +```solidity + require(_publicInputs[0] == currentStateHash, + "舊狀態雜湊值錯誤"); +``` + +零知識證明需要證明交易從我們目前的雜湊變更為新的雜湊。 + +```solidity + myVerifier.verify(_proof, _publicFields); +``` + +呼叫驗證器合約以驗證零知識證明。 如果零知識證明錯誤,此步驟會還原交易。 + +```solidity + currentStateHash = _publicFields[1]; + + emit TransactionProcessed( + _publicFields[2]<<128 | _publicFields[3], + _publicFields[0], + _publicFields[1] + ); + } +} +``` + +如果一切都檢查無誤,請將狀態雜湊更新為新值,並發出 `TransactionProcessed` 事件。 + +## 中心化元件的濫用 {#abuses} + +資訊安全包含三個屬性: + +- _機密性_,使用者無法讀取他們未經授權讀取的資訊。 +- _完整性_,資訊不能被授權使用者以外的人以未經授權的方式變更。 +- _可用性_,授權使用者可以使用系統。 + +在此系統上,完整性是透過零知識證明提供的。 可用性更難保證,而機密性則不可能,因為銀行必須知道每個帳戶的餘額和所有交易。 無法阻止擁有資訊的實體分享該資訊。 + +也許可以使用 [隱身地址](https://vitalik.eth.limo/general/2023/01/20/stealth.html) 建立一個真正機密的銀行,但這超出了本文的範圍。 + +### 不實資訊 {#false-info} + +伺服器違反完整性的一種方式是在 [要求資料](https://github.com/qbzzt/250911-zk-bank/blob/03-smart-contracts/server/index.mjs#L278-L291) 時提供不實資訊。 + +為了解決這個問題,我們可以編寫第二個 Noir 程式,該程式接收帳戶作為私密輸入,並接收要求資訊的地址作為公開輸入。 輸出是該地址的餘額和 nonce,以及帳戶的雜湊。 + +當然,此證明無法在鏈上驗證,因為我們不想在鏈上張貼 nonce 和餘額。 然而,它可以由在瀏覽器中執行的用戶端程式碼來驗證。 + +### 強制交易 {#forced-txns} + +確保 L2 可用性和防止審查的通常機制是 [強制交易](https://docs.optimism.io/stack/transactions/forced-transaction)。 但強制交易與零知識證明不相容。 伺服器是唯一可以驗證交易的實體。 + +我們可以修改 `smart-contracts/src/ZkBank.sol` 以接受強制交易,並防止伺服器在處理它們之前變更狀態。 然而,這會讓我們面臨簡單的阻斷服務攻擊。 如果強制交易無效且因此無法處理,該怎麼辦? + +解決方案是擁有一個零知識證明,證明強制交易是無效的。 這給伺服器三個選項: + +- 處理強制交易,提供一個零知識證明,證明它已處理,並提供新的狀態雜湊。 +- 拒絕強制交易,並向合約提供一個零知識證明,證明該交易無效 (未知地址、錯誤的 nonce 或餘額不足)。 +- 忽略強制交易。 無法強制伺服器實際處理交易,但這表示整個系統都不可用。 + +#### 可用性保證金 {#avail-bonds} + +在實際的實作中,可能會有某種獲利動機來維持伺服器運作。 我們可以透過讓伺服器發布可用性保證金來加強此誘因,如果強制交易未在特定時間內處理,任何人都可以銷毀該保證金。 + +### 不良的 Noir 程式碼 {#bad-noir-code} + +通常,為了讓大家信任智能合約,我們會將原始程式碼上傳到 [區塊瀏覽器](https://eth.blockscout.com/address/0x7D16d2c4e96BCFC8f815E15b771aC847EcbDB48b?tab=contract)。 然而,在零知識證明的情況下,這是不夠的。 + +`Verifier.sol` 包含驗證金鑰,這是 Noir 程式的一個函數。 然而,該金鑰並未告訴我們 Noir 程式是什麼。 為了真正擁有一個可信賴的解決方案,您需要上傳 Noir 程式 (以及建立它的版本)。 否則,零知識證明可能反映出不同的程式,一個有後門的程式。 + +在區塊瀏覽器允許我們上傳和驗證 Noir 程式之前,您應該自己動手 (最好上傳到 [IPFS](/developers/tutorials/ipfs-decentralized-ui/))。 然後,有經驗的使用者將能夠下載原始程式碼,自己編譯,建立 `Verifier.sol`,並驗證它是否與鏈上的版本相同。 + +## 結論 {#conclusion} + +Plasma 類型的應用程式需要一個中心化元件作為資訊儲存。 這會帶來潛在的漏洞,但作為回報,它讓我們能夠以區塊鏈本身無法提供的方式保護隱私。 透過零知識證明,我們可以確保完整性,並可能使執行中心化元件的人在維持可用性方面具有經濟優勢。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 + +## 致謝 {#acknowledgements} + +- Josh Crites 閱讀了本文的草稿,並幫助我解決了一個棘手的 Noir 問題。 + +任何剩餘的錯誤都由我負責。 diff --git a/public/content/translations/zh-tw/developers/tutorials/calling-a-smart-contract-from-javascript/index.md b/public/content/translations/zh-tw/developers/tutorials/calling-a-smart-contract-from-javascript/index.md new file mode 100644 index 00000000000..171f803566a --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/calling-a-smart-contract-from-javascript/index.md @@ -0,0 +1,131 @@ +--- +title: "從 JavaScript 呼叫智能合約" +description: "如何使用 JavaScript 呼叫智能合約函式:以 Dai 代幣為例" +author: jdourlens +tags: [ "交易", "前端", "JavaScript", "web3.js" ] +skill: beginner +lang: zh-tw +published: 2020-04-19 +source: EthereumDev +sourceUrl: https://ethereumdev.io/calling-a-smart-contract-from-javascript/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +在本教學中,我們將了解如何從 JavaScript 呼叫[智能合約](/developers/docs/smart-contracts/)函式。 首先,我們會讀取智能合約的狀態 (例如 ERC20 持有者的餘額),然後透過代幣傳送來修改區塊鏈的狀態。 您應該已經熟悉如何[設定 JS 環境來與區塊鏈互動](/developers/tutorials/set-up-web3js-to-use-ethereum-in-javascript/)。 + +在本範例中,我們將使用 DAI 代幣。為了測試,我們將使用 ganache-cli 來分叉區塊鏈,並解鎖一個已持有大量 DAI 的地址: + +```bash +ganache-cli -f https://mainnet.infura.io/v3/[您的 INFURA 金鑰] -d -i 66 1 --unlock 0x4d10ae710Bd8D1C31bd7465c8CBC3add6F279E81 +``` + +要與智能合約互動,我們需要它的地址和 ABI: + +```js +const ERC20TransferABI = [ + { + constant: false, + inputs: [ + { + name: "_to", + type: "address", + }, + { + name: "_value", + type: "uint256", + }, + ], + name: "transfer", + outputs: [ + { + name: "", + type: "bool", + }, + ], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [ + { + name: "_owner", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + name: "balance", + type: "uint256", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, +] + +const DAI_ADDRESS = "0x6b175474e89094c44da98b954eedeac495271d0f" +``` + +在這個專案中,我們刪減了完整的 ERC20 ABI,只保留 `balanceOf` 和 `transfer` 函式,但您可以在[此處找到完整的 ERC20 ABI](https://ethereumdev.io/abi-for-erc20-contract-on-ethereum/)。 + +接著我們需要實例化我們的智能合約: + +```js +const web3 = new Web3("http://localhost:8545") + +const daiToken = new web3.eth.Contract(ERC20TransferABI, DAI_ADDRESS) +``` + +我們也將設定兩個地址: + +- 接收傳送的地址,以及 +- 我們已解鎖、將用來傳送的地址: + +```js +const senderAddress = "0x4d10ae710Bd8D1C31bd7465c8CBC3add6F279E81" +const receiverAddress = "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +``` + +在下個部分,我們將呼叫 `balanceOf` 函式,以擷取兩個地址目前持有的代幣數量。 + +## 呼叫:從智能合約讀取數值 {#call-reading-value-from-a-smart-contract} + +第一個範例將呼叫一個「常數」(constant) 方法,並在以太坊虛擬機 (EVM) 中執行其智能合約方法,而不會傳送任何交易。 為此,我們將讀取一個地址的 ERC20 餘額。 [閱讀我們關於 ERC20 代幣的文章](/developers/tutorials/understand-the-erc-20-token-smart-contract/)。 + +您可以存取已實例化的智能合約方法,只要您已提供其 ABI,方式如下:`yourContract.methods.methodname`。 使用 `call` 函式,您將會收到執行函式的結果。 + +```js +daiToken.methods.balanceOf(senderAddress).call(function (err, res) { + if (err) { + console.log("發生錯誤", err) + return + } + console.log("餘額為:", res) +}) +``` + +請記住,DAI ERC20 有 18 位小數,這意味著您需要去掉 18 個零才能得到正確的金額。 由於 JavaScript 無法處理大數值,`uint256` 會以字串的形式傳回。 如果您不確定[如何在 JS 中處理大數值,請查看我們關於 bignumber.js 的教學](https://ethereumdev.io/how-to-deal-with-big-numbers-in-javascript/)。 + +## 傳送:傳送交易至智能合約函式 {#send-sending-a-transaction-to-a-smart-contract-function} + +在第二個範例中,我們將呼叫 DAI 智能合約的 `transfer` 函式,傳送 10 個 DAI 到我們的第二個地址。 `transfer` 函式接受兩個參數:接收方地址以及要傳送的代幣數量: + +```js +daiToken.methods + .transfer(receiverAddress, "100000000000000000000") + .send({ from: senderAddress }, function (err, res) { + if (err) { + console.log("發生錯誤", err) + return + } + console.log("交易哈希:" + res) + }) +``` + +此函式呼叫會傳回交易的哈希,該交易將被挖出並納入區塊鏈。 在以太坊上,交易哈希是可預測的——這就是我們能在交易執行前就取得其哈希的原因 ([在此了解哈希如何計算](https://ethereum.stackexchange.com/questions/45648/how-to-calculate-the-assigned-txhash-of-a-transaction))。 + +由於該函式只是將交易提交到區塊鏈,我們要等到它被挖出並納入區塊鏈後,才能看到結果。 在下一個教學中,我們將學習[如何根據交易哈希,等待交易在區塊鏈上被執行](https://ethereumdev.io/waiting-for-a-transaction-to-be-mined-on-ethereum-with-js/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md b/public/content/translations/zh-tw/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md new file mode 100644 index 00000000000..fe05b8862ac --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md @@ -0,0 +1,585 @@ +--- +title: "為你的合約建立一個使用者介面" +description: "我們將使用 TypeScript、React、Vite 和 Wagmi 等現代元件,探討一個現代但極簡的使用者介面,並學習如何將錢包連接到使用者介面、呼叫智能合約來讀取資訊、將交易傳送到智能合約,以及監視智能合約的事件來識別變更。" +author: Ori Pomerantz +tags: [ "typescript", "反應", "vite", "wagmi", "前端" ] +skill: beginner +published: 2023-11-01 +lang: zh-tw +sidebarDepth: 3 +--- + +你找到了一個我們在以太坊生態系統中需要的功能。 你編寫了智能合約來實作它,甚至可能編寫了一些在鏈外執行的相關程式碼。 這太棒了! 不幸的是,如果沒有使用者介面,你就不會有任何使用者。而且在你上一次寫網站的時候,人們還在使用撥接數據機,JavaScript 還是個新玩意兒。 + +這篇文章就是為你而寫的。 我假設你懂程式設計,可能也懂一點 JavaScript 和 HTML,但你的使用者介面技能已經生疏過時了。 我們將一起探討一個簡單的現代應用程式,讓你看看現在是怎麼做的。 + +## 為什麼這很重要 {#why-important} + +理論上,你可以讓大家直接使用 [Etherscan](https://holesky.etherscan.io/address/0x432d810484add7454ddb3b5311f0ac2e95cecea8#writeContract) 或 [Blockscout](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=write_contract) 來與你的合約互動。 對於經驗豐富的以太坊使用者來說,這很棒。 但我們正試圖為 [另外十億人](https://blog.ethereum.org/2021/05/07/ethereum-for-the-next-billion) 提供服務。 如果沒有出色的使用者體驗,這一切都不會發生,而友善的使用者介面是其中的重要一環。 + +## Greeter 應用程式 {#greeter-app} + +現代 UI 的運作背後有很多理論,也有 [很多好的網站](https://react.dev/learn/thinking-in-react) [對此進行了解釋](https://wagmi.sh/core/getting-started)。 與其重複那些網站已經完成的出色工作,我假設你更喜歡從做中學,從一個你可以實際操作的應用程式開始。 你仍然需要理論來完成工作,我們也會談到它——我們將逐一檢視原始檔,並在遇到問題時進行討論。 + +### 安裝 {#installation} + +1. 如有需要,請將 [Holesky 區塊鏈](https://chainlist.org/?search=holesky&testnets=true) 新增到你的錢包,並 [取得測試 ETH](https://www.holeskyfaucet.io/)。 + +2. 複製 GitHub 儲存庫。 + + ```sh + git clone https://github.com/qbzzt/20230801-modern-ui.git + ``` + +3. 安裝必要的套件。 + + ```sh + cd 20230801-modern-ui + pnpm install + ``` + +4. 啟動應用程式。 + + ```sh + pnpm dev + ``` + +5. 瀏覽應用程式顯示的 URL。 在大多數情況下,它是 [http://localhost:5173/](http://localhost:5173/)。 + +6. 你可以在 [區塊鏈瀏覽器](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=contract) 上看到合約的原始碼,它是 Hardhat 的 Greeter 的一個稍作修改的版本。 + +### 檔案走查 {#file-walk-through} + +#### `index.html` {#index-html} + +這個檔案是標準的 HTML 樣板,除了這一行,它匯入了腳本檔案。 + +```html + +``` + +#### `src/main.tsx` {#main-tsx} + +副檔名告訴我們這個檔案是一個用 [TypeScript](https://www.typescriptlang.org/) 編寫的 [React 元件](https://www.w3schools.com/react/react_components.asp),TypeScript 是 JavaScript 的一個擴充,支援 [型別檢查](https://en.wikipedia.org/wiki/Type_system#Type_checking)。 TypeScript 會被編譯成 JavaScript,所以我們可以用它來進行用戶端執行。 + +```tsx +import '@rainbow-me/rainbowkit/styles.css' +import { RainbowKitProvider } from '@rainbow-me/rainbowkit' +import * as React from 'react' +import * as ReactDOM from 'react-dom/client' +import { WagmiConfig } from 'wagmi' +import { chains, config } from './wagmi' +``` + +匯入我們需要的庫程式碼。 + +```tsx +import { App } from './App' +``` + +匯入實作應用程式的 React 元件(見下文)。 + +```tsx +ReactDOM.createRoot(document.getElementById('root')!).render( +``` + +建立根 React 元件。 `render` 的參數是 [JSX](https://www.w3schools.com/react/react_jsx.asp),這是一種使用 HTML 和 JavaScript/TypeScript 的擴充語言。 這裡的驚嘆號告訴 TypeScript 元件:「你不知道 `document.getElementById('root')` 將會是 `ReactDOM.createRoot` 的一個有效參數,但別擔心——我是開發者,我告訴你它會是」。 + +```tsx + +``` + +應用程式將放在 [一個 `React.StrictMode` 元件](https://react.dev/reference/react/StrictMode) 內。 此元件會告訴 React 庫插入額外的偵錯檢查,這在開發過程中很有用。 + +```tsx + +``` + +應用程式也放在 [一個 `WagmiConfig` 元件](https://wagmi.sh/react/api/WagmiProvider) 內。 [wagmi (we are going to make it) 庫](https://wagmi.sh/) 將 React UI 定義與 [viem 庫](https://viem.sh/) 連接起來,用於編寫以太坊去中心化應用程式。 + +```tsx + +``` + +最後是 [一個 `RainbowKitProvider` 元件](https://www.rainbowkit.com/)。 此元件處理登入以及錢包和應用程式之間的通訊。 + +```tsx + +``` + +現在我們可以擁有應用程式的元件,它實際實作了 UI。 元件結尾的 `/>` 告訴 React,根據 XML 標準,此元件內部沒有任何定義。 + +```tsx + + + , +) +``` + +當然,我們必須關閉其他元件。 + +#### `src/App.tsx` {#app-tsx} + +```tsx +import { ConnectButton } from '@rainbow-me/rainbowkit' +import { useAccount } from 'wagmi' +import { Greeter } from './components/Greeter' + +export function App() { +``` + +這是建立 React 元件的標準方法——定義一個函式,每次需要渲染時都會呼叫它。 這個函式通常在頂部有一些 TypeScript 或 JavaScript 程式碼,後面跟著一個回傳 JSX 程式碼的 `return` 陳述式。 + +```tsx + const { isConnected } = useAccount() +``` + +這裡我們使用 [`useAccount`](https://wagmi.sh/react/api/hooks/useAccount) 來檢查我們是否透過錢包連接到區塊鏈。 + +按照慣例,在 React 中,名為 `use...` 的函式是回傳某種資料的 [hook](https://www.w3schools.com/react/react_hooks.asp)。 當你使用這樣的 hook 時,你的元件不僅會取得資料,而且當該資料變更時,元件會用更新後的資訊重新渲染。 + +```tsx + return ( + <> +``` + +React 元件的 JSX _必須_回傳一個元件。 當我們有多個元件,並且沒有任何東西可以「自然地」包裝它們時,我們使用一個空元件(`<> ...` `) 來將它們變成單一元件。 + +```tsx +

Greeter

+ +``` + +我們從 RainbowKit 取得 [`ConnectButton` 元件](https://www.rainbowkit.com/docs/connect-button)。 當我們未連接時,它會提供一個 `Connect Wallet` 按鈕,開啟一個說明錢包的強制回應視窗,讓你選擇使用哪一個錢包。 當我們連接時,它會顯示我們使用的區塊鏈、我們的帳戶地址和我們的 ETH 餘額。 我們可以使用這些顯示來切換網路或中斷連接。 + +```tsx + {isConnected && ( +``` + +當我們需要將實際的 JavaScript(或將被編譯為 JavaScript 的 TypeScript)插入 JSX 時,我們使用大括號(`{}`)。 + +`a && b` 語法是 [`a ?` 的簡寫 b : a`](https://www.w3schools.com/react/react_es6_ternary.asp)。 也就是說,如果 `a`為 true,它的評估結果為`b`,否則它的評估結果為 `a`(可以是 `false`、`0` 等)。 這是一種簡單的方法,可以告訴 React 只有在滿足特定條件時才顯示元件。 + +在這種情況下,我們只想在使用者連接到區塊鏈時向使用者顯示 `Greeter`。 + +```tsx + + )} + + ) +} +``` + +#### `src/components/Greeter.tsx` {#greeter-tsx} + +這個檔案包含了大部分的 UI 功能。 它包含了一些通常會放在多個檔案中的定義,但因為這是一個教學,所以程式的最佳化目標是為了初次閱讀時容易理解,而不是為了效能或易於維護。 + +```tsx +import { useState, ChangeEventHandler } from 'react' +import { useNetwork, + useReadContract, + usePrepareContractWrite, + useContractWrite, + useContractEvent + } from 'wagmi' +``` + +我們使用這些庫函式。 同樣,它們在下面使用到的地方會進行解釋。 + +```tsx +import { AddressType } from 'abitype' +``` + +[`abitype` 庫](https://abitype.dev/) 為我們提供了各種以太坊資料型別的 TypeScript 定義,例如 [`AddressType`](https://abitype.dev/config#addresstype)。 + +```tsx +let greeterABI = [ + . + . + . +] as const // greeterABI +``` + +`Greeter` 合約的 ABI。 +如果你同時開發合約和 UI,通常會將它們放在同一個儲存庫中,並將 Solidity 編譯器產生的 ABI 作為一個檔案用在你的應用程式中。 然而,在這裡這不是必要的,因為合約已經開發完成,不會再變更。 + +```tsx +type AddressPerBlockchainType = { + [key: number]: AddressType +} +``` + +TypeScript 是強型別的。 我們使用這個定義來指定 `Greeter` 合約在不同鏈上部署的地址。 鍵是一個數字(chainId),值是一個 `AddressType`(一個地址)。 + +```tsx +const contractAddrs: AddressPerBlockchainType = { + // Holesky + 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8', + + // Sepolia + 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0' +} +``` + +合約在兩個支援的網路上的地址:[Holesky](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=contact_code) 和 [Sepolia](https://eth-sepolia.blockscout.com/address/0x7143d5c190F048C8d19fe325b748b081903E3BF0?tab=contact_code)。 + +注意:實際上還有第三個定義,針對 Redstone Holesky,下面將會解釋。 + +```tsx +type ShowObjectAttrsType = { + name: string, + object: any +} +``` + +這個型別被用作 `ShowObject` 元件(稍後解釋)的參數。 它包含物件的名稱和其值,這些是用於偵錯目的而顯示的。 + +```tsx +type ShowGreetingAttrsType = { + greeting: string | undefined +} +``` + +在任何時候,我們可能知道問候語是什麼(因為我們從區塊鏈讀取了它),也可能不知道(因為我們還沒有收到它)。 所以有一個可以是字串或什麼都沒有的型別是很有用的。 + +##### `Greeter` 元件 {#greeter-component} + +```tsx +const Greeter = () => { +``` + +最後,我們來定義元件。 + +```tsx + const { chain } = useNetwork() +``` + +關於我們正在使用的鏈的資訊,由 [wagmi](https://wagmi.sh/react/hooks/useNetwork) 提供。 +因為這是一個 hook (`use...`),所以每次這個資訊變更時,元件都會被重新繪製。 + +```tsx + const greeterAddr = chain && contractAddrs[chain.id] +``` + +Greeter 合約的地址,它會因鏈而異(如果我們沒有鏈的資訊,或者我們在沒有該合約的鏈上,則為 `undefined`)。 + +```tsx + const readResults = useReadContract({ + address: greeterAddr, + abi: greeterABI, + functionName: "greet" , // 無引數 + watch: true + }) +``` + +[`useReadContract` hook](https://wagmi.sh/react/api/hooks/useReadContract) 從合約中讀取資訊。 你可以在 UI 中展開 `readResults` 來查看它回傳的確切資訊。 在這種情況下,我們希望它持續檢查,以便在問候語變更時得到通知。 + +**注意:** 我們可以監聽 [`setGreeting` 事件](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=logs) 來得知問候語何時變更,並以此方式更新。 然而,雖然這樣可能更有效率,但它並不適用於所有情況。 當使用者切換到不同的鏈時,問候語也會變更,但此變更並無伴隨事件。 我們可以讓一部分程式碼監聽事件,另一部分來識別鏈的變更,但這會比僅僅設定 [`watch` 參數](https://wagmi.sh/react/api/hooks/useReadContract#watch-optional) 更複雜。 + +```tsx + const [ newGreeting, setNewGreeting ] = useState("") +``` + +React 的 [`useState` hook](https://www.w3schools.com/react/react_usestate.asp) 讓我們可以指定一個狀態變數,其值在元件的多次渲染之間保持不變。 初始值是參數,此處為空字串。 + +`useState` hook 回傳一個包含兩個值的清單: + +1. 狀態變數的目前值。 +2. 一個在需要時修改狀態變數的函式。 因為這是一個 hook,所以每次呼叫它時,元件都會重新渲染。 + +在這種情況下,我們使用一個狀態變數來儲存使用者想要設定的新問候語。 + +```tsx + const greetingChange : ChangeEventHandler = (evt) => + setNewGreeting(evt.target.value) +``` + +這是當新問候語輸入欄位變更時的事件處理常式。 型別 [`ChangeEventHandler`](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forms_and_events/) 指定這是一個 HTML 輸入元素值變更的處理常式。 使用 `` 部分是因為這是一個 [泛型型別](https://www.w3schools.com/typescript/typescript_basic_generics.php)。 + +```tsx + const preparedTx = usePrepareContractWrite({ + address: greeterAddr, + abi: greeterABI, + functionName: 'setGreeting', + args: [ newGreeting ] + }) + const workingTx = useContractWrite(preparedTx.config) +``` + +這是從用戶端角度提交區塊鏈交易的過程: + +1. 使用 [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas) 將交易傳送到區塊鏈中的一個節點。 +2. 等待節點的回應。 +3. 收到回應後,要求使用者透過錢包簽署交易。 這一步驟_必須_在收到節點回應後進行,因為使用者在簽署交易前會看到交易的 gas 成本。 +4. 等待使用者核准。 +5. 再次傳送交易,這次使用 [`eth_sendRawTransaction`](https://docs.alchemy.com/reference/eth-sendrawtransaction)。 + +步驟 2 可能會花費一段可觀的時間,在此期間,使用者會想知道他們的指令是否真的被使用者介面接收到,以及為什麼還沒有被要求簽署交易。 這會造成不好的使用者體驗(UX)。 + +解決方案是使用 [prepare hook](https://wagmi.sh/react/prepare-hooks)。 每當參數變更時,立即向節點傳送 `eth_estimateGas` 請求。 然後,當使用者實際想要傳送交易時(在此例中是按下 **更新問候語**),gas 成本是已知的,使用者可以立即看到錢包頁面。 + +```tsx + return ( +``` + +現在我們終於可以建立要回傳的實際 HTML 了。 + +```tsx + <> +

Greeter

+ { + !readResults.isError && !readResults.isLoading && + + } +
+``` + +建立一個 `ShowGreeting` 元件(下面會解釋),但只有在成功從區塊鏈讀取問候語時才建立。 + +```tsx + +``` + +這是使用者可以設定新問候語的輸入文字欄位。 每當使用者按下一個鍵,我們就呼叫 `greetingChange`,它會再呼叫 `setNewGreeting`。 由於 `setNewGreeting` 來自 `useState` hook,它會導致 `Greeter` 元件再次被渲染。 這表示: + +- 我們需要指定 `value` 來保留新問候語的值,否則它會變回預設的空字串。 +- 每當 `newGreeting` 變更時,`usePrepareContractWrite` 就會被呼叫,這表示它在準備好的交易中永遠會擁有最新的 `newGreeting`。 + +```tsx + +``` + +如果沒有 `workingTx.write`,那麼我們仍在等待傳送問候語更新所需的資訊,所以按鈕是停用的。 如果有 `workingTx.write` 值,那麼這就是傳送交易時要呼叫的函式。 + +```tsx +
+ + + + + ) +} +``` + +最後,為了幫助你了解我們在做什麼,顯示我們使用的三個物件: + +- `readResults` +- `preparedTx` +- `workingTx` + +##### `ShowGreeting` 元件 {#showgreeting-component} + +此元件顯示 + +```tsx +const ShowGreeting = (attrs : ShowGreetingAttrsType) => { +``` + +一個元件函式接收一個包含該元件所有屬性的參數。 + +```tsx + return {attrs.greeting} +} +``` + +##### `ShowObject` 元件 {#showobject-component} + +為了提供資訊,我們使用 `ShowObject` 元件來顯示重要的物件(`readResults` 用於讀取問候語,`preparedTx` 和 `workingTx` 用於我們建立的交易)。 + +```tsx +const ShowObject = (attrs: ShowObjectAttrsType ) => { + const keys = Object.keys(attrs.object) + const funs = keys.filter(k => typeof attrs.object[k] == "function") + return <> +
+``` + +我們不希望用所有資訊來塞滿 UI,所以為了可以檢視或關閉它們,我們使用了 [`details`](https://www.w3schools.com/tags/tag_details.asp) 標籤。 + +```tsx + {attrs.name} +
+        {JSON.stringify(attrs.object, null, 2)}
+```
+
+大部分的欄位都是使用 [`JSON.stringify`](https://www.w3schools.com/js/js_json_stringify.asp) 來顯示的。
+
+```tsx
+      
+ { funs.length > 0 && + <> + 函式: +
    +``` + +例外是函式,它們不是 [JSON 標準](https://www.json.org/json-en.html) 的一部分,所以必須分開顯示。 + +```tsx + {funs.map((f, i) => +``` + +在 JSX 中,`{` 大括號 `}` 內的程式碼會被解讀為 JavaScript。 然後,`(` 普通括號 `)` 內的程式碼會再次被解讀為 JSX。 + +```tsx + (
  • {f}
  • ) + )} +``` + +React 要求 [DOM 樹](https://www.w3schools.com/js/js_htmldom.asp) 中的標籤必須有不同的識別碼。 這表示同一個標籤的子標籤(在此例中為 [無序清單](https://www.w3schools.com/tags/tag_ul.asp))需要有不同的 `key` 屬性。 + +```tsx +
+ + } +
+ +} +``` + +結束各種 HTML 標籤。 + +##### 最後的 `export` {#the-final-export} + +```tsx +export { Greeter } +``` + +我們需要為應用程式匯出的就是 `Greeter` 元件。 + +#### `src/wagmi.ts` {#wagmi-ts} + +最後,與 WAGMI 相關的各種定義都在 `src/wagmi.ts` 中。 我不會在這裡解釋所有內容,因為大部分都是樣板程式碼,你不太可能需要變更。 + +這裡的程式碼與 [github 上的](https://github.com/qbzzt/20230801-modern-ui/blob/main/src/wagmi.ts) 不完全相同,因為在文章後面我們會新增另一條鏈 ([Redstone Holesky](https://redstone.xyz/docs/network-info))。 + +```ts +import { getDefaultWallets } from '@rainbow-me/rainbowkit' +import { configureChains, createConfig } from 'wagmi' +import { holesky, sepolia } from 'wagmi/chains' +``` + +匯入應用程式支援的區塊鏈。 你可以在 [viem 的 github](https://github.com/wagmi-dev/viem/tree/main/src/chains/definitions) 中看到支援的鏈的清單。 + +```ts +import { publicProvider } from 'wagmi/providers/public' + +const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575' +``` + +要能使用 [WalletConnect](https://walletconnect.com/),你的應用程式需要一個專案 ID。 你可以在 [cloud.walletconnect.com](https://cloud.walletconnect.com/sign-in) 上取得它。 + +```ts +const { chains, publicClient, webSocketPublicClient } = configureChains( + [ holesky, sepolia ], + [ + publicProvider(), + ], +) + +const { connectors } = getDefaultWallets({ + appName: 'My wagmi + RainbowKit App', + chains, + projectId: walletConnectProjectId, +}) + +export const config = createConfig({ + autoConnect: true, + connectors, + publicClient, + webSocketPublicClient, +}) + +export { chains } +``` + +### 新增另一條區塊鏈 {#add-blockchain} + +現今有很多 [L2 擴展解決方案](/layer-2/),你可能想要支援一些 viem 尚未支援的方案。 要做到這點,你需要修改 `src/wagmi.ts`。 這些說明解釋了如何新增 [Redstone Holesky](https://redstone.xyz/docs/network-info)。 + +1. 從 viem 匯入 `defineChain` 型別。 + + ```ts + import { defineChain } from 'viem' + ``` + +2. 新增網路定義。 + + ```ts + const redstoneHolesky = defineChain({ + id: 17_001, + name: 'Redstone Holesky', + network: 'redstone-holesky', + nativeCurrency: { + decimals: 18, + name: 'Ether', + symbol: 'ETH', + }, + rpcUrls: { + default: { + http: ['https://rpc.holesky.redstone.xyz'], + webSocket: ['wss://rpc.holesky.redstone.xyz/ws'], + }, + public: { + http: ['https://rpc.holesky.redstone.xyz'], + webSocket: ['wss://rpc.holesky.redstone.xyz/ws'], + }, + }, + blockExplorers: { + default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' }, + }, + }) + ``` + +3. 將新鏈新增到 `configureChains` 呼叫中。 + + ```ts + const { chains, publicClient, webSocketPublicClient } = configureChains( + [ holesky, sepolia, redstoneHolesky ], + [ publicProvider(), ], + ) + ``` + +4. 確保應用程式知道你的合約在新網路上的地址。 在這種情況下,我們修改 `src/components/Greeter.tsx`: + + ```ts + const contractAddrs : AddressPerBlockchainType = { + // Holesky + 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8', + + // Redstone Holesky + 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3', + + // Sepolia + 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0' + } + ``` + +## 結論 {#conclusion} + +當然,你並不是真的在乎為 `Greeter` 提供使用者介面。 你想要為你自己的合約建立一個使用者介面。 要建立你自己的應用程式,請執行以下步驟: + +1. 指定建立一個 wagmi 應用程式。 + + ```sh copy + pnpm create wagmi + ``` + +2. 為應用程式命名。 + +3. 選擇 **React** 框架。 + +4. 選擇 **Vite** 變體。 + +5. 你可以 [新增 Rainbow kit](https://www.rainbowkit.com/docs/installation#manual-setup)。 + +現在去讓你的合約為廣大世界所用吧。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 + diff --git a/public/content/translations/zh-tw/developers/tutorials/deploying-your-first-smart-contract/index.md b/public/content/translations/zh-tw/developers/tutorials/deploying-your-first-smart-contract/index.md new file mode 100644 index 00000000000..701b370b70e --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/deploying-your-first-smart-contract/index.md @@ -0,0 +1,95 @@ +--- +title: "部署你的第一個智能合約" +description: "在以太坊測試網上部署你的第一個智能合約的簡介" +author: "jdourlens" +tags: [ "智能合約", "remix", "穩固", "部署" ] +skill: beginner +lang: zh-tw +published: 2020-04-03 +source: EthereumDev +sourceUrl: https://ethereumdev.io/deploying-your-first-smart-contract/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +我想你和我們一樣興奮,都想在以太坊區塊鏈上[部署](/developers/docs/smart-contracts/deploying/)並與你的第一個[智能合約](/developers/docs/smart-contracts/)互動。 + +別擔心,因為這是我們的第一個智能合約,我們會在[本地測試網](/developers/docs/networks/)上部署它,所以你部署和盡情操作它都不需要任何費用。 + +## 撰寫我們的合約 {#writing-our-contract} + +第一步是[訪問 Remix](https://remix.ethereum.org/) 並建立一個新檔案。 在 Remix 介面的左上角新增一個新檔案,並輸入你想要的檔案名稱。 + +![在 Remix 介面中新增檔案](./remix.png) + +在新檔案中,我們將貼上以下程式碼: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.17; + +contract Counter { + + // 公開的無正負號整數,用來記錄次數 + uint256 public count = 0; + + // 遞增計數器的函式 + function increment() public { + count += 1; + } + + // 取得計數值的 getter,非必要 + function getCount() public view returns (uint256) { + return count; + } + +} +``` + +如果你習慣寫程式,你應該可以輕易猜出這個程式的功能。 以下是逐行說明: + +- 第 4 行:我們定義了一個名為 `Counter` 的合約。 +- 第 7 行:我們的合約儲存一個名為 `count` 的無正負號整數,初始值為 0。 +- 第 10 行:第一個函式會修改合約的狀態,並透過 `increment()` 遞增我們的 `count` 變數。 +- 第 15 行:第二個函式只是一個 getter,用來從智能合約外部讀取 `count` 變數的值。 請注意,因為我們將 `count` 變數定義為 public (公開),所以這不是必要的,只是作為範例展示。 + +這就是我們第一個簡單的智能合約。 你可能知道,它看起來像 Java 或 C++ 等物件導向程式設計 (OOP) 語言中的類別 (class)。 現在可以來操作我們的合約了。 + +## 部署我們的合約 {#deploying-our-contract} + +既然我們寫好了第一個智能合約,現在就要將它部署到區塊鏈上,以便進行操作。 + +[在區塊鏈上部署智能合約](/developers/docs/smart-contracts/deploying/),實際上只是傳送一筆交易,其中包含已編譯智能合約的程式碼,而無須指定任何接收者。 + +首先,我們要點擊左側的編譯圖示來[編譯合約](/developers/docs/smart-contracts/compiling/): + +![Remix 工具列中的編譯圖示](./remix-compile-button.png) + +然後點擊編譯按鈕: + +![Remix Solidity 編譯器中的編譯按鈕](./remix-compile.png) + +你可以選擇「自動編譯」(Auto compile) 選項,這樣每當你在文字編輯器中儲存內容時,合約就會自動編譯。 + +然後前往「部署及執行交易」(deploy and run transactions) 畫面: + +![Remix 工具列中的部署圖示](./remix-deploy.png) + +進入「部署及執行交易」畫面後,再次確認你的合約名稱是否出現,然後點擊「部署」(Deploy)。 如你在頁面頂端所見,目前環境是「JavaScript VM」(JavaScript 虛擬機),這代表我們將在一個本地測試鏈上部署我們的智能合約並與之互動,以便能更快地測試,且無須支付任何費用。 + +![Remix Solidity 編譯器中的部署按鈕](./remix-deploy-button.png) + +點擊「部署」(Deploy) 按鈕後,你會在底部看到你的合約。 點擊左邊的箭頭將它展開,我們便能看到合約的內容。 這就是我們的 `count` 變數、`increment()` 函式和 `getCounter()` getter。 + +如果你點擊 `count` 或 `getCount` 按鈕,它會實際擷取合約的 `count` 變數內容並顯示出來。 因為我們還沒呼叫 `increment` 函式,所以它應該會顯示 0。 + +![Remix Solidity 編譯器中的函式按鈕](./remix-function-button.png) + +現在讓我們點擊按鈕來呼叫 `increment` 函式。 你會在視窗底部看到所執行交易的紀錄。 你會發現,當你按下擷取資料的按鈕時,紀錄會與按下 `increment` 按鈕時不同。 這是因為在區塊鏈上讀取資料不需要任何交易 (寫入) 或費用。 因為只有修改區塊鏈的狀態才需要進行交易: + +![交易紀錄](./transaction-log.png) + +按下 increment 按鈕會產生一筆交易來呼叫我們的 `increment()` 函式,之後如果我們回頭點擊 count 或 getCount 按鈕,我們就會讀取到智能合約已更新的狀態,其中 count 變數的值會大於 0。 + +![智能合約已更新的狀態](./updated-state.png) + +在下一篇教學中,我們將介紹[如何在你的智能合約中新增事件](/developers/tutorials/logging-events-smart-contracts/)。 記錄事件是個便利的方法,可以對你的智能合約進行除錯,並了解呼叫函式時發生了什麼事。 diff --git a/public/content/translations/zh-tw/developers/tutorials/develop-and-test-dapps-with-a-multi-client-local-eth-testnet/index.md b/public/content/translations/zh-tw/developers/tutorials/develop-and-test-dapps-with-a-multi-client-local-eth-testnet/index.md new file mode 100644 index 00000000000..c248ccf6d0d --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/develop-and-test-dapps-with-a-multi-client-local-eth-testnet/index.md @@ -0,0 +1,363 @@ +--- +title: "如何在本地多用戶端測試網上開發和測試 dApp" +description: "本指南將首先引導您完成如何實例化和設定多用戶端的本地以太坊測試網,然後再使用該測試網來部署與測試 dApp。" +author: "Tedi Mitiku" +tags: [ "用戶端", "節點", "智能合約", "可組合性", "共識層", "執行層", "測試" ] +skill: intermediate +lang: zh-tw +published: 2023-04-11 +--- + +## 介紹 {#introduction} + +本指南將引導您完成實例化可設定的本地以太坊測試網、將智能合約部署到其中,並使用該測試網對您的 dApp 執行測試的過程。 本指南專為希望在部署到即時測試網或主網之前,針對不同的網路設定在本地開發和測試其 dApp 的 dApp 開發者而設計。 + +在本指南中,您將會: + +- 使用 [Kurtosis](https://www.kurtosis.com/) 和 [`eth-network-package`](https://github.com/kurtosis-tech/eth-network-package) 實例化一個本地以太坊測試網, +- 將您的 Hardhat dApp 開發環境連接到本地測試網以編譯、部署和測試 dApp,以及 +- 設定本地測試網,包括節點數量和特定的 EL/CL 用戶端配對等參數,以實現針對各種網路設定的開發和測試工作流程。 + +### 什麼是 Kurtosis? {#what-is-kurtosis} + +[Kurtosis](https://www.kurtosis.com/) 是一個可組合的建構系統,專為設定多容器測試環境而設計。 它特別允許開發者創建需要動態設定邏輯的可重現環境,例如區塊鏈測試網。 + +在本指南中,Kurtosis eth-network-package 會啟動一個本地以太坊測試網,支援 [`geth`](https://geth.ethereum.org/) 執行層 (EL) 用戶端,以及 [`teku`](https://consensys.io/teku)、[`lighthouse`](https://lighthouse.sigmaprime.io/) 和 [`lodestar`](https://lodestar.chainsafe.io/) 共識層 (CL) 用戶端。 此套件可作為 Hardhat Network、Ganache 和 Anvil 等框架中網路的可設定和可組合替代方案。 Kurtosis 為開發者提供了對其使用的測試網更大的控制權和靈活性,這也是[以太坊基金會使用 Kurtosis 測試合併](https://www.kurtosis.com/blog/testing-the-ethereum-merge)並繼續使用它來測試網路升級的主要原因。 + +## 設定 Kurtosis {#setting-up-kurtosis} + +在繼續之前,請確定您已經: + +- 在您的本地機器上[安裝並啟動 Docker 引擎](https://docs.kurtosis.com/install/#i-install--start-docker) +- [安裝 Kurtosis CLI](https://docs.kurtosis.com/install#ii-install-the-cli)(如果您已安裝 CLI,則將其升級到最新版本) +- 安裝 [Node.js](https://nodejs.org/en)、[yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable) 和 [npx](https://www.npmjs.com/package/npx)(為您的 dApp 環境) + +## 實例化本地以太坊測試網 {#instantiate-testnet} + +要啟動本地以太坊測試網,請執行: + +```python +kurtosis --enclave local-eth-testnet run github.com/kurtosis-tech/eth-network-package +``` + +注意:此命令使用 `--enclave` 標誌將您的網路命名為「local-eth-testnet」。 + +Kurtosis 在解譯、驗證和執行指令時,會印出其在幕後執行的步驟。 最後,您應該會看到類似以下的輸出: + +```python +INFO[2023-04-04T18:09:44-04:00] ====================================================== +INFO[2023-04-04T18:09:44-04:00] || Created enclave: local-eth-testnet || +INFO[2023-04-04T18:09:44-04:00] ====================================================== +Name: local-eth-testnet +UUID: 39372d756ae8 +Status: RUNNING +Creation Time: Tue, 04 Apr 2023 18:09:03 EDT + +========================================= Files Artifacts ========================================= +UUID Name +d4085a064230 cl-genesis-data +1c62cb792e4c el-genesis-data +bd60489b73a7 genesis-generation-config-cl +b2e593fe5228 genesis-generation-config-el +d552a54acf78 geth-prefunded-keys +5f7e661eb838 prysm-password +054e7338bb59 validator-keystore-0 + +========================================== User Services ========================================== +UUID Name Ports Status +e20f129ee0c5 cl-client-0-beacon http: 4000/tcp -> RUNNING + metrics: 5054/tcp -> + tcp-discovery: 9000/tcp -> 127.0.0.1:54263 + udp-discovery: 9000/udp -> 127.0.0.1:60470 +a8b6c926cdb4 cl-client-0-validator http: 5042/tcp -> 127.0.0.1:54267 RUNNING + metrics: 5064/tcp -> +d7b802f623e8 el-client-0 engine-rpc: 8551/tcp -> 127.0.0.1:54253 RUNNING + rpc: 8545/tcp -> 127.0.0.1:54251 + tcp-discovery: 30303/tcp -> 127.0.0.1:54254 + udp-discovery: 30303/udp -> 127.0.0.1:53834 + ws: 8546/tcp -> 127.0.0.1:54252 +514a829c0a84 prelaunch-data-generator-1680646157905431468 STOPPED +62bd62d0aa7a prelaunch-data-generator-1680646157915424301 STOPPED +05e9619e0e90 prelaunch-data-generator-1680646157922872635 STOPPED + +``` + +恭喜! 您透過 Docker 使用 Kurtosis 實例化了一個本地以太坊測試網,其中包含一個 CL(`lighthouse`)和一個 EL 用戶端(`geth`)。 + +### 回顧 {#review-instantiate-testnet} + +在本節中,您執行了一個指令,指示 Kurtosis 使用[遠端託管在 GitHub 上的 `eth-network-package`](https://github.com/kurtosis-tech/eth-network-package) 在 Kurtosis [Enclave](https://docs.kurtosis.com/advanced-concepts/enclaves/) 中啟動一個本地以太坊測試網。 在您的 enclave 中,您會找到「檔案成品」和「使用者服務」。 + +您 enclave 中的[檔案成品](https://docs.kurtosis.com/advanced-concepts/files-artifacts/)包含所有用於啟動 EL 和 CL 用戶端的已生成和已使用的資料。 這些資料是使用從這個 [Docker 映像檔](https://github.com/ethpandaops/ethereum-genesis-generator)建構的 `prelaunch-data-generator` 服務建立的 + +使用者服務會顯示在您 enclave 中運作的所有容器化服務。 您會注意到已建立一個單一節點,該節點同時具有 EL 用戶端和 CL 用戶端。 + +## 將您的 dApp 開發環境連接到本地以太坊測試網 {#connect-your-dapp} + +### 設定 dApp 開發環境 {#set-up-dapp-env} + +既然您已擁有一個正在運行的本地測試網,您可以將您的 dApp 開發環境連接到本地測試網來使用。 本指南將使用 Hardhat 框架將一個二十一點 dApp 部署到您的本地測試網。 + +要設定您的 dApp 開發環境,請複製包含我們的範例 dApp 的儲存庫並安裝其相依性,執行: + +```python +git clone https://github.com/kurtosis-tech/awesome-kurtosis.git && cd awesome-kurtosis/smart-contract-example && yarn +``` + +這裡使用的 [smart-contract-example](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/smart-contract-example) 資料夾包含使用 [Hardhat](https://hardhat.org/) 框架的 dApp 開發者的典型設定: + +- [`contracts/`](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/smart-contract-example/contracts) 包含一些用於二十一點 dApp 的簡單智能合約 +- [`scripts/`](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/smart-contract-example/scripts) 包含一個用於將代幣合約部署到您的本地以太坊網路的腳本 +- [`test/`](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/smart-contract-example/test) 包含一個針對您的代幣合約的簡單 .js 測試,以確認我們的二十一點 dApp 中的每個玩家都已為他們鑄造了 1000 個代幣 +- [`hardhat.config.ts`](https://github.com/kurtosis-tech/awesome-kurtosis/blob/main/smart-contract-example/hardhat.config.ts) 設定您的 Hardhat + +### 設定 Hardhat 以使用本地測試網 {#configure-hardhat} + +設定好您的 dApp 開發環境後,您現在將連接 Hardhat 以使用 Kurtosis 產生的本地以太坊測試網。 為此,請將您的 `hardhat.config.ts` 設定檔中 `localnet` 結構中的 `<$YOUR_PORT>` 替換為任何 `el-client-` 服務輸出的 RPC URI 的連接埠。 在這個範例中,連接埠會是 `64248`。 您的連接埠會不同。 + +`hardhat.config.ts` 中的範例: + +```js +localnet: { +url: 'http://127.0.0.1:<$YOUR_PORT>',// TODO:將 $YOUR_PORT 替換為 ETH NETWORK KURTOSIS 套件產生的節點 URI 的連接埠 + +// 這些是與 eth-network-package 創建的預先注資測試帳戶相關的私鑰 +// +accounts: [ + "ef5177cd0b6b21c87db5a0bf35d4084a8a57a9d6a064f86d51ac85f2b873a4e2", + "48fcc39ae27a0e8bf0274021ae6ebd8fe4a0e12623d61464c498900b28feb567", + "7988b3a148716ff800414935b305436493e1f25237a2a03e5eebc343735e2f31", + "b3c409b6b0b3aa5e65ab2dc1930534608239a478106acf6f3d9178e9f9b00b35", + "df9bb6de5d3dc59595bcaa676397d837ff49441d211878c024eabda2cd067c9f", + "7da08f856b5956d40a72968f93396f6acff17193f013e8053f6fbb6c08c194d6", + ], +}, +``` + +儲存檔案後,您的 Hardhat dApp 開發環境現在已連接到您的本地以太坊測試網! 您可以透過執行以下命令來驗證您的測試網是否正常運作: + +```python +npx hardhat balances --network localnet +``` + +輸出應該類似這樣: + +```python +0x878705ba3f8Bc32FCf7F4CAa1A35E72AF65CF766 has balance 10000000000000000000000000 +0x4E9A3d9D1cd2A2b2371b8b3F489aE72259886f1A has balance 10000000000000000000000000 +0xdF8466f277964Bb7a0FFD819403302C34DCD530A has balance 10000000000000000000000000 +0x5c613e39Fc0Ad91AfDA24587e6f52192d75FBA50 has balance 10000000000000000000000000 +0x375ae6107f8cC4cF34842B71C6F746a362Ad8EAc has balance 10000000000000000000000000 +0x1F6298457C5d76270325B724Da5d1953923a6B88 has balance 10000000000000000000000000 +``` + +這證實了 Hardhat 正在使用您的本地測試網,並偵測到由 `eth-network-package` 創建的預先注資帳戶。 + +### 在本地部署和測試您的 dApp {#deploy-and-test-dapp} + +在 dApp 開發環境完全連接到本地以太坊測試網後,您現在可以使用本地測試網對您的 dApp 執行開發和測試工作流程。 + +要編譯和部署 `ChipToken.sol` 智能合約以進行本地原型設計和開發,請執行: + +```python +npx hardhat compile +npx hardhat run scripts/deploy.ts --network localnet +``` + +輸出應該看起來像: + +```python +ChipToken deployed to: 0xAb2A01BC351770D09611Ac80f1DE076D56E0487d +``` + +現在嘗試對您的本地 dApp 執行 `simple.js` 測試,以確認我們的二十一點 dApp 中的每個玩家都已為他們鑄造了 1000 個代幣: + +輸出應該類似這樣: + +```python +npx hardhat test --network localnet +``` + +輸出應該類似這樣: + +```python +ChipToken + mint + ✔ 應為玩家一號鑄造 1000 枚籌碼 + + 1 個通過 (654ms) +``` + +### 回顧 {#review-dapp-workflows} + +至此,您已經設定了一個 dApp 開發環境,將其連接到由 Kurtosis 創建的本地以太坊網路,並已對您的 dApp 進行了編譯、部署和簡單的測試。 + +現在讓我們來探索如何設定底層網路,以便在不同的網路設定下測試我們的 dApp。 + +## 設定本地以太坊測試網 {#configure-testnet} + +### 變更用戶端設定和節點數量 {#configure-client-config-and-num-nodes} + +您的本地以太坊測試網可以設定為使用不同的 EL 和 CL 用戶端配對,以及不同數量的節點,這取決於您要開發或測試的場景和特定的網路設定。 這意味著,一旦設定完成,您就可以啟動一個客製化的本地測試網,並用它來執行相同的工作流程(部署、測試等) 在各種網路設定下,確保一切如預期般運作。 要了解更多關於您可以修改的其他參數,請造訪此連結。 + +試試看! 您可以透過 JSON 檔案將各種設定選項傳遞給 `eth-network-package`。 這個網路參數 JSON 檔案提供了 Kurtosis 用於設定本地以太坊網路的特定設定。 + +取得預設設定檔案並進行編輯,以啟動兩個具有不同 EL/CL 配對的節點: + +- 節點 1 使用 `geth`/`lighthouse` +- 節點 2 使用 `geth`/`lodestar` +- 節點 3 使用 `geth`/`teku` + +此設定建立了一個異構的以太坊節點實作網路,用於測試您的 dApp。 您的設定檔現在應該如下所示: + +```yaml +{ + "participants": + [ + { + "el_client_type": "geth", + "el_client_image": "", + "el_client_log_level": "", + "cl_client_type": "lighthouse", + "cl_client_image": "", + "cl_client_log_level": "", + "beacon_extra_params": [], + "el_extra_params": [], + "validator_extra_params": [], + "builder_network_params": null, + }, + { + "el_client_type": "geth", + "el_client_image": "", + "el_client_log_level": "", + "cl_client_type": "lodestar", + "cl_client_image": "", + "cl_client_log_level": "", + "beacon_extra_params": [], + "el_extra_params": [], + "validator_extra_params": [], + "builder_network_params": null, + }, + { + "el_client_type": "geth", + "el_client_image": "", + "el_client_log_level": "", + "cl_client_type": "teku", + "cl_client_image": "", + "cl_client_log_level": "", + "beacon_extra_params": [], + "el_extra_params": [], + "validator_extra_params": [], + "builder_network_params": null, + }, + ], + "network_params": + { + "preregistered_validator_keys_mnemonic": "giant issue aisle success illegal bike spike question tent bar rely arctic volcano long crawl hungry vocal artwork sniff fantasy very lucky have athlete", + "num_validator_keys_per_node": 64, + "network_id": "3151908", + "deposit_contract_address": "0x4242424242424242424242424242424242424242", + "seconds_per_slot": 12, + "genesis_delay": 120, + "capella_fork_epoch": 5, + }, +} +``` + +每個 `participants` 結構對應網路中的一個節點,因此 3 個 `participants` 結構將告知 Kurtosis 在您的網路中啟動 3 個節點。 每個 `participants` 結構將允許您指定該特定節點使用的 EL 和 CL 配對。 + +`network_params` 結構設定了用於為每個節點建立創世檔的網路設定,以及其他設定,例如網路的每時隙秒數。 + +將您編輯的參數檔案儲存到您希望的任何目錄中(在下面的範例中,它被儲存到桌面),然後透過執行以下命令來執行您的 Kurtosis 套件: + +```python +kurtosis clean -a && kurtosis run --enclave local-eth-testnet github.com/kurtosis-tech/eth-network-package "$(cat ~/eth-network-params.json)" +``` + +注意:這裡使用 `kurtosis clean -a` 命令來指示 Kurtosis 在啟動新的測試網之前銷毀舊的測試網及其內容。 + +同樣,Kurtosis 會運作一會兒,並印出正在進行的各個步驟。 最終,輸出應該會像這樣: + +```python +Starlark code successfully run. No output was returned. +INFO[2023-04-07T11:43:16-04:00] ========================================================== +INFO[2023-04-07T11:43:16-04:00] || Created enclave: local-eth-testnet || +INFO[2023-04-07T11:43:16-04:00] ========================================================== +Name: local-eth-testnet +UUID: bef8c192008e +Status: RUNNING +Creation Time: Fri, 07 Apr 2023 11:41:58 EDT + +========================================= Files Artifacts ========================================= +UUID Name +cc495a8e364a cl-genesis-data +7033fcdb5471 el-genesis-data +a3aef43fc738 genesis-generation-config-cl +8e968005fc9d genesis-generation-config-el +3182cca9d3cd geth-prefunded-keys +8421166e234f prysm-password +d9e6e8d44d99 validator-keystore-0 +23f5ba517394 validator-keystore-1 +4d28dea40b5c validator-keystore-2 + +========================================== User Services ========================================== +UUID Name Ports Status +485e6fde55ae cl-client-0-beacon http: 4000/tcp -> http://127.0.0.1:65010 RUNNING + metrics: 5054/tcp -> http://127.0.0.1:65011 + tcp-discovery: 9000/tcp -> 127.0.0.1:65012 + udp-discovery: 9000/udp -> 127.0.0.1:54455 +73739bd158b2 cl-client-0-validator http: 5042/tcp -> 127.0.0.1:65016 RUNNING + metrics: 5064/tcp -> http://127.0.0.1:65017 +1b0a233cd011 cl-client-1-beacon http: 4000/tcp -> 127.0.0.1:65021 RUNNING + metrics: 8008/tcp -> 127.0.0.1:65023 + tcp-discovery: 9000/tcp -> 127.0.0.1:65024 + udp-discovery: 9000/udp -> 127.0.0.1:56031 + validator-metrics: 5064/tcp -> 127.0.0.1:65022 +949b8220cd53 cl-client-1-validator http: 4000/tcp -> 127.0.0.1:65028 RUNNING + metrics: 8008/tcp -> 127.0.0.1:65030 + tcp-discovery: 9000/tcp -> 127.0.0.1:65031 + udp-discovery: 9000/udp -> 127.0.0.1:60784 + validator-metrics: 5064/tcp -> 127.0.0.1:65029 +c34417bea5fa cl-client-2 http: 4000/tcp -> 127.0.0.1:65037 RUNNING + metrics: 8008/tcp -> 127.0.0.1:65035 + tcp-discovery: 9000/tcp -> 127.0.0.1:65036 + udp-discovery: 9000/udp -> 127.0.0.1:63581 +e19738e6329d el-client-0 engine-rpc: 8551/tcp -> 127.0.0.1:64986 RUNNING + rpc: 8545/tcp -> 127.0.0.1:64988 + tcp-discovery: 30303/tcp -> 127.0.0.1:64987 + udp-discovery: 30303/udp -> 127.0.0.1:55706 + ws: 8546/tcp -> 127.0.0.1:64989 +e904687449d9 el-client-1 engine-rpc: 8551/tcp -> 127.0.0.1:64993 RUNNING + rpc: 8545/tcp -> 127.0.0.1:64995 + tcp-discovery: 30303/tcp -> 127.0.0.1:64994 + udp-discovery: 30303/udp -> 127.0.0.1:58096 + ws: 8546/tcp -> 127.0.0.1:64996 +ad6f401126fa el-client-2 engine-rpc: 8551/tcp -> 127.0.0.1:65003 RUNNING + rpc: 8545/tcp -> 127.0.0.1:65001 + tcp-discovery: 30303/tcp -> 127.0.0.1:65000 + udp-discovery: 30303/udp -> 127.0.0.1:57269 + ws: 8546/tcp -> 127.0.0.1:65002 +12d04a9dbb69 prelaunch-data-generator-1680882122181135513 STOPPED +5b45f9c0504b prelaunch-data-generator-1680882122192182847 STOPPED +3d4aaa75e218 prelaunch-data-generator-1680882122201668972 STOPPED +``` + +恭喜! 您已成功將您的本地測試網設定為擁有 3 個節點,而不是 1 個。 要在您的 dApp 上執行與之前相同的工作流程(部署和測試),請執行與之前相同的操作,將您的 `hardhat.config.ts` 設定檔中 `localnet` 結構中的 `<$YOUR_PORT>` 替換為您的新的 3 節點本地測試網中任何 `el-client-` 服務輸出的 RPC URI 的連接埠。 + +## 結論 {#conclusion} + +就是這樣! 總結一下本簡短指南,您: + +- 使用 Kurtosis 透過 Docker 建立了一個本地以太坊測試網 +- 將您的本地 dApp 開發環境連接到本地以太坊網路 +- 在本地以太坊網路上部署了一個 dApp 並對其進行了簡單的測試 +- 將底層以太坊網路設定為擁有 3 個節點 + +我們很樂意聽取您對哪些方面進展順利、哪些方面可以改進的意見,或回答您的任何問題。 請隨時透過 [GitHub](https://github.com/kurtosis-tech/kurtosis/issues/new/choose) 或[發送電子郵件給我們](mailto:feedback@kurtosistech.com)與我們聯絡! + +### 其他範例和指南 {#other-examples-guides} + +我們鼓勵您查看我們的[快速入門](https://docs.kurtosis.com/quickstart)(您將在其中建構 Postgres 資料庫和 API)以及我們 [awesome-kurtosis 儲存庫](https://github.com/kurtosis-tech/awesome-kurtosis)中的其他範例,您將在那裡找到一些很棒的範例,包括以下套件: + +- [啟動相同的本地以太坊測試網](https://github.com/kurtosis-tech/eth2-package),但連接了額外的服務,例如交易發送器(以模擬交易)、分叉監視器,以及一個已連接的 Grafana 和 Prometheus 實例 +- 對相同的本地以太坊網路執行[子網路測試](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/ethereum-network-partition-test) diff --git a/public/content/translations/zh-tw/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/index.md b/public/content/translations/zh-tw/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/index.md new file mode 100644 index 00000000000..34101b4be5f --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/index.md @@ -0,0 +1,144 @@ +--- +title: "縮小合約大小來對抗合約大小限制" +description: "你可以怎麼做來防止智能合約規模過大?" +author: Markus Waas +lang: zh-tw +tags: [ "穩固", "智能合約", "儲存" ] +skill: intermediate +published: 2020-06-26 +source: soliditydeveloper.com +sourceUrl: https://soliditydeveloper.com/max-contract-size +--- + +## 為什麼要有個限制? {#why-is-there-a-limit} + +在 [2016 年 11 月 22 日](https://blog.ethereum.org/2016/11/18/hard-fork-no-4-spurious-dragon/),Spurious Dragon 硬分叉引入了 [EIP-170](https://eips.ethereum.org/EIPS/eip-170),新增了 24.576 kb 的智能合約大小限制。 身為 Solidity 開發者,這代表當你在合約中加入越來越多功能時,在某個時間點你會達到上限,並在部署時看到以下錯誤: + +`Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). 此合約可能無法在主網上部署。 Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.` + +這個限制是為了防止阻斷服務 (DOS) 攻擊。 對合約的任何呼叫,在 gas 方面都相對便宜。 然而,合約呼叫對以太坊節點的影響,會根據被呼叫的合約程式碼大小(從磁碟讀取程式碼、預處理程式碼、將資料加入默克爾證明)而不成比例地增加。 每當發生攻擊者能以少量資源,造成他人大量工作負擔的情形,便可能產生 DOS 攻擊。 + +起初這問題不大,因為一個天然的合約大小限制就是區塊 gas 上限。 顯然,合約必須部署在一個包含其所有位元組碼的交易中。 如果你只在一個區塊中包含那筆交易,你可以用完所有的 gas,但它不是無限的。 自 [倫敦升級](/ethereum-forks/#london) 以來,區塊 gas 上限可以根據網路需求在 1500 萬到 3000 萬單位之間變動。 + +接下來,我們將按照潛在影響力的大小,來看看一些方法。 可以把它想像成減重。 一個人要達到目標體重(在我們的情況下是 24kb)的最佳策略是先專注於影響力大的方法。 在大多數情況下,單靠調整飲食就能達標,但有時候你需要做的更多一點。 然後你可能會增加一些運動(中等影響),或甚至營養補充品(小影響)。 + +## 重大影響 {#big-impact} + +### 拆分你的合約 {#separate-your-contracts} + +這應該永遠是你的首選方法。 你如何將合約拆分成多個較小的合約? 這通常會迫使你為合約設計一個好的架構。 從程式碼可讀性的角度來看,小合約總是比較好。 要拆分合約,可以問自己: + +- 哪些函式屬於同一組? 每一組函式可能最適合放在各自的合約中。 +- 哪些函式不需要讀取合約狀態,或只需要讀取狀態的特定子集? +- 你能將儲存空間和功能性拆分開來嗎? + +### 函式庫 {#libraries} + +一個將功能性程式碼從儲存空間移開的簡單方法是使用[函式庫](https://solidity.readthedocs.io/en/v0.6.10/contracts.html#libraries)。 不要將函式庫的函式宣告為 internal,因為它們會在編譯期間直接被[加到合約中](https://ethereum.stackexchange.com/questions/12975/are-internal-functions-in-libraries-not-covered-by-linking)。 但如果你使用 public 函式,那麼它們實際上會存在一個獨立的函式庫合約中。 可以考慮 [using for](https://solidity.readthedocs.io/en/v0.6.10/contracts.html#using-for) 讓函式庫的使用更方便。 + +### 代理 {#proxies} + +一個更進階的策略是代理系統。 函式庫在後端使用 `DELEGATECALL`,它只是用呼叫合約的狀態來執行另一個合約的函式。 查看[這篇部落格文章](https://hackernoon.com/how-to-make-smart-contracts-upgradable-2612e771d5a2),以了解更多關於代理系統的資訊。 它們提供你更多功能,例如,它們能讓合約可以升級,但也增加了許多複雜性。 除非出於某些原因這是你唯一的選擇,否則我不會只為了縮小合約大小而加入這些。 + +## 中等影響 {#medium-impact} + +### 移除函式 {#remove-functions} + +這點應該很明顯。 函式會增加不少合約大小。 + +- **External**:我們時常為了方便而加入許多 view 函式。 在你碰到大小限制之前,這完全沒問題。 那時你可能就得認真考慮,只保留絕對必要的函式,並移除其他的。 +- **Internal**:你也可以移除 internal/private 函式,並只要該函式只被呼叫一次,就直接內聯 (inline) 其程式碼。 + +### 避免額外的變數 {#avoid-additional-variables} + +```solidity +function get(uint id) returns (address,address) { + MyStruct memory myStruct = myStructs[id]; + return (myStruct.addr1, myStruct.addr2); +} +``` + +```solidity +function get(uint id) returns (address,address) { + return (myStructs[id].addr1, myStructs[id].addr2); +} +``` + +像這樣一個簡單的改變,就差了 **0.28kb**。 你的合約中很可能有很多類似的情況,這些情況累積起來的量可能相當可觀。 + +### 縮短錯誤訊息 {#shorten-error-message} + +長的 revert 訊息,特別是許多不同的 revert 訊息,會讓合約膨脹。 改用簡短的錯誤碼,並在你的合約中解碼它們。 一則長訊息可以變得更短: + +```solidity +require(msg.sender == owner, "Only the owner of this contract can call this function"); +``` + +```solidity +require(msg.sender == owner, "OW1"); +``` + +### 使用自訂錯誤而非錯誤訊息 + +[Solidity 0.8.4](https://blog.soliditylang.org/2021/04/21/custom-errors/) 引入了自訂錯誤。 它們是減少合約大小的好方法,因為它們會被 ABI 編碼為選擇器(就像函式一樣)。 + +```solidity +error Unauthorized(); + +if (msg.sender != owner) { + revert Unauthorized(); +} +``` + +### 在優化器中考慮使用較低的 run 值 {#consider-a-low-run-value-in-the-optimizer} + +你也可以更改優化器的設定。 預設值 200 代表它會試著優化位元組碼,就像函式被呼叫 200 次一樣。 如果你將它改為 1,基本上就是告訴優化器,針對每個函式只執行一次的情況進行優化。 一個為執行一次而優化的函式,代表它是為部署本身而優化。 請注意,**這會增加執行函式的 [gas 成本](/developers/docs/gas/)**,所以你可能不想這麼做。 + +## 微小影響 {#small-impact} + +### 避免將 struct 傳遞給函式 {#avoid-passing-structs-to-functions} + +如果你正在使用 [ABIEncoderV2](https://solidity.readthedocs.io/en/v0.6.10/layout-of-source-files.html#abiencoderv2),不將 struct 傳遞給函式會有所幫助。 不要將參數作為 struct 傳遞,而是直接傳遞所需的參數。 在這個範例中,我們又節省了 **0.1kb**。 + +```solidity +function get(uint id) returns (address,address) { + return _get(myStruct); +} + +function _get(MyStruct memory myStruct) private view returns(address,address) { + return (myStruct.addr1, myStruct.addr2); +} +``` + +```solidity +function get(uint id) returns(address,address) { + return _get(myStructs[id].addr1, myStructs[id].addr2); +} + +function _get(address addr1, address addr2) private view returns(address,address) { + return (addr1, addr2); +} +``` + +### 為函式和變數宣告正確的可見性 {#declare-correct-visibility-for-functions-and-variables} + +- 只會從外部呼叫的函式或變數? 將它們宣告為 `external` 而不是 `public`。 +- 只在合約內部呼叫的函式或變數? 將它們宣告為 `private` 或 `internal` 而不是 `public`。 + +### 移除修飾符 {#remove-modifiers} + +修飾符,特別是當大量使用時,可能對合約大小產生重大影響。 考慮移除它們,改用函式。 + +```solidity +modifier checkStuff() {} + +function doSomething() checkStuff {} +``` + +```solidity +function checkStuff() private {} + +function doSomething() { checkStuff(); } +``` + +這些技巧應該能幫助你大幅縮小合約大小。 再次強調,如果可能的話,請務必專注於拆分合約,以達到最大的影響。 diff --git a/public/content/translations/zh-tw/developers/tutorials/eip-1271-smart-contract-signatures/index.md b/public/content/translations/zh-tw/developers/tutorials/eip-1271-smart-contract-signatures/index.md new file mode 100644 index 00000000000..a0b40fe862d --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/eip-1271-smart-contract-signatures/index.md @@ -0,0 +1,123 @@ +--- +title: "EIP-1271:簽署與驗證智能合約簽章" +description: "使用 EIP-1271 生成與驗證智能合約簽章的概覽。 我們也會逐步說明 Safe (前身為 Gnosis Safe) 中使用的 EIP-1271 實作,為智能合約開發者提供具體範例以供參考。" +author: Nathan H. Leung +lang: zh-tw +tags: [ "eip-1271", "智能合約", "驗證", "簽署" ] +skill: intermediate +published: 2023-01-12 +--- + +[EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) 標準允許智能合約驗證簽章。 + +在本教學中,我們將概覽數位簽章、EIP-1271 的背景,以及 [Safe](https://safe.global/) (前身為 Gnosis Safe) 所使用的 EIP-1271 特定實作。 總而言之,本篇內容可作為您在自己的合約中實作 EIP-1271 的起點。 + +## 什麼是簽章? + +在此背景下,簽章 (更準確地說,是「數位簽章」) 是一則訊息,加上某种證明,用以證明該訊息來自特定人士/傳送者/地址。 + +例如,數位簽章可能長這樣: + +1. 訊息:「我想用我的以太坊錢包登入這個網站。」 +2. 簽署者:我的地址是 `0x000…` +3. 證明:這是一些證明,證明我 (`0x000…`) 確實建立了這整則訊息 (這通常是某种加密的東西)。 + +請務必注意,數位簽章同時包含「訊息」與「簽章」。 + +為什麼? 舉例來說,如果您給我一份合約簽署,然後我把簽章頁撕下來,只把我的簽章還給您,而沒有合約的其他部分,那麼這份合約將無效。 + +同樣地,如果沒有相關的訊息,數位簽章就沒有任何意義! + +## EIP-1271 為何存在? + +為了建立在以太坊區塊鏈上使用的數位簽章,您通常需要一個其他人不知道的秘密私密金鑰。 這就是為什麼您的簽章專屬於您 (沒有其他人能在不知道秘密金鑰的情況下建立相同的簽章)。 + +您的以太坊帳戶 (即您的外部擁有帳戶/EOA) 有一個與其關聯的私密金鑰,當網站或去中心化應用程式要求您簽章 (例如,「用以太坊登入」) 時,通常會使用此私密金鑰。 + +應用程式可以使用 ethers.js 等第三方庫來 [驗證您建立的簽章](https://www.alchemy.com/docs/how-to-verify-a-message-signature-on-ethereum),且 [無需知道您的私密金鑰](https://en.wikipedia.org/wiki/Public-key_cryptography),並確信該簽章是由_您_所建立的。 + +> 事實上,由於 EOA 數位簽章使用公開金鑰密碼學,因此可以在**鏈外**生成與驗證! 這就是無 Gas 的 DAO 投票的運作方式 — 不在鏈上提交投票,而是使用加密庫在鏈外建立和驗證數位簽章。 + +雖然 EOA 帳戶有私密金鑰,但智能合約帳戶沒有任何形式的私密或秘密金鑰 (因此「用以太坊登入」等功能無法原生支援智能合約帳戶)。 + +EIP-1271 旨在解決的問題是:如果智能合約沒有可以納入簽章的「秘密」,我們如何判斷智能合約簽章是否有效? + +## EIP-1271 如何運作? + +智能合約沒有可用於簽署訊息的私密金鑰。 那麼,我們如何判斷簽章是否真實? + +嗯,一個想法是我們可以直接_詢問_智能合約簽章是否真實! + +EIP-1271 的作用是將「詢問」智能合約特定簽章是否有效的這個想法標準化。 + +實作 EIP-1271 的合約必須有一個名為 `isValidSignature` 的函式,該函式接收一個訊息和一個簽章。 然後,合約可以執行一些驗證邏輯 (規範在此處並未強制要求任何具體內容),然後傳回一個值,指出簽章是否有效。 + +如果 `isValidSignature` 傳回有效結果,這基本上就等於合約在說:「是的,我批准這個簽章 + 訊息!」 + +### 接口 + +以下是 EIP-1271 規範中的確切介面 (我們稍後會討論 `_hash` 參數,但現在,您可以將其視為正在驗證的訊息): + +```jsx +pragma solidity ^0.5.0; + +contract ERC1271 { + + // bytes4(keccak256("isValidSignature(bytes32,bytes)") + bytes4 constant internal MAGICVALUE = 0x1626ba7e; + + /** + * @dev 應傳回所提供的簽章對於所提供的哈希是否有效 + * @param _hash 待簽署資料的哈希 + * @param _signature 與 _hash 相關的簽章位元組陣列 + * + * 函式通過時必須傳回 bytes4 魔術值 0x1626ba7e。 + * 不得修改狀態 (對於 solc < 0.5 使用 STATICCALL,對於 solc > 0.5 使用 view 修飾符) + * 必須允許外部呼叫 + */ + function isValidSignature( + bytes32 _hash, + bytes memory _signature) + public + view + returns (bytes4 magicValue); +} +``` + +## EIP-1271 實作範例:Safe + +合約可以透過多種方式實作 `isValidSignature` — 該規範對於確切的實作方式並未多加說明。 + +一個實作 EIP-1271 的知名合約是 Safe (前身為 Gnosis Safe)。 + +在 Safe 的程式碼中,`isValidSignature` 的 [實作方式](https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol) 讓簽章可以透過 [兩種方式](https://ethereum.stackexchange.com/questions/122635/signing-messages-as-a-gnosis-safe-eip1271-support) 建立與驗證: + +1. 鏈上訊息 + 1. 建立:Safe 持有者建立一筆新的 Safe 交易來「簽署」一則訊息,並將該訊息作為資料傳入交易中。 一旦有足夠的持有者簽署交易,達到多重簽章門檻,交易就會被廣播並執行。 在交易中,有一個名為 (`signMessage(bytes calldata _data)`) 的 Safe 函式,它會將訊息新增到「已批准」的訊息清單中。 + 2. 驗證:在 Safe 合約上呼叫 `isValidSignature`,並將要驗證的訊息作為訊息參數傳入,同時為簽章參數傳入 [一個空值](https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol#L32) (即 `0x`)。 Safe 將會看到簽章參數為空,於是便不會以加密方式驗證簽章,而是會直接檢查該訊息是否在「已批准」的訊息清單上。 +2. 鏈外訊息: + 1. 建立:Safe 持有者在鏈外建立一則訊息,然後讓其他 Safe 持有者各自簽署該訊息,直到簽章數量足以超過多重簽章批准門檻為止。 + 2. 驗證:呼叫 `isValidSignature`。 在訊息參數中,傳入要驗證的訊息。 在簽章參數中,將每個 Safe 持有者的個人簽章一個接一個地串接起來傳入。 Safe 將會檢查是否有足夠的簽章來滿足門檻,**並且**每個簽章都有效。 如果是,它將傳回一個值,表示簽章驗證成功。 + +## `_hash` 參數到底是什麼? 為什麼不傳遞整個訊息? + +您可能已經注意到,[EIP-1271 介面](https://eips.ethereum.org/EIPS/eip-1271) 中的 `isValidSignature` 函式並非直接接收訊息本身,而是接收一個 `_hash` 參數。 這表示我們不是將完整的任意長度訊息傳遞給 `isValidSignature`,而是傳遞該訊息的 32 位元組哈希 (通常是 keccak256)。 + +calldata 的每個位元組 — 也就是傳遞給智能合約函式的函式參數資料 — [會花費 16 gas (如果為零位元組則為 4 gas)](https://eips.ethereum.org/EIPS/eip-2028),因此如果訊息很長,這樣可以節省大量 gas。 + +### 先前的 EIP-1271 規範 + +現存的一些 EIP-1271 規範中,`isValidSignature` 函式的第一個參數類型為 `bytes` (任意長度,而非固定長度的 `bytes32`),且參數名稱為 `message`。 這是 EIP-1271 標準的 [舊版](https://github.com/safe-global/safe-contracts/issues/391#issuecomment-1075427206)。 + +## 應如何在自己的合約中實作 EIP-1271? + +規範在此處非常有彈性。 Safe 的實作有一些不錯的想法: + +- 您可以將來自合約「持有者」的 EOA 簽章視為有效。 +- 您可以儲存一份已批准的訊息清單,並只將這些訊息視為有效。 + +最終,這取決於您這位合約開發者! + +## 結論 + +[EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) 是一個多功能的標準,允許智能合約驗證簽章。 它為智能合約開啟了大門,讓它們的行為更像 EOA — 例如,為「用以太坊登入」提供了一種與智能合約搭配運作的方式 — 並且可以透過多種方式實作 (Safe 有一個值得考慮的、非同小可且有趣的實作)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/erc-721-vyper-annotated-code/index.md b/public/content/translations/zh-tw/developers/tutorials/erc-721-vyper-annotated-code/index.md new file mode 100644 index 00000000000..872225c7ea8 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/erc-721-vyper-annotated-code/index.md @@ -0,0 +1,646 @@ +--- +title: "Vyper ERC-721 合約逐步解說" +description: "Ryuya Nakamura 的 ERC-721 合約及其運作方式" +author: Ori Pomerantz +lang: zh-tw +tags: [ "vyper", "erc-721", "python" ] +skill: beginner +published: 2021-04-01 +--- + +## 介紹 {#introduction} + +[ERC-721](/developers/docs/standards/tokens/erc-721/) 標準是用於持有非同質化代幣 (NFT) 所有權的標準。 +[ERC-20](/developers/docs/standards/tokens/erc-20/) 代幣的行為類似商品,因為個別代幣之間沒有任何區別。 +與之對比,ERC-721 代幣是為相似但不相同的資產所設計,例如不同的貓 +卡通或不同房地產的所有權狀。 + +在本文中,我們將分析 [Ryuya Nakamura 的 ERC-721 合約](https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC721.vy)。 +此合約以 [Vyper](https://vyper.readthedocs.io/en/latest/index.html) 編寫,這是一種近似 Python 的合約語言,其設計目的是讓編寫不安全的程式碼比在 Solidity 中更困難。 + +## 合約 {#contract} + +```python +# @dev ERC-721 非同質化代幣標準的實作。 +# @author Ryuya Nakamura (@nrryuya) +# 修改自:https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy +``` + +Vyper 中的註解與 Python 相同,以井字號 (`#`) 開頭,並一直延續到該行結尾。 包含 `@<關鍵字>` 的註解會由 [NatSpec](https://vyper.readthedocs.io/en/latest/natspec.html) 用來產生人類可讀的文件。 + +```python +from vyper.interfaces import ERC721 + +implements: ERC721 +``` + +ERC-721 介面內建於 Vyper 語言中。 +[你可以在這裡看到程式碼定義](https://github.com/vyperlang/vyper/blob/master/vyper/builtin_interfaces/ERC721.py)。 +介面定義是以 Python 而非 Vyper 編寫的,因為介面不僅在區塊鏈內部使用,也用於從可能以 Python 編寫的外部用戶端向區塊鏈傳送交易。 + +第一行匯入介面,第二行則指明我們在此處實作該介面。 + +### ERC721Receiver 介面 {#receiver-interface} + +```python +# safeTransferFrom() 呼叫的合約介面 +interface ERC721Receiver: + def onERC721Received( +``` + +ERC-721 支援兩種轉移類型: + +- `transferFrom`,它讓傳送方可以指定任何目標地址,並將轉移的責任歸於傳送方。 這意味著你可以轉移到無效地址,在這種情況下,NFT 將會永久遺失。 +- `safeTransferFrom`,它會檢查目標地址是否為合約。 若是,ERC-721 合約會詢問接收合約是否願意接收該 NFT。 + +為了回應 `safeTransferFrom` 的請求,接收合約必須實作 `ERC721Receiver`。 + +```python + _operator: address, + _from: address, +``` + +`_from` 地址是代幣的目前擁有者。 `_operator` 地址是請求轉移的地址 (由於有授權額度的關係,這兩個地址可能不相同)。 + +```python + _tokenId: uint256, +``` + +ERC-721 代幣 ID 是 256 位元。 一般而言,它們是透過對代幣所代表之物的描述進行哈希運算而建立的。 + +```python + _data: Bytes[1024] +``` + +此請求最多可包含 1024 位元組的使用者資料。 + +```python + ) -> bytes32: view +``` + +為防止合約意外接受轉移的情況,傳回值不是布林值,而是帶有特定值的 256 位元數值。 + +此函式為 `view` 函式,代表它可以讀取區塊鏈的狀態,但不能修改它。 + +### Events {#events} + +[事件](https://media.consensys.net/technical-introduction-to-events-and-logs-in-ethereum-a074d65dd61e) +會被發出以通知區塊鏈外部的使用者和伺服器發生的事件。 請注意,事件的內容對於區塊鏈上的合約是不可用的。 + +```python +# @dev 當任何 NFT 的所有權因任何機制發生變更時發出。當 NFT 被 +# 建立 (`from` == 0) 和銷毀 (`to` == 0) 時會發出此事件。例外:在合約建立期間,可以 +# 建立並指派任意數量的 NFT 而不發出 Transfer 事件。在任何 +# 轉移時,該 NFT 的核准地址 (若有) 會被重設為無。 +# @param _from NFT 的傳送方 (若地址為零地址,則表示代幣建立)。 +# @param _to NFT 的接收方 (若地址為零地址,則表示代幣銷毀)。 +# @param _tokenId 已轉移的 NFT。 +event Transfer: + sender: indexed(address) + receiver: indexed(address) + tokenId: indexed(uint256) +``` + +這與 ERC-20 的 Transfer 事件相似,差別在於我們回報的是 `tokenId` 而非數量。 +沒有人擁有零地址,因此按照慣例,我們用它來回報代幣的建立和銷毀。 + +```python +# @dev 當一個 NFT 的核准地址被變更或再次確認時發出。零 +# 地址表示沒有核准地址。當 Transfer 事件發出時,這也 +# 表示該 NFT 的核准地址 (若有) 被重設為無。 +# @param _owner NFT 的擁有者。 +# @param _approved 我們正在核准的地址。 +# @param _tokenId 我們正在核准的 NFT。 +event Approval: + owner: indexed(address) + approved: indexed(address) + tokenId: indexed(uint256) +``` + +ERC-721 的核准類似於 ERC-20 的授權額度。 一個特定的地址被允許轉移一個特定的代幣。 這提供了一種機制,讓合約在接受代幣時能夠做出回應。 合約無法監聽事件,所以如果你只是將代幣轉移給它們,它們不會「知道」這件事。 這樣一來,擁有者首先提交一項核准,然後向合約傳送請求:"我已核准你轉移代幣 +X,請執行..."。 + +這是一項設計上的選擇,目的是讓 ERC-721 標準與 ERC-20 標準相似。 由於 ERC-721 代幣並非同質化代幣,合約也可以透過查看代幣的所有權來識別它是否取得了特定的代幣。 + +```python +# @dev 當擁有者的操作員被啟用或停用時發出。操作員可以管理 +# 該擁有者的所有 NFT。 +# @param _owner NFT 的擁有者。 +# @param _operator 我們要為其設定操作員權限的地址。 +# @param _approved 操作員權限的狀態 (true 表示授予操作員權限,false 表示 +# 撤銷)。 +event ApprovalForAll: + owner: indexed(address) + operator: indexed(address) + approved: bool +``` + +有時候,有一個可以管理某帳戶所有特定類型代幣(由特定合約管理的代幣)的_操作員_是很有用的,這類似於授權書。 例如,我可能會想將此權力賦予一個合約,讓它檢查我是否六個月未與其聯繫,若是,則將我的資產分配給我的繼承人 (前提是其中一位繼承人提出請求,因為合約若無交易呼叫則無法執行任何動作)。 在 ERC-20 中,我們可以只給予繼承合約一個高額的授權額度,但這在 ERC-721 中行不通,因為其代幣並非同質化的。 這就是等效的做法。 + +`approved` 值告訴我們該事件是進行核准,還是撤銷核准。 + +### 狀態變數 {#state-vars} + +這些變數包含代幣的目前狀態:哪些是可用的以及誰擁有它們。 這些變數大部分是 `HashMap` 物件,是[存在於兩種類型之間的單向映射](https://vyper.readthedocs.io/en/latest/types.html#mappings)。 + +```python +# @dev 從 NFT ID 到其擁有者地址的映射。 +idToOwner: HashMap[uint256, address] + +# @dev 從 NFT ID 到核准地址的映射。 +idToApprovals: HashMap[uint256, address] +``` + +在以太坊中,使用者和合約的身分由 160 位元的地址表示。 這兩個變數從代幣 ID 映射到其擁有者以及被核准轉移它們的人 (每個代幣最多一個)。 在以太坊中,未初始化的資料始終為零,所以如果沒有擁有者或核准的轉移者,該代幣的值就是零。 + +```python +# @dev 從擁有者地址到其代幣數量的映射。 +ownerToNFTokenCount: HashMap[address, uint256] +``` + +此變數持有每個擁有者的代幣數量。 由於沒有從擁有者到代幣的映射,所以識別特定擁有者所擁有的代幣的唯一方法是回顧區塊鏈的事件歷史,並查看相應的 `Transfer` 事件。 我們可以使用這個變數來知道我們何時擁有了所有的 NFT,而不需要再回溯更早的時間。 + +請注意,此演算法僅適用於使用者介面和外部伺服器。 在區塊鏈上執行的程式碼本身無法讀取過去的事件。 + +```python +# @dev 從擁有者地址到操作員地址映射的映射。 +ownerToOperators: HashMap[address, HashMap[address, bool]] +``` + +一個帳戶可能有多個操作員。 一個簡單的 `HashMap` 並不足以追蹤它們,因為每個鍵只會對應到一個值。 取而代之,你可以使用 `HashMap[address, bool]` 作為值。 預設情況下,每個地址的值為 `False`,這意味著它不是操作員。 你可以視需要將值設定為 `True`。 + +```python +# @dev 鑄幣者的地址,可以鑄造代幣 +minter: address +``` + +新代幣必須以某種方式被建立出來。 在此合約中,只有一個實體被允許這麼做,即 `minter` (鑄幣者)。 例如,對於一個遊戲來說,這可能就足夠了。 對於其他目的,可能需要建立更複雜的商業邏輯。 + +```python +# @dev 從介面 ID 到布林值的映射,代表是否支援該介面 +supportedInterfaces: HashMap[bytes32, bool] + +# @dev ERC165 的 ERC165 介面 ID +ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7 + +# @dev ERC721 的 ERC165 介面 ID +ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd +``` + +[ERC-165](https://eips.ethereum.org/EIPS/eip-165) 規定了一種機制,讓合約能夠揭露應用程式可以如何與其通訊,以及它遵循哪些 ERC 標準。 在此例中,合約遵循 ERC-165 和 ERC-721。 + +### 函數 {#functions} + +這些是實際實作 ERC-721 的函式。 + +#### 建構函式 {#constructor} + +```python +@external +def __init__(): +``` + +在 Vyper 中,與 Python 相同,建構函式稱為 `__init__`。 + +```python + """ + @dev 合約建構函式。 + """ +``` + +在 Python 和 Vyper 中,你也可以透過指定一個多行字串 (以 `"""` 開頭和結尾) 且不以任何方式使用它來建立註解。 這些註解也可以包含 [NatSpec](https://vyper.readthedocs.io/en/latest/natspec.html)。 + +```python + self.supportedInterfaces[ERC165_INTERFACE_ID] = True + self.supportedInterfaces[ERC721_INTERFACE_ID] = True + self.minter = msg.sender +``` + +若要存取狀態變數,請使用 `self.<變數名稱>`` (同樣,這和 Python 一樣)。 + +#### 檢視函式 {#views} + +這些函式不會修改區塊鏈的狀態,因此如果從外部呼叫它們,可以免費執行。 如果檢視函式是由合約呼叫的,它們仍然必須在每個節點上執行,因此會產生 gas 費用。 + +```python +@view +@external +``` + +在函式定義之前,以 at 符號 (`@`) 開頭的這些關鍵字稱為_裝飾器_。 它們指定了可以呼叫函式的條件。 + +- `@view` 指定此函式為檢視函式。 +- `@external` 指定此特定函式可由交易和其他合約呼叫。 + +```python +def supportsInterface(_interfaceID: bytes32) -> bool: +``` + +與 Python 不同,Vyper 是一種[靜態型別語言](https://wikipedia.org/wiki/Type_system#Static_type_checking)。 +若不指定[資料類型](https://vyper.readthedocs.io/en/latest/types.html),則無法宣告變數或函式參數。 在此例中,輸入參數為 `bytes32`,一個 256 位元的值 (256 位元是 [以太坊虛擬機](/developers/docs/evm/) 的原生字組大小)。 輸出是一個布林值。 按照慣例,函式參數的名稱以下底線 (`_`) 開頭。 + +```python + """ + @dev 介面識別在 ERC-165 中指定。 + @param _interfaceID 介面的 ID + """ + return self.supportedInterfaces[_interfaceID] +``` + +從 `self.supportedInterfaces` HashMap 傳回值,該值在建構函式 (`__init__`) 中設定。 + +```python +### 檢視函式 ### + +``` + +這些是讓使用者和其他合約能取得代幣相關資訊的檢視函式。 + +```python +@view +@external +def balanceOf(_owner: address) -> uint256: + """ + @dev 傳回 `_owner` 擁有的 NFT 數量。 + 如果 `_owner` 是零地址,則會擲出錯誤。指派給零地址的 NFT 被視為無效。 + @param _owner 要查詢餘額的地址。 + """ + assert _owner != ZERO_ADDRESS +``` + +此行[斷言](https://vyper.readthedocs.io/en/latest/statements.html#assert)`_owner` 不為零。 如果為零,則會出現錯誤並且操作將被還原。 + +```python + return self.ownerToNFTokenCount[_owner] + +@view +@external +def ownerOf(_tokenId: uint256) -> address: + """ + @dev 傳回 NFT 擁有者的地址。 + 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。 + @param _tokenId NFT 的識別碼。 + """ + owner: address = self.idToOwner[_tokenId] + # 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤 + assert owner != ZERO_ADDRESS + return owner +``` + +在以太坊虛擬機 (evm) 中,任何沒有儲存值的儲存空間都為零。 +如果 `_tokenId` 沒有代幣,則 `self.idToOwner[_tokenId]` 的值為零。 在這種情況下,函式會還原。 + +```python +@view +@external +def getApproved(_tokenId: uint256) -> address: + """ + @dev 取得單一 NFT 的核准地址。 + 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。 + @param _tokenId 要查詢核准的 NFT ID。 + """ + # 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤 + assert self.idToOwner[_tokenId] != ZERO_ADDRESS + return self.idToApprovals[_tokenId] +``` + +注意,`getApproved` _可以_傳回零。 如果代幣有效,它會傳回 `self.idToApprovals[_tokenId]`。 +如果沒有核准者,該值為零。 + +```python +@view +@external +def isApprovedForAll(_owner: address, _operator: address) -> bool: + """ + @dev 檢查 `_operator` 是否為 `_owner` 的核准操作員。 + @param _owner 擁有 NFT 的地址。 + @param _operator 代表擁有者行事的地址。 + """ + return (self.ownerToOperators[_owner])[_operator] +``` + +此函式檢查 `_operator` 是否被允許管理此合約中 `_owner` 的所有代幣。 +因為可以有多個操作員,這是一個兩層的 HashMap。 + +#### 轉移輔助函式 {#transfer-helpers} + +這些函式實作了轉移或管理代幣的部分操作。 + +```python + +### 轉移函式輔助工具 ### + +@view +@internal +``` + +這個裝飾器 `@internal` 表示該函式只能在同一個合約內的其他函式中存取。 按照慣例,這些函式名稱也以下底線 (`_`) 開頭。 + +```python +def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool: + """ + @dev 傳回給定的花費者是否可以轉移給定的代幣 ID + @param spender 要查詢的花費者地址 + @param tokenId 要轉移的代幣的 uint256 ID + @return bool 表示 msg.sender 是否被核准用於給定的代幣 ID、 + 是否為擁有者的操作員,或是否為代幣的擁有者 + """ + owner: address = self.idToOwner[_tokenId] + spenderIsOwner: bool = owner == _spender + spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId] + spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender] + return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll +``` + +有三種方式可以讓一個地址被允許轉移代幣: + +1. 該地址是代幣的擁有者 +2. 該地址被核准花費該代幣 +3. 該地址是代幣擁有者的操作員 + +上面的函式可以是檢視函式,因為它不會改變狀態。 為了降低操作成本,任何_可以_成為檢視函式的函式都_應該_是檢視函式。 + +```python +@internal +def _addTokenTo(_to: address, _tokenId: uint256): + """ + @dev 將 NFT 新增到給定地址 + 如果 `_tokenId` 已被他人擁有,則會擲出錯誤。 + """ + # 如果 `_tokenId` 已被他人擁有,則會擲出錯誤 + assert self.idToOwner[_tokenId] == ZERO_ADDRESS + # 變更擁有者 + self.idToOwner[_tokenId] = _to + # 變更計數追蹤 + self.ownerToNFTokenCount[_to] += 1 + + +@internal +def _removeTokenFrom(_from: address, _tokenId: uint256): + """ + @dev 從給定地址移除 NFT + 如果 `_from` 不是目前的擁有者,則會擲出錯誤。 + """ + # 如果 `_from` 不是目前的擁有者,則會擲出錯誤 + assert self.idToOwner[_tokenId] == _from + # 變更擁有者 + self.idToOwner[_tokenId] = ZERO_ADDRESS + # 變更計數追蹤 + self.ownerToNFTokenCount[_from] -= 1 +``` + +當轉移出現問題時,我們會還原該呼叫。 + +```python +@internal +def _clearApproval(_owner: address, _tokenId: uint256): + """ + @dev 清除給定地址的核准 + 如果 `_owner` 不是目前的擁有者,則會擲出錯誤。 + """ + # 如果 `_owner` 不是目前的擁有者,則會擲出錯誤 + assert self.idToOwner[_tokenId] == _owner + if self.idToApprovals[_tokenId] != ZERO_ADDRESS: + # 重設核准 + self.idToApprovals[_tokenId] = ZERO_ADDRESS +``` + +只有在必要時才變更值。 狀態變數存在儲存空間中。 寫入儲存空間是 EVM (以太坊虛擬機) 執行成本最昂貴的操作之一 (以 [gas](/developers/docs/gas/) 費用計算)。 因此,最好盡量減少寫入,即使寫入現有值也具有高成本。 + +```python +@internal +def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address): + """ + @dev 執行 NFT 的轉移。 + 除非 `msg.sender` 是目前的擁有者、授權的操作員或此 NFT 的核准 + 地址,否則會擲出錯誤。(注意:私有函式中不允許 `msg.sender`,所以傳入 `_sender`。) + 如果 `_to` 是零地址,則會擲出錯誤。 + 如果 `_from` 不是目前的擁有者,則會擲出錯誤。 + 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。 + """ +``` + +我們有這個內部函式,因為有兩種轉移代幣的方式 (常規和安全),但我們希望只在程式碼中的一個位置執行它,以便於審核。 + +```python + # 檢查需求 + assert self._isApprovedOrOwner(_sender, _tokenId) + # 如果 `_to` 是零地址,則會擲出錯誤 + assert _to != ZERO_ADDRESS + # 清除核准。如果 `_from` 不是目前的擁有者,則會擲出錯誤 + self._clearApproval(_from, _tokenId) + # 移除 NFT。如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤 + self._removeTokenFrom(_from, _tokenId) + # 新增 NFT + self._addTokenTo(_to, _tokenId) + # 記錄轉移 + log Transfer(_from, _to, _tokenId) +``` + +若要在 Vyper 中發出事件,請使用 `log` 陳述式 ([在此處查看更多詳細資訊](https://vyper.readthedocs.io/en/latest/event-logging.html#event-logging))。 + +#### 轉移函式 {#transfer-funs} + +```python + +### 轉移函式 ### + +@external +def transferFrom(_from: address, _to: address, _tokenId: uint256): + """ + @dev 除非 `msg.sender` 是目前的擁有者、授權的操作員或此 NFT 的核准 + 地址,否則會擲出錯誤。 + 如果 `_from` 不是目前的擁有者,則會擲出錯誤。 + 如果 `_to` 是零地址,則會擲出錯誤。 + 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。 + @notice 呼叫者有責任確認 `_to` 能夠接收 NFT,否則 + 它們可能會永久遺失。 + @param _from NFT 的目前擁有者。 + @param _to 新的擁有者。 + @param _tokenId 要轉移的 NFT。 + """ + self._transferFrom(_from, _to, _tokenId, msg.sender) +``` + +此函式可讓你轉移到任意地址。 除非該地址是使用者,或是知道如何轉移代幣的合約,否則你轉移的任何代幣都會卡在該地址中而無法使用。 + +```python +@external +def safeTransferFrom( + _from: address, + _to: address, + _tokenId: uint256, + _data: Bytes[1024]=b"" + ): + """ + @dev 將 NFT 的所有權從一個地址轉移到另一個地址。 + 除非 `msg.sender` 是目前的擁有者、授權的操作員或此 + NFT 的核准地址,否則會擲出錯誤。 + 如果 `_from` 不是目前的擁有者,則會擲出錯誤。 + 如果 `_to` 是零地址,則會擲出錯誤。 + 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。 + 如果 `_to` 是智慧合約,它會在 `_to` 上呼叫 `onERC721Received`,如果 + 傳回值不是 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`,則會擲出錯誤。 + 注意:bytes4 由帶有填充的 bytes32 表示 + @param _from NFT 的目前擁有者。 + @param _to 新的擁有者。 + @param _tokenId 要轉移的 NFT。 + @param _data 額外資料,無指定格式,在對 `_to` 的呼叫中傳送。 + """ + self._transferFrom(_from, _to, _tokenId, msg.sender) +``` + +先執行轉移是沒問題的,因為如果出現問題,我們無論如何都會還原,所以在呼叫中完成的所有事情都將被取消。 + +```python + if _to.is_contract: # 檢查 `_to` 是否為合約地址 +``` + +首先檢查該地址是否為合約 (如果它有程式碼)。 如果不是,則假設它是一個使用者地址,使用者將能夠使用或轉移該代幣。 但不要讓它讓你產生虛假的安全感。 即使使用 `safeTransferFrom`,如果你將代幣轉移到一個沒人知道私鑰的地址,你仍然可能會遺失代幣。 + +```python + returnValue: bytes32 = ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) +``` + +呼叫目標合約,看它是否能接收 ERC-721 代幣。 + +```python + # 如果轉移目標是不實作 'onERC721Received' 的合約,則會擲出錯誤 + assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32) +``` + +如果目標是一個合約,但不接受 ERC-721 代幣 (或決定不接受此特定轉移),則還原。 + +```python +@external +def approve(_approved: address, _tokenId: uint256): + """ + @dev 設定或再次確認 NFT 的核准地址。零地址表示沒有核准地址。 + 除非 `msg.sender` 是目前的 NFT 擁有者,或目前擁有者的授權操作員,否則會擲出錯誤。 + 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。(注意:這未在 EIP 中撰寫) + 如果 `_approved` 是目前的擁有者,則會擲出錯誤。(注意:這未在 EIP 中撰寫) + @param _approved 要為給定 NFT ID 核准的地址。 + @param _tokenId 要核准的代幣 ID。 + """ + owner: address = self.idToOwner[_tokenId] + # 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤 + assert owner != ZERO_ADDRESS + # 如果 `_approved` 是目前的擁有者,則會擲出錯誤 + assert _approved != owner +``` + +按照慣例,如果你不想有核准者,你應該指定零地址,而不是你自己。 + +```python + # 檢查需求 + senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender + senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender] + assert (senderIsOwner or senderIsApprovedForAll) +``` + +要設定核准,你可以是擁有者,也可以是擁有者授權的操作員。 + +```python + # 設定核准 + self.idToApprovals[_tokenId] = _approved + log Approval(owner, _approved, _tokenId) + + +@external +def setApprovalForAll(_operator: address, _approved: bool): + """ + @dev 啟用或停用第三方 (「操作員」) 管理 `msg.sender` + 所有資產的核准。它也會發出 ApprovalForAll 事件。 + 如果 `_operator` 是 `msg.sender`,則會擲出錯誤。(注意:這未在 EIP 中撰寫) + @notice 即使傳送方當時沒有任何代幣,此函式也有效。 + @param _operator 要新增到授權操作員集合的地址。 + @param _approved 如果操作員被核准則為 True,若要撤銷核准則為 false。 + """ + # 如果 `_operator` 是 `msg.sender`,則會擲出錯誤 + assert _operator != msg.sender + self.ownerToOperators[msg.sender][_operator] = _approved + log ApprovalForAll(msg.sender, _operator, _approved) +``` + +#### 鑄造新代幣和銷毀現有代幣 {#mint-burn} + +建立合約的帳戶是 `minter` (鑄幣者),即有權鑄造新 NFT 的超級使用者。 然而,即使是鑄幣者也不被允許銷毀現有的代幣。 只有擁有者或由擁有者授權的實體才能這麼做。 + +```python +### 鑄幣與銷毀函式 ### + +@external +def mint(_to: address, _tokenId: uint256) -> bool: +``` + +此函式始終傳回 `True`,因為如果操作失敗,它將會被還原。 + +```python + """ + @dev 鑄造代幣的函式 + 如果 `msg.sender` 不是鑄幣者,則會擲出錯誤。 + 如果 `_to` 是零地址,則會擲出錯誤。 + 如果 `_tokenId` 已被他人擁有,則會擲出錯誤。 + @param _to 將接收鑄造代幣的地址。 + @param _tokenId 要鑄造的代幣 id。 + @return 一個布林值,表示操作是否成功。 + """ + # 如果 `msg.sender` 不是鑄幣者,則會擲出錯誤 + assert msg.sender == self.minter +``` + +只有鑄幣者 (建立 ERC-721 合約的帳戶) 才能鑄造新代幣。 如果我們未來想變更鑄幣者的身分,這可能會成為一個問題。 在一個生產合約中,你可能需要一個函式,允許鑄幣者將鑄幣權限轉移給其他人。 + +```python + # 如果 `_to` 是零地址,則會擲出錯誤 + assert _to != ZERO_ADDRESS + # 新增 NFT。如果 `_tokenId` 已被他人擁有,則會擲出錯誤 + self._addTokenTo(_to, _tokenId) + log Transfer(ZERO_ADDRESS, _to, _tokenId) + return True +``` + +按照慣例,鑄造新代幣被視為從零地址轉出。 + +```python + +@external +def burn(_tokenId: uint256): + """ + @dev 銷毀特定的 ERC721 代幣。 + 除非 `msg.sender` 是目前的擁有者、授權的操作員或此 NFT 的核准 + 地址,否則會擲出錯誤。 + 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤。 + @param _tokenId 要銷毀的 ERC721 代幣的 uint256 id。 + """ + # 檢查需求 + assert self._isApprovedOrOwner(msg.sender, _tokenId) + owner: address = self.idToOwner[_tokenId] + # 如果 `_tokenId` 不是有效的 NFT,則會擲出錯誤 + assert owner != ZERO_ADDRESS + self._clearApproval(owner, _tokenId) + self._removeTokenFrom(owner, _tokenId) + log Transfer(owner, ZERO_ADDRESS, _tokenId) +``` + +任何被允許轉移代幣的人都可以銷毀它。 雖然銷毀看起來等同於轉移到零地址,但零地址實際上並不會收到代幣。 這讓我們可以釋放用於該代幣的所有儲存空間,這可以降低交易的 gas 成本。 + +## 使用此合約 {#using-contract} + +與 Solidity 不同,Vyper 沒有繼承機制。 這是一項刻意的設計選擇,旨在讓程式碼更清晰,從而更容易確保其安全性。 因此,若要建立自己的 Vyper ERC-721 合約,你可以拿取[此合約](https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC721.vy)並修改它,以實作你想要的商業邏輯。 + +## 結論 {#conclusion} + +為了複習,以下是此合約中一些最重要的概念: + +- 要透過安全轉移接收 ERC-721 代幣,合約必須實作 `ERC721Receiver` 介面。 +- 即使你使用安全轉移,如果你將代幣傳送到私鑰未知的地址,代幣仍然可能會卡住。 +- 當操作出現問題時,最好是 `revert` (還原) 該呼叫,而不是只傳回一個失敗值。 +- ERC-721 代幣在其有擁有者時存在。 +- 有三種方式可以獲得轉移 NFT 的授權。 你可以是擁有者、被核准用於特定代幣,或是擁有者所有代幣的操作員。 +- 過去的事件只有在區塊鏈外部才可見。 在區塊鏈內部執行的程式碼無法檢視它們。 + +現在去實作安全的 Vyper 合約吧。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 + diff --git a/public/content/translations/zh-tw/developers/tutorials/erc20-annotated-code/index.md b/public/content/translations/zh-tw/developers/tutorials/erc20-annotated-code/index.md new file mode 100644 index 00000000000..1221adcb386 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/erc20-annotated-code/index.md @@ -0,0 +1,803 @@ +--- +title: "ERC-20 合約逐步解說" +description: "OpenZeppelin 的 ERC-20 合約內容是什麼?這些內容又為何存在?" +author: Ori Pomerantz +lang: zh-tw +tags: [ "穩固", "erc-20" ] +skill: beginner +published: 2021-03-09 +--- + +## 介紹 {#introduction} + +以太坊最常見的用處之一就是為一個團隊建立一種可交易的代幣。某種意義上,這是屬於他們自己的貨幣。 這些代幣通常會遵循一項標準,即 [ERC-20](/developers/docs/standards/tokens/erc-20/)。 這項標準讓開發能與所有 ERC-20 代幣相容的工具(例如流動性池和錢包)成為可能。 在本文中,我們將分析 [OpenZeppelin Solidity ERC20 實作](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol) 以及 [介面定義](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol)。 + +這是附有註解的原始碼。 如果您想實作 ERC-20,請[閱讀本教學](https://docs.openzeppelin.com/contracts/2.x/erc20-supply)。 + +## 介面 {#the-interface} + +像 ERC-20 這樣的標準,其目的是為了讓許多代幣實作能夠在錢包和去中心化交易所等應用程式之間互通。 為了達成這個目標,我們建立一個[介面](https://www.geeksforgeeks.org/solidity/solidity-basics-of-interface/)。 任何需要使用代幣合約的程式碼,都可以使用介面中的相同定義,並與所有使用該介面的代幣合約相容,無論是像 MetaMask 這樣的錢包、etherscan.io 這樣的 dapp,或是像流動性池這樣的不同合約。 + +![ERC-20 介面圖解](erc20_interface.png) + +如果你是有經驗的程式設計師,你可能還記得在 [Java](https://www.w3schools.com/java/java_interface.asp) 或甚至 [C 標頭檔](https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html) 中看過類似的結構。 + +這是 OpenZeppelin 的 [ERC-20 介面](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol) 定義。 它是將[人類可讀標準](https://eips.ethereum.org/EIPS/eip-20)翻譯成 Solidity 程式碼的結果。 當然,介面本身並未定義要 _如何_ 做任何事。 這點會在下方的合約原始碼中解釋。 + +  + +```solidity +// SPDX-License-Identifier: MIT +``` + +Solidity 檔案應該要包含一個授權許可證識別碼。 [您可以在此處查看授權許可證清單](https://spdx.org/licenses/)。 如果您需要不同的授權許可證,只要在註解中說明即可。 + +  + +```solidity +pragma solidity >=0.6.0 <0.8.0; +``` + +Solidity 語言仍在快速發展,新版本可能與舊程式碼不相容([請參見此處](https://docs.soliditylang.org/en/v0.7.0/070-breaking-changes.html))。 因此,最好不僅指定語言的最低版本,還要指定最高版本,也就是您用來測試程式碼的最新版本。 + +  + +```solidity +/** + * @dev EIP 中定義的 ERC20 標準介面。 + */ +``` + +註解中的 `@dev` 是 [NatSpec 格式](https://docs.soliditylang.org/en/develop/natspec-format.html) 的一部分,用於從原始碼產生文件。 + +  + +```solidity +interface IERC20 { +``` + +按照慣例,介面名稱以 `I` 開頭。 + +  + +```solidity + /** + * @dev 傳回現存的代幣數量。 + */ + function totalSupply() external view returns (uint256); +``` + +此函式為 `external`,表示[它只能從合約外部呼叫](https://docs.soliditylang.org/en/v0.7.0/cheatsheet.html#index-2)。 +它會傳回合約中的代幣總供應量。 此值使用以太坊中最常見的類型傳回,即無正負號 256 位元(256 位元是 EVM 的原生字組大小)。 此函式也是 `view`,表示它不會改變狀態,因此可以在單一節點上執行,而不需由區塊鏈中的每個節點都執行它。 這類函式不會產生交易,也不會花費 [Gas](/developers/docs/gas/)。 + +**注意:** 理論上,合約創建者似乎可以透過傳回比實際價值還小的總供應量來作弊,讓每個代幣看起來比實際更有價值。 然而,這種恐懼忽略了區塊鏈的真正本質。 區塊鏈上發生的每件事都可以由每個節點驗證。 為了達成這點,每個合約的機器語言程式碼和儲存空間在每個節點上都是可用的。 雖然您不需要為您的合約發布 Solidity 程式碼,但除非您發布原始碼以及編譯時所用的 Solidity 版本,否則沒有人會認真看待您的合約,這樣才能將其與您提供的機器語言程式碼進行驗證比對。 +例如,請參見[此合約](https://eth.blockscout.com/address/0xa530F85085C6FE2f866E7FdB716849714a89f4CD?tab=contract)。 + +  + +```solidity + /** + * @dev 傳回 `account` 擁有的代幣數量。 + */ + function balanceOf(address account) external view returns (uint256); +``` + +如同其名,`balanceOf` 傳回帳戶的餘額。 以太坊帳戶在 Solidity 中使用 `address` 類型來識別,該類型持有 160 位元。 +它也是 `external` 和 `view`。 + +  + +```solidity + /** + * @dev 將 `amount` 數量的代幣從呼叫者的帳戶轉移到 `recipient`。 + * + * 傳回一個布林值,表示操作是否成功。 + * + * 發出一個 {Transfer} 事件。 + */ + function transfer(address recipient, uint256 amount) external returns (bool); +``` + +`transfer` 函式將代幣從呼叫者轉移到一個不同的地址。 這涉及到狀態的改變,所以它不是一個 `view`。 +當使用者呼叫此函式時,會建立一筆交易並花費 Gas。 它也會發出一個 `Transfer` 事件,通知區塊鏈上的每個人該事件的發生。 + +此函式針對兩種不同類型的呼叫者,有兩種輸出類型: + +- 從使用者介面直接呼叫函式的使用者。 通常使用者提交交易後,不會等待回應,因為回應可能需要不確定的時間。 使用者可以透過查看交易收據(由交易哈希識別)或尋找 `Transfer` 事件來了解發生了什麼事。 +- 其他合約,將函式呼叫作為整體交易的一部分。 這些合約會立即得到結果,因為它們在同一個交易中執行,所以它們可以使用函式的傳回值。 + +其他改變合約狀態的函式也會產生相同類型的輸出。 + +  + +授權額度允許一個帳戶花費屬於不同所有者的代幣。 +舉例來說,這對於作為賣方的合約很有用。 合約無法監控事件,所以如果買方直接將代幣轉移給賣方合約,該合約不會知道它已收到款項。 取而代之的是,買方授權賣方合約花費一定金額,然後由賣方轉移該金額。 +這是透過賣方合約呼叫的一個函式來完成的,因此賣方合約可以知道它是否成功。 + +```solidity + /** + * @dev 傳回 `spender` 將被允許透過 {transferFrom} 代表 `owner` 花費的 + * 剩餘代幣數量。預設為零。 + * + * 當 {approve} 或 {transferFrom} 被呼叫時,此值會改變。 + */ + function allowance(address owner, address spender) external view returns (uint256); +``` + +`allowance` 函式讓任何人都可以查詢一個地址 (`owner`) 允許另一個地址 (`spender`) 花費的授權額度。 + +  + +```solidity + /** + * @dev 將 `spender` 對呼叫者代幣的授權額度設為 `amount`。 + * + * 傳回一個布林值,表示操作是否成功。 + * + * 重要:請注意,使用此方法更改授權額度存在風險, + * 有人可能會因不幸的交易排序而同時使用舊的和新的授權額度。一種可能的解決方案是 + * 先將花費者的授權額度降至 0,然後再設定所需的值,以減輕這種競爭 + * 條件: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * 發出一個 {Approval} 事件。 + */ + function approve(address spender, uint256 amount) external returns (bool); +``` + +`approve` 函式會建立一個授權額度。 請務必閱讀有關它如何被濫用的訊息。 在以太坊中,您可以控制自己交易的順序,但無法控制其他人交易的執行順序,除非您在看到對方的交易發生後才提交自己的交易。 + +  + +```solidity + /** + * @dev 使用授權額度機制將 `amount` 的代幣從 `sender` 轉移到 `recipient`。 + * 然後,`amount` 會從呼叫者的授權額度中扣除。 + * + * 傳回一個布林值,表示操作是否成功。 + * + * 發出一個 {Transfer} 事件。 + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); +``` + +最後,`transferFrom` 由花費者用來實際花費授權額度。 + +  + +```solidity + + /** + * @dev 當 `value` 數量的代幣從一個帳戶 (`from`) 移動到 + * 另一個帳戶 (`to`) 時發出。 + * + * 請注意 `value` 可能為零。 + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev 當 `owner` 的 `spender` 的授權額度透過 + * 呼叫 {approve} 設定時發出。`value` 是新的授權額度。 + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} +``` + +當 ERC-20 合約的狀態改變時,這些事件會被發出。 + +## 實際的合約 {#the-actual-contract} + +這是實作 ERC-20 標準的實際合約,[取自此處](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol)。 +它並非設計來直接使用,但您可以[繼承](https://www.tutorialspoint.com/solidity/solidity_inheritance.htm)它來擴展成可用的東西。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0 <0.8.0; +``` + +  + +### 匯入陳述式 {#import-statements} + +除了上述的介面定義外,合約定義還匯入了另外兩個檔案: + +```solidity + +import "../../GSN/Context.sol"; +import "./IERC20.sol"; +import "../../math/SafeMath.sol"; +``` + +- `GSN/Context.sol` 是使用 [OpenGSN](https://www.opengsn.org/) 所需的定義,這是一個允許沒有以太幣的使用者使用區塊鏈的系統。 請注意這是舊版本,如果您想與 OpenGSN 整合,[請使用此教學](https://docs.opengsn.org/javascript-client/tutorial.html)。 +- [SafeMath 程式庫](https://ethereumdev.io/using-safe-math-library-to-prevent-from-overflows/),它能防止 Solidity 版本 **<0.8.0** 的算術溢位/下溢。 在 Solidity ≥0.8.0 中,算術運算會在溢位/下溢時自動還原,使得 SafeMath 不再必要。 此合約使用 SafeMath 以便向後相容於舊的編譯器版本。 + +  + +此註解說明了合約的用途。 + +```solidity +/** + * @dev {IERC20} 介面的實作。 + * + * 此實作與代幣的創建方式無關。這意味著 + * 必須在衍生合約中使用 {_mint} 新增供應機制。 + * 若要使用通用機制,請參閱 {ERC20PresetMinterPauser}。 + * + * 提示:詳細說明請參閱我們的指南 + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[如何 + * 實作供應機制]。 + * + * 我們遵循了一般的 OpenZeppelin 指南:函式在失敗時會還原 + * 而非傳回 `false`。此行為是常規的 + * 且不與 ERC20 應用程式的預期衝突。 + * + * 此外,在呼叫 {transferFrom} 時會發出 {Approval} 事件。 + * 這允許應用程式僅透過監聽所述事件來重構所有帳戶的授權額度。 + * EIP 的其他實作可能不會發出這些事件, + * 因為規範並未要求。 + * + * 最後,新增了非標準的 {decreaseAllowance} 和 {increaseAllowance} + * 函式來緩解圍繞設定授權額度的眾所周知的問題。 + * 請參閱 {IERC20-approve}。 + */ + +``` + +### 合約定義 {#contract-definition} + +```solidity +contract ERC20 is Context, IERC20 { +``` + +此行指定了繼承關係,在此例中是繼承自上方的 `IERC20` 和用於 OpenGSN 的 `Context`。 + +  + +```solidity + + using SafeMath for uint256; + +``` + +此行將 `SafeMath` 程式庫附加到 `uint256` 類型上。 您可以在[此處](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol)找到此程式庫。 + +### 變數定義 {#variable-definitions} + +這些定義指定了合約的狀態變數。 這些變數被宣告為 `private`,但這只意味著區塊鏈上的其他合約無法讀取它們。 _區塊鏈上沒有秘密_,每個節點上的軟體都擁有每個合約在每個區塊的狀態。 按照慣例,狀態變數的命名方式為 `_`。 + +前兩個變數是[映射](https://www.tutorialspoint.com/solidity/solidity_mappings.htm),這意味著它們的行為大致與[關聯陣列](https://wikipedia.org/wiki/Associative_array)相同,只是鍵是數值。 只有值與預設值(零)不同的條目才會被分配儲存空間。 + +```solidity + mapping (address => uint256) private _balances; +``` + +第一個映射 `_balances` 是地址及其對應的此代幣餘額。 要存取餘額,請使用此語法:`_balances[
]`。 + +  + +```solidity + mapping (address => mapping (address => uint256)) private _allowances; +``` + +這個變數 `_allowances` 儲存了先前解釋過的授權額度。 第一個索引是代幣的所有者,第二個是擁有授權額度的合約。 要存取地址 A 可以從地址 B 帳戶花費的金額,請使用 `_allowances[B][A]`。 + +  + +```solidity + uint256 private _totalSupply; +``` + +如同其名,這個變數追蹤代幣的總供應量。 + +  + +```solidity + string private _name; + string private _symbol; + uint8 private _decimals; +``` + +這三個變數是用來提高可讀性的。 前兩個不言自明,但 `_decimals` 則不是。 + +一方面,以太坊沒有浮點數或分數變數。 另一方面,人類喜歡能夠分割代幣。 人們選擇黃金作為貨幣的一個原因是,當有人想用牛換取等值的鴨子時,很難找零。 + +解決方法是追蹤整數,但計算的不是實際的代幣,而是一個幾乎沒有價值的分數代幣。 以以太幣為例,分數代幣稱為 wei,而 10^18 wei 等於一 ETH。 在撰寫本文時,10,000,000,000,000 wei 約等於一美分或歐分。 + +應用程式需要知道如何顯示代幣餘額。 如果一個使用者有 3,141,000,000,000,000,000 wei,那是 3.14 ETH 嗎? 31.41 ETH? 3,141 ETH? 以以太幣為例,定義是 10^18 wei 等於一 ETH,但對於您的代幣,您可以選擇不同的值。 如果分割代幣沒有意義,您可以使用 `_decimals` 值為零。 如果您想使用與 ETH 相同的標準,請使用值 **18**。 + +### 建構函式 {#the-constructor} + +```solidity + /** + * @dev 設定 {name} 和 {symbol} 的值,並將 {decimals} 初始化為 + * 預設值 18。 + * + * 要為 {decimals} 選擇不同的值,請使用 {_setupDecimals}。 + * + * 這三個值都是不可變的:它們只能在建構期間設定一次。 + */ + constructor (string memory name_, string memory symbol_) public { + // 在 Solidity ≥0.7.0 中,'public' 是隱含的,可以省略。 + + _name = name_; + _symbol = symbol_; + _decimals = 18; + } +``` + +建構函式在合約首次建立時被呼叫。 按照慣例,函式參數的命名方式為 `_`。 + +### 使用者介面函式 {#user-interface-functions} + +```solidity + /** + * @dev 傳回代幣的名稱。 + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev 傳回代幣的符號,通常是名稱的較短版本。 + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev 傳回用於獲取其使用者表示的小數位數。 + * 例如,如果 `decimals` 等於 `2`,`505` 代幣的餘額應 + * 對使用者顯示為 `5,05` (`505 / 10 ** 2`)。 + * + * 代幣通常選擇值 18,模仿以太幣和 wei 之間的關係。這是 {ERC20} 使用的值, + * 除非呼叫 {_setupDecimals}。 + * + * 注意:此資訊僅用於 _顯示_ 目的:它在 + * 任何方面都不會影響合約的任何算術,包括 + * {IERC20-balanceOf} 和 {IERC20-transfer}。 + */ + function decimals() public view returns (uint8) { + return _decimals; + } +``` + +這些函式 `name`、`symbol` 和 `decimals` 幫助使用者介面了解您的合約,以便它們能夠正確顯示。 + +傳回類型是 `string memory`,意思是傳回一個儲存在記憶體中的字串。 變數,例如字串,可以儲存在三個位置: + +| | 生命週期 | 合約存取 | Gas 成本 | +| -------- | ---- | ---- | ------------------- | +| 記憶體 | 函式呼叫 | 讀/寫 | 數十或數百(位置越高,成本越高) | +| Calldata | 函式呼叫 | 唯讀 | 不能作為傳回類型,只能作為函式參數類型 | +| 儲存 | 直到改變 | 讀/寫 | 高(讀取為 800,寫入為 2 萬) | + +在這種情況下,`memory` 是最佳選擇。 + +### 讀取代幣資訊 {#read-token-information} + +這些是提供代幣資訊的函式,可以是總供應量或帳戶餘額。 + +```solidity + /** + * @dev 請參閱 {IERC20-totalSupply}。 + */ + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } +``` + +`totalSupply` 函式傳回代幣的總供應量。 + +  + +```solidity + /** + * @dev 請參閱 {IERC20-balanceOf}。 + */ + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } +``` + +讀取帳戶的餘額。 請注意,任何人都可以獲取任何其他人的帳戶餘額。 試圖隱藏這些資訊是沒有意義的,因為它在每個節點上都是可用的。 _區塊鏈上沒有秘密。_ + +### 轉移代幣 {#transfer-tokens} + +```solidity + /** + * @dev 請參閱 {IERC20-transfer}。 + * + * 要求: + * + * - `recipient` 不能是零地址。 + * - 呼叫者必須擁有至少 `amount` 的餘額。 + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { +``` + +`transfer` 函式被呼叫來將代幣從發送者的帳戶轉移到另一個帳戶。 請注意,即使它傳回一個布林值,該值也始終為 **true**。 如果轉帳失敗,合約會還原該呼叫。 + +  + +```solidity + _transfer(_msgSender(), recipient, amount); + return true; + } +``` + +`_transfer` 函式執行實際的工作。 它是一個私有函式,只能由其他合約函式呼叫。 按照慣例,私有函式的命名方式與狀態變數相同,都是 `_`。 + +通常在 Solidity 中,我們使用 `msg.sender` 來表示訊息發送者。 然而,這會破壞 [OpenGSN](http://opengsn.org/)。 如果我們想讓我們的代幣允許無以太幣的交易,我們需要使用 `_msgSender()`。 對於正常交易,它傳回 `msg.sender`,但對於無以太幣的交易,它傳回原始簽署者,而不是轉發訊息的合約。 + +### 授權額度函式 {#allowance-functions} + +這些是實作授權額度功能的函式:`allowance`、`approve`、`transferFrom` 和 `_approve`。 此外,OpenZeppelin 的實作超越了基本標準,包含了一些提高安全性的功能:`increaseAllowance` 和 `decreaseAllowance`。 + +#### allowance 函式 {#allowance} + +```solidity + /** + * @dev 請參閱 {IERC20-allowance}。 + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } +``` + +`allowance` 函式允許每個人檢查任何授權額度。 + +#### approve 函式 {#approve} + +```solidity + /** + * @dev 請參閱 {IERC20-approve}。 + * + * 要求: + * + * - `spender` 不能是零地址。 + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { +``` + +此函式被呼叫來建立一個授權額度。 它與上面的 `transfer` 函式相似: + +- 此函式僅呼叫一個執行實際工作的內部函式(在本例中為 `_approve`)。 +- 此函式要麼傳回 `true`(如果成功),要麼還原(如果不成功)。 + +  + +```solidity + _approve(_msgSender(), spender, amount); + return true; + } +``` + +我們使用內部函式來最小化狀態變更發生的位置數量。 _任何_ 改變狀態的函式都是一個潛在的安全風險,需要進行安全審計。 這樣我們出錯的機會就更少了。 + +#### transferFrom 函式 {#transferFrom} + +這是花費者呼叫來花費授權額度的函式。 這需要兩個操作:轉移花費的金額,並將授權額度減少該金額。 + +```solidity + /** + * @dev 請參閱 {IERC20-transferFrom}。 + * + * 發出一個 {Approval} 事件,表示已更新的授權額度。這不是 EIP 所要求的。 + * 請參閱 {ERC20} 開頭的說明。 + * + * 要求: + * + * - `sender` 和 `recipient` 不能是零地址。 + * - `sender` 必須擁有至少 `amount` 的餘額。 + * - 呼叫者對 ``sender`` 的代幣的授權額度必須至少為 + * `amount`。 + */ + function transferFrom(address sender, address recipient, uint256 amount) public virtual + override returns (bool) { + _transfer(sender, recipient, amount); +``` + +  + +`a.sub(b, "message")` 函式呼叫做兩件事。 首先,它計算 `a-b`,即新的授權額度。 +其次,它檢查此結果是否為負。 如果為負,則呼叫會以提供的訊息還原。 請註意,撤銷調用後,之前在調用中完成的任何處理都會被忽略,所以我們不需要撤消 _transfer。 + +```solidity + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, + "ERC20: transfer amount exceeds allowance")); + return true; + } +``` + +#### OpenZeppelin 安全性附加功能 {#openzeppelin-safety-additions} + +將一個非零授權額度設定為另一個非零值是危險的,因為您只能控制自己交易的順序,而不能控制任何其他人的交易。 想像一下,您有兩個使用者,天真的 Alice 和不誠實的 Bill。 Alice 想要 Bill 的一些服務,她認為這需要五個代幣 - 所以她給了 Bill 五個代幣的授權額度。 + +然後情況有變,Bill 的價格漲到了十個代幣。 仍然想要服務的 Alice 發送了一筆交易,將 Bill 的授權額度設定為十。 Bill 一在交易池中看到這筆新交易,就立即發送一筆交易,花掉 Alice 的五個代幣,並設定更高的 Gas 價格,以便更快地被挖出。 這樣,Bill 可以先花掉五個代幣,然後,一旦 Alice 的新授權額度被挖出,再花掉十個,總共十五個代幣,超過了 Alice 想要授權的數量。 這種技術被稱為[預先交易](https://consensysdiligence.github.io/smart-contract-best-practices/attacks/#front-running) + +| Alice 的交易 | Alice 的 Nonce | Bill 的交易 | Bill 的 Nonce | Bill 的授權額度 | Bill 從 Alice 獲得的總收入 | +| ------------------------------------ | ------------- | ------------------------------------------------ | ------------ | ---------- | ------------------- | +| approve(Bill, 5) | 10 | | | 5 | 0 | +| | | transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | +| approve(Bill, 10) | 11 | | | 10 | 5 | +| | | transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 | + +為避免此問題,這兩個函式(`increaseAllowance` 和 `decreaseAllowance`)允許您以特定數量修改授權額度。 因此,如果 Bill 已經花費了五個代幣,他將只能再花費五個。 根據時間點的不同,這有兩種可能的方式,但最終 Bill 都只會得到十個代幣: + +A: + +| Alice 的交易 | Alice 的 Nonce | Bill 的交易 | Bill 的 Nonce | Bill 的授權額度 | Bill 從 Alice 獲得的總收入 | +| --------------------------------------------- | ------------: | ----------------------------------------------- | -----------: | ---------: | ------------------- | +| approve(Bill, 5) | 10 | | | 5 | 0 | +| | | transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | +| increaseAllowance(Bill, 5) | 11 | | | 0+5 = 5 | 5 | +| | | transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 | + +B: + +| Alice 的交易 | Alice 的 Nonce | Bill 的交易 | Bill 的 Nonce | Bill 的授權額度 | Bill 從 Alice 獲得的總收入 | +| --------------------------------------------- | ------------: | ------------------------------------------------ | -----------: | ---------: | ------------------: | +| approve(Bill, 5) | 10 | | | 5 | 0 | +| increaseAllowance(Bill, 5) | 11 | | | 5+5 = 10 | 0 | +| | | transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 | + +```solidity + /** + * @dev 以原子方式增加呼叫者授予 `spender` 的授權額度。 + * + * 這是 {approve} 的替代方案,可用於緩解 {IERC20-approve} 中描述的問題。 + * + * 發出一個 {Approval} 事件,表示已更新的授權額度。 + * + * 要求: + * + * - `spender` 不能是零地址。 + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } +``` + +`a.add(b)` 函式是安全的加法。 在 `a`+`b`>=`2^256` 的罕見情況下,它不會像正常加法那樣循環。 + +```solidity + + /** + * @dev 以原子方式減少呼叫者授予 `spender` 的授權額度。 + * + * 這是 {approve} 的替代方案,可用於緩解 {IERC20-approve} 中描述的問題。 + * + * 發出一個 {Approval} 事件,表示已更新的授權額度。 + * + * 要求: + * + * - `spender` 不能是零地址。 + * - `spender` 對呼叫者的授權額度必須至少為 + * `subtractedValue`。 + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, + "ERC20: decreased allowance below zero")); + return true; + } +``` + +### 修改代幣資訊的函式 {#functions-that-modify-token-information} + +這四個函式執行實際的工作:`_transfer`、`_mint`、`_burn` 和 `_approve`。 + +#### `_transfer` 函式 {#_transfer} + +```solidity + /** + * @dev 將 `amount` 的代幣從 `sender` 轉移到 `recipient`。 + * + * 這個內部函式相當於 {transfer},可以用於 + * 例如,實作自動代幣費用、削減機制等。 + * + * 發出一個 {Transfer} 事件。 + * + * 要求: + * + * - `sender` 不能是零地址。 + * - `recipient` 不能是零地址。 + * - `sender` 必須擁有至少 `amount` 的餘額。 + */ + function _transfer(address sender, address recipient, uint256 amount) internal virtual { +``` + +這個函式 `_transfer` 將代幣從一個帳戶轉移到另一個帳戶。 它同時被 `transfer`(用於從發送者自己的帳戶轉帳)和 `transferFrom`(用於使用授權額度從別人的帳戶轉帳)呼叫。 + +  + +```solidity + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); +``` + +在以太坊中,沒有人實際擁有零地址(也就是說,沒有人知道其對應的公鑰轉換為零地址的私鑰)。 當人們使用該地址時,通常是軟體錯誤 - 所以如果零地址被用作發送者或接收者,我們會失敗。 + +  + +```solidity + _beforeTokenTransfer(sender, recipient, amount); + +``` + +有兩種方式可以使用此合約: + +1. 將其作為您自己程式碼的範本 +2. [繼承它](https://www.bitdegree.org/learn/solidity-inheritance),並只覆寫您需要修改的函式 + +第二種方法要好得多,因為 OpenZeppelin ERC-20 程式碼已經過審計並被證明是安全的。 當您使用繼承時,您修改的函式會很清楚,要信任您的合約,人們只需要審計那些特定的函式。 + +每次代幣易手時執行一個函式通常很有用。 然而,`_transfer` 是一個非常重要的函式,而且有可能寫得不安全(見下文),所以最好不要覆寫它。 解決方案是 `_beforeTokenTransfer`,一個[鉤子函式](https://wikipedia.org/wiki/Hooking)。 您可以覆寫此函式,它將在每次轉帳時被呼叫。 + +  + +```solidity + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); +``` + +這些是實際執行轉帳的程式碼行。 請注意,它們之間**沒有任何東西**,而且我們在將轉帳金額加到接收者之前,先從發送者那裡減去它。 這很重要,因為如果中間有呼叫到不同的合約,它可能會被用來欺騙此合約。 這樣,轉帳就是原子性的,中間不會發生任何事情。 + +  + +```solidity + emit Transfer(sender, recipient, amount); + } +``` + +最後,發出一個 `Transfer` 事件。 事件無法被智慧型合約存取,但在區塊鏈外執行的程式碼可以監聽事件並對其做出反應。 例如,錢包可以追蹤所有者何時獲得更多代幣。 + +#### `_mint` 和 `_burn` 函式 {#_mint-and-_burn} + +這兩個函式(`_mint` 和 `_burn`)會修改代幣的總供應量。 +它們是內部函式,且此合約中沒有函式會呼叫它們,所以只有在您繼承此合約並加入自己的邏輯,以決定在何種情況下鑄造新代幣或銷毀現有代幣時,它們才有用。 + +**注意:** 每個 ERC-20 代幣都有自己的商業邏輯來決定代幣管理。 +例如,一個固定供應量的合約可能只在建構函式中呼叫 `_mint`,而永不呼叫 `_burn`。 一個銷售代幣的合約在收到付款時會呼叫 `_mint`,並可能在某個時間點呼叫 `_burn` 以避免失控的通貨膨脹。 + +```solidity + /** @dev 建立 `amount` 數量的代幣並將其分配給 `account`,增加 + * 總供應量。 + * + * 發出一個 `from` 設為零地址的 {Transfer} 事件。 + * + * 要求: + * + * - `to` 不能是零地址。 + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + _beforeTokenTransfer(address(0), account, amount); + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } +``` + +當代幣總數變更時,請務必更新 `_totalSupply`。 + +  + +```solidity + /** + * @dev 從 `account` 銷毀 `amount` 數量的代幣,減少 + * 總供應量。 + * + * 發出一個 `to` 設為零地址的 {Transfer} 事件。 + * + * 要求: + * + * - `account` 不能是零地址。 + * - `account` 必須至少擁有 `amount` 數量的代幣。 + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } +``` + +`_burn` 函式與 `_mint` 幾乎相同,只是方向相反。 + +#### `_approve` 函式 {#_approve} + +這個函式實際上指定了授權額度。 注意,它允許所有者指定一個高於其目前餘額的授權額度。 這是可以的,因為餘額是在轉帳時檢查的,那時的餘額可能與建立授權額度時的餘額不同。 + +```solidity + /** + * @dev 將 `spender` 對 `owner` 代幣的授權額度設為 `amount`。 + * + * 這個內部函式等同於 `approve`,可以用於例如 + * 為某些子系統設定自動授權額度等。 + * + * 發出一個 {Approval} 事件。 + * + * 要求: + * + * - `owner` 不能是零地址。 + * - `spender` 不能是零地址。 + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; +``` + +  + +發出一個 `Approval` 事件。 根據應用程式的寫法,花費者合約可以由所有者或監聽這些事件的伺服器來告知核准。 + +```solidity + emit Approval(owner, spender, amount); + } + +``` + +### 修改小數位數變數 {#modify-the-decimals-variable} + +```solidity + + + /** + * @dev 將 {decimals} 設為非預設值 18 的值。 + * + * 警告:此函式只應在建構函式中呼叫。大多數 + * 與代幣合約互動的應用程式不會預期 + * {decimals} 會改變,如果改變了可能會運作不正確。 + */ + function _setupDecimals(uint8 decimals_) internal { + _decimals = decimals_; + } +``` + +此函式修改 `_decimals` 變數,該變數用於告知使用者介面如何解讀金額。 +您應該從建構函式中呼叫它。 在之後的任何時間點呼叫它都是不誠實的,且應用程式並非設計來處理這種情況。 + +### 挂鈎 {#hooks} + +```solidity + + /** + * @dev 在任何代幣轉移之前呼叫的鉤子。這包括 + * 鑄造和銷毀。 + * + * 呼叫條件: + * + * - 當 `from` 和 `to` 都非零時,`amount` 數量的 ``from`` 的代幣 + * 將被轉移到 `to`。 + * - 當 `from` 為零時,將為 `to` 鑄造 `amount` 數量的代幣。 + * - 當 `to` 為零時,`amount` 數量的 ``from`` 的代幣將被銷毀。 + * - `from` 和 `to` 永遠不會同時為零。 + * + * 要了解更多關於鉤子的資訊,請前往 xref:ROOT:extending-contracts.adoc#using-hooks[使用鉤子]。 + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} +``` + +這是在轉帳期間被呼叫的鉤子函式。 這裡它是空的,但如果您需要它做些什麼,您只需要覆寫它即可。 + +## 結論 {#conclusion} + +總結一下,以下是此合約中一些最重要的概念(在我看來,您的看法可能會有所不同): + +- _在區塊鏈上沒有秘密_。 智慧型合約可以存取的任何資訊對全世界都是可用的。 +- 您可以控制自己交易的順序,但無法控制其他人交易的發生時間。 這就是為什麼更改授權額度可能很危險,因為它讓花費者可以花費兩個授權額度的總和。 +- `uint256` 類型的值會循環。 換句話說,_0-1=2^256-1_。 如果這不是期望的行為,您必須對其進行檢查(或使用 SafeMath 程式庫為您代勞)。 請注意,這在 [Solidity 0.8.0](https://docs.soliditylang.org/en/breaking/080-breaking-changes.html) 中已有所改變。 +- 將所有特定類型的狀態變更集中在一個特定地方處理,因為這樣更容易審計。 + 這就是為什麼我們有 `_approve`,它被 `approve`、`transferFrom`、`increaseAllowance` 和 `decreaseAllowance` 呼叫。 +- 狀態變更應該是原子性的,中間不應有任何其他操作(如您在 `_transfer` 中所見)。 這是因為在狀態變更期間,您會處於一個不一致的狀態。 例如,在您從發送者餘額中扣除和添加到接收者餘額之間的時間裡,存在的代幣數量少於應有的數量。 如果它們之間有操作,這點可能被濫用,特別是呼叫到一個不同的合約。 + +既然您已經了解 OpenZeppelin ERC-20 合約是如何編寫的,特別是它是如何變得更安全的,現在就去編寫您自己的安全合約和應用程式吧。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/erc20-with-safety-rails/index.md b/public/content/translations/zh-tw/developers/tutorials/erc20-with-safety-rails/index.md new file mode 100644 index 00000000000..808a150cfd9 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/erc20-with-safety-rails/index.md @@ -0,0 +1,217 @@ +--- +title: "帶有安全措施的 ERC-20" +description: "如何幫助人們避免愚蠢的錯誤" +author: Ori Pomerantz +lang: zh-tw +tags: [ "erc-20" ] +skill: beginner +published: 2022-08-15 +--- + +## 介紹 {#introduction} + +以太坊的一大優點是沒有中央機構可以修改或撤銷您的交易。 以太坊的一大問題是沒有中央機構有權力撤銷使用者錯誤或非法交易。 在本文中,您將了解使用者在使用 [ERC-20](/developers/docs/standards/tokens/erc-20/) 代幣時常犯的一些錯誤,以及如何創建 ERC-20 合約來幫助使用者避免這些錯誤,或賦予中央機構某些權力(例如凍結帳戶)。 + +請注意,雖然我們將使用 [OpenZeppelin ERC-20 代幣合約](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20),但本文不會詳細解釋它。 您可以在[此處](/developers/tutorials/erc20-annotated-code)找到此資訊。 + +如果您想查看完整的原始碼: + +1. 開啟 [Remix IDE](https://remix.ethereum.org/)。 +2. 點擊複製 github 圖示 (![clone github icon](icon-clone.png))。 +3. 複製 github 儲存庫 `https://github.com/qbzzt/20220815-erc20-safety-rails`。 +4. 開啟 **contracts > erc20-safety-rails.sol**。 + +## 建立 ERC-20 合約 {#creating-an-erc-20-contract} + +在新增安全措施功能之前,我們需要一個 ERC-20 合約。 在本文中,我們將使用 [OpenZeppelin Contracts Wizard](https://docs.openzeppelin.com/contracts/5.x/wizard)。 在另一個瀏覽器中開啟它,並按照以下說明操作: + +1. 選擇 **ERC20**。 + +2. 輸入以下設定: + + | 參數 | 數值 | + | ------- | ---------------- | + | 名稱 | SafetyRailsToken | + | 符號 | SAFE | + | Premint | 1000 | + | 功能 | 無 | + | 存取控制 | Ownable | + | 可升級性 | 無 | + +3. 向上捲動並點擊 **在 Remix 中開啟** (適用於 Remix) 或 **下載** 以使用不同的環境。 我將假設您正在使用 Remix,如果您使用其他工具,請進行相應的變更。 + +4. 我們現在有了一個功能齊全的 ERC-20 合約。 您可以展開 `.deps` > `npm` 以查看匯入的程式碼。 + +5. 編譯、部署並操作合約,以確認它能作為 ERC-20 合約運作。 如果您需要學習如何使用 Remix,請[使用此教學](https://remix.ethereum.org/?#activate=udapp,solidity,LearnEth)。 + +## 常見錯誤 {#common-mistakes} + +### 錯誤 {#the-mistakes} + +使用者有時會將代幣傳送到錯誤的地址。 雖然我們無法讀懂他們的心思來了解他們想做什麼,但有兩種經常發生且易於偵測的錯誤類型: + +1. 將代幣傳送到合約自己的地址。 例如,[Optimism 的 OP 代幣](https://optimism.mirror.xyz/qvd0WfuLKnePm1Gxb9dpGchPf5uDz5NSMEFdgirDS4c) 在不到兩個月的時間裡累積了[超過 120,000](https://optimism.blockscout.com/address/0x4200000000000000000000000000000000000042) 個 OP 代幣。 這代表著一筆巨大的財富,推測是人們剛剛損失的。 + +2. 將代幣傳送到一個空地址,該地址不對應於[外部擁有帳戶](/developers/docs/accounts/#externally-owned-accounts-and-key-pairs)或[智能合約](/developers/docs/smart-contracts)。 雖然我沒有關於這種情況發生頻率的統計數據,但[一次事件可能造成 20,000,000 個代幣的損失](https://gov.optimism.io/t/message-to-optimism-community-from-wintermute/2595)。 + +### 防止轉帳 {#preventing-transfers} + +OpenZeppelin ERC-20 合約包含一個[掛鉤 `_beforeTokenTransfer`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L364-L368),它在代幣轉移之前被調用。 預設情況下,這個掛鉤不做任何事情,但我們可以在其上掛載自己的功能,例如在出現問題時回復的檢查。 + +要使用此掛鉤,請在建構函式後新增此函式: + +```solidity + function _beforeTokenTransfer(address from, address to, uint256 amount) + internal virtual + override(ERC20) + { + super._beforeTokenTransfer(from, to, amount); + } +``` + +如果您對 Solidity 不太熟悉,此函式的某些部分可能對您來說是新的: + +```solidity + internal virtual +``` + +`virtual` 關鍵字表示,正如我們從 `ERC20` 繼承功能並覆寫此函式一樣,其他合約也可以從我們這裡繼承並覆寫此函式。 + +```solidity + override(ERC20) +``` + +我們必須明確指定我們正在[覆寫](https://docs.soliditylang.org/en/v0.8.15/contracts.html#function-overriding) `_beforeTokenTransfer` 的 ERC20 代幣定義。 一般來說,從安全角度來看,明確的定義比隱含的定義好得多——如果事情就在您眼前,您就不會忘記您做了什麼。 這也是我們需要指定我們正在覆寫哪個父類的 `_beforeTokenTransfer` 的原因。 + +```solidity + super._beforeTokenTransfer(from, to, amount); +``` + +此行調用我們從其繼承的合約中擁有 `_beforeTokenTransfer` 函式的函式。 在這種情況下,只有 `ERC20` 有這個掛鉤,`Ownable` 沒有。 儘管目前 `ERC20._beforeTokenTransfer` 不做任何事情,我們還是調用它,以防將來新增功能(然後我們決定重新部署合約,因為合約在部署後不會改變)。 + +### 編寫要求 {#coding-the-requirements} + +我們希望向函式新增這些要求: + +- `to` 地址不能等於 `address(this)`,即 ERC-20 合約本身的地址。 +- `to` 地址不能為空,它必須是: + - 一個外部擁有帳戶 (EOA)。 我們無法直接檢查一個地址是否為 EOA,但我們可以檢查一個地址的 ETH 餘額。 EOA 幾乎總是有餘額,即使它們不再使用——很難將它們清零到最後一個 wei。 + - 一個智能合約。 測試一個地址是否為智能合約有點困難。 有一個檢查外部程式碼長度的 opcode,稱為 [`EXTCODESIZE`](https://www.evm.codes/#3b),但它在 Solidity 中不能直接使用。 我們必須為此使用 [Yul](https://docs.soliditylang.org/en/v0.8.15/yul.html),它是一種 EVM 組合語言。 我們可以使用 Solidity 中的其他值([`
.code` 和 `
.codehash`](https://docs.soliditylang.org/en/v0.8.15/units-and-global-variables.html#members-of-address-types)),但它們的成本更高。 + +讓我們逐行查看新的程式碼: + +```solidity + require(to != address(this), "不能將代幣傳送到合約地址"); +``` + +這是第一個要求,檢查 `to` 和 `this(address)` 是否不是同一個東西。 + +```solidity + bool isToContract; + assembly { + isToContract := gt(extcodesize(to), 0) + } +``` + +這是我們檢查一個地址是否為合約的方式。 我們無法直接從 Yul 接收輸出,因此我們定義一個變數來儲存結果(在本例中為 `isToContract`)。 Yul 的工作方式是每個 opcode 都被視為一個函式。 所以首先我們調用 [`EXTCODESIZE`](https://www.evm.codes/#3b) 來獲取合約大小,然後使用 [`GT`](https://www.evm.codes/#11) 來檢查它是否不為零(我們處理的是無符號整數,所以它當然不能是負數)。 然後我們將結果寫入 `isToContract`。 + +```solidity + require(to.balance != 0 || isToContract, "不能將代幣傳送到空地址"); +``` + +最後,我們有了對空地址的實際檢查。 + +## 管理員存取權 {#admin-access} + +有時,有一個可以撤銷錯誤的管理員是很有用的。 為了減少濫用的可能性,這個管理員可以是一個[多重簽名](https://blog.logrocket.com/security-choices-multi-signature-wallets/),這樣就需要多個人同意一項操作。 在本文中,我們將介紹兩種管理功能: + +1. 凍結和解凍帳戶。 這很有用,例如,當一個帳戶可能被盜用時。 +2. 資產清理。 + + 有時,詐騙者會將欺詐性代幣傳送到真實代幣的合約中以獲得合法性。 例如,[請看這裡](https://optimism.blockscout.com/token/0x2348B1a1228DDCd2dB668c3d30207c3E1852fBbe?tab=holders)。 合法的 ERC-20 合約是 [0x4200....0042](https://optimism.blockscout.com/token/0x4200000000000000000000000000000000000042)。 冒充它的詐騙是 [0x234....bbe](https://optimism.blockscout.com/token/0x2348B1a1228DDCd2dB668c3d30207c3E1852fBbe)。 + + 人們也可能錯誤地將合法的 ERC-20 代幣傳送到我們的合約中,這也是我們希望有辦法將它們取出的另一個原因。 + +OpenZeppelin 提供了兩種啟用管理員存取權的機制: + +- [`Ownable`](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable) 合約只有一個擁有者。 具有 `onlyOwner` [修飾符](https://www.tutorialspoint.com/solidity/solidity_function_modifiers.htm)的函式只能由該擁有者調用。 擁有者可以將所有權轉讓給其他人或完全放棄。 所有其他帳戶的權利通常是相同的。 +- [`AccessControl`](https://docs.openzeppelin.com/contracts/5.x/access-control#role-based-access-control) 合約具有[基於角色的存取控制 (RBAC)](https://en.wikipedia.org/wiki/Role-based_access_control)。 + +為簡單起見,本文我們使用 `Ownable`。 + +### 凍結和解凍合約 {#freezing-and-thawing-contracts} + +凍結和解凍合約需要進行幾項變更: + +- 一個從地址到[布林值](https://en.wikipedia.org/wiki/Boolean_data_type)的[對應](https://www.tutorialspoint.com/solidity/solidity_mappings.htm),用於追蹤哪些地址被凍結。 所有值最初都為零,對於布林值,這被解釋為 false。 這就是我們想要的,因為預設情況下帳戶是未凍結的。 + + ```solidity + mapping(address => bool) public frozenAccounts; + ``` + +- 當帳戶被凍結或解凍時,使用[事件](https://www.tutorialspoint.com/solidity/solidity_events.htm)通知任何感興趣的人。 從技術上講,這些操作不需要事件,但它有助於鏈外程式碼能夠監聽這些事件並了解正在發生的事情。 當發生可能與他人相關的事情時,智能合約發出事件被認為是一種良好的習慣。 + + 事件被索引,因此可以搜尋帳戶被凍結或解凍的所有次數。 + + ```solidity + // 當帳戶被凍結或解凍時 + event AccountFrozen(address indexed _addr); + event AccountThawed(address indexed _addr); + ``` + +- 用於凍結和解凍帳戶的函式。 這兩個函式幾乎相同,所以我們只會介紹凍結函式。 + + ```solidity + function freezeAccount(address addr) + public + onlyOwner + ``` + + 標記為 [`public`](https://www.tutorialspoint.com/solidity/solidity_contracts.htm) 的函式可以從其他智能合約或直接透過交易調用。 + + ```solidity + { + require(!frozenAccounts[addr], "帳戶已凍結"); + frozenAccounts[addr] = true; + emit AccountFrozen(addr); + } // freezeAccount + ``` + + 如果帳戶已凍結,則回復。 否則,凍結它並 `emit` 一個事件。 + +- 變更 `_beforeTokenTransfer` 以防止資金從凍結帳戶中移出。 請注意,資金仍然可以轉入凍結帳戶。 + + ```solidity + require(!frozenAccounts[from], "帳戶已凍結"); + ``` + +### 資產清理 {#asset-cleanup} + +要釋放此合約持有的 ERC-20 代幣,我們需要調用它們所屬代幣合約上的一個函式,可以是 [`transfer`](https://eips.ethereum.org/EIPS/eip-20#transfer) 或 [`approve`](https://eips.ethereum.org/EIPS/eip-20#approve)。 在這種情況下,在授權上浪費 Gas 是沒有意義的,我們不如直接轉帳。 + +```solidity + function cleanupERC20( + address erc20, + address dest + ) + public + onlyOwner + { + IERC20 token = IERC20(erc20); +``` + +這是我們在收到地址時為合約建立物件的語法。 我們可以這樣做,因為我們的原始碼中包含了 ERC20 代幣的定義(參見第 4 行),並且該檔案包含了 [IERC20 的定義](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol),這是 OpenZeppelin ERC-20 合約的介面。 + +```solidity + uint balance = token.balanceOf(address(this)); + token.transfer(dest, balance); + } +``` + +這是一個清理函式,所以我們大概不希望留下任何代幣。 與其手動從使用者那裡獲取餘額,我們不如自動化這個過程。 + +## 結論 {#conclusion} + +這不是一個完美的解決方案——對於「使用者犯錯」這個問題,沒有完美的解決方案。 然而,使用這類檢查至少可以防止一些錯誤。 凍結帳戶的能力雖然危險,但可以用來限制某些駭客攻擊的損害,方法是拒絕駭客竊取資金。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/ethereum-for-web2-auth/index.md b/public/content/translations/zh-tw/developers/tutorials/ethereum-for-web2-auth/index.md new file mode 100644 index 00000000000..1ceb9671416 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/ethereum-for-web2-auth/index.md @@ -0,0 +1,886 @@ +--- +title: "使用以太坊進行 Web2 驗證" +description: "閱讀本教學後,開發者將能夠將以太坊登入 (Web3) 與 SAML 登入整合。SAML 登入是 Web2 中使用的一種標準,可提供單一登入及其他相關服務。 這允許透過以太坊簽章來驗證對 Web2 資源的存取,且使用者屬性來自證明。" +author: Ori Pomerantz +tags: [ "web2", "驗證", "eas" ] +skill: beginner +lang: zh-tw +published: 2025-04-30 +--- + +## 簡介 + +[SAML](https://www.onelogin.com/learn/saml) 是 Web2 上使用的一種標準,允許[身分提供者 (IdP)](https://en.wikipedia.org/wiki/Identity_provider#SAML_identity_provider) 為[服務提供者 (SP)](https://en.wikipedia.org/wiki/Service_provider_\(SAML\)) 提供使用者資訊。 + +在本教學中,您將學習如何將以太坊簽章與 SAML 整合,讓使用者能夠使用其以太坊錢包來對那些尚不原生支援以太坊的 Web2 服務進行身分驗證。 + +請注意,本教學是為兩種不同的受眾所撰寫: + +- 了解以太坊且需要學習 SAML 的以太坊使用者 +- 了解 SAML 和 Web2 驗證且需要學習以太坊的 Web2 使用者 + +因此,本教學會包含許多您已知的入門資料。 您可以隨意跳過。 + +### 為以太坊使用者介紹 SAML + +SAML 是一種中心化協定。 只有在服務提供者 (SP) 與身分提供者 (IdP) 或簽署該 IdP 憑證的[憑證授權單位](https://www.ssl.com/article/what-is-a-certificate-authority-ca/)有預先存在的信任關係時,服務提供者才會接受身分提供者所做的斷言 (例如「這是我的使用者 John,他應該有權限執行 A、B 和 C」)。 + +例如,SP 可以是為公司提供旅行服務的旅行社,而 IdP 可以是公司的內部網站。 當員工需要預訂商務旅行時,旅行社會在允許他們實際預訂旅行之前,先將他們傳送到公司進行驗證。 + +![SAML 流程逐步說明](./fig-01-saml.png) + +這就是瀏覽器、SP 和 IdP 這三個實體協商存取權限的方式。 SP 不需要事先知道任何關於使用瀏覽器的使用者的資訊,只需要信任 IdP 即可。 + +### 為 SAML 使用者介紹以太坊 + +以太坊是去中心化系統。 + +![以太坊登入](./fig-02-eth-logon.png) + +使用者擁有私密金鑰 (通常儲存在瀏覽器擴充功能中)。 您可以從私密金鑰衍生出公鑰,再從公鑰衍生出 20 位元組的地址。 當使用者需要登入系統時,系統會要求他們簽署一則附有 nonce (單次使用值) 的訊息。 伺服器可以驗證該簽章是由該地址所建立。 + +![從證明中取得額外資料](./fig-03-eas-data.png) + +該簽章只會驗證以太坊地址。 若要取得其他使用者屬性,您通常會使用[證明](https://attest.org/)。 證明通常具有以下欄位: + +- **證明人**,做出證明的地址 +- **接收者**,證明所適用的地址 +- **資料**,正在證明的資料,例如姓名、權限等。 +- **結構**,用於解譯資料的結構 ID。 + +由於以太坊的去中心化性質,任何使用者都可以做出證明。 證明人的身分對於識別我們認為哪些證明是可靠的至關重要。 + +## 設定 + +第一步是讓 SAML SP 和 SAML IdP 能夠互相通訊。 + +1. 下載軟體。 本文的範例軟體在 [github](https://github.com/qbzzt/250420-saml-ethereum) 上。 不同的階段儲存在不同的分支中,此階段您需要 `saml-only` + + ```sh + git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only + cd 250420-saml-ethereum + pnpm install + ``` + +2. 使用自我簽署憑證建立金鑰。 這表示該金鑰本身即是憑證授權單位,需要手動匯入至服務提供者。 如需詳細資訊,請參閱 [OpenSSL 文件](https://docs.openssl.org/master/man1/openssl-req/)。 + + ```sh + mkdir keys + cd keys + openssl req -new -x509 -days 365 -nodes -sha256 -out saml-sp.crt -keyout saml-sp.pem -subj /CN=sp/ + openssl req -new -x509 -days 365 -nodes -sha256 -out saml-idp.crt -keyout saml-idp.pem -subj /CN=idp/ + cd .. + ``` + +3. 啟動伺服器 (SP 和 IdP) + + ```sh + pnpm start + ``` + +4. 瀏覽至 SP URL [http://localhost:3000/](http://localhost:3000/),然後按一下按鈕,重新導向至 IdP (通訊埠 3001)。 + +5. 向 IdP 提供您的電子郵件地址,然後按一下「**登入服務提供者**」。 確認您已重新導向回服務提供者 (通訊埠 3000),且服務提供者可透過您的電子郵件地址識別您的身分。 + +### 詳細說明 + +以下是逐步發生的情況: + +![不含以太坊的一般 SAML 登入](./fig-04-saml-no-eth.png) + +#### src/config.mts + +此檔案包含身分提供者和服務提供者的組態。 通常這兩者是不同的實體,但為了簡便起見,我們在此共用程式碼。 + +```typescript +const fs = await import("fs") + +const protocol="http" +``` + +目前我們只是在測試,所以使用 HTTP 沒問題。 + +```typescript +export const spCert = fs.readFileSync("keys/saml-sp.crt").toString() +export const idpCert = fs.readFileSync("keys/saml-idp.crt").toString() +``` + +讀取公鑰,公鑰通常可供兩個元件使用 (直接信任,或由受信任的憑證授權單位簽署)。 + +```typescript +export const spPort = 3000 +export const spHostname = "localhost" +export const spDir = "sp" + +export const idpPort = 3001 +export const idpHostname = "localhost" +export const idpDir = "idp" + +export const spUrl = `${protocol}://${spHostname}:${spPort}/${spDir}` +export const idpUrl = `${protocol}://${idpHostname}:${idpPort}/${idpDir}` +``` + +這兩個元件的 URL。 + +```typescript +export const spPublicData = { +``` + +服務提供者的公開資料。 + +```typescript + entityID: `${spUrl}/metadata`, +``` + +在 SAML 中,依照慣例,`entityID` 是實體中繼資料所在的 URL。 此中繼資料對應於此處的公開資料,但其格式為 XML。 + +```typescript + wantAssertionsSigned: true, + authnRequestsSigned: false, + signingCert: spCert, + allowCreate: true, + assertionConsumerService: [{ + Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + Location: `${spUrl}/assertion`, + }] + } +``` + +就我們的目的而言,最重要的定義是 `assertionConsumerServer`。 這表示若要向服務提供者宣告某件事 (例如「傳送此資訊給您的使用者是 somebody@example.com」),我們需要使用 [HTTP POST](https://www.w3schools.com/tags/ref_httpmethods.asp) 到 URL `http://localhost:3000/sp/assertion`。 + +```typescript +export const idpPublicData = { + entityID: `${idpUrl}/metadata`, + signingCert: idpCert, + wantAuthnRequestsSigned: false, + singleSignOnService: [{ + Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + Location: `${idpUrl}/login` + }], + singleLogoutService: [{ + Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + Location: `${idpUrl}/logout` + }], + } +``` + +身分提供者的公開資料是相似的。 它指定若要登入使用者,您需 POST 至 `http://localhost:3001/idp/login`,若要登出使用者,則 POST 至 `http://localhost:3001/idp/logout`。 + +#### src/sp.mts + +這是實作服務提供者的程式碼。 + +```typescript +import * as config from "./config.mts" +const fs = await import("fs") +const saml = await import("samlify") +``` + +我們使用 [`samlify`](https://www.npmjs.com/package/samlify) 庫來實作 SAML。 + +```typescript +import * as validator from "@authenio/samlify-node-xmllint" +saml.setSchemaValidator(validator) +``` + +`samlify` 庫需要一個套件來驗證 XML 是否正確、是否使用預期的公鑰簽署等。 為此,我們使用 [`@authenio/samlify-node-xmllint`](https://www.npmjs.com/package/@authenio/samlify-node-xmllint)。 + +```typescript +const express = (await import("express")).default +const spRouter = express.Router() +const app = express() +``` + +[`express`](https://expressjs.com/) [`Router`](https://expressjs.com/en/5x/api.html#router) 是一個可以掛載在網站中的「迷你網站」。 在此情況下,我們使用它將所有服務提供者定義群組在一起。 + +```typescript +const spPrivateKey = fs.readFileSync("keys/saml-sp.pem").toString() + +const sp = saml.ServiceProvider({ + privateKey: spPrivateKey, + ...config.spPublicData +}) +``` + +服務提供者自身的表示是所有公開資料,以及它用來簽署資訊的私密金鑰。 + +```typescript +const idp = saml.IdentityProvider(config.idpPublicData); +``` + +公開資料包含服務提供者需要了解的身分提供者的所有資訊。 + +```typescript +spRouter.get(`/metadata`, + (req, res) => res.header("Content-Type", "text/xml").send(sp.getMetadata()) +) +``` + +為了與其他 SAML 元件互通,服務和身分提供者的公開資料 (稱為中繼資料) 應以 XML 格式在 `/metadata` 中提供。 + +```typescript +spRouter.post(`/assertion`, +``` + +這是瀏覽器存取以識別自身的頁面。 宣告包含使用者識別碼 (此處我們使用電子郵件地址),並且可以包含額外的屬性。 這是上方序列圖中步驟 7 的處理常式。 + +```typescript + async (req, res) => { + // console.log(`SAML response:\n${Buffer.from(req.body.SAMLResponse, 'base64').toString('utf-8')}`) +``` + +您可以使用已註解的指令來查看斷言中提供的 XML 資料。 它是 [base64 編碼的](https://en.wikipedia.org/wiki/Base64)。 + +```typescript + try { + const loginResponse = await sp.parseLoginResponse(idp, 'post', req); +``` + +解析來自身分伺服器的登入請求。 + +```typescript + res.send(` + + +

Hello ${loginResponse.extract.nameID}

+ + + `) + res.send(); +``` + +傳送 HTML 回應,僅是為了向使用者顯示我們已收到登入請求。 + +```typescript + } catch (err) { + console.error('Error processing SAML response:', err); + res.status(400).send('SAML authentication failed'); + } + } +) +``` + +如果失敗,請通知使用者。 + +```typescript +spRouter.get('/login', +``` + +當瀏覽器嘗試取得此頁面時,建立一個登入請求。 這是上方序列圖中步驟 1 的處理常式。 + +```typescript + async (req, res) => { + const loginRequest = await sp.createLoginRequest(idp, "post") +``` + +取得發布登入請求的資訊。 + +```typescript + res.send(` + + + +``` + +此頁面會自動提交表單 (見下文)。 如此一來,使用者就不需要執行任何操作即可被重新導向。 這是上方序列圖中的步驟 2。 + +```typescript +
+``` + +張貼到 `loginRequest.entityEndpoint` (身分提供者端點的 URL)。 + +```typescript + +``` + +輸入名稱為 `loginRequest.type` (`SAMLRequest`)。 該欄位的內容是 `loginRequest.context`,它同樣是經過 base64 編碼的 XML。 + +```typescript +
+ + + `) + } +) + +app.use(express.urlencoded({extended: true})) +``` + +[此中介軟體](https://expressjs.com/en/5x/api.html#express.urlencoded) 會讀取 [HTTP 請求](https://www.tutorialspoint.com/http/http_requests.htm)的主體。 預設情況下,express 會忽略它,因為大多數請求並不需要它。 我們需要它,因為 POST 會使用主體。 + +```typescript +app.use(`/${config.spDir}`, spRouter) +``` + +在服務提供者目錄 (`/sp`) 中掛載路由器。 + +```typescript +app.get("/", (req, res) => { + res.send(` + + + + + + `) +}) +``` + +如果瀏覽器嘗試取得根目錄,請為其提供登入頁面的連結。 + +```typescript +app.listen(config.spPort, () => { + console.log(`service provider is running on http://${config.spHostname}:${config.spPort}`) +}) +``` + +使用此 express 應用程式監聽 `spPort`。 + +#### src/idp.mts + +這是身分提供者。 它與服務提供者非常相似,以下說明是針對不同的部分。 + +```typescript +const xmlParser = new (await import("fast-xml-parser")).XMLParser( + { + ignoreAttributes: false, // Preserve attributes + attributeNamePrefix: "@_", // Prefix for attributes + } +) +``` + +我們需要讀取並理解從服務提供者收到的 XML 請求。 + +```typescript +const getLoginPage = requestId => ` +``` + +此函數會建立一個帶有自動提交表單的頁面,該頁面會在上方序列圖的步驟 4 中傳回。 + +```typescript + + + Login page + + +

Login page

+
+ + Email address: +
+ +``` + +我們傳送給服務提供者的欄位有兩個: + +1. 我們正在回應的 `requestId`。 +2. 使用者識別碼 (目前我們使用使用者提供的電子郵件地址)。 + +```typescript +
+ + + +const idpRouter = express.Router() + +idpRouter.post("/loginSubmitted", async (req, res) => { + const loginResponse = await idp.createLoginResponse( +``` + +這是上方序列圖中步驟 5 的處理常式。 [`idp.createLoginResponse`](https://github.com/tngan/samlify/blob/master/src/entity-idp.ts#L73-L125) 會建立登入回應。 + +```typescript + sp, + { + authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', + audience: sp.entityID, +``` + +對象為服務提供者。 + +```typescript + extract: { + request: { + id: req.body.requestId + } + }, +``` + +從請求中提取的資訊。 我們在請求中關心的一個參數是 requestId,它讓服務提供者能夠匹配請求及其回應。 + +```typescript + signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // Ensure signing +``` + +我們需要 `signingKey` 擁有簽署回應的資料。 服務提供者不信任未經簽署的請求。 + +```typescript + }, + "post", + { + email: req.body.email +``` + +這是我們傳送回服務提供者的使用者資訊欄位。 + +```typescript + } + ); + + res.send(` + + + + +
+ +
+ + + `) +}) +``` + +同樣,使用自動提交的表單。 這是上方序列圖中的步驟 6。 + +```typescript + +// IdP endpoint for login requests +idpRouter.post(`/login`, +``` + +這是從服務提供者接收登入請求的端點。 這是上方序列圖中步驟 3 的處理常式。 + +```typescript + async (req, res) => { + try { + // Workaround because I couldn't get parseLoginRequest to work. + // const loginRequest = await idp.parseLoginRequest(sp, 'post', req) + const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8')) + res.send(getLoginPage(samlRequest["samlp:AuthnRequest"]["@_ID"])) +``` + +我們應該能夠使用 [`idp.parseLoginRequest`](https://github.com/tngan/samlify/blob/master/src/entity-idp.ts#L127-L144) 來讀取驗證請求的 ID。 然而,我無法讓它正常運作,而且不值得花太多時間,所以我只使用[通用的 XML 解析器](https://www.npmjs.com/package/fast-xml-parser)。 我們需要的資訊是 `` 標籤內的 `ID` 屬性,它位於 XML 的最上層。 + +## 使用以太坊簽章 + +既然我們能夠將使用者身分傳送給服務提供者,下一步就是以受信任的方式取得使用者身分。 Viem 允許我們直接向錢包詢問使用者地址,但這表示要向瀏覽器索取資訊。 我們無法控制瀏覽器,所以不能自動信任從它那裡得到的回應。 + +因此,IdP 會傳送一個字串給瀏覽器進行簽署。 如果瀏覽器中的錢包簽署了這個字串,這就表示它確實是那個地址 (也就是說,它知道對應於該地址的私密金鑰)。 + +要查看此操作,請停止現有的 IdP 和 SP,並執行以下命令: + +```sh +git checkout eth-signatures +pnpm install +pnpm start +``` + +然後瀏覽至 [SP](http://localhost:3000) 並依照指示操作。 + +請注意,此時我們不知道如何從以太坊地址取得電子郵件地址,因此我們向 SP 回報 `@bad.email.address`。 + +### 詳細說明 + +變更發生在先前圖表中的步驟 4-5。 + +![帶有以太坊簽章的 SAML](./fig-05-saml-w-signature.png) + +我們唯一變更的檔案是 `idp.mts`。 以下是已變更的部分。 + +```typescript +import { v4 as uuidv4 } from 'uuid' +import { verifyMessage } from 'viem' +``` + +我們需要這兩個額外的庫。 我們使用 [`uuid`](https://www.npmjs.com/package/uuid) 來建立 [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) 值。 值本身並不重要,重要的是它只使用一次。 + +[`viem`](https://viem.sh/) 庫讓我們能夠使用以太坊的定義。 此處我們需要它來驗證簽章確實有效。 + +```typescript +const loginPrompt = "To access the service provider, sign this nonce: " +``` + +錢包會要求使用者允許簽署該訊息。 僅包含 nonce 的訊息可能會讓使用者感到困惑,因此我們加入了這個提示。 + +```typescript +// Keep requestIDs here +let nonces = {} +``` + +我們需要請求資訊才能回應它。 我們可以隨請求傳送它 (步驟 4),然後再接收回來 (步驟 5)。 然而,我們不能信任從瀏覽器取得的資訊,因為瀏覽器在一個可能具有敵意的使用者的控制之下。 所以最好將它儲存在這裡,並以 nonce 作為金鑰。 + +請注意,為了簡單起見,我們在此將它作為一個變數。 然而,這有幾個缺點: + +- 我們容易受到拒絕服務攻擊。 惡意使用者可以多次嘗試登入,耗盡我們的記憶體。 +- 如果 IdP 程序需要重新啟動,我們會遺失現有的值。 +- 我們無法在多個程序之間進行負載平衡,因為每個程序都有自己的變數。 + +在生產系統上,我們會使用資料庫並實作某種過期機制。 + +```typescript +const getSignaturePage = requestId => { + const nonce = uuidv4() + nonces[nonce] = requestId +``` + +建立一個 nonce,並儲存 `requestId` 以供日後使用。 + +```typescript + return ` + + + + + +

Please sign

+ +
+ + + +` +} +``` + +其餘的只是標準的 HTML。 + +```typescript +idpRouter.get("/signature/:nonce/:account/:signature", async (req, res) => { +``` + +這是序列圖中步驟 5 的處理常式。 + +```typescript + const requestId = nonces[req.params.nonce] + if (requestId === undefined) { + res.send("Bad nonce") + return ; + } + + nonces[req.params.nonce] = undefined +``` + +取得請求 ID,並從 `nonces` 中刪除該 nonce,以確保無法重複使用。 + +```typescript + try { +``` + +由於簽章可能無效的方式有很多種,我們將此包裝在 `try ...` `catch` 區塊中,以捕捉任何擲出的錯誤。 + +```typescript + const validSignature = await verifyMessage({ + address: req.params.account, + message: `${loginPrompt}${req.params.nonce}`, + signature: req.params.signature + }) +``` + +使用 [`verifyMessage`](https://viem.sh/docs/actions/public/verifyMessage#verifymessage) 來實作序列圖中的步驟 5.5。 + +```typescript + if (!validSignature) + throw("Bad signature") + } catch (err) { + res.send("Error:" + err) + return ; + } +``` + +此處理程式的其餘部分與我們之前在 `/loginSubmitted` 處理程式中所做的相同,除了一個小小的變更。 + +```typescript + const loginResponse = await idp.createLoginResponse( + . + . + . + { + email: req.params.account + "@bad.email.address" + } + ); +``` + +我們沒有實際的電子郵件地址 (我們將在下一節中取得),所以現在我們先傳回以太坊地址,並清楚地標示它不是一個電子郵件地址。 + +```typescript +// IdP endpoint for login requests +idpRouter.post(`/login`, + async (req, res) => { + try { + // Workaround because I couldn't get parseLoginRequest to work. + // const loginRequest = await idp.parseLoginRequest(sp, 'post', req) + const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8')) + res.send(getSignaturePage(samlRequest["samlp:AuthnRequest"]["@_ID"])) + } catch (err) { + console.error('Error processing SAML response:', err); + res.status(400).send('SAML authentication failed'); + } + } +) +``` + +現在在步驟 3 的處理常式中使用 `getSignaturePage` 取代 `getLoginPage`。 + +## 取得電子郵件地址 + +下一步是取得電子郵件地址,也就是服務提供者請求的識別碼。 為此,我們使用[以太坊證明服務 (EAS)](https://attest.org/)。 + +取得證明最簡單的方法是使用 [GraphQL API](https://docs.attest.org/docs/developer-tools/api)。 我們使用此查詢: + +``` +query GetAttestationsByRecipient { + attestations( + where: { + recipient: { equals: "${getAddress(ethAddr)}" } + schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" } + } + take: 1 + ) { + data + id + attester + } +} +``` + +這個 [`schemaId`](https://optimism.easscan.org/schema/view/0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977) 只包含一個電子郵件地址。 此查詢要求此結構的證明。 證明的主體稱為「`recipient`」(接收者)。 它永遠是以太坊地址。 + +警告:我們在此處取得證明的方式有兩個安全問題。 + +- 我們前往的 API 端點是 `https://optimism.easscan.org/graphql`,這是一個中心化元件。 我們可以取得 `id` 屬性,然後在鏈上進行查詢以驗證證明是否真實,但 API 端點仍然可以透過不告知我們來審查證明。 + + 這個問題並非無法解決,我們可以執行自己的 GraphQL 端點並從鏈記錄中取得證明,但這對我們的目的來說太過繁瑣。 + +- 我們不看證明人的身分。 任何人都可以提供我們錯誤的資訊。 在實際的實作中,我們會有一組受信任的證明人,並且只查看他們的證明。 + +要查看此操作,請停止現有的 IdP 和 SP,並執行以下命令: + +```sh +git checkout email-address +pnpm install +pnpm start +``` + +然後提供您的電子郵件地址。 您有兩種方式可以做到: + +- 使用私密金鑰匯入錢包,並使用測試私密金鑰 `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`。 + +- 為您自己的電子郵件地址新增一則證明: + + 1. 在證明瀏覽器中瀏覽至[該結構](https://optimism.easscan.org/schema/view/0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977)。 + + 2. 按一下「**使用結構證明**」。 + + 3. 輸入您的以太坊地址作為接收者,您的電子郵件地址作為 email address,並選取**鏈上**。 然後按一下「**進行證明**」。 + + 4. 在您的錢包中核准交易。 您將需要在 [Optimism 區塊鏈](https://app.optimism.io/bridge/deposit) 上擁有一些 ETH 以支付 Gas。 + +無論哪種方式,完成後請瀏覽至 [http://localhost:3000](http://localhost:3000) 並依照指示操作。 如果您匯入了測試私密金鑰,您收到的電子郵件是 `test_addr_0@example.com`。 如果您使用自己的地址,它應該是您所證明的任何內容。 + +### 詳細說明 + +![從以太坊地址取得電子郵件](./fig-06-saml-sig-n-email.png) + +新的步驟是 GraphQL 通訊,即步驟 5.6 和 5.7。 + +同樣,以下是 `idp.mts` 的已變更部分。 + +```typescript +import { GraphQLClient } from 'graphql-request' +import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk' +``` + +匯入我們需要的庫。 + +```typescript +const graphqlEndpointUrl = "https://optimism.easscan.org/graphql" +``` + +[每個區塊鏈都有一個獨立的端點](https://docs.attest.org/docs/developer-tools/api)。 + +```typescript +const graphqlClient = new GraphQLClient(graphqlEndpointUrl, { fetch }) +``` + +建立一個新的 `GraphQLClient` 用戶端,可用於查詢端點。 + +```typescript +const graphqlSchema = 'string emailAddress' +const graphqlEncoder = new SchemaEncoder(graphqlSchema) +``` + +GraphQL 只提供我們一個不透明的位元組資料物件。 若要理解它,我們需要結構。 + +```typescript +const ethereumAddressToEmail = async ethAddr => { +``` + +一個從以太坊地址取得電子郵件地址的函數。 + +```typescript + const query = ` + query GetAttestationsByRecipient { +``` + +這是一個 GraphQL 查詢。 + +```typescript + attestations( +``` + +我們正在尋找證明。 + +```typescript + where: { + recipient: { equals: "${getAddress(ethAddr)}" } + schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" } + } +``` + +我們想要的證明是我們結構中的證明,其中接收者是 `getAddress(ethAddr)`。 [`getAddress`](https://viem.sh/docs/utilities/getAddress#getaddress) 函數可確保我們的地址具有正確的[校驗和](https://github.com/ethereum/ercs/blob/master/ERCS/erc-55.md)。 這對於 GraphQL 而言是必要的,因為 GraphQL 區分大小寫。 「0xBAD060A7」、「0xBad060A7」和「0xbad060a7」是不同的值。 + +```typescript + take: 1 +``` + +無論我們找到多少則證明,我們只需要第一則。 + +```typescript + ) { + data + id + attester + } + }` +``` + +我們想要接收的欄位。 + +- `attester`:提交證明的地址。 通常這用於決定是否信任該證明。 +- `id`:證明 ID。 您可以使用此值[在鏈上讀取證明](https://optimism.blockscout.com/address/0x4200000000000000000000000000000000000021?tab=read_proxy&source_address=0x4E0275Ea5a89e7a3c1B58411379D1a0eDdc5b088#0xa3112a64)以驗證 GraphQL 查詢的資訊是否正確。 +- `data`:結構資料 (在此情況下為電子郵件地址)。 + +```typescript + const queryResult = await graphqlClient.request(query) + + if (queryResult.attestations.length == 0) + return "no_address@available.is" +``` + +如果沒有證明,則傳回一個明顯不正確的值,但服務提供者會認為它有效。 + +```typescript + const attestationDataFields = graphqlEncoder.decodeData(queryResult.attestations[0].data) + return attestationDataFields[0].value.value +} +``` + +如果有值,請使用 `decodeData` 解碼資料。 我們不需要它提供的中繼資料,只需要值本身。 + +```typescript + const loginResponse = await idp.createLoginResponse( + sp, + { + . + . + . + }, + "post", + { + email: await ethereumAddressToEmail(req.params.account) + } + ); +``` + +使用新函數取得電子郵件地址。 + +## 關於去中心化呢? + +在此組態中,只要我們依賴可信的證明人進行以太坊到電子郵件地址的對應,使用者就無法冒充他人。 然而,我們的身分提供者仍然是一個中心化元件。 任何擁有身分提供者私密金鑰的人都可以向服務提供者傳送虛假資訊。 + +使用[多方運算 (MPC)](https://en.wikipedia.org/wiki/Secure_multi-party_computation) 可能是一個解決方案。 我希望在未來的教學中寫到它。 + +## 結論 + +採用登入標準 (例如以太坊簽章) 會面臨雞生蛋、蛋生雞的問題。 服務提供者希望吸引盡可能廣泛的市場。 使用者希望能夠存取服務,而不用擔心支援其登入標準。 +建立適配器 (例如以太坊 IdP) 可以幫助我們克服這個障礙。 + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh-tw/developers/tutorials/getting-started-with-ethereum-development-using-alchemy/index.md b/public/content/translations/zh-tw/developers/tutorials/getting-started-with-ethereum-development-using-alchemy/index.md new file mode 100644 index 00000000000..c14801cb3f5 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/getting-started-with-ethereum-development-using-alchemy/index.md @@ -0,0 +1,149 @@ +--- +title: "開始以太坊開發之旅" +description: "這是一份以太坊開發的入門指南。 我們將引導您完成建立 API 端點、發出命令列請求,到撰寫您的第一個 Web3 腳本! 無需區塊鏈開發經驗!" +author: "Elan Halpern" +tags: [ "javascript", "ethers.js", "節點", "諮詢", "alchemy" ] +skill: beginner +lang: zh-tw +published: 2020-10-30 +source: Medium +sourceUrl: https://medium.com/alchemy-api/getting-started-with-ethereum-development-using-alchemy-c3d6a45c567f +--- + +![以太坊和 Alchemy 標誌](./ethereum-alchemy.png) + +這是一份以太坊開發的入門指南。 在本教學中,我們將使用 [Alchemy](https://alchemyapi.io/),這是一個領先的區塊鏈開發者平台,為 70% 的頂級區塊鏈應用程式 (包括 Maker、0x、MyEtherWallet、Dharma 和 Kyber) 的數百萬名使用者提供支援。 Alchemy 將讓我們能夠存取以太坊鏈上的 API 端點,以便我們讀取和寫入交易。 + +我們將引導您從註冊 Alchemy 到撰寫您的第一個 Web3 腳本! 無需區塊鏈開發經驗! + +## 1. 註冊免費的 Alchemy 帳戶 {#sign-up-for-a-free-alchemy-account} + +建立 Alchemy 帳戶很簡單,[在此免費註冊](https://auth.alchemy.com/)。 + +## 2. 建立 Alchemy 應用程式 {#create-an-alchemy-app} + +若要與以太坊鏈通訊並使用 Alchemy 的產品,您需要一個 API 金鑰來驗證您的請求。 + +您可以[從儀表板建立 API 金鑰](https://dashboard.alchemy.com/)。 若要建立新的金鑰,請如下所示導覽至「Create App」: + +特別感謝 [_ShapeShift_](https://shapeshift.com/) _讓我們展示他們的儀表板!_ + +![Alchemy 儀表板](./alchemy-dashboard.png) + +在「Create App」下填寫詳細資料,即可取得新的金鑰。 您也可以在此處看到您先前建立的應用程式,以及您團隊建立的應用程式。 按一下任何應用程式的「View Key」來擷取現有的金鑰。 + +![使用 Alchemy 建立應用程式的螢幕截圖](./create-app.png) + +您也可以將游標懸停在「Apps」上並選取一個應用程式,來擷取現有的 API 金鑰。 您可以在此處「View Key」(檢視金鑰) 以及「Edit App」(編輯應用程式),將特定網域加入白名單、查看多個開發者工具,以及檢視分析資料。 + +![顯示使用者如何擷取 API 金鑰的 Gif](./pull-api-keys.gif) + +## 3 從命令列發出請求 {#make-a-request-from-the-command-line} + +透過 Alchemy 使用 JSON-RPC 和 curl 與以太坊區塊鏈互動。 + +對於手動請求,我們建議透過 `POST` 請求與 `JSON-RPC` 互動。 只需傳入 `Content-Type: application/json` 標頭,並將您的查詢作為 `POST` 主體,並包含以下欄位: + +- `jsonrpc`:JSON-RPC 版本—目前僅支援 `2.0`。 +- `method`:ETH API 方法。 [請參閱 API 參考資料。](https://docs.alchemyapi.io/documentation/alchemy-api-reference/json-rpc) +- `params`:要傳遞給方法的參數清單。 +- `id`:您請求的 ID。 回應中將會傳回此 ID,以便您追蹤哪個回應屬於哪個請求。 + +以下是您可以從命令列執行的範例,用以擷取目前的 gas 價格: + +```bash +curl https://eth-mainnet.alchemyapi.io/v2/demo \ +-X POST \ +-H "Content-Type: application/json" \ +-d '{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":73}' +``` + +_\*\*注意:\*\*將 [https://eth-mainnet.alchemyapi.io/v2/demo](https://eth-mainnet.alchemyapi.io/jsonrpc/demo) 替換為您自己的 API 金鑰 `https://eth-mainnet.alchemyapi.io/v2/**your-api-key`。_ + +**結果:** + +```json +{ "id": 73,"jsonrpc": "2.0","result": "0x09184e72a000" // 10000000000000 } +``` + +## 4 設定您的 Web3 用戶端 {#set-up-your-web3-client} + +\*\*如果您有現有的用戶端,\*\*請將您目前的節點提供者 URL 變更為帶有您 API 金鑰的 Alchemy URL:`“https://eth-mainnet.alchemyapi.io/v2/your-api-key\"` + +**_注意:_** 下方的腳本需要在 **節點環境** 中執行,或 **儲存在檔案中** 執行,而非從命令列執行。 如果您尚未安裝 Node 或 npm,請查看這份快速的 [mac 版設定指南](https://app.gitbook.com/@alchemyapi/s/alchemy/guides/alchemy-for-macs)。 + +有許多 [Web3 程式庫](https://docs.alchemyapi.io/guides/getting-started#other-web3-libraries)可以與 Alchemy 整合,但我們建議使用 [Alchemy Web3](https://docs.alchemy.com/reference/api-overview),它是 web3.js 的直接替代品,其建構與設定可和 Alchemy 無縫協作。 這提供了多種優點,例如自動重試和強大的 WebSocket 支援。 + +若要安裝 AlchemyWeb3.js,請 **導覽至您的專案目錄** 並執行: + +**使用 Yarn:** + +``` +yarn add @alch/alchemy-web3 +``` + +**使用 NPM:** + +``` +npm install @alch/alchemy-web3 +``` + +若要與 Alchemy 的節點基礎架構互動,請在 NodeJS 中執行或將此新增至 JavaScript 檔案: + +```js +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3( + "https://eth-mainnet.alchemyapi.io/v2/your-api-key" +) +``` + +## 5 撰寫您的第一個 Web3 腳本! {#write-your-first-web3-script} + +現在,讓我們來實際動手做一點 Web3 程式設計,我們將撰寫一個簡單的腳本,從以太坊主網印出最新的區塊編號。 + +**1. 如果您尚未這麼做,請在您的終端機中建立一個新的專案目錄,並用 cd 進入該目錄:** + +``` +mkdir web3-example +cd web3-example +``` + +**2. 如果您尚未這麼做,請將 Alchemy Web3 (或任何 Web3) 相依性安裝到您的專案中:** + +``` +npm install @alch/alchemy-web3 +``` + +**3. 建立一個名為 `index.js` 的檔案,並新增以下內容:** + +> 最終您應該將 `demo` 替換為您的 Alchemy HTTP API 金鑰。 + +```js +async function main() { + const { createAlchemyWeb3 } = require("@alch/alchemy-web3") + const web3 = createAlchemyWeb3("https://eth-mainnet.alchemyapi.io/v2/demo") + const blockNumber = await web3.eth.getBlockNumber() + console.log("The latest block number is " + blockNumber) +} +main() +``` + +不熟悉非同步 (async) 相關內容? 請參閱這篇 [Medium 文章](https://medium.com/better-programming/understanding-async-await-in-javascript-1d81bb079b2c)。 + +**4. 使用 node 在您的終端機中執行它** + +``` +node index.js +``` + +**5. 您現在應該會在主控台中看到最新的區塊編號輸出!** + +``` +The latest block number is 11043912 +``` + +**讚!** 恭喜! 您剛使用 Alchemy 撰寫了您的第一個 Web3 腳本 🎉\*\* + +不確定下一步要做什麼? 試著部署您的第一個智能合約,並在我們的 [Hello World 智能合約指南](https://www.alchemy.com/docs/hello-world-smart-contract) 中實際動手進行一些 Solidity 程式設計,或使用 [儀表板示範應用程式](https://docs.alchemyapi.io/tutorials/demo-app) 來測試您的儀表板知識! + +_[免費註冊 Alchemy](https://auth.alchemy.com/)、查看我們的[文件](https://www.alchemy.com/docs/),以及如需最新消息,請在 [Twitter](https://twitter.com/AlchemyPlatform) 上追蹤我們_。 diff --git a/public/content/translations/zh-tw/developers/tutorials/guide-to-smart-contract-security-tools/index.md b/public/content/translations/zh-tw/developers/tutorials/guide-to-smart-contract-security-tools/index.md new file mode 100644 index 00000000000..4dfd52b75ec --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/guide-to-smart-contract-security-tools/index.md @@ -0,0 +1,102 @@ +--- +title: "智能合約安全工具指南" +description: "三種不同測試與程式分析技術的概觀" +author: "Trailofbits" +lang: zh-tw +tags: [ "穩固", "智能合約", "安全性" ] +skill: intermediate +published: 2020-09-07 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/tree/master/program-analysis +--- + +我們將使用三種獨特的測試與程式分析技術: + +- \*\*透過 [Slither](/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/) 進行靜態分析。\*\*程式的所有路徑會透過不同的程式呈現方式(例如控制流程圖)同時進行近似與分析 +- \*\*透過 [Echidna](/developers/tutorials/how-to-use-echidna-to-test-smart-contracts/) 進行模糊測試。\*\*程式碼會透過偽隨機產生的交易來執行。 模糊測試器會嘗試找到違反給定屬性的交易序列。 +- \*\*透過 [Manticore](/developers/tutorials/how-to-use-manticore-to-find-smart-contract-bugs/) 進行符號執行。\*\*一種正規的驗證技術,將每個執行路徑轉換成數學公式,並在其上檢查約束條件。 + +每種技術都有其優點和缺點,在[特定情況](#determining-security-properties)下會很有用: + +| 技術 | 工具 | 用法 | 速度 | 遺漏的錯誤 | 誤報 | +| ---- | --------- | --------------- | -- | ----- | -- | +| 靜態分析 | Slither | 命令列介面與指令碼 | 秒 | 中等 | 低 | +| 模糊測試 | Echidna | Solidity 屬性 | 分 | 低 | 無 | +| 符號執行 | Manticore | Solidity 屬性與指令碼 | 小時 | 無\* | 無 | + +\* 如果所有路徑都在沒有逾時的情況下完成探索 + +**Slither** 可在幾秒鐘內分析合約,但靜態分析可能會導致誤報,且較不適合用於複雜的檢查(例如算術檢查)。 透過應用程式介面執行 Slither,以按鈕方式存取內建的偵測器,或透過應用程式介面進行使用者定義的檢查。 + +**Echidna** 需要運行數分鐘,且只會產生真陽性結果。 Echidna 會檢查使用者提供的、以 Solidity 編寫的安全屬性。 由於它基於隨機探索,可能會遺漏錯誤。 + +**Manticore** 會執行「最重量級」的分析。 與 Echidna 類似,Manticore 也會驗證使用者提供的屬性。 它需要更長的運行時間,但可以證明屬性的有效性,且不會報告誤報。 + +## 建議工作流程 {#suggested-workflow} + +從 Slither 的內建偵測器開始,確保目前沒有簡單的錯誤,未來也不會引入。 使用 Slither 檢查與繼承、變數相依性和結構性問題相關的屬性。 隨著程式碼庫的增長,使用 Echidna 測試狀態機更複雜的屬性。 再次使用 Slither 開發自訂檢查,以提供 Solidity 中沒有的保護措施,例如防止函式被覆寫。 最後,使用 Manticore 對關鍵安全屬性(例如算術運算)執行有針對性的驗證。 + +- 使用 Slither 的命令列介面來捕捉常見問題 +- 使用 Echidna 測試合約的高階安全屬性 +- 使用 Slither 編寫自訂的靜態檢查 +- 當您想要對關鍵安全屬性進行深度保證時,請使用 Manticore + +**關於單元測試的說明**。 單元測試是建立高品質軟體的必要條件。 然而,這些技術並非最適合用來發現安全漏洞。 它們通常用於測試程式碼的正面行為(即程式碼在正常情況下如預期般運作),而安全漏洞則傾向於存在於開發者未曾考慮到的邊際情況。 在我們對數十個智能合約安全審查的研究中,我們在客戶的程式碼中發現,[單元測試覆蓋率對安全漏洞的數量或嚴重性沒有影響](https://blog.trailofbits.com/2019/08/08/246-findings-from-our-smart-contract-audits-an-executive-summary/)。 + +## 確定安全屬性 {#determining-security-properties} + +為了有效地測試和驗證您的程式碼,您必須找出需要注意的區域。 由於您在安全性上投入的資源有限,確定您程式碼庫中較弱或高價值的部分,對於優化您的投入非常重要。 威脅模型可以提供幫助。 請考慮審查: + +- [快速風險評估](https://infosec.mozilla.org/guidelines/risk/rapid_risk_assessment.html)(時間緊迫時我們的首選方法) +- [以資料為中心的系統威脅模型指南](https://csrc.nist.gov/pubs/sp/800/154/ipd) (又名 NIST 800-154) +- [Shostack 威脅模型](https://www.amazon.com/Threat-Modeling-Designing-Adam-Shostack/dp/1118809998) +- [STRIDE](https://wikipedia.org/wiki/STRIDE_\(security\)) / [DREAD](https://wikipedia.org/wiki/DREAD_\(risk_assessment_model\)) +- [PASTA](https://wikipedia.org/wiki/Threat_model#P.A.S.T.A.) +- [斷言的使用](https://blog.regehr.org/archives/1091) + +### 元件 {#components} + +了解您想檢查的內容,也有助於您選擇正確的工具。 + +與智能合約經常相關的廣泛領域包括: + +- \*\*狀態機。\*\*大多數合約都可以表示為狀態機。 考慮檢查 (1) 無法達到任何無效狀態,(2) 如果一個狀態是有效的,那麼它可以被達到,以及 (3) 沒有任何狀態會讓合約陷入陷阱。 + + - Echidna 和 Manticore 是測試狀態機規格的首選工具。 + +- \*\*存取控制。\*\*如果您的系統有特權使用者(例如擁有者、控制者等) 您必須確保 (1) 每個使用者只能執行授權的動作,以及 (2) 沒有使用者可以阻止更具特權的使用者執行動作。 + + - Slither、Echidna 和 Manticore 可以檢查存取控制的正確性。 例如,Slither 可以檢查是否只有列入白名單的函式缺少 `onlyOwner` 修飾符。 Echidna 和 Manticore 對於更複雜的存取控制很有用,例如只有在合約達到給定狀態時才授予權限。 + +- \*\*算術運算。\*\*檢查算術運算的健全性至關重要。 在各處使用 `SafeMath` 是防止溢出/下溢的好方法,但您仍需考慮其他算術缺陷,包括捨入問題和會讓合約陷入陷阱的缺陷。 + + - Manticore 是這裡的最佳選擇。 如果算術超出 SMT 求解器的範圍,可以使用 Echidna。 + +- \*\*繼承正確性。\*\*Solidity 合約高度依賴多重繼承。 很容易會出現錯誤,例如遮蔽函式缺少 `super` 呼叫,以及對 C3 線性化順序的誤解。 + + - Slither 是確保偵測到這些問題的工具。 + +- \*\*外部互動。\*\*合約會彼此互動,而某些外部合約不應被信任。 例如,如果您的合約依賴外部預言機,當一半可用的預言機遭到入侵時,它還能保持安全嗎? + + - Manticore 和 Echidna 是測試您的合約與外部互動的最佳選擇。 Manticore 有內建機制來模擬外部合約。 + +- \*\*標準符合性。\*\*以太坊標準(例如 ERC20)的設計歷史上曾出現過瑕疵。 請注意您所依據的標準的限制。 + - Slither、Echidna 和 Manticore 將幫助您偵測與給定標準的偏差。 + +### 工具選擇快捷手冊 {#tool-selection-cheatsheet} + +| 元件 | 工具 | 範例 | +| ----- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 狀態機 | Echidna、Manticore | | +| 存取控制 | Slither、Echidna、Manticore | [Slither 練習 2](https://github.com/crytic/slither/blob/7f54c8b948c34fb35e1d61adaa1bd568ca733253/docs/src/tutorials/exercise2.md)、[Echidna 練習 2](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/exercises/Exercise-2.md) | +| 算術運算 | Manticore、Echidna | [Echidna 練習 1](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/exercises/Exercise-1.md)、[Manticore 練習 1 - 3](https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/manticore/exercises) | +| 繼承正確性 | Slither | [Slither 練習 1](https://github.com/crytic/slither/blob/7f54c8b948c34fb35e1d61adaa1bd568ca733253/docs/src/tutorials/exercise1.md) | +| 外部互動 | Manticore、Echidna | | +| 標準符合性 | Slither、Echidna、Manticore | [`slither-erc`](https://github.com/crytic/slither/wiki/ERC-Conformance) | + +根據您的目標,可能還需要檢查其他領域,但這些粗略的重點領域對於任何智能合約系統來說都是一個好的開始。 + +我們的公開審計報告中包含了經過驗證或測試的屬性範例。 請考慮閱讀以下報告的「自動化測試與驗證」部分,以審查真實世界的安全屬性: + +- [0x](https://github.com/trailofbits/publications/blob/master/reviews/0x-protocol.pdf) +- [Balancer](https://github.com/trailofbits/publications/blob/master/reviews/BalancerCore.pdf) diff --git a/public/content/translations/zh-tw/developers/tutorials/hello-world-smart-contract-fullstack/index.md b/public/content/translations/zh-tw/developers/tutorials/hello-world-smart-contract-fullstack/index.md new file mode 100644 index 00000000000..5024603cfbc --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/hello-world-smart-contract-fullstack/index.md @@ -0,0 +1,1540 @@ +--- +title: "給初學者的 Hello World 智慧型合約 - 全端" +description: "在以太坊上撰寫和部署簡單智能合約的入門教學。" +author: "nstrike2" +tags: + [ + "穩固", + "hardhat", + "alchemy", + "智能合約", + "部署", + "區塊瀏覽器", + "前端", + "交易" + ] +skill: beginner +lang: zh-tw +published: 2021-10-25 +--- + +如果您是區塊鏈開發新手,不知道從何開始,或不知道如何部署智慧型合約並與之互動,本指南就是為您準備的。 我們將逐步說明如何使用 [MetaMask](https://metamask.io)、[Solidity](https://docs.soliditylang.org/en/v0.8.0/)、[Hardhat](https://hardhat.org) 和 [Alchemy](https://alchemy.com/eth),在 Goerli 測試網上建立並部署一個簡單的智慧型合約。 + +您需要一個 Alchemy 帳戶才能完成本教學。 [註冊免費帳戶](https://www.alchemy.com/) + +如果您在任何時候有任何疑問,歡迎隨時到 [Alchemy Discord](https://discord.gg/gWuC7zB) 提問! + +## 第一部分 - 使用 Hardhat 建立與部署您的智慧型合約 {#part-1} + +### 連線至以太坊網路 {#connect-to-the-ethereum-network} + +向以太坊鏈發出請求有很多種方式。 為求簡單,我們將使用 Alchemy 上的免費帳戶。Alchemy 是一個區塊鏈開發者平台及 API,讓我們無須自行執行節點就能與以太坊鏈通訊。 Alchemy 也有用於監控和分析的開發者工具;我們將在本教學中利用這些工具來了解我們智慧型合約部署的底層運作情況。 + +### 建立您的應用程式和 API 金鑰 {#create-your-app-and-api-key} + +建立 Alchemy 帳戶後,您可以透過建立應用程式來產生 API 金鑰。 這將允許您向 Goerli 測試網發出請求。 如果您不熟悉測試網,可以閱讀 [Alchemy 選擇網路的指南](https://www.alchemy.com/docs/choosing-a-web3-network)。 + +在 Alchemy 儀表板上,於導覽列中找到 **Apps** 下拉式選單,然後點擊 **Create App**。 + +![Hello world 創建應用程式](./hello-world-create-app.png) + +將您的應用程式命名為「_Hello World_」,並寫下簡短描述。 選擇 **Staging** 作為您的環境,**Goerli** 作為您的網路。 + +![創建應用程式檢視 hello world](./create-app-view-hello-world.png) + +_注意:請務必選擇 **Goerli**,否則本教學將無法運作。_ + +點擊 **Create app**。 您的應用程式將出現在下方的表格中。 + +### 建立一個以太坊帳戶 {#create-an-ethereum-account} + +您需要一個以太坊帳戶來傳送和接收交易。 我們將使用 MetaMask,這是一款瀏覽器內的虛擬錢包,可讓使用者管理其以太坊帳戶地址。 + +您可以在[這裡](https://metamask.io/download)免費下載並建立 MetaMask 帳戶。 在建立帳戶時,或如果您已有帳戶,請確保切換到右上角的「Goerli 測試網」(這樣我們就不會處理真實貨幣)。 + +### 第 4 步:從水龍頭取得以太幣 {#step-4-add-ether-from-a-faucet} + +要將您的智慧型合約部署到測試網,您會需要一些假的 ETH。 要在 Goerli 網路上取得 ETH,請前往 Goerli 水龍頭,並輸入您的 Goerli 帳戶地址。 請注意,Goerli 水龍頭最近可能有點不穩定 - 請參閱[測試網頁面](/developers/docs/networks/#goerli),查看可嘗試的選項清單: + +_注意:由於網路壅塞,這可能需要一些時間。_ +`` + +### 步驟 5:檢查您的餘額 {#step-5-check-your-balance} + +為了再次確認 ETH 已在您的錢包中,讓我們使用 [Alchemy 的編寫工具](https://composer.alchemyapi.io/?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D) 發出 [eth_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance) 請求。 這將會回傳你的錢包裡的餘額。 要了解更多資訊,請查看 [Alchemy 關於如何使用編寫工具的簡短教學](https://youtu.be/r6sjRxBZJuU)。 + +輸入您的 MetaMask 帳戶地址,然後點擊 **Send Request**。 您將會看到類似下方程式碼片段的回應。 + +```json +{ "jsonrpc": "2.0", "id": 0, "result": "0x2B5E3AF16B1880000" } +``` + +> _注意:此結果以 wei 為單位,而非 ETH。_ Wei 是以太幣的最小單位。_ + +哈! 我們的假錢都在這。 + +### 步驟 6:初始化我們的專案 {#step-6-initialize-our-project} + +首先,我們需要為我們的專案建立一個資料夾。 導覽至您的命令列並輸入以下內容。 + +``` +mkdir hello-world +cd hello-world +``` + +現在我們在專案資料夾中了,我們將使用 `npm init` 來初始化專案。 + +> 如果您尚未安裝 npm,請遵循[這些說明來安裝 Node.js 和 npm](https://docs.alchemyapi.io/alchemy/guides/alchemy-for-macs#1-install-nodejs-and-npm)。 + +就本教學而言,您如何回答初始化問題並不重要。 以下是我們的做法,僅供參考: + +``` +套件名稱:(hello-world) +版本:(1.0.0) +描述:hello world 智慧型合約 +進入點:(index.js) +測試指令: +git 儲存庫: +關鍵字: +作者: +授權:(ISC) + +即將寫入 /Users/.../.../.../hello-world/package.json: + +{ + "name": "hello-world", + "version": "1.0.0", + "description": "hello world 智慧型合約", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} +``` + +核准 package.json,我們就可以開始了! + +### 步驟 7:下載 Hardhat {#step-7-download-hardhat} + +Hardhat 是一個開發環境,提供你去編譯、部屬、測試、以及除錯你的以太坊軟體。 它能協助開發人員在部署至即時鏈之前,於本機建立智慧合約和去中心化應用程式。 + +在我們的 `hello-world` 專案中執行: + +``` +npm install --save-dev hardhat +``` + +如需更多[安裝指示](https://hardhat.org/getting-started/#overview)的詳細資訊,請查看此頁面。 + +### 步驟 8:建立 Hardhat 專案 {#step-8-create-hardhat-project} + +在我們的 `hello-world` 專案資料夾中,執行: + +``` +npx hardhat +``` + +你接下來會看到歡迎訊息以及關於你想做什麼的選項。 選擇"create an empty hardhat.config.js": + +``` +888 888 888 888 888 +888 888 888 888 888 +888 888 888 888 888 +8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888 +888 888 "88b 888P" d88" 888 888 "88b "88b 888 +888 888 .d888888 888 888 888 888 888 .d888888 888 +888 888 888 888 888 Y88b 888 888 888 888 888 Y88b. +888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888 + +👷 歡迎使用 Hardhat v2.0.11 👷‍ + +您想做什麼?… +建立範例專案 +❯ 建立一個空的 hardhat.config.js +退出 +``` + +這將在專案中產生一個 `hardhat.config.js` 檔案。 我們稍後將在本教學中使用此檔案來指定專案的設定。 + +### 步驟 9:新增專案資料夾 {#step-9-add-project-folders} + +為了讓專案保持井然有序,我們來建立兩個新資料夾。 在命令列中,導覽至 `hello-world` 專案的根目錄並輸入: + +``` +mkdir contracts +mkdir scripts +``` + +- `contracts/` 是我們存放 hello world 智能合約程式碼檔案的地方 +- `scripts/` 是我們存放部署和與合約互動的腳本的地方 + +### 步驟 10:編寫我們的合約 {#step-10-write-our-contract} + +您可能會問自己,我們什麼時候才要開始寫程式碼? 就是現在! + +在您喜歡的編輯器中開啟 hello-world 專案。 智慧型合約最常用 Solidity 編寫,我們將使用它來編寫我們的智慧型合約。‌ + +1. 導覽至 `contracts` 資料夾並建立一個名為 `HelloWorld.sol` 的新檔案 +2. 以下是我們將在本教學中使用的範例 Hello World 智慧型合約。 將以下內容複製到 `HelloWorld.sol` 檔案中。 + +_注意:請務必閱讀註解以了解此合約的功能。_ + +``` +// 指定 Solidity 的版本,使用語意化版本控制。 +// 了解更多:https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma +pragma solidity >=0.7.3; + +// 定義一個名為「HelloWorld」的合約。 +// 合約是函式和資料 (其狀態) 的集合。部署後,合約會存放在以太坊區塊鏈的特定地址上。了解更多:https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html +contract HelloWorld { + + // 呼叫更新函式時發出 + // 智慧型合約事件是您合約的一種方式,可將區塊鏈上發生的事情傳達給您的應用程式前端,前端可以「監聽」某些事件並在事件發生時採取行動。 + event UpdatedMessages(string oldStr, string newStr); + + // 宣告一個「string」類型的狀態變數「message」。 + // 狀態變數是其值永久儲存在合約儲存空間中的變數。關鍵字「public」可讓變數從合約外部存取,並建立一個其他合約或用戶端可呼叫以存取該值的函式。 + string public message; + + // 與許多以類別為基礎的物件導向語言相似,建構函式是一個特殊函式,只在合約建立時執行。 + // 建構函式用於初始化合約的資料。了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors + constructor(string memory initMessage) { + + // 接受一個字串引數「initMessage」,並將該值設定到合約的「message」儲存變數中)。 + message = initMessage; + } + + // 一個公共函式,接受一個字串引數並更新「message」儲存變數。 + function update(string memory newMessage) public { + string memory oldMsg = message; + message = newMessage; + emit UpdatedMessages(oldMsg, newMessage); + } +} +``` + +這是一個基本的智慧型合約,在建立時儲存一則訊息。 可以透過呼叫 `update` 函式來更新。 + +### 步驟 11:將 MetaMask 和 Alchemy 連線至您的專案 {#step-11-connect-metamask-alchemy-to-your-project} + +我們已經建立了 MetaMask 錢包、Alchemy 帳戶,並編寫了我們的智能合約,現在是時候將這三者連接起來了。 + +從您的錢包傳送的每筆交易都需要使用您唯一的私密金鑰簽署。 為了向我們的程式提供此權限,我們可以安全地將我們的私密金鑰儲存在環境檔案中。 我們也將在此處儲存 Alchemy 的 API 金鑰。 + +> 若要深入了解如何傳送交易,請查看這篇關於使用 web3 傳送交易的[教學](https://www.alchemy.com/docs/hello-world-smart-contract#step-11-connect-metamask--alchemy-to-your-project)。 + +首先,安裝 dotenv 套件。 + +``` +npm install dotenv --save +``` + +然後,在專案的根目錄中建立一個 `.env` 檔案。 將您的 MetaMask 私密金鑰和 HTTP Alchemy API URL 新增至其中。 + +您的環境檔案必須命名為 `.env`,否則它將不會被辨識為環境檔案。 + +請勿將其命名為 `process.env`、`.env-custom` 或任何其他名稱。 + +- 請遵循[這些說明](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key)來匯出您的私密金鑰 +- 請參閱下文以取得 HTTP Alchemy API URL + +![](./get-alchemy-api-key.gif) + +你的 `.env` 應該看起來像這樣: + +``` +API_URL = "https://eth-goerli.alchemyapi.io/v2/your-api-key" +PRIVATE_KEY = "your-metamask-private-key" +``` + +為了實際將這些連接到我們的程式碼,我們將在第 13 步的 `hardhat.config.js` 檔案中引用這些變數。 + +### 第 12 步:安裝 Ethers.js {#step-12-install-ethersjs} + +Ethers.js 是一個函式庫,它透過將[標準 JSON-RPC 方法](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc)包裝成更方便使用者使用的方法,讓與以太坊的互動和請求變得更容易。 + +Hardhat 可讓您整合[外掛程式](https://hardhat.org/plugins/)以取得額外的工具和擴充功能。 我們將利用 [Ethers 外掛程式](https://hardhat.org/docs/plugins/official-plugins#hardhat-ethers)來部署合約。 + +在你的專案目錄輸入: + +```bash +npm install --save-dev @nomiclabs/hardhat-ethers "ethers@^5.0.0" +``` + +### 步驟 13:更新 hardhat.config.js {#step-13-update-hardhat-configjs} + +我們目前已經新增了幾個相依套件和外掛程式,現在我們需要更新 `hardhat.config.js`,讓我們的專案知道它們全部。 + +將你的 `hardhat.config.js` 更新成如下所示: + +```javascript +/** + * @type import('hardhat/config').HardhatUserConfig + */ + +require("dotenv").config() +require("@nomiclabs/hardhat-ethers") + +const { API_URL, PRIVATE_KEY } = process.env + +module.exports = { + solidity: "0.7.3", + defaultNetwork: "goerli", + networks: { + hardhat: {}, + goerli: { + url: API_URL, + accounts: [`0x${PRIVATE_KEY}`], + }, + }, +} +``` + +### 步驟 14:編譯我們的合約 {#step-14-compile-our-contract} + +為了確認一切運作正常,我們來編譯合約。 `compile` 任務是內建的 hardhat 任務之一。 + +在命令列工具輸入: + +```bash +npx hardhat compile +``` + +您可能會收到關於「原始程式檔中未提供 SPDX 授權識別碼」的警告,但無須擔心,希望其他一切都沒問題! 如果沒有,您隨時可以在 [Alchemy discord](https://discord.gg/u72VCg3) 中傳送訊息。 + +### 步驟 15:編寫我們的部署指令碼 {#step-15-write-our-deploy-script} + +現在我們已經寫好了合約,並且也搞定配置檔案。現在我們該來撰寫部署合約的腳本。 + +導覽至 `scripts/` 資料夾並建立一個名為 `deploy.js` 的新檔案,將以下內容加入其中: + +```javascript +async function main() { + const HelloWorld = await ethers.getContractFactory("HelloWorld") + + // 開始部署,回傳一個解析為合約物件的 promise + const hello_world = await HelloWorld.deploy("Hello World!") + console.log("合約已部署至地址:", hello_world.address) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) +``` + +Hardhat 在其[合約教學文章](https://hardhat.org/tutorial/testing-contracts.html#writing-tests)中詳細地解釋了每一行程式碼的作用,我們在此採用了他們的解釋。 + +```javascript +const HelloWorld = await ethers.getContractFactory("HelloWorld") +``` + +ethers.js 中的 `ContractFactory` 是用於部署新智慧型合約的抽象概念,因此這裡的 `HelloWorld` 是我們 hello world 合約執行個體的[工廠](https://en.wikipedia.org/wiki/Factory_\(object-oriented_programming\))。 使用 `hardhat-ethers` 外掛程式時,`ContractFactory` 和 `Contract` 執行個體預設會連線至第一個簽署者 (擁有者)。 + +```javascript +const hello_world = await HelloWorld.deploy() +``` + +在 `ContractFactory` 上呼叫 `deploy()` 將會開始部署,並回傳一個解析為 `Contract` 物件的 `Promise`。 這就是和我們的智慧型合約函數有一對一的方法的物件。 + +### 第 16 步:部署我們的合約 {#step-16-deploy-our-contract} + +我們終於準備好要部署合約了! 導覽至命令列並執行: + +```bash +npx hardhat run scripts/deploy.js --network goerli +``` + +你會看到像這樣的輸出: + +```bash +合約已部署至地址:0x6cd7d44516a20882cEa2DE9f205bF401c0d23570 +``` + +**請儲存此地址**。 我們稍後將在本教學中使用它。 + +如果我們前往 [Goerli etherscan](https://goerli.etherscan.io) 並搜尋我們的合約地址,我們應該能夠看到它已成功部署。 這個交易執行看起來會像這樣: + +![](./etherscan-contract.png) + +`From` 地址應與您的 MetaMask 帳戶地址相符,而 `To` 地址將顯示 **Contract Creation**。 如果我們點擊進入交易,我們將在 `To` 欄位中看到我們的合約地址。 + +![](./etherscan-transaction.png) + +恭喜! 您剛剛將智慧型合約部署到以太坊測試網。 + +若要了解底層的運作情況,讓我們導覽至 [Alchemy 儀表板](https://dashboard.alchemy.com/explorer)中的 Explorer 標籤。 如果您有多個 Alchemy 應用程式,請務必依應用程式篩選並選取 **Hello World**。 + +![](./hello-world-explorer.png) + +在這裡您會看到當我們呼叫 `.deploy()` 函式時,Hardhat/Ethers 在幕後為我們進行的一些 JSON-RPC 方法。 這裡有兩個重要的方法:[`eth_sendRawTransaction`](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_sendrawtransaction) (這是將我們的合約寫入 Goerli 鏈的請求),以及 [`eth_getTransactionByHash`](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_gettransactionbyhash) (這是在給定哈希值的情況下,讀取我們交易資訊的請求)。 若要深入了解如何傳送交易,請查看[我們關於使用 Web3 傳送交易的教學](/developers/tutorials/sending-transactions-using-web3-and-alchemy/)。 + +## 第二部分:與您的智慧型合約互動 {#part-2-interact-with-your-smart-contract} + +既然我們已成功將智慧型合約部署至 Goerli 網路,讓我們來學習如何與它互動。 + +### 建立一個 interact.js 檔案 {#create-a-interactjs-file} + +這就是我們將編寫互動腳本的檔案。 我們將使用您先前在第一部分中安裝的 Ethers.js 函式庫。 + +在 `scripts/` 資料夾中,建立一個名為 `interact.js` 的新檔案,並新增以下程式碼: + +```javascript +// interact.js + +const API_KEY = process.env.API_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY +const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS +``` + +### 更新您的 .env 檔案 {#update-your-env-file} + +我們將使用新的環境變數,因此我們需要在[先前建立](#step-11-connect-metamask-&-alchemy-to-your-project) 的 `.env` 檔案中定義它們。 + +我們需要新增我們的 Alchemy `API_KEY` 和部署智慧型合約的 `CONTRACT_ADDRESS` 的定義。 + +您的 `.env` 檔案應如下所示: + +```bash +# .env + +API_URL = "https://eth-goerli.alchemyapi.io/v2/" +API_KEY = "" +PRIVATE_KEY = "" +CONTRACT_ADDRESS = "0x" +``` + +### 取得您的合約 ABI {#grab-your-contract-ABI} + +我們的合約 [ABI (應用程式二進位介面)](/glossary/#abi) 是與我們的智慧型合約互動的介面。 Hardhat 會自動產生 ABI 並將其儲存在 `HelloWorld.json` 中。 若要使用 ABI,我們需要透過將以下程式碼行新增至我們的 `interact.js` 檔案來解析內容: + +```javascript +// interact.js +const contract = require("../artifacts/contracts/HelloWorld.sol/HelloWorld.json") +``` + +如果您想查看 ABI,可以將其輸出到您的主控台: + +```javascript +console.log(JSON.stringify(contract.abi)) +``` + +若要查看您的 ABI 列印至主控台,請導覽至您的終端機並執行: + +```bash +npx hardhat run scripts/interact.js +``` + +### 建立您的合約執行個體 {#create-an-instance-of-your-contract} + +若要與我們的合約互動,我們需要在我們的程式碼中建立一個合約執行個體。 若要使用 Ethers.js 執行此操作,我們需要處理三個概念: + +1. 提供者 - 一個節點提供者,提供您對區塊鏈的讀寫存取權 +2. 簽署者 - 代表一個可以簽署交易的以太坊帳戶 +3. 合約 - 代表部署在鏈上特定合約的 Ethers.js 物件 + +我們將使用上一步的合約 ABI 來建立我們的合約執行個體: + +```javascript +// interact.js + +// Provider +const alchemyProvider = new ethers.providers.AlchemyProvider( + (network = "goerli"), + API_KEY +) + +// Signer +const signer = new ethers.Wallet(PRIVATE_KEY, alchemyProvider) + +// Contract +const helloWorldContract = new ethers.Contract( + CONTRACT_ADDRESS, + contract.abi, + signer +) +``` + +在 [ethers.js 文件](https://docs.ethers.io/v5/) 中深入了解提供者、簽署者和合約。 + +### 讀取初始訊息 {#read-the-init-message} + +還記得我們在部署合約時設定了 `initMessage = "Hello world!"` 嗎? 我們現在要讀取儲存在我們智慧型合約中的那則訊息,並將它列印到主控台。 + +在 JavaScript 中,與網路互動時會使用非同步函式。 要深入了解非同步函式,請[閱讀這篇 Medium 文章](https://blog.bitsrc.io/understanding-asynchronous-javascript-the-event-loop-74cd408419ff)。 + +使用以下程式碼呼叫我們智慧型合約中的 `message` 函式並讀取初始訊息: + +```javascript +// interact.js + +// ... + +async function main() { + const message = await helloWorldContract.message() + console.log("訊息是:" + message) +} +main() +``` + +在終端機中使用 `npx hardhat run scripts/interact.js` 執行檔案後,我們應該會看到以下回應: + +``` +訊息是:Hello world! +``` + +恭喜! 您剛剛成功地從以太坊區塊鏈讀取智慧型合約資料,太棒了! + +### 更新訊息 {#update-the-message} + +除了只讀取訊息,我們還可以使用 `update` 函式來更新儲存在我們智慧型合約中的訊息! 很酷,對吧? + +若要更新訊息,我們可以直接在我們實例化的合約物件上呼叫 `update` 函式: + +```javascript +// interact.js + +// ... + +async function main() { + const message = await helloWorldContract.message() + console.log("訊息是:" + message) + + console.log("正在更新訊息...") + const tx = await helloWorldContract.update("這是新訊息。") + await tx.wait() +} +main() +``` + +請注意,在第 11 行,我們在回傳的交易物件上呼叫了 `.wait()`。 這可確保我們的腳本在結束函式前,會等待交易在區塊鏈上被挖出。 如果未包含 `.wait()` 呼叫,腳本可能無法看到合約中更新的 `message` 值。 + +### 讀取新訊息 {#read-the-new-message} + +您應該能夠重複[上一步](#read-the-init-message)來讀取更新的 `message` 值。 花點時間看看您是否能做出必要的變更來列印出那個新值! + +如果您需要提示,以下是您此時的 `interact.js` 檔案應有的樣子: + +```javascript +// interact.js + +const API_KEY = process.env.API_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY +const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS + +const contract = require("../artifacts/contracts/HelloWorld.sol/HelloWorld.json") + +// 提供者 - Alchemy +const alchemyProvider = new ethers.providers.AlchemyProvider( + (network = "goerli"), + API_KEY +) + +// 簽署者 - 您 +const signer = new ethers.Wallet(PRIVATE_KEY, alchemyProvider) + +// 合約執行個體 +const helloWorldContract = new ethers.Contract( + CONTRACT_ADDRESS, + contract.abi, + signer +) + +async function main() { + const message = await helloWorldContract.message() + console.log("訊息是:" + message) + + console.log("正在更新訊息...") + const tx = await helloWorldContract.update("這是新訊息") + await tx.wait() + + const newMessage = await helloWorldContract.message() + console.log("新訊息是:" + newMessage) +} + +main() +``` + +現在只要執行腳本,您就應該能看到舊訊息、更新狀態,以及新訊息列印到您的終端機上! + +`npx hardhat run scripts/interact.js --network goerli` + +``` +訊息是:Hello World! +正在更新訊息... +新訊息是:這是新訊息。 +``` + +執行該腳本時,您可能會注意到「正在更新訊息...」步驟在新訊息載入前需要一些時間。 這是由於挖礦過程所致;如果您好奇在挖礦時追蹤交易,請造訪 [Alchemy 記憶體池](https://dashboard.alchemyapi.io/mempool)來查看交易狀態。 如果交易被丟棄,檢查 [Goerli Etherscan](https://goerli.etherscan.io) 並搜尋您的交易哈希也很有幫助。 + +## 第三部分:將您的智慧型合約發布至 Etherscan {#part-3-publish-your-smart-contract-to-etherscan} + +您已費盡心力讓您的智慧型合約活起來;現在是時候與全世界分享它了! + +透過在 Etherscan 上驗證您的智慧型合約,任何人都可以檢視您的原始碼並與您的智慧型合約互動。 我們開始吧! + +### 步驟 1:在您的 Etherscan 帳戶上產生 API 金鑰 {#step-1-generate-an-api-key-on-your-etherscan-account} + +需要 Etherscan API 金鑰來驗證您擁有您嘗試發布的智慧型合約。 + +如果您還沒有 Etherscan 帳戶,請[註冊一個帳戶](https://etherscan.io/register)。 + +登入後,在導覽列中找到您的使用者名稱,將滑鼠懸停在其上並選取 **My profile** 按鈕。 + +在您的個人資料頁面上,您應該會看到一個側邊導覽列。 從側邊導覽列中,選取 **API Keys**。 接下來,按下「新增」按鈕以建立新的 API 金鑰,將您的應用程式命名為 **hello-world** 並按下 **Create New API Key** 按鈕。 + +您的新 API 金鑰應會出現在 API 金鑰表格中。 將 API 金鑰複製到您的剪貼簿。 + +接下來,我們需要將 Etherscan API 金鑰新增至我們的 `.env` 檔案。 + +新增後,您的 `.env` 檔案應如下所示: + +```javascript +API_URL = "https://eth-goerli.alchemyapi.io/v2/your-api-key" +PUBLIC_KEY = "your-public-account-address" +PRIVATE_KEY = "your-private-account-address" +CONTRACT_ADDRESS = "your-contract-address" +ETHERSCAN_API_KEY = "your-etherscan-key" +``` + +### Hardhat 部署的智慧型合約 {#hardhat-deployed-smart-contracts} + +#### 安裝 hardhat-etherscan {#install-hardhat-etherscan} + +使用 Hardhat 將您的合約發布至 Etherscan 非常簡單。 您首先需要安裝 `hardhat-etherscan` 外掛程式才能開始。 `hardhat-etherscan` 會自動在 Etherscan 上驗證智慧型合約的原始碼和 ABI。 若要新增此項,請在 `hello-world` 目錄中執行: + +```text +npm install --save-dev @nomiclabs/hardhat-etherscan +``` + +安裝後,在您的 `hardhat.config.js` 頂端包含以下陳述式,並新增 Etherscan 設定選項: + +```javascript +// hardhat.config.js + +require("dotenv").config() +require("@nomiclabs/hardhat-ethers") +require("@nomiclabs/hardhat-etherscan") + +const { API_URL, PRIVATE_KEY, ETHERSCAN_API_KEY } = process.env + +module.exports = { + solidity: "0.7.3", + defaultNetwork: "goerli", + networks: { + hardhat: {}, + goerli: { + url: API_URL, + accounts: [`0x${PRIVATE_KEY}`], + }, + }, + etherscan: { + // Your API key for Etherscan + // 在 https://etherscan.io/ 取得一個 + apiKey: ETHERSCAN_API_KEY, + }, +} +``` + +#### 在 Etherscan 上驗證您的智慧型合約 {#verify-your-smart-contract-on-etherscan} + +確保所有檔案都已儲存,且所有 `.env` 變數都已正確設定。 + +執行 `verify` 任務,傳遞合約地址以及部署到的網路: + +```text +npx hardhat verify --network goerli DEPLOYED_CONTRACT_ADDRESS 'Hello World!' +``` + +請確定 `DEPLOYED_CONTRACT_ADDRESS` 是您在 Goerli 測試網上部署的智慧型合約的地址。 此外,最後一個引數 (`'Hello World!'`) 必須與[第一部分中的部署步驟](#write-our-deploy-script)中所使用的字串值相同。 + +如果一切順利,您將在終端機中看到以下訊息: + +```text +Successfully submitted source code for contract +contracts/HelloWorld.sol:HelloWorld at 0xdeployed-contract-address +for verification on Etherscan. Waiting for verification result... + + +Successfully verified contract HelloWorld on Etherscan. +https://goerli.etherscan.io/address/#contracts +``` + +恭喜! 您的智慧型合約程式碼已在 Etherscan 上! + +### 在 Etherscan 上查看您的智慧型合約! {#check-out-your-smart-contract-on-etherscan} + +當您導覽至終端機中提供的連結時,您應該能夠看到您在 Etherscan 上發布的智慧型合約程式碼和 ABI! + +**哇嗚 - 你做到了,冠軍! 現在任何人都可以呼叫或寫入您的智慧型合約! 我們迫不及待想看看您接下來會打造出什麼!** + +## 第四部分 - 將您的智慧型合約與前端整合 {#part-4-integrating-your-smart-contract-with-the-frontend} + +完成本教學後,您將知道如何: + +- 將 MetaMask 錢包連線至您的去中心化應用程式 +- 使用 [Alchemy Web3](https://docs.alchemy.com/alchemy/documentation/alchemy-web3) API 從您的智慧型合約讀取資料 +- 使用 MetaMask 簽署以太坊交易 + +對於這個去中心化應用程式,我們將使用 [React](https://react.dev/) 作為我們的前端框架;然而,需要注意的是,我們不會花太多時間分解其基礎知識,因為我們將主要專注於為我們的專案帶來 Web3 功能。 + +作為先決條件,您應該對 React 有初學者級的了解。 如果沒有,我們建議完成官方的[React 入門教學](https://react.dev/learn)。 + +### 複製入門檔案 {#clone-the-starter-files} + +首先,前往 [hello-world-part-four GitHub 儲存庫](https://github.com/alchemyplatform/hello-world-part-four-tutorial) 以取得此專案的入門檔案,並將此儲存庫複製到您的本機電腦。 + +在本機開啟複製的儲存庫。 請注意,它包含兩個資料夾:`starter-files` 和 `completed`。 + +- `starter-files` - **我們將在此目錄中工作**,我們將把 UI 連線至您的以太坊錢包和我們在[第三部分](#part-3)中發布到 Etherscan 的智慧型合約。 +- `completed` 包含整個已完成的教學,如果您遇到困難,應僅用作參考。 + +接下來,在您最喜歡的程式碼編輯器中開啟您的 `starter-files` 副本,然後導覽至 `src` 資料夾。 + +我們將撰寫的所有程式碼都會放在 `src` 資料夾底下。 我們將編輯 `HelloWorld.js` 元件和 `util/interact.js` JavaScript 檔案,為我們的專案提供 Web3 功能。 + +### 查看入門檔案 {#check-out-the-starter-files} + +在我們開始編寫程式碼之前,讓我們來探索一下入門檔案中提供了什麼。 + +#### 讓你的 React 專案動起來 {#get-your-react-project-running} + +讓我們透過在我們的瀏覽器內運行這個「反應」專案來開始是日的教程吧: 「反應」的美在於一旦我們在瀏覽器內已經有在運行自己的專案,我們儲存下來的任何改變都將會被實時更新到我們的瀏覽器裡。 + +若要讓專案執行,請導覽至 `starter-files` 資料夾的根目錄,然後在您的終端機中執行 `npm install` 以安裝專案的相依性: + +```bash +cd starter-files +npm install +``` + +安裝完成後,在你的終端機中執行 `npm start`: + +```bash +npm start +``` + +這麼做應該會在您的瀏覽器中開啟 [http://localhost:3000/](http://localhost:3000/),您將在那裡看到我們專案的前端。 它應該包含一個欄位 (一個更新儲存在您智慧型合約中訊息的地方)、一個「連線錢包」按鈕,和一個「更新」按鈕。 + +如果您嘗試點擊任一按鈕,您會發現它們無法運作——那是因為我們還需要編寫它們的功能。 + +#### `HelloWorld.js` 元件 {#the-helloworld-js-component} + +讓我們回到編輯器中的 `src` 資料夾,並開啟 `HelloWorld.js` 檔案。 這個動作在我們理解該檔案內所有東西上有著超級關鍵的作用,因為它是我們將會首先處理的第一個「反應」組件。 + +在此檔案的頂端,您會注意到我們有幾個執行專案所必需的匯入陳述式,包括 React 函式庫、useEffect 和 useState hook、來自 `./util/interact.js` 的一些項目 (我們稍後將更詳細地描述它們!),以及 Alchemy 標誌。 + +```javascript +// HelloWorld.js + +import React from "react" +import { useEffect, useState } from "react" +import { + helloWorldContract, + connectWallet, + updateMessage, + loadCurrentMessage, + getCurrentWalletConnected, +} from "./util/interact.js" + +import alchemylogo from "./alchemylogo.svg" +``` + +接下來,我們有我們的狀態變數,我們將在特定事件後更新它們。 + +```javascript +// HelloWorld.js + +//狀態變數 +const [walletAddress, setWallet] = useState("") +const [status, setStatus] = useState("") +const [message, setMessage] = useState("未連線至網路。") +const [newMessage, setNewMessage] = useState("") +``` + +以下是每個變數所代表的意義: + +- `walletAddress` - 一個儲存使用者錢包位址的字串 +- `status` - 一個儲存有用訊息的字串,引導使用者如何與去中心化應用程式互動 +- `message` - 一個儲存智慧型合約中目前訊息的字串 +- `newMessage` - 一個儲存將寫入智慧型合約的新訊息的字串 + +在狀態變數之後,您會看到五個未實作的函式:`useEffect`、`addSmartContractListener`、`addWalletListener`、`connectWalletPressed` 和 `onUpdatePressed`。 我們將在下方解釋它們的功能: + +```javascript +// HelloWorld.js + +//僅呼叫一次 +useEffect(async () => { + //TODO: 實作 +}, []) + +function addSmartContractListener() { + //TODO: 實作 +} + +function addWalletListener() { + //TODO: 實作 +} + +const connectWalletPressed = async () => { + //TODO: 實作 +} + +const onUpdatePressed = async () => { + //TODO: 實作 +} +``` + +- [`useEffect`](https://legacy.reactjs.org/docs/hooks-effect.html) - 這是一個 React hook,在您的元件渲染後呼叫。 因為它傳入了一個空陣列 `[]` prop (見第 4 行),所以它只會在元件的_第一次_渲染時被呼叫。 在這裡,我們將載入儲存在我們智慧型合約中的目前訊息,呼叫我們的智慧型合約和錢包監聽器,並更新我們的 UI 以反映錢包是否已連線。 +- `addSmartContractListener` - 此函式設定一個監聽器,它將監看我們的 HelloWorld 合約的 `UpdatedMessages` 事件,並在我們智慧型合約中的訊息變更時更新我們的 UI。 +- `addWalletListener` - 此函式設定一個監聽器,偵測使用者 MetaMask 錢包狀態的變更,例如使用者中斷錢包連線或切換地址時。 +- `connectWalletPressed` - 此函式將被呼叫以將使用者的 MetaMask 錢包連線至我們的去中心化應用程式。 +- `onUpdatePressed` - 當使用者想要更新儲存在智慧型合約中的訊息時,將會呼叫此函式。 + +接近這份檔案的尾聲,我們得到了我們組件的UI。 + +```javascript +// HelloWorld.js + +//我們元件的 UI +return ( +
+ + + +

目前訊息:

+

{message}

+ +

新訊息:

+ +
+ setNewMessage(e.target.value)} + value={newMessage} + /> +

{status}

+ + +
+ +
+) +``` + +如果您仔細掃描此程式碼,您會注意到我們在 UI 中使用了各種狀態變數: + +- 在第 6-12 行,如果使用者的錢包已連線 (即 `walletAddress.length > 0`),我們會在 ID 為「walletButton」的按鈕中顯示使用者 `walletAddress` 的截斷版本;否則它只會顯示「連線錢包」。 +- 在第 17 行,我們顯示儲存在智慧型合約中的目前訊息,該訊息擷取在 `message` 字串中。 +- 在第 23-26 行,我們使用[受控元件](https://legacy.reactjs.org/docs/forms.html#controlled-components)來更新我們的 `newMessage` 狀態變數,當文字欄位中的輸入變更時。 + +除了我們的狀態變數,您還會看到 `connectWalletPressed` 和 `onUpdatePressed` 函式分別在點擊 ID 為 `publishButton` 和 `walletButton` 的按鈕時被呼叫。 + +最後,讓我們來處理這個 `HelloWorld.js` 元件被新增到哪裡的問題。 + +如果您前往 `App.js` 檔案,這是 React 中的主要元件,作為所有其他元件的容器,您會看到我們的 `HelloWorld.js` 元件被注入在第 7 行。 + +最後但同樣重要的是,讓我們查看為您提供的另一個檔案,即 `interact.js` 檔案。 + +#### `interact.js` 檔案 {#the-interact-js-file} + +因為我們想要遵循 [M-V-C](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 範式,我們將需要一個單獨的檔案,其中包含我們所有的函式來管理我們去中心化應用程式的邏輯、資料和規則,然後能夠將這些函式匯出到我們的前端 (我們的 `HelloWorld.js` 元件)。 + +👆🏽這正是我們的 `interact.js` 檔案的目的! + +導覽至您 `src` 目錄中的 `util` 資料夾,您會注意到我們包含了一個名為 `interact.js` 的檔案,它將包含我們所有的智慧型合約互動和錢包函式與變數。 + +```javascript +// interact.js + +//export const helloWorldContract; + +export const loadCurrentMessage = async () => {} + +export const connectWallet = async () => {} + +const getCurrentWalletConnected = async () => {} + +export const updateMessage = async (message) => {} +``` + +您會注意到檔案頂端,我們已註解掉 `helloWorldContract` 物件。 稍後在本教學中,我們將取消註解此物件,並在此變數中實例化我們的智慧型合約,然後我們將其匯出至我們的 `HelloWorld.js` 元件。 + +我們 `helloWorldContract` 物件之後的四個未實作函式執行以下操作: + +- `loadCurrentMessage` - 此函式處理載入儲存在智慧型合約中目前訊息的邏輯。 它將使用 [Alchemy Web3 API](https://github.com/alchemyplatform/alchemy-web3) 對 Hello World 智慧型合約進行_讀取_呼叫。 +- `connectWallet` - 此函式將把使用者的 MetaMask 連線至我們的去中心化應用程式。 +- `getCurrentWalletConnected` - 此函式將在頁面載入時檢查以太坊帳戶是否已連線至我們的去中心化應用程式,並相應地更新我們的 UI。 +- `updateMessage` - 此函式將更新儲存在智慧型合約中的訊息。 它將對 Hello World 智慧型合約進行_寫入_呼叫,因此使用者的 MetaMask 錢包必須簽署一筆以太坊交易才能更新訊息。 + +既然我們了解了我們正在處理的內容,讓我們來弄清楚如何從我們的智慧型合約中讀取! + +### 步驟 3:從您的智慧型合約讀取 {#step-3-read-from-your-smart-contract} + +若要從您的智慧型合約讀取,您需要成功設定: + +- 與以太坊鏈的 API 連線 +- 您智慧型合約的載入執行個體 +- 呼叫您智慧型合約函式的函式 +- 一個監聽器,用於在您從智慧型合約讀取的資料變更時監看更新 + +這聽起來可能有很多步驟,但別擔心! 我們將一步一步引導您完成它們! :\) + +#### 建立與以太坊鏈的 API 連線 {#establish-an-api-connection-to-the-ethereum-chain} + +還記得在本教學的第二部分中,我們如何使用我們的 [Alchemy Web3 金鑰從我們的智慧型合約讀取](https://docs.alchemy.com/alchemy/tutorials/hello-world-smart-contract/interacting-with-a-smart-contract#step-1-install-web3-library)嗎? 您還需要在您的去中心化應用程式中使用 Alchemy Web3 金鑰才能從鏈上讀取。 + +如果您還沒有,首先安裝 [Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3),方法是導覽至您 `starter-files` 的根目錄,並在您的終端機中執行以下指令: + +```text +npm install @alch/alchemy-web3 +``` + +[Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3) 是 [Web3.js](https://docs.web3js.org/) 的一個包裝器,提供了增強的 API 方法和其他關鍵優勢,讓你的 web3 開發者生活更輕鬆。 它是被設計成最低配置,因此你能夠在你的應用程式內馬上開始使用它! + +然後,在您的專案目錄中安裝 [dotenv](https://www.npmjs.com/package/dotenv) 套件,這樣我們在取得 API 金鑰後就有一個安全的地方來儲存它。 + +```text +npm install dotenv --save +``` + +對於我們的去中心化應用程式,**我們將使用我們的 Websockets API 金鑰** 而非我們的 HTTP API 金鑰,因為它將允許我們設定一個監聽器,偵測儲存在智慧型合約中的訊息何時變更。 + +取得您的 API 金鑰後,在您的根目錄中建立一個 `.env` 檔案,並將您的 Alchemy Websockets url 新增至其中。 之後,您的 `.env` 檔案應如下所示: + +```javascript +REACT_APP_ALCHEMY_KEY = wss://eth-goerli.ws.alchemyapi.io/v2/ +``` + +現在,我們已準備好在我們的去中心化應用程式中設定我們的 Alchemy Web3 端點! 讓我們回到我們的 `interact.js`,它巢狀在我們的 `util` 資料夾中,並在檔案頂端新增以下程式碼: + +```javascript +// interact.js + +require("dotenv").config() +const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(alchemyKey) + +//export const helloWorldContract; +``` + +在上方,我們首先從我們的 `.env` 檔案匯入 Alchemy 金鑰,然後將我們的 `alchemyKey` 傳遞給 `createAlchemyWeb3` 以建立我們的 Alchemy Web3 端點。 + +有了這個端點,是時候載入我們的智慧型合約了! + +#### 載入您的 Hello World 智慧型合約 {#loading-your-hello-world-smart-contract} + +要載入您的 Hello World 智慧型合約,您需要其合約地址和 ABI,如果您完成了[本教學的第三部分](/developers/tutorials/hello-world-smart-contract-fullstack/#part-3-publish-your-smart-contract-to-etherscan-part-3-publish-your-smart-contract-to-etherscan),兩者都可以在 Etherscan 上找到。 + +#### 如何從 Etherscan 取得您的合約 ABI {#how-to-get-your-contract-abi-from-etherscan} + +如果您跳過了本教學的第三部分,您可以使用地址為 [0x6f3f635A9762B47954229Ea479b4541eAF402A6A](https://goerli.etherscan.io/address/0x6f3f635a9762b47954229ea479b4541eaf402a6a#code) 的 HelloWorld 合約。 其 ABI 可以在[這裡](https://goerli.etherscan.io/address/0x6f3f635a9762b47954229ea479b4541eaf402a6a#code)找到。 + +合約 ABI 對於指定合約將調用哪個函式以及確保函式將以您期望的格式回傳資料是必要的。 複製我們的合約 ABI 後,讓我們將其另存為一個名為 `contract-abi.json` 的 JSON 檔案,儲存在您的 `src` 目錄中。 + +您的 contract-abi.json 應儲存在您的 src 資料夾中。 + +有了我們的合約地址、ABI 和 Alchemy Web3 端點,我們可以使用 [contract 方法](https://docs.web3js.org/api/web3-eth-contract/class/Contract)來載入我們智慧型合約的執行個體。 將您的合約 ABI 匯入 `interact.js` 檔案並新增您的合約地址。 + +```javascript +// interact.js + +const contractABI = require("../contract-abi.json") +const contractAddress = "0x6f3f635A9762B47954229Ea479b4541eAF402A6A" +``` + +我們現在終於可以取消註解我們的 `helloWorldContract` 變數,並使用我們的 AlchemyWeb3 端點載入智慧型合約: + +```javascript +// interact.js +export const helloWorldContract = new web3.eth.Contract( + contractABI, + contractAddress +) +``` + +總結一下,您 `interact.js` 的前 12 行現在應該如下所示: + +```javascript +// interact.js + +require("dotenv").config() +const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(alchemyKey) + +const contractABI = require("../contract-abi.json") +const contractAddress = "0x6f3f635A9762B47954229Ea479b4541eAF402A6A" + +export const helloWorldContract = new web3.eth.Contract( + contractABI, + contractAddress +) +``` + +既然我們已載入合約,我們就可以實作我們的 `loadCurrentMessage` 函式了! + +#### 在您的 `interact.js` 檔案中實作 `loadCurrentMessage` {#implementing-loadCurrentMessage-in-your-interact-js-file} + +這個函式非常簡單。 我們將進行一個簡單的非同步 web3 呼叫來從我們的合約中讀取。 我們的函式將回傳儲存在智慧型合約中的訊息: + +將您 `interact.js` 檔案中的 `loadCurrentMessage` 更新為以下內容: + +```javascript +// interact.js + +export const loadCurrentMessage = async () => { + const message = await helloWorldContract.methods.message().call() + return message +} +``` + +由於我們希望在我們的 UI 中顯示此智慧型合約,讓我們將 `HelloWorld.js` 元件中的 `useEffect` 函式更新為以下內容: + +```javascript +// HelloWorld.js + +//僅呼叫一次 +useEffect(async () => { + const message = await loadCurrentMessage() + setMessage(message) +}, []) +``` + +請注意,我們只希望在元件第一次渲染時呼叫一次 `loadCurrentMessage`。 我們很快就會實作 `addSmartContractListener`,以便在智慧型合約中的訊息變更後自動更新 UI。 + +在我們深入研究我們的監聽器之前,讓我們來看看我們目前為止的成果! 儲存您的 `HelloWorld.js` 和 `interact.js` 檔案,然後前往 [http://localhost:3000/](http://localhost:3000/) + +您會注意到目前訊息不再顯示「未連線至網路」。 而是反映儲存在智慧型合約中的訊息。 太棒了! + +#### 您的 UI 現在應該反映儲存在智慧型合約中的訊息 {#your-UI-should-now-reflect-the-message-stored-in-the-smart-contract} + +現在說到那個監聽器... + +#### 實作 `addSmartContractListener` {#implement-addsmartcontractlistener} + +如果您回想一下我們在本教學系列[第一部分](https://docs.alchemy.com/alchemy/tutorials/hello-world-smart-contract#step-10-write-our-contract)中編寫的 `HelloWorld.sol` 檔案,您會記得在我們的智慧型合約的 `update` 函式被調用後 (見第 9 和 27 行),會發出一個名為 `UpdatedMessages` 的智慧型合約事件: + +```javascript +// HelloWorld.sol + +// 指定 Solidity 的版本,使用語意化版本控制。 +// 了解更多:https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma +pragma solidity ^0.7.3; + +// 定義一個名為「HelloWorld」的合約。 +// 合約是函式和資料 (其狀態) 的集合。部署後,合約會存放在以太坊區塊鏈的特定地址上。了解更多:https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html +contract HelloWorld { + + // 呼叫更新函式時發出 + // 智慧型合約事件是您合約的一種方式,可將區塊鏈上發生的事情傳達給您的應用程式前端,前端可以「監聽」某些事件並在事件發生時採取行動。 + event UpdatedMessages(string oldStr, string newStr); + + // 宣告一個「string」類型的狀態變數「message」。 + // 狀態變數是其值永久儲存在合約儲存空間中的變數。關鍵字「public」可讓變數從合約外部存取,並建立一個其他合約或用戶端可呼叫以存取該值的函式。 + string public message; + + // 與許多以類別為基礎的物件導向語言相似,建構函式是一個特殊函式,只在合約建立時執行。 + // 建構函式用於初始化合約的資料。了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors + constructor(string memory initMessage) { + + // 接受一個字串引數「initMessage」,並將該值設定到合約的「message」儲存變數中)。 + message = initMessage; + } + + // 一個公共函式,接受一個字串引數並更新「message」儲存變數。 + function update(string memory newMessage) public { + string memory oldMsg = message; + message = newMessage; + emit UpdatedMessages(oldMsg, newMessage); + } +} +``` + +智慧型合約事件是您合約的一種方式,可將區塊鏈上發生的事情 (即發生了_事件_) 傳達給您的前端應用程式,前端可以「監聽」特定事件並在事件發生時採取行動。 + +`addSmartContractListener` 函式將專門監聽我們的 Hello World 智慧型合約的 `UpdatedMessages` 事件,並更新我們的 UI 以顯示新訊息。 + +將 `addSmartContractListener` 修改為以下內容: + +```javascript +// HelloWorld.js + +function addSmartContractListener() { + helloWorldContract.events.UpdatedMessages({}, (error, data) => { + if (error) { + setStatus("😥 " + error.message) + } else { + setMessage(data.returnValues[1]) + setNewMessage("") + setStatus("🎉 您的訊息已更新!") + } + }) +} +``` + +讓我們來分解一下監聽器偵測到事件時會發生什麼事: + +- 如果事件發出時發生錯誤,它將透過我們的 `status` 狀態變數反映在 UI 中。 +- 否則,我們將使用回傳的 `data` 物件。 `data.returnValues` 是一個從零開始索引的陣列,其中陣列中的第一個元素儲存先前的訊息,第二個元素儲存更新後的訊息。 總而言之,在一個成功的事件上,我們將把我們的 `message` 字串設定為更新後的訊息,清除 `newMessage` 字串,並更新我們的 `status` 狀態變數以反映新訊息已發布在我們的智慧型合約上。 + +最後,讓我們在我們的 `useEffect` 函式中呼叫我們的監聽器,以便它在 `HelloWorld.js` 元件的第一次渲染時被初始化。 總而言之,您的 `useEffect` 函式應如下所示: + +```javascript +// HelloWorld.js + +useEffect(async () => { + const message = await loadCurrentMessage() + setMessage(message) + addSmartContractListener() +}, []) +``` + +既然我們能從我們的智慧型合約中讀取,如果能弄清楚如何寫入就太好了! 然而,若要寫入我們的去中心化應用程式,我們必須先有一個以太坊錢包連線到它。 + +所以,接下來我們將處理設定我們的以太坊錢包 (MetaMask),然後將它連線至我們的去中心化應用程式! + +### 步驟 4:設定您的以太坊錢包 {#step-4-set-up-your-ethereum-wallet} + +若要向以太坊鏈寫入任何內容,使用者必須使用其虛擬錢包的私密金鑰簽署交易。 在本教學中,我們將使用 [MetaMask](https://metamask.io/),這是一款瀏覽器中的虛擬錢包,用於管理您的以太坊帳戶地址,因為它讓最終使用者簽署交易變得非常容易。 + +如果您想深入了解以太坊上的交易如何運作,請參閱以太坊基金會的[此頁面](/developers/docs/transactions/)。 + +#### 下載 MetaMask {#download-metamask} + +您可以在[這裡](https://metamask.io/download)免費下載並建立 MetaMask 帳戶。 在建立帳戶時,或如果您已有帳戶,請確保切換到右上角的「Goerli 測試網」 (這樣我們就不會處理真實貨幣)。 + +#### 從水龍頭新增以太幣 {#add-ether-from-a-faucet} + +若要在以太坊區塊鏈上簽署交易,我們需要一些假的 Eth。 若要取得 Eth,您可以前往 [FaucETH](https://fauceth.komputing.org) 並輸入您的 Goerli 帳戶地址,點擊「請求資金」,然後在下拉式選單中選取「以太坊測試網 Goerli」,最後再次點擊「請求資金」按鈕。 你應該很快便能在你的MetaMask帳戶裡看見ETH! + +#### 檢查您的餘額 {#check-your-balance} + +為了再次確認我們的餘額,讓我們使用 [Alchemy 的 composer 工具](https://composer.alchemyapi.io/?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D) 發出一個 [eth_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance) 請求。 這將會把我們錢包內的以太結餘回傳。 在你輸入自己的MetaMask帳戶地址,並且點下「寄送請求」後,你理應會看見一個這樣子的回應: + +```text +{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"} +``` + +**注意:** 此結果的單位是 wei,不是 eth。 Wei是一個被用來計算以太最少分數的單位。 要把wei換算到ETH的算術是:1 ETH = 10¹⁸ wei。 所以,如果我們要換算 0xde0b6b3a7640000 到小數點,我們會得到 1\*10¹⁸的結果,它相當於一個ETH的數值。 + +哈! 我們的假錢都在這! 🤑 + +### 步驟 5:將 MetaMask 連線至您的 UI {#step-5-connect-metamask-to-your-UI} + +既然我們的 MetaMask 錢包已經設定好了,就讓我們把去中心化應用程式連接到它吧! + +#### `connectWallet` 函式 {#the-connectWallet-function} + +在我們的 `interact.js` 檔案中,讓我們來實作 `connectWallet` 函式,然後我們可以在我們的 `HelloWorld.js` 元件中呼叫它。 + +讓我們將 `connectWallet` 修改為以下內容: + +```javascript +// interact.js + +export const connectWallet = async () => { + if (window.ethereum) { + try { + const addressArray = await window.ethereum.request({ + method: "eth_requestAccounts", + }) + const obj = { + status: "👆🏽 在上方的文字欄位中寫一則訊息。", + address: addressArray[0], + } + return obj + } catch (err) { + return { + address: "", + status: "😥 " + err.message, + } + } + } else { + return { + address: "", + status: ( + +

+ {" "} + 🦊 + 您必須在瀏覽器中安裝 MetaMask,這是一款虛擬以太坊錢包。 + +

+
+ ), + } + } +} +``` + +那麼這段龐大的程式碼究竟做了什麼? + +首先,它會檢查您的瀏覽器中是否啟用了 `window.ethereum`。 + +`window.ethereum` 是由 MetaMask 和其他錢包提供者注入的全域 API,允許網站請求使用者的以太坊帳戶。 如果獲得批准,它可以從使用者連線的區塊鏈讀取資料,並建議使用者簽署訊息和交易。 查看 [MetaMask 文件](https://docs.metamask.io/guide/ethereum-provider.html#table-of-contents)以獲得更多資訊! + +如果 `window.ethereum` _不存在_,那表示 MetaMask 沒有安裝。 這會回傳一個 JSON 物件,其中回傳的 `address` 是一個空字串,而 `status` JSX 物件則傳達使用者必須安裝 MetaMask 的訊息。 + +現在如果 `window.ethereum` _存在_,事情就變得有趣了。 + +使用 try/catch 迴圈,我們將透過呼叫 [`window.ethereum.request({ method: "eth_requestAccounts" });`](https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts) 來嘗試連接 MetaMask。 呼叫這個函式會在瀏覽器中開啟 MetaMask,提示使用者將他們的錢包連接到你的去中心化應用程式。 + +- 如果使用者選擇連線,`method: "eth_requestAccounts"` 將回傳一個陣列,其中包含所有連線至去中心化應用程式的使用者帳戶地址。 總而言之,我們的 `connectWallet` 函式會回傳一個 JSON 物件,其中包含此陣列中的_第一個_ `address` (見第 9 行) 和一則 `status` 訊息,提示使用者向智能合約寫入一則訊息。 +- 如果使用者拒絕連接,那麼 JSON 物件將包含一個空字串作為回傳的 `address`,以及一則反映使用者拒絕連接的 `status` 訊息。 + +既然我們已編寫此 `connectWallet` 函式,下一步就是將它呼叫至我們的 `HelloWorld.js` 元件。 + +#### 將 `connectWallet` 函式新增至您的 `HelloWorld.js` UI 元件 {#add-the-connectWallet-function-to-your-HelloWorld-js-ui-component} + +導覽至 `HelloWorld.js` 中的 `connectWalletPressed` 函式,並將其更新為以下內容: + +```javascript +// HelloWorld.js + +const connectWalletPressed = async () => { + const walletResponse = await connectWallet() + setStatus(walletResponse.status) + setWallet(walletResponse.address) +} +``` + +注意到我們的大部分功能是如何從 `interact.js` 檔案中抽象出來,遠離我們的 `HelloWorld.js` 元件嗎? 這是我們跟M-V-C規範相容的做法! + +在 `connectWalletPressed` 中,我們只需對匯入的 `connectWallet` 函式進行一個 await 呼叫,並利用其回應,透過它們的 state hooks 更新我們的 `status` 和 `walletAddress` 變數。 + +現在,讓我們儲存這兩個檔案 (`HelloWorld.js` 和 `interact.js`),並測試一下我們目前的 UI。 + +在 [http://localhost:3000/](http://localhost:3000/) 頁面上開啟您的瀏覽器,並按下頁面右上角的「連線錢包」按鈕。 + +如果你已安裝 MetaMask,系統應該會提示你將錢包連接到你的去中心化應用程式。 請接受進行連結的邀請。 + +您應該會看到錢包按鈕現在反映您的地址已連線! 太棒了 🔥 + +接下來,試著重新整理頁面... 這很奇怪。 我們的錢包按鈕會鼓勵我們對MetaMask進行連結,就算它已經被連結好了。。。。。。 + +不過,別擔心! 我們可以輕鬆地處理這個問題 (懂嗎?) 透過實作 `getCurrentWalletConnected`,它將檢查是否有地址已連線至我們的去中心化應用程式,並相應地更新我們的 UI! + +#### `getCurrentWalletConnected` 函式 {#the-getcurrentwalletconnected-function} + +將您 `interact.js` 檔案中的 `getCurrentWalletConnected` 函式更新為以下內容: + +```javascript +// interact.js + +export const getCurrentWalletConnected = async () => { + if (window.ethereum) { + try { + const addressArray = await window.ethereum.request({ + method: "eth_accounts", + }) + if (addressArray.length > 0) { + return { + address: addressArray[0], + status: "👆🏽 在上方的文字欄位中寫一則訊息。", + } + } else { + return { + address: "", + status: "🦊 使用右上角按鈕連線至 MetaMask。", + } + } + } catch (err) { + return { + address: "", + status: "😥 " + err.message, + } + } + } else { + return { + address: "", + status: ( + +

+ {" "} + 🦊 + 您必須在瀏覽器中安裝 MetaMask,這是一款虛擬以太坊錢包。 + +

+
+ ), + } + } +} +``` + +這段程式碼與我們剛在上一步中編寫的 `connectWallet` 函式_非常_相似。 + +主要的區別在於,我們不是呼叫 `eth_requestAccounts` 方法 (這會開啟 MetaMask 讓使用者連接他們的錢包),而是在這裡呼叫 `eth_accounts` 方法,它只會回傳一個包含當前連接到我們去中心化應用程式的 MetaMask 位址的陣列。 + +若要查看此函式的實際運作情況,讓我們在我們的 `HelloWorld.js` 元件的 `useEffect` 函式中呼叫它: + +```javascript +// HelloWorld.js + +useEffect(async () => { + const message = await loadCurrentMessage() + setMessage(message) + addSmartContractListener() + + const { address, status } = await getCurrentWalletConnected() + setWallet(address) + setStatus(status) +}, []) +``` + +注意,我們使用對 `getCurrentWalletConnected` 呼叫的回應來更新我們的 `walletAddress` 和 `status` 狀態變數。 + +既然您已新增此程式碼,讓我們試著重新整理我們的瀏覽器視窗。 + +太棒了! 這個按鈕應該會跟你說:「你已經連結好了。」,然後會顯出一個你錢包被連結好的地址的預視 - 就算在你刷新之後也會這樣! + +#### 實作 `addWalletListener` {#implement-addwalletlistener} + +我們去中心化應用程式錢包設定的最後一個步驟是實作錢包監聽器,這樣當我們錢包的狀態改變時 (例如使用者中斷連線或切換帳戶),我們的 UI 就會更新。 + +在您的 `HelloWorld.js` 檔案中,將您的 `addWalletListener` 函式修改為以下內容: + +```javascript +// HelloWorld.js + +function addWalletListener() { + if (window.ethereum) { + window.ethereum.on("accountsChanged", (accounts) => { + if (accounts.length > 0) { + setWallet(accounts[0]) + setStatus("👆🏽 在上方的文字欄位中寫一則訊息。") + } else { + setWallet("") + setStatus("🦊 使用右上角按鈕連線至 MetaMask。") + } + }) + } else { + setStatus( +

+ {" "} + 🦊 + 您必須在瀏覽器中安裝 MetaMask,這是一款虛擬以太坊錢包。 + +

+ ) + } +} +``` + +我敢打賭,此時您甚至不需要我們的幫助就能了解這裡發生了什麼事,但為了周全起見,讓我們快速分解一下: + +- 首先,我們的函式會檢查 `window.ethereum` 是否已啟用 (即 MetaMask 已安裝)。 + - 如果沒有啟用,我們只需將 `status` 狀態變數設定為一個 JSX 字串,提示使用者安裝 MetaMask。 + - 如果已啟用,我們在第 3 行設定監聽器 `window.ethereum.on("accountsChanged")`,它會監聽 MetaMask 錢包的狀態變化,包括使用者將額外帳戶連接到去中心化應用程式、切換帳戶或中斷帳戶連線時。 如果至少有一個帳戶已連接,`walletAddress` 狀態變數會更新為監聽器回傳的 `accounts` 陣列中的第一個帳戶。 否則,`walletAddress` 會被設定為空字串。 + +最後但同樣重要的是,我們必須在我們的 `useEffect` 函式中呼叫它: + +```javascript +// HelloWorld.js + +useEffect(async () => { + const message = await loadCurrentMessage() + setMessage(message) + addSmartContractListener() + + const { address, status } = await getCurrentWalletConnected() + setWallet(address) + setStatus(status) + + addWalletListener() +}, []) +``` + +就是這樣! 我們已成功完成所有錢包功能的編寫! 現在進入我們最後的任務:更新儲存在我們智慧型合約中的訊息! + +### 步驟 6:實作 `updateMessage` 函式 {#step-6-implement-the-updateMessage-function} + +好了,各位,我們已進入最後階段! 在您 `interact.js` 檔案的 `updateMessage` 中,我們將執行以下操作: + +1. 確保我們希望在我們的智慧合約中發布的訊息是有效的 +2. 使用 MetaMask 簽署我們的交易 +3. 從我們的 `HelloWorld.js` 前端元件呼叫此函式 + +這不會花很長時間;讓我們完成這個去中心化應用程式! + +#### 輸入錯誤處理 {#input-error-handling} + +自然地,在函式開始時進行某種輸入錯誤處理是合理的。 + +如果沒有安裝 MetaMask 擴充功能、沒有連線錢包 (即傳入的 `address` 是空字串),或者 `message` 是空字串,我們希望我們的函式能提早回傳。 讓我們將以下錯誤處理新增至 `updateMessage`: + +```javascript +// interact.js + +export const updateMessage = async (address, message) => { + if (!window.ethereum || address === null) { + return { + status: + "💡 連線您的 MetaMask 錢包以更新區塊鏈上的訊息。", + } + } + + if (message.trim() === "") { + return { + status: "❌ 您的訊息不能是空字串。", + } + } +} +``` + +既然它有了適當的輸入錯誤處理,是時候透過 MetaMask 簽署交易了! + +#### 簽署我們的交易 {#signing-our-transaction} + +如果您已經熟悉傳統的 web3 以太坊交易,我們接下來編寫的程式碼將會非常熟悉。 在您的輸入錯誤處理程式碼下方,將以下內容新增至 `updateMessage`: + +```javascript +// interact.js + +//設定交易參數 +const transactionParameters = { + to: contractAddress, // 合約發布期間除外為必要 + from: address, // 必須與使用者目前的地址相符 + data: helloWorldContract.methods.update(message).encodeABI(), +} + +//簽署交易 +try { + const txHash = await window.ethereum.request({ + method: "eth_sendTransaction", + params: [transactionParameters], + }) + return { + status: ( + + ✅{" "} + + 在 Etherscan 上查看您交易的狀態! + +
+ ℹ️ 交易一旦經網路驗證,訊息將自動更新。 +
+ ), + } +} catch (error) { + return { + status: "😥 " + error.message, + } +} +``` + +讓我們來分解一下發生了什麼事。 首先,我們設定我們的交易參數,其中: + +- `to` 指定接收方位址 (我們的智能合約) +- `from` 指定交易的簽署者,即我們傳入函式的 `address` 變數 +- `data` 包含對我們 Hello World 智慧型合約的 `update` 方法的呼叫,並接收我們的 `message` 字串變數作為輸入 + +然後,我們進行一個 await 呼叫,`window.ethereum.request`,我們在此請求 MetaMask 簽署交易。 請注意,在第 11 和 12 行,我們正在指定我們的 eth 方法 `eth_sendTransaction`,並傳入我們的 `transactionParameters`。 + +在這時機,MetaMask將會在瀏覽器中被開啟,然後鼓勵用戶去簽署或拒絕該筆交易。 + +- 如果交易成功,函式將回傳一個 JSON 物件,其中 `status` JSX 字串會提示使用者查看 Etherscan 以取得有關其交易的更多資訊。 +- 如果交易失敗,函式將回傳一個 JSON 物件,其中 `status` 字串會轉達錯誤訊息。 + +總而言之,我們的 `updateMessage` 函式應如下所示: + +```javascript +// interact.js + +export const updateMessage = async (address, message) => { + //輸入錯誤處理 + if (!window.ethereum || address === null) { + return { + status: + "💡 連線您的 MetaMask 錢包以更新區塊鏈上的訊息。", + } + } + + if (message.trim() === "") { + return { + status: "❌ 您的訊息不能是空字串。", + } + } + + //設定交易參數 + const transactionParameters = { + to: contractAddress, // 合約發布期間除外為必要 + from: address, // 必須與使用者目前的地址相符 + data: helloWorldContract.methods.update(message).encodeABI(), + } + + //簽署交易 + try { + const txHash = await window.ethereum.request({ + method: "eth_sendTransaction", + params: [transactionParameters], + }) + return { + status: ( + + ✅{" "} + + 在 Etherscan 上查看您交易的狀態! + +
+ ℹ️ 交易一旦經網路驗證,訊息將自動更新。 +
+ ), + } + } catch (error) { + return { + status: "😥 " + error.message, + } + } +} +``` + +最後但同樣重要的是,我們需要將我們的 `updateMessage` 函式連線至我們的 `HelloWorld.js` 元件。 + +#### 將 `updateMessage` 連線至 `HelloWorld.js` 前端 {#connect-updatemessage-to-the-helloworld-js-frontend} + +我們的 `onUpdatePressed` 函式應對匯入的 `updateMessage` 函式進行 await 呼叫,並修改 `status` 狀態變數以反映我們的交易是成功還是失敗: + +```javascript +// HelloWorld.js + +const onUpdatePressed = async () => { + const { status } = await updateMessage(walletAddress, newMessage) + setStatus(status) +} +``` + +它非常乾淨簡單。 您猜怎麼著... 您的去中心化應用程式完成了!!! + +去測試一下**更新**按鈕吧! + +### 製作您自己的自訂去中心化應用程式 {#make-your-own-custom-dapp} + +哇,您完成了本教學! 總結一下,您學會了如何: + +- 將 MetaMask 錢包連線至您的去中心化應用程式專案 +- 使用 [Alchemy Web3](https://docs.alchemy.com/alchemy/documentation/alchemy-web3) API 從您的智慧型合約讀取資料 +- 使用 MetaMask 簽署以太坊交易 + +現在您已完全具備應用本教學的技能,可以打造您自己的自訂去中心化應用程式專案了! 一如既往,如果您有任何問題,請隨時到 [Alchemy Discord](https://discord.gg/gWuC7zB) 尋求協助。 🧙‍♂️ + +完成本教學後,請在 Twitter 上標記我們 [@alchemyplatform](https://twitter.com/AlchemyPlatform),讓我們知道您的體驗如何,或是否有任何回饋!