使用python在Base测试网络上批量为每个账户部署合约,领取你的NFT

     我是一个刚学习智能合约的小白,最近coinbase的base测试网络现在部署一个合约可以免费mint一个nft,目前已mint了90多W个,感觉比较火,现在把我批量部署合约的方法分享给大家,使用语言为python。思路很简单,以ERC20代币为例,在openzeppelin上找到对应的ERC20标准库,写一个通用的ERC20代币的合约。为了让每个账户部署的合约都不相同(让编译的字节码不同),要让源码不同所以我们需要对应更改合约的名字就可以让每次合约部署后的字节码不同,然后在部署合约时随机生成该合约需要的三个传入参数(名字、符号、总供给),这样就实现了每个账户都创建了不同的合约。 具体过程: 1、通用的ERC20代币合约。 2、跨桥的脚本(从goerli到base)。 2、替换合约名字、编译合约内容、部署合约的脚本。 3、批量创建钱包脚本。 4、批量转账脚本。 5、批量mint脚本(由于这个NFTmint使用的是mintwithSignture方法mint目前还没找到需要怎么搞!!!替代方法可以用selenium代替。) 6、合约验证(如果需要让合约源码能让大家看到,就需要这一步。可以在hardhat上实现,因为这个合约部署时有三个参数,验证的时候也需要。所以我们在部署时就需要把他存储下来,以便后面使用hardhat批量验证合约。)

合约部署和使用hardhat验证的官方文档在这里

https://docs.base.org/guides/deploy-smart-contracts

合约文件

前面部分全是openzeppelin拷贝的,合约部署时需要传入代币名字、符号、以及要mint给部署者的代币数量。

//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;

interface IERC20 {
    event Transfer(address indexed from, address indexed to, uint256 value);

    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );

    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(address to, uint256 amount) external returns (bool);

    function allowance(
        address owner,
        address spender
    ) external view returns (uint256);

    function approve(address spender, uint256 amount) external returns (bool);

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

interface IERC20Metadata is IERC20 {
    function name() external view returns (string memory);

    function symbol() external view returns (string memory);

    function decimals() external view returns (uint8);
}

abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

contract ERC20 is Context, IERC20, IERC20Metadata {
    mapping(address => uint256) private _balances;

    mapping(address => mapping(address => uint256)) private _allowances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    function name() public view virtual override returns (string memory) {
        return _name;
    }

    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    function decimals() public view virtual override returns (uint8) {
        return 18;
    }

    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(
        address account
    ) public view virtual override returns (uint256) {
        return _balances[account];
    }

    function transfer(
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

    function allowance(
        address owner,
        address spender
    ) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

    function approve(
        address spender,
        uint256 amount
    ) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    function increaseAllowance(
        address spender,
        uint256 addedValue
    ) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }

    function decreaseAllowance(
        address spender,
        uint256 subtractedValue
    ) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(
            currentAllowance >= subtractedValue,
            "ERC20: decreased allowance below zero"
        );
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }

    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(
            fromBalance >= amount,
            "ERC20: transfer amount exceeds balance"
        );
        unchecked {
            _balances[from] = fromBalance - amount;

            _balances[to] += amount;
        }
        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        unchecked {
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
            _totalSupply -= amount;
        }

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);
    }

    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(
                currentAllowance >= amount,
                "ERC20: insufficient allowance"
            );
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}

    function _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
}

contract ErcToken is ERC20 {
   
    constructor(
        string memory name,
        string memory symbol,
        uint256 totalSupply
    ) ERC20(name, symbol) {
        _mint(msg.sender, totalSupply * (10 ** 18));
    }
}

跨桥代码

从goerli到base

from web3 import Web3
from web3.middleware import geth_poa_middleware
# goerli rpc 替换为自己的或者公用的
goerli_rpc = "xxxxx"
# goerli_rpc = "https://mainnet.infura.io/v3/"
w3_goerli = Web3(Web3.HTTPProvider(goerli_rpc))
w3_goerli.middleware_onion.inject(geth_poa_middleware, layer=0)
# bridge contract
# 跨桥合约地址
bridge_contract_address = Web3.to_checksum_address("0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA")
# 跨桥合约用到的ABI
bridge_abi = [{"inputs": [{"internalType": "address", "name": "_to", "type": "address"}, {"internalType": "uint256", "name": "_value", "type": "uint256"},
                          {"internalType": "uint64", "name": "_gasLimit", "type": "uint64"}, {"internalType": "bool", "name": "_isCreation", "type": "bool"},
                          {"internalType": "bytes", "name": "_data", "type": "bytes"}], "name": "depositTransaction", "outputs": [], "stateMutability": "payable", "type": "function"}]
# 构建跨桥合约对象
bridge_contract = w3_goerli.eth.contract(address=bridge_contract_address, abi=bridge_abi)
# 实现跨桥的函数,传入账户私钥和跨桥金额即可。
def goerli_base_bridge(private_key, amount):
    account = w3_goerli.eth.account.from_key(private_key)
    tx_data = {"from": account.address, "nonce": w3_goerli.eth.get_transaction_count(account.address), "value": Web3.to_wei(amount, "ether")}
    tx = bridge_contract.functions.depositTransaction(account.address, Web3.to_wei(amount, "ether"), 100000, False, b'').build_transaction(tx_data)
    signed_tx = account.sign_transaction(tx)
    tx_hash = w3_goerli.eth.send_raw_transaction(signed_tx.rawTransaction)
    tx_receipt = w3_goerli.eth.wait_for_transaction_receipt(tx_hash)
    return tx_hash.hex()

# ex: 跨桥1个ETH
goerli_base_bridge("你的私钥",1)

批量创建账户

from eth_account import Account
import eth_account.hdaccount as HDAccount
from web3 import Web3
import os
Account.enable_unaudited_hdwallet_features()  # 若需创建助记词需要执行此句
web3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/"))


def createAccount(numbers: int, Seed: bool):
    """传入钱包数量和是否需要助记词"""
    if Seed:
        with open('wallets.txt', 'a+', encoding='utf-8') as f:
            for _ in range(numbers):
                mnemonic = HDAccount.generate_mnemonic(12, 'english')
                account = Account.from_mnemonic(mnemonic)
                address = account.address
                private_key = account.key.hex()
                f.write(f"{address} {private_key} {mnemonic}\n")
    else:
        with open('wallets.txt', 'a+', encoding='utf-8') as f:
            for _ in range(numbers):
                account = Account.create()
                address = account.address
                private_key = account.key.hex()
                f.write(f"{address} {private_key}\n")
    return


# ex:创建10000个无助记词钱包
createAccount(10000,False)  
# ex:创建10000个有助记词钱包
createAccount(10000,True)

批量发送ETH

# coding:utf-8
import random
import string
from web3 import Web3
from web3.middleware import geth_poa_middleware

# goerli_rpc = "https://rpc.ankr.com/eth_goerli"
goerli_rpc = "你的goerli网络rpc"
w3_goerli = Web3(Web3.HTTPProvider(goerli_rpc))
w3_goerli.middleware_onion.inject(geth_poa_middleware, layer=0)
# base network rpc
base_rpc = "https://goerli.base.org"
w3 = Web3(Web3.HTTPProvider(base_rpc))
w3.middleware_onion.inject(geth_poa_middleware, layer=0)

# 从批量创建的账户文件中读取账户列表
def get_account_list(file):
    account_list = []
    with open(file, "r", encoding='utf-8') as f:
        for line in f.readlines():
            temp_address = line.split(" ")[0]
            temp_private_key = line.split(" ")[1].strip()
            account_list.append([temp_address, temp_private_key])
    return account_list

# 在base网络上批量发送eth
def base_transfer_eth(send_account, receipt, amount):
    tx_params = {
        "from": send_account.address,
        "chainId": w3.eth.chain_id,
        "to": Web3.to_checksum_address(receipt),
        "value": Web3.to_wei(amount, "ether"),
        "gas": 21000,
        "gasPrice": w3.eth.gas_price,
        "nonce": w3.eth.get_transaction_count(send_account.address),
    }
    # 签署交易并发送
    signed_txn = send_account.signTransaction(tx_params)
    txn_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
    wait_txn = w3.eth.wait_for_transaction_receipt(txn_hash)
    return txn_hash.hex()

# 在goerli网络上批量发送eth
def goerli_transfer_eth(send_account, receipt, amount):
    tx_params = {
        "from": send_account.address,
        "chainId": w3_goerli.eth.chain_id,
        "to": Web3.to_checksum_address(receipt),
        "value": Web3.to_wei(amount, "ether"),
        "gas": 21000,
        "gasPrice": w3_goerli.eth.gas_price,
        "nonce": w3_goerli.eth.get_transaction_count(send_account.address),
    }
    # 签署交易并发送
    signed_txn = send_account.signTransaction(tx_params)
    txn_hash = w3_goerli.eth.send_raw_transaction(signed_txn.rawTransaction)
    wait_txn = w3_goerli.eth.wait_for_transaction_receipt(txn_hash)
    return txn_hash.hex()


if __name__ == '__main__':
    send_key = "你要发送ETH的账户的私钥"
    send_account = w3.eth.account.from_key(send_key)
    # 获取需要接收ETH的账户列表
    account_list = get_account_list("wallets.txt")
    for account_data in account_list:
        account = w3.eth.account.from_key(account_data[1])
        print(f"{account_data[0]} transfering...")
        try:
            # 示例在base网络上转账,如果要在goerli上换成对应的函数即可
            tx = base_transfer_eth(send_account, account.address, 0.001)
            # 记录转账成功账户
            with open("transfer_eth.txt", "a+", encoding='utf-8') as f:
                f.write(f"{account_data[0]} {tx}\n")
        except:
            # 记录转账失败的账户
            with open("transfer_eth.txt", "a+", encoding='utf-8') as f:
                f.write(f"{account_data[0]} {account_data[1]}\n")
        print(f"{account_data[0]} transfered")
    print("done")

随机生成合约名字-符号-总供给、替换合约内容、随机构造合约部署时传入的参数、编译合约

使用python编译合约需要安装py-solc-x这个库,再随机生成合约名字的时候,为了让名字有含义,我们还需安装nltk这个库,它是一个常用的自然语言处理工具包,广泛应用于文本分析、语言模型等,他可以解决我们随机生成的合约名字有含义。

import nltk
import random
# 下载words集合(如果还没有下载的话)
nltk.download('words')

# 获取words集合中的所有单词
words_list = nltk.corpus.words.words()

# 从所有单词中随机选择一万个单词
def create_random_name():
    # 为了挑选出10000万个符合条件的词,我们需要扩大范围所以这里在100000个筛选出长度在3到8之间的单词
    random_words = [_ for _ in [random.choice(words_list) for _ in range(100000)] if (len(_) > 3 and len(_) < 9)]
    # 去重
    random_words = list(set(random_words))
    # 选出10000个单词
    result_words = [_ for _ in [random.choice(random_words) for _ in range(10000)] if (len(_) > 3 and len(_) < 9)]
    return result_words


# 替换sol文件中的合约名
def replace_contract_name(contranct_name_words_list):
    for one_words in contranct_name_words_list:
        # 保存合约名
        with open("Contract_name.txt", "a+") as f:
            # 随机生成一个长度为0到2的随机数
            randons = len(one_words)-random.randint(0, 2)
            # 生成合约名,和一个基于合约名的symbol(长度随机减少了0,2) 写入文件,后面部署合约会用到
            f.write(f"{str(one_words).capitalize()} {str(one_words[:randons]).upper()}\n")
        # 替换sol文件中的合约名
        with open("Token.sol", "r") as f:
            # 读取文件内容
            contents = f.read()
            # 替换单词
            new_contents = contents.replace("需要替换为你通用合约文件中的合约名字(尽量唯一,以防替换到合约文件中其它的内容)", f"{str(one_words).capitalize()}")
            # 打开新的sol文件并写入新内容./contractfile/是保存合约文件的文件夹,如果没有请自行创建和更改
            with open(f"./contractfile/{str(one_words).capitalize()}.sol", "w") as f:
                f.write(new_contents)

# 读取保存的随机名字的文件
def create_name_symbol():
    """Create random name and symbol"""
    temp_list = []
    with open("Contract_name.txt", "r", encoding="utf-8") as f:
        name_list = f.readlines()
        for i in name_list:
            temp_list.append([i.split(" ")[0].strip(), i.split(" ")[1].strip()])
    return temp_list

# 随机给出代币的总供给
def create_total_supply():
    """Create random data for contract"""
    total_supply_head = random.randint(1, 99)
    total_supply_tail = random.randint(6, 10)
    total_supply = total_supply_head * 10 ** total_supply_tail
    return total_supply

# 编译合约 传入使用的合约文件和合约名字,返回该合约的abi和字节码用于部署
def compile_sol_file(sol_file, contract_name):
    """Compile solidity file"""
    with open(sol_file, "r", encoding='utf-8') as file:
        source_code = file.read()
    compile_sol = compile_standard(
        {
            "language": "Solidity",
            "sources": {f"{contract_name}.sol": {"content": source_code}},
            "settings": {
                "outputSelection": {"*": {"*": ["abi", "metadata", "evm.bytecode", "evm.sourceMap"]}}
            }
        }
    )
    # get bytecode
    byte_code = compile_sol["contracts"][f"{contract_name}.sol"][f"{contract_name}"]["evm"]["bytecode"]["object"]
    # get abi
    abi = compile_sol["contracts"][f"{contract_name}.sol"][f"{contract_name}"]["abi"]
    return abi, byte_code

完整部署合约代码

在做好账户创建、保证每个账户中有足够的ETH、合约名字、合约名字对应的sol文件这些准备工作后,就可以开始批量部署了。

# coding:utf-8
import random
import nltk
import string
from web3 import Web3
from web3.middleware import geth_poa_middleware
from solcx import compile_standard,install_solc
import time
# 确保安装了你想要的sol文件编译版本
install_solc("0.8.18")
# 确保下载了词库
nltk.download('words')
words_list = nltk.corpus.words.words()
# base RPC 公共的,如果有更快的你可以更换
base_rpc = "https://goerli.base.org"
w3 = Web3(Web3.HTTPProvider(base_rpc))
w3.middleware_onion.inject(geth_poa_middleware, layer=0)

# 编译合约
def compile_sol_file(sol_file, contract_name):
    """Compile solidity file"""
    with open(sol_file, "r", encoding='utf-8') as file:
        source_code = file.read()
    compile_sol = compile_standard(
        {
            "language": "Solidity",
            "sources": {f"{contract_name}.sol": {"content": source_code}},
            "settings": {
                "outputSelection": {"*": {"*": ["abi", "metadata", "evm.bytecode", "evm.sourceMap"]}}
            }
        }
    )
    # get bytecode
    byte_code = compile_sol["contracts"][f"{contract_name}.sol"][f"{contract_name}"]["evm"]["bytecode"]["object"]
    # get abi
    abi = compile_sol["contracts"][f"{contract_name}.sol"][f"{contract_name}"]["abi"]
    return abi, byte_code

# 获取随机合约名
def create_name_symbol():
    """Create random name and symbol"""
    temp_list = []
    with open("Contract_name.txt", "r", encoding="utf-8") as f:
        name_list = f.readlines()
        for i in name_list:
            temp_list.append([i.split(" ")[0].strip(), i.split(" ")[1].strip()])
    return temp_list

# 创建随机供给
def create_total_supply():
    """Create random data for contract"""
    total_supply_head = random.randint(1, 99)
    total_supply_tail = random.randint(6, 10)
    total_supply = total_supply_head * 10 ** total_supply_tail
    return total_supply

# 部署合约
def deploy_contract(private_key, abi, byte_code, _name, _symbol, _total_supply):
    """Deploy contract"""
    account = w3.eth.account.from_key(private_key)
    # create contract
    contract = w3.eth.contract(abi=abi, bytecode=byte_code)
    # deploy contract
    tx_data = {"from": account.address, "nonce": w3.eth.get_transaction_count(account.address), "gasPrice": w3.eth.gas_price}
    tx = contract.constructor(_name, _symbol, _total_supply).build_transaction(tx_data)
    signed_tx = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    # get contract address
    contract_address = tx_receipt.contractAddress
    return contract_address

def get_account_list(file):
    account_list = []
    with open(file, "r", encoding='utf-8') as f:
        for line in f.readlines():
            temp_address = line.split(" ")[0]
            temp_private_key = line.split(" ")[1].strip()
            account_list.append([temp_address, temp_private_key])
    return account_list


if __name__ == '__main__':
    # 获取要部署合约的用户列表
    account_list = get_account_list("wallets.txt")
    # 获取合约名字和符号的列表
    name_symbol_list = create_name_symbol()
    # 任务索引id
    index_i = 0
    # 循环每个账户进行部署合约
    for account_data in account_list:
        # 使用的合约名字
        name = name_symbol_list[index_i][0]
        # 使用的合约符号
        symbol = name_symbol_list[index_i][1]
        # 使用的总供给
        total_supply = create_total_supply()
        # 对应合约文件编译后的abi、字节码
        abi, byte_code = compile_sol_file(f"./contractfile/{name}.sol", name)
        # 账户实例化
        account = w3.eth.account.from_key(account_data[1])
        print(f"{account_data[0]} deploying...")
        try:
            # 开始部署
            tx = deploy_contract(account_data[1], abi, byte_code, name, symbol, total_supply)
            # 写入部署成功的信息,包含你部署的合约地址,后面mint会用到。
            with open("deploy.txt", "a+", encoding='utf-8') as f:
                f.write(f"{account_data[0]} {account_data[1]} {tx} {name} {symbol} {total_supply}\n")
        except Exception as e:
            # 打印报错信息
            print(e)
            # 写入部署失败的信息
            with open("deployfaild.txt", "a+", encoding='utf-8') as f:
                f.write(f"{account_data[0]} {account_data[1]}\n")
        print(f"{account_data[0]} deployed")
        index_i += 1

合约部署完成以后你就可以使用你部署的合约地址去下面这个网址去mint你的NFT了。

https://quests.base.org/

合约验证

合约验证使用hardhat来实现。不需要像主网、goerli和bsc验证合约需要api。你只需要按照官方的那个文档配置号你的config文件就可以了。

在你的hardhat.config.js中大概就像这样

require("@nomiclabs/hardhat-etherscan");
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config()
/** @type import('hardhat/config').HardhatUserConfig */
// 合约验证
module.exports = {
  solidity: {
    version: "0.8.19",
  },
  etherscan: {
    apiKey: {
      // Basescan doesn't require an API key, however
      // Hardhat still expects an arbitrary string to be provided.
      "basegoerli": "PLACEHOLDER_STRING"
    },
    customChains: [
      {
        network: "basegoerli",
        chainId: 84531,
        urls: {
          apiURL: "https://api-goerli.basescan.org/api",
          browserURL: "https://goerli.basescan.org"
        }
      }
    ]
  },
  networks: {
    basegoerli: {
      url: process.env.BASEGOERLI_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
};

1、把你部署好的合约文件拷贝一份到hardhat的contract文件夹下

合约文件
合约文件

2、在命令行输入npx hardhat compile编译好这个合约文件,然后在scripts文件夹下编写验证合约的js脚本,我这里以两个为例我是这样写的。然后在命令行输入npx hardhat run scripts/(这个部署js的文件名.js)--network basegoerli(我给他去的名字是这个,你的要对应更改)

const hre = require("hardhat");

async function main() {
    const { ethers } = hre;

    //你对应的合约地址,多个的话可以写个函数读取
    const contractAddresses = [
        "0x045d788952D4DA8aA1D0dEDc2fBe1dDeE387C018",
        // ...
    ];
    //你对应的合约部署时的参数,多个的话可以写个函数读取
    const constructorArgs = [
        ["Gobelin", "GOBELI", 1000000000],
        // ...
    ];

    for (let i = 0; i < contractAddresses.length; i++) {
        const address = contractAddresses[i];
        const args = constructorArgs[i];
        console.log(`Verifying contract at ${address}...`);
        await hre.run("verify:verify", {
            address,
            constructorArguments: args,
            # 你的合约文件名和合约名,多个的话写成函数传参形式替换
            contract: "Token.sol:Token",
            network: "basegoerli",
        });
        console.log(`Contract at ${address} verified successfully.`);
    }
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });

3、如果你只有单个合约,那验证那就直接在命令行就可以解决了。对应的更改合约地址(contract_address),参数(arg1,arg2,arg3,…)就可以了,参数有多少个传多少个。

npx hardhat verify --network network_name contract_address arg1 arg2 arg3 ....

4、验证完以后的合约就有这个对钩啦。

合约验证
合约验证
Subscribe

刚开始学写这些内容,如果对你有用的话,给我点个关注吧,Thanks♪(・ω・)ノ。

https://twitter.com/runker54