構建去中心化智慧合約程式設計貨幣

買賣虛擬貨幣
[第1部分]  使用Solidity[4] 和 React在以太坊上構建具有社交找回功能的智慧合約錢包前言

我第一次對以太坊感到興奮那會兒是閱讀這10行程式碼的時候:

該程式碼在建立合約時會跟蹤owner,並且只允許“owner”使用require()語句呼叫withdraw() 。

該智慧合約控制自己的資金。它具有地址和餘額,可以傳送和接收資金,甚至可以與其他智慧合約進行互動。

這是一臺永遠線上的公共狀態機,你可以對其程式設計,世界上任何人都可以與它互動!

依賴條件

你需要事先安裝 NodeJS>=10[5], Yarn[6]和 Git[7].

本教程將假定你對Web應用程式開發[8] 有基本的瞭解,並且稍微接觸過以太坊核心概念[9]。你可以隨時在文件中閱讀有關Solidity的更多資訊[10],但是先試試這個吧:

開始

開啟一個終端並克隆  scaffold-eth[11]倉庫。我們構建去中心化應用程式原型所需的一切都包含在這裡:

git clone https://github.com/austintgriffith/scaffold-eth
cd scaffold-eth
git checkout part1-smart-contract-wallet-social-recovery
yarn install

 警告,執行 yarn install  繼續並執行接下來的三個命令時,你可能會收到看起來像錯誤的警告,它可能沒有影響!

注意本教程是如何獲取part1-smart-contract-wallet-social-recovery分支的,  scaffold-eth[12]是一個可fork的以太坊開發技術棧,每個教程都是一個分支,你可以fork和使用!

在你喜歡的編輯器中本地開啟程式碼,然後概覽一下:

你可以在packages/buidler/contracts中找到SmartContract Wallet.sol, 這是我們的智慧合約(後端)。

packages/react-app/src中的 App.js 和 SmartContractWallet.js 是我們的web應用程式(前端).

開啟你的前端:yarn start

警告,如果沒有執行接下來的兩行,你的CPU會抽風:

在第二個終端中啟動由Builder[13]驅動的本地區塊鏈:yarn run chain

在第三個終端中,編譯並部署合約:yarn run deploy

警告,此專案中有幾個名為contracts的目錄。多花一點時間,以確保所處的目錄在packages/buidler/contracts資料夾 。

我們智慧合約中的程式碼被編譯為稱為位元組碼和ABI的“工件”(artifacts)。這個ABI定義了我們如何與合約互動,而bytecode是“機器程式碼”。你可以在packages/buidler/artifacts資料夾中找到這些工件。

為了部署合約,首先需要在交易中傳送位元組碼,然後我們的合約將在本地鏈上的特定地址執行。這些工件會自動注入到我們的前端,以便我們可以與合約進行互動。

在瀏覽器中開啟 http://localhost:3000[14] :

讓我們快速瀏覽一下這個腳手架,為後面的做鋪墊…

提供者(Provider)

使用你的編輯器開啟packages/react-app/src資料夾下的App.js前端檔案。

在App.js中scaffold-eth 有三個不同的 providers[15] :

mainnetProvider : Infura[16]支援只讀的以太坊主網,它用於獲取主網餘額並與現有的執行的合約互動,例如Uniswap的ETH價格或ENS域名查詢。

localProvider : Buidler[17] 是本地鏈,當我們在本地對Solidity進行迭代時,會將你的合約部署到這裡。該provider的第一個帳戶提供本地的水龍頭。

injectedProvider : 程式會先啟動burner provider[18](頁面載入後的即時帳戶),但隨後你可以點選connect以引入由 Web3Modal[19]支援的更安全的錢包。該provider會對傳送到我們的本地和主網的交易進行簽名。

區塊鏈是一個節點網路,每一節點都擁有當前狀態。如果我們想訪問以太坊網路,我們可以執行自己的節點,但我們不希望使用者僅因為使用我們的應用程式就必須同步整條鏈;因此,我們將使用簡單的Web請求與基礎設施的provider進行互動。

鉤子函式(Hooks)

我們還將利用scaffold-eth中的一堆美味鉤子[20]比如userBalance()來追蹤地址的餘額或useContractReader()使我們的狀態與合約保持同步。在此處[21]閱讀更多有關React鉤子的資訊。

元件(Components)

這個腳手架還包含許多用於構建Dapp的方便元件[22]。我們很快就會看到的<AddressInput />就是一個很好的例子。在此處[23]閱讀有關React元件的更多資訊。

函式(Functions)

我們在packages/buidler/contracts中的SmartContractWallet.sol中建立一個 isOwner()的函式. 這個函式可以查詢錢包是否是某個地址的所有者:

function isOwner(address possibleOwner) public view returns (bool) {
  return (possibleOwner==owner);
}

注意該函式為什麼被標記為view?函式可以寫入狀態或讀取狀態。當我們需要寫入狀態時,我們必須支付gas才能將交易傳送給合約,但是讀狀態既簡單又便宜,因為我們可以向任何provider詢問狀態。

要在智慧合約上呼叫函式,你需要將交易傳送到合約的地址。

我們再建立一個名為updateOwner()可修改狀態的函式,該函式使當前所有者可以設定新的所有者:

function updateOwner(address newOwner) public {
  require(isOwner(msg.sender),"NOT THE OWNER!");
  owner = newOwner;
}

我們在這裡使用了msg.sender和msg.value,msg.sender是傳送交易的地址,msg.value是隨交易傳送的以太幣數量。你可以在此處詳細瞭解單位和全域性變數[24]。

注意require()語句如何確保msg.sender是當前的所有者。如果條件不滿足,它將revert(),並且整個交易都被撤消。

以太坊交易是原子的:要麼一切正常,要麼一切撤銷。如果我們將一個代幣傳送給Alice,並且在同一合約呼叫中,我們未能從Bob那裡獲取一個代幣,則整個交易將被撤消。

儲存,編譯和部署合約:yarn run deploy

合約執行後,我們可以看到你的地址不是所有者:

讓我們在部署智慧合約時將我們的帳戶地址傳遞給智慧合約,以便我們成為所有者。首先,從右上角複製你的帳戶(這個圖中的操作後面還會用到,記為TODO LIST):

然後,在packages/builder/contracts中編輯檔案SmartContract Wallet.args,並將地址更改為你的地址。然後,重新部署:yarn run deploy

我們正在使用一個自動化指令碼,該指令碼試圖找到我們的合約並進行部署。最終,我們將需要一個更具定製性的解決方案,但是你可以瀏覽packages/buidler目錄中的scripts/deploy.js。

你的地址現在應該是合約的所有者:

你需要一些測試ether支付與合約互動所需的gas:

仿照“TODO LIST”圖中的操作,並向我們的帳戶傳送一些測試ETH。從右上方複製你的地址,然後將其貼上到左下方的水龍頭中(然後單擊傳送)。你可以為你的地址提供所有想要的測試ether。

然後,嘗試使用“Deposit”按鈕將一些資金存入你的智慧合約中:

該操作將失敗,因為向我們的智慧合約傳遞價值的交易將被撤銷,因為我們尚未新增“fallback”函式。

讓我們在SmartContractWallet.sol中新增一個payable fallback()函式,使其可以接受交易。在packages/buidler中編輯你的智慧合約並新增:

fallback() external payable {    
  console.log(msg.sender,"just deposited",msg.value);  
}

每當有人與我們的合約進行互動而未指定要呼叫的函式名稱時,都會自動呼叫“fallback”函式。例如,如果他們將ETH直接傳送到合約地址。

編譯並重新部署你的智慧合約:yarn run deploy

現在,當你存入資金時,合約應該執行成功!

但這是“可程式設計的貨幣”,讓我們新增一些程式碼以將總ETH的數量限制為0.005(按今天的價格為1.00美元),以確保沒有人在我們的未經審計的合約中投入100萬美元。替換 你的 fallback() 為:

uint constant public limit = 0.005 * 10**18;
fallback() external payable {
  require(((address(this)).balance) <= limit, "WALLET LIMIT REACHED");
  console.log(msg.sender,"just deposited",msg.value);
}

譯者注:在 Solidity 0.6之後的版本中,可以使用接收函式[25]

注意我們為何乘以10¹⁸?Solidity不支援浮點數,只支援整數。1 ETH等於10¹⁸wei。此外,如果你傳送的交易值為1,則是1 wei,wei是以太坊中允許的最小單位。在撰寫本文時,1 ETH的價格是:

現在重新部署並嘗試多次depositing,呼叫次數達到上限後,會報錯:

請注意,在智慧合約中,前端如何透過require()語句第二個引數的訊息獲得有價值的反饋。使用它來以及在yarn run chain終端中顯示的console.log幫助你除錯智慧合約:

你可以調整錢包限額,或者只需要重新部署新合約即可重置所有內容:yarn run deploy

儲存和計算

假設我們要跟蹤允許與我們的合約互動的朋友的地址。我們可以保留一個whilelist []陣列[26],但隨後我們將擁有遍歷陣列比較值以檢視給定地址是否在白名單中。我們還可以使用mapping[27]來追蹤,但是我們將無法迭代他們。我們必須抉擇使用哪種資料更好。

在鏈上儲存資料相對昂貴。每個世界各地的礦工都需要執行和儲存每個狀態更改。注意不要有昂貴的迴圈或過多的計算。值得探索一些示例[28]和閱讀有關EVM的更多資訊[29]。

這就是為什麼這個東西如此具有彈性/抗審查性的原因。數千個(受激勵的)第三方都在執行相同的程式碼,並且在沒有中央授權的情況下就它們儲存的狀態達成一致。它永不停止!

回到智慧合約中,讓我們使用mapping[30]儲存餘額。我們無法遍歷合約中的所有朋友,但是它允許我們快速讀取和寫入任何給定地址的bool訪問許可權。將此程式碼新增到你的合約中:

mapping(address => bool) public friends;

注意我們為什麼將這個friends對映標記為public?這是一個公鏈,所以你應該假設一切都是公共的。

警告:即使我們將此對映設定為 private ,也僅表示外部合約無法讀取它,任何人仍然可以鏈下讀取私有變數 :

建立一個函式 updateFriend()並設定它的 true 或 false引數:

function updateFriend(address friendAddress, bool isFriend) public {
  require(isOwner(msg.sender),"NOT THE OWNER!");
  friends[friendAddress] = isFriend;
  console.log(friendAddress,"friend bool set to",isFriend);
}

注意我們一定要複用 msg.sender 為owner的這行程式碼嗎?你可以使用 修改器Modifier[31]進行清理。然後,每當你需要一個只能由所有者執行的函式時,可以在函式中新增 onlyOwner modifier ,而不是此行。完全可選).

現在,我們部署它並回到前端:yarn run deploy

我們可以同時對前端合約和智慧合約進行小的增量更改。這個緊密的開發迴圈使我們能夠快速迭代並測試新的想法或機制。

我們將要在packages/react-app/src目錄中的SmartContractWallet.js中的display中新增一個表單。首先,讓我們新增一個狀態變數:

const [ friendAddress, setFriendAddress ] = useState("")

然後,讓我們建立一個變數,該變數 建立一個函式,該函式呼叫updateFriend():

const updateFriend = (isFriend)=>{
  return ()=>{
    tx(writeContracts['SmartContractWallet'].updateFriend(friendAddress, isFriend))
    setFriendAddress("")
  }
}

注意在我們在合約上呼叫函式的程式碼結構:contract. functionname(args)全部包裹在tx()中,因此我們可以跟蹤交易進度。你還可以等待此tx()函式以獲取生成的雜湊,狀態等。

當你寫入地址公共所有者地址時,它會自動為此變數建立一個“getter”函式,我們可以透過useContractReader()鉤子輕鬆地獲取它。

接下來,讓我們建立一個ownerDisplay部分,該部分僅針對owner顯示。這將顯示一個帶有兩個按鈕的AddressInput(地址輸入元件),分別用於updateFriend(false)和updateFriend(true)。

let ownerDisplay = []
if(props.address==owner){
  ownerDisplay.push(
    <Row align="middle" gutter={4}>
      <Col span={8} style={{textAlign:"right",opacity:0.333,paddingRight:6,fontSize:24}}>Friend:</Col>
      <Col span={10}>
        <AddressInput
          value={friendAddress}
          ensProvider={props.ensProvider}
          onChange={(address)=>{setFriendAddress(address)}}
        />
      </Col>
      <Col span={6}>
        <Button style={{marginLeft:4}} onClick={updateFriend(true)} shape="circle" icon={<CheckCircleOutlined />} />
        <Button style={{marginLeft:4}} onClick={updateFriend(false)} shape="circle" icon={<CloseCircleOutlined />} />
      </Col>
    </Row>
  )
}

最後,將{ownerDisplay}新增到所有者行下的display中:

在你的應用程式重新熱載入後,嘗試點選一下。(你可以在新的瀏覽器或隱身模式下導航到http://localhost:3000[32]以獲取獲取新的會話帳戶以複製新地址。)

如果不進行地址迭代,很難知道在發生什麼,也很難列出我們所有的朋友以及他們在前端的狀態。

這是事件events的工作.

事件(Events)

事件幾乎就像是一種儲存形式。它們在執行過程中從智慧合約中發出的成本相對較低,但是智慧合約卻不能讀取事件。

讓我們回到智慧合約 SmartContractWallet.sol.

在updateFriend()函式上方或下方建立一個事件:

event UpdateFriend(address sender, address friend, bool isFriend);

然後,在updateFriend()函式中,新增此emit:

emit UpdateFriend(msg.sender,friendAddress,isFriend);

編譯並部署更改:yarn run deploy

然後,在前端,我們可以新增事件監聽器鉤子。將此程式碼與我們的其他鉤子一起新增到SmartContractWallet.js:

const friendUpdates = useEventListener(readContracts,contractName,"UpdateFriend",props.localProvider,1);
(因為需要用在TODO List,上面這一行程式碼裡之前已經寫好了。)

在我們的渲染中,在之後新增一個顯示:

<List
  style={{ width: 550, marginTop: 25}}
  header={<div><b>Friend Updates</b></div>}
  bordered
  dataSource={friendUpdates}
  renderItem={item => (
    <List.Item style={{ fontSize:22 }}>
      <Address 
        ensProvider={props.ensProvider} 
        value={item.friend}
      /> {item.isFriend?"✔":"✘"}
    </List.Item>
  )}
/>

 現在,當它重新載入時,我們應該能夠新增和刪除朋友!

社交找回(Social Recovery)

現在我們在合約中設定了“朋友”,讓我們建立一個可以觸發的“恢復模式”.

讓我們想象一下,我們以某種方式丟失了“所有者”的私有金鑰[33],現在我們被鎖定在智慧合約錢包之外了 。我們需要讓我們的一個朋友觸發某種恢復。

我們還需要確保,如果某個朋友意外(或惡意)觸發了恢復並且我們仍然可以訪問所有者帳戶,我們可以在幾秒鐘內的timeDelay內取消恢復。

首先,我們在SmartContractWallet.sol中設定一些變數 :

uint public timeToRecover = 0;
uint constant public timeDelay = 120; //seconds
address public recoveryAddress;

然後賦予所有者設定recoveryAddress的函式:

function setRecoveryAddress(address _recoveryAddress) public {
  require(isOwner(msg.sender),"NOT THE OWNER!");
  console.log(msg.sender,"set the recoveryAddress to",recoveryAddress);
  recoveryAddress = _recoveryAddress;
}

本教程中有很多程式碼需要複製和貼上。請務必花一點時間放慢速度閱讀,以瞭解發生了什麼。

如果你曾經感到困惑和沮喪,請在 Twitter DM[34]上給我留言,我們將看看能否一起解決!Github issues [35]也非常適合反饋!

讓我們為朋友新增一個函式,以幫助我們找回資金:

function friendRecover() public {
  require(friends[msg.sender],"NOT A FRIEND");
  timeToRecover = block.timestamp + timeDelay;
  console.log(msg.sender,"triggered recovery",timeToRecover,recoveryAddress);
}

我們使用block.timestamp,你可以在 special variables here[36]閱讀更多內容.

如果不小心觸發了friendRecover(),我們希望所有者能夠取消恢復:

function cancelRecover() public {
  require(isOwner(msg.sender),"NOT THE OWNER");
  timeToRecover = 0;
  console.log(msg.sender,"canceled recovery");
}

最後,如果我們處於恢復模式並且已經過去了足夠的時間,任何人都可以銷燬我們的合約並將其所有以太幣傳送到recoveryAddress:

function recover() public {
  require(timeToRecover>0 && timeToRecover<block.timestamp,"NOT EXPIRED");
  console.log(msg.sender,"triggered recover");
  selfdestruct(payable(recoveryAddress));
}

[selfdestruct()](https://solidity.readthedocs.io/en/v0.6.8/cheatsheet.html?highlight=selfdestruct#global-variables "selfdestruct( "selfdestruct()")")將從區塊鏈中刪除我們的智慧合約,並將所有資金返還到recoveryAddress.

警告,具有 owner 且可以隨時呼叫 selfdestruct() 的智慧合約實際上並不是“去中心化”的。開發人員應非常注意任何個人或組織都無法控制或審查的機制。

讓我們編譯,部署並回到前端:yarn run deploy

在我們的SmartContractWallet.js和其他鉤子函式中,我們將要跟蹤recoveryAddress:

const [ recoveryAddress, setRecoveryAddress ] = useState("")

這是讓所有者設定recoveryAddress表單的程式碼 :

ownerDisplay.push(
  <Row align="middle" gutter={4}>
    <Col span={8} style={{textAlign:"right",opacity:0.333,paddingRight:6,fontSize:24}}>Recovery:</Col>
    <Col span={10}>
      <AddressInput
        value={recoveryAddress}
        ensProvider={props.ensProvider}
        onChange={(address)=>{
          setRecoveryAddress(address)
        }}
      />
    </Col>
    <Col span={6}>
      <Button style={{marginLeft:4}} onClick={()=>{
        tx(writeContracts['SmartContractWallet'].setRecoveryAddress(recoveryAddress))
        setRecoveryAddress("")
      }} shape="circle" icon={<CheckCircleOutlined />} />
    </Col>
  </Row>
)

然後我們要跟蹤在合約中的currentRecoveryAddress:

const currentRecoveryAddress = 
useContractReader(readContracts,contractName,"recoveryAddress",1777);

我們還要跟蹤timeToRecover和localTimestamp:

const timeToRecover = useContractReader(readContracts,contractName,"timeToRecover",1777);
const localTimestamp = useTimestamp(props.localProvider)

並在恢復按鈕之後使用<Address />顯示恢復地址。另外,我們將為所有者新增一個cancelRecover()按鈕。將此程式碼放在setRecoveryAddress()按鈕之後:

{timeToRecover&&timeToRecover.toNumber()>0 ? (
  <Button style={{marginLeft:4}} onClick={()=>{
    tx( writeContracts['SmartContractWallet'].cancelRecover() )
  }} shape="circle" icon={<CloseCircleOutlined />}/>
):""}
{currentRecoveryAddress && currentRecoveryAddress!="0x0000000000000000000000000000000000000000"?(
  <span style={{marginLeft:8}}>
    <Address
      minimized={true}
      value={currentRecoveryAddress}
      ensProvider={props.ensProvider}
    />
  </span>
):""}

我們在這裡使用ENS[37]將名稱轉換為地址並返回。這類似於傳統的DNS,你可以在其中註冊名稱.

現在,讓我們來跟蹤使用者是否是isFriend:

const isFriend = 
useContractReader(readContracts,contractName,"friends",[props.address],1777);

如果他們是朋友,請給他們顯示一個按鈕,以呼叫friendRecover(),然後在localTimestamp在timeToRecover之後最終呼叫recover()。在if(props.address == owner){檢查所有者的末尾新增這個大的else if:

}else if(isFriend){
  let recoverDisplay = (
    <Button style={{marginLeft:4}} onClick={()=>{
      tx( writeContracts['SmartContractWallet'].friendRecover() )
    }} shape="circle" icon={<SafetyOutlined />}/>
  )
  if(localTimestamp&&timeToRecover.toNumber()>0){
    const secondsLeft = timeToRecover.sub(localTimestamp).toNumber()
    if(secondsLeft>0){
      recoverDisplay = (
        <div>
          {secondsLeft+"s"}
        </div>
      )
    }else{
      recoverDisplay = (
        <Button style={{marginLeft:4}} onClick={()=>{
          tx( writeContracts['SmartContractWallet'].recover() )
        }} shape="circle" icon={<RocketOutlined />}/>
      )
    }
  }
  ownerDisplay = (
    <Row align="middle" gutter={4}>
      <Col span={8} style={{textAlign:"right",opacity:0.333,paddingRight:6,fontSize:24}}>Recovery:</Col>
      <Col span={16}>
        {recoverDisplay}
      </Col>
    </Row>
  )
}

嘗試一下,感受一下該應用程式。玩玩合約,玩玩前端。現在它是你的!

你可以根據需要使用不同的瀏覽器和隱身模式建立儘可能多的帳戶。然後用水龍頭給他們一些ether。

警告,我們正在從本地鏈中獲取時間戳,但是它不會像主網那樣定時出塊。因此,我們將不得不時不時地傳送一些事務以更新時間戳。

執行的Demo請檢視連結( https://img.learnblockchain.cn/2020/07/29/1_1Mqo-87iqGEswsyaT4jI2g.gif ),其中左邊的帳戶擁有錢包,在右邊的帳戶是朋友賬戶,然後最終該朋友可以恢復以太幣:

恭喜你!

我們圍繞智慧合約錢包構建了具有安全限制和社交找回功能的去中心化應用程式!!!

你應該已經有足夠的瞭解,甚至可以克隆  scaffold-eth[38] 來構建出迄今為止最強大的應用!!!

想象這個錢包是否具有某種自治市場,世界上任何人都可以以動態定價買賣資產?

我們甚至可以鑄造收藏品並在curve上出售它們?!

我們甚至可以建立了一個即時錢包以快速傳送和接收資金?!

我們甚至可以構建gas花費很少應用程式以使使用者願意上車!?

我們甚至可以用提交/顯示隨機數建立了一個遊戲?!

我們甚至可以建立一個本地預測市場,只有我們的朋友和朋友的朋友可以參與?!

我們甚至可以部署了$me代幣並構建一個應用程式,持有人可以向你投資下一個應用程式??!

我們可以將這些me代幣流化為用於在scaffold-eth[39]上構建有趣事物的幫助資源!?!

簡直無限可能!!!

本教程還有一個影片:https://www.youtube.com/watch?v=7rq3TPL-tgI[40]

如果你想了解有關Solidity的更多資訊,建議你玩Ethernaut[41],Crypto Zombies[42],然後甚至是RTFM[43]。

前往https://ethereum.org/developers[44]瞭解更多資源.

隨時在Twitter DM[45] 或github倉庫[46]給我留言

原文連結:https://medium.com/@austin_48503/programming-decentralized-money-300bacec3a4f[47]

作者:Austin Thomas Griffith[48]

參考資料
[1]
登鏈翻譯計劃: https://github.com/lbc-team/Pioneer

[2]
Johnathan: https://learnblockchain.cn/people/720

[3]
Tiny熊: https://learnblockchain.cn/people/15

[4]
第1部分]  使用[Solidity: https://learnblockchain.cn/docs/solidity/

[5]
NodeJS>=10: https://nodejs.org/en/download/

[6]
Yarn: https://classic.yarnpkg.com/en/docs/install/

[7]
Git: https://git-scm.com/downloads

[8]
Web應用程式開發: https://reactjs.org/tutorial/tutorial.html

[9]
以太坊核心概念: https://www.youtube.com/watch?v=9LtBDy67Tho&feature=youtu.be&list=PLJz1HruEnenCXH7KW7wBCEBnBLOVkiqIi&t=13

[10]
閱讀有關Solidity的更多資訊: https://solidity.readthedocs.io/en/v0.6.7/introduction-to-smart-contracts.html

[11]
scaffold-eth: https://github.com/austintgriffith/

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;