Solidity-Notes – 实音符区块链毕设代写

区块链毕设代写本文提供国外最新区块链项目源码下载,包括solidity,eth,fabric等blockchain区块链,Solidity-Notes – 实音符区块链毕设代写 是一篇很好的国外资料

Solidity-Notes

These notes are not prepared by me these are from tutorial

These are for quick reference. For hands on practice, checkout tutorial

Solidity official Documentation

As the file is large, use readme index/navigation bar to navigate through the page. Solidity-Notes - 实音符

contracts

Solidity’s code is encapsulated in contracts. A contract is the fundamental building block of Ethereum applications — all variables and functions belong to a contract, and this will be the starting point of all your projects. An empty contract named HelloWorld would look like this:

contract HelloWorld {  }

Version Pragma

All solidity source code should start with a “version pragma” — a declaration of the version of the Solidity compiler this code should use. This is to prevent issues with future compiler versions potentially introducing changes that would break your code.

For the scope of this tutorial, we’ll want to be able to compile our smart contracts with any compiler version in the range of 0.5.0 (inclusive) to 0.6.0 (exclusive). It looks like this: pragma solidity >=0.5.0 <0.6.0;.

Putting it together, here is a bare-bones starting contract — the first thing you’ll write every time you start a new project:

pragma solidity >=0.5.0 <0.6.0;  contract HelloWorld {  } 

State Variables & Integers

State variables

State variables are permanently stored in contract storage. This means they’re written to the Ethereum blockchain. Think of them like writing to a DB.

contract Example {   // This will be stored permanently in the blockchain   uint myUnsignedInteger = 100; } 

In this example contract, we created a uint called myUnsignedInteger and set it equal to 100.

Unsigned Integers: uint

The uint data type is an unsigned integer, meaning its value must be non-negative. There’s also an int data type for signed integers.

Note: In Solidity, uint is actually an alias for uint256, a 256-bit unsigned integer. You can declare uints with less bits — uint8, uint16, uint32, etc.. But in general you want to simply use uint except in specific cases, which we’ll talk about in later lessons.

Math Operations

Math in Solidity is pretty straightforward. The following operations are the same as in most programming languages:

  • Addition: x + y
  • Subtraction: x – y,
  • Multiplication: x * y
  • Division: x / y
  • Modulus / remainder: x % y (for example, 13 % 5 is 3, because if you divide 5 into 13, 3 is the remainder)

Solidity also supports an exponential operator (i.e. “x to the power of y”, x^y):

uint x = 5 ** 2; // equal to 5^2 = 25

Structs

struct Person {   uint age;   string name; }

Structs allow you to create more complicated data types that have multiple properties.

Note that we just introduced a new type, string. Strings are used for arbitrary-length UTF-8 data. Ex. string greeting = “Hello world!”

Arrays

When you want a collection of something, you can use an array. There are two types of arrays in Solidity: fixed arrays and dynamic arrays:

// Array with a fixed length of 2 elements: uint[2] fixedArray; // another fixed Array, can contain 5 strings: string[5] stringArray; // a dynamic Array - has no fixed size, can keep growing: uint[] dynamicArray;

You can also create an array of structs. Using the previous chapter’s Person struct:

Person[] people; // dynamic Array, we can keep adding to it

Remember that state variables are stored permanently in the blockchain? So creating a dynamic array of structs like this can be useful for storing structured data in your contract, kind of like a database.

Public Arrays

You can declare an array as public, and Solidity will automatically create a getter method for it. The syntax looks like:

Person[] public people;

Other contracts would then be able to read from, but not write to, this array. So this is a useful pattern for storing public data in your contract.

Function Declarations

A function declaration in solidity looks like the following:

function eatHamburgers(string memory _name, uint _amount) public {  }

This is a function named eatHamburgers that takes 2 parameters: a string and a uint. For now the body of the function is empty. Note that we’re specifying the function visibility as public. We’re also providing instructions about where the _name variable should be stored- in memory. This is required for all reference types such as arrays, structs, mappings, and strings.

What is a reference type you ask?

Well, there are two ways in which you can pass an argument to a Solidity function:

  • By value, which means that the Solidity compiler creates a new copy of the parameter’s value and passes it to your function. This allows your function to modify the value without worrying that the value of the initial parameter gets changed.

  • By reference, which means that your function is called with a… reference to the original variable. Thus, if your function changes the value of the variable it receives, the value of the original variable gets changed.

Note: It’s convention (but not required) to start function parameter variable names with an underscore (_) in order to differentiate them from global variables. We’ll use that convention throughout our tutorial.

You would call this function like so:

eatHamburgers("vitalik", 100);

Working With Structs and Arrays

Creating New Structs

Remember our Person struct in the previous example?

struct Person {   uint age;   string name; }  Person[] public people;

Now we’re going to learn how to create new Persons and add them to our people array.

// create a New Person: Person satoshi = Person(172, "Satoshi");  // Add that person to the Array: people.push(satoshi);

We can also combine these together and do them in one line of code to keep things clean:

people.push(Person(16, "Vitalik"));

Note that array.push() adds something to the end of the array, so the elements are in the order we added them. See the following example:

uint[] numbers; numbers.push(5); numbers.push(10); numbers.push(15); // numbers is now equal to [5, 10, 15]

Private / Public Functions

In Solidity, functions are public by default. This means anyone (or any other contract) can call your contract’s function and execute its code.

Obviously this isn’t always desirable, and can make your contract vulnerable to attacks. Thus it’s good practice to mark your functions as private by default, and then only make public the functions you want to expose to the world.

Let’s look at how to declare a private function:

uint[] numbers;  function _addToArray(uint _number) private {   numbers.push(_number); }

This means only other functions within our contract will be able to call this function and add to the numbers array.

As you can see, we use the keyword private after the function name. And as with function parameters, it’s convention to start private function names with an underscore (_).

Return Values

To return a value from a function, the declaration looks like this:

string greeting = "What's up dog";  function sayHello() public returns (string memory) {   return greeting; }

In Solidity, the function declaration contains the type of the return value (in this case string).

Function modifiers

The above function doesn’t actually change state in Solidity — e.g. it doesn’t change any values or write anything.

So in this case we could declare it as a view function, meaning it’s only viewing the data but not modifying it:

function sayHello() public view returns (string memory) { 

Solidity also contains pure functions, which means you’re not even accessing any data in the app. Consider the following:

function _multiply(uint a, uint b) private pure returns (uint) {   return a * b; }

This function doesn’t even read from the state of the app — its return value depends only on its function parameters. So in this case we would declare the function as pure.

Note: It may be hard to remember when to mark functions as pure/view. Luckily the Solidity compiler is good about issuing warnings to let you know when you should use one of these modifiers.

Keccak256 and Typecasting

Ethereum has the hash function keccak256 built in, which is a version of SHA3. A hash function basically maps an input into a random 256-bit hexadecimal number. A slight change in the input will cause a large change in the hash.

It’s useful for many purposes in Ethereum, but for right now we’re just going to use it for pseudo-random number generation.

Also important, keccak256 expects a single parameter of type bytes. This means that we have to “pack” any parameters before calling keccak256:

Example:

//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5 keccak256(abi.encodePacked("aaaab")); //b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9 keccak256(abi.encodePacked("aaaac"));

As you can see, the returned values are totally different despite only a 1 character change in the input.

Note: Secure random-number generation in blockchain is a very difficult problem. Our method here is insecure, but since security isn’t top priority for our Zombie DNA, it will be good enough for our purposes.

Typecasting

Sometimes you need to convert between data types. Take the following example:

uint8 a = 5; uint b = 6; // throws an error because a * b returns a uint, not uint8: uint8 c = a * b; // we have to typecast b as a uint8 to make it work: uint8 c = a * uint8(b);

In the above, a * b returns a uint, but we were trying to store it as a uint8, which could cause potential problems. By casting it as a uint8, it works and the compiler won’t throw an error.

Events

Events are a way for your contract to communicate that something happened on the blockchain to your app front-end, which can be ‘listening’ for certain events and take action when they happen.

Example:

// declare the event event IntegersAdded(uint x, uint y, uint result);  function add(uint _x, uint _y) public returns (uint) {   uint result = _x + _y;   // fire an event to let the app know the function was called:   emit IntegersAdded(_x, _y, result);   return result; }

Your app front-end could then listen for the event. A javascript implementation would look something like:

YourContract.IntegersAdded(function(error, result) {   // do something with result })

Mappings and Addresses

Addresses

The Ethereum blockchain is made up of accounts, which you can think of like bank accounts. An account has a balance of Ether (the currency used on the Ethereum blockchain), and you can send and receive Ether payments to other accounts, just like your bank account can wire transfer money to other bank accounts.

Each account has an address, which you can think of like a bank account number. It’s a unique identifier that points to that account, and it looks like this: 0x6B2eBFe3FE5c5B84746105421de93Df383b222E8

Mappings

Just like structs and arrays. Mappings are another way of storing organized data in Solidity. Defining a mapping looks like this:

// For a financial app, storing a uint that holds the user's account balance: mapping (address => uint) public accountBalance; // Or could be used to store / lookup usernames based on userId mapping (uint => string) userIdToName; 

A mapping is essentially a key-value store for storing and looking up data. In the first example, the key is an address and the value is a uint, and in the second example the key is a uint and the value a string.

Msg.sender

In Solidity, there are certain global variables that are available to all functions. One of these is msg.sender, which refers to the address of the person (or smart contract) who called the current function.

Note: In Solidity, function execution always needs to start with an external caller. A contract will just sit on the blockchain doing nothing until someone calls one of its functions. So there will always be a msg.sender.

Here’s an example of using msg.sender and updating a mapping:

mapping (address => uint) favoriteNumber;  function setMyNumber(uint _myNumber) public {   // Update our `favoriteNumber` mapping to store `_myNumber` under `msg.sender`   favoriteNumber[msg.sender] = _myNumber;   // ^ The syntax for storing data in a mapping is just like with arrays } function whatIsMyNumber() public view returns (uint) {   // Retrieve the value stored in the sender's address   // Will be `0` if the sender hasn't called `setMyNumber` yet   return favoriteNumber[msg.sender]; }

In this trivial example, anyone could call setMyNumber and store a uint in our contract, which would be tied to their address. Then when they called whatIsMyNumber, they would be returned the uint that they stored.

Using msg.sender gives you the security of the Ethereum blockchain — the only way someone can modify someone else’s data would be to steal the private key associated with their Ethereum address.

Require

we use require. require makes it so that the function will throw an error and stop executing if some condition is not true:

function sayHiToVitalik(string memory _name) public returns (string memory) {  // Compares if _name equals "Vitalik". Throws an error and exits if not true.  // (Side note: Solidity doesn't have native string comparison, so we  // compare their keccak256 hashes to see if the strings are equal)  require(keccak256(abi.encodePacked(_name)) == keccak256(abi.encodePacked("Vitalik")));  // If it's true, proceed with the function:  return "Hi!"; }

If you call this function with sayHiToVitalik("Vitalik"), it will return “Hi!”. If you call it with any other input, it will throw an error and not execute.

Thus require is quite useful for verifying certain conditions that must be true before running a function.

Inheritance

Most of the time code is getting quite long. Rather than making one extremely long contract, sometimes it makes sense to split your code logic across multiple contracts to organize the code.

One feature of Solidity that makes this more manageable is contract inheritance:

contract Doge {   function catchphrase() public returns (string memory) {     return "So Wow CryptoDoge";   } }  contract BabyDoge is Doge {   function anotherCatchphrase() public returns (string memory) {     return "Such Moon BabyDoge";   } }

BabyDoge inherits from Doge. That means if you compile and deploy BabyDoge, it will have access to both catchphrase() and anotherCatchphrase() (and any other public functions we may define on Doge).

This can be used for logical inheritance (such as with a subclass, a Cat is an Animal). But it can also be used simply for organizing your code by grouping similar logic together into different contracts.

Import

When you have multiple files and you want to import one file into another, Solidity uses the import keyword:

import "./someothercontract.sol";  contract newContract is SomeOtherContract {  }

So if we had a file named someothercontract.sol in the same directory as this contract (that’s what the ./ means), it would get imported by the compiler.

Storage vs Memory (Data location)

In Solidity, there are two locations you can store variables — in storage and in memory.

Storage refers to variables stored permanently on the blockchain. Memory variables are temporary, and are erased between external function calls to your contract. Think of it like your computer’s hard disk vs RAM.

Most of the time you don’t need to use these keywords because Solidity handles them by default. State variables (variables declared outside of functions) are by default storage and written permanently to the blockchain, while variables declared inside functions are memory and will disappear when the function call ends.

However, there are times when you do need to use these keywords, namely when dealing with structs and arrays within functions:

contract SandwichFactory {   struct Sandwich {     string name;     string status;   }    Sandwich[] sandwiches;    function eatSandwich(uint _index) public {     // Sandwich mySandwich = sandwiches[_index];      // ^ Seems pretty straightforward, but solidity will give you a warning     // telling you that you should explicitly declare `storage` or `memory` here.      // So instead, you should declare with the `storage` keyword, like:     Sandwich storage mySandwich = sandwiches[_index];     // ...in which case `mySandwich` is a pointer to `sandwiches[_index]`     // in storage, and...     mySandwich.status = "Eaten!";     // ...this will permanently change `sandwiches[_index]` on the blockchain.      // If you just want a copy, you can use `memory`:     Sandwich memory anotherSandwich = sandwiches[_index + 1];     // ...in which case `anotherSandwich` will simply be a copy of the      // data in memory, and...     anotherSandwich.status = "Eaten!";     // ...will just modify the temporary variable and have no effect      // on `sandwiches[_index + 1]`. But you can do this:     sandwiches[_index + 1] = anotherSandwich;     // ...if you want to copy the changes back into blockchain storage.   } }

Don’t worry if you don’t fully understand when to use which one yet — throughout this tutorial we’ll tell you when to use storage and when to use memory, and the Solidity compiler will also give you warnings to let you know when you should be using one of these keywords.

For now, it’s enough to understand that there are cases where you’ll need to explicitly declare storage or memory!

More on Function Visibility

Internal and External

In addition to public and private, Solidity has two more types of visibility for functions: internal and external.

internal is the same as private, except that it’s also accessible to contracts that inherit from this contract. (Hey, that sounds like what we want here!).

external is similar to public, except that these functions can ONLY be called outside the contract — they can’t be called by other functions inside that contract. We’ll talk about why you might want to use external vs public later.

For declaring internal or external functions, the syntax is the same as private and public:

contract Sandwich {   uint private sandwichesEaten = 0;    function eat() internal {     sandwichesEaten++;   } }  contract BLT is Sandwich {   uint private baconSandwichesEaten = 0;    function eatWithBacon() public returns (string memory) {     baconSandwichesEaten++;     // We can call this here because it's internal     eat();   } }

Interacting with other contracts

For our contract to talk to another contract on the blockchain that we don’t own, first we need to define an interface.

Let’s look at a simple example. Say there was a contract on the blockchain that looked like this:

contract LuckyNumber {   mapping(address => uint) numbers;    function setNum(uint _num) public {     numbers[msg.sender] = _num;   }    function getNum(address _myAddress) public view returns (uint) {     return numbers[_myAddress];   } }

This would be a simple contract where anyone could store their lucky number, and it will be associated with their Ethereum address. Then anyone else could look up that person’s lucky number using their address.

Now let’s say we had an external contract that wanted to read the data in this contract using the getNum function.

First we’d have to define an interface of the LuckyNumber contract:

contract NumberInterface {   function getNum(address _myAddress) public view returns (uint); }

Notice that this looks like defining a contract, with a few differences. For one, we’re only declaring the functions we want to interact with — in this case getNum — and we don’t mention any of the other functions or state variables.

Secondly, we’re not defining the function bodies. Instead of curly braces ({ and }), we’re simply ending the function declaration with a semi-colon (;).

So it kind of looks like a contract skeleton. This is how the compiler knows it’s an interface.

By including this interface in our dapp’s code our contract knows what the other contract’s functions look like, how to call them, and what sort of response to expect. In Solidity you can return more than one value from a function We can interact and use funciton from other smart contract this way, it in a contract as follows:

contract MyContract {   address NumberInterfaceAddress = 0xab38...    // ^ The address of the FavoriteNumber contract on Ethereum   NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);   // Now `numberContract` is pointing to the other contract    function someFunction() public {     // Now we can call `getNum` from that contract:     uint num = numberContract.getNum(msg.sender);     // ...and do something with `num` here   } }

In this way, your contract can interact with any other contract on the Ethereum blockchain, as long they expose those functions as public or external.

Handling Multiple Return Values

If a function returns multiple things then let’s look at how to handle them:

function multipleReturns() internal returns(uint a, uint b, uint c) {   return (1, 2, 3); }  function processMultipleReturns() external {   uint a;   uint b;   uint c;   // This is how you do multiple assignment:   (a, b, c) = multipleReturns(); }  // Or if we only cared about one of the values: function getLastReturnValue() external {   uint c;   // We can just leave the other fields blank:   (,,c) = multipleReturns(); }

If statements

If statements in Solidity look just like javascript:

function eatBLT(string memory sandwich) public {   // Remember with strings, we have to compare their keccak256 hashes   // to check equality   if (keccak256(abi.encodePacked(sandwich)) == keccak256(abi.encodePacked("BLT"))) {     eat();   } }

Immutability of Contracts

Up until now, Solidity has looked quite similar to other languages like JavaScript. But there are a number of ways that Ethereum DApps are actually quite different from normal applications.

To start with, after you deploy a contract to Ethereum, it’s immutable, which means that it can never be modified or updated again.

The initial code you deploy to a contract is there to stay, permanently, on the blockchain. This is one reason security is such a huge concern in Solidity. If there’s a flaw in your contract code, there’s no way for you to patch it later. You would have to tell your users to start using a different smart contract address that has the fix.

But this is also a feature of smart contracts. The code is law. If you read the code of a smart contract and verify it, you can be sure that every time you call a function it’s going to do exactly what the code says it will do. No one can later change that function and give you unexpected results.

External dependencies

we hard-code contract address into our DApp. But what would happen if a contract had a bug?

It’s unlikely, but if this did happen it would render our DApp completely useless — our DApp would point to a hardcoded address that no longer returned and we’d be unable to modify our contract to fix it.

For this reason, it often makes sense to have functions that will allow you to update key portions of the DApp.

Ownable contract

OpenZeppelin’s Ownable contract Below is the Ownable contract taken from the OpenZeppelin Solidity library. OpenZeppelin is a library of secure and community-vetted smart contracts that you can use in your own DApps. After this lesson, we highly recommend you check out their site to further your learning!

Documentation to OpenZepplin

Give the contract below a read-through. You’re going to see a few things we haven’t learned yet, but don’t worry, we’ll talk about them afterward.

/**  * @title Ownable  * @dev The Ownable contract has an owner address, and provides basic authorization control  * functions, this simplifies the implementation of "user permissions".  */ contract Ownable {   address private _owner;    event OwnershipTransferred(     address indexed previousOwner,     address indexed newOwner   );    /**    * @dev The Ownable constructor sets the original `owner` of the contract to the sender    * account.    */   constructor() internal {     _owner = msg.sender;     emit OwnershipTransferred(address(0), _owner);   }    /**    * @return the address of the owner.    */   function owner() public view returns(address) {     return _owner;   }    /**    * @dev Throws if called by any account other than the owner.    */   modifier onlyOwner() {     require(isOwner());     _;   }    /**    * @return true if `msg.sender` is the owner of the contract.    */   function isOwner() public view returns(bool) {     return msg.sender == _owner;   }    /**    * @dev Allows the current owner to relinquish control of the contract.    * @notice Renouncing to ownership will leave the contract without an owner.    * It will not be possible to call the functions with the `onlyOwner`    * modifier anymore.    */   function renounceOwnership() public onlyOwner {     emit OwnershipTransferred(_owner, address(0));     _owner = address(0);   }    /**    * @dev Allows the current owner to transfer control of the contract to a newOwner.    * @param newOwner The address to transfer ownership to.    */   function transferOwnership(address newOwner) public onlyOwner {     _transferOwnership(newOwner);   }    /**    * @dev Transfers control of the contract to a newOwner.    * @param newOwner The address to transfer ownership to.    */   function _transferOwnership(address newOwner) internal {     require(newOwner != address(0));     emit OwnershipTransferred(_owner, newOwner);     _owner = newOwner;   } }

A few new things here we haven’t seen before:

  • Constructors: constructor() is a constructor, which is an optional special function that has the same name as the contract. It will get executed only one time, when the contract is first created.
  • Function Modifiers: modifier onlyOwner(). Modifiers are kind of half-functions that are used to modify other functions, usually to check some requirements prior to execution. In this case, onlyOwner can be used to limit access so only the owner of the contract can run this function. We’ll talk more about function modifiers in the next chapter, and what that weird _; does.
  • indexed keyword: don’t worry about this one, we don’t need it yet.

So the Ownable contract basically does the following:

  1. When a contract is created, its constructor sets the owner to msg.sender (the person who deployed it)
  2. It adds an onlyOwner modifier, which can restrict access to certain functions to only the owner
  3. It allows you to transfer the contract to a new owner

onlyOwner is such a common requirement for contracts that most Solidity DApps start with a copy/paste of this Ownable contract, and then their first contract inherits from it once inherited, this applies to any contracts that inherit from first inherited contract in the future as well

Function Modifiers

A function modifier looks just like a function, but uses the keyword modifier instead of the keyword function. And it can’t be called directly like a function can — instead we can attach the modifier’s name at the end of a function definition to change that function’s behavior.

Let’s take a closer look by examining onlyOwner:

pragma solidity >=0.5.0 <0.6.0;  /**  * @title Ownable  * @dev The Ownable contract has an owner address, and provides basic authorization control  * functions, this simplifies the implementation of "user permissions".  */ contract Ownable {   address private _owner;    event OwnershipTransferred(     address indexed previousOwner,     address indexed newOwner   );    /**    * @dev The Ownable constructor sets the original `owner` of the contract to the sender    * account.    */   constructor() internal {     _owner = msg.sender;     emit OwnershipTransferred(address(0), _owner);   }    /**    * @return the address of the owner.    */   function owner() public view returns(address) {     return _owner;   }    /**    * @dev Throws if called by any account other than the owner.    */   modifier onlyOwner() {     require(isOwner());     _;   }    /**    * @return true if `msg.sender` is the owner of the contract.    */   function isOwner() public view returns(bool) {     return msg.sender == _owner;   }    /**    * @dev Allows the current owner to relinquish control of the contract.    * @notice Renouncing to ownership will leave the contract without an owner.    * It will not be possible to call the functions with the `onlyOwner`    * modifier anymore.    */   function renounceOwnership() public onlyOwner {     emit OwnershipTransferred(_owner, address(0));     _owner = address(0);   }    /**    * @dev Allows the current owner to transfer control of the contract to a newOwner.    * @param newOwner The address to transfer ownership to.    */   function transferOwnership(address newOwner) public onlyOwner {     _transferOwnership(newOwner);   }    /**    * @dev Transfers control of the contract to a newOwner.    * @param newOwner The address to transfer ownership to.    */   function _transferOwnership(address newOwner) internal {     require(newOwner != address(0));     emit OwnershipTransferred(_owner, newOwner);     _owner = newOwner;   } }

Notice the onlyOwner modifier on the renounceOwnership function. When you call renounceOwnership, the code inside onlyOwner executes first. Then when it hits the _; statement in onlyOwner, it goes back and executes the code inside renounceOwnership.

So while there are other ways you can use modifiers, one of the most common use-cases is to add a quick require check before a function executes.

In the case of onlyOwner, adding this modifier to a function makes it so only the owner of the contract (you, if you deployed it) can call that function.

Note: Giving the owner special powers over the contract like this is often necessary, but it could also be used maliciously. For example, the owner could add a backdoor function that would allow him to transfer anyone’s assests to himself!

So it’s important to remember that just because a DApp is on Ethereum does not automatically mean it’s decentralized — you have to actually read the full source code to make sure it’s free of special controls by the owner that you need to potentially worry about. There’s a careful balance as a developer between maintaining control over a DApp such that you can fix potential bugs, and building an owner-less platform that your users can trust to secure their data.

Gas


Solidity-Notes

这些笔记不是我准备的,它们来自教程,仅供快速参考。对于实际操作,请查阅教程《Solidity官方文档》

由于文件较大,请使用自述索引/导航栏浏览页面Solidity-Notes - 实音符

Solidity的代码封装在合同中。契约是以太坊eth应用程序的基本构建块——所有变量和函数都属于契约,这将是您所有项目的起点。名为HelloWorld的空契约如下所示:

所有solidity源代码都应该以“version pragma”开头,这是该代码应该使用的solidity编译器版本的声明。这是为了防止将来的编译器版本可能引入的更改会破坏您的代码

contracts

在本教程的范围内,我们希望能够使用0.5.0(含)到0.6.0(不含)范围内的任何编译器版本编译智能合约。看起来是这样的:pragma solidity&gt=0.5.0&lt;0.6.0;.

contract HelloWorld {  }

Version Pragma

总而言之,这里是一个简单的启动契约——每次启动新项目时,您首先要写的是:

状态变量永久存储在契约存储中。这意味着它们被写入以太坊eth区块链blockchain。把它们想象成给数据库写东西

在这个示例契约中,我们创建了一个名为myUnsignedInteger的uint,并将其设置为100

pragma solidity >=0.5.0 <0.6.0;  contract HelloWorld {  } 

State Variables & Integers

State variables

uint数据类型是无符号整数,这意味着它的值必须是非负的。还有一种用于有符号整数的int数据类型

contract Example {   // This will be stored permanently in the blockchain   uint myUnsignedInteger = 100; } 

注意:在Solidity中,uint实际上是uint256的别名,一个256位无符号整数。您可以用更少的位声明UINT—uint8、uint16、uint32等。。但一般来说,您只想在特定情况下使用uint,我们将在后面的课程中讨论

Unsigned Integers: uint

实体数学非常简单。以下操作与大多数编程语言中的操作相同:

Solidity还支持指数运算符(即“x为y的幂”,x^y):

Math Operations

结构允许您创建具有多个属性的更复杂的数据类型

  • Addition: x + y
  • Subtraction: x – y,
  • Multiplication: x * y
  • Division: x / y
  • Modulus / remainder: x % y (for example, 13 % 5 is 3, because if you divide 5 into 13, 3 is the remainder)

注意,我们刚刚引入了一个新类型,string。字符串用于任意长度的UTF-8数据。例如,string greeting=“Hello world!”

uint x = 5 ** 2; // equal to 5^2 = 25

Structs

struct Person {   uint age;   string name; }

当您需要某个集合时,可以使用数组。Solidity中有两种类型的数组:固定数组和动态数组:

您还可以创建结构数组。使用上一章的Person结构:

Arrays

还记得状态变量永久存储在区块链blockchain中吗?因此,创建这样的结构动态数组对于在合同中存储结构化数据非常有用,有点像数据库

// Array with a fixed length of 2 elements: uint[2] fixedArray; // another fixed Array, can contain 5 strings: string[5] stringArray; // a dynamic Array - has no fixed size, can keep growing: uint[] dynamicArray;

可以将数组声明为public,Solidity将自动为其创建一个getter方法。语法如下所示:

Person[] people; // dynamic Array, we can keep adding to it

其他契约将能够从该数组中读取,但不能写入该数组。因此,这是一种在合同中存储公共数据的有用模式

Public Arrays

以solidity表示的函数声明如下所示:

Person[] public people;

这是一个名为eatHamburgers的函数,它接受两个参数:一个字符串和一个uint。目前,函数体是空的。注意,我们将函数可见性指定为public。我们还提供了关于_name变量应该存储在内存中的位置的说明。这对于所有引用类型(如数组、结构、映射和字符串)都是必需的

Function Declarations

您询问的参考类型是什么

function eatHamburgers(string memory _name, uint _amount) public {  }

嗯,有两种方法可以将参数传递给Solidity函数:

按值传递,这意味着Solidity编译器创建参数值的新副本并将其传递给函数。这允许您的函数修改该值,而不必担心初始参数的值会发生更改

通过引用,这意味着调用函数时使用。。。对原始变量的引用。因此,如果您的函数更改了它接收的变量的值,那么原始变量的值就会更改

  • 注意:函数参数变量名的开头通常(但不是必需的)带有下划线(389;),以区别于全局变量。我们将在整个教程中使用该约定

  • 您可以这样调用此函数:

还记得上一个示例中的Person结构吗

现在我们将学习如何创建新的人员并将其添加到人员数组中

eatHamburgers("vitalik", 100);

Working With Structs and Arrays

Creating New Structs

我们还可以将这些元素组合在一起,并在一行代码中执行它们以保持整洁:

struct Person {   uint age;   string name; }  Person[] public people;

注意,array.push()在数组的末尾添加了一些内容,因此元素的顺序与我们添加它们的顺序相同。请参见以下示例:

// create a New Person: Person satoshi = Person(172, "Satoshi");  // Add that person to the Array: people.push(satoshi);

在Solidity中,函数默认为公共函数。这意味着任何人(或任何其他合同)都可以调用合同的函数并执行其代码

people.push(Person(16, "Vitalik"));

显然,这并不总是可取的,而且会使您的合同容易受到攻击。因此,在默认情况下,最好将函数标记为private,然后只公开要公开的函数

uint[] numbers; numbers.push(5); numbers.push(10); numbers.push(15); // numbers is now equal to [5, 10, 15]

Private / Public Functions

让我们看看如何声明私有函数:

这意味着只有合同中的其他函数才能调用此函数并添加到数字数组中

如您所见,我们在函数名后面使用关键字private。与函数参数一样,私有函数名通常以下划线(389;)开头

uint[] numbers;  function _addToArray(uint _number) private {   numbers.push(_number); }

要从函数返回值,声明如下所示:

在Solidity中,函数声明包含返回值的类型(在本例中为字符串)

Return Values

上述函数实际上不会更改实体状态-例如,它不会更改任何值或写入任何内容

string greeting = "What's up dog";  function sayHello() public returns (string memory) {   return greeting; }

Solidity还包含纯函数,这意味着您甚至不访问应用程序中的任何数据。考虑下面的内容:

Function modifiers

这个函数甚至不从应用程序的状态中读取它的返回值只取决于它的函数参数。在这种情况下,我们将函数声明为纯函数

注意:可能很难记住何时将函数标记为pure/view。幸运的是,Solidity编译器能够发出警告,让您知道何时应该使用这些修饰符之一

function sayHello() public view returns (string memory) { 

它在以太坊eth中有很多用途,但现在我们将使用它生成伪随机数

function _multiply(uint a, uint b) private pure returns (uint) {   return a * b; }

同样重要的是,keccak256需要一个字节类型的参数。这意味着我们必须在调用keccak256之前“打包”任何参数:

示例:

Keccak256 and Typecasting

正如您所看到的,尽管输入中只有1个字符的更改,但返回的值是完全不同的

注意:区块链blockchain中的安全随机数生成是一个非常困难的问题。我们这里的方法是不安全的,但由于安全性不是我们僵尸DNA的首要任务,它将足以满足我们的目的

有时需要在数据类型之间进行转换。以下面的示例为例:

在上面的示例中,a*b返回一个uint,但我们试图将其存储为uint8,这可能会导致潜在的问题。通过将其转换为uint8,它可以工作,编译器不会抛出错误

//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5 keccak256(abi.encodePacked("aaaab")); //b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9 keccak256(abi.encodePacked("aaaac"));

事件是您的合同将区块链blockchain上发生的事情传达给您的应用程序前端的一种方式,应用程序前端可以“监听”某些事件,并在事件发生时采取行动

示例:

Typecasting

您的应用程序前端可以监听事件。javascript实现类似于:

uint8 a = 5; uint b = 6; // throws an error because a * b returns a uint, not uint8: uint8 c = a * b; // we have to typecast b as a uint8 to make it work: uint8 c = a * uint8(b);

以太坊eth区块链blockchain由账户组成,您可以将其视为银行账户。账户有以太余额(以太坊eth区块链blockchain上使用的货币),您可以向其他账户发送和接收以太付款,就像您的银行账户可以将资金电汇到其他银行账户一样

Events

每个账户都有一个地址,你可以把它想象成一个银行账号。它是指向该帐户的唯一标识符,看起来像这样:0x6B2eBFe3FE5c5B84746105421de93Df383b222E8,就像结构和数组一样。映射是另一种可靠地存储有组织数据的方法。定义映射如下所示:

映射本质上是用于存储和查找数据的键值存储。在第一个示例中,键是地址,值是uint,在第二个示例中,键是uint,值是字符串

// declare the event event IntegersAdded(uint x, uint y, uint result);  function add(uint _x, uint _y) public returns (uint) {   uint result = _x + _y;   // fire an event to let the app know the function was called:   emit IntegersAdded(_x, _y, result);   return result; }

在Solidity中,有一些全局变量可用于所有函数。其中之一是msg.sender,它指调用当前函数的人员(或智能合约)的地址

YourContract.IntegersAdded(function(error, result) {   // do something with result })

Mappings and Addresses

Addresses

注意:在Solidity中,函数执行始终需要从外部调用方开始。在有人调用其某个功能之前,合同将只是坐在区块链blockchain上无所事事。所以总会有一个msg.sender

下面是一个使用msg.sender并更新映射的示例:

Mappings

在这个简单的示例中,任何人都可以调用setMyNumber并在我们的合同中存储uint,该uint将绑定到他们的地址。然后,当他们调用whatIsMyNumber时,会返回他们存储的uint

// For a financial app, storing a uint that holds the user's account balance: mapping (address => uint) public accountBalance; // Or could be used to store / lookup usernames based on userId mapping (uint => string) userIdToName; 

使用msg.sender可为您提供以太坊eth区块链blockchain的安全性-有人修改其他人数据的唯一方式是窃取与其以太坊eth地址相关联的私钥

Msg.sender

我们使用require。require使函数在某些条件不成立时抛出错误并停止执行:

如果使用sayHiToVitalik(“Vitalik”)调用此函数,它将返回“Hi!”。如果您使用任何其他输入调用它,它将抛出错误而不执行

Here’s an example of using msg.sender and updating a mapping:

mapping (address => uint) favoriteNumber;  function setMyNumber(uint _myNumber) public {   // Update our `favoriteNumber` mapping to store `_myNumber` under `msg.sender`   favoriteNumber[msg.sender] = _myNumber;   // ^ The syntax for storing data in a mapping is just like with arrays } function whatIsMyNumber() public view returns (uint) {   // Retrieve the value stored in the sender's address   // Will be `0` if the sender hasn't called `setMyNumber` yet   return favoriteNumber[msg.sender]; }

In this trivial example, anyone could call setMyNumber and store a uint in our contract, which would be tied to their address. Then when they called whatIsMyNumber, they would be returned the uint that they stored.

Using msg.sender gives you the security of the Ethereum blockchain — the only way someone can modify someone else’s data would be to steal the private key associated with their Ethereum address.

Require

we use require. require makes it so that the function will throw an error and stop executing if some condition is not true:

function sayHiToVitalik(string memory _name) public returns (string memory) {  // Compares if _name equals "Vitalik". Throws an error and exits if not true.  // (Side note: Solidity doesn't have native string comparison, so we  // compare their keccak256 hashes to see if the strings are equal)  require(keccak256(abi.encodePacked(_name)) == keccak256(abi.encodePacked("Vitalik")));  // If it's true, proceed with the function:  return "Hi!"; }

If you call this function with sayHiToVitalik("Vitalik"), it will return “Hi!”. If you call it with any other input, it will throw an error and not execute.

Thus require is quite useful for verifying certain conditions that must be true before running a function.

Inheritance

Most of the time code is getting quite long. Rather than making one extremely long contract, sometimes it makes sense to split your code logic across multiple contracts to organize the code.

One feature of Solidity that makes this more manageable is contract inheritance:

contract Doge {   function catchphrase() public returns (string memory) {     return "So Wow CryptoDoge";   } }  contract BabyDoge is Doge {   function anotherCatchphrase() public returns (string memory) {     return "Such Moon BabyDoge";   } }

BabyDoge inherits from Doge. That means if you compile and deploy BabyDoge, it will have access to both catchphrase() and anotherCatchphrase() (and any other public functions we may define on Doge).

This can be used for logical inheritance (such as with a subclass, a Cat is an Animal). But it can also be used simply for organizing your code by grouping similar logic together into different contracts.

Import

When you have multiple files and you want to import one file into another, Solidity uses the import keyword:

import "./someothercontract.sol";  contract newContract is SomeOtherContract {  }

So if we had a file named someothercontract.sol in the same directory as this contract (that’s what the ./ means), it would get imported by the compiler.

Storage vs Memory (Data location)

In Solidity, there are two locations you can store variables — in storage and in memory.

Storage refers to variables stored permanently on the blockchain. Memory variables are temporary, and are erased between external function calls to your contract. Think of it like your computer’s hard disk vs RAM.

Most of the time you don’t need to use these keywords because Solidity handles them by default. State variables (variables declared outside of functions) are by default storage and written permanently to the blockchain, while variables declared inside functions are memory and will disappear when the function call ends.

However, there are times when you do need to use these keywords, namely when dealing with structs and arrays within functions:

contract SandwichFactory {   struct Sandwich {     string name;     string status;   }    Sandwich[] sandwiches;    function eatSandwich(uint _index) public {     // Sandwich mySandwich = sandwiches[_index];      // ^ Seems pretty straightforward, but solidity will give you a warning     // telling you that you should explicitly declare `storage` or `memory` here.      // So instead, you should declare with the `storage` keyword, like:     Sandwich storage mySandwich = sandwiches[_index];     // ...in which case `mySandwich` is a pointer to `sandwiches[_index]`     // in storage, and...     mySandwich.status = "Eaten!";     // ...this will permanently change `sandwiches[_index]` on the blockchain.      // If you just want a copy, you can use `memory`:     Sandwich memory anotherSandwich = sandwiches[_index + 1];     // ...in which case `anotherSandwich` will simply be a copy of the      // data in memory, and...     anotherSandwich.status = "Eaten!";     // ...will just modify the temporary variable and have no effect      // on `sandwiches[_index + 1]`. But you can do this:     sandwiches[_index + 1] = anotherSandwich;     // ...if you want to copy the changes back into blockchain storage.   } }

Don’t worry if you don’t fully understand when to use which one yet — throughout this tutorial we’ll tell you when to use storage and when to use memory, and the Solidity compiler will also give you warnings to let you know when you should be using one of these keywords.

For now, it’s enough to understand that there are cases where you’ll need to explicitly declare storage or memory!

More on Function Visibility

Internal and External

In addition to public and private, Solidity has two more types of visibility for functions: internal and external.

internal is the same as private, except that it’s also accessible to contracts that inherit from this contract. (Hey, that sounds like what we want here!).

external is similar to public, except that these functions can ONLY be called outside the contract — they can’t be called by other functions inside that contract. We’ll talk about why you might want to use external vs public later.

For declaring internal or external functions, the syntax is the same as private and public:

contract Sandwich {   uint private sandwichesEaten = 0;    function eat() internal {     sandwichesEaten++;   } }  contract BLT is Sandwich {   uint private baconSandwichesEaten = 0;    function eatWithBacon() public returns (string memory) {     baconSandwichesEaten++;     // We can call this here because it's internal     eat();   } }

Interacting with other contracts

For our contract to talk to another contract on the blockchain that we don’t own, first we need to define an interface.

Let’s look at a simple example. Say there was a contract on the blockchain that looked like this:

contract LuckyNumber {   mapping(address => uint) numbers;    function setNum(uint _num) public {     numbers[msg.sender] = _num;   }    function getNum(address _myAddress) public view returns (uint) {     return numbers[_myAddress];   } }

This would be a simple contract where anyone could store their lucky number, and it will be associated with their Ethereum address. Then anyone else could look up that person’s lucky number using their address.

Now let’s say we had an external contract that wanted to read the data in this contract using the getNum function.

First we’d have to define an interface of the LuckyNumber contract:

contract NumberInterface {   function getNum(address _myAddress) public view returns (uint); }

Notice that this looks like defining a contract, with a few differences. For one, we’re only declaring the functions we want to interact with — in this case getNum — and we don’t mention any of the other functions or state variables.

Secondly, we’re not defining the function bodies. Instead of curly braces ({ and }), we’re simply ending the function declaration with a semi-colon (;).

So it kind of looks like a contract skeleton. This is how the compiler knows it’s an interface.

By including this interface in our dapp’s code our contract knows what the other contract’s functions look like, how to call them, and what sort of response to expect. In Solidity you can return more than one value from a function We can interact and use funciton from other smart contract this way, it in a contract as follows:

contract MyContract {   address NumberInterfaceAddress = 0xab38...    // ^ The address of the FavoriteNumber contract on Ethereum   NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);   // Now `numberContract` is pointing to the other contract    function someFunction() public {     // Now we can call `getNum` from that contract:     uint num = numberContract.getNum(msg.sender);     // ...and do something with `num` here   } }

In this way, your contract can interact with any other contract on the Ethereum blockchain, as long they expose those functions as public or external.

Handling Multiple Return Values

If a function returns multiple things then let’s look at how to handle them:

function multipleReturns() internal returns(uint a, uint b, uint c) {   return (1, 2, 3); }  function processMultipleReturns() external {   uint a;   uint b;   uint c;   // This is how you do multiple assignment:   (a, b, c) = multipleReturns(); }  // Or if we only cared about one of the values: function getLastReturnValue() external {   uint c;   // We can just leave the other fields blank:   (,,c) = multipleReturns(); }

If statements

If statements in Solidity look just like javascript:

function eatBLT(string memory sandwich) public {   // Remember with strings, we have to compare their keccak256 hashes   // to check equality   if (keccak256(abi.encodePacked(sandwich)) == keccak256(abi.encodePacked("BLT"))) {     eat();   } }

Immutability of Contracts

Up until now, Solidity has looked quite similar to other languages like JavaScript. But there are a number of ways that Ethereum DApps are actually quite different from normal applications.

To start with, after you deploy a contract to Ethereum, it’s immutable, which means that it can never be modified or updated again.

The initial code you deploy to a contract is there to stay, permanently, on the blockchain. This is one reason security is such a huge concern in Solidity. If there’s a flaw in your contract code, there’s no way for you to patch it later. You would have to tell your users to start using a different smart contract address that has the fix.

But this is also a feature of smart contracts. The code is law. If you read the code of a smart contract and verify it, you can be sure that every time you call a function it’s going to do exactly what the code says it will do. No one can later change that function and give you unexpected results.

External dependencies

we hard-code contract address into our DApp. But what would happen if a contract had a bug?

It’s unlikely, but if this did happen it would render our DApp completely useless — our DApp would point to a hardcoded address that no longer returned and we’d be unable to modify our contract to fix it.

For this reason, it often makes sense to have functions that will allow you to update key portions of the DApp.

Ownable contract

OpenZeppelin’s Ownable contract Below is the Ownable contract taken from the OpenZeppelin Solidity library. OpenZeppelin is a library of secure and community-vetted smart contracts that you can use in your own DApps. After this lesson, we highly recommend you check out their site to further your learning!

Documentation to OpenZepplin

Give the contract below a read-through. You’re going to see a few things we haven’t learned yet, but don’t worry, we’ll talk about them afterward.

/**  * @title Ownable  * @dev The Ownable contract has an owner address, and provides basic authorization control  * functions, this simplifies the implementation of "user permissions".  */ contract Ownable {   address private _owner;    event OwnershipTransferred(     address indexed previousOwner,     address indexed newOwner   );    /**    * @dev The Ownable constructor sets the original `owner` of the contract to the sender    * account.    */   constructor() internal {     _owner = msg.sender;     emit OwnershipTransferred(address(0), _owner);   }    /**    * @return the address of the owner.    */   function owner() public view returns(address) {     return _owner;   }    /**    * @dev Throws if called by any account other than the owner.    */   modifier onlyOwner() {     require(isOwner());     _;   }    /**    * @return true if `msg.sender` is the owner of the contract.    */   function isOwner() public view returns(bool) {     return msg.sender == _owner;   }    /**    * @dev Allows the current owner to relinquish control of the contract.    * @notice Renouncing to ownership will leave the contract without an owner.    * It will not be possible to call the functions with the `onlyOwner`    * modifier anymore.    */   function renounceOwnership() public onlyOwner {     emit OwnershipTransferred(_owner, address(0));     _owner = address(0);   }    /**    * @dev Allows the current owner to transfer control of the contract to a newOwner.    * @param newOwner The address to transfer ownership to.    */   function transferOwnership(address newOwner) public onlyOwner {     _transferOwnership(newOwner);   }    /**    * @dev Transfers control of the contract to a newOwner.    * @param newOwner The address to transfer ownership to.    */   function _transferOwnership(address newOwner) internal {     require(newOwner != address(0));     emit OwnershipTransferred(_owner, newOwner);     _owner = newOwner;   } }

A few new things here we haven’t seen before:

  • Constructors: constructor() is a constructor, which is an optional special function that has the same name as the contract. It will get executed only one time, when the contract is first created.
  • Function Modifiers: modifier onlyOwner(). Modifiers are kind of half-functions that are used to modify other functions, usually to check some requirements prior to execution. In this case, onlyOwner can be used to limit access so only the owner of the contract can run this function. We’ll talk more about function modifiers in the next chapter, and what that weird _; does.
  • indexed keyword: don’t worry about this one, we don’t need it yet.

So the Ownable contract basically does the following:

  1. When a contract is created, its constructor sets the owner to msg.sender (the person who deployed it)
  2. It adds an onlyOwner modifier, which can restrict access to certain functions to only the owner
  3. It allows you to transfer the contract to a new owner

onlyOwner is such a common requirement for contracts that most Solidity DApps start with a copy/paste of this Ownable contract, and then their first contract inherits from it once inherited, this applies to any contracts that inherit from first inherited contract in the future as well

Function Modifiers

A function modifier looks just like a function, but uses the keyword modifier instead of the keyword function. And it can’t be called directly like a function can — instead we can attach the modifier’s name at the end of a function definition to change that function’s behavior.

Let’s take a closer look by examining onlyOwner:

pragma solidity >=0.5.0 <0.6.0;  /**  * @title Ownable  * @dev The Ownable contract has an owner address, and provides basic authorization control  * functions, this simplifies the implementation of "user permissions".  */ contract Ownable {   address private _owner;    event OwnershipTransferred(     address indexed previousOwner,     address indexed newOwner   );    /**    * @dev The Ownable constructor sets the original `owner` of the contract to the sender    * account.    */   constructor() internal {     _owner = msg.sender;     emit OwnershipTransferred(address(0), _owner);   }    /**    * @return the address of the owner.    */   function owner() public view returns(address) {     return _owner;   }    /**    * @dev Throws if called by any account other than the owner.    */   modifier onlyOwner() {     require(isOwner());     _;   }    /**    * @return true if `msg.sender` is the owner of the contract.    */   function isOwner() public view returns(bool) {     return msg.sender == _owner;   }    /**    * @dev Allows the current owner to relinquish control of the contract.    * @notice Renouncing to ownership will leave the contract without an owner.    * It will not be possible to call the functions with the `onlyOwner`    * modifier anymore.    */   function renounceOwnership() public onlyOwner {     emit OwnershipTransferred(_owner, address(0));     _owner = address(0);   }    /**    * @dev Allows the current owner to transfer control of the contract to a newOwner.    * @param newOwner The address to transfer ownership to.    */   function transferOwnership(address newOwner) public onlyOwner {     _transferOwnership(newOwner);   }    /**    * @dev Transfers control of the contract to a newOwner.    * @param newOwner The address to transfer ownership to.    */   function _transferOwnership(address newOwner) internal {     require(newOwner != address(0));     emit OwnershipTransferred(_owner, newOwner);     _owner = newOwner;   } }

Notice the onlyOwner modifier on the renounceOwnership function. When you call renounceOwnership, the code inside onlyOwner executes first. Then when it hits the _; statement in onlyOwner, it goes back and executes the code inside renounceOwnership.

So while there are other ways you can use modifiers, one of the most common use-cases is to add a quick require check before a function executes.

In the case of onlyOwner, adding this modifier to a function makes it so only the owner of the contract (you, if you deployed it) can call that function.

Note: Giving the owner special powers over the contract like this is often necessary, but it could also be used maliciously. For example, the owner could add a backdoor function that would allow him to transfer anyone’s assests to himself!

So it’s important to remember that just because a DApp is on Ethereum does not automatically mean it’s decentralized — you have to actually read the full source code to make sure it’s free of special controls by the owner that you need to potentially worry about. There’s a careful balance as a developer between maintaining control over a DApp such that you can fix potential bugs, and building an owner-less platform that your users can trust to secure their data.

Gas

部分转自网络,侵权联系删除区块链源码网

www.interchains.cc

https://www.interchains.cc/24566.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » Solidity-Notes – 实音符区块链毕设代写

提供最优质的资源集合

立即查看 了解详情