钱包开发经验分享:omniUSDT篇

钱包开发经验分享:omniUSDT篇

omni USDT

想要深入了解omni协议的代币,首先就必须了解omni协议的历史,可以参考认识omni协议——USDT。文章提到BTC本身是现金系统,不支持智能合约,为了让BTC支持智能合约,在BTC之上又开发了omni的公链,因此omni协议是依赖于BTC的。简单了解了omni公链是基于BTC公链的,那么他们之间是如何关联起来的呢?首先可以参考廖雪峰的深入理解比特币交易的脚本,文章中提到了比特币公链一些交易的脚本命令的执行,其实omni协议的交易数据也是通过OP_Return脚本命令存储在区块链上,也就是每一笔基于omni协议的代币的交易,都是依赖于一笔小额的比特币交易上的,这笔交易的实际交易数据被写进了OP_Return脚本中,这些数据比特币无法正确解析,这时候就由依赖于比特币网络的上层网络协议omni 协议来解析。所以说omni协议层是在BTC网络上增加了智能合约的功能。因此,omniUSDT的地址可以直接使用BTC的地址, 而USDT的交易,是创建一笔BTC交易,在之上又附加一笔USDT的交易。

omniUSDT转账

参考代码:

    /**
     *  usdt 离线签名
     *  @param privateKey:私钥
     *  @param toAddress:接收地址
     *  @param amount:转账金额
     *  @return
     *      
     */
    public static String signTransaction(String fromAddress, String toAddress, String changeAddress, Long amount, boolean isMainNet, int propertyId, String privateKey) throws Exception {
        List<UTXO> utxos = getUnspent(fromAddress, isMainNet);
        // 获取手续费
        Long fee = getFee(546L, utxos);
        // 判断是主链试试测试链
        NetworkParameters networkParameters = isMainNet ? MainNetParams.get() : TestNet3Params.get();
        Transaction tran = new Transaction(networkParameters);
        if (utxos == null || utxos.size() == 0) {
            throw new Exception("utxo为空");
        }
        //这是比特币的限制最小转账金额,所以很多usdt转账会收到一笔0.00000546的btc
        Long miniBtc = 546L;
        tran.addOutput(Coin.valueOf(miniBtc), Address.fromBase58(networkParameters, toAddress));

        //构建usdt的输出脚本 注意这里的金额是要乘10的8次方
        String usdtHex = "6a146f6d6e69" + String.format("%016x", propertyId) + String.format("%016x", amount);
        tran.addOutput(Coin.valueOf(0L), new Script(Utils.HEX.decode(usdtHex)));

        Long changeAmount = 0L;
        Long utxoAmount = 0L;
        List<UTXO> needUtxo = new ArrayList<>();
        //过滤掉多的uxto
        for (UTXO utxo : utxos) {
            if (utxoAmount > (fee + miniBtc)) {
                break;
            } else {
                needUtxo.add(utxo);
                utxoAmount += utxo.getValue().value;
            }
        }
        changeAmount = utxoAmount - (fee + miniBtc);
        //余额判断
        if (changeAmount < 0) {
            throw new Exception("utxo余额不足");
        }
        if (changeAmount > 0) {
            tran.addOutput(Coin.valueOf(changeAmount), Address.fromBase58(networkParameters, changeAddress));
        }

        //先添加未签名的输入,也就是utxo
        for (UTXO utxo : needUtxo) {
            tran.addInput(utxo.getHash(), utxo.getIndex(), utxo.getScript()).setSequenceNumber(TransactionInput.NO_SEQUENCE - 2);
        }


        //下面就是签名
        for (int i = 0; i < needUtxo.size(); i++) {
            //这里获取地址
            String addr = needUtxo.get(i).getAddress();

            ECKey ecKey = DumpedPrivateKey.fromBase58(networkParameters, privateKey).getKey();
            TransactionInput transactionInput = tran.getInput(i);
            Script scriptPubKey = ScriptBuilder.createOutputScript(Address.fromBase58(networkParameters, addr));
            Sha256Hash hash = tran.hashForSignature(i, scriptPubKey, Transaction.SigHash.ALL, false);
            ECKey.ECDSASignature ecSig = ecKey.sign(hash);
            TransactionSignature txSig = new TransactionSignature(ecSig, Transaction.SigHash.ALL, false);
            transactionInput.setScriptSig(ScriptBuilder.createInputScript(txSig, ecKey));
        }

        //这是签名之后的原始交易,直接去广播就行了
        String signedHex = Hex.toHexString(tran.bitcoinSerialize());
        //这是交易的hash
        String txHash = Hex.toHexString(Utils.reverseBytes(Sha256Hash.hash(Sha256Hash.hash(tran.bitcoinSerialize()))));
        return signedHex;
    }

关于propertyId可以参考区块链浏览器,从区块链浏览器可以看到,USDT在omni协议中的propertyId是31。propertyId可以认为是omni协议实现的代币的主键,每个token的propertyId是不同的,而omniUSDT的propertyId是31。签名的结果是原始交易的Hex字符串,需要广播出去这笔交易才会被网络认可,广播交易可以用推送原始交易端点进行广播,有一些区块链浏览器也提供了广播交易的API,可以去翻找一下。

指定支付矿工费地址

在做代币归集的时候,代币支付的矿工费一般都是公链的币种,例如erc20USDT的转账是使用ETH支付矿工费的,而omniUSDT则是使用BTC支付矿工费,omniUSDT矿工费的支付在归集的面前成了一个棘手的难题。一般在做归集的时候会发生两次转账,一次是打入矿工费的转账,一次是归集的转账,而在omniUSDT归集的时候,这两次转账所支付的矿工费有时候是多余的,因为omniUSDT基于BTC公链,BTC公链本身就拥堵,矿工费又高,这样频繁归集来回打手续费,支出了太多不必要的矿工费。一般会有两个解决方案,一个是omniUSDT和BTC账户直接不做归集,需要的时候在手动归集,另一个设置一个打手续费的门槛和归集的门槛,当账户余额不大于某个阈值不考虑归集。其实还有第三种做法,指定支付矿工费地址,参考USDT钱包归集

参考代码:

    /**
     *
     * @param privBtcKey 支付矿工费地址私钥
     * @param btcAddress 支付矿工费地址
     * @param privUsdtKey 支付USDT地址私钥
     * @param usdtAddress 支付USDT地址
     * @param recevieUsdtAddr 接收USDT地址
     * @param amount 数量
     * @param propertyId token唯一标识
     * @param isMainNet 是否正式网络
     * @return
     */
    public static String createRawTransaction(String privBtcKey, String btcAddress, String privUsdtKey, String usdtAddress, String recevieUsdtAddr,  long amount, int propertyId, boolean isMainNet) {

        // 获取未花费列表
        List<UTXO> unBtcUtxos = getUnspentFromTestNet(btcAddress);
        List<UTXO> unUsdtUtxos = getUnspentFromTestNet(usdtAddress);

        // 优化列表
        Collections.sort(unBtcUtxos, (o1, o2) -> BigInteger.valueOf(o2.getValue().value).compareTo(BigInteger.valueOf(o1.getValue().value)));


        List<UTXO> btcUtxos = new ArrayList<>();
        List<UTXO> usdtUtxos = new ArrayList<>();

        // 获取手续费
        Long fee = getFee(546L, unBtcUtxos);

        // 过滤输入
        long btcTotalMoney = 0;
        long usdtTotalMoney = 0;
        for (UTXO us : unBtcUtxos) {
            if (btcTotalMoney >= (1092L + fee))
                break;
            UTXO utxo = new UTXO(us.getHash(), us.getIndex(), us.getValue(), us.getHeight(), us.isCoinbase(), us.getScript());
            btcUtxos.add(utxo);
            btcTotalMoney += us.getValue().value;
        }
        for (UTXO us : unUsdtUtxos) {
            if (usdtTotalMoney >= 546L)
                break;
            UTXO utxo = new UTXO(us.getHash(), us.getIndex(), us.getValue(), us.getHeight(), us.isCoinbase(), us.getScript());
            usdtUtxos.add(utxo);
            usdtTotalMoney += us.getValue().value;
        }

        // 判断是测试链还是正式链
        NetworkParameters networkParameters = isMainNet ? MainNetParams.get() : TestNet3Params.get();
        try {
            if (!btcUtxos.isEmpty() && !usdtUtxos.isEmpty()) {
                // find a btc eckey info
                DumpedPrivateKey btcPrivateKey = DumpedPrivateKey.fromBase58(networkParameters, privBtcKey);
                ECKey btcKey = btcPrivateKey.getKey();
                // a usdt eckey info
                DumpedPrivateKey usdtPrivateKey = DumpedPrivateKey.fromBase58(networkParameters, privUsdtKey);
                ECKey usdtKey = usdtPrivateKey.getKey();

                // receive address
                Address receiveAddress = Address.fromBase58(networkParameters, recevieUsdtAddr);
                // create a transaction
                Transaction tx = new Transaction(networkParameters);
                // odd address
                Address oddAddress = Address.fromBase58(networkParameters, btcAddress);
                // 如果需要找零 消费列表总金额 - 已经转账的金额 - 手续费
                long value_btc = btcUtxos.stream().mapToLong(x -> x.getValue().value).sum();
                long value_usdt = usdtUtxos.stream().mapToLong(x -> x.getValue().value).sum();
                // 总输入 - 手续费 - 546 -546 = 找零金额
                long leave = (value_btc + value_usdt) - fee - 1092;
                if (leave > 0) {
                    tx.addOutput(Coin.valueOf(leave), oddAddress);
                }

                String usdtHex = "6a146f6d6e69" + String.format("%016x", propertyId) + String.format("%016x", amount);

                // usdt transaction
                tx.addOutput(Coin.valueOf(546), new Script(Utils.HEX.decode(usdtHex)));
                // send to address
                tx.addOutput(Coin.valueOf(546), receiveAddress);

                // create usdt utxo data
                for (UTXO utxo : usdtUtxos) {
                    TransactionOutPoint outPoint = new TransactionOutPoint(networkParameters, utxo.getIndex(), utxo.getHash());
                    tx.addSignedInput(outPoint, utxo.getScript(), usdtKey, Transaction.SigHash.ALL, true);
                }

                for (UTXO utxo : btcUtxos) {
                    TransactionOutPoint outPoint = new TransactionOutPoint(networkParameters, utxo.getIndex(), utxo.getHash());
                    tx.addSignedInput(outPoint, utxo.getScript(), btcKey, Transaction.SigHash.ALL, true);
                }

                new Context(networkParameters);
                tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK);
                tx.setPurpose(Transaction.Purpose.USER_PAYMENT);

                return Hex.toHexString(tx.bitcoinSerialize());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

测试代码:

    @Test
    public void sendUsdtSpecifyMineAddress(){
        String btcAddress = "mtESbeXpf5qYkbYnphhKEJ7FU3UyQKYQzy";
        String btcPrivateKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
        String usdtAddress = "mnvDACAJgny2yxxHNhud96sq9KRazbEeX2";
        String usdtPrivateKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
        String receiveAddress = "miiJAkDV4zfiZhSMwtiMCZr2rr4UecToMz";
        String signTx = BtcUtil.createRawTransaction(btcPrivateKey, btcAddress, usdtPrivateKey, usdtAddress, receiveAddress, 100000000, 1, false);
        String response = BtcUtil.sendTx(signTx, false);
        logger.warn("response: {}", response);
    }

测试结果:

指定矿工费地址.png

这笔交易是在测试网络进行的,测试代币的获取可以通过向moneyqMan7uh8FqdCA2BV5yZ8qVrc9ikLP转入一些测试比特币,等待交易被确认就会获得对应的omni代币,兑换比例是1TBTC = 100OMNI。现在从这笔交易出发,可以看到这笔交易有两个输入,三个输出。两个输入分别是一个输入由一笔最小额度的BTC转账携带USDT转账,另一个输入则是支付手续费。

获取USDT余额

对于搭建了节点的朋友要获取token余额是一件很容易的事,直接调用RPC接口就可以获取,关于omni的RPC接口可以参看比特币核心omni api速查表 原,下面是使用第三方浏览器获取USDT余额的参考例子。

参考代码:

    /**
     * 获取USDT余额
     * @param address
     * @return
     */
    public static String getUsdtBalance(String address){
        String url = "https://api.omniexplorer.info/v1/address/addr/";
        OkHttpClient client = new OkHttpClient();
        String content = "addr=" + address;
        String response = null;
        try {
            response = client.newCall(new Request.Builder().url(url).post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), content)).build()).execute().body().string();
        } catch (IOException e) {
            e.printStackTrace();
        }
        JSONObject jsonObject = JSONObject.parseObject(response);
        JSONArray balances = jsonObject.getJSONArray("balance");
        List<Object> result = balances.parallelStream().filter(o -> JSONObject.parseObject(JSONObject.toJSONString(o)).getString("id").equals("31")).collect(Collectors.toList());
        if(CollectionUtils.isEmpty(result)){
            return BigDecimal.ZERO.toPlainString();
        }
        return new BigDecimal(JSONObject.parseObject(JSONObject.toJSONString(result.get(0))).getString("value")).divide(new BigDecimal("100000000")).toPlainString();
    }

测试代码:

    @Test
    public void testGetUsdtBalance(){
        String address = "1Po1oWkD2LmodfkBYiAktwh76vkF93LKnh";
        String balance = BtcUtil.getUsdtBalance(address);
        logger.warn("balance: {}", balance);
    }

上面例子里通过/v1/address/addr/details/接口查询的结果包括了所有的token余额,而omniUSDT的propertyid为31,propertyid是基于omni协议发布的代币的唯一字段,在omni协议层的发布的代币,目前只有omniUSDT使用比较广泛。

对我的文章感兴趣的话,请关注我的公众号

公众号二维码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容