├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierrc ├── .remarkignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── config.js ├── dapps.js ├── demo.jpg ├── http.js ├── message.js ├── qrcode.png └── socket.js ├── lib ├── base.js ├── endpoints.js ├── http.js ├── index.d.ts ├── index.js ├── message.js └── socket.js ├── package.json ├── test ├── http.js └── message.js ├── tools ├── generate-dts.js └── patch-dts.sh └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | .idea 4 | node_modules 5 | coverage 6 | .nyc_output 7 | *.key 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "trailingComma": "es5", 5 | "jsxBracketSameLine": true, 6 | "semi": true, 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | test/snapshots/**/*.md 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | after_success: 5 | npm run coverage 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wangshijun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mixin-node-client 2 | 3 | [![build status](https://img.shields.io/travis/wangshijun/mixin-node-sdk.svg)](https://travis-ci.org/wangshijun/mixin-node-sdk) 4 | [![code coverage](https://img.shields.io/codecov/c/github/wangshijun/mixin-node-sdk.svg)](https://codecov.io/gh/wangshijun/mixin-node-sdk) 5 | [![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 6 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 7 | [![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org) 8 | [![license](https://img.shields.io/github/license/wangshijun/mixin-node-sdk.svg)](LICENSE) 9 | 10 | > Node.js SDK for Mixin Network, heavily inspired by [mixin-node](https://www.npmjs.com/package/mixin-node), but is much more developer friendly 11 | 12 | ## Table of Contents 13 | 14 | - [Install](#install) 15 | - [Usage](#usage) 16 | - [Contributors](#contributors) 17 | - [License](#license) 18 | 19 | ## Install 20 | 21 | ```sh 22 | yarn add mixin-node-client 23 | # OR npm install mixin-node-client -S 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### 1. Generate Config 29 | 30 | Steps to use generate config for dapp: 31 | 32 | 1. Create a dapp on [developers.mixin.one](https://developers.mixin.one), get clientId and clientSecret(the result when **Click to generate a new secret**) 33 | 2. Generate config from a new session info of your dapp (the result when **Click to generate a new session**) using [mixin-cli](https://github.com/wangshijun/mixin-cli) (**a command line tool by me**). 34 | 35 | Config file format, **remember to replace `clientId` and `clientSecret` with yours**: 36 | 37 | ```javascript 38 | // Generated with awesome https://github.com/wangshijun/mixin-cli 39 | module.exports = { 40 | clientId: '', 41 | clientSecret: '', 42 | assetPin: '310012', 43 | sessionId: '621c905b-1739-45e7-b668-b5531dd83646', 44 | aesKey: '56GcGs2EFHBPV2Xsb/OiwLdgjGt3q53JcFeLmbUutEk=', 45 | privateKey: `-----BEGIN RSA PRIVATE KEY----- 46 | MIICXAIBAAKBgQCsNaGbDx1UeKrTux01nC6R7/bu2GUELe6Q2mBSPymkZW2fpiaO 47 | FjkTI1MkEE8Eq1kGm/+6vAP84LMXG/W49UqZTBkKkrQ= 48 | -----END RSA PRIVATE KEY-----`, 49 | }; 50 | ``` 51 | 52 | ### 2. HttpClient 53 | 54 | `HttpClient` provides wrapper for all API supported by mixin network and mixin messenger, such as pin/user/asset/snapshot: 55 | 56 | ```javascript 57 | const { HttpClient } = require('mixin-node-client'); 58 | const config = require('./config'); 59 | 60 | const client = new HttpClient(config); 61 | 62 | const recipientId = 'ca630936-5af6-427e-ac4a-864a4c64f372'; // UserId 63 | const senderId = '7701e7bf-2a86-4655-982e-023564fa8945'; // UserID 64 | const assetId = '965e5c6e-434c-3fa9-b780-c50f43cd955c'; // CNB 65 | 66 | (async () => { 67 | const assets = await client.getAssets(); 68 | const verifyPin = await client.verifyPin(config.assetPin); 69 | const user = await client.getUser(senderId); 70 | console.log({ assets, verifyPin, user }); 71 | })(); 72 | ``` 73 | 74 | Full API list supported by `HttpClient`: 75 | 76 | - **getUserAssets**, get asset list owned by user 77 | - **getUserAsset**, get asset detail owned by user 78 | - **getNetworkAssets**, get top asset list by mixin network 79 | - **getNetworkAsset**, get top asset detail by mixin network 80 | - **getWithdrawAddress**, get withdraw address 81 | - **createWithdrawAddress**, create withdraw address 82 | - **deleteWithdrawAddress**, delete withdraw address 83 | - **withdraw**, request withdraw from mixin network 84 | - **deposit**, get deposit address for asset 85 | - **getSnapshots**, get network snapshot list 86 | - **getSnapshot**, get network snapshot detail 87 | - **verifyPin**, verify asset pin 88 | - **updatePin**, update/create asset pin 89 | - **createTransfer**, create transfer with an asset 90 | - **getTransfer**, read transfer detail 91 | - **verifyPayment**, verify transfer state 92 | - **getProfile**, get user profile 93 | - **updatePreference**, update user preference 94 | - **updateProfile**, update user profile 95 | - **createUser**, create user 96 | - **getUser**, get user by id 97 | - **getUsers**, get multiple users by id 98 | - **getFriends**, get friend list 99 | - **getContacts**, get contact list 100 | - **createConversation**, create new conversation 101 | - **readConversation**, read conversation detail 102 | - **sendMessage**, send raw message to specific conversation, see next section for message sender util. 103 | 104 | **Working example for `HttpClient` can be found [HERE](./examples/http.js)** 105 | 106 | #### Message Sender Util 107 | 108 | Because we can send messages to a conversation, `HttpClient` provide neat methods to send all kinds of message to Mixin Messenger: 109 | 110 | ```javascript 111 | console.log(client.getMessageSenders()); 112 | // [ 'sendText', 113 | // 'sendImage', 114 | // 'sendVideo', 115 | // 'sendData', 116 | // 'sendSticker', 117 | // 'sendContact', 118 | // 'sendButton', 119 | // 'sendButtons', 120 | // 'sendApp' ] 121 | const text = await client.sendText({ 122 | conversationId: conversation.conversation_id, 123 | data: 'Hello from node.js new client sdk', 124 | }); 125 | const button = await client.sendButton({ 126 | conversationId: conversation.conversation_id, 127 | data: { label: 'Open Mixin', color: '#FF0000', action: 'https://mixin.one' }, 128 | }); 129 | ``` 130 | 131 | For syntax of sending messages, see working example [HERE](./examples/message.js). 132 | 133 | ### 3. SocketClient 134 | 135 | `SocketClient` provide basic wrapper for Mixin Messenger WebSocket Messages, you can use it to listen and react to socket messages. 136 | 137 | ```javascript 138 | const { SocketClient } = require('mixin-node-client'); 139 | const config = require('./config'); 140 | 141 | const client = new SocketClient(config); 142 | 143 | socket.on( 144 | 'message', 145 | socket.getMessageHandler(message => { 146 | console.log('Message Received', message); 147 | if (message.data && message.data.category === 'PLAIN_TEXT' && message.data.data.toLowerCase() === 'hi') { 148 | // We support `sendText`, `sendButton`, `sendImage` here 149 | return socket.sendText('Hi there!', message); 150 | } 151 | 152 | return Promise.resolve(message); 153 | }) 154 | ); 155 | ``` 156 | 157 | **Working example for `SocketClient` can be found [HERE](./examples/socket.js)** 158 | 159 | Same set of message sender utils are also supported by `SocketClient` (**Note**: parameters are different for message sender utils of `HttpClient` and `SocketClient`, because we have the conversationId from the `onMessage` callback): 160 | 161 | ```javascript 162 | socket.sendText('Hi there!', message); 163 | socket.sendButton({ label: 'Open Mixin', color: '#FF0000', action: 'https://mixin.one' }, message); 164 | ``` 165 | 166 | ### Debugging 167 | 168 | If you are curious what happened during each API call, try run example code with following command: 169 | 170 | ```bash 171 | DEBUG=mixin-node-client:* node examples/http.js 172 | DEBUG=mixin-node-client:* node examples/socket.js 173 | DEBUG=mixin-node-client:* node examples/message.js 174 | ``` 175 | 176 | The mixin dapp included in the examples folder can be found with the following qrcode: 177 | 178 | ![](./examples/qrcode.png) 179 | 180 | ## Contributors 181 | 182 | | Name | 183 | | -------------- | 184 | | **wangshijun** | 185 | 186 | ## License 187 | 188 | [MIT](LICENSE) © wangshijun 189 | -------------------------------------------------------------------------------- /examples/config.js: -------------------------------------------------------------------------------- 1 | // NOTE: please update this config file with your own 2 | module.exports = { 3 | clientId: '1946399e-4303-4c44-bbe8-6fb39f82bdf9', 4 | clientSecret: '8bb1734940a4a95d7edd0393c18d848f298b3d3099a274876da57877736b067e', 5 | aesKey: 'hB/M96xzZRcda2iYklIBKE9pqwxQxl+nHsChqcWpl2M=', 6 | assetPin: '123456', 7 | sessionId: '28b8dc2a-5df6-4ead-8c7e-5af61fe7f27d', 8 | privateKey: `-----BEGIN RSA PRIVATE KEY----- 9 | MIICXAIBAAKBgQCOKr6amLfHj3Hz17DEs3KGfdUtndcorT8eQvfmOi2pS2dvG5+i 10 | mmMspnnpZ/wKKYUS4FIeUQABmr3etH2eCW6QBkn1KrLGyzgO2i66gHM8+7ucTnyM 11 | Altbyb8zAHY8y2AK+2fMu/kuJfaCMJ2Fo9zeUqWhXSJCs8LYGMzy9u5QzwIDAQAB 12 | AoGBAIdZ5MwVF+uUA16vfKlZW3D2Pk7G1sDwaIZsJc54l1513s0mlI5fxIoPGUSB 13 | 7rDSJNL65NPeNE5Sv/BXEL20pyBilaHv8MWQLiG9wUf4H1dSqeg2F2LzHDzV7f1i 14 | EWVvRmfOzvPxzi9chEVr5Qj8fzxFX+x0mf9wy7ikHOcWGn8RAkEA3hWLY+mO5Evb 15 | N6N9WoSgKOVp6WJJu7/7Y4quE8PlFGEBdW+9Tr4l8A5KyKljE5zXa5heDbVEefGj 16 | Y2ZNwLLyVwJBAKPgz30tXSLQV+esZ1c17WArCXf0/wondjvxJPQeo/oFkZQWiJDK 17 | Mp6KYzuEd5GykHQhBODkV4nY+qUnxqJMukkCQDQ5Fr0Y5+wRVmxf+pM1ir/zKpOh 18 | 9Dq9K2096C9nqk+/e+tUqkyA9ETd54IGKvEquZFok/1fOcn/I2+1V+VPbyMCQH5n 19 | rsQYVKnA2L1Ln16GqzWtnJ0ZrguwCqQLynw+ki8NUpBOnUJJpKfjiM1kzTHl8USS 20 | H4g0uU0Lv7Har32K/3kCQE1HoOltfPFx6Kh/sw3S3R4yN0brbtikFM/TYg1RNuo/ 21 | U4dKTBpN8wxz/UFtXz2Fxf1Fj98aVyW1ew9VLo090Lw= 22 | -----END RSA PRIVATE KEY-----`, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/dapps.js: -------------------------------------------------------------------------------- 1 | const { HttpClient } = require('../'); 2 | const config = require('./config'); 3 | const client = new HttpClient(config); 4 | 5 | // Read/write data on mixin blockchain and messenger through HTTP API 6 | (async () => { 7 | try { 8 | const user = await client.searchUser(7000); 9 | console.log({ user }); 10 | } catch (err) { 11 | console.error(err); 12 | } 13 | })(); 14 | -------------------------------------------------------------------------------- /examples/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshijun/mixin-node-client/34740a709e166ea7eab0423360a84bbdcd5b7767/examples/demo.jpg -------------------------------------------------------------------------------- /examples/http.js: -------------------------------------------------------------------------------- 1 | const { HttpClient } = require('../'); 2 | const config = require('./config'); 3 | const client = new HttpClient(config); 4 | 5 | const recipientId = 'ca630936-5af6-427e-ac4a-864a4c64f372'; // UserId 6 | const senderId = '7701e7bf-2a86-4655-982e-023564fa8945'; // UserID 7 | const assetId = '965e5c6e-434c-3fa9-b780-c50f43cd955c'; // CNB 8 | const UUID = client.getUUID(); // since UUID v4 is widely used, we can cache one here 9 | 10 | console.log('Supported Endpoints by HttpClient', client.getEndpoints()); 11 | 12 | // Read/write data on mixin blockchain and messenger through HTTP API 13 | (async () => { 14 | try { 15 | // asset related 16 | const assets = await client.getUserAssets(); 17 | const asset = await client.getUserAsset(assetId); 18 | const topAssets = await client.getNetworkAssets(); 19 | const topAsset = await client.getNetworkAsset(assetId); 20 | console.log({ assets, asset, topAssets, topAsset }); 21 | 22 | // pin verify/update/create 23 | const verifyPin = await client.verifyPin(config.assetPin); 24 | // const updatePin = await client.updatePin({ oldPin: config.assetPin, newPin: '123456' }); // CAUTION 25 | console.log({ verifyPin }); 26 | 27 | // address/asset management (deposit & withdraw) 28 | const depositAddress = await client.deposit(assetId); 29 | const withdrawAddress = await client.createWithdrawAddress({ 30 | assetId, 31 | label: 'Chui Niu Bi', 32 | publicKey: '0x4e088890f58dba45eb215613e9f01ed362ec87fb', 33 | }); 34 | const addressList = await client.getWithdrawAddress(assetId); 35 | const withdrawResult = await client.withdraw({ 36 | addressId: withdrawAddress.address_id, 37 | assetId, 38 | amount: 100, 39 | memo: 'test from mixin-node-client', 40 | }); 41 | 42 | const deleteAddress = await client.deleteWithdrawAddress(withdrawAddress.address_id); 43 | console.log({ depositAddress, withdrawAddress, addressList, withdrawResult, deleteAddress }); 44 | 45 | // transfer and payments 46 | const createTransfer = await client.createTransfer({ 47 | assetId, 48 | recipientId: senderId, 49 | traceId: UUID, 50 | amount: '1', 51 | memo: 'Test', 52 | }); 53 | const readTransfer = await client.getTransfer(UUID); 54 | const verifyPayment = await client.verifyPayment({ 55 | assetId, 56 | recipientId: senderId, 57 | traceId: UUID, 58 | amount: '1', 59 | }); 60 | console.log({ createTransfer, readTransfer, verifyPayment }); 61 | 62 | // conversation & message 63 | const conversation = await client.createConversation({ 64 | category: 'CONTACT', 65 | participants: [senderId], 66 | }); 67 | const group = await client.createConversation({ 68 | category: 'GROUP', 69 | participants: [senderId, recipientId], 70 | conversationId: UUID, 71 | }); 72 | const message = await client.sendMessage({ 73 | category: 'PLAIN_TEXT', 74 | conversationId: conversation.conversation_id, 75 | recipientId: senderId, 76 | data: Buffer.from('Hello from node.js new client sdk').toString('base64'), 77 | }); 78 | console.log({ conversation, group, message }); 79 | 80 | // snapshot 81 | const snapshots = await client.getSnapshots({ limit: 10, asset: assetId, offset: new Date().toString() }); 82 | const snapshot = await client.getSnapshot(snapshots[0].snapshot_id); 83 | console.log({ snapshots, snapshot }); 84 | 85 | // user profile/contacts 86 | const profile = await client.getProfile(); 87 | const user = await client.getUser(senderId); 88 | const users = await client.getUsers([recipientId, senderId]); 89 | const friends = await client.getFriends(); 90 | const contacts = await client.getContacts(); 91 | console.log({ profile, friends, user, users, contacts }); 92 | 93 | const { generateKeyPairSync } = require('crypto'); 94 | const { publicKey } = generateKeyPairSync('rsa', { 95 | modulusLength: 1024, // the length of your key in bits 96 | publicKeyEncoding: { 97 | type: 'spki', // recommended to be 'spki' by the Node.js docs 98 | format: 'pem', 99 | }, 100 | privateKeyEncoding: { 101 | type: 'pkcs1', // recommended to be 'pkcs8' by the Node.js docs 102 | format: 'pem', 103 | // cipher: 'aes-256-cbc', // *optional* 104 | // passphrase: 'top secret' // *optional* 105 | }, 106 | }); 107 | const publicKey1 = publicKey.replace('-----BEGIN PUBLIC KEY-----', ''); 108 | const publicKey2 = publicKey1.replace('-----END PUBLIC KEY-----', ''); 109 | const publicKey3 = publicKey2.replace(/\r?\n|\r/g, ''); 110 | 111 | const info = await client.createUser({ 112 | full_name: 'bitcoin wallet', 113 | session_secret: publicKey3, 114 | }); 115 | console.log(info); 116 | } catch (err) { 117 | console.error('error', err); 118 | console.trace(err); 119 | } 120 | })(); 121 | -------------------------------------------------------------------------------- /examples/message.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { HttpClient } = require('../'); 3 | const config = require('./config'); 4 | const client = new HttpClient(config); 5 | 6 | const senderId = '7701e7bf-2a86-4655-982e-023564fa8945'; // UserID 7 | 8 | console.log('Supported MessageSenders by HttpClient', client.getMessageSenders()); 9 | 10 | (async () => { 11 | try { 12 | // conversation & message 13 | const conversation = await client.createConversation({ 14 | category: 'CONTACT', 15 | participants: [senderId], 16 | }); 17 | const text = await client.sendText({ 18 | conversationId: conversation.conversation_id, 19 | data: 'Hello from node.js new client sdk', 20 | }); 21 | const button = await client.sendButton({ 22 | conversationId: conversation.conversation_id, 23 | data: { label: 'Open Baidu', color: '#FF0000', action: 'https://www.baidu.com' }, 24 | }); 25 | const contact = await client.sendContact({ 26 | conversationId: conversation.conversation_id, 27 | data: senderId, 28 | }); 29 | const app = await client.sendApp({ 30 | conversationId: conversation.conversation_id, 31 | data: { 32 | icon_url: 33 | 'https://images.mixin.one/PQ2dYjNNXYYCCcSi_jDxrh0PJM8XBaiwu4I5_5e7tJhpQNbCVULnc5VRzR4AHF2e7AK6mVpvaHxO0EZr24cUjbg=s256', 34 | title: '福来红包DEV', 35 | description: '方便好用的红包发送工具', 36 | action: 'https://github.com/wangshijun/mixin-node-client', 37 | }, 38 | }); 39 | const image = await client.sendImage({ 40 | conversationId: conversation.conversation_id, 41 | data: path.join(__dirname, './demo.jpg'), 42 | }); 43 | console.log({ conversation, text, button, contact, app, image }); 44 | } catch (err) { 45 | console.error(err); 46 | } 47 | })(); 48 | -------------------------------------------------------------------------------- /examples/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangshijun/mixin-node-client/34740a709e166ea7eab0423360a84bbdcd5b7767/examples/qrcode.png -------------------------------------------------------------------------------- /examples/socket.js: -------------------------------------------------------------------------------- 1 | const { SocketClient, isMessageType } = require('../'); 2 | const config = require('./config'); 3 | const client = new SocketClient(config); 4 | 5 | console.log('Supported MessageSenders by SocketClient', client.getMessageSenders()); 6 | 7 | // Listen and react to socket messages 8 | client.on( 9 | 'message', 10 | client.getMessageHandler(message => { 11 | console.log('Message Received', message); 12 | 13 | if (isMessageType(message, 'text')) { 14 | const text = message.data.data.toLowerCase(); 15 | if (text === 'button') { 16 | return client.sendButton( 17 | { 18 | label: 'Open Node.js Client SDK', 19 | color: '#FF0000', 20 | action: 'https://github.com/wangshijun/mixin-node-client', 21 | }, 22 | message 23 | ); 24 | } 25 | 26 | if (text === 'contact') { 27 | return client.sendContact('7701e7bf-2a86-4655-982e-023564fa8945', message); 28 | } 29 | 30 | if (text === 'app') { 31 | return client.sendApp( 32 | { 33 | icon_url: 34 | 'https://images.mixin.one/PQ2dYjNNXYYCCcSi_jDxrh0PJM8XBaiwu4I5_5e7tJhpQNbCVULnc5VRzR4AHF2e7AK6mVpvaHxO0EZr24cUjbg=s256', 35 | title: 'Mixin Node.js SDK', 36 | description: 'Utilities to easy Mixin dapp development', 37 | action: 'https://github.com/wangshijun/mixin-node-client', 38 | }, 39 | message 40 | ); 41 | } 42 | 43 | return client.sendText('Hi there!', message); 44 | } 45 | 46 | return Promise.resolve(message); 47 | }) 48 | ); 49 | 50 | client.on('error', err => console.error(err.message)); 51 | -------------------------------------------------------------------------------- /lib/base.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const crypto = require('crypto'); 3 | const { EventEmitter } = require('events'); 4 | const jwt = require('jsonwebtoken'); 5 | const nano = require('nano-time'); 6 | const { Uint64LE } = require('int64-buffer'); 7 | 8 | const debug = require('debug')(`${require('../package.json').name}:base`); 9 | 10 | const AES_BLOCK_SIZE = 16; 11 | 12 | class MixinClient extends EventEmitter { 13 | constructor({ clientId, clientSecret, assetPin, aesKey, sessionId, privateKey, timeout = 3600 }) { 14 | super(); 15 | 16 | // accept options 17 | this.clientId = clientId; 18 | this.clientSecret = clientSecret; 19 | this.assetPin = assetPin; 20 | this.aesKey = aesKey; 21 | this.sessionId = sessionId; 22 | 23 | this.timeout = timeout; 24 | 25 | if (fs.existsSync(privateKey)) { 26 | this.privateKey = fs.readFileSync(privateKey); 27 | } else { 28 | this.privateKey = privateKey; 29 | } 30 | 31 | // validate options 32 | const requiredOptions = ['clientId', 'clientSecret', 'assetPin', 'aesKey', 'sessionId', 'privateKey']; 33 | for (const key of requiredOptions) { 34 | if (!this[key]) { 35 | throw new Error(`${key} param is required to create an MixinClient instance`); 36 | } 37 | } 38 | } 39 | 40 | getEncryptedPin(assetPin) { 41 | if (!assetPin) { 42 | throw new Error('MixinClient.getEncryptedPin requires an non-empty pin'); 43 | } 44 | 45 | const seconds = Math.floor(Date.now() / 1e3); 46 | const nanoseconds = nano(); 47 | const pinBuffer = Buffer.concat([ 48 | Buffer.from(assetPin, 'utf8'), 49 | new Uint64LE(seconds).toBuffer(), 50 | // eslint-disable-next-line no-implicit-coercion 51 | new Uint64LE(+nanoseconds).toBuffer(), 52 | ]); 53 | 54 | const paddingSize = AES_BLOCK_SIZE - (pinBuffer.length % AES_BLOCK_SIZE); 55 | const paddingArray = []; 56 | for (let i = 0; i < paddingSize; i++) { 57 | paddingArray.push(paddingSize); 58 | } 59 | const paddingBuffer = Buffer.from(paddingArray); 60 | 61 | const encryptBuffer = Buffer.concat([pinBuffer, paddingBuffer]); 62 | const aesKey = Buffer.from(this.aesKey, 'base64'); 63 | const iv16 = crypto.randomBytes(16); 64 | const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv16); 65 | cipher.setAutoPadding(false); 66 | const encryptedPinBuffer = cipher.update(encryptBuffer, 'utf-8'); 67 | return Buffer.from(Buffer.concat([iv16, encryptedPinBuffer])).toString('base64'); 68 | } 69 | 70 | getUUID() { 71 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 72 | const r = (Math.random() * 16) | 0; 73 | const v = c === 'x' ? r : (r & 0x3) | 0x8; 74 | return v.toString(16); 75 | }); 76 | } 77 | 78 | validateUUID(uuid) { 79 | return /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(uuid); 80 | } 81 | 82 | getConversationId(userId, recipientId) { 83 | userId = userId.toString(); 84 | recipientId = recipientId.toString(); 85 | 86 | let [minId, maxId] = [userId, recipientId]; 87 | if (minId > maxId) { 88 | [minId, maxId] = [recipientId, userId]; 89 | } 90 | 91 | const hash = crypto.createHash('md5'); 92 | hash.update(minId); 93 | hash.update(maxId); 94 | const bytes = hash.digest(); 95 | 96 | bytes[6] = (bytes[6] & 0x0f) | 0x30; 97 | bytes[8] = (bytes[8] & 0x3f) | 0x80; 98 | 99 | // eslint-disable-next-line unicorn/prefer-spread 100 | const digest = Array.from(bytes, byte => `0${(byte & 0xff).toString(16)}`.slice(-2)).join(''); 101 | const uuid = `${digest.slice(0, 8)}-${digest.slice(8, 12)}-${digest.slice(12, 16)}-${digest.slice( 102 | 16, 103 | 20 104 | )}-${digest.slice(20, 32)}`; 105 | return uuid; 106 | } 107 | 108 | getRequestSignature(method, uri, body) { 109 | const payload = method + uri + (typeof body === 'object' ? JSON.stringify(body) : body.toString()); 110 | const signature = crypto 111 | .createHash('sha256') 112 | .update(payload) 113 | .digest('hex'); 114 | 115 | debug('getRequestSignature', { method, uri, body, payload, signature }); 116 | return signature; 117 | } 118 | 119 | getJwtToken(method, uri, body) { 120 | const issuedAt = Math.floor(Date.now() / 1000); 121 | const expireAt = issuedAt + this.timeout; 122 | const payload = { 123 | uid: this.clientId, 124 | sid: this.sessionId, 125 | iat: issuedAt, 126 | exp: expireAt, 127 | jti: this.getUUID(), 128 | sig: this.getRequestSignature(method, uri, body), 129 | }; 130 | const token = jwt.sign(payload, this.privateKey, { algorithm: 'RS512' }); 131 | debug('getJwtToken', { method, uri, body, payload, token }); 132 | 133 | return token; 134 | } 135 | 136 | getEndpoints() { 137 | return this.endpoints || []; 138 | } 139 | 140 | getMessageSenders() { 141 | return this.messageSenders || []; 142 | } 143 | } 144 | 145 | module.exports = MixinClient; 146 | -------------------------------------------------------------------------------- /lib/endpoints.js: -------------------------------------------------------------------------------- 1 | const { types } = require('./message'); 2 | 3 | const messageTypes = Object.keys(types).map(x => types[x].category); 4 | const validateAssetId = (assetId, client) => (client.validateUUID(assetId) ? true : new Error('assetId not valid')); 5 | const validateAddressId = (addressId, client) => 6 | client.validateUUID(addressId) ? true : new Error('addressId not valid'); 7 | 8 | module.exports = { 9 | // mixin network related api: https://developers.mixin.one/api/alpha-mixin-network/beginning/ 10 | // assets 11 | getUserAssets: { 12 | method: 'get', 13 | url: '/assets', 14 | }, 15 | getUserAsset: { 16 | method: 'get', 17 | url: '/assets/{assetId}', 18 | param: { 19 | name: 'assetId', 20 | type: String, 21 | validate: validateAssetId, 22 | }, 23 | }, 24 | getNetworkAssets: { 25 | method: 'get', 26 | url: '/network/assets/top', 27 | }, 28 | getNetworkAsset: { 29 | method: 'get', 30 | url: '/network/assets/{assetId}', 31 | param: { 32 | name: 'assetId', 33 | type: String, 34 | validate: validateAssetId, 35 | }, 36 | }, 37 | 38 | // addresses 39 | getWithdrawAddress: { 40 | method: 'get', 41 | url: '/assets/{assetId}/addresses', 42 | param: { 43 | name: 'assetId', 44 | type: String, 45 | validate: validateAssetId, 46 | }, 47 | }, 48 | createWithdrawAddress: { 49 | method: 'post', 50 | url: '/addresses', 51 | payload: { 52 | format: (args, client) => { 53 | const payload = { 54 | asset_id: args.assetId, 55 | public_key: args.publicKey, 56 | account_name: args.accountName, 57 | account_tag: args.accountTag, 58 | label: args.label, 59 | pin: client.getEncryptedPin(client.assetPin), 60 | }; 61 | 62 | return Object.keys(payload) 63 | .filter(x => payload[x] !== undefined) 64 | .reduce((obj, x) => { 65 | obj[x] = payload[x]; 66 | return obj; 67 | }, {}); 68 | }, 69 | validate: (args, client) => { 70 | if (!client.validateUUID(args.assetId)) { 71 | return new Error('assetId must be a valid UUID'); 72 | } 73 | if (!args.publicKey) { 74 | return new Error('publicKey must not be empty'); 75 | } 76 | 77 | return true; 78 | }, 79 | }, 80 | }, 81 | deleteWithdrawAddress: { 82 | method: 'post', 83 | url: '/addresses/{addressId}/delete', 84 | param: { 85 | name: 'addressId', 86 | type: String, 87 | description: 'addressId', 88 | validate: validateAddressId, 89 | }, 90 | payload: { 91 | format: (args, client) => ({ 92 | pin: client.getEncryptedPin(client.assetPin), 93 | }), 94 | }, 95 | }, 96 | 97 | // deposit & withdraw 98 | withdraw: { 99 | method: 'post', 100 | url: '/withdrawals', 101 | payload: { 102 | format: (args, client) => ({ 103 | address_id: args.addressId, 104 | amount: String(args.amount), 105 | pin: client.getEncryptedPin(client.assetPin), 106 | trace_id: args.traceId || client.getUUID(), 107 | memo: args.memo || '', 108 | }), 109 | validate: (args, client) => { 110 | if (!client.validateUUID(args.addressId)) { 111 | throw new Error('args.addressId is required'); 112 | } 113 | if (Number(args.amount) < 0) { 114 | throw new Error('args.amount must be greater thant 0'); 115 | } 116 | if (args.traceId && !client.validateUUID(args.traceId)) { 117 | throw new Error('args.traceId must be valid UUID'); 118 | } 119 | 120 | return true; 121 | }, 122 | }, 123 | }, 124 | deposit: { 125 | method: 'get', 126 | url: '/assets/{assetId}', 127 | param: { 128 | name: 'assetId', 129 | type: String, 130 | validate: validateAssetId, 131 | }, 132 | }, 133 | 134 | // snapshots 135 | getSnapshots: { 136 | method: 'get', 137 | url: '/network/snapshots', 138 | param: { 139 | validate: (args, client) => { 140 | const limit = Number(args.limit); 141 | if (limit <= 0 || limit > 500) { 142 | throw new Error('args.limit must between 0 ~ 500'); 143 | } 144 | if (!args.offset) { 145 | throw new Error('args.offset is required'); 146 | } 147 | if (args.asset && !client.validateUUID(args.asset)) { 148 | throw new Error('args.assetId is invalid'); 149 | } 150 | if (args.order && !['ASC', 'DESC'].includes(args.order)) { 151 | throw new Error('args.assetId is invalid'); 152 | } 153 | 154 | return true; 155 | }, 156 | }, 157 | }, 158 | getSnapshot: { 159 | method: 'get', 160 | url: '/network/snapshots/{snapshotId}', 161 | param: { 162 | name: 'snapshotId', 163 | type: String, 164 | validate: (snapshotId, client) => (client.validateUUID(snapshotId) ? true : new Error('snapshotId not valid')), 165 | }, 166 | }, 167 | 168 | // pin code 169 | verifyPin: { 170 | method: 'post', 171 | url: '/pin/verify', 172 | payload: { 173 | format: (pin, client) => ({ pin: client.getEncryptedPin(pin) }), 174 | validate: pin => { 175 | if (!/\d{6}/.test(pin)) { 176 | return new Error('pin must be 6 digit number'); 177 | } 178 | 179 | return true; 180 | }, 181 | }, 182 | }, 183 | updatePin: { 184 | method: 'post', 185 | url: '/pin/update', 186 | payload: { 187 | format: (args, client) => ({ 188 | old_pin: args.oldPin ? client.getEncryptedPin(args.oldPin) : '', 189 | pin: client.getEncryptedPin(args.newPin), 190 | }), 191 | validate: args => { 192 | if (!/\d{6}/.test(args.newPin)) { 193 | return new Error('newPin must be 6 digit number'); 194 | } 195 | if (args.oldPin && args.oldPin === args.newPin) { 196 | return new Error('newPin must be different with oldPin'); 197 | } 198 | if (args.oldPin && !/\d{6}/.test(args.oldPin)) { 199 | return new Error('oldPin must be 6 digit number'); 200 | } 201 | 202 | return true; 203 | }, 204 | }, 205 | }, 206 | 207 | // payment and transfer 208 | createTransfer: { 209 | method: 'post', 210 | url: '/transfers', 211 | payload: { 212 | format: (args, client) => { 213 | const payload = { 214 | asset_id: args.assetId, 215 | opponent_id: args.recipientId, 216 | trace_id: args.traceId, 217 | amount: String(args.amount), 218 | memo: args.memo || '', 219 | pin: client.getEncryptedPin(client.assetPin), 220 | }; 221 | 222 | return Object.keys(payload) 223 | .filter(x => payload[x] !== undefined) 224 | .reduce((obj, x) => { 225 | obj[x] = payload[x]; 226 | return obj; 227 | }, {}); 228 | }, 229 | validate: (args, client) => { 230 | if (!client.validateUUID(args.assetId)) { 231 | return new Error('assetId must be a valid UUID'); 232 | } 233 | if (!client.validateUUID(args.recipientId)) { 234 | return new Error('recipientId must be a valid UUID'); 235 | } 236 | if (!client.validateUUID(args.traceId)) { 237 | return new Error('traceId must be a valid UUID'); 238 | } 239 | if (Number(args.amount) <= 0) { 240 | return new Error('amount must be a number string'); 241 | } 242 | 243 | return true; 244 | }, 245 | }, 246 | }, 247 | getTransfer: { 248 | method: 'get', 249 | url: '/transfers/trace/{traceId}', 250 | param: { 251 | name: 'traceId', 252 | type: String, 253 | validate: (traceId, client) => (client.validateUUID(traceId) ? true : new Error('traceId not valid')), 254 | }, 255 | }, 256 | verifyPayment: { 257 | method: 'post', 258 | url: '/payments', 259 | payload: { 260 | format: args => { 261 | const payload = { 262 | asset_id: args.assetId, 263 | opponent_id: args.recipientId, 264 | trace_id: args.traceId, 265 | amount: String(args.amount), 266 | }; 267 | 268 | return Object.keys(payload) 269 | .filter(x => payload[x] !== undefined) 270 | .reduce((obj, x) => { 271 | obj[x] = payload[x]; 272 | return obj; 273 | }, {}); 274 | }, 275 | validate: (args, client) => { 276 | if (!client.validateUUID(args.assetId)) { 277 | return new Error('assetId must be a valid UUID'); 278 | } 279 | if (!client.validateUUID(args.recipientId)) { 280 | return new Error('recipientId must be a valid UUID'); 281 | } 282 | if (!client.validateUUID(args.traceId)) { 283 | return new Error('traceId must be a valid UUID'); 284 | } 285 | if (Number(args.amount) <= 0) { 286 | return new Error('amount must be a number string'); 287 | } 288 | 289 | return true; 290 | }, 291 | }, 292 | }, 293 | 294 | // mixin messenger related api: https://developers.mixin.one/api/beta-mixin-message/beginning/ 295 | getProfile: { 296 | method: 'get', 297 | url: '/me', 298 | }, 299 | updatePreference: { 300 | method: 'post', 301 | url: '/me/preferences', 302 | payload: { 303 | format: args => ({ 304 | receive_message_source: args.receiveMessageSource, 305 | accept_conversation_source: args.acceptConversationSource, 306 | }), 307 | validate: args => { 308 | const types = ['EVERYBODY', 'CONTACTS']; 309 | if (!types.includes(args.receiveMessageSource)) { 310 | return new Error('receiveMessageSource must be valid'); 311 | } 312 | if (!types.includes(args.acceptConversationSource)) { 313 | return new Error('acceptConversationSource must be valid'); 314 | } 315 | 316 | return true; 317 | }, 318 | }, 319 | }, 320 | updateProfile: { 321 | method: 'post', 322 | url: '/me', 323 | payload: { 324 | format: args => ({ 325 | full_name: args.fullName, 326 | avatar_base64: args.avatar, 327 | }), 328 | validate: args => { 329 | if (!args.fullName) { 330 | return new Error('fullName must not be empty'); 331 | } 332 | 333 | return true; 334 | }, 335 | }, 336 | }, 337 | getUser: { 338 | method: 'get', 339 | url: '/users/{userId}', 340 | param: { 341 | name: 'userId', 342 | type: String, 343 | description: 'userId must not be empty', 344 | validate: (userId, client) => (client.validateUUID(userId) ? true : new Error('userId not valid')), 345 | }, 346 | }, 347 | getUsers: { 348 | method: 'post', 349 | url: '/users/fetch', 350 | payload: { 351 | validate: (ids, client) => { 352 | if (!Array.isArray(ids)) { 353 | return new Error('userId list must not be empty'); 354 | } 355 | if (!ids.every(id => client.validateUUID(id))) { 356 | return new Error('each userId must be valid'); 357 | } 358 | 359 | return true; 360 | }, 361 | }, 362 | }, 363 | 364 | // friends and contacts 365 | getFriends: { 366 | method: 'get', 367 | url: '/friends', 368 | }, 369 | getContacts: { 370 | method: 'get', 371 | url: '/contacts', 372 | }, 373 | 374 | // conversations 375 | createConversation: { 376 | method: 'post', 377 | url: '/conversations', 378 | payload: { 379 | format: (args, client) => { 380 | const participants = args.participants.map(x => { 381 | if (typeof x === 'string') { 382 | return { user_id: x, action: 'ADD', role: '' }; 383 | } 384 | 385 | // TODO: validate each participant? 386 | return x; 387 | }); 388 | 389 | const conversationId = 390 | args.category === 'CONTACT' 391 | ? client.getConversationId(client.clientId, participants[0].user_id) 392 | : client.getUUID(); 393 | 394 | return { 395 | category: args.category, 396 | conversation_id: args.conversationId || conversationId, // create | update conversation 397 | participants, 398 | }; 399 | }, 400 | validate: args => { 401 | if (!['GROUP', 'CONTACT'].includes(args.category)) { 402 | return new Error('category must be valid'); 403 | } 404 | // TODO: support group conversation member add/remove/edit 405 | if (!Array.isArray(args.participants) || args.participants.length === 0) { 406 | return new Error('participants must be a valid array'); 407 | } 408 | 409 | return true; 410 | }, 411 | }, 412 | }, 413 | readConversation: { 414 | method: 'get', 415 | url: '/conversations/{conversationId}', 416 | param: { 417 | name: 'conversationId', 418 | type: String, 419 | validate: (conversationId, client) => 420 | client.validateUUID(conversationId) ? true : new Error('conversationId not valid'), 421 | }, 422 | }, 423 | 424 | // messages 425 | sendMessage: { 426 | method: 'post', 427 | url: '/messages', 428 | payload: { 429 | format: (args, client) => ({ 430 | category: args.category, 431 | conversation_id: args.conversationId, 432 | message_id: args.messageId || client.getUUID(), 433 | data: args.data, 434 | }), 435 | validate: (args, client) => { 436 | if (!messageTypes.includes(args.category)) { 437 | return new Error('category must be valid'); 438 | } 439 | if (!args.data) { 440 | return new Error('message must not be empty'); 441 | } 442 | if (!client.validateUUID(args.conversationId)) { 443 | return new Error('conversationId must not be empty'); 444 | } 445 | 446 | return true; 447 | }, 448 | }, 449 | }, 450 | createUser: { 451 | method: 'post', 452 | url: '/users', 453 | payload: { 454 | format: args => ({ 455 | full_name: args.full_name, 456 | session_secret: args.session_secret, 457 | }), 458 | validate: args => { 459 | if (!args.full_name) { 460 | return new Error('fullName must not be empty'); 461 | } 462 | if (!args.session_secret) { 463 | return new Error('session_secret must not be empty'); 464 | } 465 | return true; 466 | }, 467 | }, 468 | }, 469 | }; 470 | -------------------------------------------------------------------------------- /lib/http.js: -------------------------------------------------------------------------------- 1 | const qs = require('querystring'); 2 | const axios = require('axios'); 3 | const strformat = require('strformat'); 4 | const axiosRetry = require('axios-retry'); 5 | const camelcase = require('camelcase'); 6 | const camelcaseKeys = require('camelcase-keys'); 7 | 8 | const debug = require('debug')(`${require('../package.json').name}:http`); 9 | 10 | const { types, createMessage } = require('./message'); 11 | const endpoints = require('./endpoints'); 12 | const BaseClient = require('./base'); 13 | 14 | class HttpClient extends BaseClient { 15 | constructor(args) { 16 | super(args); 17 | 18 | this._api = axios.create({ 19 | baseURL: args.httpEndpoint || 'https://api.mixin.one', 20 | timeout: 60000, 21 | }); 22 | 23 | axiosRetry(this._api, { retries: 3, retryDelay: axiosRetry.exponentialDelay }); 24 | 25 | this._initEndpoints(); 26 | this._initMessageSenders(); 27 | } 28 | 29 | /** 30 | * Get OAuth access_token by authorization code 31 | * 32 | * @param {String} code 33 | * @returns Promise 34 | * @memberof HttpClient 35 | */ 36 | async getOAuthToken(code) { 37 | const payload = { 38 | code, 39 | client_id: this.clientId, 40 | client_secret: this.clientSecret, 41 | }; 42 | const res = await this._api.post('/oauth/token', payload); 43 | const { data, error } = res.data; 44 | if (error) { 45 | const err = new Error(error.description); 46 | err.code = error.code; 47 | err.status = error.status; 48 | throw err; 49 | } 50 | 51 | return data; 52 | } 53 | 54 | /** 55 | * Transfer from dapp to mixin messenger user 56 | * 57 | * @param {Object} { assetId, recipientId, traceId, amount, memo = '' } 58 | * @returns Promise 59 | * @memberof HttpClient 60 | */ 61 | async transferFromBot({ assetId, recipientId, traceId, amount, memo = '' }) { 62 | return this.createTransfer({ 63 | assetId, 64 | recipientId, 65 | traceId: traceId || this.getUUID(), 66 | amount, 67 | memo, 68 | }); 69 | } 70 | 71 | /** 72 | * Send request to mixin API 73 | * 74 | * When authToken is provided, we are requesting on behalf of the user 75 | * If not, we are requesting on behalf of the dapp 76 | * 77 | * @param {*} method 78 | * @param {*} endpoint 79 | * @param {*} payload 80 | * @param {string} [authToken=''] 81 | * @returns 82 | * @memberof HttpClient 83 | */ 84 | async _doRequest(method, endpoint, payload, authToken = '') { 85 | const methodUpper = method.toUpperCase(); 86 | const methodLower = method.toLowerCase(); 87 | const token = authToken || this.getJwtToken(methodUpper, endpoint, payload); 88 | const res = await this._api({ 89 | method: methodLower, 90 | url: endpoint, 91 | data: payload, 92 | headers: { 93 | Authorization: `Bearer ${token}`, 94 | }, 95 | }); 96 | 97 | debug('_doRequest', { method, endpoint, payload, response: res.data }); 98 | return res.data; 99 | } 100 | 101 | _initEndpoints() { 102 | this.endpoints = []; 103 | 104 | Object.keys(endpoints).forEach(key => { 105 | const endpoint = endpoints[key]; 106 | if (!['get', 'post'].includes(endpoint.method) || endpoint.deprecated) { 107 | console.warn('Ignore invalid MixinClient endpoint declaration', endpoint); 108 | return; 109 | } 110 | 111 | debug('Generate method for: ', key); 112 | 113 | /** 114 | * An Generated shortcut methods to get data from mixin network 115 | * 116 | * @param {Undefined|String|Number|Object} queryParam 117 | * @param {Undefined|Object} postPayload 118 | * @param {String} authToken 119 | * @returns Promise 120 | */ 121 | this[key] = async (queryParam, postPayload, authToken = '') => { 122 | let { url, param, payload, method } = endpoint; 123 | 124 | // Compose query param 125 | if (param) { 126 | const error = typeof param.validate === 'function' ? param.validate(queryParam, this) : false; 127 | if (typeof error !== 'boolean') { 128 | throw error; 129 | } 130 | if (typeof queryParam === 'object') { 131 | url = `${url}?${qs.stringify(queryParam)}`; 132 | } else if (typeof queryParam !== 'undefined') { 133 | url = strformat(endpoint.url, { [param.name]: queryParam.toString() }); 134 | } 135 | debug('QueryParam', { spec: url, value: queryParam }); 136 | } else { 137 | authToken = postPayload; 138 | postPayload = queryParam; 139 | } 140 | 141 | // Compose post payload 142 | let _payload = ''; 143 | if (payload) { 144 | const error = typeof payload.validate === 'function' ? payload.validate(postPayload, this) : false; 145 | if (error && typeof error !== 'boolean') { 146 | throw error; 147 | } 148 | 149 | _payload = typeof payload.format === 'function' ? payload.format(postPayload, this) : postPayload; 150 | debug('PostPayload', { spec: payload, value: _payload }); 151 | } 152 | 153 | // Error handling 154 | const { error, data } = await this._doRequest(method.toUpperCase(), url, _payload, authToken); 155 | if (error) { 156 | const err = new Error(error.description); 157 | err.code = error.code; 158 | err.status = error.status; 159 | throw err; 160 | } 161 | 162 | return data; 163 | }; 164 | 165 | this.endpoints.push(key); 166 | }); 167 | } 168 | 169 | _initMessageSenders() { 170 | this.messageSenders = []; 171 | if (typeof this.sendMessage !== 'function') { 172 | return; 173 | } 174 | 175 | Object.keys(types).forEach(x => { 176 | const key = camelcase(['send', x]); 177 | this[key] = async ({ recipientId, conversationId, data }) => { 178 | const message = createMessage(x, { conversationId, recipientId, [x]: data }, this.getUUID); 179 | return this.sendMessage(camelcaseKeys(message.params)); 180 | }; 181 | 182 | debug(`Generate message sender fn: ${key}`); 183 | this.messageSenders.push(key); 184 | }); 185 | } 186 | } 187 | 188 | module.exports = HttpClient; 189 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generate by [js2dts@0.3.2](https://github.com/whxaxes/js2dts#readme) 2 | 3 | export interface T101 { 4 | clientId: any; 5 | clientSecret: any; 6 | assetPin: any; 7 | aesKey: any; 8 | sessionId: any; 9 | privateKey: any; 10 | timeout?: number; 11 | } 12 | declare class MixinClient { 13 | clientId: any; 14 | clientSecret: any; 15 | assetPin: any; 16 | aesKey: any; 17 | sessionId: any; 18 | timeout: number; 19 | privateKey: any; 20 | constructor(T100: T101); 21 | getEncryptedPin(assetPin: any): any; 22 | getUUID(): string; 23 | validateUUID(uuid: any): boolean; 24 | getConversationId(userId: any, recipientId: any): string; 25 | getRequestSignature(method: any, uri: any, body: any): any; 26 | getJwtToken(method: any, uri: any, body: any): any; 27 | getEndpoints(): any; 28 | getMessageSenders(): any; 29 | } 30 | declare class HttpClient_1 extends MixinClient { 31 | constructor(args: any); 32 | /** 33 | * Get OAuth access_token by authorization code 34 | * 35 | * @param {String} code 36 | * @returns Promise 37 | * @memberof HttpClient 38 | */ 39 | getOAuthToken(code: string): Promise; 40 | /** 41 | * Transfer from dapp to mixin messenger user 42 | * 43 | * @param {Object} { assetId, recipientId, traceId, amount, memo = '' } 44 | * @returns Promise 45 | * @memberof HttpClient 46 | */ 47 | transferFromBot(T102: any): Promise; 48 | } 49 | export const HttpClient: typeof HttpClient_1; 50 | declare class SocketClient_1 extends MixinClient { 51 | autoStart: any; 52 | listPendingMessage: any; 53 | url: any; 54 | protocols: any; 55 | socket: any; 56 | isConnected: boolean; 57 | reconnectTimeoutId: number; 58 | reconnectInterval: any; 59 | shouldAttemptReconnect: boolean; 60 | constructor(args: any); 61 | start(): void; 62 | destroy(): void; 63 | sendRaw(message: any): Promise; 64 | /** 65 | * Wrap a message handler to auto confirm message recipient after handling 66 | * @param {*} messageId 67 | */ 68 | getMessageHandler(handler: any): (message: any) => void; 69 | /** 70 | * Enable pending message fetching 71 | */ 72 | listPendingMessages(): Promise; 73 | /** 74 | * Decode message to normalized object 75 | * @param {*} data 76 | */ 77 | decode(data: any): Promise; 78 | ping(): Promise; 79 | } 80 | export const SocketClient: typeof SocketClient_1; 81 | export function isMessageType(message: any, type: any): boolean; 82 | export interface T103 { 83 | conversation_id: any; 84 | category: any; 85 | status: string; 86 | message_id: any; 87 | data: any; 88 | } 89 | export interface T104 { 90 | id: any; 91 | action: string; 92 | params: T103; 93 | } 94 | export function createMessage(type: any, args: any, genUUID: any): T104; 95 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | exports.HttpClient = require('./http'); 2 | exports.SocketClient = require('./socket'); 3 | exports.isMessageType = require('./message').isMessageType; 4 | exports.createMessage = require('./message').createMessage; 5 | -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | // link https://developers.mixin.one/api/beta-mixin-message/websocket-messages/ 2 | const fs = require('fs'); 3 | 4 | const types = { 5 | text: { 6 | category: 'PLAIN_TEXT', 7 | validate: ({ text }) => Boolean(text.toString()), 8 | format: ({ text }) => Buffer.from(text.toString()).toString('base64'), 9 | }, 10 | image: { 11 | category: 'PLAIN_IMAGE', 12 | validate: ({ image }) => Boolean(image), 13 | format: ({ image }) => { 14 | // from attachment 15 | if (typeof image === 'object') { 16 | return Buffer.from(JSON.stringify(image)).toString('base64'); 17 | } 18 | 19 | // from local file 20 | if (fs.existsSync(image)) { 21 | return Buffer.from(fs.readFileSync(image)).toString('base64'); 22 | } 23 | 24 | // image buffer 25 | return image; 26 | }, 27 | }, 28 | video: { 29 | category: 'PLAIN_VIDEO', 30 | validate: ({ video }) => Boolean(video), 31 | format: ({ video }) => { 32 | // from attachment 33 | if (typeof video === 'object') { 34 | return Buffer.from(JSON.stringify(video)).toString('base64'); 35 | } 36 | 37 | // from local file 38 | if (fs.existsSync(video)) { 39 | return Buffer.from(fs.readFileSync(video)).toString('base64'); 40 | } 41 | 42 | // video buffer 43 | return video; 44 | }, 45 | }, 46 | data: { 47 | category: 'PLAIN_DATA', 48 | validate: ({ data }) => Boolean(data), 49 | format: ({ data }) => Buffer.from(data.toString()).toString('base64'), 50 | }, 51 | // TODO: example for this 52 | sticker: { 53 | category: 'PLAIN_STICKER', 54 | validate: ({ sticker }) => sticker && sticker.name && sticker.album_id, 55 | format: ({ sticker }) => Buffer.from(JSON.stringify(sticker)).toString('base64'), 56 | }, 57 | contact: { 58 | category: 'PLAIN_CONTACT', 59 | validate: ({ contact }) => Boolean(contact), 60 | format: ({ contact }) => Buffer.from(JSON.stringify({ user_id: contact })).toString('base64'), 61 | }, 62 | button: { 63 | category: 'APP_BUTTON_GROUP', 64 | validate: ({ button }) => button && button.label && button.color && button.action, 65 | format: ({ button }) => Buffer.from(JSON.stringify([button])).toString('base64'), 66 | }, 67 | buttons: { 68 | category: 'APP_BUTTON_GROUP', 69 | validate: ({ buttons }) => Array.isArray(buttons) && buttons.every(x => x.label && x.color && x.action), 70 | format: ({ buttons }) => Buffer.from(JSON.stringify(buttons)).toString('base64'), 71 | }, 72 | app: { 73 | category: 'APP_CARD', 74 | validate: ({ app }) => app && app.icon_url && app.title && app.description && app.action, 75 | format: ({ app }) => Buffer.from(JSON.stringify(app)).toString('base64'), 76 | }, 77 | }; 78 | 79 | const createMessage = (type, args, genUUID) => { 80 | const spec = types[type]; 81 | if (!spec) { 82 | throw new Error(`createMessage: invalid message type: ${type}`); 83 | } 84 | 85 | if (!spec.validate(args)) { 86 | throw new Error(`createMessage: invalid message body: ${JSON.stringify(args)}`); 87 | } 88 | 89 | return { 90 | id: genUUID(), 91 | action: 'CREATE_MESSAGE', 92 | params: { 93 | conversation_id: args.conversationId, 94 | category: types[type].category, 95 | status: 'SENT', 96 | message_id: genUUID(), 97 | data: spec.format(args), 98 | }, 99 | }; 100 | }; 101 | 102 | const isMessageType = (message, type) => { 103 | if (!types[type]) { 104 | return false; 105 | } 106 | 107 | if (message.data && message.data.category === types[type].category && message.data.data) { 108 | return true; 109 | } 110 | 111 | return false; 112 | }; 113 | 114 | exports.createMessage = createMessage; 115 | exports.isMessageType = isMessageType; 116 | exports.types = Object.freeze(types); 117 | -------------------------------------------------------------------------------- /lib/socket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Websocket client that can reconnect on failure 3 | * 4 | * Please note that this file is refactored from https://github.com/virushuo/mixin-node/blob/master/ws-reconnect.js 5 | * Debug feature is added 6 | * 7 | * Data Structure for Mixin WebSocket Message Object (named as `msgObj` in following context) 8 | * 9 | * { 10 | * id: 'e1e850e3-2a5a-4510-a18d-20a6267a9d87', 11 | * action: 'CREATE_MESSAGE', 12 | * data: { 13 | * type: 'message', 14 | * representative_id: '', 15 | * quote_message_id: '', 16 | * conversation_id: '1514dd62-0127-398c-b95a-012d841de265', 17 | * user_id: '7701e7bf-2a86-4655-982e-023564fa8945', 18 | * message_id: '93c7dcbb-c72c-499f-bb2e-64450dcad852', 19 | * category: 'PLAIN_TEXT', 20 | * data: 'emhlIHNoaSBnZSBzaGE=', 21 | * status: 'SENT', 22 | * source: 'CREATE_MESSAGE', 23 | * created_at: '2018-12-02T07:36:41.764171Z', 24 | * updated_at: '2018-12-02T07:36:41.764171Z', 25 | * }, 26 | * } 27 | */ 28 | const zlib = require('zlib'); 29 | const util = require('util'); 30 | const WebSocket = require('ws'); 31 | const camelcase = require('camelcase'); 32 | const interval = require('interval-promise'); 33 | const debug = require('debug')(`${require('../package.json').name}:socket`); 34 | 35 | const { types, createMessage } = require('./message'); 36 | const BaseClient = require('./base'); 37 | 38 | class SocketClient extends BaseClient { 39 | constructor(args) { 40 | super(args); 41 | 42 | const socketUrl = args.socketUrl || 'wss://blaze.mixin.one/'; 43 | const socketProtocols = args.socketProtocols || 'Mixin-Blaze-1'; 44 | 45 | this.autoStart = args.autoStart || true; 46 | this.listPendingMessage = args.listPendingMessage || true; 47 | this.url = socketUrl && socketUrl.indexOf('ws') === -1 ? 'ws://' + socketUrl : socketUrl; 48 | this.protocols = socketProtocols; 49 | this.socket = null; 50 | this.isConnected = false; 51 | this.reconnectTimeoutId = 0; 52 | this.reconnectInterval = args.reconnectInterval === undefined ? 5 : args.reconnectInterval; 53 | this.shouldAttemptReconnect = Boolean(this.reconnectInterval); 54 | 55 | this._initMessageSenders(); 56 | 57 | if (this.autoStart) { 58 | this.start(); 59 | } 60 | } 61 | 62 | start() { 63 | const headers = { 64 | Authorization: `Bearer ${this.getJwtToken('GET', '/', '')}`, 65 | perMessageDeflate: false, 66 | }; 67 | 68 | this.shouldAttemptReconnect = Boolean(this.reconnectInterval); 69 | this.isConnected = false; 70 | this.socket = new WebSocket(this.url, this.protocols, { headers }); 71 | this.socket.onmessage = this._onMessage.bind(this); 72 | this.socket.onopen = this._onOpen.bind(this); 73 | this.socket.onerror = this._onError.bind(this); 74 | this.socket.onclose = this._onClose.bind(this); 75 | 76 | interval(async (iteration, _) => { 77 | debug('heartbeat check', { iteration }); 78 | try { 79 | await this.ping(); 80 | } catch (err) { 81 | this.socket.terminate(); 82 | this.start(); 83 | } 84 | }, 30 * 1000); 85 | } 86 | 87 | destroy() { 88 | clearTimeout(this.reconnectTimeoutId); 89 | this.shouldAttemptReconnect = false; 90 | this.socket.close(); 91 | } 92 | 93 | sendRaw(message) { 94 | debug('sendRawMessage', message); 95 | return new Promise((resolve, reject) => { 96 | try { 97 | const buffer = Buffer.from(JSON.stringify(message), 'utf-8'); 98 | zlib.gzip(buffer, (_, zipped) => { 99 | if (this.socket.readyState === WebSocket.OPEN) { 100 | this.socket.send(zipped); 101 | resolve(); 102 | } else { 103 | reject(new Error('Socket connection not ready')); 104 | } 105 | }); 106 | } catch (err) { 107 | reject(err); 108 | } 109 | }); 110 | } 111 | 112 | _initMessageSenders() { 113 | this.messageSenders = []; 114 | if (typeof this.sendRaw !== 'function') { 115 | return; 116 | } 117 | 118 | Object.keys(types).forEach(x => { 119 | const key = camelcase(['send', x]); 120 | this[key] = async (data, msgObj) => { 121 | const message = createMessage(x, { conversationId: msgObj.data.conversation_id, [x]: data }, this.getUUID); 122 | return this.sendRaw(message); 123 | }; 124 | 125 | debug(`Generate message sender fn: ${key}`); 126 | this.messageSenders.push(key); 127 | }); 128 | } 129 | 130 | /** 131 | * Wrap a message handler to auto confirm message recipient after handling 132 | * @param {*} messageId 133 | */ 134 | getMessageHandler(handler) { 135 | const promised = util.promisify(handler); 136 | return message => { 137 | promised(message).then(result => { 138 | if ( 139 | message.action && 140 | message.action !== 'ACKNOWLEDGE_MESSAGE_RECEIPT' && 141 | message.action !== 'LIST_PENDING_MESSAGES' 142 | ) { 143 | return new Promise((resolve, reject) => { 144 | this.sendRaw({ 145 | id: this.getUUID(), 146 | action: 'ACKNOWLEDGE_MESSAGE_RECEIPT', 147 | params: { 148 | message_id: message.data.message_id, 149 | status: 'READ', 150 | }, 151 | }) 152 | .then(() => resolve(result)) 153 | .catch(reject); 154 | }); 155 | } 156 | 157 | return result; 158 | }); 159 | }; 160 | } 161 | 162 | /** 163 | * Enable pending message fetching 164 | */ 165 | listPendingMessages() { 166 | return new Promise((resolve, reject) => { 167 | const id = this.getUUID(); 168 | this.sendRaw({ id, action: 'LIST_PENDING_MESSAGES' }) 169 | .then(() => resolve(id)) 170 | .catch(reject); 171 | }); 172 | } 173 | 174 | /** 175 | * Decode message to normalized object 176 | * @param {*} data 177 | */ 178 | decode(data) { 179 | return new Promise((resolve, reject) => { 180 | try { 181 | zlib.gunzip(data, (err, unzipped) => { 182 | if (err) { 183 | return reject(err); 184 | } 185 | const msgObj = JSON.parse(unzipped.toString()); 186 | // TODO: decode messages automatically 187 | if (msgObj && msgObj.action === 'CREATE_MESSAGE' && msgObj.data && msgObj.data.category === 'PLAIN_TEXT') { 188 | msgObj.data.data = Buffer.from(msgObj.data.data, 'base64').toString('utf-8'); 189 | } 190 | resolve(msgObj); 191 | }); 192 | } catch (err) { 193 | reject(err); 194 | } 195 | }); 196 | } 197 | 198 | ping() { 199 | return new Promise((resolve, reject) => { 200 | try { 201 | this.socket.ping(); 202 | let pong = false; 203 | this.socket.once('pong', () => { 204 | pong = true; 205 | resolve(); 206 | }); 207 | setTimeout(() => { 208 | if (!pong) { 209 | reject(new Error('timeout')); 210 | } 211 | }, 5000); 212 | } catch (err) { 213 | reject(err); 214 | } 215 | }); 216 | } 217 | 218 | _onOpen() { 219 | this.isConnected = true; 220 | debug('is connected'); 221 | this.emit('connect'); 222 | 223 | if (this.listPendingMessage) { 224 | this.listPendingMessages().then(receiptId => { 225 | debug(`List pending message: ${receiptId}`); 226 | }); 227 | } 228 | } 229 | 230 | _onClose(event) { 231 | if (this.shouldAttemptReconnect) { 232 | this.emit('closed', event); 233 | clearTimeout(this.reconnectTimeoutId); 234 | this.reconnectTimeoutId = setTimeout(() => { 235 | this.emit('reconnect'); 236 | this.start(); 237 | }, this.reconnectInterval * 1000); 238 | } else { 239 | this.emit('destroyed', event); 240 | } 241 | } 242 | 243 | _onMessage(message) { 244 | debug('onMessage.raw', message.data); 245 | this.decode(message.data) 246 | .then(decoded => { 247 | debug('onMessage.decoded', decoded); 248 | this.emit('message', decoded); 249 | }) 250 | .catch(err => { 251 | console.error('SocketClient.decodeError', err); 252 | this.emit('error', err); 253 | }); 254 | } 255 | 256 | _onError(event) { 257 | this.emit('error', event); 258 | } 259 | } 260 | 261 | module.exports = SocketClient; 262 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mixin-node-client", 3 | "description": "Node.js SDK for Mixin Network", 4 | "version": "0.11.0", 5 | "author": "wangshijun ", 6 | "bugs": { 7 | "url": "https://github.com/wangshijun/mixin-node-client/issues", 8 | "email": "wangshijun2010@gmail.com" 9 | }, 10 | "contributors": [ 11 | "wangshijun " 12 | ], 13 | "dependencies": { 14 | "axios": "^0.20.0", 15 | "axios-retry": "^3.1.9", 16 | "camelcase": "^5.0.0", 17 | "camelcase-keys": "^5.0.0", 18 | "int64-buffer": "^0.1.10", 19 | "interval-promise": "^1.3.0", 20 | "jsonwebtoken": "^8.3.0", 21 | "nano-time": "^1.0.0", 22 | "rfc3339nano": "^1.0.2", 23 | "strformat": "^0.0.7", 24 | "ws": "^6.1.2" 25 | }, 26 | "ava": { 27 | "failFast": true, 28 | "verbose": true 29 | }, 30 | "devDependencies": { 31 | "auto-bind": "^1.1.0", 32 | "ava": "^0.22.0", 33 | "cross-env": "^5.0.5", 34 | "debug": "^3.1.0", 35 | "eslint": "^4.5.0", 36 | "eslint-config-prettier": "latest", 37 | "eslint-plugin-prettier": "latest", 38 | "husky": "^0.14.3", 39 | "js2dts": "^0.3.2", 40 | "lint-staged": "^4.0.4", 41 | "nyc": "^11.1.0", 42 | "prettier": "^1.6.1", 43 | "xo": "latest" 44 | }, 45 | "engines": { 46 | "node": ">=8.3" 47 | }, 48 | "homepage": "https://github.com/wangshijun/mixin-node-client", 49 | "keywords": [ 50 | "mixin", 51 | "blockchain", 52 | "lass", 53 | "node.js", 54 | "crypto" 55 | ], 56 | "license": "MIT", 57 | "lint-staged": { 58 | "*.{js,jsx,mjs,ts,tsx,css,less,scss,json,graphql}": [ 59 | "prettier", 60 | "git add" 61 | ] 62 | }, 63 | "main": "lib/index.js", 64 | "nyc": { 65 | "check-coverage": true, 66 | "lines": 40, 67 | "functions": 25, 68 | "branches": 20, 69 | "reporter": [ 70 | "lcov", 71 | "html", 72 | "text" 73 | ] 74 | }, 75 | "repository": { 76 | "type": "git", 77 | "url": "https://github.com/wangshijun/mixin-node-client" 78 | }, 79 | "xo": { 80 | "extends": "prettier", 81 | "plugins": [ 82 | "prettier" 83 | ], 84 | "parserOptions": { 85 | "sourceType": "script" 86 | }, 87 | "rules": { 88 | "unicorn/number-literal-case": "off", 89 | "prettier/prettier": [ 90 | "error", 91 | { 92 | "printWidth": 120, 93 | "tabWidth": 2, 94 | "trailingComma": "es5", 95 | "jsxBracketSameLine": true, 96 | "semi": true, 97 | "singleQuote": true, 98 | "bracketSpacing": true 99 | } 100 | ], 101 | "max-len": [ 102 | "error", 103 | { 104 | "code": 120, 105 | "ignoreUrls": true 106 | } 107 | ], 108 | "valid-jsdoc": "off", 109 | "unicorn/prefer-add-event-listener": "off", 110 | "promise/prefer-await-to-then": "off", 111 | "capitalized-comments": "off", 112 | "camelcase": "off", 113 | "unicorn/import-index": "off", 114 | "import/newline-after-import": "off", 115 | "no-warning-comments": "off" 116 | }, 117 | "space": true 118 | }, 119 | "scripts": { 120 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov", 121 | "lint": "xo", 122 | "precommit": "lint-staged && npm run test", 123 | "prepublish": "npm run generate-dts", 124 | "test": "npm run lint && npm run test-coverage", 125 | "generate-dts": "j2d lib/index.js && ./tools/patch-dts.sh && node tools/generate-dts.js", 126 | "test-coverage": "cross-env NODE_ENV=test nyc ava" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/http.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const config = require('../examples/config'); 3 | const { HttpClient } = require('../lib'); 4 | 5 | const client = new HttpClient(config); 6 | const recipientId = 'ca630936-5af6-427e-ac4a-864a4c64f372'; // UserId 7 | const senderId = '7701e7bf-2a86-4655-982e-023564fa8945'; // UserID 8 | const assetId = '965e5c6e-434c-3fa9-b780-c50f43cd955c'; // CNB 9 | 10 | test('should returns itself', t => { 11 | const endpoints = client.getEndpoints(); 12 | const senders = client.getMessageSenders(); 13 | t.true(client instanceof HttpClient); 14 | t.true(Array.isArray(endpoints) && endpoints.length > 0); 15 | t.true(Array.isArray(senders) && senders.length > 0); 16 | }); 17 | 18 | test('should generate and validate uuid', t => { 19 | t.true(client.validateUUID(client.getUUID())); 20 | }); 21 | 22 | test('should get bot asset list', async t => { 23 | const assets = await client.getUserAssets(); 24 | t.true(Array.isArray(assets)); 25 | t.true(assets.length > 0); 26 | }); 27 | 28 | test('should can bot get asset item', async t => { 29 | const asset = await client.getUserAsset(assetId); 30 | t.true(Boolean(asset)); 31 | t.true(asset.asset_id === assetId); 32 | }); 33 | 34 | test('should get network asset list', async t => { 35 | const topAssets = await client.getNetworkAssets(); 36 | t.true(Array.isArray(topAssets)); 37 | t.true(topAssets.length > 10); 38 | }); 39 | 40 | test('should get network asset item', async t => { 41 | const topAsset = await client.getNetworkAsset(assetId); 42 | t.true(Boolean(topAsset)); 43 | t.true(topAsset.asset_id === assetId); 44 | }); 45 | 46 | test('should get snapshot list/detail', async t => { 47 | const snapshots = await client.getSnapshots({ limit: 10, asset: assetId, offset: new Date().toString() }); 48 | const snapshot = await client.getSnapshot(snapshots[0].snapshot_id); 49 | t.true(Array.isArray(snapshots)); 50 | t.true(snapshots.length === 10); 51 | t.true(Boolean(snapshot)); 52 | }); 53 | 54 | test('should get profile/user/friends/contacts', async t => { 55 | const [profile, user, users, friends, contacts] = await Promise.all([ 56 | client.getProfile(), 57 | client.getUser(senderId), 58 | client.getUsers([recipientId, senderId]), 59 | client.getFriends(), 60 | client.getContacts(), 61 | ]); 62 | 63 | t.true(Boolean(profile)); 64 | t.true(Boolean(user)); 65 | t.true(Array.isArray(users) && users.length > 0); 66 | t.true(Array.isArray(friends)); 67 | t.true(Array.isArray(contacts)); 68 | }); 69 | 70 | test('should get conversation id', t => { 71 | t.true(client.getConversationId(senderId, recipientId) === client.getConversationId(recipientId, senderId)); 72 | t.true(client.validateUUID(client.getConversationId(senderId, recipientId))); 73 | }); 74 | 75 | test('should create/read/verify transfer', async t => { 76 | const traceId = client.getUUID(); 77 | const createTransfer = await client.createTransfer({ 78 | assetId, 79 | recipientId: senderId, 80 | traceId, 81 | amount: 1, 82 | memo: 'Test', 83 | }); 84 | const readTransfer = await client.getTransfer(traceId); 85 | const verifyPayment = await client.verifyPayment({ 86 | assetId, 87 | recipientId: senderId, 88 | traceId, 89 | amount: 1, 90 | }); 91 | 92 | console.log({ createTransfer, readTransfer, verifyPayment }); 93 | t.true(Boolean(createTransfer)); 94 | t.true(Boolean(readTransfer)); 95 | t.true(Boolean(verifyPayment)); 96 | }); 97 | -------------------------------------------------------------------------------- /test/message.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const config = require('../examples/config'); 3 | const { HttpClient, createMessage } = require('../lib'); 4 | 5 | const client = new HttpClient(config); 6 | const recipientId = 'ca630936-5af6-427e-ac4a-864a4c64f372'; // UserId 7 | const senderId = '7701e7bf-2a86-4655-982e-023564fa8945'; // UserID 8 | 9 | test('should create/verify message', t => { 10 | const conversationId = client.getConversationId(senderId, recipientId); 11 | const message = createMessage( 12 | 'text', 13 | { 14 | conversationId, 15 | text: 'hello world', 16 | }, 17 | client.getUUID 18 | ); 19 | 20 | t.true(message.action === 'CREATE_MESSAGE'); 21 | t.true(client.validateUUID(message.id)); 22 | t.true(client.validateUUID(message.params.message_id)); 23 | t.true(message.params.conversation_id === conversationId); 24 | t.true(message.params.data === Buffer.from('hello world').toString('base64')); 25 | }); 26 | -------------------------------------------------------------------------------- /tools/generate-dts.js: -------------------------------------------------------------------------------- 1 | /* eslint unicorn/no-process-exit:"off" */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { HttpClient, SocketClient } = require('../lib/index'); 5 | const config = require('../examples/config'); 6 | const client = new HttpClient(config); 7 | const socket = new SocketClient(config); 8 | 9 | const httpMethods = client.getEndpoints().map(x => `${x}(params:any): Promise;`); 10 | const socketMethods = socket.getMessageSenders().map(x => `${x}(params:any): Promise;`); 11 | console.log({ httpMethods, socketMethods }); 12 | const filePath = path.join(__dirname, '../lib/index.d.ts'); 13 | let fileContent = fs.readFileSync(filePath).toString(); 14 | fileContent = fileContent.replace(/__HttpClientMethods__/, `\n${httpMethods.concat(socketMethods).join('\n')}\n`); 15 | fileContent = fileContent.replace(/__SocketClientMethods__/, `\n${socketMethods.join('\n')}\n`); 16 | fs.writeFileSync(filePath, fileContent); 17 | 18 | console.log('dts file written', filePath); 19 | process.exit(0); 20 | -------------------------------------------------------------------------------- /tools/patch-dts.sh: -------------------------------------------------------------------------------- 1 | sed -i -E "s/getOAuthToken\(code: string\): Promise;/getOAuthToken(code: string): Promise;__HttpClientMethods__/" lib/index.d.ts 2 | sed -i -E "s/sendRaw\(message: any\): Promise;/sendRaw(message: any): Promise;__SocketClientMethods__/" lib/index.d.ts 3 | 4 | echo "lib/index.d.ts was patched"; 5 | --------------------------------------------------------------------------------