在本教程中,你將學習如何生成錢包、查詢餘額、轉移代幣和調用智能合約。

在Coinbase,我們希望可以創建一個開放的金融系統。我們堅信提高金融的自由度可以讓世界更美好。去中心化金融,簡稱DeFi是一個開放,無界限並且可以程序化的金融,是提供金融自由度的一種方式。

智能合約

DeFi是運行在去中心化網絡上(例如以太坊),由智能合約(例如USD幣:一種區塊鏈上美元代幣)驅動的。智能合約其實是很好理解的,Nick Szabo是數字貨幣和加密學的先驅者,在1997年他最早提出智能合約並將其比喻爲自動販賣機。

自動販賣機就如一個被植入自動化程序的合約,他有如下特點:

  1. 你按照顯示的金額放入貨幣,機器會給你飲料;

  2. 你不按照顯示的金額付款,你拿不到飲料;

  3. 如果你付了應付金額,但是機器沒給你飲料,亦或是你在沒付錢的情況下機器給了你飲料,這些都是違反自動販賣機的規則。

自動販賣機可以在無人干涉情況下,很好的履行他的合約精神。

現代智能合約工作原理也是類似的,合約的條件是用可執行的代碼來表達的。去中心化網絡保證按要求執行,並且任何人都不能破壞規則或者篡改結果。因爲網絡會一字不差地執行代碼,有瑕疵的智能合約會產生預想不到的後果。(“代碼是條例”)

把握當下

很多人覺得在區塊鏈上去搭建應用比較困難,認爲只有高級玩家可以嘗試。但是近幾年出現來了很多工具,開發者界面,幫助編程能力一般的人去實現構建。

最近,DeFi生態呈現爆發式地增長。 USDC不到2年捕獲的總價值達到10億美元 ,同時各種各樣的DeFi服務在不到3年的時間,總價值超過20億美金。當下可謂是DeFi發展的最佳時機。

*來源:* [*DeFi Pulse*](https://defipulse.com/)

下面的教程主要目的是介紹如何開發自己的DeFi智能合約。我們希望,本教程可以幫助創建一個全球、開放的金融體系。

開始

本系列教程假設你有使用 JavaScript 的經驗,這是世界上使用最廣泛的編程語言。你還將學習 SolidityEthereum 上使用的智能合約編程語言。最後,你也會認識 USDC ,這是DeFi應用程序中最廣泛採用的由法幣支持的穩定代幣。

設置開發環境

首先,我們需要一個類unix的環境,並在上面安裝 Node.js v12.x (LTS的最新版本)。macOS本身就是Unix環境,Windows用戶可以通過從微軟商店安裝 Ubuntu on WSL 來獲得它。更詳細的步驟macOS可以查看 這裏 ,Windows查看 這裏 。對於文本編輯器,強烈推薦使用 Visual Studio Code ,因爲你將使用的項目模板是預先配置的,但你可以使用任何編輯器。哦,我更喜歡 Vim的快捷鍵綁定方式

建立項目

建立一個Solidity項目需要一些工作,而且老實說,在這個階段我們不希望被搭建項目瑣碎的工作而分心了,所以已經爲你準備了一個 預配置模板

通過在終端中運行以下命令下載和設置模板:

$ git clone [https://github.com/CoinbaseStablecoin/solidity-tutorial.git](https://github.com/CoinbaseStablecoin/solidity-tutorial.git)
$ cd solidity-tutorial
$ npm install -g yarn        # Install yarn package manager
$ yarn                       # Install project dependencies

當yarn在安裝的時候,你可能會看到一些編譯錯誤。你可以忽略這些錯誤。當你最後看到“完成”信息,你就可以開始了。

在Visual Studio Code打開項目

在Visual Studio Code中打開項目文件夾( solidity-tutorial )。項目第一次打開時,Visual Studio Code可能會提示你安裝擴展。繼續並點擊“安裝所有”,這將增加各種有用的擴展,如代碼自動格式化和solidity語法高亮。

在以太坊建立賬戶

在以太坊上做任何事情之前,你需要有一個帳戶。賬戶通常被稱爲“錢包”,因爲它們可以包含像ETH和USDC這樣的數字資產。終端用戶通常使用以太坊錢包應用,像 Coinbase錢包Metamask 來創建錢包,但通過程序使用 ethers.js 方式創建一個賬戶也很簡單。

src 目錄下,創建一個新的js文件 createWallet.js ,寫入如下代碼:

const ethers = require("ethers");

const wallet = ethers.Wallet.createRandom();

console.log(`Mnemonic: ${wallet.mnemonic.phrase}`);
console.log(`Address: ${wallet.address}`);

保存文件,然後使用Node.js來執行文件

$ node src/createWallet.js
Mnemonic: caveat expect rebel donate vault space gentle visa all garage during culture
Address: 0x742B802F28622E1fdc47Df948D61303b4BA52114

剛纔發生了什麼?好吧,你得到了一個全新的Ethereum賬號。“mnemonic”是“助記符”或被稱爲的“恢復短語”,是用於帳戶執行操作所需的加密密鑰,地址是帳戶的名稱。記得把它們寫下來。另外,爲了防止你們使用我的助記符,我已經做了輕微的修改,請使用你自己的!

可以把這些看作是密碼和銀行賬戶的帳號,不過錢包地址可以在幾秒鐘內創建一個,而且你不需要填寫申請表格或分享任何個人信息。而且你可以在任何地方運行此代碼。

*:warning:*助記符必須保密。如果你丟失了它,你將永遠無法訪問你的帳戶和帳戶中存儲的任何資產,沒有人能夠幫助你!把它放在安全的地方!

*:information_source:*從技術上講,你並沒有真正“創造”一個帳戶本身。相反,你創建的是一個私有/公共密鑰對。如果你好奇到底發生了什麼,可以看下 橢圓曲線密碼學 ,比特幣和以太坊規範 BIP39 , BIP32 , EIP55 及其 在本項目中 的實現。

關於Gas和挖礦

以太坊是一個去中心化的網絡,由世界各地成千上萬臺計算機組成,但是它們並不是免費運行的。要在區塊鏈上執行變更狀態,如存儲和更新數據,你必須用用ETH向網絡支付交易費,在以太坊上也稱爲“gas”。gas費用和增加新區塊獲得的獎金就是激勵礦工運算的激勵。這個過程被稱爲“挖礦”,不斷做運算的被稱爲“挖礦者”。我們將在稍後的教程中再次討論這個問題(gas,gas價格和gas限額)。

獲得測試網絡ETH

現在你有了賬戶,你應該存一些ETH。在開發的時候我們不想浪費真正的ETH,所以我們需要一些ETH用於在測試網絡開發和測試網絡(“testnet”)。現在有許多不同的Ethereum測試網絡,我們將會使用Ropsten,因爲獲得測試代幣比較容易。首先,讓我們使用 Etherscan 檢查當前餘額,這是一個以太坊的區塊信息的瀏覽器。你可以在瀏覽器中輸入以下URL,將 你的地址 替換爲之前創建的地址,以 0x 開始。

https://ropsten.etherscan.io/address/ YOUR_ADDRESS

*來源:* [*ropsten.etherscan.io*](https://ropsten.etherscan.io/)

你可以看到現在餘額是0。保持該頁面打開,並在另一個頁面中打開 Ropsten Ethereum Faucet 。在第二個頁面中,輸入你的地址,然後點擊“發送我(Send me)”按鈕。完成後可能只需要幾秒鐘到一兩分鐘。稍後再次檢查Etherscan,你應該會看到新的餘額爲1ETH和轉入交易。

*來源:* [*faucet.ropsten.be*](https://faucet.ropsten.be/)

通過編程獲取ETH餘額

連接以太坊網絡

我們可以使用Etherscan查看餘額,但是使用代碼也可以很容易查看餘額。在我們寫代碼之前,我們需要連接到以太坊網絡。有許多方法可以實現,包括在自己的計算機上運行一個網絡節點,但到目前爲止,最快和最簡單的方法是通過一個託管節點來實現,例如 INFURAAlchemy 。前往 INFURA ,創建一個免費帳戶並創建一個新項目來獲取API密鑰(項目ID)。

:information_source: Go Ethereum (“geth”)Open Ethereum (之前被稱爲Parity Ethereum)。這兩個是最爲廣泛使用地節點軟件。

通過代碼查看ETH餘額

首先,通過讀取助記符進入到我們的賬戶中。在 src 文件夾下,創建一個名爲 wallet.js 的JavaScript文件。敲入以下代碼:

const ethers = require("ethers");

// 在這裏替換你自己的助記符

const mnemonic =
  "rabbit enforce proof always embrace tennis version reward scout shock license wing";
const wallet = ethers.Wallet.fromMnemonic(mnemonic);

console.log(`Mnemonic: ${wallet.mnemonic.phrase}`);
console.log(`Address: ${wallet.address}`);

module.exports = wallet;

用你自己的字符串替換代碼中的助記符字符串。請注意,在生產中,助記符不應該像這樣直接寫在代碼中。理想的是它從配置文件或環境變量中讀取,這樣它就不會因爲寫在源代碼中而泄漏。

執行代碼,你應該能夠看到和之前相同的地址

$ node src/wallet.js
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a

接下來,在同一個文件夾中,創建一個名爲 provider.js 的新文件。在這個文件中,我們將使用前面獲得的INFURA API密鑰。記得替換成你自己的api key:

const ethers = require("ethers");


const provider = ethers.getDefaultProvider("ropsten", {
  // 替換INFURA API KEY
  infura: "0123456789abcdef0123456789abcdef",
});

module.exports = provider;

最後,我們會引用 wallet.jsprovider.js ,在同一目錄下創建新的文件 getBalance.js

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main() {
  const account = wallet.connect(provider);
  const balance = await account.getBalance();
  console.log(`ETH Balance: ${ethers.utils.formatUnits(balance, 18)}`);
}

main();

執行代碼,你就可以看到餘額了

$ node src/getBalance.js
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
ETH Balance: 1.0

代幣換算

我們剛剛創建的代碼非常容易理解,但是你會想知道**ethers.utils.formatUnits(balance, 18)**的作用。嗯,ETH實際上有18位,最小的單位叫“wei”(發音爲“way”)。換句話說,一個ETH等於1000,000,000,000,000,000,000 wei。另一個常見的單位是Gwei(發音爲“Giga-way”),也就是1,000,000,000 wei。 getBalance 方法是以wei中返回了結果,因此我們必須通過將結果除以10的18次方將其轉換回ETH。你可以在 這裏 找到全部的單位名稱。

:information_source: 你也可以使用 ethers.utils.formatEther(balance) , 相當於**ethers.utils.formatUnits(balance, 18)**的簡寫.

獲得測試網絡的USDC

你賬戶裏的只有ETH,略顯孤單,所以我們打算增加一些USDC。我已經在Ropsten testnet上部署了一個 僞USDC智能合約 。雖然我們沒有專門獲得免費USDC的網站,但是在合約中已經包含了該功能,當你調用它時,它會給你一些免費的testnet USDC。你可以在Etherscan中的 合約代碼欄目 找到合約,並在合約源代碼中搜索 gimmeSome 。我們將調用這個函數來將一些USDC發送到我們的帳戶。

發起交易來調用智能合約

在以太坊的智能合約中有主要有兩類方法:讀寫和只讀。第一種方式可以修改區塊鏈上的數據,而第二種僅僅是讀取區塊鏈上的數據,但是不能修改數據。 只讀方法不用通過交易來調用,所以不會耗費ETH,除非是在讀寫方法中的一部分。讀寫方法是一定要通過交易來調用,所以一定會消耗ETH。調用 gimmeSome 方法會改變USDC數量的改變,所以必須通過一次交易來完成。

調用智能合約的方法需要再多些步驟,但是也不復雜。第一,需要知道調用方法的完整接口,被稱爲函數簽名或函數原型。我們看下 gimmeSome 方法的源碼如下:

function gimmeSome() external

這是一個沒有任何參數的方法,而且被標記爲 external ,表示只能從外部可以調用,不能被合約內的其他方法調用。這個對我們來說不影響,因爲我們就是從外部調用。

在主鏈上的 真實的USDC合約 是沒有 gimmeSome 方法的

src 文件夾下創建一個新文件,命名爲 getTestnetUSDC.js ,然後輸入以下代碼

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main() {
  const account = wallet.connect(provider);

  const usdc = new ethers.Contract(
    "0x68ec573C119826db2eaEA1Efbfc2970cDaC869c4",
    ["function gimmeSome() external"],
    account
  );

  const tx = await usdc.gimmeSome({ gasPrice: 20e9 });
  console.log(`Transaction hash: ${tx.hash}`);

  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
  console.log(`Gas used: ${receipt.gasUsed.toString()}`);
}

main();

代碼開始部分, 使用我們感興趣的 gimmeSome 的接口和測試網絡的地址USDC合約 0x68ec⋯69c4 地址實例化了一個合約對象( new ethers.Contract )。 這個方法是不需要任何參數,但是你可以在最後加入一個參數。這次我20 Gwei的gas費,來加快交易打包速度。與網絡交互的所有方法在本質上是異步的,返回一個 Promise ,所以我們使用JavaScript的 await 。完成後會返回交易的hash值,這是用於查看交易的惟一標識符。

運行該代碼,你將看到如下內容:

$ node src/getTestnetUSDC.js
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Transaction hash: 0xd8b4b06c19f5d1393f29b408fc0065d0774ec3b4d11d41be9fd72a8d84cb6208
Transaction confirmed in block 8156350
Gas used: 35121

好的,祝賀你通過代碼的方式完成了第一次ETH的交易。在 Ropsten Etherscan 查看下你的賬戶地址和交易hash。你應該可以查看到,賬戶裏有10個測試USDC,ETH的餘額小於1,因爲支付了gas費用。

*:information_source:*如果你在看Etherscan交易,你會發現這是一筆發送0個ETH連同4個字節的數據到合約地址。如果調用方法時有參數,就會有超過4字節的數據。如果你想了解該數據是如何編碼的,請閱讀 Ethereum合約ABI規範

Gas,Gas費用 和 Gas限制

之前我提到過,我們給這筆交易20Gwei的gas價格來加快交易速度,程序也顯示了使用的gas的量。這一切意味着什麼?嗯,以太坊是由網絡運營商組成的網絡。可以把它想象成一臺世界計算機。這不是一臺免費的電腦,你在這臺電腦上運行的每條指令都要花錢。這臺電腦也被全世界的人共享,這意味着每個人都必須互相競爭,以獲得他們使用這臺電腦的時間。

我們怎樣才能做到公平呢?嗯,我們可以把這臺電腦上的時間進行拍賣,你願意出的價越高,你執行的效率也更快。這當然不是十全十美的,因爲可能會導致只有有很多ETH的人才有特權使用這個電腦。然而,在系統變得更可擴展並能夠容納更多交易之前,這是我們可以選擇的一個可行解決方案。

回到區塊鏈術語上來, “gas used”是在完成交易所消耗的計算資源的數量,“gas price”是你願意爲每一單位gas支付的價格。一般來說,你願意支付的金額越高,你的交易優先級就越高,通過網絡確認的速度也就越快。上面我們使用20 Gwei作爲gas價格,所使用的gas爲35,121(可以在Etherscan中查看交易),所以總共使用gas費用爲35,121 * 20 Gwei = 702,420 Gwei或0.00070242 ETH。

因爲gas需要消耗金錢,你可能想要設定你願意花費的最多gas。幸運的是,你可以通過“gas limit”設置。如果交易最終需要的gas超過規定的限額,交易就會失敗,而不會繼續執行。需要注意的是如果交易因爲gas限額而失敗,已經花費的gas將不會退還給你。

通過調用智能合約讀取數據

你可以在Etherscan上查看到收到了10個USDC,讓我們通過代碼檢查餘額來確認這一點。

我們修改下 src 文件夾下的 getBalance.js 文件

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main() {
  const account = wallet.connect(provider);

  // 定義合約接口
  const usdc = new ethers.Contract(
    "0x68ec573C119826db2eaEA1Efbfc2970cDaC869c4",
    [
      "function balanceOf(address _owner) public view returns (uint256 balance)",
    ],
    account
  );

  const ethBalance = await account.getBalance();
  console.log(`ETH Balance: ${ethers.utils.formatEther(ethBalance)}`);

  // 調用balanceOf方法
  const usdcBalance = await usdc.balanceOf(account.address);
  console.log(`USDC Balance: ${ethers.utils.formatUnits(usdcBalance, 6)}`);
}

main();

USDC是ERC20代幣,因此它包含 ERC20規範 中定義的所有方法。 balanceOf 就是其中之一,它的接口直接來自規範定義的。 balanceOf 是一個只讀函數,所以它可以免費調用。最後,值得注意的是,USDC使用6位小數精度,而其他許多ERC20代幣使用18位小數。

:information_source: 你可以在 這裏 瞭解更多關於Solidity方法。

執行以下代碼,你就可以看到USDC餘額

$ node src/getBalance.js
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
ETH Balance: 0.9961879
USDC Balance: 10.0

ETH和USDC轉賬

現在我們來看看怎麼可以使用賬戶中的ETH和USDC

使用ETH

src 文件夾下創建 transferETH.js 文件

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main(args) {
  const account = wallet.connect(provider);
  let to, value;

  // 生成第一個參數——接受地址
  try {
    to = ethers.utils.getAddress(args[0]);
  } catch {
    console.error(`Invalid recipient address: ${args[0]}`);
    process.exit(1);
  }

    // 生成第二個參數——數量
  try {
    value = ethers.utils.parseEther(args[1]);
    if (value.isNegative()) {
      throw new Error();
    }
  } catch {
    console.error(`Invalid amount: ${args[1]}`);
    process.exit(1);
  }
  const valueFormatted = ethers.utils.formatEther(value);

  //檢查賬戶有足夠餘額
  const balance = await account.getBalance();
  if (balance.lt(value)) {
    const balanceFormatted = ethers.utils.formatEther(balance);

    console.error(
      `Insufficient balance to send ${valueFormatted} (You have ${balanceFormatted})`
    );
    process.exit(1);
  }

  console.log(`Transferring ${valueFormatted} ETH to ${to}...`);

  // 提交轉賬
  const tx = await account.sendTransaction({ to, value, gasPrice: 20e9 });
  console.log(`Transaction hash: ${tx.hash}`);

  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
}

main(process.argv.slice(2));

這段代碼雖然比前面的代碼長,但實際上只是將之前所學的代碼組合起來。這段代碼中要有兩個命令行參數。第一個是接收者地址,第二個是要發送的金額。然後確保提供的地址是有效的,提供的金額不是負數,並且帳戶有足夠的餘額能夠發送請求的金額。然後,提交交易並等待它被確認。

用之前的 createWallet.js 創建一個新賬戶,然後嘗試向這個地址轉些ETH

$ node src/createWallet.js
Mnemonic: napkin invite special reform cheese hunt refuse ketchup arena bag love caution
Address: 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13$ **node src/transferETH.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 0.1**Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Transferring 0.1 ETH to 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13...
Transaction hash: 0xa9f159fa8a9509ec8f8afa8ebb1131c3952cb3b2526471605fd84e8be408cebf
Transaction confirmed in block 8162896

你可以在 Etherscan 看到結果,我們再來測試驗證邏輯是有效的。

$ node src/transferETH.js foo
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Invalid address: foo$ **node src/transferETH.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 0.1.2**Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Invalid amount: 0.1.2$ **node src/transferETH.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 -0.1**Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Invalid amount: -0.1$ **node src/transferETH.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 100**Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Insufficient balance to send 100.0 (You have 0.89328474)

USDC轉賬

上面很大一部的代碼可以用到這裏,主要的區別是USDC是精確到6位,還有你是使用ERC20 規範中的 transfer 。入參依然是“ to ” 及 “ value ”,然後調用智能合約的 transfer 方法。

在同一文件下創建 transferUSDC.js 文件

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main(args) {
  const account = wallet.connect(provider);

  // 在合約中定義balanceOf和transfer方法
  const usdc = new ethers.Contract(
    "0x68ec573C119826db2eaEA1Efbfc2970cDaC869c4",
    [
      "function balanceOf(address _owner) public view returns (uint256 balance)",
      "function transfer(address _to, uint256 _value) public returns (bool success)",
    ],
    account
  );

  let to, value;

    // 生成第一個參數——接受地址
  try {
    to = ethers.utils.getAddress(args[0]);
  } catch {
    console.error(`Invalid address: ${args[0]}`);
    process.exit(1);
  }

    // 生成第二個參數——數量
  try {
    value = ethers.utils.parseUnits(args[1], 6);
    if (value.isNegative()) {
      throw new Error();
    }
  } catch {
    console.error(`Invalid amount: ${args[1]}`);
    process.exit(1);
  }
  const valueFormatted = ethers.utils.formatUnits(value, 6);

  //檢查賬戶有足夠餘額
  const balance = await usdc.balanceOf(account.address);
  if (balance.lt(value)) {
    const balanceFormatted = ethers.utils.formatUnits(balance, 6);

    console.error(
      `Insufficient balance to send ${valueFormatted} (You have ${balanceFormatted})`
    );
    process.exit(1);
  }

  console.log(`Transferring ${valueFormatted} USDC to ${to}...`);

  // 提交轉賬,調用transfer方法
  const tx = await usdc.transfer(to, value, { gasPrice: 20e9 });
  console.log(`Transaction hash: ${tx.hash}`);

  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
}

main(process.argv.slice(2));

試一試,你應該可以看到以下結果:

$ node src/transferUSDC.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 1
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Transferring 1.0 USDC to 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13...
Transaction hash: 0xc1b2157a83f29d6c04f960bc49e968a0cd2ef884761af7f95cc83880631fe4af
Transaction confirmed in block 8162963

恭喜

在本教程中,你學習瞭如何生成錢包、查詢餘額、轉移代幣和調用智能合約。你可能覺得自己還不太瞭解區塊鏈,不過你已經有足夠的知識,去構建自己加密錢包應用程序。爲了保持簡單,我們一直在編寫命令行腳本,那麼是否可以嘗試構建一個圖形界面的網頁呢?

在本教程系列的下一部分中,我們將從頭開始用solidity編寫智能合約,並學習如何構建自己的硬幣,可與USDC交換。我們還將使用今天學到的技術來與我們構建的合約進行互動。請繼續關注。

在Coinbase,我們希望可以創建一個開放的金融系統。我們堅信提高金融的自由度可以讓世界更美好。去中心化金融,簡稱DeFi是一個開放,無界限並且可以程序化的金融,是提供金融自由度的一種方式。

智能合約

DeFi是運行在去中心化網絡上(例如以太坊),由智能合約(例如USD幣:一種區塊鏈上美元代幣)驅動的。智能合約其實是很好理解的,Nick Szabo是數字貨幣和加密學的先驅者,在1997年他最早提出智能合約並將其比喻爲自動販賣機。

自動販賣機就如一個被植入自動化程序的合約,他有如下特點:

  1. 你按照顯示的金額放入貨幣,機器會給你飲料;

  2. 你不按照顯示的金額付款,你拿不到飲料;

  3. 如果你付了應付金額,但是機器沒給你飲料,亦或是你在沒付錢的情況下機器給了你飲料,這些都是違反自動販賣機的規則。

自動販賣機可以在無人干涉情況下,很好的履行他的合約精神。

現代智能合約工作原理也是類似的,合約的條件是用可執行的代碼來表達的。去中心化網絡保證按要求執行,並且任何人都不能破壞規則或者篡改結果。因爲網絡會一字不差地執行代碼,有瑕疵的智能合約會產生預想不到的後果。(“代碼是條例”)

把握當下

很多人覺得在區塊鏈上去搭建應用比較困難,認爲只有高級玩家可以嘗試。但是近幾年出現來了很多工具,開發者界面,幫助編程能力一般的人去實現構建。

最近,DeFi生態呈現爆發式地增長。 USDC不到2年捕獲的總價值達到10億美元 ,同時各種各樣的DeFi服務在不到3年的時間,總價值超過20億美金。當下可謂是DeFi發展的最佳時機。

*來源:* [*DeFi Pulse*](https://defipulse.com/)

下面的教程主要目的是介紹如何開發自己的DeFi智能合約。我們希望,本教程可以幫助創建一個全球、開放的金融體系。

開始

本系列教程假設你有使用 JavaScript 的經驗,這是世界上使用最廣泛的編程語言。你還將學習 SolidityEthereum 上使用的智能合約編程語言。最後,你也會認識 USDC ,這是DeFi應用程序中最廣泛採用的由法幣支持的穩定代幣。

設置開發環境

首先,我們需要一個類unix的環境,並在上面安裝 Node.js v12.x (LTS的最新版本)。macOS本身就是Unix環境,Windows用戶可以通過從微軟商店安裝 Ubuntu on WSL 來獲得它。更詳細的步驟macOS可以查看 這裏 ,Windows查看 這裏 。對於文本編輯器,強烈推薦使用 Visual Studio Code ,因爲你將使用的項目模板是預先配置的,但你可以使用任何編輯器。哦,我更喜歡 Vim的快捷鍵綁定方式

建立項目

建立一個Solidity項目需要一些工作,而且老實說,在這個階段我們不希望被搭建項目瑣碎的工作而分心了,所以已經爲你準備了一個 預配置模板

通過在終端中運行以下命令下載和設置模板:

$ git clone [https://github.com/CoinbaseStablecoin/solidity-tutorial.git](https://github.com/CoinbaseStablecoin/solidity-tutorial.git)
$ cd solidity-tutorial
$ npm install -g yarn        # Install yarn package manager
$ yarn                       # Install project dependencies

當yarn在安裝的時候,你可能會看到一些編譯錯誤。你可以忽略這些錯誤。當你最後看到“完成”信息,你就可以開始了。

在Visual Studio Code打開項目

在Visual Studio Code中打開項目文件夾( solidity-tutorial )。項目第一次打開時,Visual Studio Code可能會提示你安裝擴展。繼續並點擊“安裝所有”,這將增加各種有用的擴展,如代碼自動格式化和solidity語法高亮。

在以太坊建立賬戶

在以太坊上做任何事情之前,你需要有一個帳戶。賬戶通常被稱爲“錢包”,因爲它們可以包含像ETH和USDC這樣的數字資產。終端用戶通常使用以太坊錢包應用,像 Coinbase錢包Metamask 來創建錢包,但通過程序使用 ethers.js 方式創建一個賬戶也很簡單。

src 目錄下,創建一個新的js文件 createWallet.js ,寫入如下代碼:

const ethers = require("ethers");

const wallet = ethers.Wallet.createRandom();

console.log(`Mnemonic: ${wallet.mnemonic.phrase}`);
console.log(`Address: ${wallet.address}`);

保存文件,然後使用Node.js來執行文件

$ node src/createWallet.js
Mnemonic: caveat expect rebel donate vault space gentle visa all garage during culture
Address: 0x742B802F28622E1fdc47Df948D61303b4BA52114

剛纔發生了什麼?好吧,你得到了一個全新的Ethereum賬號。“mnemonic”是“助記符”或被稱爲的“恢復短語”,是用於帳戶執行操作所需的加密密鑰,地址是帳戶的名稱。記得把它們寫下來。另外,爲了防止你們使用我的助記符,我已經做了輕微的修改,請使用你自己的!

可以把這些看作是密碼和銀行賬戶的帳號,不過錢包地址可以在幾秒鐘內創建一個,而且你不需要填寫申請表格或分享任何個人信息。而且你可以在任何地方運行此代碼。

:warning: 助記符必須保密。如果你丟失了它,你將永遠無法訪問你的帳戶和帳戶中存儲的任何資產,沒有人能夠幫助你!把它放在安全的地方!

:information_source: 從技術上講,你並沒有真正“創造”一個帳戶本身。相反,你創建的是一個私有/公共密鑰對。如果你好奇到底發生了什麼,可以看下 橢圓曲線密碼學 ,比特幣和以太坊規範 BIP39 , BIP32 , EIP55 及其 在本項目中 的實現。

關於Gas和挖礦

以太坊是一個去中心化的網絡,由世界各地成千上萬臺計算機組成,但是它們並不是免費運行的。要在區塊鏈上執行變更狀態,如存儲和更新數據,你必須用用ETH向網絡支付交易費,在以太坊上也稱爲“gas”。gas費用和增加新區塊獲得的獎金就是激勵礦工運算的激勵。這個過程被稱爲“挖礦”,不斷做運算的被稱爲“挖礦者”。我們將在稍後的教程中再次討論這個問題(gas,gas價格和gas限額)。

獲得測試網絡ETH

現在你有了賬戶,你應該存一些ETH。在開發的時候我們不想浪費真正的ETH,所以我們需要一些ETH用於在測試網絡開發和測試網絡(“testnet”)。現在有許多不同的Ethereum測試網絡,我們將會使用Ropsten,因爲獲得測試代幣比較容易。首先,讓我們使用 Etherscan 檢查當前餘額,這是一個以太坊的區塊信息的瀏覽器。你可以在瀏覽器中輸入以下URL,將 你的地址 替換爲之前創建的地址,以 0x 開始。

https://ropsten.etherscan.io/address/ YOUR_ADDRESS

*來源:* [*ropsten.etherscan.io*](https://ropsten.etherscan.io/)

你可以看到現在餘額是0。保持該頁面打開,並在另一個頁面中打開 Ropsten Ethereum Faucet 。在第二個頁面中,輸入你的地址,然後點擊“發送我(Send me)”按鈕。完成後可能只需要幾秒鐘到一兩分鐘。稍後再次檢查Etherscan,你應該會看到新的餘額爲1ETH和轉入交易。

*來源:* [*faucet.ropsten.be*](https://faucet.ropsten.be/)

通過編程獲取ETH餘額

連接以太坊網絡

我們可以使用Etherscan查看餘額,但是使用代碼也可以很容易查看餘額。在我們寫代碼之前,我們需要連接到以太坊網絡。有許多方法可以實現,包括在自己的計算機上運行一個網絡節點,但到目前爲止,最快和最簡單的方法是通過一個託管節點來實現,例如 INFURAAlchemy 。前往 INFURA ,創建一個免費帳戶並創建一個新項目來獲取API密鑰(項目ID)。

:information_source: Go Ethereum (“geth”)Open Ethereum (之前被稱爲Parity Ethereum)。這兩個是最爲廣泛使用地節點軟件。

通過代碼查看ETH餘額

首先,通過讀取助記符進入到我們的賬戶中。在 src 文件夾下,創建一個名爲 wallet.js 的JavaScript文件。敲入以下代碼:

const ethers = require("ethers");

// 在這裏替換你自己的助記符

const mnemonic =
  "rabbit enforce proof always embrace tennis version reward scout shock license wing";
const wallet = ethers.Wallet.fromMnemonic(mnemonic);

console.log(`Mnemonic: ${wallet.mnemonic.phrase}`);
console.log(`Address: ${wallet.address}`);

module.exports = wallet;

用你自己的字符串替換代碼中的助記符字符串。請注意,在生產中,助記符不應該像這樣直接寫在代碼中。理想的是它從配置文件或環境變量中讀取,這樣它就不會因爲寫在源代碼中而泄漏。

執行代碼,你應該能夠看到和之前相同的地址

$ node src/wallet.js
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a

接下來,在同一個文件夾中,創建一個名爲 provider.js 的新文件。在這個文件中,我們將使用前面獲得的INFURA API密鑰。記得替換成你自己的api key:

const ethers = require("ethers");

const provider = ethers.getDefaultProvider("ropsten", {
  // 替換INFURA API KEY
  infura: "0123456789abcdef0123456789abcdef",
});

module.exports = provider;

最後,我們會引用 wallet.jsprovider.js ,在同一目錄下創建新的文件 getBalance.js

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main() {
  const account = wallet.connect(provider);
  const balance = await account.getBalance();
  console.log(`ETH Balance: ${ethers.utils.formatUnits(balance, 18)}`);
}

main();

執行代碼,你就可以看到餘額了

$ node src/getBalance.js
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
ETH Balance: 1.0

代幣換算

我們剛剛創建的代碼非常容易理解,但是你會想知道 ethers.utils.formatUnits(balance, 18) 的作用。嗯,ETH實際上有18位,最小的單位叫“wei”(發音爲“way”)。換句話說,一個ETH等於1000,000,000,000,000,000,000 wei。另一個常見的單位是Gwei(發音爲“Giga-way”),也就是1,000,000,000 wei。 getBalance 方法是以wei中返回了結果,因此我們必須通過將結果除以10的18次方將其轉換回ETH。你可以在 這裏 找到全部的單位名稱。

:information_source: 你也可以使用 ethers.utils.formatEther(balance) , 相當於 ethers.utils.formatUnits(balance, 18) 的簡寫.

獲得測試網絡的USDC

你賬戶裏的只有ETH,略顯孤單,所以我們打算增加一些USDC。我已經在Ropsten testnet上部署了一個 僞USDC智能合約 。雖然我們沒有專門獲得免費USDC的網站,但是在合約中已經包含了該功能,當你調用它時,它會給你一些免費的testnet USDC。你可以在Etherscan中的 合約代碼欄目 找到合約,並在合約源代碼中搜索 gimmeSome 。我們將調用這個函數來將一些USDC發送到我們的帳戶。

發起交易來調用智能合約

在以太坊的智能合約中有主要有兩類方法:讀寫和只讀。第一種方式可以修改區塊鏈上的數據,而第二種僅僅是讀取區塊鏈上的數據,但是不能修改數據。 只讀方法不用通過交易來調用,所以不會耗費ETH,除非是在讀寫方法中的一部分。讀寫方法是一定要通過交易來調用,所以一定會消耗ETH。調用 gimmeSome 方法會改變USDC數量的改變,所以必須通過一次交易來完成。

調用智能合約的方法需要再多些步驟,但是也不復雜。第一,需要知道調用方法的完整接口,被稱爲函數簽名或函數原型。我們看下 gimmeSome 方法的源碼如下:

function gimmeSome() external

這是一個沒有任何參數的方法,而且被標記爲 external ,表示只能從外部可以調用,不能被合約內的其他方法調用。這個對我們來說不影響,因爲我們就是從外部調用。

在主鏈上的 真實的USDC合約 是沒有 gimmeSome 方法的

src 文件夾下創建一個新文件,命名爲 getTestnetUSDC.js ,然後輸入以下代碼

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main() {
  const account = wallet.connect(provider);

  const usdc = new ethers.Contract(
    "0x68ec573C119826db2eaEA1Efbfc2970cDaC869c4",
    ["function gimmeSome() external"],
    account
  );

  const tx = await usdc.gimmeSome({ gasPrice: 20e9 });
  console.log(`Transaction hash: ${tx.hash}`);

  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
  console.log(`Gas used: ${receipt.gasUsed.toString()}`);
}

main();

代碼開始部分, 使用我們感興趣的 gimmeSome 的接口和測試網絡的地址USDC合約 0x68ec⋯69c4 地址實例化了一個合約對象( new ethers.Contract )。 這個方法是不需要任何參數,但是你可以在最後加入一個參數。這次我20 Gwei的gas費,來加快交易打包速度。與網絡交互的所有方法在本質上是異步的,返回一個 Promise ,所以我們使用JavaScript的 await 。完成後會返回交易的hash值,這是用於查看交易的惟一標識符。

運行該代碼,你將看到如下內容:

$ node src/getTestnetUSDC.js
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Transaction hash: 0xd8b4b06c19f5d1393f29b408fc0065d0774ec3b4d11d41be9fd72a8d84cb6208
Transaction confirmed in block 8156350
Gas used: 35121

好的,祝賀你通過代碼的方式完成了第一次ETH的交易。在 Ropsten Etherscan 查看下你的賬戶地址和交易hash。你應該可以查看到,賬戶裏有10個測試USDC,ETH的餘額小於1,因爲支付了gas費用。

:information_source: 如果你在看Etherscan交易,你會發現這是一筆發送0個ETH連同4個字節的數據到合約地址。如果調用方法時有參數,就會有超過4字節的數據。如果你想了解該數據是如何編碼的,請閱讀 Ethereum合約ABI規範

Gas,Gas費用 和 Gas限制

之前我提到過,我們給這筆交易20Gwei的gas價格來加快交易速度,程序也顯示了使用的gas的量。這一切意味着什麼?嗯,以太坊是由網絡運營商組成的網絡。可以把它想象成一臺世界計算機。這不是一臺免費的電腦,你在這臺電腦上運行的每條指令都要花錢。這臺電腦也被全世界的人共享,這意味着每個人都必須互相競爭,以獲得他們使用這臺電腦的時間。

我們怎樣才能做到公平呢?嗯,我們可以把這臺電腦上的時間進行拍賣,你願意出的價越高,你執行的效率也更快。這當然不是十全十美的,因爲可能會導致只有有很多ETH的人才有特權使用這個電腦。然而,在系統變得更可擴展並能夠容納更多交易之前,這是我們可以選擇的一個可行解決方案。

回到區塊鏈術語上來, “gas used”是在完成交易所消耗的計算資源的數量,“gas price”是你願意爲每一單位gas支付的價格。一般來說,你願意支付的金額越高,你的交易優先級就越高,通過網絡確認的速度也就越快。上面我們使用20 Gwei作爲gas價格,所使用的gas爲35,121(可以在Etherscan中查看交易),所以總共使用gas費用爲35,121 * 20 Gwei = 702,420 Gwei或0.00070242 ETH。

因爲gas需要消耗金錢,你可能想要設定你願意花費的最多gas。幸運的是,你可以通過“gas limit”設置。如果交易最終需要的gas超過規定的限額,交易就會失敗,而不會繼續執行。需要注意的是如果交易因爲gas限額而失敗,已經花費的gas將不會退還給你。

通過調用智能合約讀取數據

你可以在Etherscan上查看到收到了10個USDC,讓我們通過代碼檢查餘額來確認這一點。

我們修改下 src 文件夾下的 getBalance.js 文件

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main() {
  const account = wallet.connect(provider);

  // 定義合約接口
  const usdc = new ethers.Contract(
    "0x68ec573C119826db2eaEA1Efbfc2970cDaC869c4",
    [
      "function balanceOf(address _owner) public view returns (uint256 balance)",
    ],
    account
  );

  const ethBalance = await account.getBalance();
  console.log(`ETH Balance: ${ethers.utils.formatEther(ethBalance)}`);

  // 調用balanceOf方法
  const usdcBalance = await usdc.balanceOf(account.address);
  console.log(`USDC Balance: ${ethers.utils.formatUnits(usdcBalance, 6)}`);
}

main();

USDC是ERC20代幣,因此它包含 ERC20規範 中定義的所有方法。 balanceOf 就是其中之一,它的接口直接來自規範定義的。 balanceOf 是一個只讀函數,所以它可以免費調用。最後,值得注意的是,USDC使用6位小數精度,而其他許多ERC20代幣使用18位小數。

:information_source: 你可以在 這裏 瞭解更多關於Solidity方法。

執行以下代碼,你就可以看到USDC餘額

$ node src/getBalance.js
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
ETH Balance: 0.9961879
USDC Balance: 10.0

ETH和USDC轉賬

現在我們來看看怎麼可以使用賬戶中的ETH和USDC

使用ETH

src 文件夾下創建 transferETH.js 文件

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main(args) {
  const account = wallet.connect(provider);
  let to, value;

  // 生成第一個參數——接受地址
  try {
    to = ethers.utils.getAddress(args[0]);
  } catch {
    console.error(`Invalid recipient address: ${args[0]}`);
    process.exit(1);
  }

    // 生成第二個參數——數量
  try {
    value = ethers.utils.parseEther(args[1]);
    if (value.isNegative()) {
      throw new Error();
    }
  } catch {
    console.error(`Invalid amount: ${args[1]}`);
    process.exit(1);
  }
  const valueFormatted = ethers.utils.formatEther(value);

  //檢查賬戶有足夠餘額
  const balance = await account.getBalance();
  if (balance.lt(value)) {
    const balanceFormatted = ethers.utils.formatEther(balance);

    console.error(
      `Insufficient balance to send ${valueFormatted} (You have ${balanceFormatted})`
    );
    process.exit(1);
  }

  console.log(`Transferring ${valueFormatted} ETH to ${to}...`);

  // 提交轉賬
  const tx = await account.sendTransaction({ to, value, gasPrice: 20e9 });
  console.log(`Transaction hash: ${tx.hash}`);

  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
}

main(process.argv.slice(2));

這段代碼雖然比前面的代碼長,但實際上只是將之前所學的代碼組合起來。這段代碼中要有兩個命令行參數。第一個是接收者地址,第二個是要發送的金額。然後確保提供的地址是有效的,提供的金額不是負數,並且帳戶有足夠的餘額能夠發送請求的金額。然後,提交交易並等待它被確認。

用之前的 createWallet.js 創建一個新賬戶,然後嘗試向這個地址轉些ETH

$ node src/createWallet.js
Mnemonic: napkin invite special reform cheese hunt refuse ketchup arena bag love caution
Address: 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13$ **node src/transferETH.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 0.1**Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Transferring 0.1 ETH to 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13...
Transaction hash: 0xa9f159fa8a9509ec8f8afa8ebb1131c3952cb3b2526471605fd84e8be408cebf
Transaction confirmed in block 8162896

你可以在 Etherscan 看到結果,我們再來測試驗證邏輯是有效的。

$ node src/transferETH.js foo
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Invalid address: foo$ **node src/transferETH.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 0.1.2**Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Invalid amount: 0.1.2$ **node src/transferETH.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 -0.1**Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Invalid amount: -0.1$ **node src/transferETH.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 100**Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Insufficient balance to send 100.0 (You have 0.89328474)

USDC轉賬

上面很大一部的代碼可以用到這裏,主要的區別是USDC是精確到6位,還有你是使用ERC20 規範中的 transfer 。入參依然是“ to ” 及 “ value ”,然後調用智能合約的 transfer 方法。

在同一文件下創建 transferUSDC.js 文件

const ethers = require("ethers");
const wallet = require("./wallet");
const provider = require("./provider");

async function main(args) {
  const account = wallet.connect(provider);

  // 在合約中定義balanceOf和transfer方法
  const usdc = new ethers.Contract(
    "0x68ec573C119826db2eaEA1Efbfc2970cDaC869c4",
    [
      "function balanceOf(address _owner) public view returns (uint256 balance)",
      "function transfer(address _to, uint256 _value) public returns (bool success)",
    ],
    account
  );

  let to, value;

    // 生成第一個參數——接受地址
  try {
    to = ethers.utils.getAddress(args[0]);
  } catch {
    console.error(`Invalid address: ${args[0]}`);
    process.exit(1);
  }

    // 生成第二個參數——數量
  try {
    value = ethers.utils.parseUnits(args[1], 6);
    if (value.isNegative()) {
      throw new Error();
    }
  } catch {
    console.error(`Invalid amount: ${args[1]}`);
    process.exit(1);
  }
  const valueFormatted = ethers.utils.formatUnits(value, 6);

  //檢查賬戶有足夠餘額
  const balance = await usdc.balanceOf(account.address);
  if (balance.lt(value)) {
    const balanceFormatted = ethers.utils.formatUnits(balance, 6);

    console.error(
      `Insufficient balance to send ${valueFormatted} (You have ${balanceFormatted})`
    );
    process.exit(1);
  }

  console.log(`Transferring ${valueFormatted} USDC to ${to}...`);

  // 提交轉賬,調用transfer方法
  const tx = await usdc.transfer(to, value, { gasPrice: 20e9 });
  console.log(`Transaction hash: ${tx.hash}`);

  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
}

main(process.argv.slice(2));

試一試,你應該可以看到以下結果:

$ node src/transferUSDC.js 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13 1
Address: 0xB3512cF013F71598F359bd5CA3f53C1F4260956a
Transferring 1.0 USDC to 0xDdAC089Fe56F0a9C70e6a04C74DCE52F86a91e13...
Transaction hash: 0xc1b2157a83f29d6c04f960bc49e968a0cd2ef884761af7f95cc83880631fe4af
Transaction confirmed in block 8162963

恭喜

在本教程中,你學習瞭如何生成錢包、查詢餘額、轉移代幣和調用智能合約。你可能覺得自己還不太瞭解區塊鏈,不過你已經有足夠的知識,去構建自己加密錢包應用程序。爲了保持簡單,我們一直在編寫命令行腳本,那麼是否可以嘗試構建一個圖形界面的網頁呢?

在本教程系列的下一部分中,我們將從頭開始用solidity編寫智能合約,並學習如何構建自己的硬幣,可與USDC交換。我們還將使用今天學到的技術來與我們構建的合約進行互動。請繼續關注。

  • 發表於 49分鐘前
  • 閱讀 ( 30 )
  • 學分 ( 499 )
  • 分類:DeFi
相關文章