Wallet development experience sharing: omniUSDT

Keywords: Programming network Blockchain

omni USDT

To understand the tokens of the omni protocol in depth, you must first understand the history of the omni protocol. Please refer to Understanding the omni protocol -- USDT . BTC itself is a cash system and does not support smart contracts. In order for BTC to support smart contracts, omni public chain is developed on top of BTC, so omni protocol is dependent on BTC. After a brief understanding of omni public chain is based on BTC public chain, how are they related? First of all, we can refer to Liao Xuefeng's In depth understanding of bitcoin transaction script In this paper, the execution of some transaction script commands in the bitcoin public chain is mentioned. In fact, the transaction data of the omni protocol is also stored in the blockchain through the op'return script command. That is to say, every transaction of token based on the omni protocol depends on a small bitcoin transaction. The actual transaction data of this transaction is written into the op'return script. These numbers Since bitcoin can't be parsed correctly, it is parsed by the upper layer network protocol Omni protocol which depends on bitcoin network. So the omni protocol layer adds the function of smart contract on BTC network. Therefore, the address of omniUSDT can directly use the address of BTC, and the transaction of USDT is to create a BTC transaction and attach another transaction of USDT.

omniUSDT transfer

Reference code:

    /**
     *	usdt Offline signature
     *  @param privateKey: Private key
     *  @param toAddress: Receiving address
     *  @param amount:Transfer 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);
        // Service charge for acquisition
        Long fee = getFee(546L, utxos);
        // Judge is the main chain try the test chain
        NetworkParameters networkParameters = isMainNet ? MainNetParams.get() : TestNet3Params.get();
        Transaction tran = new Transaction(networkParameters);
        if (utxos == null || utxos.size() == 0) {
            throw new Exception("utxo Empty");
        }
        //This is the limited minimum transfer amount of bitcoin, so many usdt transfers will receive a btc of 0.00000546
        Long miniBtc = 546L;
        tran.addOutput(Coin.valueOf(miniBtc), Address.fromBase58(networkParameters, toAddress));

        //Build the output script of usdt. Note that the amount here is multiplied by 8 times 10
        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<>();
        //Filter out more uxto
        for (UTXO utxo : utxos) {
            if (utxoAmount > (fee + miniBtc)) {
                break;
            } else {
                needUtxo.add(utxo);
                utxoAmount += utxo.getValue().value;
            }
        }
        changeAmount = utxoAmount - (fee + miniBtc);
        //Balance judgement
        if (changeAmount < 0) {
            throw new Exception("utxo Sorry, your credit is running low");
        }
        if (changeAmount > 0) {
            tran.addOutput(Coin.valueOf(changeAmount), Address.fromBase58(networkParameters, changeAddress));
        }

        //First add the unsigned input, which is utxo
        for (UTXO utxo : needUtxo) {
            tran.addInput(utxo.getHash(), utxo.getIndex(), utxo.getScript()).setSequenceNumber(TransactionInput.NO_SEQUENCE - 2);
        }


        //Here is the signature
        for (int i = 0; i < needUtxo.size(); i++) {
            //Get the address here
            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));
        }

        //This is the original deal after signing. Just go to the radio
        String signedHex = Hex.toHexString(tran.bitcoinSerialize());
        //This is the hash of the deal
        String txHash = Hex.toHexString(Utils.reverseBytes(Sha256Hash.hash(Sha256Hash.hash(tran.bitcoinSerialize()))));
        return signedHex;
    }

For propertyId, please refer to Blockchain browser , as can be seen from the blockchain browser, the propertyId of USDT in the omni protocol is 31. The propertyId can be considered as the primary key of the token implemented by the omni protocol. The propertyId of each token is different, while the propertyId of omniUSDT is 31. The signature result is the Hex string of the original transaction, which needs to be broadcast to be recognized by the network. Broadcast transactions can be used to Push original transaction endpoint For broadcasting, some blockchain browsers also provide the API for broadcasting transactions, which can be searched.

Designated address for payment of miners' fees

When collecting tokens, the miner's fees paid by tokens are generally public chain currencies. For example, the transfer of erc20USDT uses ETH to pay miner's fees, while the payment of omniUSDT uses BTC to pay miner's fees. The payment of omniUSDT miner's fees becomes a difficult problem in front of the collection. In general, there will be two transfers when collecting, one is the transfer into the miner's fee, and the other is the transfer from collection. When collecting omniUSDT, the miner's fee paid by the two transfers is sometimes redundant. Because omniUSDT is based on the BTC public chain, the BTC public chain itself is congested, and the miner's fee is high. In this way, frequent collection and back-to-back service fees are paid too much and unnecessary Miners cost. Generally, there are two solutions. One is that the omniUSDT and BTC accounts do not collect directly. When necessary, they are collected manually. The other is to set a threshold for handling charges and collection. When the account balance is not greater than a certain threshold, collection is not considered. In fact, there is a third way to specify the address to pay the miner's fees. Please refer to USDT wallet collection.

Reference code:

    /**
     *
     * @param privBtcKey Private key of address for payment of miner's fee
     * @param btcAddress Address for payment of miners' fees
     * @param privUsdtKey Payment USDT address private key
     * @param usdtAddress Address of payment USDT
     * @param recevieUsdtAddr Receive USDT address
     * @param amount Number
     * @param propertyId token Unique identification
     * @param isMainNet Formal network or not
     * @return
     */
    public static String createRawTransaction(String privBtcKey, String btcAddress, String privUsdtKey, String usdtAddress, String recevieUsdtAddr,  long amount, int propertyId, boolean isMainNet) {

        // Get unused list
        List<UTXO> unBtcUtxos = getUnspentFromTestNet(btcAddress);
        List<UTXO> unUsdtUtxos = getUnspentFromTestNet(usdtAddress);

        // Optimization list
        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<>();

        // Service charge for acquisition
        Long fee = getFee(546L, unBtcUtxos);

        // Filter input
        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;
        }

        // Judge whether it is a test chain or a formal chain
        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);
                // If you need to change the total amount of consumption list - transferred amount - handling fee
                long value_btc = btcUtxos.stream().mapToLong(x -> x.getValue().value).sum();
                long value_usdt = usdtUtxos.stream().mapToLong(x -> x.getValue().value).sum();
                // Total input - handling fee - 546 - 546 = change amount
                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 code:

	@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);
	}

Test results:

This transaction is conducted in the test network. The acquisition of test token can be transferred to some test bitcoin from moneyqman7uh8fqdca2bv5yz8qrc9iklp, and the corresponding Omni token will be obtained after the transaction is confirmed. The exchange ratio is 1TBTC = 100OMNI. Now from this transaction, we can see that this transaction has two inputs and three outputs. The two inputs are: one is carried by a BTC transfer with a minimum amount of USDT transfer, and the other is to pay the handling fee.

Get USDT balance

It's easy for a friend who has built a node to get the token balance. You can get it by calling the RPC interface directly. For the RPC interface of omni, please refer to Bitcoin core omni api quick reference table , the following is a reference example of using a third-party browser to obtain the USDT balance.

Reference code:

    /**
     * Get USDT balance
     * @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 code:

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

In the above example, the result of query through / v1/address/addr/details / interface includes all token balances, while the propertyid of omniUSDT is 31. Propertyid is the only field of tokens published based on omni protocol. Currently, only omniUSDT is widely used for tokens published in omni protocol layer.

If you are interested in my article, please pay attention to my public address.

Posted by Vorotaev on Tue, 21 Jan 2020 01:48:18 -0800