在幣嚴BIZZAN開發數字貨幣交易所的過程當中,一共有兩大難點,一個是高速撮合交易引擎,另外一個是錢包對接,這二者是咱們團隊之前沒有接觸過的。這個系列的文章主要介紹數字貨幣交易所錢包對接實現技術。第一個要對接的是比特幣BTC,由於BTC的提現過程比較簡單,用戶在交易所提交提幣需求,而後由交易所進行審覈,接着進行系統打幣或者人工打幣。java
交易所對接比特幣錢包的方式有兩種:
1. 自建BTC節點
BTC的節點支持經過RPC的方式進行訪問,所以自建一個節點能夠很快速對區塊和交易進行查詢。可是由於BTC節點會同步全部區塊,截至到如今(2018/09/10)的區塊高度已達500000,所以這須要大量的硬盤存儲空間,16年就已須要至少160G。若是每一個幣種都自建節點,耗費的硬盤存儲空間會很大,而且要承擔維護節點的工做。而初創團隊對資金的合理利用要求比較高,所以,咱們捨棄了這種方式,而採用了第二種方式。
2. 第三方API+區塊鏈瀏覽器
經過第三方API能夠對比特的錢包進行操做,如生成密鑰對、打幣等。同時經過區塊鏈瀏覽器提供的開放接口也能夠進行交易的查詢。所以咱們團隊採用了這種方式對接比特幣錢包。數據庫
1.比特幣錢包密鑰生成
這裏須要瞭解的知識點是比特幣錢包:首先、比特幣錢包能夠離線生成,經過一系列的加密運算過程,若是你不嫌麻煩也能夠本身去實現,可是不必重複造輪子;其次、比特幣錢包離線生成也能夠保證不重複。你不用擔憂比特幣地址會用完,由於理論上比特幣錢包能夠生成地址數量比宇宙中全部原子加起來的數量都多,因此擔憂離線生成的錢包地址與別人重複這種擔憂是徹底不必的。apache
2.比特幣網絡中沒有帳戶概念
若是你接觸過以太坊,就知道以太坊有帳戶概念,帳戶這個概念更加貼近於咱們現實生活,有了帳戶就能夠看到餘額。可是比特幣沒有這樣的概念,感興趣的能夠去研究一下比特幣裏的UTXO。比特幣只有交易,那你很好奇,只有流水帳那怎麼知道一個地址的餘額的?比特幣是經過計算你的錢包地址全部轉入與全部轉出之間的差值來計算出來的。你可能還想問,要轉帳給某個地址10個比特幣的過程是怎樣的?首先比特幣網絡會將你當前餘額計算出來,看看夠不夠轉,若是夠,那麼比特幣網絡會把之前轉入你地址的交易所謂輸入,也就是input,再把你轉入到其餘地址的交易做爲輸出,也就是output。咱們從區塊鏈瀏覽器blockchain.info提供的API能夠查詢到下面這樣的結果:json
{ "hash":"b6f6991d03df0e2e04dafffcd6bc418aac66049e2cd74b80f14ac86db1e3f0da", "ver":1, "vin_sz":1, "vout_sz":2, "lock_time":"Unavailable", "size":258, "relayed_by":"64.179.201.80", "block_height, 12200, "tx_index":"12563028", "inputs":[ { "prev_out":{ "hash":"a3e2bcc9a5f776112497a32b05f4b9e5b2405ed9", "value":"100000000", "tx_index":"12554260", "n":"2" }, "script":"76a914641ad5051edd97029a003fe9efb29359fcee409d88ac" } ], "out":[ { "value":"98000000", "hash":"29d6a3540acfa0a950bef2bfdc75cd51c24390fd", "script":"76a914641ad5051edd97029a003fe9efb29359fcee409d88ac" }, { "value":"2000000", "hash":"17b5038a413f5c5ee288caa64cfab35a0c01914e", "script":"76a914641ad5051edd97029a003fe9efb29359fcee409d88ac" } ] }
查詢連接:https://blockchain.info/rawtx/b6f6991d03df0e2e04dafffcd6bc418aac66049e2cd74b80f14ac86db1e3f0da
上面是查詢一個交易(Transaction)查詢到的結果,能夠看到其中int和out包含多條結果。瀏覽器
上圖是交易所充值邏輯的設計圖,下面簡單說一下流程:
首先、咱們經過比特幣錢包API批量生成如10000個比特幣錢包地址。當用戶在交易所中獲取本身的BTC充值地址時,咱們從這10000個地址中爲他分配一個(地址綁定UID)
而後、咱們會事先運行一個監放任務程序,這個程序負責從比特幣區塊鏈瀏覽器獲取最新的區塊信息,而後解析其中的交易涉及到的地址,當發現output中出現咱們已分配的地址時,咱們就爲這個地址綁定的用戶資產表中添加BTC餘額。
最後、提醒用戶充值到帳。網絡
1. 經過第三方錢包API生成比特幣地址:
這裏咱們使用的是開源代碼bitcoinj,經過maven導入這個庫,示例代碼以下maven
import java.io.File; import java.io.IOException; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.wallet.Wallet; /** * 演示生成/保存密鑰對和錢包地址到文件 * @author bizzan.com * */ public class GenerateKeyPair { public static void main(String[] args) { // 如在主網中生成錢包密鑰對,須要改成MainNetParms NetworkParameters params = TestNet3Params.get(); Wallet wallet = null; // 建立一個文件用於保存錢包文件 final File walletFile = new File("test.wallet"); wallet = new Wallet(params); // 循環生成10個密鑰對並添加到錢包 for(int i = 0; i < 10000; i++) { ECKey key = new ECKey(); wallet.importKey(key); } try { // 保存錢包文件 wallet.saveToFile(walletFile); // 打印錢包內容信息 System.out.println(wallet.toString(true, true, true, null)); } catch (IOException e) { e.printStackTrace(); } } }
經過這段代碼就能夠生成10000個錢包地址,你能夠把它保存在文件裏。實際應用時,你還須要把10000個錢包地址保存到數據庫中,可是私鑰千萬不要保存到聯網數據庫中。
經過下面的代碼能夠讀取你保存到文件裏的錢包內容:區塊鏈
import java.io.File; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.wallet.UnreadableWalletException; import org.bitcoinj.wallet.Wallet; /** * 從文件導入錢包,注意須要先運行GenerateKeyPair生成錢包文件 * @author bizzan.com * */ public class ImportWalletFromFile { public static void main(String[] args) { // 如在主網中生成錢包密鑰對,須要改成MainNetParms NetworkParameters params = TestNet3Params.get(); Wallet wallet = null; // 建立一個文件用於保存錢包文件 final File walletFile = new File("test.wallet"); try { // 從文件加載錢包 wallet = Wallet.loadFromFile(walletFile); // 打印錢包信息 System.out.println(wallet.toString(true, true, true, null)); // 獲取錢包第一個密鑰對 ECKey firstKey = wallet.getImportedKeys().get(0); // 打印密鑰對信息 System.out.println("The first key is: \n" + firstKey.toString()); // 打印密鑰對中的私鑰(HEX) System.out.println("The first key Private Key(HEX) is: " + firstKey.getPrivateKeyAsHex()); // 打印密鑰對中的私鑰(WIF=Wallet Import Format) System.out.println("The first key Private Key(WIF) is: " + firstKey.getPrivateKeyAsWiF(params)); // 打印密鑰對中的公鑰: System.out.println("The first key Public Key is: " +firstKey.getPubKey()); // 打印密鑰對中的公鑰(HEX) System.out.println("The first key Public Key(HEX) is: " + firstKey.getPublicKeyAsHex()); // 打印密鑰對中的公鑰(Hash) System.out.println("The first key Public Key(Hash) is: " + firstKey.getPubKeyHash()); // 打印密鑰對錢包地址 System.out.println("The first key Wallet Address is: " + firstKey.toAddress(params)); // 經過公鑰查找密鑰對:findKeyFromPubHash(byte[] pubkeyHash) ECKey resultKey1 = wallet.findKeyFromPubHash(firstKey.getPubKeyHash()); System.out.println("Find Key From PubHash: " + resultKey1.toString()); // 判斷公鑰是否在錢包:isPubKeyHashMine(byte[] pubkeyHash) System.out.println("Is PubKeyHash Mine: " + wallet.isPubKeyHashMine(firstKey.getPubKeyHash())); // 經過公鑰查找密鑰對:findKeyFromPubKey(byte[] pubkey) ECKey resultKey2 = wallet.findKeyFromPubKey(firstKey.getPubKey()); System.out.println("Find Key From PubKey: " + resultKey2.toString()); // 判斷公鑰是否在錢包:isPubKeyMine(byte[] pubkey) System.out.println("Is PubKey Mine: " + wallet.isPubKeyMine(firstKey.getPubKey())); } catch (UnreadableWalletException e) { e.printStackTrace(); } } }
2. 經過區塊鏈瀏覽器監聽錢包地址:
經過輪詢的方式,如每隔1個小時查詢一次區塊鏈瀏覽器,獲取最新的區塊信息,再對其中的地址信息進行解析,示例代碼以下:ui
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import org.apache.http.HttpEntity; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; /** * 演示經過Blockchain.info提供的JSON Http獲取比特幣區塊信息 * @author bizzan.com * */ public class ApiApp { private final static String requstHttpUrl = "https://blockchain.info/"; public static void main(String[] args) { String param = "block-height/500000?format=json"; String result = sendGet(requstHttpUrl, param); JSONObject jsonObject = JSONObject.parseObject(result); JSONArray arr = jsonObject.getJSONArray("blocks"); JSONArray txList = arr.getJSONObject(0).getJSONArray("tx"); for(int i = 0; i < txList.size(); i++) { JSONObject txObj = txList.getJSONObject(i); JSONArray outs = txObj.getJSONArray("out"); for(int j = 0; j < outs.size(); j++) { JSONObject outObj = outs.getJSONObject(j); // 這裏已獲取交易信息(包含收幣地址信息),由此可將此地址與充值地址庫對比,進行充值後續工做 System.out.println("tx_index:" + outObj.getIntValue("tx_index") + " - addr:" + outObj.getString("addr")); } } } public static String sendGet(String url, String param) { CloseableHttpClient httpClient = null; CloseableHttpResponse response = null; String result = ""; try { httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet(url + param); RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(35000) .setConnectionRequestTimeout(35000) .setSocketTimeout(60000) .build(); httpGet.setConfig(requestConfig); response = httpClient.execute(httpGet); HttpEntity entity = response.getEntity(); result = EntityUtils.toString(entity); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (null != response) { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != httpClient) { try { httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } } return result; } }
上面的代碼是獲取高度爲500000的區塊信息,並對其中的地址進行解析。加密
BIZZAN(幣嚴) 數字貨幣交易所官方網址:
www.bizzan.com