# Ryan 第四週作業 Foundry
## Windows系統從零建立Foundry環境
參考資料 : https://learnblockchain.cn/docs/foundry/i18n/zh/getting-started/installation.html
### 1. 下載並安裝Rust
https://www.rust-lang.org/tools/install
#### ==== 如果遇到錯誤訊息 Start ====
假如錯誤訊息如下圖所示,則需要安裝Visual Studio

https://visualstudio.microsoft.com/zh-hant/downloads/
不必安裝全部的東西,所以下載Build Tools即可
進去後往下拉至Visual Studio工具,展開並找到Build Tools for Visual Studio 2022,並點擊旁邊的下載按鈕

只需要安裝 使用C++的桌面開發、.NET桌面建置工具、通用Windows平台建置工具

因預設路徑是放在C槽,建議可以存放於D槽
我在D槽建一個Visual Studio 2022的目錄,但Visual Studio IDE存放的目錄不能有其他檔案,所以又建立一個BuildTools的目錄給他放。

#### = = = = 如果遇到錯誤訊息 End = = = =
安裝選第一個預設即可

### 2. 安裝Foundry套件(cli、anvil、chisel)
參考資料 : https://github.com/foundry-rs/foundry
因檔案也是滿大的,所以也在D槽建立一個Foundry資料夾,並使用git clone進Foundry資料夾
下方為終端機(cmd)操作指令
```
D:
cd Foundry
# git clone到Foundry目錄
git clone https://github.com/foundry-rs/foundry
# clone完 會產生一個foundry目錄
cd foundry
# 安裝install cast + forge
cargo install --path ./cli --profile local --bins --locked --force
# 安裝install anvil
cargo install --path ./anvil --profile local --locked --force
# 安裝install chisel
cargo install --path ./chisel --profile local --locked --force
```
安裝好可以使用下方指令查看版本,是否成功安裝完成
```
forge --version
成功會跳出類似下方訊息
forge 0.2.0 (f7a535d 2023-04-03T03:06:14.931199000Z)
```
到這邊環境全部都建置完成,可以開始建立專案了
## 建立Foundry專案
github : https://github.com/ryan19910912/FoundryDemo
```
## 建立一個名為FoundryDemo的foundry專案
forge init FoundryDemo
## 添加lib
forge install transmissions11/solmate Openzeppelin/openzeppelin-contracts
```
### 1. 建立一個NFTDemo.sol
```solidity=
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "solmate/tokens/ERC721.sol";
import "openzeppelin-contracts/contracts/utils/Strings.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";
contract NFTDemo is ERC721, Ownable {
using Strings for uint256;
uint256 public currentTokenId;
uint256 public constant TOTAL_SUPPLY = 10000; //最大數量
uint256 public constant MINT_PRICE = 10000 wei; //鑄造費用
bool private isBoxOpened = false; //盲盒控制打開與否
string private baseUri; //圖片uri
string private unrevealedURI; //盲盒圖片uri
constructor(
string memory _name,
string memory _symbol,
string memory _baseUri,
string memory _unrevealedURI
) ERC721(_name, _symbol) {
baseUri = _baseUri;
unrevealedURI = _unrevealedURI;
}
//打開盲盒
function openBox() external onlyOwner {
isBoxOpened = true;
}
//關閉盲盒
function closeBox() external onlyOwner {
isBoxOpened = false;
}
//鑄造
function mintTo(address recipient) public payable returns (uint256) {
if (msg.value != MINT_PRICE) {
//如果給的錢不等於鑄造費用
revert("Mint Price Not Paid");
}
uint256 newTokenId = ++currentTokenId;
if (newTokenId > TOTAL_SUPPLY) {
//如果鑄造數量已達最大
revert("Max Supply");
}
_safeMint(recipient, newTokenId);
return newTokenId;
}
//取得base Uri
function _baseURI() internal view returns (string memory) {
return isBoxOpened ? baseUri : unrevealedURI;
}
//取得token Uri
function tokenURI(
uint256 _tokenId
) public view override returns (string memory) {
require(_tokenId <= TOTAL_SUPPLY, "Token ID not exist!!");
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI,Strings.toString(_tokenId),".json")): "";
}
//提款
function withdrawPayments(address payable payee) external onlyOwner {
uint256 balance = address(this).balance;
(bool transferTx, ) = payee.call{value: balance}("");
if (!transferTx) {
revert("Withdraw Transfer Fail");
}
}
}
```
### 2. 建立一個NFTDemoTest.sol用來測試
```solidity=
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/NFTDemo.sol";
contract NFTDemoTest is Test {
using stdStorage for StdStorage;
uint256 cost = 10000 wei;
NFTDemo private nft;
function setUp() public {
//初始化建立一個NFTDemo物件
nft = new NFTDemo(vm.envString("TEST_NFT_NAME"), vm.envString("TEST_NFT_SYMBOL"), vm.envString("TEST_NFT_BASE_URI"), vm.envString("TEST_NFT_UNREVEALED_URI"));
}
//測試mint沒傳遞value
function testFailNoMintPricePaid() public {
nft.mintTo(address(1));
}
//測試mint有傳遞value
function testMintPricePaid() public {
nft.mintTo{value: cost}(address(1));
}
//測試超過最大數量
function testFailMaxSupplyReached() public {
uint256 slot = stdstore
.target(address(nft))
.sig("currentTokenId()")
.find();
bytes32 loc = bytes32(slot);
bytes32 mockedCurrentTokenId = bytes32(abi.encode(10000));
vm.store(address(nft), loc, mockedCurrentTokenId);
nft.mintTo{value: cost}(address(1));
}
//測試mint到零地址
function testFailMintToZeroAddress() public {
nft.mintTo{value: cost}(address(0));
}
//測試mint給owner
function testNewMintOwnerRegistered() public {
nft.mintTo{value: cost}(address(1));
uint256 slotOfNewOwner = stdstore
.target(address(nft))
.sig(nft.ownerOf.selector)
.with_key(1)
.find();
uint160 ownerOfTokenIdOne = uint160(
uint256(
(vm.load(address(nft), bytes32(abi.encode(slotOfNewOwner))))
)
);
assertEq(address(ownerOfTokenIdOne), address(1));
}
//測試增加餘額
function testBalanceIncremented() public {
nft.mintTo{value: cost}(address(1));
uint256 slotBalance = stdstore
.target(address(nft))
.sig(nft.balanceOf.selector)
.with_key(address(1))
.find();
uint256 balanceFirstMint = uint256(
vm.load(address(nft), bytes32(slotBalance))
);
assertEq(balanceFirstMint, 1);
nft.mintTo{value: cost}(address(1));
uint256 balanceSecondMint = uint256(
vm.load(address(nft), bytes32(slotBalance))
);
assertEq(balanceSecondMint, 2);
}
//測試合約安全接收
function testSafeContractReceiver() public {
Receiver receiver = new Receiver();
nft.mintTo{value: cost}(address(receiver));
uint256 slotBalance = stdstore
.target(address(nft))
.sig(nft.balanceOf.selector)
.with_key(address(receiver))
.find();
uint256 balance = uint256(vm.load(address(nft), bytes32(slotBalance)));
assertEq(balance, 1);
}
//測試合約不安全接收
function testFailUnSafeContractReceiver() public {
vm.etch(address(1), bytes("mock code"));
nft.mintTo{value: cost}(address(1));
}
//測試owner提款功能
function testWithdrawalWorksAsOwner() public {
// Mint an NFT, sending eth to the contract
Receiver receiver = new Receiver();
address payable payee = payable(address(0x1337));
uint256 priorPayeeBalance = payee.balance;
nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
// Check that the balance of the contract is correct
assertEq(address(nft).balance, nft.MINT_PRICE());
uint256 nftBalance = address(nft).balance;
// Withdraw the balance and assert it was transferred
nft.withdrawPayments(payee);
assertEq(payee.balance, priorPayeeBalance + nftBalance);
}
//測試非owner提款功能
function testWithdrawalFailsAsNotOwner() public {
// Mint an NFT, sending eth to the contract
Receiver receiver = new Receiver();
nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
// Check that the balance of the contract is correct
assertEq(address(nft).balance, nft.MINT_PRICE());
// Confirm that a non-owner cannot withdraw
vm.expectRevert("Ownable: caller is not the owner");
vm.startPrank(address(0xd3ad));
nft.withdrawPayments(payable(address(0xd3ad)));
vm.stopPrank();
}
}
//安全交易傳輸接收器
contract Receiver is ERC721TokenReceiver {
function onERC721Received(
address operator,
address from,
uint256 id,
bytes calldata data
) external override returns (bytes4) {
return this.onERC721Received.selector;
}
}
```
### 2. 使用指令進行測試
```
forge test --gas-report
```

## 作業基本題
### 1. 連接本地節點
```
//連接本地節點指令
anvil
```

### 2. 在終端機建立環境變數
```
//節點
$Env:RPC_URL="http://127.0.0.1:8545"
//私鑰
$Env:PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
//NFT名稱
$Env:TEST_NFT_NAME="RyanNFTDemo"
//NFT縮寫
$Env:TEST_NFT_SYMBOL="RND"
//NFT盲盒開啟網址
$Env:TEST_NFT_BASE_URI="https://gateway.pinata.cloud/ipfs/QmRLxwSkzJskXcjpq7t4Ve6mmvdkWPVZfWaCp6Nhs14KrU/"
//NFT盲盒關閉網址
$Env:TEST_NFT_UNREVEALED_URI="https://gateway.pinata.cloud/ipfs/QmbCfG8EFsAisCMZGr9unhUktUPyUNS52TcHcMc5b8bwbh/"
```

建立一個目錄deploy,用來存放deploy用的bat檔
在deploy目錄內建立一個deploy_NFT_0.bat
```
@echo off
forge create NFTDemo --rpc-url=%RPC_URL% --private-key=%PRIVATE_KEY% --constructor-args %TEST_NFT_NAME% %TEST_NFT_SYMBOL% %TEST_NFT_BASE_URI% %TEST_NFT_UNREVEALED_URI%;
exit
```

執行deploy_NFT_0.bat
```
.\deploy\deploy_NFT_0.bat
```


### 3. 在本地節點鑄造NFT
指令參考 : https://book.getfoundry.sh/reference/cast/cast-send
建立一個deploy_NFT_mint.bat
```
@echo off
cast send --rpc-url=%RPC_URL% %TEST_NFT_CONTRACT_ADDRESS% "mintTo(address)" %TEST_NFT_SEND_ADDRESS% --value %TEST_NFT_SEND_VALUE% --private-key=%PRIVATE_KEY%
exit
```
把本地節點的NFT合約地址建立在環境變數
```
//合約地址
$Env:TEST_NFT_CONTRACT_ADDRESS="0x5fbdb2315678afecb367f032d93f642f64180aa3"
//鑄造對象地址
$Env:TEST_NFT_SEND_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
//鑄造的金額
$Env:TEST_NFT_SEND_VALUE="10000wei"
```
執行deploy_NFT_mint.bat


### 4. 查看NFT tokenId對應的擁有者地址
建立一個deploy_NFT_select.bat
```
@echo off
cast call --rpc-url=%RPC_URL% %TEST_NFT_CONTRACT_ADDRESS% "ownerOf(uint256)" %TEST_NFT_TOKEN_ID% --private-key=%PRIVATE_KEY%
exit
```
把要查找的Token ID建立在環境變數
```
//查找的Token ID
$Env:TEST_NFT_TOKEN_ID="3"
```
執行deploy_NFT_select.bat

## 作業進階題
### 1. 建立測試鏈的環境變數
```
//goerli測試鏈 rpc url
$Env:RPC_URL="https://rpc.ankr.com/eth_goerli"
//小狐狸錢包的private key
$Env:PRIVATE_KEY=""
//NFT名稱
$Env:TEST_NFT_NAME="RyanNFTDemo"
//NFT縮寫
$Env:TEST_NFT_SYMBOL="RND"
//NFT盲盒開啟時的URL
$Env:TEST_NFT_BASE_URI="https://gateway.pinata.cloud/ipfs/QmRLxwSkzJskXcjpq7t4Ve6mmvdkWPVZfWaCp6Nhs14KrU/"
//NFT盲盒關閉時的URL
$Env:TEST_NFT_UNREVEALED_URI="https://gateway.pinata.cloud/ipfs/QmbCfG8EFsAisCMZGr9unhUktUPyUNS52TcHcMc5b8bwbh/"
```
==== 取得小狐狸錢包的Private Key步驟 ====


### 2. 在測試鏈發佈合約
執行deploy_NFT_0.bay



把合約地址建立在變數環境
```
//合約地址
$Env:TEST_NFT_CONTRACT_ADDRESS="0x28fC7124652602ee91afb57c83f175Fde0b2F50F"
//鑄造對象地址
$Env:TEST_NFT_SEND_ADDRESS="0x7A4D6c296B28460cda81Fb584234A15Fc105e182"
//鑄造金額
$Env:TEST_NFT_SEND_VALUE="10000wei"
```
### 3. 鑄造NFT
執行deploy_NFT_mint.bat


### 4. 查看OpenSea測試網
https://testnets.opensea.io/zh-TW/assets/goerli/0x28fc7124652602ee91afb57c83f175fde0b2f50f/1

### 5. 發現圖片沒出來,進行Debug
先查詢Token Uri是否正確
把要查詢的Token ID放入環境變數
```
//查詢的Token ID
$Env:TEST_NFT_TOKEN_ID="1"
```
執行deploy_NFT_token_uri.bat

token uri 不正確,多個 ; 符號https://gateway.pinata.cloud/ipfs/QmbCfG8EFsAisCMZGr9unhUktUPyUNS52TcHcMc5b8bwbh/;1.json
### 6. 開啟盲盒
建立deploy_NFT_openBox.bat
```
@echo off
cast send --rpc-url=%RPC_URL% %TEST_NFT_CONTRACT_ADDRESS% "openBox()" --private-key=%PRIVATE_KEY%
exit
```
執行deploy_NFT_openBox.bat

查看token uri是否改變
執行deploy_NFT_token_uri.bat

toke uri已成功改變,並正確
https://gateway.pinata.cloud/ipfs/QmRLxwSkzJskXcjpq7t4Ve6mmvdkWPVZfWaCp6Nhs14KrU/1.json
### 7. 查看OpenSea測試網盲盒是否開啟
盲盒成功開啟

###### tags: `Solidity 工程師實戰營第 5 期`