前言
提示:服务外包区块链学习
5被ban了,也不知道怎么改能过,无所谓了,我以后能看的见就行,不知道这篇能不能过审
Web3j的使用基本都是看博客和源码一点一点琢磨的
源码地址
项目源码地址
说明:从这篇博客开始就不用Ubuntu写区块链了,改用Windows,因为需要链接前面写的有关NFT系统的后台,而后台在Windows的主机上,即便用VMware设置了端口映射,主机的MataMask还是链接不上虚拟机上的,所以改用Windows,感觉都差不多,甚至Windows还要简单些。
只记录 *** 作
尚硅谷以太坊区块链直达链接
<!--- web3j -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.web3j</groupId>
<artifactId>codegen</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.3.1</version>
</dependency>
<dependency>
<groupId>org.web3j</groupId>
<artifactId>geth</artifactId>
<version>5.0.0</version>
<scope>compile</scope>
</dependency>
2、配置类
Web3j框架主要使用的3个对象:
Web3j对象类似Geth中的eth
Admin对象类似Geth中的personal
Geth跟Admin差不多(我也不知道,只知道一个可以解锁账号,一个可以锁账号)
配置类主要用来对这三个对象进行初始化,以及读写超时的设置
package com.nftmanage.nftmanage.config;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.admin.Admin;
import org.web3j.protocol.geth.Geth;
import org.web3j.protocol.http.HttpService;
import java.util.concurrent.TimeUnit;
@Configuration
public class ETHConfig {
@Value("${web3j.client-address}")
private String rpc;
@Bean
public Web3j web3j() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(60*1000, TimeUnit.MILLISECONDS);
builder.writeTimeout(60*1000, TimeUnit.MILLISECONDS);
builder.readTimeout(60*1000, TimeUnit.MILLISECONDS);
OkHttpClient httpClient = builder.build();
Web3j web3j = Web3j.build(new HttpService(rpc,httpClient,false));
return web3j;
}
@Bean
public Admin admin() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(60 * 1000, TimeUnit.MILLISECONDS);
builder.writeTimeout(60 * 1000, TimeUnit.MILLISECONDS);
builder.readTimeout(60 * 1000, TimeUnit.MILLISECONDS);
OkHttpClient httpClient = builder.build();
Admin admin = Admin.build(new HttpService(rpc, httpClient, false));
return admin;
}
@Bean
public Geth geth() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(60 * 1000, TimeUnit.MILLISECONDS);
builder.writeTimeout(60 * 1000, TimeUnit.MILLISECONDS);
builder.readTimeout(60 * 1000, TimeUnit.MILLISECONDS);
OkHttpClient httpClient = builder.build();
Geth geth = Geth.build(new HttpService(rpc, httpClient, false));
return geth;
}
}
二、Web3j智能合约
1、智能合约类准备
要把智能合约转化为对应的类,才可以调用智能合约
转化需要使用智能合约的ABI和BIN
需要两个工具类
两种方法,后面一种简单的多
package com.nftmanage.nftmanage.utils;
import org.web3j.codegen.SolidityFunctionWrapperGenerator;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.stream.Stream;
public class SolidityUtils {
/**
* 利用abi信息 与 bin信息 生成对应的abi,bin文件
* @param abi 合约编译后的abi信息
* @param bin 合约编译后的bin信息
*/
public static void generateABIAndBIN(String abi,String bin,String abiFileName,String binFileName){
File abiFile = new File("src/main/resources/"+abiFileName);
File binFile = new File("src/main/resources/"+binFileName);
BufferedOutputStream abiBos = null;
BufferedOutputStream binBos = null;
try{
FileOutputStream abiFos = new FileOutputStream(abiFile);
FileOutputStream binFos = new FileOutputStream(binFile);
abiBos = new BufferedOutputStream(abiFos);
binBos = new BufferedOutputStream(binFos);
abiBos.write(abi.getBytes());
abiBos.flush();
binBos.write(bin.getBytes());
binBos.flush();
}catch (Exception e){
e.printStackTrace();
// throw new BlogException(201,"留言写入过程出现错误");
}finally {
if(abiBos != null){
try{
abiBos.close();;
}catch (IOException e){
e.printStackTrace();
}
}
if(binBos != null){
try {
binBos.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
/**
*
* 生成合约的java代码
* 其中 -p 为生成java代码的包路径此参数和 -o 参数配合使用,以便将java文件放入正确的路径当中
* @param abiFile abi的文件路径
* @param binFile bin的文件路径
* @param generateFile 生成的java文件路径
*/
public static void generateClass(String abiFile,String binFile,String generateFile){
String[] args = Arrays.asList(
"-a",abiFile,
"-b",binFile,
"-p","",
"-o",generateFile
).toArray(new String[0]);
Stream.of(args).forEach(System.out::println);
SolidityFunctionWrapperGenerator.main(args);
}
}
package com.nftmanage.nftmanage.utils;
import org.junit.Test;
public class ETHUtils {
@Test
public void genAbi(){
String abi = "[\n" +
"\t{\n" +
"\t\t\"constant\": false,\n" +
"\t\t\"inputs\": [\n" +
"\t\t\t{\n" +
"\t\t\t\t\"name\": \"amount\",\n" +
"\t\t\t\t\"type\": \"uint256\"\n" +
"\t\t\t}\n" +
"\t\t],\n" +
"\t\t\"name\": \"withdraw\",\n" +
"\t\t\"outputs\": [],\n" +
"\t\t\"payable\": false,\n" +
"\t\t\"stateMutability\": \"nonpayable\",\n" +
"\t\t\"type\": \"function\"\n" +
"\t},\n" +
"\t{\n" +
"\t\t\"payable\": true,,\n" +
"\t\t\"stateMutability\": \"payable\",\n" +
"\t\t\"type\": \"fallback\"\n" +
"\t}\n" +
"]";
String bin = "{\n" +
"\t\"linkReferences\": {},\n" +
"\t\"object\": \"608060405234801561001057600080fd5b5060f78061001f6000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b348015604c57600080fd5b50606960048036038101908080359060200190929190505050606b565b005b683635c9adc5dea000008111151515608257600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f1935050505015801560c7573d6000803e3d6000fd5b50505600a165627a7a723058202bb5be9eae163caf1d06733e5ba29e5b2a51e2290708d5d287635637bd249a920029\",\n" +
"\t\"opcodes\": \"PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0xF7 DUP1 PUSH2 0x1F PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x3F JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x2E1A7D4D EQ PUSH1 0x41 JUMPI JUMPDEST STOP JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x4C JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x69 PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x6B JUMP JUMPDEST STOP JUMPDEST PUSH9 0x3635C9ADC5DEA00000 DUP2 GT ISZERO ISZERO ISZERO PUSH1 0x82 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST CALLER PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND PUSH2 0x8FC DUP3 SWAP1 DUP2 ISZERO MUL SWAP1 PUSH1 0x40 MLOAD PUSH1 0x0 PUSH1 0x40 MLOAD DUP1 DUP4 SUB DUP2 DUP6 DUP9 DUP9 CALL SWAP4 POP POP POP POP ISZERO DUP1 ISZERO PUSH1 0xC7 JUMPI RETURNDATASIZE PUSH1 0x0 DUP1 RETURNDATACOPY RETURNDATASIZE PUSH1 0x0 REVERT JUMPDEST POP POP JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0x2b 0xb5 0xbe SWAP15 0xae AND EXTCODECOPY 0xaf SAR MOD PUSH20 0x3E5BA29E5B2A51E2290708D5D287635637BD249A SWAP3 STOP 0x29 \",\n" +
"\t\"sourceMap\": \"28:197:0:-;;;;8:9:-1;5:2;;;30:1;27;20:12;5:2;28:197:0;;;;;;;\"\n" +
"}";
String abiFileName = "contract/Faucet.abi";
String binFileName = "contract/Faucet.bin";
SolidityUtils.generateABIAndBIN(abi,bin,abiFileName,binFileName);
}
@Test
public void generateJavaFile(){
String abiFile = "src/main/resources/contract/NFTMarket.abi";
String binFile = "src/main/resources/contract/NFTMarket.bin";
String generateFile = "src/main/java/com/nftmanage/nftmanage/solidity/";
SolidityUtils.generateClass(abiFile,binFile,generateFile);
}
}
第二个类中ABI和BIN需要具体调整成我这样,最后生成的abi和bin文件名可以根据实际使用情况更
准备完毕之后,在第二个类第一个方法那里右键运行方法就可以运行生成abi和bin文件
第二个方法就是通过已有的abi和bin文件来生成Java类的
ABI和BIN的来源:
编译的地方先选择你的智能合约,然后ABI就是复制ABI的地方,Bytecode就是BIN
觉得麻烦的直接在你想要文件在的地方右键新建一个文件命名成abi和bin文件,然后把复制的之间粘贴到文件里面就可以了,前面那些都太麻烦了
NFT智能合约Java类会报字符串超出String的长度的错,所以需要使用StringBuilder将类里的那一长串字符分开,最后合到一起。
像我这样
controller层
package com.nftmanage.nftmanage.controller;
import com.nftmanage.nftmanage.service.BlockChainService;
import com.nftmanage.nftmanage.utils.Result;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.web3j.crypto.Credentials;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Map;
import java.util.concurrent.ExecutionException;
/**
*区块链接口
*/
@Api(tags="区块链接口类")
@RestController
@CrossOrigin
public class BlockChainController {
@Autowired
BlockChainService blockChainService;
@PostMapping("getHeight")
public Result getHeight() {
return blockChainService.getHeight();
}
@PostMapping("getAccount")
public Result getAccount(){
return blockChainService.getAccounts();
}
@PostMapping("getBalanceOf")
public Result getBalanceOf(@RequestBody Map<String, String> map){
return blockChainService.getBlanceOf(map.get("address"));
}
@PostMapping("sendTransaction")
public Result sendTransaction(@RequestBody Map<String, String> map) throws IOException {
return blockChainService.sendEtherTransaction(map.get("from"),map.get("to"),new BigInteger(map.get("value")+"000000000000000000"),map.get("password"));
}
@PostMapping("withdraw")
public Result withdraw(@RequestBody Map<String, String> map) throws Exception {
return blockChainService.withdraw(Credentials.create(map.get("privateKey")),map.get("amount"));
}
@PostMapping("mintNFT")
public Result mintNFT(@RequestBody Map<String, String> map) throws IOException, ExecutionException, InterruptedException {
blockChainService.mintNFT(Credentials.create(map.get("privateKey")),map.get("tokenURI"));
return Result.ok();
}
@PostMapping("totalSupply")
public Result totalSupply(@RequestBody Map<String, String> map) throws IOException, ExecutionException, InterruptedException {
return Result.ok(blockChainService.totalSupply(Credentials.create(map.get("privateKey"))));
}
@PostMapping("burnNFT")
public Result burnNFT(@RequestBody Map<String, String> map) throws IOException, ExecutionException, InterruptedException {
blockChainService.burnNFT(Credentials.create(map.get("privateKey")),new BigInteger(map.get("tokenId")));
return Result.ok();
}
@PostMapping("tranNFT")
public Result tranNFT(@RequestBody Map<String, String> map) throws IOException, ExecutionException, InterruptedException {
blockChainService.tranNFT(Credentials.create(map.get("privateKey")),map.get("to"),new BigInteger(map.get("tokenId")));
return Result.ok();
}
@PostMapping("tokenURI")
public Result tokenURI(@RequestBody Map<String, String> map) throws IOException, ExecutionException, InterruptedException {
return blockChainService.tokenURI(Credentials.create(map.get("privateKey")),new BigInteger(map.get("tokenId")));
}
}
serviceImpl层:
package com.nftmanage.nftmanage.service.impl;
import com.nftmanage.nftmanage.service.BlockChainService;
import com.nftmanage.nftmanage.solidity.Faucet;
import com.nftmanage.nftmanage.solidity.NFTMarket;
import com.nftmanage.nftmanage.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.admin.Admin;
import org.web3j.protocol.admin.methods.response.BooleanResponse;
import org.web3j.protocol.admin.methods.response.PersonalUnlockAccount;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.RemoteFunctionCall;
import org.web3j.protocol.core.Request;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.protocol.geth.Geth;
import org.web3j.tx.RawTransactionManager;
import org.web3j.tx.TransactionManager;
import org.web3j.tx.gas.StaticGasProvider;
import org.web3j.utils.Convert;
import java.io.IOException;
import java.math.BigInteger;
import java.util.concurrent.ExecutionException;
@Service
public class BlockChainServiceImpl implements BlockChainService {
@Autowired
private Web3j web3j;
@Autowired
private Admin admin;
@Autowired
private Geth geth;
// 获得区块高度
public Result getHeight(){
try {
EthBlockNumber blockNumber = web3j.ethBlockNumber().send();
return Result.ok(blockNumber.getBlockNumber().longValue());
} catch (Exception e) {
e.printStackTrace();
}
return Result.error();
}
@Override
public Result getAccounts() {
try {
EthAccounts accounts = web3j.ethAccounts().send();
return Result.ok(accounts.getAccounts());
} catch (IOException e) {
e.printStackTrace();
}
return Result.error();
}
/**
* 获得账号的账户余额
*
* @param address 地址
* @return
*/
@Override
public Result getBlanceOf(String address) {
EthGetBalance balance = null;
try {
// DefaultBlockParameter.valueOf("latest") 可以理解为不同区块数量时链不同的状态 latest就是指最新区块挖出来时链的状态
balance = web3j.ethGetBalance(address, DefaultBlockParameter.valueOf("latest")).send();
//格式转化 wei-ether
return Result.ok(Convert.fromWei(balance.getBalance().toString(), Convert.Unit.ETHER).toPlainString().concat("ether"));
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 解锁账户,发送交易前需要对账户进行解锁
*
* @param address 地址
* @param password 密码
* @return
* @throws IOException
*/
public boolean unlockAccount(String address, String password) throws IOException {
Request<?, PersonalUnlockAccount> request = admin.personalUnlockAccount(address, password);
return request.send().accountUnlocked();
}
/**
* 交易发起成功后要锁定账号
*
* @param address 账号地址
* @return
* @throws IOException
*/
public boolean lockAccount(String address) throws IOException {
Request<?, BooleanResponse> request = geth.personalLockAccount(address);
BooleanResponse response = request.send();
return response.success();
}
/**
* 指定地址发送交易所需nonce获取
*
* @param address 待发送交易地址
* @return
* @throws IOException
*/
public BigInteger getNonce(String address) throws IOException {
Request<?, EthGetTransactionCount> request = web3j.ethGetTransactionCount(address, DefaultBlockParameterName.LATEST);
return request.send().getTransactionCount();
}
/**
* 发起以太币交易
*
* @param from 发送者地址
* @param to 接受者地址
* @param value 交易值
* @param password 发送者账号密码
* @return
*/
@Override
public Result sendEtherTransaction(String from,String to,BigInteger value,String password) throws IOException {
if(unlockAccount(from,password)){
try {
Transaction transaction = new Transaction(from,null,null,null,to,value,null);
String transactionHash = web3j.ethSendTransaction(transaction).send().getTransactionHash();
lockAccount(from);
return Result.ok(transactionHash);
} catch (IOException e) {
e.printStackTrace();
}
}
return Result.error();
}
/**
*
* @param amount 多少Wei
* @return
* @throws IOException
* @throws ExecutionException
* @throws InterruptedException
*/
@Override
public Result withdraw(Credentials credentials,String amount) throws Exception {
// 加载已经部署在链上的合约
BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
TransactionManager transactionManager = new RawTransactionManager(web3j, credentials, 123);
Faucet contract = Faucet.load("0xF700ebBC2B452b1D699e8F9DAB0Cf994BCb4E802",web3j,transactionManager,
new StaticGasProvider(gasPrice,BigInteger.valueOf(3000000L)));
// 部署合约,获取合约地址
// LeaveMsg contract = LeaveMsg.deploy(web3,credentials,web3.ethGasPrice().send().getGasPrice()
// ,Contract.GAS_PRICE).send();
// System.out.println(contract.getContractAddress());
// LeaveMsg contract = LeaveMsg.load("0xE3720A6D1dA0b27aCd735aA5Bc121d7AbD55Ff68",web3,credentials,
// GAS_PRICE,GAS_LIMIT);
// 异步调用写法
RemoteFunctionCall<TransactionReceipt> setWord = contract.withdraw(new BigInteger("100"));
TransactionReceipt transactionReceipt = setWord.sendAsync().get();
String transactionHash = transactionReceipt.getTransactionHash();
System.out.println(transactionHash);
// TransactionReceipt send1 = setWord.send();
// String blockHash = send1.getBlockHash();
// System.out.println(blockHash);
// RemoteFunctionCall> randomWord = contract.getRandomWord(new BigInteger("7"));
// Tuple4 send = randomWord.send();
// System.out.println(send.toString());
return Result.ok(transactionHash);
}
@Override
public void mintNFT(Credentials credentials,String tokenURI) throws IOException, ExecutionException, InterruptedException {
// 加载已经部署在链上的合约
BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
TransactionManager transactionManager = new RawTransactionManager(web3j, credentials, 123);
NFTMarket contract = NFTMarket.load("0x44F0c18BB444Ce4ff856Ae5938d4c0360e90D81b",web3j,transactionManager,
new StaticGasProvider(gasPrice,BigInteger.valueOf(3000000L)));
// 异步调用写法
RemoteFunctionCall<TransactionReceipt> setWord = contract.mintNFT(credentials.getAddress(),tokenURI);
TransactionReceipt transactionReceipt = setWord.sendAsync().get();
transactionReceipt.getTransactionHash();
}
@Override
public BigInteger totalSupply(Credentials credentials) throws ExecutionException, InterruptedException, IOException {
// 加载已经部署在链上的合约
BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
TransactionManager transactionManager = new RawTransactionManager(web3j, credentials, 123);
NFTMarket contract = NFTMarket.load("0x44F0c18BB444Ce4ff856Ae5938d4c0360e90D81b",web3j,transactionManager,
new StaticGasProvider(gasPrice,BigInteger.valueOf(3000000L)));
// 异步调用写法
RemoteFunctionCall<BigInteger> setWord = contract.totalSupply();
BigInteger total = setWord.sendAsync().get();
return total;
}
@Override
public BigInteger balanceOfOwner(Credentials credentials) throws Exception {
// 加载已经部署在链上的合约
BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
TransactionManager transactionManager = new RawTransactionManager(web3j, credentials, 123);
NFTMarket contract = NFTMarket.load("0x44F0c18BB444Ce4ff856Ae5938d4c0360e90D81b",web3j,transactionManager,
new StaticGasProvider(gasPrice,BigInteger.valueOf(3000000L)));
// 异步调用写法
RemoteFunctionCall<BigInteger> setWord = contract.balanceOf(credentials.getAddress());
BigInteger total = setWord.sendAsync().get();
return total;
}
@Override
public void burnNFT(Credentials credentials,BigInteger tokenId) throws IOException, ExecutionException, InterruptedException {
// 加载已经部署在链上的合约
BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
TransactionManager transactionManager = new RawTransactionManager(web3j, credentials, 123);
NFTMarket contract = NFTMarket.load("0x44F0c18BB444Ce4ff856Ae5938d4c0360e90D81b",web3j,transactionManager,
new StaticGasProvider(gasPrice,BigInteger.valueOf(3000000L)));
// 异步调用写法
RemoteFunctionCall<TransactionReceipt> setWord = contract.burnNFT(tokenId);
setWord.sendAsync().get();
}
@Override
public void tranNFT(Credentials credentials,String to,BigInteger tokenId) throws IOException, ExecutionException, InterruptedException {
// 加载已经部署在链上的合约
BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
TransactionManager transactionManager = new RawTransactionManager(web3j, credentials, 123);
NFTMarket contract = NFTMarket.load("0x44F0c18BB444Ce4ff856Ae5938d4c0360e90D81b",web3j,transactionManager,
new StaticGasProvider(gasPrice,BigInteger.valueOf(3000000L)));
// 异步调用写法
RemoteFunctionCall<TransactionReceipt> setWord = contract.safeTransferFrom(credentials.getAddress(),to,tokenId);
setWord.sendAsync().get();
}
@Override
public Result tokenURI(Credentials credentials,BigInteger tokenId) throws IOException, ExecutionException, InterruptedException {
// 加载已经部署在链上的合约
BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
TransactionManager transactionManager = new RawTransactionManager(web3j, credentials, 123);
NFTMarket contract = NFTMarket.load("0x44F0c18BB444Ce4ff856Ae5938d4c0360e90D81b",web3j,transactionManager,
new StaticGasProvider(gasPrice,BigInteger.valueOf(3000000L)));
// 异步调用写法
RemoteFunctionCall<String> setWord = contract.tokenURI(tokenId);
String tokenURI = setWord.sendAsync().get();
return Result.ok(tokenURI);
}
}
测试框架(Swager)
测试getBalance方法
测试成功!
其余的方法都是可以实现的,有兴趣的小伙伴自行尝试哦
账号私钥的获取方法:
MataMask查看账号详情
导出私钥,输入MataMask的密码即可
Over
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)