├── py ├── requirements.txt ├── custody_key.py ├── README.md └── custody_client.py ├── java ├── settings.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.gradle ├── gradlew.bat ├── src │ └── main │ │ └── java │ │ └── com │ │ └── cobo │ │ └── custody │ │ └── demo │ │ └── Main.java └── gradlew ├── go ├── go.mod ├── custody_key.go ├── README.md ├── go.sum └── custody_client.go ├── .gitignore ├── js ├── custody_key.js ├── package.json ├── README.md ├── custody_client.js └── package-lock.json ├── php ├── custody_key.php └── custody_client.php └── README.md /py/requirements.txt: -------------------------------------------------------------------------------- 1 | pycoin==0.80 2 | requests 3 | -------------------------------------------------------------------------------- /java/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | 3 | -------------------------------------------------------------------------------- /go/go.mod: -------------------------------------------------------------------------------- 1 | module custody_clients 2 | 3 | go 1.12 4 | 5 | require github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 6 | -------------------------------------------------------------------------------- /java/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cobowallet/custody-clients/HEAD/java/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv/ 3 | 4 | node_modules 5 | npm-debug.log* 6 | .idea 7 | .vscode 8 | .DS_Store 9 | 10 | coverage 11 | .coverage 12 | *.pid 13 | .npm 14 | .grunt 15 | 16 | vendor 17 | .gradle 18 | out/ 19 | -------------------------------------------------------------------------------- /java/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /js/custody_key.js: -------------------------------------------------------------------------------- 1 | const ec = new require('elliptic').ec('secp256k1') 2 | const generate = () => { 3 | let key = ec.genKeyPair() 4 | return [key.getPublic(true, "hex"), key.getPrivate("hex")] 5 | } 6 | pair = generate(); 7 | console.log("API_KEY: " + pair[0]); 8 | console.log("API_SECRET: " + pair[1]); 9 | -------------------------------------------------------------------------------- /php/custody_key.php: -------------------------------------------------------------------------------- 1 | genKeyPair(); 6 | echo "API_KEY:"; 7 | echo $key->getPublic(true, "hex"); 8 | echo "\n"; 9 | echo "API_SECRET:"; 10 | echo $key->getPrivate("hex"); 11 | } 12 | generate(); 13 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cobo_Custody_Js_Client", 3 | "version": "1.0.0", 4 | "description": "Cobo Custody Js Client", 5 | "keywords": [ 6 | "Cobo", 7 | "Custody" 8 | ], 9 | "scripts": {}, 10 | "author": "", 11 | "license": "GPL-3.0", 12 | "dependencies": { 13 | "sha256": "0.2.0", 14 | "bip66": "1.1.5", 15 | "node-fetch": "2.6.1", 16 | "elliptic": "6.5.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /go/custody_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | ) 7 | import "github.com/btcsuite/btcd/btcec" 8 | 9 | func GenerateRandomKeyPair() { 10 | apiSecret := make([]byte, 32) 11 | if _, err := rand.Read(apiSecret); err != nil { 12 | panic(err) 13 | } 14 | privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), apiSecret) 15 | apiKey := fmt.Sprintf("%x", privKey.PubKey().SerializeCompressed()) 16 | apiSecretStr := fmt.Sprintf("%x", apiSecret) 17 | 18 | fmt.Printf("API_Key: %s\nAPI_SECRET: %s\n", apiKey, apiSecretStr) 19 | } 20 | 21 | func main() { 22 | GenerateRandomKeyPair() 23 | } 24 | -------------------------------------------------------------------------------- /py/custody_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # import _4quila 4 | import secrets 5 | from pycoin.encoding import public_pair_to_sec, to_bytes_32 6 | from binascii import b2a_hex 7 | from pycoin.key import Key 8 | 9 | 10 | def generate_new_key(): 11 | secret = secrets.randbits(256) 12 | secret_hex = b2a_hex(to_bytes_32(secret)).decode() 13 | key = Key(secret_exponent=secret) 14 | sec = public_pair_to_sec(key.public_pair()) 15 | return b2a_hex(sec).decode(), secret_hex 16 | 17 | 18 | if __name__ == "__main__": 19 | _key, _secret = generate_new_key() 20 | print("API_KEY: %s" % _key) 21 | print("API_SECRET: %s" % _secret) 22 | -------------------------------------------------------------------------------- /go/README.md: -------------------------------------------------------------------------------- 1 | ## 查询账户详情 2 | 3 | ``` 4 | Request("GET", "/v1/custody/org_info/", map[string]string{}) 5 | ``` 6 | 7 | ## 获取新地址 8 | 9 | ``` 10 | Request("POST", "/v1/custody/new_address/", map[string]string{ 11 | "coin": "ETH", 12 | }) 13 | ``` 14 | 15 | ## 获取交易记录 16 | 17 | ``` 18 | Request("GET", "/v1/custody/transaction_history/", map[string]string{ 19 | "coin": "ETH", 20 | "side": "deposit", 21 | }) 22 | ``` 23 | 24 | ## 提交提现申请 25 | 26 | ``` 27 | Request("POST", "/v1/custody/new_withdraw_request/", map[string]string{ 28 | "coin": "ETH", 29 | "address": "0x8e2782aabdf80fbb69399ce3d9bd5ae69a60462c", 30 | "amount": "100000000000000", 31 | "request_id": "unique_123456", 32 | "memo": "hello test", 33 | }) 34 | ``` 35 | 36 | ## 获取提现申请信息 37 | 38 | ``` 39 | Request("GET", "/v1/custody/withdraw_info_by_request_id/", map[string]string{ 40 | "request_id": "unique_123456", 41 | }) 42 | ``` 43 | -------------------------------------------------------------------------------- /java/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.cobo.custody' 6 | version '1.0-SNAPSHOT' 7 | 8 | sourceCompatibility = 1.8 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | // https://mvnrepository.com/artifact/org.bitcoinj/bitcoinj-core 16 | compile group: 'org.bitcoinj', name: 'bitcoinj-core', version: '0.15' 17 | compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.12.1' 18 | compile group: 'com.squareup.okio', name: 'okio', version: '2.2.2' 19 | 20 | testCompile group: 'junit', name: 'junit', version: '4.12' 21 | } 22 | 23 | apply plugin: 'application' 24 | mainClassName = 'com.cobo.custody.demo.Main' 25 | 26 | task key(type: JavaExec) { 27 | group = "Execution" 28 | description = "Run the main class with JavaExecTask" 29 | classpath = sourceSets.main.runtimeClasspath 30 | main = mainClassName 31 | args "key" 32 | } 33 | -------------------------------------------------------------------------------- /py/README.md: -------------------------------------------------------------------------------- 1 | ## 查询账户详情 2 | 3 | ``` 4 | request( 5 | "GET", 6 | "/v1/custody/org_info/", 7 | {}, 8 | api_key, api_secret, host 9 | ) 10 | ``` 11 | 12 | ## 获取新地址 13 | 14 | ``` 15 | request( 16 | "POST", 17 | "/v1/custody/new_address/", 18 | { 19 | "coin": "ETH" 20 | }, 21 | api_key, api_secret, host 22 | ) 23 | ``` 24 | 25 | ## 获取交易记录 26 | 27 | ``` 28 | request( 29 | 'GET', 30 | '/v1/custody/transaction_history/', 31 | { 32 | "coin": "ETH", 33 | "side": "deposit" 34 | }, 35 | api_key, api_secret, host 36 | ) 37 | ``` 38 | 39 | ## 提交提现申请 40 | 41 | ``` 42 | request( 43 | "POST", 44 | "/v1/custody/new_withdraw_request/", 45 | { 46 | "coin": "ETH", 47 | "address": "0x8e2782aabdf80fbb69399ce3d9bd5ae69a60462c", 48 | "amount": "100000000000000", 49 | "request_id": "unique_123456", 50 | "memo": "hello test" 51 | }, 52 | api_key, api_secret, host 53 | ) 54 | ``` 55 | 56 | ## 获取提现申请信息 57 | 58 | ``` 59 | request( 60 | "GET", 61 | "/v1/custody/withdraw_info_by_request_id/", 62 | { 63 | "request_id": "unique_123456" 64 | }, 65 | api_key, api_secret, host 66 | ) 67 | ``` 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Cobo Custody Api Client Demo 2 | 3 | Cobo Custody offers a RESTful API to integrate WaaS (Wallet as a service) for over 43 main chains and 1000+ tokens with your application through a simple and unified interface. Here's some samples guiding you how to generate API Key pairs (Key & Secret) and interact with Cobo Custody. 4 | 5 | More info: [API Documentation](https://doc.custody.cobo.com/) 6 | 7 | ## Requirements && Execute 8 | 9 | * Python 10 | - python3.6 11 | - pip install -r requirements.txt 12 | - python custody_key.py 13 | - register API_KEY on Custody Web 14 | - replace API_KEY, API_SECRET in custody_client.py 15 | - python custody_client.py 16 | 17 | * Go 18 | - go run custody_key.go 19 | - register API_KEY on Custody Web 20 | - replace API_KEY, API_SECRET in custody_client.go 21 | - go run custody_client.go 22 | 23 | * Js 24 | - npm install 25 | - node custody_key.js 26 | - register API_KEY on Custody Web 27 | - replace API_KEY API_SECRET in custody_client.js 28 | - node custody_client.js 29 | 30 | * Java 31 | - use your IDE to manage com/cobo/custody/demo/Main.java 32 | - OR 33 | - gradle build 34 | - gradle key 35 | - register API_KEY on Custody Web 36 | - replace API_KEY API_SECRET in com/cobo/custody/demo/Main.java 37 | - gradle build 38 | - gradle run 39 | 40 | * Php 41 | - composer require simplito/elliptic-php 42 | - php custody_key.php 43 | - register API_KEY on Custody Web 44 | - replace API_KEY API_SECRET in custody_client.php 45 | - php custody_client.php 46 | 47 | ## Support 48 | 49 | Please contact your VIP Customer Service or custodyservice@cobo.com 50 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | ## 查询账户详情 2 | 3 | ``` 4 | coboFetch('GET', '/v1/custody/org_info/', {}, api_key, api_secret, host) 5 | .then(res => { 6 | res.json().then((data)=>{ 7 | console.log(JSON.stringify(data, null, 4)); 8 | }) 9 | }).catch(err => { 10 | console.log(err) 11 | }); 12 | ``` 13 | 14 | ## 获取新地址 15 | 16 | ``` 17 | coboFetch('POST', '/v1/custody/new_address/', 18 | { 19 | "coin": "ETH" 20 | }, 21 | api_key, api_secret, host 22 | ).then(res => { 23 | res.json().then((data)=>{ 24 | console.log(JSON.stringify(data, null, 4)); 25 | }) 26 | }).catch(err => { 27 | console.log(err) 28 | }); 29 | ``` 30 | 31 | ## 获取交易记录 32 | 33 | ``` 34 | coboFetch('GET', '/v1/custody/transaction_history/', 35 | { 36 | "coin": "ETH", 37 | "side": "deposit" 38 | }, 39 | api_key, api_secret, host 40 | ).then(res => { 41 | res.json().then((data)=>{ 42 | console.log(JSON.stringify(data, null, 4)); 43 | }) 44 | }).catch(err => { 45 | console.log(err) 46 | }); 47 | ``` 48 | 49 | ## 提交提现申请 50 | 51 | ``` 52 | coboFetch('POST', '/v1/custody/new_withdraw_request/', 53 | { 54 | "coin": "ETH", 55 | "address": "0x8e2782aabdf80fbb69399ce3d9bd5ae69a60462c", 56 | "amount": "100000000000000", 57 | "request_id": "unique_123456", 58 | "memo": "hello test" 59 | }, 60 | api_key, api_secret, host 61 | ).then(res => { 62 | res.json().then((data)=>{ 63 | console.log(JSON.stringify(data, null, 4)); 64 | }) 65 | }).catch(err => { 66 | console.log(err) 67 | }); 68 | ``` 69 | 70 | ## 获取提现申请信息 71 | 72 | ``` 73 | coboFetch('GET', '/v1/custody/withdraw_info_by_request_id/', 74 | { 75 | "request_id": "unique_123456" 76 | }, 77 | api_key, api_secret, host 78 | ).then(res => { 79 | res.json().then((data)=>{ 80 | console.log(JSON.stringify(data, null, 4)); 81 | }) 82 | }).catch(err => { 83 | console.log(err) 84 | }); 85 | ``` 86 | -------------------------------------------------------------------------------- /java/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /php/custody_client.php: -------------------------------------------------------------------------------- 1 | keyFromPrivate($API_SECRET); 21 | return $key->sign(bin2hex($message))->toDER('hex'); 22 | } 23 | function verify_ecdsa($message, $timestamp, $signature){ 24 | global $COBO_PUB; 25 | $message = hash("sha256", hash("sha256", "{$message}|{$timestamp}", True), True); 26 | $ec = new EC('secp256k1'); 27 | $key = $ec->keyFromPublic($COBO_PUB, "hex"); 28 | return $key->verify(bin2hex($message), $signature); 29 | } 30 | function sort_data($data){ 31 | ksort($data); 32 | $result = []; 33 | foreach ($data as $key => $val) { 34 | array_push($result, $key."=".urlencode($val)); 35 | } 36 | return join("&", $result); 37 | } 38 | function request($method, $path, $data){ 39 | global $HOST; 40 | global $API_KEY; 41 | $ch = curl_init(); 42 | $sorted_data = sort_data($data); 43 | $nonce = time() * 1000; 44 | curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); 45 | curl_setopt ($ch, CURLOPT_HEADER, 1); 46 | curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT, 10); 47 | curl_setopt ($ch, CURLOPT_HTTPHEADER, [ 48 | "Biz-Api-Key:".$API_KEY, 49 | "Biz-Api-Nonce:".$nonce, 50 | "Biz-Api-Signature:".sign_ecdsa(join("|", [$method, $path, $nonce, $sorted_data])) 51 | ]); 52 | if ($method == "POST"){ 53 | curl_setopt ($ch, CURLOPT_URL, $HOST.$path); 54 | curl_setopt ($ch, CURLOPT_POST, 1); 55 | curl_setopt ($ch, CURLOPT_POSTFIELDS, $data); 56 | } else { 57 | curl_setopt ($ch, CURLOPT_URL, $HOST.$path."?".$sorted_data); 58 | } 59 | list($header, $body) = explode("\r\n\r\n", curl_exec($ch), 2); 60 | preg_match("/biz_timestamp: (?[0-9]*)/i", $header, $match); 61 | $timestamp = $match["timestamp"]; 62 | preg_match("/biz_resp_signature: (?[0-9abcdef]*)/i", $header, $match); 63 | $signature = $match["signature"]; 64 | if (verify_ecdsa($body, $timestamp, $signature) != 1){ 65 | throw new Exception("signature verify fail"); 66 | } 67 | curl_close($ch); 68 | return $body; 69 | } 70 | echo request("GET", "/v1/custody/coin_info/", ["coin" => "BTC"]); 71 | echo request("POST", "/v1/custody/new_address/", ["coin" => "BTC"]); 72 | -------------------------------------------------------------------------------- /go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 2 | github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 h1:A/EVblehb75cUgXA5njHPn0kLAsykn6mJGz7rnmW5W0= 3 | github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= 4 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 5 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 6 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 7 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 8 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 9 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 10 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 11 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495 h1:6IyqGr3fnd0tM3YxipK27TUskaOVUjU2nG45yzwcQKY= 12 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 16 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 17 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 18 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 19 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 20 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 21 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 22 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 23 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 24 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 26 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 29 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 30 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 31 | -------------------------------------------------------------------------------- /go/custody_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "time" 5 | import "strings" 6 | import "sort" 7 | import "net/url" 8 | import "io/ioutil" 9 | import "encoding/hex" 10 | import "crypto/sha256" 11 | import "github.com/btcsuite/btcd/btcec" 12 | import "net/http" 13 | 14 | const API_KEY = "x" 15 | const API_SECRET = "x" 16 | const HOST = "https://api.sandbox.cobo.com" 17 | const COBO_PUB = "032f45930f652d72e0c90f71869dfe9af7d713b1f67dc2f7cb51f9572778b9c876" 18 | 19 | func SortParams(params map[string]string) string { 20 | keys := make([]string, len(params)) 21 | i := 0 22 | for k, _ := range params { 23 | keys[i] = k 24 | i++ 25 | } 26 | sort.Strings(keys) 27 | sorted := make([]string, len(params)) 28 | i = 0 29 | for _, k := range keys { 30 | sorted[i] = k + "=" + url.QueryEscape(params[k]) 31 | i++ 32 | } 33 | return strings.Join(sorted, "&") 34 | } 35 | func Hash256(s string) string { 36 | hash_result := sha256.Sum256([]byte(s)) 37 | hash_string := string(hash_result[:]) 38 | return hash_string 39 | } 40 | func Hash256x2(s string) string { 41 | return Hash256(Hash256(s)) 42 | } 43 | func SignEcc(message string) string { 44 | api_secret, _ := hex.DecodeString(API_SECRET) 45 | privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), api_secret) 46 | sig, _ := privKey.Sign([]byte(Hash256x2(message))) 47 | return fmt.Sprintf("%x", sig.Serialize()) 48 | } 49 | 50 | func VerifyEcc(message string, signature string) bool { 51 | pub_key, _ := hex.DecodeString(COBO_PUB) 52 | pubKey, _ := btcec.ParsePubKey(pub_key, btcec.S256()) 53 | 54 | sigBytes, _ := hex.DecodeString(signature) 55 | sigObj, _ := btcec.ParseSignature(sigBytes, btcec.S256()) 56 | 57 | verified := sigObj.Verify([]byte(Hash256x2(message)), pubKey) 58 | return verified 59 | } 60 | 61 | func Request(method string, path string, params map[string]string) string { 62 | client := &http.Client{} 63 | nonce := fmt.Sprintf("%d", time.Now().Unix()*1000) 64 | sorted := SortParams(params) 65 | var req *http.Request 66 | if method == "POST" { 67 | req, _ = http.NewRequest(method, HOST+path, strings.NewReader(sorted)) 68 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 69 | } else { 70 | req, _ = http.NewRequest(method, HOST+path+"?"+sorted, strings.NewReader("")) 71 | } 72 | content := strings.Join([]string{method, path, nonce, sorted}, "|") 73 | 74 | req.Header.Set("Biz-Api-Key", API_KEY) 75 | req.Header.Set("Biz-Api-Nonce", nonce) 76 | req.Header.Set("Biz-Api-Signature", SignEcc(content)) 77 | 78 | resp, _ := client.Do(req) 79 | 80 | defer resp.Body.Close() 81 | 82 | body, _ := ioutil.ReadAll(resp.Body) 83 | 84 | timestamp := resp.Header["Biz-Timestamp"][0] 85 | signature := resp.Header["Biz-Resp-Signature"][0] 86 | success := VerifyEcc(string(body)+"|"+timestamp, signature) 87 | fmt.Println("verify success?", success) 88 | return string(body) 89 | } 90 | 91 | func main() { 92 | res := Request("GET", "/v1/custody/org_info/", map[string]string{}) 93 | fmt.Println("res", res) 94 | res = Request("GET", "/v1/custody/transaction_history/", map[string]string{ 95 | "coin": "ETH", 96 | "side": "deposit", 97 | }) 98 | fmt.Println("res", res) 99 | } 100 | -------------------------------------------------------------------------------- /js/custody_client.js: -------------------------------------------------------------------------------- 1 | //var bitcoin = require('bitcoinjs-lib') // v3.x.x 2 | //var keyPair = bitcoin.ECPair.fromWIF('L3oe5Wz3wmbxq9tar8nS5bqqCfjMZJNKPXAbbemoHWKXR1SAN8j2') 3 | //var privateKey = keyPair.privateKey 4 | /* global require Buffer*/ 5 | const sha256 = require('sha256'); 6 | const bip66 = require('bip66'); 7 | const fetch = require('node-fetch'); 8 | const ec = new require('elliptic').ec('secp256k1') 9 | 10 | const { URLSearchParams } = require('url'); 11 | 12 | const ZERO = Buffer.alloc(1, 0) 13 | function toDER(x){ 14 | let i = 0 15 | while (x[i] === 0) ++i 16 | if (i === x.length) return ZERO 17 | x = x.slice(i) 18 | if (x[0] & 0x80) return Buffer.concat([ZERO, x], 1 + x.length) 19 | return x 20 | } 21 | 22 | const generate = () => { 23 | let key = ec.genKeyPair() 24 | return [key.getPublic(true, "hex"), key.getPrivate("hex")] 25 | } 26 | 27 | 28 | const sign = (message, api_hex) =>{ 29 | //let message = 'GET|/v1/custody/org_info/|1541560385699|' 30 | let privateKey = Buffer.from(api_hex, 'hex') 31 | let result = ec.sign(Buffer.from(sha256.x2(message), 'hex'), privateKey) 32 | var r = new Buffer(result.r.toString(16, 64), 'hex') 33 | var s = new Buffer(result.s.toString(16, 64), 'hex') 34 | r = toDER(r); 35 | s = toDER(s); 36 | return bip66.encode(r, s).toString('hex'); 37 | }; 38 | 39 | const coboFetch = (method, path, params, api_key, api_hex, base = 'https://api.sandbox.cobo.com') => { 40 | let nonce = String(new Date().getTime()); 41 | let sort_params = Object.keys(params).sort().map((k) => { 42 | return k + '=' + encodeURIComponent(params[k]).replace(/%20/g, "+"); 43 | }).join('&'); 44 | let content = [method, path, nonce, sort_params].join('|'); 45 | let headers = { 46 | 'Biz-Api-Key': api_key, 47 | 'Biz-Api-Nonce': nonce, 48 | 'Biz-Api-Signature': sign(content, api_hex) 49 | }; 50 | if (method == 'GET'){ 51 | return fetch(base + path + '?' + sort_params, { 52 | 'method': method, 53 | 'headers': headers, 54 | }); 55 | }else if (method == 'POST'){ 56 | 57 | let urlParams = new URLSearchParams(); 58 | for (let k in params){ 59 | urlParams.append(k, params[k]) 60 | } 61 | 62 | return fetch(base + path, { 63 | 'method': method, 64 | 'headers': headers, 65 | 'body': urlParams 66 | }); 67 | }else{ 68 | throw "unexpected method " + method; 69 | } 70 | } 71 | 72 | 73 | let api_key = 'x' 74 | let api_hex = 'x' 75 | 76 | coboFetch('GET', '/v1/custody/org_info/', {}, api_key, api_hex) 77 | .then(res => { 78 | console.log(res.status); 79 | res.json().then((data)=>{ 80 | console.log(data); 81 | }) 82 | }).catch(err => { 83 | console.log(err) 84 | }); 85 | coboFetch('GET', '/v1/custody/transaction_history/', {'coin': 'ETH', 'side': 'deposit'}, api_key, api_hex) 86 | .then(res => { 87 | console.log(res.status); 88 | res.json().then((data)=>{ 89 | console.log(data); 90 | }) 91 | }).catch(err => { 92 | console.log(err) 93 | }); 94 | coboFetch('POST', '/v1/custody/new_address/', {"coin": "ETH"}, api_key, api_hex) 95 | .then(res => { 96 | console.log(res.status); 97 | res.json().then((data)=>{ 98 | console.log(data); 99 | }) 100 | }).catch(err => { 101 | console.log(err) 102 | }); 103 | -------------------------------------------------------------------------------- /js/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cobo_Custody_Js_Client", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bip66": { 8 | "version": "1.1.5", 9 | "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", 10 | "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", 11 | "requires": { 12 | "safe-buffer": "^5.0.1" 13 | } 14 | }, 15 | "bn.js": { 16 | "version": "4.11.8", 17 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", 18 | "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" 19 | }, 20 | "brorand": { 21 | "version": "1.1.0", 22 | "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", 23 | "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" 24 | }, 25 | "convert-hex": { 26 | "version": "0.1.0", 27 | "resolved": "https://registry.npmjs.org/convert-hex/-/convert-hex-0.1.0.tgz", 28 | "integrity": "sha1-CMBFaJIsJ3drii6BqV05M2LqC2U=" 29 | }, 30 | "convert-string": { 31 | "version": "0.1.0", 32 | "resolved": "https://registry.npmjs.org/convert-string/-/convert-string-0.1.0.tgz", 33 | "integrity": "sha1-ec5BqbsNA7z3LNxqjzxW+7xkQQo=" 34 | }, 35 | "elliptic": { 36 | "version": "6.4.1", 37 | "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", 38 | "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", 39 | "requires": { 40 | "bn.js": "^4.4.0", 41 | "brorand": "^1.0.1", 42 | "hash.js": "^1.0.0", 43 | "hmac-drbg": "^1.0.0", 44 | "inherits": "^2.0.1", 45 | "minimalistic-assert": "^1.0.0", 46 | "minimalistic-crypto-utils": "^1.0.0" 47 | } 48 | }, 49 | "hash.js": { 50 | "version": "1.1.7", 51 | "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", 52 | "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", 53 | "requires": { 54 | "inherits": "^2.0.3", 55 | "minimalistic-assert": "^1.0.1" 56 | } 57 | }, 58 | "hmac-drbg": { 59 | "version": "1.0.1", 60 | "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", 61 | "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", 62 | "requires": { 63 | "hash.js": "^1.0.3", 64 | "minimalistic-assert": "^1.0.0", 65 | "minimalistic-crypto-utils": "^1.0.1" 66 | } 67 | }, 68 | "inherits": { 69 | "version": "2.0.3", 70 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 71 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 72 | }, 73 | "minimalistic-assert": { 74 | "version": "1.0.1", 75 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 76 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" 77 | }, 78 | "minimalistic-crypto-utils": { 79 | "version": "1.0.1", 80 | "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", 81 | "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" 82 | }, 83 | "node-fetch": { 84 | "version": "2.3.0", 85 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", 86 | "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==" 87 | }, 88 | "safe-buffer": { 89 | "version": "5.1.2", 90 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 91 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 92 | }, 93 | "sha256": { 94 | "version": "0.2.0", 95 | "resolved": "https://registry.npmjs.org/sha256/-/sha256-0.2.0.tgz", 96 | "integrity": "sha1-c6C0GNqrcDW/+G6EkeNjQS/CqwU=", 97 | "requires": { 98 | "convert-hex": "~0.1.0", 99 | "convert-string": "~0.1.0" 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /java/src/main/java/com/cobo/custody/demo/Main.java: -------------------------------------------------------------------------------- 1 | package com.cobo.custody.demo; 2 | 3 | import okhttp3.*; 4 | import okio.ByteString; 5 | import org.bitcoinj.core.ECKey; 6 | import org.bitcoinj.core.Sha256Hash; 7 | 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.URLEncoder; 10 | import java.util.TreeMap; 11 | 12 | public class Main { 13 | private static String API_KEY = "x"; 14 | private static String API_SECRET = "x"; 15 | private static String HOST = "https://api.sandbox.cobo.com"; 16 | private static String COBO_PUB = "032f45930f652d72e0c90f71869dfe9af7d713b1f67dc2f7cb51f9572778b9c876"; 17 | 18 | private static OkHttpClient HTTP_CLIENT = new OkHttpClient(); 19 | 20 | private static byte[] doubleSha256(String content) { 21 | return Sha256Hash.hashTwice(content.getBytes()); 22 | } 23 | 24 | private static byte[] hex2bytes(String s) { 25 | return ByteString.decodeHex(s).toByteArray(); 26 | } 27 | 28 | private static String bytes2Hex(byte[] b) { 29 | return ByteString.of(b).hex(); 30 | } 31 | 32 | private static String generateEccSignature(String content, String key) { 33 | ECKey eckey = ECKey.fromPrivate(hex2bytes(key)); 34 | return bytes2Hex(eckey.sign(Sha256Hash.wrap(doubleSha256(content))).encodeToDER()); 35 | } 36 | 37 | private static String composeParams(TreeMap params) { 38 | StringBuffer sb = new StringBuffer(); 39 | params.forEach((s, o) -> { 40 | try { 41 | sb.append(s).append("=").append(URLEncoder.encode(String.valueOf(o), "UTF-8")).append("&"); 42 | } catch (UnsupportedEncodingException e) { 43 | e.printStackTrace(); 44 | } 45 | }); 46 | if (sb.length() > 0) { 47 | sb.deleteCharAt(sb.length() - 1); 48 | } 49 | return sb.toString(); 50 | } 51 | 52 | private static boolean verifyResponse(String content, String sig, String pubkey) throws Exception { 53 | ECKey key = ECKey.fromPublicOnly(hex2bytes(pubkey)); 54 | return key.verify(doubleSha256(content), hex2bytes(sig)); 55 | } 56 | 57 | private static String request(String method, String path, TreeMap params, String apiKey, String apiSecret, String host) throws Exception { 58 | method = method.toUpperCase(); 59 | String nonce = String.valueOf(System.currentTimeMillis()); 60 | 61 | String paramString = composeParams(params); 62 | 63 | String content = method + "|" + path + "|" + nonce + "|" + paramString; 64 | 65 | String signature = generateEccSignature(content, apiSecret); 66 | 67 | Request.Builder builder = new Request.Builder() 68 | .addHeader("Biz-Api-Key", apiKey) 69 | .addHeader("Biz-Api-Nonce", nonce) 70 | .addHeader("Biz-Api-Signature", signature); 71 | Request request; 72 | if ("GET".equalsIgnoreCase(method)) { 73 | request = builder 74 | .url(host + path + "?" + paramString) 75 | .build(); 76 | } else if ("POST".equalsIgnoreCase(method)) { 77 | FormBody.Builder bodyBuilder = new FormBody.Builder(); 78 | params.forEach((s, o) -> bodyBuilder.add(s, String.valueOf(o))); 79 | RequestBody formBody = bodyBuilder.build(); 80 | request = builder 81 | .url(host + path) 82 | .post(formBody) 83 | .build(); 84 | } else { 85 | throw new RuntimeException("not supported http method"); 86 | } 87 | try (Response response = HTTP_CLIENT.newCall(request).execute()) { 88 | String ts = response.header("BIZ_TIMESTAMP"); 89 | String sig = response.header("BIZ_RESP_SIGNATURE"); 90 | String body = response.body().string(); 91 | boolean verifyResult = verifyResponse(body + "|" + ts, sig, COBO_PUB); 92 | System.out.println("verify success? " + verifyResult); 93 | if (!verifyResult) { 94 | throw new RuntimeException("verify response error"); 95 | } 96 | return body; 97 | } 98 | } 99 | 100 | public static void main(String... args) throws Exception { 101 | if (args.length == 1 && args[0].equals("key")){ 102 | testGenerateKeysAndSignMessage(); 103 | } else { 104 | testApi(); 105 | } 106 | } 107 | 108 | public static void testApi() throws Exception { 109 | TreeMap params = new TreeMap<>(); 110 | params.put("coin", "BTC"); 111 | String res = request("GET", "/v1/custody/org_info/", params, API_KEY, API_SECRET, HOST); 112 | System.out.println(res); 113 | } 114 | 115 | public static void testGenerateKeysAndSignMessage() { 116 | ECKey key = new ECKey(); 117 | String privHex = bytes2Hex(key.getPrivKeyBytes()); 118 | String pubHex = bytes2Hex(key.getPubKey()); 119 | System.out.println("API_KEY: " + pubHex + "; API_SECRET: " + privHex); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /java/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /py/custody_client.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import hashlib 3 | import json 4 | import random 5 | import time 6 | from binascii import b2a_hex, a2b_hex 7 | from cmd import Cmd 8 | 9 | try: 10 | from urllib.parse import urlencode 11 | except Exception: 12 | from urllib import urlencode 13 | 14 | from pycoin.key import Key 15 | 16 | from pycoin.encoding import from_bytes_32 17 | 18 | import requests 19 | 20 | API_KEY = "x" 21 | API_SECRET = "x" 22 | HOST = "https://api.sandbox.cobo.com" 23 | COBO_PUB = "032f45930f652d72e0c90f71869dfe9af7d713b1f67dc2f7cb51f9572778b9c876" 24 | 25 | def double_hash256(content): 26 | return hashlib.sha256(hashlib.sha256(content.encode()).digest()).digest() 27 | 28 | 29 | def verify(content, signature, pub_key): 30 | key = Key.from_sec(a2b_hex(pub_key)) 31 | return key.verify(double_hash256(content), a2b_hex(signature)) 32 | 33 | def generate_ecc_signature(content, key): 34 | key = Key(secret_exponent=from_bytes_32(a2b_hex(key))) 35 | return b2a_hex(key.sign(double_hash256(content))).decode() 36 | 37 | 38 | def sort_params(params): 39 | params = [(key, val) for key, val in params.items()] 40 | 41 | params.sort(key=lambda x: x[0]) 42 | return urlencode(params) 43 | 44 | 45 | def verify_response(response): 46 | content = response.content.decode() 47 | success = True 48 | try: 49 | timestamp = response.headers["BIZ_TIMESTAMP"] 50 | signature = response.headers["BIZ_RESP_SIGNATURE"] 51 | success = verify("%s|%s" % (content, timestamp), signature, COBO_PUB) 52 | except KeyError: 53 | pass 54 | return success, json.loads(content) 55 | 56 | 57 | def request( 58 | method, 59 | path, 60 | params, 61 | api_key, 62 | api_secret, 63 | ): 64 | method = method.upper() 65 | nonce = str(int(time.time() * 1000)) 66 | content = "%s|%s|%s|%s" % (method, path, nonce, sort_params(params)) 67 | sign = generate_ecc_signature(content, api_secret) 68 | 69 | headers = { 70 | "Biz-Api-Key": api_key, 71 | "Biz-Api-Nonce": nonce, 72 | "Biz-Api-Signature": sign, 73 | } 74 | if method == "GET": 75 | resp = requests.get( 76 | "%s%s" % (HOST, path), params=urlencode(params), headers=headers 77 | ) 78 | elif method == "POST": 79 | resp = requests.post("%s%s" % (HOST, path), data=params, headers=headers) 80 | else: 81 | raise Exception("Not support http method") 82 | verify_success, result = verify_response(resp) 83 | if not verify_success: 84 | raise Exception("Fatal: verify content error, maybe encounter mid man attack") 85 | return result 86 | 87 | 88 | get = functools.partial(request, "GET") 89 | post = functools.partial(request, "POST") 90 | 91 | 92 | class Client(Cmd): 93 | prompt = "Custody> " 94 | intro = "Welcome to the Cobo Custody shell. Type help or ? to list commands. \n" 95 | 96 | def __init__( 97 | self, 98 | api_key=None, 99 | api_secret=None, 100 | ): 101 | super(Client, self).__init__() 102 | assert api_key 103 | assert api_secret 104 | self.key = api_key 105 | self.secret = api_secret 106 | 107 | def _request(self, method, url, data): 108 | res = method(url, data, self.key, self.secret) 109 | print(json.dumps(res, indent=4)) 110 | 111 | def do_info(self, line): 112 | ("\n\tget org info\n\t" "example: \033[91minfo\033[0m\n") 113 | return self._request(get, "/v1/custody/org_info/", {}) 114 | 115 | def do_coin_info(self, line): 116 | ("\n\tget org coin info\n\t" "example: \033[91mcoin_info [coin]\033[0m\n") 117 | coin = line.strip() 118 | return self._request(get, "/v1/custody/coin_info/", {"coin": coin}) 119 | 120 | def do_new_address(self, coin): 121 | ( 122 | "\n\tget new address of coin, format: \033[93m[coin]\033[0m " 123 | "\n\texample: \033[91mnew_address LONT_ONT\033[0m\n" 124 | ) 125 | coin = coin.strip() 126 | return self._request(post, "/v1/custody/new_address/", {"coin": coin}) 127 | 128 | def do_query_address(self, line): 129 | ( 130 | "\n\tquery address of coin, format: \033[93m[coin] [address]\033[0m " 131 | "\n\texample: \033[91mquery_address LONT_ONT ADDRESS\033[0m\n" 132 | ) 133 | if len(line.split()) != 2: 134 | print("format: [coin] [address]") 135 | return 136 | coin, address = line.split() 137 | return self._request( 138 | get, "/v1/custody/address_info/", {"coin": coin, "address": address} 139 | ) 140 | 141 | def do_query_internal_address(self, line): 142 | ( 143 | "\n\tquery internal address of coin, format: \033[93m[coin] [address] [memo]\033[0m " 144 | "\n\texample: \033[91mquery_internal_address LONT_ONT ADDRESS\033[0m\n" 145 | ) 146 | if len(line.split()) not in [2, 3]: 147 | print("format: [coin] [address] [memo]") 148 | return 149 | if len(line.split()) == 2: 150 | coin, address = line.split() 151 | memo = "" 152 | if len(line.split()) == 3: 153 | coin, address, memo = line.split() 154 | return self._request( 155 | get, 156 | "/v1/custody/internal_address_info/", 157 | {"coin": coin, "address": address, "memo": memo}, 158 | ) 159 | 160 | def do_query_internal_addresses(self, line): 161 | ( 162 | "\n\tquery internal addresses of coin, format: \033[93m[coin] [addresses]\033[0m " 163 | "\n\texample: \033[91mquery_internal_addresses LONT_ONT ADDRESS,ADDRESS|CCCCC\033[0m\n" 164 | ) 165 | if len(line.split()) != 2: 166 | print("format: [coin] [addresses]") 167 | return 168 | coin, addresses = line.split() 169 | return self._request( 170 | get, 171 | "/v1/custody/internal_address_info_batch/", 172 | {"coin": coin, "address": addresses}, 173 | ) 174 | 175 | def do_check_address(self, line): 176 | ( 177 | "\n\tquery address if coin, format: \033[93m[coin] [address]\033[0m " 178 | "\n\texample: \033[91mquery_address LONT_ONT ADDRESS\033[0m\n" 179 | ) 180 | if len(line.split()) != 2: 181 | print("format: [coin] [address]") 182 | return 183 | coin, address = line.split() 184 | return self._request( 185 | get, "/v1/custody/is_valid_address/", {"coin": coin, "address": address} 186 | ) 187 | 188 | def do_address_history(self, line): 189 | ( 190 | "\n\tlist address of coin, format: \033[93m[coin]\033[0m " 191 | "\n\texample: \033[91maddress_history LONT_ONT\033[0m\n" 192 | ) 193 | coin = line.strip() 194 | return self._request(get, "/v1/custody/address_history/", {"coin": coin}) 195 | 196 | def do_history(self, line): 197 | ( 198 | "\n\tget transaction history, " 199 | "format: \033[93m[coin] [side=(w/d)] [limit?=5]\033[0m " 200 | "\n\texample: \033[91mhistory LONT_ONT deposit 2\033[0m\n" 201 | ) 202 | info = line.split() 203 | key = "" 204 | if len(info) not in (2, 3, 4, 5): 205 | print("format: [coin] [side] [limit?=5] [max|min=111] [need_fee_detail]") 206 | return 207 | if len(info) == 2: 208 | coin, side = info 209 | limit = 5 210 | need_fee_detail = "" 211 | elif len(info) == 3: 212 | coin, side, limit = info 213 | need_fee_detail = "" 214 | else: 215 | coin, side, limit = info[:3] 216 | key, key_id = info[3].split("=") 217 | key = key + "_id" 218 | need_fee_detail = "1" if len(info) == 5 else "" 219 | if side == "w": 220 | side = "withdraw" 221 | elif side == "d": 222 | side = "deposit" 223 | data = { 224 | "coin": coin, 225 | "side": side, 226 | "limit": limit, 227 | "need_fee_detail": need_fee_detail, 228 | } 229 | if key: 230 | data[key] = key_id 231 | return self._request(get, "/v1/custody/transaction_history/", data) 232 | 233 | def do_pending(self, line): 234 | ( 235 | "\n\tget pending transaction, " 236 | "format: \033[93m[coin] [side=(w/d)] [limit?=5]\033[0m " 237 | "\n\texample: \033[91mhistory LONT_ONT deposit 2\033[0m\n" 238 | ) 239 | info = line.split() 240 | key = "" 241 | if len(info) not in (2, 3, 4): 242 | print("format: [coin] [side] [limit?=5] [max|min=111]") 243 | return 244 | if len(info) == 2: 245 | coin, side = info 246 | limit = 5 247 | elif len(info) == 3: 248 | coin, side, limit = info 249 | else: 250 | coin, side, limit = info[:3] 251 | key, key_id = info[3].split("=") 252 | key = key + "_id" 253 | if side == "w": 254 | side = "withdraw" 255 | elif side == "d": 256 | side = "deposit" 257 | data = {"coin": coin, "side": side, "limit": limit} 258 | if key: 259 | data[key] = key_id 260 | return self._request(get, "/v1/custody/pending_transactions/", data) 261 | 262 | def do_transaction(self, unique_id): 263 | "\n\tget transaction by id, format: \033[93m[id]\033[0m\n" 264 | unique_id = unique_id.strip() 265 | if " " in unique_id: 266 | unique_id = unique_id.split(" ")[0] 267 | return self._request( 268 | get, 269 | "/v1/custody/transaction/", 270 | { 271 | "id": unique_id, 272 | "need_fee_detail": "t", 273 | "need_source_address_detail": "t", 274 | }, 275 | ) 276 | else: 277 | return self._request(get, "/v1/custody/transaction/", {"id": unique_id}) 278 | 279 | def do_withdraw(self, line): 280 | ( 281 | "\n\twithdraw a coin, format: " 282 | "\033[93m[coin] [address] [amount] [in|ex]\033[0m " 283 | "\n\texample: " 284 | "\033[91mwithdraw LONT_ONT ASUACHaNnL4Q8UZ5tg1jFNBs1yzM3dFgrp 5\033[0m\n" 285 | ) 286 | info = line.split() 287 | if len(info) not in [3, 4]: 288 | print("format: [coin] [address] [amount] [in|ex]") 289 | return 290 | if len(info) == 3: 291 | coin, address, amount = info 292 | force = "" 293 | if len(info) == 4: 294 | coin, address, amount, force = info 295 | if "|" in address: 296 | memo = address.split("|")[1] 297 | address = address.split("|")[0] 298 | else: 299 | memo = None 300 | request_id = "tool_%s%s" % (int(time.time()), random.randint(0, 1000)) 301 | data = { 302 | "request_id": request_id, 303 | "coin": coin, 304 | "address": address, 305 | "amount": amount, 306 | } 307 | if force == "in": 308 | data["force_internal"] = "1" 309 | if force == "ex": 310 | data["force_external"] = "1" 311 | if memo is not None: 312 | data["memo"] = memo 313 | 314 | return self._request(post, "/v1/custody/new_withdraw_request/", data) 315 | 316 | def do_query_withdraw(self, request_id): 317 | ( 318 | "\n\twithdraw a coin, format: " 319 | "\033[93m[request_id]\033[0m " 320 | "\n\texample: \033[91mquery_withdraw tool_154458886571\033[0m\n" 321 | ) 322 | request_id = request_id.strip() 323 | if " " in request_id: 324 | request_id = request_id.split()[0] 325 | need_confirm_detail = True 326 | else: 327 | need_confirm_detail = False 328 | if need_confirm_detail: 329 | data = {"request_id": request_id, "need_confirm_detail": "1"} 330 | else: 331 | data = {"request_id": request_id} 332 | return self._request(get, "/v1/custody/withdraw_info_by_request_id/", data) 333 | 334 | def do_query_deposit(self, transaction_id): 335 | ( 336 | "\n\tdeposit a coin, format: " 337 | "\033[93m[transaction_id]\033[0m " 338 | "\n\texample: \033[91mquery_deposit 2019020254458886571\033[0m\n" 339 | ) 340 | transaction_id = transaction_id.strip() 341 | data = {"id": transaction_id} 342 | return self._request(get, "/v1/custody/deposit_info/", data) 343 | 344 | def do_add_hd_address(self, line): 345 | info = line.split() 346 | if len(info) not in (2,): 347 | print("format: [coin] [address]") 348 | return 349 | coin, addr = info 350 | return self._request( 351 | post, "/v1/custody/hd/add_address/", {"coin": coin, "address": addr} 352 | ) 353 | 354 | 355 | if __name__ == "__main__": 356 | # Replace by your own keys 357 | client = Client( 358 | api_key=API_KEY, 359 | api_secret=API_SECRET, 360 | ) 361 | 362 | client.cmdloop() 363 | --------------------------------------------------------------------------------