Chainlink学习
Chainlink预言机
区块链基础
https://github.com/smartcontractkit/full-blockchain-solidity-course-js
1、智能合约是通过去中心化的方式来执行一些列的指令,在这些指令执行中,并不需要中心化或者第三方的中介。 也就是传统的契约或者合同
2、智能合约也是约定多个参与方的一些指令。不同的是,传统的合同是通过笔写在纸上,或者通过word把内容打进去,而智能合约是代码写的,并且嵌入到一个去中心化区块链平台。智能合约就是在这个去中心化区块链平台中被执行,而不是被二个,三个或者多个参与方去执行
3、比特币有智能合约,但是比特币不是图灵完备的,也就是比特币不能执行一个编程语言的所有的指令。
4、比特币开发者的想把比特币作为价值存储,而以太坊开发者除了把以太坊看作价值存储以外,还把它当成一个运行去中心化合约的平台
5、智能合约能够获得发生在区块链上的所有信息,但是它们想要作为日常合同使用的话,他们需要外部的数据,和外部的计算,而这些就是预言机的作用。
6、预言机是任何可以向区块链输入数据或者进行链下计算的设备。然而,如果想要应用是真正去中心化的,我们不能只通过单个预言机或者单个数据提供商获得数据,
或者单个资源去进行外部运算。
7、我们需要一个去中心化的预言机网络,正如去中心化区块链网络一样。
8、你在链上的逻辑是去中心化的,你在链上的数据和计算也需要是去中心化的
9、将链上去中心化逻辑和链下去中心化数据和计算相结合这个东西叫做混合型智能合约。
9、大多数我们现在使用的最流行的协议,都是某种类型的混合型智能合约,或者在某种程度上,与混合型智能合约交互。
10、混合智能合约会用到Chainlink。chainlink是一个组件化,去中心化的预言机网络。Chainlink 不仅可以给智能合约提供外部数据,还可以提供链下计算。
11、Chainlink 可以让混合型智能合约,在链上和链下部分都是去中心化的。它提供丰富的功能,让智能合约可以像日常合同一样使用。Chainlink 可以让我们获得数据,合约自动化执行,随机数。以任何有意义的方式自定义你的智能合约。
12、智能合约这个名称,来统称混合型智能合约和智能合约。提到混合智能合约的时候,我们特指那些有链下组件的智能合约。在混合型智能合约出现以后,很多智能合约平台都对此做了支持。比如说Avalanche,Polygon,Fantom,Harmony,等等。只需要改动一行代码就可以适配其他链,非常简单不需要担心不了解某些链上某些工具,因为他们大部分都可以互通
13、你会听到这两个名称经常被无差别使用,有时我会说智能合约平台,有时我说区块链,它们指的是同一个东西
显然,区块链可能包括了价值存储和智能合约平台,但是你清楚就好
14、Chainlink 对于所有区块链都是中立的,它和以太坊,Avalanche,Polygon,Solana,Terra和很多其他的区块链都有集成。
15、DAPP,去中心化协议,智能合约应用,去中心化应用这些术语,它们指的都是同一个东西。
16、去中心化应用通常是由多个智能合约组成
- web3是:种观点指的是以区块链和智能合约为基础的下一代的网络
- Web1指的是一个有着固定内容的非许可的开源网络
- Web2指的是:一个有着动态内容的许可网络,你所有的逻辑和协议运行在中心化的服务器上,这些中心化服务器控制着你的信息。
17、在 Web3 中,又回到了非许可网络,但是却再一次承载动态内容。相比于中心化服务器,去中心化网络运行着你的逻辑。智能合约可以允许你创建无需审核的合同和逻辑。在 Web3 中,通常伴随着一个概念,即用户拥有他们所使用的协议,也叫拥有者经济。
最简单能够总结智能合约做什么的就是:智能合约创建了信任最小化协议。智能合约创建不可违背的承诺
智能合约的作用:
智能合约是不可更改,去中心化和透明的
智能合约的简单来说就是部署在去中心化区块链上的一个合约或者一组指令,当这个合约或者这组指令被部署以后它就不能被改变了,它会自动执行,每个人都可以看到合约中的条款。更深层次的理解是,这些代码会被去中心化地执行。在智能合约,Web3和区块链中,不能像以前一样更改合同,特别是当智能合约部署在去中心化的区块链上同时结合另一个去中心化的预言机网络获取真实世界的资产和信息
HTTPS
一旦部署,智能合约的内容就无法被改动
将智能合约与Chainlink的VRF相结合获得一个可验证的随机数
区块链的其他优势:
比特币是一种电子黄金或者说是一种价值存储,用户间可以去中心化的方式发送交易
...
智能合约行业应用
DeFi
DAOs
NFTs
第一笔交易
Solidity部分
第一个智能合约
Pragma Solidity+用的版本号放在代码最上方
版本号:
0.8.7最稳
^0.8.7择优
dayu=0.8.7<0.9.0指定范围
;分号结尾
//注释
在代码最上方,可以加入SPDX License Identifier,不加,有些编译器会出现警告,这个会定义license和代码分享规则
MIT是限制最少的License之一,大多数使用
contract 是 Solidity 的关键字,它告诉编译器后面的代码是来定义智能合约的,可以把它当成 Java 和 avascript 这样的面向对象编程语言中的,类似的关键字
contract SimpleStorage{//合约内容
Ctrl+s--->绿色对勾:成功编译,没有错误(不管用就点击编译,进入界面)
基础数据类型
- 值类型:按值传递
- 引用类型
- 映射类型
最基础的四种数据类型:
1、布尔值bool:定义true和false
2、整数uint :无符号整数,表示这个数字不是可正可负,只能是正数。可分配空间,eg:uint256(默认)uint256可以设置8个bytes,8bit是一个byte,我们按照byte来增加空间
3、字符串string:代表一个词,可以通过双引号表达,可以是一个词,一个短语或者任何其他组合。是一种bytes,但是string只能存储文本
4、int:表示正数或者负数int256
5、address:表示地址,占20个字节。通常是“Ox”开头
-
定义长字节数组:bytes1-bytes32
-
不定长字节数组bytes:一种更底层的数据类型。bytes变量通常是“Ox”开头然后有一些随机的数字和字母
枚举类型:enum
数组:T[k] 或者不定长数组[]
结构体struct
字典
通过数据类型定义变量,变量是用来存储不同的数值的。如果不给变量赋值,他会有一个默认值,null值,在Solidity中是0(会以0初始化变量)
bool hasFavoriteNumberm = false;
uint256 FavoriteNumber = 123;
string FavoriteNumberInText = "Six";
int256 FavoriteNumber = 6;
address myAddress = 。。。;
byte32 favoriteBytes = "cat";
函数
函数或者方法指的是独立模块,在我们调用的时候会执行某些命令
函数通过“function”关键字表示,创建一个任意名的函数,这个函数会把变量改成一个新的值。要改变的数,是传给函数的参数
为了看实际运行结果,会把合约部署在一个测试环境上,把它部署在本地网络,JavaScript VM(新版本叫Remix VM),表示合约将被部署到本地的javas虚拟机上
JS VM是本地测试用的区块链,在上面可以快速模拟交易,不需要等待测试网的流程,JS VM中的测试以太币在部署的合约的交易中,
可以设置gas limit,同时可以选择要部署的合约。在左手边的页面,点击“deplay”按钮来部署合约
函数和变量有4种可见度标识符:
1、public:在外部和内部都可见,任何与合约交互的人,都可以看见变量中存的值。public会创建storage和state变量的getter函数。给变量加上public,实际上就是给变量创建了getter函数。
2、private:表示只有这个合约是唯一可以调用这个函数的合约。只对合约内部可见
3、external:表示只对合约外部可以见,合约外的账户可以调用这个函数
4、internal:表示只有这个合约或者继承它的合约可以读取
变量是函数调用。在第一次部署后,没有在左边页面看到的favoriNumber的原因是少写了一个关键字,没有给函数和变量一个可见度标识符,默认是internal
pragma solidity ^0.8.7;
contract SimpleStorage{
uint256 public FavoriteNumber ;
function store(uint256 _FavoriteNumber) public {
FavoriteNumber=_FavoriteNumber;
}
}
在这个参数前加下划线,是为了区分这个参数和全局变量favoriteNum不一样
//带下划线的必须是参数,不能是全局变量
每次调用store函数,都会发布一个交易。因为每次在更改区块链状态的时候,我们都会发送交易。
作用域
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.7;
contract SimpleStorage{
uint256 public FavoriteNumber ;
function store(uint256 _FavoriteNumber) public {
FavoriteNumber=_FavoriteNumber;
FavoriteNumber=FavoriteNumber + 1;
}
}
scope(作用域),
1、FavoriteNumber是在global scope(全局作用),表示所有在括号中的任何函数,都可以获取它
//合约部署在链上,执行的时候需要耗费链上资源,所以使用gas表示消耗
2、橙色和蓝色按钮的区别在于view。Solidity中有两个关键字,标识函数的调用不需要消耗gas,view和pure。点击蓝色按钮,不发送交易,只是在链下读取数值
3、view函数,意味着只会读取这个合约的状态。eg:retrieve函数只读取FasvoriteNumber的值,如下。view函数不允许修改任何状态,
4、pure函数不允许修改状态,但,也不允许读取区块链数据,所以也不能读取FavoriteNumber的值。通过pure函数,常用的方法,或者某个不需要读取数据的算法
5、如果调用view或pure函数,是不需要支付gas的。因为只是读区块链。只有更改状态的时候才支付gas,发交易
6、在控制台,如果调用retrieve函数,会有一个call,与调用store函数不同。store函数调用后,会有绿色的对勾和交易哈希,但是在call中没有哈希和绿色对勾
7、查看call的细节信息时有一个执行消耗(”消耗只有在被合约调用时才会计算在内)
8、如果一个要改变区块链状态的函数调用了类似retrieve这种view或者pure函数才会消耗gas。eg:store不是view函数,他在某处调用了retrieve,那它就要支付retrieve的gas。因为读取区块链的信息,消耗了计算量和gas。调用view函数是免费的,除非在消耗gas的函数中调用它
9、带有public的变量FavoriteNumber,可以看作是一个返回uint256的view函数
10、关键字return表示调用函数后会得到什么类型的数据
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.7;
contract SimpleStorage{
uint256 public FavoriteNumber ;
function store(uint256 _FavoriteNumber) public {
FavoriteNumber=_FavoriteNumber;
}
//创建一个函数返回FavoriteNumber,模拟被自动创建的getter函数
function retrieve() public view returns (uint256){
return FavoriteNumber;
}
}
数组和结构体(1)
1、存储复杂点的
//很简单 也就是说一个代币的各种规则,比如总数,每次消耗数,是否可以增发,修改都可以用各种函数写出来。
方式1:创建一个关于人物的“结构体”(struct),也就是说在Solidity中创建一个新的类型。创建一个人物对象,其中包含某人的名字以及他们喜欢的数字。
1)、如果合约里有一大堆变量,比如oublic、Favaoritenumber
从技术上来说,实际上这个FavoritenNumber是放到了存储槽(storage slot)的索引0处,如果再搞一个从技术上说就是被放到了索引为1的槽位上,以此类推
2)、创建列表的方法:是使用一种被称为数组的(array)的数据结构
数组是储存列表,或者说储存一系列对象的一种方式。创建一个数组的方法和初始化其他类型没什么区别。先声明对象的类型,然后是对象的可见性,最后是变量名
[]:表示一个包含People(自定义)类型的数组
eg:
People[] public people;
因为它是一个public变量,会自动获得一个view函数(蓝色按钮)。此时的people列表是个空列表,没有任何结果。输入值是想要获得对象的索引
3)、如何给数组添加内容
People[] public people;
这种类型的数组就是所谓的动态数组(Dynamic Array),因为在初始化这个数组的时候并没有规定它的大小
People[3] public people;
最多只能放进去三个People对象。如果不给它设定大小那就表明它可以是任意大小并且数组的大小会随着添加和减少People而增大和减小
分析:
People public person = People({FavoriteNumber:2,name:"Patrick"});
struct People{
uint256 FavoriteNumber;
string name;
}
People[] public people;
//一
function addPerson(string memory _name,uint256 _FavoriteNumber) public {
people.push(People(_FavoriteNumber,_name));
}
大写的People表示的是名为People的结构体(struct),不是people变量
小写的people指的是这个数组。数组+点+push。一个新的People获得了_FavoriteNumber和 _ name
另一方式:先创建一个People类型的变量,再添加
//二
function addPerson(string memory _name,uint256 _FavoriteNumber) public{
People memory newPerson = People({FavoriteNumber:_FavoriteNumber,name:_name});
people.push(newPerson );
}
变量的数据位置必须是”storage“,”memory“,”calldata“
function addPerson(string memory _name,uint256 _FavoriteNumber) public{
// People memory newPerson = People({FavoriteNumber:_FavoriteNumber,name:_name});
// people.push(newPerson );
people.push(People(_FavoriteNumber,_name));
}
报错
People[3] public people;
function addPerson(string memory _name,uint256 _FavoriteNumber) public {
people.push(People(_FavoriteNumber,_name));
}
如果数组里有三个人则会报错,如果空则无事
错误与警告
红色:出错
黄色:警告
绿色:成功
Memory,Storage&Calldata
1、函数参数的数据位置(Data location)必须是”memory“或”calldata“
2、目前(不是与时俱进的目前)Solidity中有6种方式可以存储数据
Stack,Memory,Storage,Calldata,Code,Logs
Memory,Storage,Calldata(三个最大的)
3、Calldata和Memory意味着这个变量只是暂时存在,存储变量甚至存在于正在执行的函数之外
//calldata是函数调用栈,标志着函数参数没有使用指针引用
Memory:是可以被修改的临时变量
Storage:是可以被修改的永久变量
Calldata:是不能被修改的临时变量
4、实际上有六个地方可以让我们访问和存储信息,但我们不能说一个变量是Stack、Code、Logs,只能说Memory、Storage或Calldata
5、数据位置只适用于数组、结构或映射类型,这三种在Solidity中被认为是特殊的类型
从背后原理上来说。一个string实际上是一个bytes数组
6、总结:结构体、映射和数组在作为参数被添加到不同的函数时。需要给定一个Memory和Calldata关键
Mappings
字典,是一组键值对,每个key键返回与改建关联的某个value值。创建一个映射变量的方式与创建所有其他变量的方式完全相同。创建一部字典后,。每一个名字都会映射到一个特定的数字
当创建一个映射时,会把所有东西都初始化为空值。以下举例,在例子中每一个可能的字符都有一个对应的初始值FavoriteNumber为0.如想改动,手动添加
mapping初始化每个值的时候使用都是些相应的空值
mapping (string => uint256) public nameToFavoriteNumber;
function addPerson(string memory _name,uint256 _FavoriteNumber) public{
// People memory newPerson = People({FavoriteNumber:_FavoriteNumber,name:_name});
// people.push(newPerson );
people.push(People(_FavoriteNumber,_name));
nameToFavoriteNumber[_name] = _FavoriteNumber;
}
部署第一个合约
测试网完全是人们出于好心而自发运行的
编译--->将之前的合约删掉--->改变环境Injected Provider/Web3 Provider--->连接
无论Injected Web3是哪个网络,钱包就会连接到了哪个网络上就会部署到哪个网络
第二课总结
点击编译时,代码是在EVM(以太坊虚拟机:Ethereum Virtual Machine)上编译的。EVM就是往以太坊区块链上部署智能合约的一个标准并且任何实现某种EVM的区块链。可以把Solidity代码部署上去
与EVM兼容的区块链的例子:Avalance,Fantom,Polygon。可以把编写的Solidity代码,部署到这些区块链上
1、告诉Solidity正在使用的版本+SPDX-License-Identifier:MIT
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.26;
2、创建合约对象并命名
1)Solidity中的合约(contract)就类似于其他编程语言中的类(class)
并且花括号里的所有内容都是该合约的组成部分
contract TR{
}
2)数据类型:
uint256(unsigned integer 256),布尔值(bool),字符串(string),32位字节(byte 32)etc
//bool hasFavoriteNumberm = false;
uint256 FavoriteNumber = 123;
//string FavoriteNumberInText = "Six";
//int256 FavoriteNumber = 6;
//address myAddress = 。。。;
//byte32 favoriteBytes = "cat";
3)新类型
可以在Solidity中创建一个所谓的结构体(struct)
struct People{
uint256 FavoriteNumber;
string name;
}
4)创建数组(array)或者列表(list)
//uint256[] public FavoriteList;
People[] public people;
5)创建字典(dictionary),或者称为solidity中的映射(mapping)或叫哈希表(hash table)。当给他一个键(key)时,它会返回与该键对应的值(value)
mapping (string => uint256) public nameToFavoriteNumber;
6)创建函数以修改区块链的状态
/ function store(uint256 _FavoriteNumber) public {
FavoriteNumber=_FavoriteNumber;
也可以创建函数而不像修改区块链状态(view,pure)
function retrieve() public view returns (uint256){
return FavoriteNumber;
}
function add()public pure returns (uint256){
return (1+1);
}
也可以在函数中指定不同的数据位置(data location)
calldata和memory:该数据只是临时数据,只在函数运行期间存在,storage变量则是永久存在。函数的参数不能是storage变量,因为他们只能存在于函数运行期间
function addPerson(string memory _name,uint256 _FavoriteNumber) public{
// People memory newPerson = People({FavoriteNumber:_FavoriteNumber,name:_name});
// people.push(newPerson );
people.push(People(_FavoriteNumber,_name));
nameToFavoriteNumber[_name] = _FavoriteNumber;
}
3、编译,点击部署,它就会按照以太坊虚拟机的格式进行编译
合约
自身就能够部署其他合约并与之交互
function createSimpleStorageContract()....会有橙色按钮,点击即可
引入其他合约
使用一个合约来为我们部署其他合约并进一步的使用它和部署的这些合约进行交互。合约间的互相交互是使用Solidity和智能合约工作中必不可少的一部分。合约可以无缝地互相交互--->可组合性
智能合约是可以组合的,因为其可以轻易地互相交互。当我们构建Defi应用时,这些特性显得尤其重要。复杂的金融产品间就可以非常轻易的互相交互,因为所有的合约代码在链上都可用
//一个new就创建一个智能合约,并且部署了
做法:
1、保持SimpleStorage合约原样不变
2、创建一个名为StorageFactory的新合约
//StorageFactory合约如何知道SimpleStorage合约是什么样的
法一:复制SimpleStorage.sol中pragma solidity下方的所有代码粘贴到StorageFactory.sol放置到pragma solidity下方,编译并保存StorageFactory.sol
法二:使用import进行优化。import "./SimpleStorage.sol";
//" "里可以指定其他文件的路径,也可以指定其他包或者GitHUb的路径
//按ctrl+s会自动生成其他文件夹,不要删除保留下来,就可以部署
单个solidity文件,可以拥有多个不同的合约
3、创建一个全局变量,就像创建其他全局变量一样
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.7;
import "./SimpleStorage.sol";//" "里可以指定其他文件的路径,也可以指定其他包或者GitHUb的路径
//新合约
contract StorageFactory{
SimpleStorage public simpleStorage;
//可以部署SimpleStorage合约的函数并将其保存在一个全局变量中
//StorageFactory合约如何知道SimpleStorage合约是什么样的:复制粘贴
function createSimpleStorageContract() public{
simpleStorage = new SimpleStorage();
}
}
注意:版本兼容问题
createSimpleStorageContract函数每调用一次都会替换掉变量simpleStorage的值
改进:让其能够存储一个列表,包含部署的所有的SimpleStorage合约
SimpleStorage[] public simpleStorageArray;
//每创建一个合约都会将其存储为SimpleStorage类型的内存变量并将这个变量添加到数组中
End:
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.7;
import "./SimpleStorage.sol";//" "里可以指定其他文件的路径,也可以指定其他包或者GitHUb的路径
//新合约
contract StorageFactory{
SimpleStorage public simpleStorage;
//每创建一个合约都会将其存储为SimpleStorage类型的内存变量并将这个变量添加到数组中
SimpleStorage[] public simpleStorageArray;
//可以部署SimpleStorage合约的函数并将其保存在一个全局变量中
function createSimpleStorageContract() public{
simpleStorage = new SimpleStorage();
simpleStorageArray.push(simpleStorage);
}
}
与其他合约交互
如何交互?eg:在StorageFactory中调用SimpleStorage的store函。
可以把StorageFactory当成是所有 SimpleStorage的管理者。
要和其他的合约进行交互,需要两样东西:一是合约地址,二是合约ABI
ABI:指的是应用程序二进制接口。会告诉代码如何来跟合约进行交互//接口的签名信息
显示了所有不同的输入和输出,即这个合约能做到的所有事情。告诉了我们可以和合约进行交互的所有的这些不同的方式,可以调用的不同的函数
在StorageFactory中调用SimpleStorage的store函,首先要获得合约对象
定义:(名字 类型 = 类型 对象)
括号,可以访问数组中的不同元素
继承和重载
继承:一个合约能继承另一个合约的所有功能 eg:ExtraStorage合约是SimpleStorade合约的子合约
import 导入合约,is关键字指定继承关系
import "./SimpleStorage.sol";
contract ExtraStorage is SimpleStorage{
}
重载:要用到两个关键字,virtual和override
eg:store函数把参数中传递进来的_favoriteNumber赋值给了全局变量
在Extea
//new 实例 调用类里面的public 方法 跟js简直太像了
function store(uint256 _FavoriteNumber) public virtual {
FavoriteNumber=_FavoriteNumber;
}
function store(uint256 _FavoriteNumber) public override {
FavoriteNumber = _FavoriteNumber + 5;
}
总结
- new关键字可以在一个合约中部署另一个合约
- import关键字可以导入其他合约
- 只要获得了合约地址和ABI,就可以和其他合约进行交互
- 要创建一个子合约并继承其他合约的功能,可以使用继承。用关键字is
- 要改变父合约里函数的实现,可以重载函数,在要重载的函数上添加virtual修饰符
FundMe.sol
是一个智能合约,人们发起一个众筹。可以向它发送ETH、Polygon、Available、Fantom或者其他区块链原生通证。智能合约拥有者可以提取这些通行证来做他们想做的事。只有部署合约的人才可以提取其中的资金
FundMe.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "./PriceConverter.sol";
error NotOwner();
contract FundMe {
using PriceConverter for uint256;
mapping(address => uint256) public addressToAmountFunded;
address[] public funders;
// Could we make this constant? /* hint: no! We should make it immutable! */
address public /* immutable */ i_owner;
uint256 public constant MINIMUM_USD = 50 * 10 ** 18;
constructor() {
i_owner = msg.sender;
}
function fund() public payable {
require(msg.value.getConversionRate() >= MINIMUM_USD, "You need to spend more ETH!");
// require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
addressToAmountFunded[msg.sender] += msg.value;
funders.push(msg.sender);
}
function getVersion() public view returns (uint256){
// ETH/USD price feed address of Sepolia Network.
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
return priceFeed.version();
}
modifier onlyOwner {
// require(msg.sender == owner);
if (msg.sender != i_owner) revert NotOwner();
_;
}
function withdraw() public onlyOwner {
for (uint256 funderIndex=0; funderIndex < funders.length; funderIndex++){
address funder = funders[funderIndex];
addressToAmountFunded[funder] = 0;
}
funders = new address[](0);
// // transfer
// payable(msg.sender).transfer(address(this).balance);
// // send
// bool sendSuccess = payable(msg.sender).send(address(this).balance);
// require(sendSuccess, "Send failed");
// call
(bool callSuccess, ) = payable(msg.sender).call{value: address(this).balance}("");
require(callSuccess, "Call failed");
}
// Explainer from: https://solidity-by-example.org/fallback/
// Ether is sent to contract
// is msg.data empty?
// / \
// yes no
// / \
// receive()? fallback()
// / \
// yes no
// / \
//receive() fallback()
fallback() external payable {
fund();
}
receive() external payable {
fund();
}
}
// Concepts we didn't cover yet (will cover in later sections)
// 1. Enum
// 2. Events
// 3. Try / Catch
// 4. Function Selector
// 5. abi.encode / decode
// 6. Hash with keccak256
// 7. Yul / Assembly
PriceConvert.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
// Why is this a library and not abstract?
// Why not an interface?
library PriceConverter {
// We could make this public, but then we'd have to deploy it
function getPrice() internal view returns (uint256) {
// Sepolia ETH / USD Address
// https://docs.chain.link/data-feeds/price-feeds/addresses#Sepolia%20Testnet
AggregatorV3Interface priceFeed = AggregatorV3Interface(
0x694AA1769357215DE4FAC081bf1f309aDC325306
);
(, int256 answer, , , ) = priceFeed.latestRoundData();
// ETH/USD rate in 18 digit
return uint256(answer * 10000000000);
// or (Both will do the same thing)
// return uint256(answer * 1e10); // 1* 10 ** 10 == 10000000000
}
// 1000000000
function getConversionRate(uint256 ethAmount)
internal
view
returns (uint256)
{
uint256 ethPrice = getPrice();
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
// or (Both will do the same thing)
// uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1e18; // 1 * 10 ** 18 == 1000000000000000000
// the actual ETH/USD conversion rate, after adjusting the extra 0s.
return ethAmountInUsd;
}
})
//github上直接导入的代码withdraw函数需要修改成视频里的payable,不然不对(有待考证)
红色按钮表示fund函数和withdraw函数都是payable函数。withdraw函数允许用户提取资金,fund函数允许用户向合约转账。当调用fund函数时,可以转一些通证
只有部署合约的人才可以提取其中的资金。一旦资金被提取,funder余额将重置为零
用Chainlink的price feed(喂价)来设定用户支付的金额,以美元(usd)而不是ETH来作为单价
通过函数发送ETH&返回
合约内容:
想要合约能从用户处获得资金,然后提取资金,同时设置一个以usd计价的最小资助额
- 从用户处获得资金
- 提取资金
- 设置一个以usd计价的最小资助额:1、如何向这个合约转ETH
在实现所有函数逻辑之前,先把要实现的函数写下来:
fund函数--->通过它来发送资金
withdraw函数--->合约的owner(拥有者)可以提取不同的funder发送的资金
每当在任何一个兼容EVM的区块链上创建一笔交易时,value的值代表将通过这笔交易发送多少ETH。例如,当在不同账户间转移ETH时,实际就是在value中填充不同的ETH数量
发送的每笔交易都将有以下字段:
- Nonce:该账户的交易序号
- Gas Price:eg
- Gas Limit:eg:2100
- To:交易将要发送到的地址。在函数调用的交易中,仍可以这种方式填写to。可以调用一个函数,并同时进行转账
- Value:发送的数量
- Data:在调用函数和部署合约时会发送.eg:空
- v,r,s:交易签名时复杂的加密技术
payable:使函数可以被ETH或任何其他通证支付。(按钮)会使Fund函数变红,而不是橙色
msg.value:在函数中使用这个全局关键字,可以知道某人转账的金额
require:会检查msg.value是否大于1,如果不是它将revert这个交易。revert会同时显示错误信息“Didn't send enough”如下。如果require第一部分是false,那么revert显示这个错误。
revert:是将之前的操作回滚,并将剩余的gas费返回
eg:如果调用fund函数,number就会被赋值为5,但require条件没有满足。查看日志,value为0,所以fund函数会revert。调用函数,交易失败,因为require条件没满足,number仍为0
uint256 public number;
function fund() public payable{
number = 5;
require(msg.value>1e18,"Didn't send enough");
//1e18==1*10**18==1,000,000,000,000,000,000
}
钱包和合约地址都可以持有资金。每次部署合约时,可以获得一个合约地址,与钱包地址几乎一致。所以,钱包和合约都可以持有像ETH这样的原生区块链通证
假设将msg.value赋予一个某个数值的以太币,在交易中至少转一个以太币
function fund() public payable{
//设置一个以usd计算的最小金额
//1、如何向这个合约转ETH
require(msg.value>1e18);
//1e18==1*10**18==1,000,000,000,000,000,000
}
//请注意这里的步骤! 是在以太币数量为0的条件下进行部署,部署完成之后不要删除已部署的合约,直接修改以太币的数量点fund!
当有一个require时,如果条件没有满足,交易将被取消,之前任何的操作都将被撤销并且会发送一条错误信息
Chainlink&预言机
检查msg.value是否大于某个数量的法币,eg:大于$50
首先设置,随fund函数转出的最小usd金额
检查最小usd金额,要求msg.value>=这个最小的usd金额
但最小的usdjine是以美元计算,value则以ETH计算。如何将以太币转为usd--->预言机。
为了获得以太币的美元价格,需要从区块链之外获得信息返回到以太坊或其他原生智能合约平台。因此,为了获得区块链之外的价格,使用去中心的预言机网络,来获得一个ETH的USD价格
//链上的智能合约与链下的平台之间的交互
去中心化的预言机网络架构
区块链是确定性系统,自身不能与现实世界的数据和事件交互。//链上界有自己的元世界
智能合约链接问题或者预言机问题:区块链在设计上就是确定性系统,所有节点需要达成共识。如果想要添加可变数据或者随机数,或者调用API获得返回值,不同节点可能会得到不同结果,并且它们永远不可能达成共识,这被称为智能合约链接问题或者预言机问题
区块链预言机就是一种工具,它可以与世界交互,给智能合约提供外部数据或计算。如果使用中心化预言机,将重新引入单点失败风险。
Chainlink是一个解决方案
Chainlink是一个去中心化的预言机网络,用于将数据和外部计算引入我们的智能合约。这产生了混合型智能合约。它们结合了链上和链下,构建了功能丰富和强大的应用程序,Chainlink 是一个模块化、去中心化的 0racle 网络,可定制以传输任何数据或执行任何你想要的外部计算。
区块链节点无法进行HTTPS调用,无法达成共识
//https不能实现的原因是区块链的基建和平时写进程代码的基建是完全不同的
//区块链的所有代码逻辑都设计到区块的更新逻辑和共识的实现
Chainlink可验证随机性函数
Get a Random Number | Chainlink Documentation
Chainlink中最强大的去中心化功能:
喂价:是Chainlink中最强大的去中心化功能中功能之一
Chainlink Keepers:用于监听指定的不同触发事件的注册合约的Chainlink节点
docs.chain.link/ethereum/
可以直接向指定的任何API发出请求,但必须选择Chainlink节点和url/data来发送请求。可制定性最强的也是最难的
//可以认为喂价和VRF只是这个自定义的一个子集
总结:
1、为了发送ETH或其他区块链原生通行证,函数需要被标记为payable
2、如果想要合约中某事发生,如果没发生则整个交易失败,可以使用require语句
3、要获取交易中的ETH或区块链原生通证,使用全局关键词msg.value
4、
- Chainlink是一种技术,它可以在去中心化环境中为智能合约获取外部数据和进行外部计算
- Chainlink喂价是从现实世界中读取定价信息或其他数据的方法,这些数据已经为我们聚合和去中心化
- Chainlink VRF是一种将可证明的随机数从现实世界获取到智能合约中的方法
- Chainlink Keepers是一种去中心化驱动事件的方法。可以设置一些触发器,比如需要触发器触发做某事,就可以定义触发器做这件事
- Chainlink Any API是Chainlink节点的终极定制,可以使我们连接到任何事物
接口&喂价
msg.value值是否大于设置minmumUsd值
1、将msg.value从ETH转换为等值的美元:获得以太币、Phantom、Avalanche或任何正在使用的layer1 区块链 通证的价格======》创建函数获取价格
getPrice:获取以美元为单位的价格函数
getConversionRate:
为获得价格,使用Chainlink喂价来获取定价信息
当与Chainlink喂价交互时--->从其中一个称为聚合器的合约中读取最新一轮的数据
即“int price”
获取价格函数及调用——————>需要合约的ABI和合约地址
合约地址:可以从Chainlink喂价合约地址中获取合约的地址
//0xf599f4cd075a34b92169cf57271da65a7a936c35e3f31e854447fbb3e7eb736d
ABI:实际就是一个合约中不同功能和交互的列表,本身不需要包含任何逻辑,它只需要包含
|
| 方法
|
接口:编译接口会得到一个合约的ABI,接口定义了可以与合约交互的所有不同方式
version函数
//对方地址和接口,就可以实例化一个调用对象
//view是表面该接口只是get,没有任何set的操作
//view表示读取合约上的数据,不做任何其他操作,不会话费额外的gas
//这个粘贴过来的代码只是对面合约的api信息
//找不到仓库目录的小伙伴可以在src/v0.8/shared/interfaces查找
//聚合器节点之所以正常工作是因为前面提到的信誉和激励机制
//需要继续听,听不懂的地方其实就是讲了一个调用链下api
从GitHub中引入&NPM
从GitHub中引入&NPM
可复制,可导入(同上)
import "@chinlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
//函数只查看不修改 可以用view修饰符 不消耗gas
Remix可以自动识别:
@chainlink/contracts //(指向Chainlink/contracts的NPM包)是直接从Chainlink的GitHub代码仓库中创建的
NPM:是包管理器,可以保留不同合约的版本,可直接导入代码中
//新位置:@chinlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol
Solidity中的浮点数计算
加减乘除:一般先相乘,然后除
数组和结构体(2)
当人们给这个合约发送资金的时候,要想记录下来这些人。可以创建一个数据类型la
msg.value:有多少EYTH或者其他原生通证被发送
msg.sender//全局关键字,调用这个函数的人的地址
复习:
1、要使用一个合约,一定需要ABI和合约地址
2、编译一个接口会生成一个最小化ABI,能够和项目外的合约交互
3、若将编译好的接口和地址结合起来,可以调用这本合约在接口上的函数
4、Chainlink喂价可以以去中心化的方式获取真实世界的信息
5、在Solidity的数学计算不包括小数,确保使用单位正确//不包含小数,看来我之前的理解有问题,也就是直接把小数点后面直接包含了
6、msg.sender:(全局变量)代表着交易或组合信息的发送者
msg.value:代表着这个交易中发送的wei的数量
库(library)
Library | Solidity by Example | 0.8.24 (solidity-by-example.org)
//库里面不能有状态变量,只是提供函数功能
库和智能合约类似,但不能声明任何静态变量也不能发送ETH。可以使用库给不同的变量增加更多的功能性//静态变量可以理解为, 函数外声明的一般变量
msg.value.getConversionRate();
库和智能合约非常相似,刚开始会有协议,版本号
library{
}
一个库的所有的函数都是internal
这里的嵌入式,就是提供对象的操作方法
SafeMath,Overflow checking和_unchecked关键字
Solidity0.8版之前,SafeMath无处不在,之后基本销声匿迹。
如果将值为255的变量bigNumber+1----->被重置,为0
如果255是最大值,那么uite8会变成?
uint8 public bigNumber = 255;//checked
function add() public {
bigNumber = bigNumber + 1;
}
在Solidity0.8之前,无符号整型和整型是运行在unchecked这个概念下的。意味着如果你超过一个数字的上限,它只会绕回去并从可能的最低数字开始
Safemath库:会进行基础性地检查以确保不会在uint256和int256边界的时候绕回去
0.8版添加了一点。它会自动检查以确保是否要对变量执行所谓的溢出或下溢。实际上可以通过使用unchecked关键字恢复到unchecked版本
unchecked关键字会让代码更节省gas(为什么要使用这个未经检查的关键字?)
如果绝对肯定使用的数字永远不会达到数字的上限或下线,unchecked关键字可能就是有意义的
function add() public {
unchecked { bigNumber = bigNumber + 1;}
}
//基础编译组件提供了safe能力,对交易来说,底层提供这个能力是应该的
For loop(for 循环)
//因为如果同一个人发送多次的话,需要累加他的金额
//设置了一个mapping,进行映射,存放每个sender给了多少value.
//一个sender可能会多次捐款,进行了累加
//同一地址多次交易,累加同一地址value,计算总值
项目方可以从合约中提取资金——————————>withdraw函数
//在遍历的同时,如果有人同时在捐赠,会有并发问题吗?不会有并发问题 有共识机制保证最终一致性
for循环是一种将某种类型的索引对象进行循环的方式或者将某些范围内的数字进行循环的方法或者只是将一项任务重复执行某个次数。eg:有一个数组或一个列表
该列表上有一二三四。如果想要获取此数组或此列表中的所有元素(即一二三四)
可以使用for循环遍历(索引)这些对象中的每一个。第零个索引是1,第一个索引是2,以此类推。将索引0到3进行循环来获取所有的元素
[a,b,c,d]
0,1,2,3
在for循环中,首先要写起始索引和结束索引
for (init; condition; increment)
{
code
};
起始索引可以直接引用
function withdraw() public {
//遍历--->for循环 不会有并发问题 有共识机制保证最终一致性
/*开始,结束,每次增加的值*/
for (uint256 funderIndex = 0; funderIndex < funders.length; funderIndex = funderIndex+1){//funderIndex = funderIndex+1=====funderIndex++
address funder = funders[funderIndex];
addressToAmountFunded[funder] = 0;
}
}
如果说中间部分就是结束的index也不完全正确。因为我们会通过检查布尔值来看其是否仍然为真
在fund函数中,只有在合同被提供资金时,我们才会更新金额。当从合约中提取资金时,会把它重置为零
//难道不可以直接对map进行遍历和更新么----不行,因为mapping没有索引
//addressToAmountFunded映射的是address->uint,funderIndex是索引,用来获得funders数组中的address类型的funder
重置mapping的余额 后还有两件事:仍需重置数组以使资助者成为空数组,还需真正提取资金
当我们给合约发送资金,在fund函数发送了message.value,但实际没有提取资金,所以要重置数组的话,可以遍历一遍数组并从这个地址数组中删除里面的对象或者整个重置这个变量
重置数组
funders = new address[](0);//adrdess类型数组并且里面没有任何对象
如何实际地从这个合约中提取资金/如何把资金发送给合约的调用者
要想发送以太币或者其他 区块链原生货币的话,有三种不同的方式可供使用:
1、transfer:最简单,使用起来也是最直观的。
这种方法也可以用于不同合约之间互相发送代币,只需把想要发送的目标地址放到payable关键字里然后写下.transfer并在这里告诉他要转移多少资金
但transfer还存在一些自身问题
//仍需重置数组以使资助者成为空数组
funders = new address[](0);//adrdess类型数组并且里面没有任何对象
//还需真正提取资金
//如果想要转移资金,给调用这个withdraw函数的人,得到合约余额
payable(msg.sender).transfer(address(this).balance);//this指的就是这整个合约本身
//如此,就可以获得这个地址的区块链原生货币/以太币余额
//再 类型转换:msg.sender从address类型转换到payable address类型
//可以说msg.sender是一个address类型,payable(msg.sender)是payable address类型
//在Solidity中要想发送区块链原生货币,比如以太币,必须要使用payable address类型才能做到
//对于transfer来说,若这一行运行失败,会直接报错并回滚交易。但是用send,不会直接报错,返回一个表示运行是否成功的布尔值,但它要想回滚交易就必须添加require语句
2、send:
//payable(msg.sender).send(address(this).balance);//如果这一行运行失败,合约不会回滚交易,只是单纯没拿到钱
bool sendSuccess=payable(msg.sender).send(address(this).balance);
require(sendSuccess,"Send failed");
3、call:在solidity中实际使用的较为底层的命令,可以用它来调用几乎所有的solidity的函数,甚至不需要依赖ABI
(call Success,byte memory dataReturned) = payable (mag.sender).call{value:address(this).balancck}("")//""处就是填写任何函数信息/调用的某个合约的函数信息 ""表示空白
//这个call函数实际上有两个返回值。当一个函数有两个返回值时,可以把它们放到左边的括号里来表示
//这两个返回值的其中之一是布尔值,我们把它称为callSuccess。另一则是bytes对象,称为dataReturned
//call允许调用不同的函数。如果那个函数本身就返回一些数据或者说有返回值,就得把这个返回值给保存下来,它返回callSuccess。
//当函数被call成功调用时,这个值就是true。反之,就是false //bi:payable is not needed when using call function
//因为bytes对象是数组,所以dataReturned需放到memory? (因为在这里没有实际调用某个函数,不用关心dataReturned)
//只需把"byte memory dataReturned"删掉留下逗号,以此告诉solidity,我知道有两个但只关心其中一个
require(callSuccess,"Call failed"); //要求callSuccess必须为true,否则将回滚交易并抛出一个Call failed错误
区别:
- transfer最大允许2300gas并且运行失败时会直接报错
- send最大允许2300gas运行失败时会返回一个布尔值
- call转移所有gas,没有gas上限,返回一个布尔值,表示其运行成功或失败
目前来说,call是最推荐发送和接收以太币或其他区块链原生货币的方式
//B:call是一个底层调用,后面“”是一个可以自定义的回调函数。这里置空则只发送
Sending Ether (transfer, send, call) | Solidity by Example | 0.8.24 (solidity-by-example.org)
构造函数
如何可以让withdraw函数只能被合约的owner调用?
function callMeRightAway(){//需要发起两次交易,麻烦
}
//constructor()//构造函数,与其他编程语言完全相同
constructor(){
//调用机制:会在部署合约后立即调用一次.eg:部署本合约在这里写下
minimumUsd = 2;//minimumUsd就不再是50*10的18次方了,它会立即更新为2
//因为构造函数在创建合约的那笔交易中被立刻调用了一次
}
构造函数,允许我们以想要的方式设置合约。eg:想要withdraw函数只能被合约的拥有者调用,可以通过构造函数来设置合约的拥有者是谁
//创建全局变量
address public owner;
//constructor()//构造函数,与其他编程语言完全相同
constructor(){
owner = msg.sender;//msg.sender就是部署这个合约的人
}
Modifier
续上
function withdraw() public onlyOwner {
require(msg.sender == owner,"Sender is not owner!");
//双等号和等号:单个等号--->一个设置参数
// 双等号--->检查两个变量是否等价
for (uint256 funderIndex = 0; funderIndex < funders.length; funderIndex = funderIndex+1){//funderIndex = funderIndex+1=====funderIndex++
address funder = funders[funderIndex];
addressToAmountFunded[funder] = 0;
}
funders = new address[](0);
payable(msg.sender).transfer(address(this).balance);
bool sendSuccess=payable(msg.sender).send(address(this).balance);
require(sendSuccess,"Send failed");
(call Success,byte memory dataReturned) = payable (mag.sender).call{value:address(this).balancck}("")
require(callSuccess,"Call failed");
modifier onliyOwner{
require(msg.sender == owner,"Sender is not owner!");//装饰器模式,类似python的装饰器,java的spring AOP
_;//下划线表示运行余下的代码
//modifier给函数加了一层保护器
}
假设合约有很多函数都要求合约的拥有者来调用/合约有大量函数有不同要求
使用修饰器(Modifier)
修饰器是一个可以直接在函数声明中添加的关键字,从而修饰给函数某些功能
测试网演示
概念型知识


1、使用一个库和合约,创建了功能强大的去中心化应用
在智能合约中使用库为任意的数据类型提供功能
2、乘法
3、Solidity和智能合约中的计量单位
4、映射
5、关于数组的构造函数
6、如何发送资金
7、For循环,发送资金的不同方式
8、至少可以从一个低级别的角度解释,修饰符
Immutable&Constant
owner和minimumUsd变量:
owner在合约中被设定了一次后,就不会在改变。
同样,minimumUsd也被设置一次,虽然在构造函数之外
如果有这种只需设置一次的变量,可以使用Slolidity中的一些工具让他们更加节省gas---->Immutable&Constant
Constant(变量)和inmmutable(不可变的),可以使变量不能被改变
如果只是在函数外分配一次变量,然后永远不再改变它
如果是在编译时分配就可以继续添加constant关键字
uint256 public constant minimumUsd = 50 * 1e18;
当添加这个关键字后,minimumUsd不在占用一个存储空间,也更加容易被读
//每行代码都有可能花费gas 写代码的时候要注意了
通常情况下,常量变量有不同的命名规则
如全部大写并使用下划线(显式的提示,变量是一个恒定的变量)
view函数被合约调用是会消耗gas
//写保证合约功能,再考虑gas优化
一次性设置的变量,但在被声明的同一行之外(owner),如果在函数构造里设置它们可以将其标记为immutable .通常,一个好的immutable变量命名约定,是使用i_owner(i+下划线+变量名)
address public i_owner;
//immutable 一次赋值之后不可更改.声明但是未赋值
在全局范围内,没有函数会运行,不会有msg.sender
只有当在一个函数里面时,才会有一个msg.sender.所以在函数内部,可能会说
constructor(){
// owner = msg.sender;
i_owner = msg.sender;
}
modifier onliyOwner{
// require(msg.sender == owner,"Sender is not owner!");
require(msg.sender == i_owner,"Sender is not owner!");
_;
}
当涉及到储存变量的时候,这两种节省gas的原因是,不是把这些变量存储在一个存储槽里面而是直接把他们存储到合约的字节码中
//在编译阶段就就完成了固化,执行阶段不是变量,不占用执行内存
Custom Error(自定义错误类型)
怎样还可以使这个合约的gas效率更高一点--->更新require语句
modifier onliyOwner{
require(msg.sender == i_owner,"Sender is not owner!");
_;
}
目前,require语句必须要把"Send is not owner!"存储为一个是string字符串数组,这个错误日志中的每一个字符都需要被单独存储,
对于revertz:可以在顶部声明,然后使用if而不是require,然后添加一个revert语句
因为只是调用错误代码而不是调用与error相关的整个string字符串
error NotOwner();
contract{
modifier onliyOwner{
// require(msg.sender == owner,"Sender is not owner!");
//require(msg.sender == i_owner,"Sender is not owner!");
if(msg.sender != i_owner) { revert NotOwner();}
_;
}
}
revert关键字所做的事情,与之前没有判断条件时完全一样
Receive&Fallback
改进合约的另一办法
当人们给这个合约转钱或调用一个不存在的函数,仍可以触发合约的某些代码
特殊函数:receive,fallback,构造函数(不需要function的)
合约 — Solidity 0.8.26 文档 (soliditylang.org)
一个合约最多可以有一个使用receive函数
receivereceive() external payable { ... }functionexternalpayable
这个 区域是一种发送和处理不同函数的方式.可以在这个交易中添加参数,通过这里(Trandsct)去调整上面的变量.如果保持calldata留白就像是在meatamask中,点击send然后直接选合约地址一样.同样,不能使用metaMadk,因为这是一个虚拟环境
contract FallbackExample {
uint256 public result;
receive() external payable {
result = 1;//result是i还是0,1
//当像合约发送交易的时候,如果没有指定某个函数
//receive函数就会被触发(当calltada没有值时)
}
}
在任何其他合约,如FundMe中,调用这些函数之一时,实际上只是用某些数据来填充calldata,这些数据指向这里的这些函数之一
//因为这里的合约没有定义任何可以调用的方法
//calldata为空则认为是纯交易行为,调用receiver函数;如果不为空且指定了函数,则调用指定函数,如果没有指定,则调用兜底的发,fallback函数
fallback函数:即使数据与交易一起被发送,它也会被触发
fallback() external payable {
result = 2;
}
Fallback | Solidity by Example | 0.8.24 (solidity-by-example.org)
第四节课总结
- 枚举Enums
- 事件Events
- Try/Catch
- 函数选择器Function Selectors
- abi编码abi.encode /decode
- 哈希Hashing
- Yul/Assumbly
//contructor构造器在合约一开始就执行的函数
Simple Storage(Ether.js)
Debug方法和问题解决
遇到错误时首先要根据错误信息找出到底发生了什么,尝试独立修改错误==============20分钟纠错/至少15分钟尝试独立解决
假设已经尽可能地纠错并尝试但仍无结果========Google进行精准纠错
在论坛进行提问Stack Exchange ETH和Stack Overflow
Markdown
为什么+如何设置问题的格式,要使用什么工具
如何调试任何错误
提出问题,解决问题,查看文档,网上搜素Aler
Stack Overflow
GitHub
/ethdev==>指reddit的以太坊开发者版块,类似于国内常用的贴吧
Chainlink的Discord==>Discord是一种聊天社区,有些类似于国内常用的qq或微信
知乎
开源代码库提出问题
最好的格式:
1\search
2\制定一个能够概括问题细节的标题
3\在放代码之前大致介绍一下问题的由来
4\添加一段简洁,可复现的代码
5\学习Markdown来格式化代码
6\标签
7\阅读论坛指南
Git==|=GitHub
软件安全和环境zhun
在Linux环境中打开VScode
1、C+S+P=>wsl
2、Ubuntu
mkdir folder
cd folder
code .
3、WSl
//有遇到Connection refused问题的 可以去hosts文件添加一下ip

浙公网安备 33010602011771号