Java FilCoin的充值以及上链操作详情流程(附有代码)

Posted binlixia

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java FilCoin的充值以及上链操作详情流程(附有代码)相关的知识,希望对你有一定的参考价值。

对于fil的充提,公司原来用的是www.ztpay.org这个网站提供的服务。这个平台后面卷款跑路了,导致公司损失了十几万。没办法,后面想去找一些比较可靠的第三方平台,但是由于每个月的服务费都要几万块。所以公司就要求能不能自己部署节点,然后自己提供充提服务。但是部署节点有个问题是,由于服务器的性能不是很好,磁盘容量不够,导致需要经常清数据,这样的运维成本比较大。后面发现可以直接使用infura提供的服务。Ethereum API | IPFS API & Gateway | ETH Nodes as a Service | Infura

但是infura有个缺点就是,它不提供完整的区块链信息。只是提供三天内产生的区块信息,这个要特别注意一下。不过这个对于大多数的应用来说,都不是什么问题。

一、在infura上注册一个应用

1、注册并登陆infura平台,进入到以下控制台

 2、点击左边的菜单栏"FILCOIN",进入filcoin的设置页面

 3、点击右上角的"CREATE NEW PROJECT"按钮,在弹出的弹框中输入项目的名称

4、如下图所示,我们就在infura创建了一个filCoin的项目,记住“PROJECT ID” 和"PROJECT SECRET",这两个值在后面的操作中,我们要用到。

 二、关键代码

1、fil节点地址配置类

public class FilApiConfig {
    //指定fil节点的地址
    public final static String FIL_RPC_URL = "https://filecoin.infura.io";
    

    public final static String FIL_METHOD_CHAINGETTIPSETBYHEIGHT =   "Filecoin.ChainGetTipSetByHeight";
    public final static String FIL_METHOD_CHAINGETBLOCKMESSAGES = "Filecoin.ChainGetBlockMessages";
    public final static String FIL_METHOD_CHAINHEAD = "Filecoin.ChainHead";
}

 2、fil的http请求工具类(主要配置权限认证)

import com.alibaba.fastjson.JSONObject;
import com.alpha.fil.mining.pool.common.util.filChain.config.FilApiConfig;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.Credentials;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class FilHttpUtils {


    private static RestTemplate restTemplate;
    private static ObjectMapper objectMapper = new ObjectMapper();
    private static HttpHeaders headers = new HttpHeaders();
    private static JSONObject jsonObject = new JSONObject();

    static {
        SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();
        clientHttpRequestFactory.setConnectTimeout(20 * 1000);
        clientHttpRequestFactory.setReadTimeout(20 * 1000);
        restTemplate = new RestTemplate(clientHttpRequestFactory);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        headers.add("Content-Type", "application/json");
        //Infura提供的访问点需要http basic身份认证
       String credential = Credentials.basic("项目ID", "秘钥");

        //如果是自己部署的fil节点,则在节点配置上拿到请求秘钥,配置如下
         //String credential = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJyZWFkIiwid3JpdGUiLCJzaWduIiwiYWRtaW4iXX0.eqQKY2EE9dgQ3f6PgEM7B-IEPLMYWJpkOZkUf-BemhE";


        headers.add("Authorization", credential);

        jsonObject.put("jsonrpc", "2.0");
        jsonObject.put("id", 1);
    }

    public static Optional<String> post(List<Object> object, String method) {
        jsonObject.put("method", method);
        jsonObject.put("params", object);
        HttpEntity<String> httpEntity = new HttpEntity<>(jsonObject.toString(), headers);
        ResponseEntity<String> mapResponseEntity = restTemplate.postForEntity(FilApiConfig.FIL_RPC_URL, httpEntity, String.class);
        return Optional.ofNullable(mapResponseEntity).filter(x -> x.getStatusCode() == HttpStatus.OK).map(x -> x.getBody());
    }

    public static Optional<String> get(String url) {
        try {
            ResponseEntity<String> mapResponseEntity = restTemplate.getForEntity(url, String.class);
            return Optional.ofNullable(mapResponseEntity).filter(x -> x.getStatusCode() == HttpStatus.OK).map(x -> x.getBody());
        } catch (RestClientException e) {
            e.printStackTrace();
        }
        return Optional.empty();
    }

  
}

3、fil的操作类(包含充提操作,测试例子都在main函数中)

package com.canye.fil.demo.filChain;

import cn.hutool.core.codec.Base32;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.HexUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.canye.fil.demo.filChain.blake.Blake2b;
import com.canye.fil.demo.filChain.config.FilecoinCnt;
import com.canye.fil.demo.filChain.crypto.ECKey;
import com.canye.fil.demo.filChain.handler.TransactionHandler;
import com.canye.fil.demo.filChain.vo.*;
import org.apache.commons.lang3.StringUtils;


import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * fil的工具类
 * @author plb
 * @date 2021-06-04 14:50
 */
public class FilCoinUtil {
    /**
     * 获取余额
     * @param address  地址
     * @return
     */
    public static BigDecimal  getBalance(String address){
        try{
            String method = "Filecoin.WalletBalance";
            List<Object> object = new ArrayList<>();
            object.add(address);
            Optional optional = FilHttpUtils.post(object,method);
            JSONObject dataJson = JSONObject.parseObject(String.valueOf(optional.get()));
            System.out.println("FilCoinUtil.getBalance " + address + " | res:" + dataJson);
            BigDecimal amount = dataJson.getBigDecimal("result");
            amount = amount.divide(BigDecimal.TEN.pow(18));
            return amount;
        }catch (Exception e){
            System.out.println("FilCoinUtil.getBalance: " + address + e.getMessage());
            throw  new RuntimeException("获取余额失败",e);
        }
    }

    /**
     * 检测地址是否有效
     * @param address    地址
     * @return
     */
    public static boolean checkAddress(String address){
        try{
            String method = "Filecoin.WalletValidateAddress";
            List<Object> object = new ArrayList<>();
            object.add(address);
            Optional optional = FilHttpUtils.post(object,method);
            JSONObject dataJson = JSONObject.parseObject(String.valueOf(optional.get()));
            System.out.println("FilCoinUtil.checkAddress " + address + "|res:" + dataJson);
            if(dataJson.containsKey("result") && dataJson.getString("result").equals(address)) {
                return true;
            }
            return false;
        }catch (Exception e){
            System.out.println("FilCoinUtil.checkAddress: " + address+ e.getMessage());
            throw  new RuntimeException("检验地址失败",e);
        }
    }

    /**
     * 生成地址
     * @return
     */
    public static WalletResult createWallet() {
        try {
            ECKey ecKey = new ECKey();
            byte[] privKeyBytes = ecKey.getPrivKeyBytes();
            byte[] pubKey = ecKey.getPubKey();
            if (privKeyBytes == null || privKeyBytes.length < 1) {
                throw new RuntimeException("create wallet error");
            }
            String filAddress = byteToAddress(pubKey);
            String privatekey = HexUtil.encodeHexStr(privKeyBytes);
            return WalletResult.builder().address(filAddress).privatekey(privatekey).build();
        }catch (Exception e){
            System.out.println("createWallet error"+ e.getMessage());
            throw  new RuntimeException("生成地址失败!");
        }
    }

    /**
     * 字节转地址
     * @param pub
     * @return
     */
    private static String byteToAddress(byte[] pub) {
        Blake2b.Digest digest = Blake2b.Digest.newInstance(20);
        String hash = HexUtil.encodeHexStr(digest.digest(pub));

        //4.计算校验和
        String pubKeyHash = "01" + HexUtil.encodeHexStr(digest.digest(pub));

        Blake2b.Digest blake2b3 = Blake2b.Digest.newInstance(4);
        String checksum = HexUtil.encodeHexStr(blake2b3.digest(HexUtil.decodeHex(pubKeyHash)));
        //5.生成地址

        return "f1" + Base32.encode(HexUtil.decodeHex(hash + checksum)).toLowerCase();
    }


    /**
     * 根据区块链高度获取区块链信息
     */
    public static ChainResult getChainTipSetByHeight(BigInteger heigth){
        try{
            String method = "Filecoin.ChainGetTipSetByHeight";
            List<Object> object = new ArrayList<>();
            object.add(heigth);
            object.add(null);
            Optional optional = FilHttpUtils.post(object,method);
            JSONObject dataJson = JSONObject.parseObject(String.valueOf(optional.get()));
            System.out.println("FilCoinUtil.getChainTipSetByHeight " + heigth + " | res:" + dataJson);
            ChainResult chainResult = chainJsonToChainResult(dataJson);
            return chainResult;
        }catch (Exception e){
            System.out.println("FilCoinUtil.getChainTipSetByHeight: " + heigth+ e.getMessage());
            throw  new RuntimeException("getChainTipSetByHeight error",e);
        }
    }

    /**
     * 区块链json数据转对象
     * @param jsonObject
     * @return
     */
    private static ChainResult chainJsonToChainResult(JSONObject jsonObject){
        JSONObject result = jsonObject.getJSONObject("result");
        BigInteger height = result.getBigInteger("Height");
        ArrayList<String> cidList = new ArrayList<>();
        ArrayList<String> parentCidList = new ArrayList<>();
        JSONArray cidJsonArr = result.getJSONArray("Cids");
        JSONArray blocksArr = result.getJSONArray("Blocks");
        if (cidJsonArr != null){
            for (Object o : cidJsonArr) {
                cn.hutool.json.JSONObject cidKy = new cn.hutool.json.JSONObject(o);
                String cid = cidKy.getStr("/");
                if (!StringUtils.isEmpty(cid)){
                    cidList.add(cid);
                }
            }
        }
        if (blocksArr != null && blocksArr.size() > 0){
            JSONArray cidArr =  JSONObject.parseObject(String.valueOf(blocksArr.get(0))).getJSONArray("Parents");
            if (cidArr != null){
                for (Object o : cidArr) {
                    String cid = new cn.hutool.json.JSONObject(o).getStr("/");
                    if (!StringUtils.isEmpty(cid)){
                        parentCidList.add(cid);
                    }
                }
            }
        }
        return ChainResult.builder().height(height).blockCidList(cidList).parentBlockCidList(parentCidList).build();
    }


    /**
     * 根据消息cid获取消息详情
     * @param cid 消息cid
     */
    public static MessagesResult getMessageByCid(String cid){
        try{
            String method = "Filecoin.ChainGetMessage";
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("/", cid);
            List<Object> object = new ArrayList<>();
            object.add(jsonObject);
            Optional optional = FilHttpUtils.post(object,method);
            JSONObject dataJson = JSONObject.parseObject(String.valueOf(optional.get()));
            System.out.println(optional.get());
            System.out.println("FilCoinUtil.getTradeMessageByCid " + cid + " | res:" + dataJson);
            System.out.println(dataJson);
            MessagesResult messagesResult = messagesJsonToMessagesResult(dataJson.getJSONObject("result"));
            return messagesResult;
        }catch (Exception e){
            System.out.println("FilCoinUtil.getTradeMessageByCid: " + cid + e.getMessage());
            throw  new RuntimeException("获取消息详情失败",e);
        }
    }

    /**
     * 消息json转为消息对象
     * @param jsonObject
     * @return
     */
    private static MessagesResult messagesJsonToMessagesResult(JSONObject jsonObject){
        return MessagesResult.builder().from(jsonObject.getString("From"))
                .to(jsonObject.getString("To"))
                .version(jsonObject.getInteger("Version"))
                .nonce(jsonObject.getInteger("Nonce"))
                .value(Convert.fromAtto(jsonObject.getBigInteger("Value").toString(), Convert.Unit.FIL))
                .gasLimit(jsonObject.getInteger("GasLimit"))
                .gasFeeCap(jsonObject.getBigInteger("GasFeeCap"))
                .gasPremium(jsonObject.getBigInteger("GasPremium"))
                .method(jsonObject.getInteger("Method"))
                .params(jsonObject.getString("Params"))
                .cid(jsonObject.getJSONObject("CID")
                        .getString("/"))
                .build();
    }


    /**
     * 根据区块cid获取区块内所有消息
     * @param blockCid 区块id
     * @return
     */
    public static List<MessagesResult>  getMessagesByBlockCid(String blockCid){
        try{
            String method = "Filecoin.ChainGetBlockMessages";
            JSONObject _cid = new JSONObject();
            _cid.put("/", blockCid);
            List<Object> object = new ArrayList<>();
            object.add(_cid);
            Optional optional = FilHttpUtils.post(object,method);
            JSONObject dataJson = JSONObject.parseObject(String.valueOf(optional.get()));
            System.out.println("FilCoinUtil.getMessagesByBlockCid " + blockCid + " | res:" + dataJson);
            System.out.println(dataJson.toJSONString());
            ArrayList<MessagesResult> messagesResults = new ArrayList<>();
            JSONObject resultJson = dataJson.getJSONObject("result");
            if(resultJson == null){
                resultJson = new JSONObject();
            }
            JSONArray blsMessagesArr = resultJson.getJSONArray("BlsMessages");
            JSONArray secpkMessagesArr = resultJson.getJSONArray("SecpkMessages");
            if (blsMessagesArr != null){
                for (Object o : blsMessagesArr) {
                    messagesResults.add(messagesJsonToMessagesResult(JSONObject.parseObject(String.valueOf(o))));
                }
            }
            if (secpkMessagesArr != null){
                for (Object o : secpkMessagesArr) {
                    JSONObject secpkMessagesObj = JSONObject.parseObject(String.valueOf(o));
                    JSONObject message = secpkMessagesObj.getJSONObject("Message");
                    message.put("CID", secpkMessagesObj.get("CID"));
                    messagesResults.add(messagesJsonToMessagesResult(message));
                }
            }

            return messagesResults;

        }catch (Exception e){
            System.out.println("FilCoinUtil.getMessagesByBlockCid: " + blockCid + e.getMessage());
            throw  new RuntimeException("获取区块内所有消息",e);
        }
    }



    /**
     * 获取指定高度的所有消息
     * @param height
     * @return
     */
    public static ChainMessagesResult getAllMessagesByHeight(BigInteger height) {
        ChainMessagesResult res = null;
        ChainResult chainTipSet = getChainTipSetByHeight(height);
        if (chainTipSet != null && chainTipSet.getBlockCidList() != null){
            res = ChainMessagesResult.builder().blockCidList(chainTipSet.getBlockCidList()).build();
            ArrayList<MessagesResult> messageList = new ArrayList<>();
            for (String blockCid : chainTipSet.getBlockCidList()) {
                List<MessagesResult> messagesList = getMessagesByBlockCid(blockCid);
                messageList.addAll(messagesList);
            }
            //消息去重
            List list = messageList.stream().distinct().collect(Collectors.toList());
            res.setMessageList(list);
        }
        return res;
    }


    /**
     * 获取nonce值
     * @param address  地址
     * @return
     */
    public static int getNonce(String address) {

        try{
            String method = FilecoinCnt.GET_NONCE;

            List<Object> object = new ArrayList<>();
            object.add(address);
            Optional optional = FilHttpUtils.post(object,method);
            JSONObject dataJson = JSONObject.parseObject(String.valueOf(optional.get()));
            System.out.println("FilCoinUtil.getNonce " + address + " | res:" + dataJson);
            return dataJson.getIntValue("result");

        }catch (Exception e){
            System.out.println("FilCoinUtil.getNonce: " + address + e.getMessage());
            throw  new RuntimeException("获取Nonce失败",e);
        }

    }


    /**
     * 获取gas信息
     * @param gas
     * @return
     */
    public  static GasResult getGas(GetGas gas)  {
        if (gas == null || StringUtils.isBlank(gas.getFrom())
                || StringUtils.isBlank(gas.getTo())
                || gas.getValue() == null) {
            throw new RuntimeException("paramter cannot be empty");
        }
        if (gas.getValue().compareTo(BigInteger.ZERO) < 1) {
            throw new RuntimeException("the transfer amount must be greater than 0 !" + JSONObject.toJSONString(gas));
        }
        List<Object> list = new ArrayList<>();
        JSONObject json = new JSONObject();
        json.put("From", gas.getFrom());
        json.put("To", gas.getTo());
        json.put("Value", gas.getValue().toString());
        list.add(json);
        list.add(null);
        list.add(null);
        Optional optional = FilHttpUtils.post(list,FilecoinCnt.GET_GAS);

        GasResult gasResult = null;
        try {
            JSONObject result = JSONObject.parseObject(String.valueOf(optional.get()));
            JSONObject jsonObject = result.getJSONObject("result");
            gasResult = GasResult.builder().gasFeeCap(jsonObject.getString("GasFeeCap"))
                    .gasLimit(jsonObject.getBigInteger("GasLimit"))
                    .gasPremium(jsonObject.getString("GasPremium")).build();
            return gasResult;
        } catch (Exception e) {
            System.out.println("FilCoinUtil.getGas " + e.getMessage());
            throw new RuntimeException("get gas error " ,e);
        }

    }




    public static SendResult send(Transaction transaction, String privatekey) throws RuntimeException {
        if (transaction == null || StringUtils.isBlank(transaction.getFrom())
                || StringUtils.isBlank(transaction.getTo())
                || StringUtils.isBlank(transaction.getGasFeeCap())
                || StringUtils.isBlank(transaction.getGasPremium())
                || StringUtils.isBlank(transaction.getValue())
                || transaction.getGasLimit() == null
                || transaction.getMethod() == null
                || transaction.getNonce() == null
                || StringUtils.isBlank(privatekey)) {
            throw new RuntimeException("parameter cnanot be empty");
        }
        BigInteger account = new BigInteger(transaction.getValue());
        if (account.compareTo(BigInteger.ZERO) < 1) {
            throw new RuntimeException("the transfer amount must be greater than 0");
        }
        byte[] cidHash = null;
        try {
            cidHash = TransactionHandler.transactionSerialize(transaction);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("transaction entity serialization failed");
        }
        //签名
        ECKey ecKey = ECKey.fromPrivate(HexUtil.decodeHex(privatekey));
        String sing = Base64.encode(ecKey.sign(cidHash).toByteArray());

        List<Object> list = new ArrayList<>();
        JSONObject signatureJson = new JSONObject();
        JSONObject messageJson = JSONObject.parseObject(JSONObject.toJSONString(transaction));
        JSONObject json = new JSONObject();
        messageJson.put("version", 0);
        signatureJson.put("data", sing);
        signatureJson.put("type", 1);
        json.put("message", messageJson);
        json.put("signature", signatureJson);

        list.add(json);

        SendResult build = null;
        Optional optional = null;
        try {
             optional = FilHttpUtils.post(list,FilecoinCnt.BOARD_TRANSACTION);

            JSONObject executeJson = JSONObject.parseObject(String.valueOf(optional.get()));
            String result = executeJson.getJSONObject("result").getString("/");
            build = SendResult.builder().cid(result)
                    .nonce(transaction.getNonce()).build();
            return build;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("send error " + optional , e);
        }

    }

    public static  SendResult easySend(EasySend send)  {
        if (send == null || StringUtils.isBlank(send.getFrom())
                || StringUtils.isBlank(send.getTo())
                || StringUtils.isBlank(send.getPrivatekey())
                || send.getValue() == null) {
            throw new RuntimeException("parameter cannot be empty");
        }

        BigDecimal amount = Convert.toAtto(send.getValue(), Convert.Unit.FIL);
        BigInteger bigInteger = amount.toBigInteger();
        //获取gas
        GasResult gas = getGas(GetGas.builder().from(send.getFrom())
                .to(send.getTo())
                .value(bigInteger).build());

        //获取nonce
        int nonce = getNonce(send.getFrom());
        //拼装交易参数
        Transaction transaction = Transaction.builder().from(send.getFrom())
                .to(send.getTo())
                .gasFeeCap(gas.getGasFeeCap())
                .gasLimit(gas.getGasLimit().longValue() * 2)
                .gasPremium(gas.getGasPremium())
                .method(0L)
                .nonce((long) nonce)
                .params("")
                .value(bigInteger.toString()).build();

        return send(transaction, send.getPrivatekey());
    }


    /**
     * 获取消息收据
     * @param messageCid  消息id
     * @return
     */
    public static StateGetReceiptResult stateGetReceipt(String messageCid) {

        List<Object> list = new ArrayList<>();
        HashMap<String, String> cidParam = new HashMap<>(8);
        cidParam.put("/", messageCid);
        list.add(cidParam);
        list.add(null);

        StateGetReceiptResult res = null;
        JSONObject jsonObject = null;
        try {
            Optional optional = FilHttpUtils.post(list,FilecoinCnt.STATE_GET_RECEIPT);
            jsonObject = JSONObject.parseObject(String.valueOf(optional.get()));
            JSONObject result = jsonObject.getJSONObject("result");
            res = StateGetReceiptResult.builder().exitCode(result.getInteger("ExitCode"))
                    .messageReturn(result.getString("Return"))
                    .gasUsed(result.getBigInteger("GasUsed"))
                    .build();
            return res;
        } catch (Exception e) {
            System.out.println("stateGetReceipt error messgeid:" + messageCid + ",jsonObject=" + JSON.toJSONString(jsonObject) + e.getMessage());
            throw new RuntimeException("stateGetReceipt error " +  messageCid + ",jsonObject=" + JSON.toJSONString(jsonObject) + e.getMessage());
        }
    }


    public static  void main(String[] args){
        //获取地址余额
        //System.out.println(getBalance("f1l3aboc6csauxuqrb2mftpvsjahuli4gxro6tllq"));
        //校验地址
       // System.out.println(checkAddress("f1l3aboc6csauxuqrb2mftpvsjahuli4gxro6tll2"));
        //测试生成地址
        //System.out.println(createWallet());


          //获取某个高度的所有消息(充值到账)
//        ChainMessagesResult chainMessagesResult = getAllMessagesByHeight(BigInteger.valueOf(1142577L));
//        for(MessagesResult messagesResult:chainMessagesResult.getMessageList()){
//
//            //判断消息是否是交易信息,等于0
//            if(messagesResult.getMethod().intValue() == 0) {
//                //校验交易是否正常
//                StateGetReceiptResult stateGetReceiptResult = FilCoinUtil.stateGetReceipt(messagesResult.getCid());
//                if(stateGetReceiptResult.getExitCode().intValue() == 0 ) {
//                    //到账地址
//                    String toAddress = messagesResult.getTo();
//                    //交易hash
//                    String hash = messagesResult.getCid();
//                    //todo 业务处理,充值到账
//                }
//            }
//        }

        //测试提币上链
//        //私钥
//        String privatekey = "fil秘钥";
//        EasySend easySend = new EasySend();
//        //来源地址
//        easySend.setFrom("f1sle4slx2xqap7p2ehatextarj5nlgeilclgs723");
//        //目标地址
//        easySend.setTo("f1sle4slx2xqap7p2ehatextarj5nlgeilclgs7qi");
//        easySend.setPrivatekey(privatekey);
//        //转账金额
//        BigDecimal amount = new BigDecimal("0.0001");
//        easySend.setValue(amount);
//        System.out.println(easySend(easySend));



    }
}

如在使用的过程中,有问题,麻烦发信息到邮箱408337459@qq.com

以上是关于Java FilCoin的充值以及上链操作详情流程(附有代码)的主要内容,如果未能解决你的问题,请参考以下文章

Fabric上链流程

2019最新苹果手游充值退款流程详细解读

交易所撮合引擎架构设计

iOS用户是怎样充值抖音币的:来这里就对了

关注公众号的微信用户收到非本人操作的充值消费记录,故障记录

一个支付流程要考虑到哪些测试点?