├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── lint.yml │ ├── npm-publish.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── config.default.json ├── example ├── blaze.ts ├── create_user.js ├── message.ts ├── multisig.js ├── nft.js ├── safe_multisig.js ├── safe_tx.js ├── safe_withdraw.js ├── scheme.js └── storage.js ├── index.js ├── jest.config.js ├── keystore.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src ├── blaze │ ├── client.ts │ ├── index.ts │ ├── type.ts │ └── utils.ts ├── client │ ├── address.ts │ ├── app.ts │ ├── asset.ts │ ├── attachment.ts │ ├── circle.ts │ ├── code.ts │ ├── collectible.ts │ ├── conversation.ts │ ├── error.ts │ ├── external.ts │ ├── http.ts │ ├── index.ts │ ├── message.ts │ ├── mixin-client.ts │ ├── multisig.ts │ ├── network.ts │ ├── oauth.ts │ ├── payment.ts │ ├── pin.ts │ ├── safe.ts │ ├── transfer.ts │ ├── types │ │ ├── address.ts │ │ ├── app.ts │ │ ├── asset.ts │ │ ├── attachment.ts │ │ ├── circle.ts │ │ ├── client.ts │ │ ├── code.ts │ │ ├── collectible.ts │ │ ├── conversation.ts │ │ ├── error.ts │ │ ├── external.ts │ │ ├── index.ts │ │ ├── invoice.ts │ │ ├── keystore.ts │ │ ├── message.ts │ │ ├── mixin_transaction.ts │ │ ├── multisig.ts │ │ ├── network.ts │ │ ├── nfo.ts │ │ ├── oauth.ts │ │ ├── payment.ts │ │ ├── safe.ts │ │ ├── snapshot.ts │ │ ├── transaction.ts │ │ ├── transfer.ts │ │ ├── user.ts │ │ └── utxo.ts │ ├── user.ts │ ├── utils │ │ ├── address.ts │ │ ├── amount.ts │ │ ├── auth.ts │ │ ├── base64.ts │ │ ├── client.ts │ │ ├── computer.ts │ │ ├── decoder.ts │ │ ├── ed25519.ts │ │ ├── encoder.ts │ │ ├── index.ts │ │ ├── invoice.ts │ │ ├── multisigs.ts │ │ ├── nfo.ts │ │ ├── pin.ts │ │ ├── safe.ts │ │ ├── sleep.ts │ │ ├── tip.ts │ │ └── uniq.ts │ └── utxo.ts ├── constant.ts ├── index.ts ├── types.ts └── webview │ ├── client.ts │ ├── index.ts │ └── type.ts ├── test ├── address.test.ts ├── common.ts ├── crypto.ts ├── forge.test.ts ├── keystore.ts ├── mixin │ ├── code.test.ts │ ├── error.test.ts │ ├── network.test.ts │ ├── nfo.test.ts │ ├── safe.test.ts │ ├── user.test.ts │ └── utils.test.ts ├── nft.test.ts └── user.test.ts ├── tsconfig.json └── tsdx.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | 6 | *.json 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | 'jest/globals': true, 7 | }, 8 | extends: ['airbnb-base', 'prettier'], 9 | plugins: ['@typescript-eslint', 'jest'], 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | ecmaVersion: 'latest', 13 | sourceType: 'module', 14 | }, 15 | rules: { 16 | 'no-continue': 'off', 17 | 'no-bitwise': 'off', 18 | 'import/extensions': 'off', 19 | 'import/prefer-default-export': 'off', 20 | '@typescript-eslint/no-unused-vars': 'error', 21 | 'no-underscore-dangle': 'off', 22 | 'no-plusplus': 'off', 23 | 'no-shadow': 'off', 24 | 'no-redeclare': 'off', 25 | '@typescript-eslint/no-redeclare': ['error'], 26 | camelcase: 'off', 27 | 28 | // toro remove 29 | 'no-use-before-define': 'off', 30 | 'no-param-reassign': 'off', 31 | }, 32 | settings: { 33 | 'import/resolver': { 34 | typescript: {}, 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' 12 | day: 'monday' 13 | 14 | ignore: 15 | - dependency-name: 'is-retry-allowed' 16 | versions: ['3.x'] 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build for test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm i 19 | - run: npm run build 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm i 19 | - run: npm run lint 20 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm i 19 | - run: npm run test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | .idea 6 | yarn.lock 7 | 8 | config.json 9 | test/index.js 10 | keystore.json 11 | 12 | test/mixin/keystore.ts 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | config.json 2 | test/index.js -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | pnpm-lock.yaml 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSpacing: true, 4 | printWidth: 180, 5 | singleQuote: true, 6 | tabWidth: 2, 7 | trailingComma: 'all', 8 | semi: true, 9 | printWidth: 180, 10 | }; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bot-api-nodejs-client 2 | 3 | The Node.js version of the mixin SDK [https://developers.mixin.one/docs/api-overview](https://developers.mixin.one/docs/api-overview) 4 | 5 | [中文版](./README.zh-CN.md) 6 | 7 | ## New version features 8 | 9 | 1. More friendly type and code hints 10 | 2. More standardized function naming 11 | 3. More comprehensive test coverage 12 | 13 | ## Install 14 | 15 | ```shell 16 | npm install @mixin.dev/mixin-node-sdk 17 | ``` 18 | 19 | If you use `yarn` 20 | 21 | ```shell 22 | yarn add @mixin.dev/mixin-node-sdk 23 | ``` 24 | 25 | ## Usage 26 | 27 | 1. Use Mixin API 28 | 29 | ```js 30 | const { MixinApi } = require('@mixin.dev/mixin-node-sdk'); 31 | 32 | const keystore = { 33 | app_id: '', 34 | session_id: '', 35 | server_public_key: '', 36 | session_private_key: '', 37 | }; 38 | const client = MixinApi({ keystore }); 39 | 40 | // Use Promise 41 | client.user.profile().then(console.log); 42 | // Use async await 43 | async function getMe() { 44 | const me = await client.user.profile(); 45 | console.log(me); 46 | } 47 | ``` 48 | 49 | 2. Receive Mixin Messenger messages 50 | 51 | ```js 52 | const { MixinApi } = require('@mixin.dev/mixin-node-sdk'); 53 | 54 | const keystore = { 55 | app_id: '', 56 | session_id: '', 57 | server_public_key: '', 58 | session_private_key: '', 59 | }; 60 | const config = { 61 | keystore, 62 | blazeOptions: { 63 | parse: true, 64 | syncAck: true, 65 | }, 66 | }; 67 | 68 | const client = MixinApi(config); 69 | client.blaze.loop({ 70 | onMessage(msg) { 71 | console.log(msg); 72 | }, 73 | }); 74 | ``` 75 | 76 | 3. OAuth 77 | 78 | ```js 79 | const { MixinApi, getED25519KeyPair, base64RawURLEncode } = require('@mixin.dev/mixin-node-sdk'); 80 | 81 | const code = ''; // from OAuth url 82 | const app_id = ''; // app_id of your bot 83 | const client_secret = ''; // OAuth Client Secret of your bot 84 | 85 | const { seed, publicKey } = getED25519KeyPair(); // Generate random seed and ed25519 key pairs 86 | 87 | let client = MixinApi(); 88 | const { scope, authorization_id } = await client.oauth.getToken({ 89 | client_id: app_id, 90 | code, 91 | ed25519: base64RawURLEncode(publicKey), 92 | client_secret, 93 | }); 94 | const keystore = { 95 | app_id, 96 | scope, 97 | authorization_id, 98 | session_private_key: Buffer.from(seed).toString('hex'), 99 | }; 100 | client = MixinApi({ keystore }); 101 | const user = await client.user.profile(); 102 | ``` 103 | 104 | ## Use the sdk in web browser 105 | 106 | This SDK uses node `Buffer`, which is not available in web browser. You can use polyfills to make it work. 107 | 108 | For example, you can use `vite-plugin-node-polyfills` for vite. 109 | 110 | ```js 111 | // vite.config.js 112 | import { nodePolyfills } from 'vite-plugin-node-polyfills'; 113 | // ... 114 | export default defineConfig({ 115 | // ... 116 | plugins: [ 117 | nodePolyfills({ 118 | globals: { 119 | Buffer: true, 120 | }, 121 | }), 122 | ], 123 | }); 124 | ``` 125 | 126 | ## License 127 | 128 | ``` 129 | Copyright 2024 Mixin. 130 | 131 | Licensed under the Apache License, Version 2.0 (the "License"); 132 | you may not use this file except in compliance with the License. 133 | You may obtain a copy of the License at 134 | 135 | http://www.apache.org/licenses/LICENSE-2.0 136 | 137 | Unless required by applicable law or agreed to in writing, software 138 | distributed under the License is distributed on an "AS IS" BASIS, 139 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 140 | See the License for the specific language governing permissions and 141 | limitations under the License. 142 | ``` 143 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # bot-api-nodejs-client 2 | 3 | mixin 的 js 版 sdk 4 | 5 | ## 新版本特性 6 | 7 | 1. 更友好的类型和代码提示 8 | 2. 更规范的函数命名 9 | 3. 更全面的测试覆盖 10 | 11 | ## 安装 12 | 13 | ```shell 14 | npm install @mixin.dev/mixin-node-sdk 15 | ``` 16 | 17 | 使用 `yarn` 安装 18 | 19 | ```shell 20 | yarn add @mixin.dev/mixin-node-sdk 21 | ``` 22 | 23 | ## 使用 24 | 25 | 1. 仅使用 Mixin 的 Api 26 | 27 | ```js 28 | const { MixinApi } = require('@mixin.dev/mixin-node-sdk'); 29 | 30 | const keystore = { 31 | app_id: '', 32 | session_id: '', 33 | server_public_key: '', 34 | session_private_key: '', 35 | }; 36 | const client = MixinApi({ keystore }); 37 | 38 | // 使用 Promise 39 | client.user.profile().then(console.log); 40 | // 使用 async await 41 | async function getMe() { 42 | const me = await client.user.profile(); 43 | console.log(me); 44 | } 45 | ``` 46 | 47 | 2. 使用 Mixin 的消息功能 48 | 49 | ```js 50 | const { MixinApi } = require('@mixin.dev/mixin-node-sdk'); 51 | 52 | const keystore = { 53 | app_id: '', 54 | session_id: '', 55 | server_public_key: '', 56 | session_private_key: '', 57 | }; 58 | const config = { 59 | keystore, 60 | blazeOptions: { 61 | parse: true, 62 | syncAck: true, 63 | }, 64 | }; 65 | 66 | const client = MixinApi(config); 67 | client.blaze.loop({ 68 | onMessage(msg) { 69 | console.log(msg); 70 | }, 71 | }); 72 | ``` 73 | 74 | 3. OAuth 授权 75 | 76 | ```js 77 | const { MixinApi, getED25519KeyPair, base64RawURLEncode } = require('@mixin.dev/mixin-node-sdk'); 78 | 79 | const code = ''; // from OAuth url 80 | const app_id = ''; // app_id of your bot 81 | const client_secret = ''; // OAuth Client Secret of your bot 82 | 83 | const { seed, publicKey } = getED25519KeyPair(); // Generate random seed and ed25519 key pairs 84 | 85 | let client = MixinApi(); 86 | const { scope, authorization_id } = await client.oauth.getToken({ 87 | client_id: app_id, 88 | code, 89 | ed25519: base64RawURLEncode(publicKey), 90 | client_secret, 91 | }); 92 | const keystore = { 93 | app_id, 94 | scope, 95 | authorization_id, 96 | session_private_key: Buffer.from(seed).toString('hex'), 97 | }; 98 | client = MixinApi({ keystore }); 99 | const user = await client.user.profile(); 100 | ``` 101 | 102 | ## 贡献 103 | 104 | 可接受 PRs. 105 | 106 | ## API 107 | 108 | > 1. [https://developers.mixin.one/docs/api-overview](https://developers.mixin.one/docs/api-overview) 109 | 110 | ## 版本所有 111 | 112 | Copyright 2024 Mixin. 113 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | test: { 4 | plugins: ['@babel/plugin-transform-modules-commonjs'], 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /config.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id": "", 3 | "session_id": "", 4 | "pin_token": "", 5 | "private_key": "", 6 | "pin": "", 7 | "client_secret": "" 8 | } 9 | -------------------------------------------------------------------------------- /example/blaze.ts: -------------------------------------------------------------------------------- 1 | const { MixinApi } = require('..'); 2 | const keystore = require('../keystore.json'); // keystore from your bot 3 | 4 | const client = MixinApi({ 5 | keystore, 6 | blazeOptions: { 7 | parse: true, // parse the message content by sdk 8 | syncAck: true, // automatically send read status after receiving message 9 | }, 10 | }); 11 | 12 | const handler = { 13 | // callback when bot receive message 14 | onMessage: async msg => { 15 | const user = await client.user.fetch(msg.user_id); 16 | console.log(`${user.full_name} send you a ${msg.category} message: ${msg.data}`); 17 | 18 | // make your bot automatically reply 19 | const res = await client.message.sendText(msg.user_id, 'received'); 20 | console.log(`message ${res.message_id} is sent`); 21 | }, 22 | // callback when bot receive message status update 23 | // msg.source === 'ACKNOWLEDGE_MESSAGE_RECEIPT' 24 | onAckReceipt: async msg => { 25 | console.log(`message ${msg.message_id} is ${msg.status}`); 26 | }, 27 | // callback when bot receive transfer 28 | // msg.category === 'SYSTEM_ACCOUNT_SNAPSHOT' 29 | // RECOMMEND use /snapshots api to listen transfer 30 | onTransfer: async msg => { 31 | const { data: transfer } = msg; 32 | const user = await client.user.fetch(transfer.counter_user_id); 33 | const asset = await client.asset.fetch(transfer.asset_id); 34 | console.log(`user ${user.full_name} transfer ${transfer.amount} ${asset.symbol} to you`); 35 | }, 36 | // callback when group information update, which your bot is in 37 | // msg.category === 'SYSTEM_CONVERSATION' 38 | onConversation: async msg => { 39 | const group = await client.conversation.fetch(msg.conversation_id); 40 | console.log(`group ${group.name} information updated`); 41 | }, 42 | }; 43 | // ws will auto reconnect after connect closing 44 | client.blaze.loop(handler); 45 | -------------------------------------------------------------------------------- /example/create_user.js: -------------------------------------------------------------------------------- 1 | const { MixinApi, getED25519KeyPair, base64RawURLEncode, base64RawURLDecode, getTipPinUpdateMsg } = require('..'); 2 | const keystore = require('../keystore.json'); 3 | 4 | const main = async () => { 5 | const client = MixinApi({ keystore }); 6 | 7 | const { seed: sessionSeed, publicKey: sessionPublicKey } = getED25519KeyPair(); 8 | const session_private_key = sessionSeed.toString('hex'); 9 | console.log('user session_private_key', session_private_key); 10 | const user = await client.user.createBareUser('test-user', base64RawURLEncode(sessionPublicKey)); 11 | console.log('user_id', user.user_id); 12 | console.log('user session_id', user.session_id); 13 | 14 | const userClient = MixinApi({ 15 | keystore: { 16 | app_id: user.user_id, 17 | session_id: user.session_id, 18 | pin_token_base64: user.pin_token_base64, 19 | session_private_key, 20 | }, 21 | }); 22 | 23 | const { seed: spendPrivateKey, publicKey: spendPublicKey } = getED25519KeyPair(); 24 | const spend_private_key = spendPrivateKey.toString('hex'); 25 | console.log('user spend_private_key', spend_private_key); 26 | await userClient.pin.updateTipPin('', spendPublicKey.toString('hex'), user.tip_counter + 1); 27 | console.log('tip pin updated'); 28 | await userClient.pin.verifyTipPin(spendPrivateKey); 29 | console.log('tip pin verified'); 30 | 31 | const account = await userClient.safe.register(user.user_id, spend_private_key, spendPrivateKey); 32 | console.log(`account: ${account.user_id} has_safe: ${account.has_safe}`); 33 | }; 34 | 35 | main(); 36 | -------------------------------------------------------------------------------- /example/message.ts: -------------------------------------------------------------------------------- 1 | const { MixinApi, base64RawURLEncode } = require('..'); 2 | const keystore = require('../keystore.json'); 3 | const { v4 } = require('uuid'); 4 | 5 | const main = async () => { 6 | console.log(keystore); 7 | 8 | const client = MixinApi({ keystore }); 9 | 10 | const resp = await client.message.sendLegacy({ 11 | conversation_id: '9451292c-c81c-4574-961a-ce9075e32400', 12 | message_id: v4(), 13 | category: 'PLAIN_TEXT', 14 | data_base64: base64RawURLEncode('hi'), 15 | }); 16 | console.log(resp); 17 | }; 18 | 19 | main(); 20 | -------------------------------------------------------------------------------- /example/multisig.js: -------------------------------------------------------------------------------- 1 | const { v4 } = require('uuid'); 2 | const { MixinApi, buildMultiSigsTransaction, sleep, encodeScript } = require('..'); 3 | const keystore = require('../keystore.json'); 4 | 5 | const client = MixinApi({ 6 | requestConfig: { 7 | responseCallback: err => { 8 | console.log(err); 9 | }, 10 | }, 11 | keystore, 12 | }); 13 | 14 | const readOutput = async (hash, members, threshold, offset = '') => { 15 | let new_offset = offset; 16 | const outputs = await client.multisig.outputs({ 17 | members, 18 | threshold, 19 | offset, 20 | limit: 10, 21 | }); 22 | 23 | // eslint-disable-next-line no-restricted-syntax 24 | for (const output of outputs) { 25 | new_offset = output.updated_at; 26 | if (output.transaction_hash === hash) return output; 27 | } 28 | 29 | await sleep(1000 * 5); 30 | return readOutput(hash, members, threshold, new_offset); 31 | }; 32 | 33 | const main = async () => { 34 | const bot = await client.user.profile(); 35 | const asset_id = '965e5c6e-434c-3fa9-b780-c50f43cd955c'; 36 | const amount = '0.0001'; 37 | const members = [bot.app.creator_id, keystore.app_id]; 38 | const threshold = 1; 39 | 40 | // 1. send to multisig account 41 | // should have balance in your bot 42 | const sendTxReceipt = await client.transfer.toAddress(keystore.pin, { 43 | asset_id, 44 | amount, 45 | trace_id: v4(), 46 | memo: 'send to multisig', 47 | opponent_multisig: { 48 | threshold, 49 | receivers: members, 50 | }, 51 | }); 52 | console.log('send to multi-signature account'); 53 | console.log('transaction hash:', sendTxReceipt.transaction_hash); 54 | 55 | // 2. wait tx complete 56 | console.log('read transaction output...'); 57 | const utxo = await readOutput(sendTxReceipt.transaction_hash, members, threshold, ''); 58 | console.log(utxo); 59 | 60 | // 3. refund 61 | console.log('refund to bot:'); 62 | const asset = await client.asset.fetch(asset_id); 63 | const receivers = await client.transfer.outputs([ 64 | { 65 | receivers: [keystore.app_id], 66 | index: 0, 67 | }, 68 | ]); 69 | console.log('generate raw transaction'); 70 | const raw = buildMultiSigsTransaction({ 71 | version: 2, 72 | asset: asset.mixin_id, 73 | inputs: [ 74 | { 75 | hash: utxo.transaction_hash, 76 | index: utxo.output_index, 77 | }, 78 | ], 79 | outputs: [ 80 | { 81 | amount, 82 | mask: receivers[0].mask, 83 | keys: receivers[0].keys, 84 | script: encodeScript(threshold), 85 | }, 86 | ], 87 | extra: 'refund', 88 | }); 89 | 90 | // Generate a multi-signature 91 | console.log('generate a multi-signature request...'); 92 | const multisig = await client.multisig.create('sign', raw); 93 | 94 | // Sign a multi-signature 95 | console.log('sign...'); 96 | const signed = await client.multisig.sign(keystore.pin, multisig.request_id); 97 | console.log(signed); 98 | 99 | // Send signed tx to mainnet 100 | console.log('send to mainnet...'); 101 | const res = await client.external.proxy({ 102 | method: 'sendrawtransaction', 103 | params: [signed.raw_transaction], 104 | }); 105 | console.log(res); 106 | }; 107 | 108 | main(); 109 | -------------------------------------------------------------------------------- /example/nft.js: -------------------------------------------------------------------------------- 1 | const { MixinApi, sleep, buildTokenId, buildNfoTransferRequest } = require('..'); 2 | const botKeystore = require('../keystore.json'); 3 | 4 | const owner = 'user'; // bot 5 | 6 | // If it's the mixin user owns the nft, get user oauth using /oauth/token api 7 | const keystore = { 8 | app_id: botKeystore.app_id, 9 | scope: 'PROFILE:READ COLLECTIBLES:READ', 10 | authorization_id: '', 11 | private_key: '', 12 | pin_token: botKeystore.pin_token, 13 | }; 14 | 15 | const client = MixinApi({ 16 | keystore: owner === 'user' ? keystore : botKeystore, 17 | requestConfig: { 18 | responseCallback(e) { 19 | console.log(e.response); 20 | }, 21 | }, 22 | }); 23 | 24 | const readCollectibleOutput = async (id, receivers, offset = '') => { 25 | let new_offset = offset; 26 | const outputs = await client.collection.outputs({ 27 | offset, 28 | limit: 500, 29 | members: receivers, 30 | threshold: 1, 31 | }); 32 | 33 | // eslint-disable-next-line no-restricted-syntax 34 | for (const output of outputs) { 35 | if (output.token_id === id && output.state === 'unspent') return output; // 36 | new_offset = output.created_at; 37 | } 38 | 39 | await sleep(500); 40 | return readCollectibleOutput(id, receivers, new_offset); 41 | }; 42 | 43 | async function main() { 44 | const user = await client.user.profile(); 45 | // The mixin user or multisigs account that user want transfer the nft to 46 | const receivers = [owner === 'user' ? botKeystore.app_id : '7766b24c-1a03-4c3a-83a3-b4358266875d']; 47 | const threshold = 1; 48 | 49 | // The nft token information that user owns 50 | const collectionId = 'dbef5999-fcb1-4f58-b84f-6b7af9694280'; 51 | const tokenId = 354; 52 | const tokenUuid = buildTokenId(collectionId, tokenId); 53 | 54 | // Fetch the transaction that user received nft token 55 | const utxo = await readCollectibleOutput(tokenUuid, [user.app_id]); 56 | console.log(utxo); 57 | 58 | const multisig = await buildNfoTransferRequest(client, utxo, receivers, threshold, Buffer.from('test').toString('hex')); 59 | console.log(multisig); 60 | 61 | // If a bot owns the nft, sign the transaction 62 | // Otherwise, let user sign in the mixin messenger 63 | let raw_transaction; 64 | if (user.app) { 65 | const signed = await client.collection.sign(botKeystore.pin, multisig.request_id); 66 | raw_transaction = signed.raw_transaction; 67 | } else { 68 | console.log('Sign in messenger'); 69 | console.log(`mixin://codes/${multisig.code_id}`); 70 | raw_transaction = await new Promise(resolve => { 71 | const timer = setInterval(async () => { 72 | const payment = await client.code.fetch(multisig.code_id); 73 | if (payment.state === 'signed') { 74 | clearInterval(timer); 75 | resolve(payment.raw_transaction); 76 | } 77 | }, 1000); 78 | }); 79 | } 80 | 81 | console.log('send to mainnet...'); 82 | console.log(raw_transaction); 83 | const res = await client.external.proxy({ 84 | method: 'sendrawtransaction', 85 | params: [raw_transaction], 86 | }); 87 | console.log(res); 88 | } 89 | 90 | main(); 91 | -------------------------------------------------------------------------------- /example/safe_multisig.js: -------------------------------------------------------------------------------- 1 | const { 2 | MixinApi, 3 | encodeSafeTransaction, 4 | getUnspentOutputsForRecipients, 5 | buildSafeTransactionRecipient, 6 | buildSafeTransaction, 7 | signSafeTransaction, 8 | decodeSafeTransaction, 9 | } = require('..'); 10 | const { v4 } = require('uuid'); 11 | const keystore = require('../keystore.json'); // keystore from your bot 12 | 13 | const safePrivateKey = ''; 14 | 15 | // multisigs 16 | const members = [keystore.client_id, '7766b24c-1a03-4c3a-83a3-b4358266875d']; 17 | const threshold = 1; 18 | // destination 19 | const recipients = [buildSafeTransactionRecipient([keystore.client_id], 1, '0.0001')]; 20 | 21 | const main = async () => { 22 | const client = MixinApi({ keystore }); 23 | 24 | // get unspent utxos 25 | const outputs = await client.utxo.safeOutputs({ 26 | members, 27 | threshold, 28 | asset: 'f3bed3e0f6738938c8988eb8853c5647baa263901deb217ee53586d5de831f3b', 29 | state: 'unspent', 30 | }); 31 | 32 | // Get utxo inputs and change fot tx 33 | const { utxos, change } = getUnspentOutputsForRecipients(outputs, recipients); 34 | if (!change.isZero() && !change.isNegative()) { 35 | recipients.push(buildSafeTransactionRecipient(outputs[0].receivers, outputs[0].receivers_threshold, change.toString())); 36 | } 37 | // get ghost key to send tx 38 | const request_id = v4(); 39 | const ghosts = await client.utxo.ghostKey(recipients, request_id, safePrivateKey); 40 | 41 | // build safe transaction raw 42 | const tx = buildSafeTransaction(utxos, recipients, ghosts, Buffer.from('multisigs-test-memo')); 43 | console.log(tx); 44 | const raw = encodeSafeTransaction(tx); 45 | 46 | // create multisig tx 47 | console.log(request_id); 48 | let multisig = await client.multisig.createSafeMultisigs([ 49 | { 50 | raw, 51 | request_id, 52 | }, 53 | ]); 54 | console.log(multisig); 55 | 56 | // unlock multisig tx 57 | const unlock = await client.multisig.unlockSafeMultisigs(request_id); 58 | console.log('unlock'); 59 | console.log(unlock); 60 | 61 | // you can continue to sign this unlocked multisig tx 62 | const index = outputs[0].receivers.sort().findIndex(u => u === keystore.client_id); 63 | // sign safe multisigs with the private key registerd to safe 64 | const signedRaw = signSafeTransaction(tx, multisig[0].views, safePrivateKey, index); 65 | multisig = await client.multisig.signSafeMultisigs(request_id, signedRaw); 66 | console.log(multisig); 67 | 68 | // others in the gourp are required to sign the multisigs transaction 69 | otherSign(multisig.request_id); 70 | }; 71 | 72 | const otherSign = async id => { 73 | const keystore = { 74 | client_id: '', 75 | session_id: '', 76 | pin_token: '', 77 | private_key: '', 78 | }; 79 | const privateKey = ''; 80 | 81 | const client = MixinApi({ keystore }); 82 | let multisig = await client.multisig.fetchSafeMultisigs(id); 83 | const tx = decodeSafeTransaction(multisig.raw_transaction); 84 | 85 | const index = multisig.senders.sort().findIndex(u => u === keystore.client_id); 86 | // sign safe multisigs with the private key registerd to safe 87 | const signedRaw = signSafeTransaction(tx, multisig.views, privateKey, index); 88 | multisig = await client.multisig.signSafeMultisigs(id, signedRaw); 89 | console.log(multisig); 90 | }; 91 | 92 | main(); 93 | -------------------------------------------------------------------------------- /example/safe_tx.js: -------------------------------------------------------------------------------- 1 | const { MixinApi, encodeSafeTransaction, getUnspentOutputsForRecipients, buildSafeTransactionRecipient, buildSafeTransaction, signSafeTransaction } = require('..'); 2 | const { v4 } = require('uuid'); 3 | const keystore = require('../keystore.json'); // keystore from your bot 4 | 5 | let privateKey = ''; 6 | 7 | const main = async () => { 8 | const client = MixinApi({ keystore }); 9 | 10 | // destination 11 | const members = ['7766b24c-1a03-4c3a-83a3-b4358266875d']; 12 | const threshold = 1; 13 | const recipients = [buildSafeTransactionRecipient(members, threshold, '1')]; 14 | 15 | // get unspent utxos 16 | const outputs = await client.utxo.safeOutputs({ 17 | members: [keystore.client_id], 18 | threshold: 1, 19 | asset: 'f3bed3e0f6738938c8988eb8853c5647baa263901deb217ee53586d5de831f3b', 20 | state: 'unspent', 21 | }); 22 | console.log(outputs); 23 | const balance = await client.utxo.safeAssetBalance({ 24 | members: [keystore.client_id], 25 | threshold: 1, 26 | asset: 'f3bed3e0f6738938c8988eb8853c5647baa263901deb217ee53586d5de831f3b', 27 | state: 'unspent', 28 | }); 29 | console.log(balance); 30 | 31 | // Get utxo inputs and change fot tx 32 | const { utxos, change } = getUnspentOutputsForRecipients(outputs, recipients); 33 | if (!change.isZero() && !change.isNegative()) { 34 | recipients.push(buildSafeTransactionRecipient(outputs[0].receivers, outputs[0].receivers_threshold, change.toString())); 35 | } 36 | 37 | // get ghost key to send tx 38 | const request_id = v4(); 39 | const ghosts = await client.utxo.ghostKey(recipients, request_id, privateKey); 40 | console.log(ghosts); 41 | 42 | // build safe transaction raw 43 | const tx = buildSafeTransaction(utxos, recipients, ghosts, Buffer.from('test-memo')); 44 | console.log(tx); 45 | const raw = encodeSafeTransaction(tx); 46 | console.log(raw); 47 | 48 | // verify safe transaction 49 | const verifiedTx = await client.utxo.verifyTransaction([ 50 | { 51 | raw, 52 | request_id, 53 | }, 54 | ]); 55 | console.log(verifiedTx); 56 | 57 | // sign safe transaction with the private key registerd to safe 58 | const signedRaw = signSafeTransaction(tx, verifiedTx[0].views, privateKey); 59 | console.log(signedRaw); 60 | const sendedTx = await client.utxo.sendTransactions([ 61 | { 62 | raw: signedRaw, 63 | request_id, 64 | }, 65 | ]); 66 | console.log(sendedTx); 67 | }; 68 | 69 | main(); 70 | -------------------------------------------------------------------------------- /example/safe_withdraw.js: -------------------------------------------------------------------------------- 1 | const { 2 | MixinApi, 3 | MixinCashier, 4 | buildSafeTransactionRecipient, 5 | getUnspentOutputsForRecipients, 6 | buildSafeTransaction, 7 | encodeSafeTransaction, 8 | signSafeTransaction, 9 | blake3Hash, 10 | } = require('..'); 11 | const { v4 } = require('uuid'); 12 | const keystore = require('../keystore.json'); 13 | 14 | const withdrawal_asset_id = 'b91e18ff-a9ae-3dc7-8679-e935d9a4b34b'; 15 | const withdrawal_amount = '1'; 16 | const withdrawal_memo = 'memo'; 17 | const withdrawal_destination = ''; 18 | const spendPrivateKey = ''; 19 | 20 | const main = async () => { 21 | const client = MixinApi({ keystore }); 22 | 23 | const asset = await client.safe.fetchAsset(withdrawal_asset_id); 24 | const chain = asset.chain_id === asset.asset_id ? asset : await client.safe.fetchAsset(asset.chain_id); 25 | const fees = await client.safe.fetchFee(asset.asset_id, withdrawal_destination); 26 | const assetFee = fees.find(f => f.asset_id === asset.asset_id); 27 | const chainFee = fees.find(f => f.asset_id === chain.asset_id); 28 | const fee = assetFee ?? chainFee; 29 | console.log(fee); 30 | 31 | // withdrawal with chain asset as fee 32 | if (fee.asset_id !== asset.asset_id) { 33 | const outputs = await client.utxo.safeOutputs({ 34 | asset: withdrawal_asset_id, 35 | state: 'unspent', 36 | }); 37 | const feeOutputs = await client.utxo.safeOutputs({ 38 | asset: fee.asset_id, 39 | state: 'unspent', 40 | }); 41 | console.log(outputs, feeOutputs); 42 | 43 | let recipients = [ 44 | // withdrawal output, must be put first 45 | { 46 | amount: withdrawal_amount, 47 | destination: withdrawal_destination, 48 | tag: withdrawal_memo, 49 | }, 50 | ]; 51 | const { utxos, change } = getUnspentOutputsForRecipients(outputs, recipients); 52 | if (!change.isZero() && !change.isNegative()) { 53 | // add change output if needed 54 | recipients.push(buildSafeTransactionRecipient(outputs[0].receivers, outputs[0].receivers_threshold, change.toString())); 55 | } 56 | // the index of ghost keys must be the same with the index of outputs 57 | // but withdrawal output doesnt need ghost key 58 | // get ghost key to send tx 59 | const txId = v4(); 60 | const ghosts = await client.utxo.ghostKey(recipients, txId, spendPrivateKey); 61 | 62 | // spare the 0 inedx for withdrawal output, withdrawal output doesnt need ghost key 63 | const tx = buildSafeTransaction(utxos, recipients, [undefined, ...ghosts], Buffer.from('mainnet-transaction-extra')); 64 | console.log(tx); 65 | const raw = encodeSafeTransaction(tx); 66 | const ref = blake3Hash(Buffer.from(raw, 'hex')).toString('hex'); 67 | 68 | const feeRecipients = [ 69 | // fee output 70 | buildSafeTransactionRecipient([MixinCashier], 1, fee.amount), 71 | ]; 72 | const { utxos: feeUtxos, change: feeChange } = getUnspentOutputsForRecipients(feeOutputs, feeRecipients); 73 | if (!feeChange.isZero() && !feeChange.isNegative()) { 74 | // add fee change output if needed 75 | feeRecipients.push(buildSafeTransactionRecipient(feeOutputs[0].receivers, feeOutputs[0].receivers_threshold, feeChange.toString())); 76 | } 77 | const feeId = v4(); 78 | const feeGhosts = await client.utxo.ghostKey(feeRecipients, feeId, spendPrivateKey); 79 | const feeTx = buildSafeTransaction(feeUtxos, feeRecipients, feeGhosts, Buffer.from('mainnet-fee-transaction-extra'), [ref]); 80 | console.log(feeTx); 81 | const feeRaw = encodeSafeTransaction(feeTx); 82 | console.log(feeRaw); 83 | 84 | console.log(txId, feeId); 85 | let txs = await client.utxo.verifyTransaction([ 86 | { 87 | raw, 88 | request_id: txId, 89 | }, 90 | { 91 | raw: feeRaw, 92 | request_id: feeId, 93 | }, 94 | ]); 95 | 96 | const signedRaw = signSafeTransaction(tx, txs[0].views, spendPrivateKey); 97 | const signedFeeRaw = signSafeTransaction(feeTx, txs[1].views, spendPrivateKey); 98 | const res = await client.utxo.sendTransactions([ 99 | { 100 | raw: signedRaw, 101 | request_id: txId, 102 | }, 103 | { 104 | raw: signedFeeRaw, 105 | request_id: feeId, 106 | }, 107 | ]); 108 | console.log(res); 109 | } 110 | // withdrawal with asset as fee 111 | else { 112 | const outputs = await client.utxo.safeOutputs({ 113 | asset: withdrawal_asset_id, 114 | state: 'unspent', 115 | }); 116 | console.log(outputs); 117 | 118 | let recipients = [ 119 | // withdrawal output, must be put first 120 | { 121 | amount: withdrawal_amount, 122 | destination: withdrawal_destination, 123 | tag: withdrawal_memo, 124 | }, 125 | // fee output 126 | buildSafeTransactionRecipient([MixinCashier], 1, fee.amount), 127 | ]; 128 | const { utxos, change } = getUnspentOutputsForRecipients(outputs, recipients); 129 | if (!change.isZero() && !change.isNegative()) { 130 | // add change output if needed 131 | recipients.push(buildSafeTransactionRecipient(outputs[0].receivers, outputs[0].receivers_threshold, change.toString())); 132 | } 133 | // the index of ghost keys must be the same with the index of outputs 134 | // but withdrawal output doesnt need ghost key 135 | const request_id = v4(); 136 | console.log(request_id); 137 | const ghosts = await client.utxo.ghostKey(recipients, request_id, spendPrivateKey); 138 | // spare the 0 inedx for withdrawal output, withdrawal output doesnt need ghost key 139 | const tx = buildSafeTransaction(utxos, recipients, [undefined, ...ghosts], Buffer.from('mainnet-transaction-extra')); 140 | console.log(tx); 141 | const raw = encodeSafeTransaction(tx); 142 | 143 | let txs = await client.utxo.verifyTransaction([ 144 | { 145 | raw, 146 | request_id, 147 | }, 148 | ]); 149 | 150 | const signedRaw = signSafeTransaction(tx, txs[0].views, spendPrivateKey); 151 | const res = await client.utxo.sendTransactions([ 152 | { 153 | raw: signedRaw, 154 | request_id, 155 | }, 156 | ]); 157 | console.log(res); 158 | } 159 | }; 160 | 161 | main(); 162 | -------------------------------------------------------------------------------- /example/scheme.js: -------------------------------------------------------------------------------- 1 | const { MixinApi } = require('../dist'); 2 | 3 | const main = async () => { 4 | const url = 5 | 'https://mixin.one/pay/MINAAAjAgICFJ4rl_85SKOyVF61rP1FStO-4jqB1EYukCoi2unvif8CbkgvwojeRRWr4UUrEd6-Z2YVLAszVTjvnsXK6X4pRyoKMC4wMDAwMDAwMQAJZXh0cmEgb25lAI7oV9hB2EfktLEwsvTmHQmWXlxuQ0w_qbeAxQ9DzZVcCjAuMDAwMDAwMDEACWV4dHJhIHR3bwEBAFguXmU'; 6 | const client = MixinApi(); 7 | const resp = await client.code.schemes(url); 8 | console.log(`https://mixin.one/schemes/${resp.scheme_id}`); 9 | }; 10 | 11 | main(); 12 | -------------------------------------------------------------------------------- /example/storage.js: -------------------------------------------------------------------------------- 1 | const { 2 | MixinApi, 3 | getRecipientForStorage, 4 | encodeSafeTransaction, 5 | getUnspentOutputsForRecipients, 6 | buildSafeTransactionRecipient, 7 | buildSafeTransaction, 8 | signSafeTransaction, 9 | } = require('..'); 10 | const { v4 } = require('uuid'); 11 | const keystore = require('../keystore.json'); // keystore from your bot 12 | 13 | let privateKey = ''; 14 | 15 | const main = async () => { 16 | const client = MixinApi({ keystore }); 17 | const extra = Buffer.from( 18 | '0301050acdc56c8d087a301b21144b2ab5e1286b50a5d941ee02f62488db0308b943d2d6c4db1d1f598d6a8197daf51b68d7fc0ef139c4dec5a496bac9679563bd3127dbfb17b60698d36d45bc624c8e210b4c845233c99a7ae312a27e883a8aa8444b9ba312eb6037b384f6011418d8e6a489a1e32a172c56219563726941e2bbef47d12792d9583a68efc92d451e7b57fa739db17aa693cc1554b053e3d8d546c4908e06a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea9400000000000000000000000000000000000000000000000000000000000000000000006a7d517192c5c51218cc94c3d4af17f58daee089ba1fd44e3dbd98a0000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90b7065b1e3d17c45389d527f6b04c3cd58b86c731aa0fdb549b6d1bc03f82946e4b982550388271987bed3f574e7259fca44ec259bee744ef65fc5d9dbe50d000406030305000404000000060200013400000000604d160000000000520000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9080101231408fb17b60698d36d45bc624c8e210b4c845233c99a7ae312a27e883a8aa8444b9b000907040102000206079e0121100000004c697465636f696e20284d6978696e29030000004c54437700000068747470733a2f2f75706c6f6164732e6d6978696e2e6f6e652f6d6978696e2f6174746163686d656e74732f313733393030353832362d3264633161666133663333323766346432396362623032653362343163663537643438343266336334343465386538323938373136393961633433643231623200000000000000', 19 | 'hex', 20 | ); 21 | 22 | // destination 23 | const rp = getRecipientForStorage(extra); 24 | const recipients = [rp]; 25 | 26 | // get unspent utxos 27 | const outputs = await client.utxo.safeOutputs({ 28 | members: [keystore.app_id], 29 | threshold: 1, 30 | asset: 'c94ac88f-4671-3976-b60a-09064f1811e8', 31 | state: 'unspent', 32 | }); 33 | const balance = await client.utxo.safeAssetBalance({ 34 | members: [keystore.app_id], 35 | threshold: 1, 36 | asset: 'c94ac88f-4671-3976-b60a-09064f1811e8', 37 | state: 'unspent', 38 | }); 39 | console.log(balance); 40 | 41 | // Get utxo inputs and change fot tx 42 | const { utxos, change } = getUnspentOutputsForRecipients(outputs, recipients); 43 | if (!change.isZero() && !change.isNegative()) { 44 | recipients.push(buildSafeTransactionRecipient(outputs[0].receivers, outputs[0].receivers_threshold, change.toString())); 45 | } 46 | console.log(recipients); 47 | 48 | // get ghost key to send tx to uuid multisigs 49 | const request_id = v4(); 50 | // For Mixin Kernel Address start with 'XIN', get ghost key with getMainnetAddressGhostKey 51 | const ghosts = await client.utxo.ghostKey(recipients, request_id, privateKey); 52 | console.log(ghosts); 53 | 54 | // build safe transaction raw 55 | const tx = buildSafeTransaction(utxos, recipients, ghosts, extra); 56 | console.log(tx); 57 | const raw = encodeSafeTransaction(tx); 58 | console.log(raw); 59 | 60 | // verify safe transaction 61 | const verifiedTx = await client.utxo.verifyTransaction([ 62 | { 63 | raw, 64 | request_id, 65 | }, 66 | ]); 67 | console.log(verifiedTx); 68 | 69 | // sign safe transaction with the private key registerd to safe 70 | const signedRaw = signSafeTransaction(tx, verifiedTx[0].views, privateKey); 71 | console.log(signedRaw); 72 | const sendedTx = await client.utxo.sendTransactions([ 73 | { 74 | raw: signedRaw, 75 | request_id, 76 | }, 77 | ]); 78 | console.log(sendedTx); 79 | }; 80 | 81 | main(); 82 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src'); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\](?!(axios|is-retry-allowed)).+\\.(js|jsx)$'], 3 | setupFilesAfterEnv: ['/test/crypto.ts'], 4 | }; 5 | -------------------------------------------------------------------------------- /keystore.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MixinNetwork/bot-api-nodejs-client/6ba7d99e06b0934d6e12c2ad6fa246bde12e1b16/keystore.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mixin.dev/mixin-node-sdk", 3 | "version": "7.4.2", 4 | "license": "MIT", 5 | "description": "Mixin SDK for Node.js and Javascript", 6 | "main": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "src" 11 | ], 12 | "engines": { 13 | "node": ">=10" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/MixinNetwork/bot-api-nodejs-client.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/MixinNetwork/bot-api-nodejs-client/issues" 21 | }, 22 | "homepage": "https://github.com/MixinNetwork/bot-api-nodejs-client#readme", 23 | "keywords": [ 24 | "mixin", 25 | "node", 26 | "blockchain", 27 | "crypto", 28 | "js" 29 | ], 30 | "scripts": { 31 | "start": "tsdx watch", 32 | "build": "rm -r node_modules/.cache;tsdx build", 33 | "build:umd": "tsdx build --format umd", 34 | "test": "tsdx test", 35 | "lint": "eslint 'src/**' && prettier --check .", 36 | "format": "prettier --write .", 37 | "prepare": "tsdx build", 38 | "size": "size-limit", 39 | "analyze": "size-limit --why" 40 | }, 41 | "author": "contact@mixin.one", 42 | "module": "dist/mixin-node-sdk.esm.js", 43 | "size-limit": [ 44 | { 45 | "path": "dist/mixin-node-sdk.cjs.production.min.js", 46 | "limit": "10 KB" 47 | }, 48 | { 49 | "path": "dist/mixin-node-sdk.esm.js", 50 | "limit": "10 KB" 51 | } 52 | ], 53 | "devDependencies": { 54 | "@babel/plugin-transform-modules-commonjs": "^7.18.6", 55 | "@size-limit/esbuild": "^11.2.0", 56 | "@size-limit/esbuild-why": "11.2.0", 57 | "@size-limit/preset-small-lib": "^11.2.0", 58 | "@types/eslint": "^8.4.2", 59 | "@types/eslint-config-prettier": "^6.11.2", 60 | "@types/eslint-plugin-prettier": "^3.1.0", 61 | "@types/jest": "^29.0.1", 62 | "@types/lodash.merge": "^4.6.7", 63 | "@types/md5": "^2.3.5", 64 | "@types/node": "^22.15.3", 65 | "@types/node-forge": "^1.3.8", 66 | "@types/pako": "^2.0.0", 67 | "@types/prettier": "^3.0.0", 68 | "@types/serialize-javascript": "^5.0.3", 69 | "@types/uuid": "^10.0.0", 70 | "@types/ws": "^8.5.3", 71 | "@typescript-eslint/eslint-plugin": "^5.27.0", 72 | "@typescript-eslint/parser": "^5.27.0", 73 | "buffer": "^6.0.3", 74 | "eslint": "^8.16.0", 75 | "eslint-config-airbnb-base": "^15.0.0", 76 | "eslint-config-prettier": "^10.1.2", 77 | "eslint-import-resolver-typescript": "^4.3.4", 78 | "eslint-plugin-import": "^2.26.0", 79 | "eslint-plugin-jest": "^27.0.1", 80 | "eslint-plugin-prettier": "^5.2.6", 81 | "node-forge": "^1.3.1", 82 | "prettier": "^3.3.3", 83 | "size-limit": "11.2.0", 84 | "tsdx": "^0.14.1", 85 | "tslib": "^2.4.0", 86 | "typescript": "^4.7.2", 87 | "yarn-upgrade-all": "^0.7.1" 88 | }, 89 | "dependencies": { 90 | "@noble/ciphers": "^1.0.0", 91 | "@noble/curves": "^1.2.0", 92 | "@noble/hashes": "^1.5.0", 93 | "axios": "1.9.0", 94 | "axios-retry": "4.5.0", 95 | "bignumber.js": "^9.1.2", 96 | "bs58": "^6.0.0", 97 | "int64-buffer": "^1.0.1", 98 | "is-retry-allowed": "2.2.0", 99 | "lodash.merge": "^4.6.2", 100 | "md5": "^2.3.0", 101 | "nano-seconds": "^1.2.2", 102 | "pako": "^2.0.4", 103 | "serialize-javascript": "^6.0.0", 104 | "uuid": "^11.1.0", 105 | "ws": "^8.7.0" 106 | }, 107 | "directories": { 108 | "example": "example", 109 | "test": "test" 110 | }, 111 | "jest": { 112 | "moduleNameMapper": { 113 | "^axios$": "axios/dist/node/axios.cjs" 114 | } 115 | }, 116 | "publishConfig": { 117 | "access": "public", 118 | "registry": "https://registry.npmjs.org/" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/blaze/client.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws'; 2 | import { v4 as uuid } from 'uuid'; 3 | import type Keystore from '../client/types/keystore'; 4 | import type { BlazeOptions, BlazeHandler } from './type'; 5 | import { websocket, sendRaw } from './utils'; 6 | 7 | export const BlazeKeystoreClient = (keystore: Keystore | undefined, wsOptions: BlazeOptions | undefined) => { 8 | const url = 'wss://blaze.mixin.one'; 9 | 10 | let ws: WebSocket | undefined; 11 | let pingTimeout: ReturnType | undefined; 12 | 13 | const terminate = () => { 14 | clearTimeout(Number(pingTimeout)); 15 | if (!ws) return; 16 | ws.terminate(); 17 | ws = undefined; 18 | }; 19 | 20 | const heartbeat = () => { 21 | clearTimeout(Number(pingTimeout)); 22 | pingTimeout = setTimeout(terminate, 1000 * 30); 23 | }; 24 | 25 | const loopBlaze = (h: BlazeHandler) => { 26 | if (ws) return; 27 | ws = websocket(keystore, url, h, wsOptions); 28 | heartbeat(); 29 | 30 | ws.on('ping', heartbeat); 31 | 32 | ws.onopen = () => { 33 | heartbeat(); 34 | if (ws) sendRaw(ws, { id: uuid(), action: 'LIST_PENDING_MESSAGES' }); 35 | }; 36 | 37 | ws.onclose = () => { 38 | terminate(); 39 | loopBlaze(h); 40 | }; 41 | 42 | ws.onerror = e => { 43 | if (e.message !== 'Opening handshake has timed out') return; 44 | terminate(); 45 | }; 46 | }; 47 | 48 | return { 49 | loop: (h: BlazeHandler) => { 50 | if (ws) throw new Error('Blaze is already running'); 51 | if (!h.onMessage) throw new Error('OnMessage not set'); 52 | loopBlaze(h); 53 | }, 54 | stopLoop: () => { 55 | terminate(); 56 | }, 57 | getWebSocket: () => ws, 58 | }; 59 | }; 60 | 61 | export const BlazeClient = (keystore: Keystore, wsOptions?: BlazeOptions) => ({ blaze: BlazeKeystoreClient(keystore, wsOptions) }); 62 | 63 | export default BlazeClient; 64 | -------------------------------------------------------------------------------- /src/blaze/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './utils'; 3 | export * from './type'; 4 | -------------------------------------------------------------------------------- /src/blaze/type.ts: -------------------------------------------------------------------------------- 1 | import { MessageView } from '../client/types/message'; 2 | 3 | export type MessageType = MessageView | TransferView | SystemConversationPayload; 4 | 5 | interface TransferView { 6 | type: 'transfer'; 7 | snapshot_id: string; 8 | counter_user_id: string; 9 | asset_id: string; 10 | amount: string; 11 | trace_id: string; 12 | memo: string; 13 | created_at: string; 14 | } 15 | 16 | interface SystemConversationPayload { 17 | action: string; 18 | participant_id: string; 19 | user_id?: string; 20 | role?: string; 21 | } 22 | 23 | export interface BlazeOptions { 24 | /** whether to parse message */ 25 | parse?: boolean; 26 | /** whether to sync ack */ 27 | syncAck?: boolean; 28 | } 29 | 30 | export interface BlazeHandler { 31 | onMessage: (message: MessageView) => void | Promise; 32 | onAckReceipt?: (message: MessageView) => void | Promise; 33 | onTransfer?: (transfer: MessageView) => void | Promise; 34 | onConversation?: (conversation: MessageView) => void | Promise; 35 | } 36 | 37 | export interface BlazeMessage { 38 | id: string; 39 | action: string; 40 | params?: { [key: string]: any }; 41 | data?: MessageType; 42 | } 43 | -------------------------------------------------------------------------------- /src/blaze/utils.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws'; 2 | import { v4 as uuid } from 'uuid'; 3 | import { gzip, ungzip } from 'pako'; 4 | import type Keystore from '../client/types/keystore'; 5 | import type { BlazeHandler, BlazeOptions, MessageView, BlazeMessage } from '../client/types'; 6 | import { signAccessToken } from '../client/utils'; 7 | 8 | export function websocket( 9 | keystore: Keystore | undefined, 10 | url: string, 11 | handler: BlazeHandler, 12 | option: BlazeOptions = { 13 | parse: false, 14 | syncAck: false, 15 | }, 16 | ): WebSocket { 17 | const jwtToken = signAccessToken('GET', '/', '', uuid(), keystore) || ''; 18 | const headers = { 19 | Authorization: `Bearer ${jwtToken}`, 20 | }; 21 | const ws = new WebSocket(url, 'Mixin-Blaze-1', { 22 | headers, 23 | handshakeTimeout: 3000, 24 | }); 25 | 26 | ws.onmessage = async event => { 27 | const msg = decodeMessage(event.data as Uint8Array, option); 28 | if (!msg) return; 29 | 30 | if (msg.source === 'ACKNOWLEDGE_MESSAGE_RECEIPT' && handler.onAckReceipt) await handler.onAckReceipt(msg); 31 | else if (msg.category === 'SYSTEM_CONVERSATION' && handler.onConversation) await handler.onConversation(msg); 32 | else if (msg.category === 'SYSTEM_ACCOUNT_SNAPSHOT' && handler.onTransfer) await handler.onTransfer(msg); 33 | else await handler.onMessage(msg); 34 | 35 | if (option.syncAck) { 36 | const message = { 37 | id: uuid(), 38 | action: 'ACKNOWLEDGE_MESSAGE_RECEIPT', 39 | params: { message_id: msg.message_id, status: 'READ' }, 40 | }; 41 | await sendRaw(ws, message); 42 | } 43 | }; 44 | 45 | return ws; 46 | } 47 | 48 | export const decodeMessage = (data: Uint8Array, options: BlazeOptions): MessageView => { 49 | const t = ungzip(data, { to: 'string' }); 50 | const msgObj = JSON.parse(t); 51 | 52 | if (options.parse && msgObj.data && msgObj.data.data) { 53 | msgObj.data.data = Buffer.from(msgObj.data.data, 'base64').toString(); 54 | 55 | try { 56 | msgObj.data.data = JSON.parse(msgObj.data.data); 57 | } catch (e) { 58 | // ignore error 59 | } 60 | } 61 | 62 | return msgObj.data; 63 | }; 64 | 65 | export const sendRaw = (ws: WebSocket, message: BlazeMessage): Promise => 66 | new Promise(resolve => { 67 | const buffer = Buffer.from(JSON.stringify(message), 'utf-8'); 68 | const zipped = gzip(buffer); 69 | if (ws.readyState === WebSocket.OPEN) { 70 | const timer = setTimeout(() => { 71 | resolve(false); 72 | }, 5000); 73 | const cb = () => { 74 | clearTimeout(timer); 75 | resolve(true); 76 | }; 77 | ws.send(zipped, cb); 78 | return; 79 | } 80 | resolve(false); 81 | }); 82 | -------------------------------------------------------------------------------- /src/client/address.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type Keystore from './types/keystore'; 3 | import type { AddressResponse, AddressRequest } from './types/address'; 4 | import { getCreateAddressTipBody, getRemoveAddressTipBody, signEd25519PIN, signTipBody } from './utils/pin'; 5 | import { buildClient } from './utils/client'; 6 | 7 | /** 8 | * All tokens withdrawal needs an address 9 | * Should create an address first, the address can be deleted, can't be updated. 10 | * If the address belongs to another mixin user, the withdrawal fee will be free. 11 | * tag or memo can be blank. 12 | * Detail: https://developers.mixin.one/docs/api/withdrawal/address-add 13 | */ 14 | export const AddressKeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined) => ({ 15 | /** Get an address by addressID */ 16 | fetch: (addressID: string): Promise => axiosInstance.get(`/addresses/${addressID}`), 17 | 18 | /** @deprecated Use fetchListOfChain() instead */ 19 | /** Get a list of withdrawal addresses for the given asset */ 20 | fetchList: (assetID: string): Promise => axiosInstance.get(`/assets/${assetID}/addresses`), 21 | 22 | fetchListOfChain: (chainID: string): Promise => axiosInstance.get(`/safe/addresses?chain=${chainID}`), 23 | 24 | /** Create a new withdrawal address */ 25 | create: (pin: string, params: AddressRequest): Promise => { 26 | const msg = getCreateAddressTipBody(params.asset_id, params.destination, params.tag ?? '', params.label); 27 | const signedTipPin = signTipBody(pin, msg); 28 | const encrypted = signEd25519PIN(signedTipPin, keystore); 29 | return axiosInstance.post('/addresses', { ...params, pin: encrypted }); 30 | }, 31 | 32 | /** Delete a specified address by addressID */ 33 | delete: (pin: string, addressID: string): Promise => { 34 | const msg = getRemoveAddressTipBody(addressID); 35 | const signedTipPin = signTipBody(pin, msg); 36 | const encrypted = signEd25519PIN(signedTipPin, keystore); 37 | return axiosInstance.post(`/addresses/${addressID}/delete`, { pin: encrypted }); 38 | }, 39 | }); 40 | 41 | export const AddressClient = buildClient(AddressKeystoreClient); 42 | 43 | export default AddressClient; 44 | -------------------------------------------------------------------------------- /src/client/app.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { 3 | AppResponse, 4 | AppPropertyResponse, 5 | AppRequest, 6 | AppSafeSessionRequest, 7 | AppSafeRegistrationRequest, 8 | AppSessionResponse, 9 | AppRegistrationResponse, 10 | AppSecretResponse, 11 | AppBillingResponse, 12 | } from './types/app'; 13 | import { buildClient } from './utils/client'; 14 | 15 | // TODO add app api for developer document 16 | /** 17 | * API for mixin users and official app 18 | * Notes: 19 | * * Some api only available for mixin official app 20 | * * Each Mixin user can only create two free apps 21 | * https://developers.mixin.one/ 22 | */ 23 | export const AppKeystoreClient = (axiosInstance: AxiosInstance) => ({ 24 | /** Get information of current user's a specific app */ 25 | fetch: (appID: string): Promise => axiosInstance.get(`/apps/${appID}`), 26 | 27 | /** 28 | * Get app list of current user 29 | * Available for mixin official developer app only 30 | */ 31 | fetchList: (): Promise => axiosInstance.get(`/apps`), 32 | 33 | /** 34 | * Get app number of current user and the price to buy new credit 35 | * Available for mixin official developer app only 36 | */ 37 | properties: (): Promise => axiosInstance.get(`/apps/property`), 38 | 39 | /** Get app billing */ 40 | billing: (appID: string): Promise => axiosInstance.get(`/safe/apps/${appID}/billing`), 41 | 42 | /** Get user's app share list */ 43 | favorites: (userID: string): Promise => axiosInstance.get(`/users/${userID}/apps/favorite`), 44 | 45 | /** Developer can create up to 2 free apps, or pay for more unlimited apps */ 46 | create: (params: AppRequest): Promise => axiosInstance.post(`/apps`, params), 47 | 48 | /** Update app setting */ 49 | update: (appID: string, params: AppRequest): Promise => axiosInstance.post(`/apps/${appID}`, params), 50 | 51 | /** Get a new app secret */ 52 | updateSecret: (appID: string): Promise => axiosInstance.post(`/apps/${appID}/secret`), 53 | 54 | /** 55 | * Get a new app session 56 | * @param session_public_key: public key of ed25519 session keys 57 | */ 58 | updateSafeSession: (appID: string, data: AppSafeSessionRequest): Promise => 59 | axiosInstance.post(`/safe/apps/${appID}/session`, data), 60 | 61 | /** 62 | * Register app to safe, the spend private key would be the same as the tip private key 63 | * @param spend_public_key: hex public key of ed25519 tip/spend keys 64 | * @param signature_base64: signature of the SHA256Hash of the app_id using ed25519 tip/spend private key 65 | */ 66 | registerSafe: (appID: string, data: AppSafeRegistrationRequest): Promise => 67 | axiosInstance.post(`/safe/apps/${appID}/register`, data), 68 | 69 | /** 70 | * Add to your share list 71 | * User can have up to 3 favorite apps 72 | */ 73 | favorite: (appID: string): Promise => axiosInstance.post(`/apps/${appID}/favorite`), 74 | 75 | /** Removing from your share list */ 76 | unfavorite: (appID: string): Promise => axiosInstance.post(`/apps/${appID}/unfavorite`), 77 | }); 78 | 79 | export const AppClient = buildClient(AppKeystoreClient); 80 | 81 | export default AppClient; 82 | -------------------------------------------------------------------------------- /src/client/asset.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { AssetResponse } from './types/asset'; 3 | import type { SnapshotResponse } from './types/snapshot'; 4 | import { buildClient } from './utils/client'; 5 | 6 | /** 7 | * Get personal information about asset. 8 | * Notes: 9 | * * Get /assets may not have a deposit address, if you want a deposit address, 10 | * * should request /assets/:asset_id first. 11 | * https://developers.mixin.one/docs/api/assets/assets 12 | */ 13 | export const AssetKeystoreClient = (axiosInstance: AxiosInstance) => ({ 14 | /** Get the specified asset of current user, the ASSETS:READ permission is required */ 15 | fetch: (assetID: string): Promise => axiosInstance.get(`/assets/${assetID}`), 16 | 17 | /** Get the asset list of current user */ 18 | fetchList: (): Promise => axiosInstance.get('/assets'), 19 | 20 | /** Get specific asset's snapshots of current user */ 21 | snapshots: (assetID: string): Promise => axiosInstance.get(`/assets/${assetID}/snapshots`), 22 | }); 23 | 24 | export const AssetClient = buildClient(AssetKeystoreClient); 25 | 26 | export default AssetClient; 27 | -------------------------------------------------------------------------------- /src/client/attachment.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type { AxiosInstance } from 'axios'; 3 | import type { AttachmentResponse } from './types/attachment'; 4 | import { buildClient } from './utils/client'; 5 | 6 | export const AttachmentKeystoreClient = (axiosInstance: AxiosInstance) => { 7 | const createAttachment = (): Promise => axiosInstance.post(`/attachments`); 8 | 9 | const uploadAttachmentTo = (uploadURL: string, file: File): Promise => 10 | axios.create()({ 11 | url: uploadURL, 12 | method: 'PUT', 13 | data: file, 14 | headers: { 15 | 'x-amz-acl': 'public-read', 16 | 'Content-Type': 'application/octet-stream', 17 | }, 18 | maxContentLength: 2147483648, 19 | }); 20 | 21 | return { 22 | /** Get a specific attachment by attachmentID */ 23 | fetch: (attachmentID: string): Promise => axiosInstance.get(`/attachments/${attachmentID}`), 24 | 25 | /** Create a new attachment before upload it */ 26 | create: createAttachment, 27 | 28 | /** Upload a attachment */ 29 | upload: async (file: File) => { 30 | const { view_url, upload_url, attachment_id } = await createAttachment(); 31 | if (!upload_url) throw new Error('No upload URL'); 32 | 33 | await uploadAttachmentTo(upload_url, file); 34 | return { view_url, attachment_id }; 35 | }, 36 | }; 37 | }; 38 | 39 | export const AttachmentClient = buildClient(AttachmentKeystoreClient); 40 | 41 | export default AttachmentClient; 42 | -------------------------------------------------------------------------------- /src/client/circle.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { CircleResponse, CircleRequest, CircleConversationResponse } from './types/circle'; 3 | import { buildClient } from './utils/client'; 4 | 5 | /** 6 | * Circle is used to classify conversations 7 | * User can have no more than 64 circles 8 | * Docs: https://developers.mixin.one/docs/api/circles/list 9 | */ 10 | export const CircleKeystoreClient = (axiosInstance: AxiosInstance) => ({ 11 | /** Get the details of a certain circle */ 12 | fetch: (circleID: string): Promise => axiosInstance.get(`/circles/${circleID}`), 13 | 14 | /** Get all circles of a user */ 15 | fetchList: (): Promise => axiosInstance.get('/circles'), 16 | 17 | /** Get all the conversations in a circle of a user */ 18 | conversations: (circleID: string, params: CircleRequest): Promise => 19 | axiosInstance.get(`/circles/${circleID}/conversations`, { params }), 20 | 21 | /** Create a circle */ 22 | create: (name: string): Promise => axiosInstance.post('/circles', { name }), 23 | 24 | /** Modify the circle name */ 25 | update: (circleID: string, name: string): Promise => axiosInstance.post(`/circles/${circleID}`, { name }), 26 | 27 | /** Delete a circle */ 28 | delete: (circleID: string): Promise => axiosInstance.post(`/circles/${circleID}/delete`), 29 | 30 | /** Add the user to a circle */ 31 | addUser: (userID: string, circleID: string): Promise => axiosInstance.post(`/users/${userID}/circles`, { circleID, action: 'ADD' }), 32 | 33 | /** Remove the user from a circle */ 34 | removeUser: (userID: string, circleID: string): Promise => 35 | axiosInstance.post(`/users/${userID}/circles`, { circleID, action: 'REMOVE' }), 36 | 37 | /** Add the group from a certain circle */ 38 | addConversation: (conversationID: string, circleID: string): Promise => 39 | axiosInstance.post(`/conversations/${conversationID}/circles`, { circleID, action: 'ADD' }), 40 | 41 | /** Remove the group from a certain circle */ 42 | removeConversation: (conversation_id: string, circleID: string): Promise => 43 | axiosInstance.post(`/conversations/${conversation_id}/circles`, { circleID, action: 'REMOVE' }), 44 | }); 45 | 46 | export const CircleClient = buildClient(CircleKeystoreClient); 47 | 48 | export default CircleClient; 49 | -------------------------------------------------------------------------------- /src/client/code.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { CodeResponse, SchemeResponse } from './types/code'; 3 | import { buildClient } from './utils/client'; 4 | 5 | /** 6 | * Some information in Mixin is non-public, through codes/:id you can share it. 7 | * It also facilitates privacy protection 8 | */ 9 | export const CodeKeystoreClient = (axiosInstance: AxiosInstance) => ({ 10 | fetch: (codeID: string): Promise => axiosInstance.get(`/codes/${codeID}`), 11 | 12 | schemes: (target: string): Promise => axiosInstance.post(`/schemes`, { target }), 13 | }); 14 | 15 | export const CodeClient = buildClient(CodeKeystoreClient); 16 | 17 | export default CodeClient; 18 | -------------------------------------------------------------------------------- /src/client/collectible.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type Keystore from './types/keystore'; 3 | import type { MultisigInitAction } from './types/multisig'; 4 | import type { 5 | CollectibleRequestAction, 6 | CollectibleResponse, 7 | CollectionResponse, 8 | CollectibleOutputsRequest, 9 | CollectibleOutputsResponse, 10 | CollectibleTransactionResponse, 11 | } from './types/collectible'; 12 | import { hashMembers, signEd25519PIN, buildClient } from './utils'; 13 | 14 | export const MintMinimumCost = '0.001'; 15 | 16 | export const GroupMembers = [ 17 | '4b188942-9fb0-4b99-b4be-e741a06d1ebf', 18 | 'dd655520-c919-4349-822f-af92fabdbdf4', 19 | '047061e6-496d-4c35-b06b-b0424a8a400d', 20 | 'acf65344-c778-41ee-bacb-eb546bacfb9f', 21 | 'a51006d0-146b-4b32-a2ce-7defbf0d7735', 22 | 'cf4abd9c-2cfa-4b5a-b1bd-e2b61a83fabd', 23 | '50115496-7247-4e2c-857b-ec8680756bee', 24 | ]; 25 | 26 | export const GroupThreshold = 5; 27 | 28 | /** 29 | * Users can use those APIs to manage their NFTs 30 | * Note: 31 | * * Before transferring a collectible, user should create a request first. 32 | * * only unsigned request can be canceled. 33 | * * only uncompleted sign transaction can be unlocked. 34 | * Docs: https://developers.mixin.one/docs/api/collectibles/request 35 | */ 36 | export const CollectibleKeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined) => { 37 | const manageRequest = (pin: string, requestID: string, action: CollectibleRequestAction): Promise => { 38 | const encrypted = signEd25519PIN(pin, keystore); 39 | return axiosInstance.post(`/collectibles/requests/${requestID}/${action}`, { pin: encrypted }); 40 | }; 41 | 42 | const transfer = (action: MultisigInitAction, raw: string): Promise => 43 | axiosInstance.post('/collectibles/requests', { action, raw }); 44 | 45 | return { 46 | /** Get the information of the collectible */ 47 | fetch: (tokenID: string): Promise => axiosInstance.get(`/collectibles/tokens/${tokenID}`), 48 | 49 | /** Get the information of the collectible collection */ 50 | fetchCollection: (collectionID: string): Promise => axiosInstance.get(`/collectibles/collections/${collectionID}`), 51 | 52 | /** Get collectibles outputs */ 53 | outputs: (params: CollectibleOutputsRequest): Promise => { 54 | const hashedParams = { 55 | ...params, 56 | members: hashMembers(params.members), 57 | }; 58 | return axiosInstance.get('/collectibles/outputs', { params: hashedParams }); 59 | }, 60 | 61 | /** @deprecated Use transfer() instead */ 62 | request: transfer, 63 | 64 | /** Create a collectibles transfer request */ 65 | transfer, 66 | 67 | /** Initiate or participate in signing */ 68 | sign: (pin: string, requestID: string) => manageRequest(pin, requestID, 'sign'), 69 | 70 | /** Cancel my signature */ 71 | cancel: (pin: string, requestID: string) => manageRequest(pin, requestID, 'cancel'), 72 | 73 | /** Cancel collectibles */ 74 | unlock: (pin: string, requestID: string) => manageRequest(pin, requestID, 'unlock'), 75 | }; 76 | }; 77 | 78 | export const CollectibleClient = buildClient(CollectibleKeystoreClient); 79 | 80 | export default CollectibleClient; 81 | -------------------------------------------------------------------------------- /src/client/conversation.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type Keystore from './types/keystore'; 3 | import type { ConversationRequest, ConversationResponse, ConversationAction, ParticipantRequest } from './types/conversation'; 4 | import { uniqueConversationID } from './utils/uniq'; 5 | import { buildClient } from './utils/client'; 6 | 7 | /** 8 | * Create and manage a conversation 9 | * Notes: 10 | * * only owner and admin can add or remove participants, and rotate conversation code 11 | * * only owner can change the role of participants 12 | * * only owner and admin can setup disappear 13 | * * for group conversation, creator will be the owner and can't be changed. 14 | * Docs: https://developers.mixin.one/docs/api/conversations/read 15 | */ 16 | export const ConversationKeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined) => { 17 | const createConversation = (params: ConversationRequest): Promise => axiosInstance.post('/conversations', params); 18 | 19 | const managerConversation = (conversationID: string, action: ConversationAction, participant: ParticipantRequest[]): Promise => 20 | axiosInstance.post(`/conversations/${conversationID}/participants/${action}`, participant); 21 | 22 | const createContactConversation = (userID: string): Promise => { 23 | if (!keystore) throw new Error('No Keystore Provided'); 24 | return createConversation({ 25 | category: 'CONTACT', 26 | conversation_id: uniqueConversationID(keystore.app_id, userID), 27 | participants: [{ user_id: userID }], 28 | }); 29 | }; 30 | 31 | const muteConversation = (conversationID: string, duration: number): Promise => 32 | axiosInstance.post(`/conversations/${conversationID}/mute`, { duration }); 33 | 34 | return { 35 | /** Get specific conversation information by conversationID */ 36 | fetch: (conversationID: string): Promise => axiosInstance.get(`/conversations/${conversationID}`), 37 | 38 | /** 39 | * Ensure the conversation is created 40 | * when creating a new group or having a conversation with a user 41 | * for the first time. 42 | */ 43 | create: createConversation, 44 | 45 | /** Create a conversation with a user for the first time */ 46 | createContact: createContactConversation, 47 | 48 | /** Create a new group for the first time */ 49 | createGroup: (conversationID: string, name: string, participant: ParticipantRequest[]) => 50 | createConversation({ 51 | category: 'GROUP', 52 | conversation_id: conversationID, 53 | name, 54 | participants: participant, 55 | }), 56 | 57 | /** Join a group by codeID */ 58 | joinGroup: (codeID: string): Promise => axiosInstance.post(`/conversations/${codeID}/join`), 59 | 60 | /** Exit a group */ 61 | exitGroup: (conversationID: string): Promise => axiosInstance.post(`/conversations/${conversationID}/exit`), 62 | 63 | /** Add/remove other participants or add/remove admin in a group */ 64 | updateParticipants: managerConversation, 65 | 66 | /** Add users, if you are the owner or admin of this group conversation */ 67 | addParticipants: (conversationID: string, userIDs: string[]) => 68 | managerConversation( 69 | conversationID, 70 | 'ADD', 71 | userIDs.map(userID => ({ user_id: userID })), 72 | ), 73 | 74 | /** Remove users, if you are the owner or admin of this group conversation */ 75 | removeParticipants: (conversationID: string, userIDs: string[]) => 76 | managerConversation( 77 | conversationID, 78 | 'REMOVE', 79 | userIDs.map(userID => ({ user_id: userID })), 80 | ), 81 | 82 | /** Set admin privileges for a user, group owners Only */ 83 | setAdmin: (conversationID: string, userIDs: string[]) => 84 | managerConversation( 85 | conversationID, 86 | 'ROLE', 87 | userIDs.map(userID => ({ user_id: userID, role: 'ADMIN' })), 88 | ), 89 | 90 | /** Remove admin privileges for a user, group owners Only */ 91 | revokeAdmin: (conversationID: string, userIDs: string[]) => 92 | managerConversation( 93 | conversationID, 94 | 'ROLE', 95 | userIDs.map(userID => ({ user_id: userID, role: '' })), 96 | ), 97 | 98 | /** Reset invitation link and codeId */ 99 | resetGroupCode: (conversationID: string): Promise => axiosInstance.post(`/conversations/${conversationID}/rotate`), 100 | 101 | /** Update a group's title and announcement by conversationID */ 102 | updateGroupInfo: (conversationID: string, params: Pick): Promise => 103 | axiosInstance.post(`/conversations/${conversationID}`, params), 104 | 105 | /** Mute contact for seconds */ 106 | mute: (conversationID: string, duration: number) => muteConversation(conversationID, duration), 107 | 108 | /** Unmute contact */ 109 | unmute: (conversationID: string) => muteConversation(conversationID, 0), 110 | 111 | /** Set the disappearing message expiration duration for group */ 112 | disappearDuration: (conversationID: string, duration: number) => axiosInstance.post(`/conversations/${conversationID}/disappear`, { duration }), 113 | }; 114 | }; 115 | 116 | export const ConversationClient = buildClient(ConversationKeystoreClient); 117 | 118 | export default ConversationClient; 119 | -------------------------------------------------------------------------------- /src/client/error.ts: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | 3 | export class ResponseError extends Error { 4 | constructor( 5 | public code: number, 6 | public description: string, 7 | public status: number, 8 | public extra: object, 9 | public requestId: string | undefined, 10 | public originalError: unknown, 11 | ) { 12 | super(`code: ${code}, description: ${description}, status: ${status}, extra: ${serialize(extra)}, requestId: ${requestId} originalError: ${serialize(originalError)}`); 13 | } 14 | } 15 | 16 | export default ResponseError; 17 | -------------------------------------------------------------------------------- /src/client/external.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { DepositRequest, ProxyRequest } from './types/external'; 3 | import type { CheckAddressRequest, CheckAddressResponse, ExchangeRateResponse, ExternalTransactionResponse } from './types/network'; 4 | import { buildClient } from './utils/client'; 5 | 6 | export const ExternalKeystoreClient = (axiosInstance: AxiosInstance) => ({ 7 | /** 8 | * Get the pending deposits 9 | * Which confirmations is less then threshold 10 | */ 11 | deposits: (params: DepositRequest): Promise => axiosInstance.get('/external/transactions', { params }), 12 | 13 | /** 14 | * Check if an address belongs to Mixin 15 | */ 16 | checkAddress: (params: CheckAddressRequest): Promise => axiosInstance.get(`/external/addresses/check`, { params }), 17 | 18 | /** 19 | * GET the list of all fiat exchange rates based on US Dollar 20 | */ 21 | exchangeRates: (): Promise => axiosInstance.get('/external/fiats'), 22 | 23 | /** 24 | * Submit a raw transaction to a random mainnet node 25 | * { 26 | * method: 'sendrawtransaction', 27 | * params: array of transaction hash 28 | * } 29 | * */ 30 | proxy: (params: ProxyRequest): Promise => axiosInstance.post('/external/proxy', params), 31 | }); 32 | 33 | export const ExternalClient = buildClient(ExternalKeystoreClient); 34 | 35 | export default ExternalClient; 36 | -------------------------------------------------------------------------------- /src/client/http.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import type { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios'; 3 | import axiosRetry, { isIdempotentRequestError } from 'axios-retry'; 4 | import { v4 as uuid } from 'uuid'; 5 | import isRetryAllowed from 'is-retry-allowed'; 6 | import type { Keystore } from './types/keystore'; 7 | import type { RequestConfig } from './types/client'; 8 | import { ResponseError } from './error'; 9 | import { signAccessToken } from './utils/auth'; 10 | 11 | axios.defaults.headers.post['Content-Type'] = 'application/json'; 12 | axios.defaults.headers.put['Content-Type'] = 'application/json'; 13 | axios.defaults.headers.patch['Content-Type'] = 'application/json'; 14 | export function http(keystore?: Keystore, config?: RequestConfig): AxiosInstance { 15 | const timeout = config?.timeout || 3000; 16 | const retries = config?.retry || 5; 17 | 18 | const ins = axios.create({ 19 | baseURL: 'https://api.mixin.one', 20 | timeout, 21 | ...config, 22 | }); 23 | 24 | ins.interceptors.request.use((config: InternalAxiosRequestConfig) => { 25 | const { method, data } = config; 26 | const url = axios.getUri(config).slice(config.baseURL?.length); 27 | 28 | if (config.headers) { 29 | const requestID = uuid(); 30 | config.headers['X-Request-Id'] = requestID; 31 | const jwtToken = signAccessToken(method, url, data, requestID, keystore); 32 | config.headers.Authorization = `Bearer ${jwtToken}`; 33 | } 34 | 35 | return config; 36 | }); 37 | 38 | ins.interceptors.response.use(async (res: AxiosResponse) => { 39 | const { data, error } = res.data; 40 | if (error) throw new ResponseError(error.code, error.description, error.status, error.extra, res.headers['x-request-id'], error); 41 | return data; 42 | }); 43 | 44 | ins.interceptors.response.use(undefined, async (e: any) => { 45 | await config?.responseCallback?.(e); 46 | await config?.errorMap?.(e); 47 | return Promise.reject(e); 48 | }); 49 | 50 | axiosRetry(ins, { 51 | retries, 52 | shouldResetTimeout: true, 53 | retryDelay: () => 500, 54 | retryCondition: error => 55 | (!error.response && 56 | Boolean(error.code) && // Prevents retrying cancelled requests 57 | isRetryAllowed(error)) || 58 | isIdempotentRequestError(error), 59 | }); 60 | 61 | return ins; 62 | } 63 | 64 | export const mixinRequest = http(); 65 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | // Mixin provides different APIs for different services and applications. 2 | // This is typescript & JS sdk for mixin developers 3 | // Docs: https://developers.mixin.one/docs/api-overview 4 | export * from './types'; 5 | 6 | export { AddressClient } from './address'; 7 | export { AppClient } from './app'; 8 | export { AssetClient } from './asset'; 9 | export { AttachmentClient } from './attachment'; 10 | export { BlazeClient } from '../blaze/client'; 11 | export { CircleClient } from './circle'; 12 | export { CodeClient } from './code'; 13 | export { CollectibleClient, MintMinimumCost, GroupMembers, GroupThreshold } from './collectible'; 14 | export { ConversationClient } from './conversation'; 15 | export { ExternalClient } from './external'; 16 | export { MessageClient } from './message'; 17 | export { MultisigClient } from './multisig'; 18 | export { NetworkClient } from './network'; 19 | export { OAuthClient } from './oauth'; 20 | export { PaymentClient } from './payment'; 21 | export { PinClient } from './pin'; 22 | export { SafeClient } from './safe'; 23 | export { TransferClient } from './transfer'; 24 | export { UserClient } from './user'; 25 | export { UtxoClient } from './utxo'; 26 | 27 | export * from './mixin-client'; 28 | export { mixinRequest } from './http'; 29 | export { ResponseError } from './error'; 30 | export * from './utils'; 31 | -------------------------------------------------------------------------------- /src/client/message.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import { v4 as uuid } from 'uuid'; 3 | import type Keystore from './types/keystore'; 4 | import type { 5 | AcknowledgementRequest, 6 | AcknowledgementResponse, 7 | MessageCategory, 8 | MessageRequest, 9 | StickerMessageRequest, 10 | ImageMessageRequest, 11 | AudioMessageRequest, 12 | VideoMessageRequest, 13 | ContactMessageRequest, 14 | AppCardMessageRequest, 15 | FileMessageRequest, 16 | LiveMessageRequest, 17 | LocationMessageRequest, 18 | AppButtonMessageRequest, 19 | TransferMessageRequest, 20 | RecallMessageRequest, 21 | } from './types/message'; 22 | import { uniqueConversationID, base64RawURLEncode, buildClient } from './utils'; 23 | 24 | /** 25 | * Methods to send messages 26 | * Note: 27 | * * To receive a list of messages from Mixin message service, you need to setup a websocket connection. 28 | * After receiving the message via WebSocket, you need to acknowledge the message to Mixin message service, 29 | * otherwise it will keep pushing the message. 30 | */ 31 | export const MessageKeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined) => { 32 | const send = (message: MessageRequest) => axiosInstance.post('/messages', [message]); 33 | const sendLegacy = (message: MessageRequest) => axiosInstance.post('/messages', message); 34 | 35 | const sendMsg = async (recipientID: string, category: MessageCategory, data: any): Promise => { 36 | if (!keystore) throw new Error('No Keystore Provided'); 37 | if (typeof data === 'object') data = JSON.stringify(data); 38 | 39 | const messageRequest = { 40 | category, 41 | recipient_id: recipientID, 42 | conversation_id: uniqueConversationID(keystore.app_id, recipientID), 43 | message_id: uuid(), 44 | data_base64: base64RawURLEncode(Buffer.from(data)), 45 | }; 46 | await send(messageRequest); 47 | return messageRequest; 48 | }; 49 | 50 | return { 51 | /** Send the status of single message in bulk to Mixin Server */ 52 | sendAcknowledgement: (message: AcknowledgementRequest): Promise => 53 | axiosInstance.post('/acknowledgements', [message]), 54 | 55 | /** Send the status of messages in bulk to Mixin Server */ 56 | sendAcknowledges: (messages: AcknowledgementRequest[]): Promise => 57 | axiosInstance.post('/acknowledgements', messages), 58 | 59 | /** Send one message */ 60 | sendOne: send, 61 | 62 | /** 63 | * Send messages in bulk 64 | * A maximum of 100 messages can be sent in batch each time, and the message body cannot exceed 128Kb 65 | */ 66 | sendBatch: (messages: MessageRequest[]) => axiosInstance.post('/messages', messages), 67 | 68 | /** send one kind of message */ 69 | sendMsg, 70 | 71 | sendText: (userID: string, text: string): Promise => sendMsg(userID, 'PLAIN_TEXT', text), 72 | 73 | sendSticker: (userID: string, sticker: StickerMessageRequest): Promise => sendMsg(userID, 'PLAIN_STICKER', sticker), 74 | 75 | sendImage: (userID: string, image: ImageMessageRequest): Promise => sendMsg(userID, 'PLAIN_IMAGE', image), 76 | 77 | sendAudio: (userID: string, audio: AudioMessageRequest): Promise => sendMsg(userID, 'PLAIN_AUDIO', audio), 78 | 79 | sendVideo: (userID: string, video: VideoMessageRequest): Promise => sendMsg(userID, 'PLAIN_VIDEO', video), 80 | 81 | sendContact: (userID: string, contact: ContactMessageRequest): Promise => sendMsg(userID, 'PLAIN_CONTACT', contact), 82 | 83 | sendAppCard: (userID: string, appCard: AppCardMessageRequest): Promise => sendMsg(userID, 'APP_CARD', appCard), 84 | 85 | sendFile: (userID: string, file: FileMessageRequest): Promise => sendMsg(userID, 'PLAIN_DATA', file), 86 | 87 | sendLive: (userID: string, live: LiveMessageRequest): Promise => sendMsg(userID, 'PLAIN_LIVE', live), 88 | 89 | sendLocation: (userID: string, location: LocationMessageRequest): Promise => sendMsg(userID, 'PLAIN_LOCATION', location), 90 | 91 | sendPost: (userID: string, text: string): Promise => sendMsg(userID, 'PLAIN_POST', text), 92 | 93 | sendAppButton: (userID: string, appButton: AppButtonMessageRequest[]): Promise => sendMsg(userID, 'APP_BUTTON_GROUP', appButton), 94 | 95 | sendTransfer: (userID: string, transfer: TransferMessageRequest): Promise => sendMsg(userID, 'SYSTEM_ACCOUNT_SNAPSHOT', transfer), 96 | 97 | sendRecall: (userID: string, message: RecallMessageRequest): Promise => sendMsg(userID, 'MESSAGE_RECALL', message), 98 | 99 | sendLegacy: (message: MessageRequest) => sendLegacy(message), 100 | }; 101 | }; 102 | 103 | export const MessageClient = buildClient(MessageKeystoreClient); 104 | 105 | export default MessageClient; 106 | -------------------------------------------------------------------------------- /src/client/mixin-client.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | import type { AxiosInstance } from 'axios'; 3 | import type Keystore from './types/keystore'; 4 | import type { HTTPConfig, RequestClient } from './types'; 5 | import { createAxiosClient, createRequestClient } from './utils'; 6 | import { AddressKeystoreClient } from './address'; 7 | import { AppKeystoreClient } from './app'; 8 | import { AssetKeystoreClient } from './asset'; 9 | import { AttachmentKeystoreClient } from './attachment'; 10 | import { CircleKeystoreClient } from './circle'; 11 | import { CodeKeystoreClient } from './code'; 12 | import { CollectibleKeystoreClient } from './collectible'; 13 | import { ConversationKeystoreClient } from './conversation'; 14 | import { ExternalKeystoreClient } from './external'; 15 | import { MessageKeystoreClient } from './message'; 16 | import { MultisigKeystoreClient } from './multisig'; 17 | import { NetworkBaseClient } from './network'; 18 | import { OAuthBaseClient } from './oauth'; 19 | import { PaymentBaseClient } from './payment'; 20 | import { PinKeystoreClient } from './pin'; 21 | import { TransferKeystoreClient } from './transfer'; 22 | import { UserKeystoreClient } from './user'; 23 | import { BlazeKeystoreClient } from '../blaze/client'; 24 | import { UtxoKeystoreClient } from './utxo'; 25 | import { SafeKeystoreClient } from './safe'; 26 | 27 | const KeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined, config: HTTPConfig) => ({ 28 | address: AddressKeystoreClient(axiosInstance, keystore), 29 | app: AppKeystoreClient(axiosInstance), 30 | asset: AssetKeystoreClient(axiosInstance), 31 | blaze: BlazeKeystoreClient(keystore, config.blazeOptions), 32 | attachment: AttachmentKeystoreClient(axiosInstance), 33 | circle: CircleKeystoreClient(axiosInstance), 34 | code: CodeKeystoreClient(axiosInstance), 35 | collection: CollectibleKeystoreClient(axiosInstance, keystore), 36 | conversation: ConversationKeystoreClient(axiosInstance, keystore), 37 | external: ExternalKeystoreClient(axiosInstance), 38 | message: MessageKeystoreClient(axiosInstance, keystore), 39 | multisig: MultisigKeystoreClient(axiosInstance, keystore), 40 | network: NetworkBaseClient(axiosInstance), 41 | oauth: OAuthBaseClient(axiosInstance), 42 | payment: PaymentBaseClient(axiosInstance), 43 | pin: PinKeystoreClient(axiosInstance, keystore), 44 | safe: SafeKeystoreClient(axiosInstance, keystore), 45 | transfer: TransferKeystoreClient(axiosInstance, keystore), 46 | user: UserKeystoreClient(axiosInstance), 47 | utxo: UtxoKeystoreClient(axiosInstance), 48 | }); 49 | 50 | export type KeystoreClientReturnType = ReturnType; 51 | 52 | export function MixinApi(config: HTTPConfig = {}): KeystoreClientReturnType & RequestClient { 53 | const { keystore, requestConfig } = config; 54 | 55 | const axiosInstance = createAxiosClient(keystore, requestConfig); 56 | const requestClient = createRequestClient(axiosInstance); 57 | 58 | const keystoreClient = KeystoreClient(axiosInstance, keystore, config); 59 | 60 | return merge(keystoreClient, requestClient); 61 | } 62 | -------------------------------------------------------------------------------- /src/client/multisig.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type Keystore from './types/keystore'; 3 | import type { MultisigAction, MultisigInitAction, MultisigRequest, MultisigUtxoResponse, MultisigRequestResponse, SafeMultisigsResponse, TransactionRequest } from './types'; 4 | import { signEd25519PIN } from './utils/pin'; 5 | import { hashMembers } from './utils/uniq'; 6 | import { buildClient } from './utils/client'; 7 | 8 | /** 9 | * Users can use those APIs to manage their multisig outputs 10 | * Note: 11 | * * Before transferring tokens, user should create a request first. 12 | * * only unsigned request can be canceled. 13 | * * only uncompleted sign transaction can be unlocked. 14 | * Docs: https://developers.mixin.one/docs/api/multisigs/request 15 | */ 16 | export const MultisigKeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined) => { 17 | const initMultisig = (pin: string, requestID: string, action: MultisigAction): Promise => { 18 | const encrypted = signEd25519PIN(pin, keystore); 19 | return axiosInstance.post(`/multisigs/requests/${requestID}/${action}`, { pin: encrypted }); 20 | }; 21 | 22 | return { 23 | /** Get signature outputs, if an account participates in it */ 24 | outputs: (params: MultisigRequest): Promise => { 25 | const { members, threshold } = params; 26 | if (members.length === 0 || threshold < 1 || threshold > members.length) return Promise.reject(new Error('Invalid threshold or members')); 27 | 28 | const hashedParams = { 29 | ...params, 30 | members: hashMembers(members), 31 | }; 32 | return axiosInstance.get(`/multisigs/outputs`, { params: hashedParams }); 33 | }, 34 | 35 | /** Generate a multi-signature request to obtain request_id */ 36 | create: (action: MultisigInitAction, raw: string): Promise => 37 | axiosInstance.post(`/multisigs/requests`, { action, raw }), 38 | 39 | /** Initiate or participate in signing */ 40 | sign: (pin: string, requestID: string): Promise => initMultisig(pin, requestID, 'sign'), 41 | 42 | /** Cancel my signature before the multisig finish */ 43 | unlock: (pin: string, requestID: string): Promise => initMultisig(pin, requestID, 'unlock'), 44 | 45 | /** Cancel my multisig request */ 46 | cancel: (pin: string, requestID: string): Promise => initMultisig(pin, requestID, 'cancel'), 47 | 48 | createSafeMultisigs: (params: TransactionRequest[]): Promise => axiosInstance.post('/safe/multisigs', params), 49 | 50 | fetchSafeMultisigs: (id: string): Promise => axiosInstance.get(`/safe/multisigs/${id}`), 51 | 52 | signSafeMultisigs: (id: string, raw: string): Promise => axiosInstance.post(`/safe/multisigs/${id}/sign`, { raw }), 53 | 54 | unlockSafeMultisigs: (id: string): Promise => axiosInstance.post(`/safe/multisigs/${id}/unlock`), 55 | }; 56 | }; 57 | 58 | export const MultisigClient = buildClient(MultisigKeystoreClient); 59 | 60 | export default MultisigClient; 61 | -------------------------------------------------------------------------------- /src/client/network.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { AssetResponse } from './types/asset'; 3 | import type { NetworkSnapshotRequest, NetworkInfoResponse, NetworkChainResponse, NetworkAssetResponse, NetworkPriceResponse, NetworkSnapshotResponse } from './types/network'; 4 | import { buildClient } from './utils/client'; 5 | 6 | /** 7 | * Public methods that need no permission 8 | * Docs: https://developers.mixin.one/docs/api/transfer/snapshots 9 | */ 10 | export const NetworkBaseClient = (axiosInstance: AxiosInstance) => ({ 11 | /** Get network info */ 12 | info: (): Promise => axiosInstance.get('/network'), 13 | 14 | /** Get the list of all public chains supported by Mixin */ 15 | chains: (): Promise => axiosInstance.get('/network/chains'), 16 | 17 | /** Get information of a chain */ 18 | fetchChain: (chainID: string): Promise => axiosInstance.get(`/network/chains/${chainID}`), 19 | 20 | /** Get public information of an asset */ 21 | fetchAsset: (assetID: string): Promise => axiosInstance.get(`/network/assets/${assetID}`), 22 | 23 | /** 24 | * Get the list of the top 100 assets on the entire network 25 | * kind parameter is used to specify the top assets, for NORMAL value will not swap lp tokens 26 | */ 27 | topAssets: (kind = 'ALL'): Promise => { 28 | const params = { kind }; 29 | return axiosInstance.get('/network/assets/top', { params }); 30 | }, 31 | 32 | /** 33 | * Search for popular assets by symbol or name 34 | * This API only returns assets with icons or prices. 35 | */ 36 | searchAssets: (keyword: string, kind = 'ALL'): Promise => { 37 | const params = { kind }; 38 | return axiosInstance.get(`/network/assets/search/${keyword}`, { params }); 39 | }, 40 | 41 | /** 42 | * Get the historical price of a given asset 43 | * If no ticker found, price_usd and price_usd will return 0 44 | */ 45 | historicalPrice: (assetID: string, offset?: string): Promise => { 46 | const params = { 47 | asset: assetID, 48 | offset, 49 | }; 50 | return axiosInstance.get(`/network/ticker`, { params }); 51 | }, 52 | 53 | /** Get public information of specific snapshot by snapshot_id */ 54 | snapshot: (snapshotID: string): Promise => axiosInstance.get(`/network/snapshots/${snapshotID}`), 55 | 56 | /** Get public information of snapshot records, which including transfers, deposits, withdrawals, etc */ 57 | snapshots: (inputParams: NetworkSnapshotRequest): Promise => { 58 | const params = { 59 | ...inputParams, 60 | order: inputParams.order || 'DESC', 61 | }; 62 | return axiosInstance.get(`/network/snapshots`, { params }); 63 | }, 64 | }); 65 | 66 | export const NetworkClient = buildClient(NetworkBaseClient); 67 | 68 | export default NetworkClient; 69 | -------------------------------------------------------------------------------- /src/client/oauth.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { AccessTokenRequest, AccessTokenResponse, AuthorizationResponse, AuthorizeRequest } from './types/oauth'; 3 | import { buildClient } from './utils/client'; 4 | 5 | /** 6 | * Method to get user access code 7 | * To access some information of Mixin Messenger users, the developer needs to apply for authorization from the user 8 | * After that, the page will automatically jump to the application's OAuth URL, accompanied by the authorization code 9 | * Detail: https://developers.mixin.one/docs/api/oauth/oauth 10 | */ 11 | export const OAuthBaseClient = (axiosInstance: AxiosInstance) => ({ 12 | /** Get the access code based on authorization code */ 13 | getToken: (data: AccessTokenRequest) => axiosInstance.post('/oauth/token', data), 14 | 15 | authorize: (data: AuthorizeRequest) => axiosInstance.post('/oauth/authorize', data), 16 | 17 | authorizations: (appId?: string) => axiosInstance.get('/authorizations', { params: { app: appId } }), 18 | 19 | revokeAuthorize: (clientId: string) => axiosInstance.post('/oauth/cancel', { client_id: clientId }), 20 | }); 21 | 22 | export const OAuthClient = buildClient(OAuthBaseClient); 23 | 24 | export default OAuthClient; 25 | -------------------------------------------------------------------------------- /src/client/payment.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { PaymentRequestResponse, RawTransactionRequest, TransferRequest } from './types'; 3 | import { buildClient } from './utils/client'; 4 | 5 | export const PaymentBaseClient = (axiosInstance: AxiosInstance) => { 6 | const payment = (params: TransferRequest | RawTransactionRequest) => axiosInstance.post('/payments', params); 7 | return { 8 | /** @deprecated Use payment() instead */ 9 | request: payment, 10 | 11 | // Generate code id for transaction/transfer or verify payments by trace id 12 | payment, 13 | }; 14 | }; 15 | 16 | export const PaymentClient = buildClient(PaymentBaseClient); 17 | 18 | export default PaymentClient; 19 | -------------------------------------------------------------------------------- /src/client/pin.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type Keystore from './types/keystore'; 3 | import type { AuthenticationUserResponse } from './types/user'; 4 | import { buildClient } from './utils/client'; 5 | import { signTipBody, getNanoTime, getTipPinUpdateMsg, getVerifyPinTipBody, signEd25519PIN } from './utils/pin'; 6 | 7 | /** 8 | * Methods to verify or update pin with keystore 9 | * Note: 10 | * * If you forget your PIN, there is no way to retrieve or restore it 11 | * Docs: https://developers.mixin.one/docs/api/pin/pin-update 12 | */ 13 | export const PinKeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined) => { 14 | function updatePin(firstPin: string, secondPin = ''): Promise { 15 | const oldEncrypted = firstPin ? signEd25519PIN(firstPin, keystore) : ''; 16 | const newEncrypted = signEd25519PIN(secondPin, keystore); 17 | return axiosInstance.post('/pin/update', { old_pin_base64: oldEncrypted, pin_base64: newEncrypted }); 18 | } 19 | 20 | function updateTipPin(firstPin: string, secondPin: string, counter: number): Promise { 21 | const pubTipBuf = Buffer.from(secondPin, 'hex'); 22 | if (pubTipBuf.byteLength !== 32) throw new Error('invalid public key'); 23 | const pubTipHex = getTipPinUpdateMsg(pubTipBuf, counter).toString('hex'); 24 | 25 | const oldEncrypted = firstPin ? signEd25519PIN(firstPin, keystore) : ''; 26 | const newEncrypted = signEd25519PIN(pubTipHex, keystore); 27 | return axiosInstance.post('/pin/update', { old_pin_base64: oldEncrypted, pin_base64: newEncrypted }); 28 | } 29 | 30 | return { 31 | /** Verify a user's PIN, the iterator of the pin will increment also */ 32 | verify: (pin: string) => { 33 | const encrypted = signEd25519PIN(pin, keystore); 34 | return axiosInstance.post('/pin/verify', { pin: encrypted }); 35 | }, 36 | 37 | verifyTipPin: (pin: string) => { 38 | const timestamp = getNanoTime(); 39 | const msg = getVerifyPinTipBody(timestamp); 40 | const signedTipPin = signTipBody(pin, msg); 41 | return axiosInstance.post('/pin/verify', { 42 | pin_base64: signEd25519PIN(signedTipPin, keystore), 43 | timestamp, 44 | }); 45 | }, 46 | 47 | /** Change the PIN of the user, or setup a new PIN if it is not set yet */ 48 | update: updatePin, 49 | 50 | updateTipPin, 51 | }; 52 | }; 53 | 54 | export const PinClient = buildClient(PinKeystoreClient); 55 | 56 | export default PinClient; 57 | -------------------------------------------------------------------------------- /src/client/safe.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { 3 | Keystore, 4 | AuthenticationUserResponse, 5 | SafeAsset, 6 | SafeDepositEntriesRequest, 7 | SafeDepositEntryResponse, 8 | SafePendingDepositRequest, 9 | SafePendingDepositResponse, 10 | SafeSnapshot, 11 | SafeSnapshotsRequest, 12 | SafeWithdrawalFee, 13 | } from './types'; 14 | import { buildClient, signEd25519PIN, signSafeRegistration } from './utils'; 15 | 16 | export const SafeKeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined) => ({ 17 | /** If you want to register safe user, you need to upgrade TIP PIN first. */ 18 | register: (user_id: string, tipPin: string, priv: Buffer): Promise => { 19 | const data = signSafeRegistration(user_id, tipPin, priv); 20 | data.pin_base64 = signEd25519PIN(data.pin_base64, keystore); 21 | return axiosInstance.post('/safe/users', data); 22 | }, 23 | 24 | checkRegisteration: () => axiosInstance.get(`/safe/me`), 25 | 26 | assets: (): Promise => axiosInstance.get('/safe/assets'), 27 | 28 | fetchAsset: (id: string): Promise => axiosInstance.get(`/safe/assets/${id}`), 29 | 30 | fetchAssets: (assetIds: string[]): Promise => axiosInstance.post(`/safe/assets/fetch`, assetIds), 31 | 32 | fetchFee: (asset: string, destination: string) => axiosInstance.get(`/safe/assets/${asset}/fees`, { params: { destination } }), 33 | 34 | depositEntries: (data: SafeDepositEntriesRequest) => axiosInstance.post(`/safe/deposit/entries`, data), 35 | 36 | createDeposit: (chain_id: string) => axiosInstance.post('/safe/deposit/entries', { chain_id }), 37 | 38 | pendingDeposits: (params: SafePendingDepositRequest): Promise => 39 | axiosInstance.get(`/safe/deposits`, { params }), 40 | 41 | /** 42 | * Get snapshots for single user 43 | * Or Get snapshots for all network users with app uuid passed 44 | */ 45 | fetchSafeSnapshots: (data: SafeSnapshotsRequest): Promise => 46 | axiosInstance.get(`/safe/snapshots`, { 47 | params: data, 48 | }), 49 | 50 | fetchSafeSnapshot: (id: string): Promise => axiosInstance.get(`/safe/snapshots/${id}`), 51 | }); 52 | export const SafeClient = buildClient(SafeKeystoreClient); 53 | 54 | export default SafeClient; 55 | -------------------------------------------------------------------------------- /src/client/transfer.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { Keystore } from './types/keystore'; 3 | import type { SnapshotRequest, SnapshotResponse } from './types/snapshot'; 4 | import type { TransferRequest } from './types/transfer'; 5 | import type { GhostInputRequest, RawTransactionRequest, GhostKeysResponse } from './types/transaction'; 6 | import { signEd25519PIN } from './utils/pin'; 7 | import { buildClient } from './utils/client'; 8 | 9 | /** 10 | * Methods to transfer asset, withdraw and obtain transfer information 11 | * Note: 12 | * * Once /transfers API is successfully called, it means data has been confirmed by all nodes, and it is irreversible 13 | * Docs: https://developers.mixin.one/docs/api/transfer/transfer 14 | */ 15 | export const TransferKeystoreClient = (axiosInstance: AxiosInstance, keystore: Keystore | undefined) => ({ 16 | /** Get transfer information by traceID */ 17 | fetch: (traceID: string): Promise => axiosInstance.get(`/transfers/trace/${traceID}`), 18 | 19 | /** Get specific snapshot of current user */ 20 | snapshot: (snapshotID: string): Promise => axiosInstance.get(`/snapshots/${snapshotID}`), 21 | 22 | /** Get the snapshots of current user */ 23 | snapshots: (params: SnapshotRequest): Promise => axiosInstance.get(`/snapshots`, { params }), 24 | 25 | /** 26 | * Transfer to specific user 27 | * If you encounter 500 error, do it over again 28 | * If you see the error 20119 password is wrong, do not try again. It is recommended to call the PIN Verification API to confirm 29 | */ 30 | toUser: (pin: string, params: TransferRequest): Promise => { 31 | const request: TransferRequest = { 32 | ...params, 33 | pin: signEd25519PIN(pin, keystore), 34 | }; 35 | return axiosInstance.post('/transfers', request); 36 | }, 37 | 38 | /** Send raw transactions to the mainnet or multisig address */ 39 | toAddress: (pin: string, params: RawTransactionRequest): Promise => { 40 | const request: RawTransactionRequest = { 41 | ...params, 42 | pin: signEd25519PIN(pin, keystore), 43 | }; 44 | return axiosInstance.post('/transactions', request); 45 | }, 46 | 47 | /** Get one-time user keys for mainnet */ 48 | outputs: (input: GhostInputRequest[]): Promise => axiosInstance.post(`/outputs`, input), 49 | }); 50 | 51 | export const TransferClient = buildClient(TransferKeystoreClient); 52 | 53 | export default TransferClient; 54 | -------------------------------------------------------------------------------- /src/client/types/address.ts: -------------------------------------------------------------------------------- 1 | export interface AddressResponse { 2 | type: 'address'; 3 | address_id: string; 4 | asset_id: string; 5 | chain_id: string; 6 | destination: string; 7 | dust: string; 8 | label: string; 9 | tag: string; 10 | updated_at: string; 11 | } 12 | 13 | export interface AddressRequest { 14 | asset_id: string; 15 | label: string; 16 | /** alias public_key */ 17 | destination: string; 18 | /** alias memo */ 19 | tag?: string; 20 | } 21 | 22 | export interface MixAddress { 23 | version: number; 24 | uuidMembers: string[]; 25 | xinMembers: string[]; 26 | threshold: number; 27 | } 28 | -------------------------------------------------------------------------------- /src/client/types/app.ts: -------------------------------------------------------------------------------- 1 | export interface AppResponse { 2 | type: 'app'; 3 | app_id: string; 4 | app_number: string; 5 | redirect_url: string; 6 | home_url: string; 7 | name: string; 8 | icon_url: string; 9 | description: string; 10 | capabilities: string[]; 11 | resource_patterns: string[]; 12 | category: string; 13 | creator_id: string; 14 | app_secret: string; 15 | session_secret: string; 16 | session_public_key: string; 17 | has_safe: boolean; 18 | spend_public_key: string; 19 | safe_created_at: string; 20 | updated_at: string; 21 | } 22 | 23 | export interface AppPropertyResponse { 24 | count: number; 25 | price: string; 26 | } 27 | 28 | export interface AppBillingCostResponse { 29 | users: string; 30 | resources: string; 31 | } 32 | 33 | export interface AppBillingResponse { 34 | app_id: string; 35 | cost: AppBillingCostResponse; 36 | credit: string; 37 | } 38 | 39 | export interface AppSecretResponse { 40 | app_secret: string; 41 | } 42 | 43 | export interface AppSessionResponse { 44 | session_id: string; 45 | server_public_key: string; 46 | } 47 | 48 | export interface AppRegistrationResponse { 49 | spend_public_key: string; 50 | } 51 | 52 | export interface AppRequest { 53 | redirect_uri: string; 54 | home_uri: string; 55 | name: string; 56 | description: string; 57 | icon_base64: string; 58 | category: string; 59 | capabilities: string[]; 60 | resource_patterns: string[]; 61 | } 62 | 63 | export interface AppSafeSessionRequest { 64 | session_public_key: string; 65 | } 66 | 67 | export interface AppSafeRegistrationRequest { 68 | spend_public_key: string; 69 | signature_base64: string; 70 | } 71 | -------------------------------------------------------------------------------- /src/client/types/asset.ts: -------------------------------------------------------------------------------- 1 | export interface DepositEntryResponse { 2 | destination: string; 3 | tag?: string; 4 | properties?: string[]; 5 | } 6 | 7 | export interface AssetCommonResponse { 8 | type: 'asset'; 9 | asset_id: string; 10 | chain_id: string; 11 | symbol: string; 12 | name: string; 13 | display_symbol: string; 14 | display_name: string; 15 | icon_url: string; 16 | price_btc: string; 17 | price_usd: string; 18 | change_btc: string; 19 | change_usd: string; 20 | asset_key: string; 21 | mixin_id: string; 22 | confirmations: number; 23 | capitalization: number; 24 | liquidity: string; 25 | reserve: string; 26 | precision: number; 27 | withdrawal_memo_possibility: 'negative' | 'positive' | 'possible'; 28 | } 29 | 30 | // fields for 31 | // GET /assets/:id 32 | // GET /network/assets/top 33 | // GET /network/search/:q 34 | // GET /network/assets/:id 35 | // GET /safe/assets/:id 36 | export interface AssetResponse extends AssetCommonResponse { 37 | balance: string; 38 | deposit_entries: DepositEntryResponse[]; 39 | /** 40 | * @Deprecated use deposit_entries 41 | */ 42 | destination: string; 43 | tag: string; 44 | } 45 | -------------------------------------------------------------------------------- /src/client/types/attachment.ts: -------------------------------------------------------------------------------- 1 | export interface AttachmentResponse { 2 | type: 'attachment'; 3 | attachment_id: string; 4 | upload_url: string; 5 | view_url: string; 6 | created_at: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/client/types/circle.ts: -------------------------------------------------------------------------------- 1 | export interface CircleResponse { 2 | type: 'circle'; 3 | circle_id: string; 4 | user_id: string; 5 | name: string; 6 | created_at: string; 7 | } 8 | 9 | export interface CircleRequest { 10 | offset: string; 11 | limit: number; 12 | } 13 | 14 | export interface CircleConversationResponse { 15 | type: 'circle_conversation'; 16 | circle_id: string; 17 | conversation_id: string; 18 | user_id: string; 19 | created_at: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/client/types/client.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance, AxiosRequestConfig } from 'axios'; 2 | import type { Keystore } from './keystore'; 3 | import { BlazeOptions } from '../../blaze/type'; 4 | 5 | export interface RequestConfig 6 | extends Partial> { 7 | responseCallback?: (rep: unknown) => void; // deprecated 8 | errorMap?: (error: any) => void; 9 | retry?: number; 10 | } 11 | 12 | export interface HTTPConfig { 13 | blazeOptions?: BlazeOptions; 14 | keystore?: Keystore; 15 | requestConfig?: RequestConfig; 16 | } 17 | 18 | export interface BaseClient { 19 | (): KeystoreReturnType & RequestClient; 20 | (config: HTTPConfig): KeystoreReturnType & RequestClient; 21 | } 22 | 23 | export type BaseInnerClient = (axiosInstance: AxiosInstance) => KeystoreReturnType; 24 | export type KeystoreClient = (axiosInstance: AxiosInstance, keystore?: Keystore) => KeystoreReturnType; 25 | export type UnionKeystoreClient = BaseInnerClient | KeystoreClient; 26 | 27 | export interface BuildClient { 28 | (KeystoreClient: UnionKeystoreClient): BaseClient; 29 | } 30 | 31 | export interface RequestClient { 32 | request: ( 33 | config: Partial< 34 | Pick< 35 | AxiosRequestConfig, 36 | 'url' | 'method' | 'data' | 'params' | 'headers' | 'proxy' | 'httpAgent' | 'httpsAgent' | 'cancelToken' | 'baseURL' | 'onDownloadProgress' | 'onUploadProgress' 37 | > 38 | >, 39 | ) => Promise; 40 | } 41 | -------------------------------------------------------------------------------- /src/client/types/code.ts: -------------------------------------------------------------------------------- 1 | import { ConversationResponse } from './conversation'; 2 | import { MultisigRequestResponse } from './multisig'; 3 | import { CollectibleTransactionResponse } from './collectible'; 4 | import { PaymentRequestResponse } from './payment'; 5 | import { UserResponse } from './user'; 6 | import { AuthorizationResponse } from './oauth'; 7 | 8 | export type CodeResponse = ConversationResponse | MultisigRequestResponse | CollectibleTransactionResponse | PaymentRequestResponse | UserResponse | AuthorizationResponse; 9 | 10 | export interface SchemeResponse { 11 | type: 'scheme'; 12 | scheme_id: string; 13 | target: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/client/types/collectible.ts: -------------------------------------------------------------------------------- 1 | export type CollectibleOutputState = 'unspent' | 'signed' | 'spent'; 2 | 3 | export type CollectibleAction = 'sign' | 'unlock'; 4 | 5 | export type CollectibleRequestAction = 'sign' | 'unlock' | 'cancel'; 6 | 7 | export type CollectibleRequestState = 'initial' | 'signed'; 8 | 9 | interface CollectibleMetaResponse { 10 | group: string; 11 | name: string; 12 | description: string; 13 | icon_url: string; 14 | media_url: string; 15 | mime: string; 16 | hash: string; 17 | } 18 | 19 | export interface CollectibleResponse { 20 | type: 'non_fungible_token'; 21 | token_id: string; 22 | group: string; 23 | token: string; 24 | mixin_id: string; 25 | collection_id: string; 26 | nfo: string; 27 | meta: CollectibleMetaResponse; 28 | receivers: string[]; 29 | receivers_threshold: number; 30 | created_at: string; 31 | } 32 | 33 | export interface CollectionResponse { 34 | type: 'collection'; 35 | collection_id: string; 36 | name: string; 37 | description: string; 38 | icon_url: string; 39 | created_at: string; 40 | } 41 | 42 | export interface CollectibleOutputsRequest { 43 | state?: CollectibleOutputState; 44 | offset?: string; 45 | limit?: number; 46 | members: string[]; 47 | threshold: number; 48 | } 49 | 50 | export interface CollectibleOutputsResponse { 51 | type: 'non_fungible_output'; 52 | user_id: string; 53 | output_id: string; 54 | token_id: string; 55 | transaction_hash: string; 56 | output_index: number; 57 | amount: string; 58 | senders: string[]; 59 | senders_threshold: number; 60 | receivers: string[]; 61 | receivers_threshold: number; 62 | extra: string; 63 | state: string; 64 | created_at: string; 65 | updated_at: string; 66 | signed_by: string; 67 | signed_tx: string; 68 | } 69 | 70 | export interface CollectibleTransactionRequest { 71 | action: CollectibleAction; 72 | raw: string; 73 | } 74 | 75 | export interface CollectibleTransactionResponse { 76 | type: ''; 77 | request_id: string; 78 | user_id: string; 79 | token_id: string; 80 | amount: string; 81 | senders: string[]; 82 | senders_threshold: number; 83 | receivers: string[]; 84 | receivers_threshold: number; 85 | signers: string; 86 | action: string; 87 | state: string; 88 | transaction_hash: string; 89 | raw_transaction: string; 90 | created_at: string; 91 | updated_at: string; 92 | code_id: string; 93 | } 94 | -------------------------------------------------------------------------------- /src/client/types/conversation.ts: -------------------------------------------------------------------------------- 1 | import { CircleConversationResponse } from './circle'; 2 | 3 | type ConversationCategory = 'CONTACT' | 'GROUP'; 4 | type ConversationRole = 'ADMIN' | ''; 5 | export type ConversationAction = 'ADD' | 'REMOVE' | 'ROLE'; 6 | 7 | export interface ParticipantResponse { 8 | type: 'participant'; 9 | user_id: string; 10 | session_id: string; 11 | public_key: string; 12 | } 13 | 14 | export interface ConversationResponse { 15 | type: 'conversation'; 16 | conversation_id: string; 17 | creator_id: string; 18 | category: ConversationCategory; 19 | name: string; 20 | icon_url: string; 21 | announcement: string; 22 | created_at: string; 23 | 24 | code_id: string; 25 | code_url: string; 26 | mute_until: string; 27 | expire_in: number; 28 | 29 | participant_sessions: ParticipantResponse[]; 30 | circles: CircleConversationResponse[]; 31 | } 32 | 33 | export interface ParticipantRequest { 34 | user_id: string; 35 | role?: ConversationRole; 36 | } 37 | 38 | export interface ConversationRequest { 39 | category: ConversationCategory; 40 | conversation_id: string; 41 | participants: ParticipantRequest[]; 42 | name?: string; 43 | announcement?: string; 44 | } 45 | -------------------------------------------------------------------------------- /src/client/types/error.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorResponse { 2 | status: number; 3 | code: number; 4 | description: string; 5 | extra?: object; 6 | request_id?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/client/types/external.ts: -------------------------------------------------------------------------------- 1 | export interface DepositRequest { 2 | asset?: string; 3 | destination?: string; 4 | tag?: string; 5 | order?: 'ASC' | 'DESC'; 6 | offset?: string; 7 | limit?: number; 8 | user?: string; 9 | } 10 | 11 | export interface ProxyRequest { 12 | method: string; 13 | params: any[]; 14 | } 15 | -------------------------------------------------------------------------------- /src/client/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './address'; 2 | export * from './app'; 3 | export * from './asset'; 4 | export * from './attachment'; 5 | export * from '../../blaze/type'; 6 | export * from './circle'; 7 | export * from './client'; 8 | export * from './code'; 9 | export * from './collectible'; 10 | export * from './conversation'; 11 | export * from './error'; 12 | export * from './external'; 13 | export * from './invoice'; 14 | export * from './keystore'; 15 | export * from './message'; 16 | export * from './mixin_transaction'; 17 | export * from './multisig'; 18 | export * from './network'; 19 | export * from './nfo'; 20 | export * from './oauth'; 21 | export * from './payment'; 22 | export * from './safe'; 23 | export * from './snapshot'; 24 | export * from './transaction'; 25 | export * from './transfer'; 26 | export * from './user'; 27 | export * from './utxo'; 28 | -------------------------------------------------------------------------------- /src/client/types/invoice.ts: -------------------------------------------------------------------------------- 1 | import type { MixAddress } from './address'; 2 | 3 | export interface InvoiceEntry { 4 | trace_id: string; 5 | asset_id: string; 6 | amount: string; 7 | extra: Buffer; 8 | index_references: number[]; 9 | hash_references: string[]; 10 | } 11 | 12 | export interface MixinInvoice { 13 | version: number; 14 | recipient: MixAddress; 15 | entries: InvoiceEntry[]; 16 | } 17 | -------------------------------------------------------------------------------- /src/client/types/keystore.ts: -------------------------------------------------------------------------------- 1 | export interface AppKeystore { 2 | app_id: string; 3 | session_id: string; 4 | server_public_key: string; 5 | session_private_key: string; 6 | } 7 | 8 | export interface OAuthKeystore { 9 | app_id: string; 10 | scope: string; 11 | authorization_id: string; 12 | session_private_key: string; 13 | } 14 | 15 | export interface NetworkUserKeystore { 16 | app_id: string; 17 | session_id: string; 18 | session_private_key: string; 19 | pin_token_base64: string; 20 | } 21 | 22 | export type Keystore = AppKeystore | OAuthKeystore | NetworkUserKeystore; 23 | 24 | export default Keystore; 25 | -------------------------------------------------------------------------------- /src/client/types/message.ts: -------------------------------------------------------------------------------- 1 | export type MessageStatus = 'SENT' | 'DELIVERED' | 'READ'; 2 | 3 | export type MessageCategory = 4 | | 'PLAIN_TEXT' 5 | | 'PLAIN_AUDIO' 6 | | 'PLAIN_POST' 7 | | 'PLAIN_IMAGE' 8 | | 'PLAIN_DATA' 9 | | 'PLAIN_STICKER' 10 | | 'PLAIN_LIVE' 11 | | 'PLAIN_LOCATION' 12 | | 'PLAIN_VIDEO' 13 | | 'PLAIN_CONTACT' 14 | | 'APP_CARD' 15 | | 'APP_BUTTON_GROUP' 16 | | 'MESSAGE_RECALL' 17 | | 'SYSTEM_CONVERSATION' 18 | | 'SYSTEM_ACCOUNT_SNAPSHOT'; 19 | 20 | export type EncryptedMessageStatus = 'SUCCESS' | 'FAILED'; 21 | 22 | export interface AcknowledgementResponse { 23 | message_id: string; 24 | status: MessageStatus; 25 | } 26 | 27 | export interface AcknowledgementRequest { 28 | message_id: string; 29 | status: string; 30 | } 31 | 32 | export interface MessageView { 33 | type: 'message'; 34 | representative_id: string; 35 | quote_message_id: string; 36 | conversation_id: string; 37 | user_id: string; 38 | session_id: string; 39 | message_id: string; 40 | category: MessageCategory; 41 | data: string; 42 | data_base64: string; 43 | status: MessageStatus; 44 | source: string; 45 | created_at: string; 46 | updated_at: string; 47 | } 48 | 49 | export interface MessageRequest { 50 | conversation_id: string; 51 | recipient_id?: string; 52 | message_id: string; 53 | category: MessageCategory; 54 | data_base64: string; 55 | representative_id?: string; 56 | quote_message_id?: string; 57 | silent?: boolean; 58 | expire_in?: number; 59 | } 60 | 61 | export interface StickerMessageRequest { 62 | sticker_id: string; 63 | name?: string; 64 | album_id?: string; 65 | } 66 | 67 | export interface ImageMessageRequest { 68 | attachment_id: string; 69 | mime_type: string; 70 | width: number; 71 | height: number; 72 | size: number; 73 | thumbnail?: string; 74 | } 75 | 76 | export interface AudioMessageRequest { 77 | attachment_id: string; 78 | mime_type: string; 79 | size: number; 80 | duration: number; 81 | wave_form?: string; 82 | } 83 | 84 | export interface VideoMessageRequest { 85 | attachment_id: string; 86 | mime_type: string; 87 | width: number; 88 | height: number; 89 | size: number; 90 | duration: number; 91 | thumbnail?: string; 92 | } 93 | 94 | export interface ContactMessageRequest { 95 | user_id: string; 96 | } 97 | 98 | export interface AppCardMessageRequest { 99 | app_id: string; 100 | icon_url: string; 101 | title: string; 102 | description: string; 103 | action: string; 104 | shareable?: boolean; 105 | } 106 | 107 | export interface FileMessageRequest { 108 | attachment_id: string; 109 | mime_type: string; 110 | size: number; 111 | name: string; 112 | } 113 | 114 | export interface LiveMessageRequest { 115 | width: number; 116 | height: number; 117 | thumb_url: string; 118 | url: string; 119 | shareable?: boolean; 120 | } 121 | 122 | export interface LocationMessageRequest { 123 | longitude: number; 124 | latitude: number; 125 | address?: string; 126 | name?: string; 127 | } 128 | 129 | export interface AppButtonMessageRequest { 130 | label: string; 131 | action: string; 132 | color: string; 133 | } 134 | 135 | export interface TransferMessageRequest { 136 | type: string; 137 | snapshot_id: string; 138 | opponent_id: string; 139 | asset_id: string; 140 | amount: number; 141 | trace_id: string; 142 | memo: string; 143 | created_at: string; 144 | } 145 | 146 | export interface RecallMessageRequest { 147 | message_id: string; 148 | } 149 | 150 | export interface EncryptedMessageResponse { 151 | type: 'message'; 152 | message_id: string; 153 | recipient_id: string; 154 | state: EncryptedMessageStatus; 155 | } 156 | -------------------------------------------------------------------------------- /src/client/types/mixin_transaction.ts: -------------------------------------------------------------------------------- 1 | export interface MintData { 2 | group: string; 3 | batch: bigint; 4 | amount: number; 5 | } 6 | 7 | export interface DepositData { 8 | chain: string; 9 | asset: string; 10 | transaction: string; 11 | index: bigint; 12 | amount: number; 13 | } 14 | 15 | export interface WithdrawData { 16 | address: string; 17 | tag: string; 18 | chain?: string; 19 | asset?: string; 20 | } 21 | 22 | export interface Input { 23 | hash: string; 24 | index: number; 25 | genesis?: string; 26 | deposit?: DepositData; 27 | mint?: MintData; 28 | } 29 | 30 | export interface Output { 31 | type?: number; 32 | amount: string; 33 | keys: string[]; 34 | withdrawal?: WithdrawData; 35 | script?: string; 36 | mask?: string; 37 | } 38 | 39 | export interface Aggregated { 40 | signers: number[]; 41 | signature: string; 42 | } 43 | 44 | export interface Transaction { 45 | hash?: string; 46 | snapshot?: string; 47 | signatures?: { 48 | [key: number]: string; 49 | }; 50 | aggregated?: { 51 | signers: number[]; 52 | signature: string; 53 | }; 54 | 55 | version?: number; 56 | asset: string; 57 | inputs?: Input[]; 58 | outputs?: Output[]; 59 | extra: string; 60 | } 61 | -------------------------------------------------------------------------------- /src/client/types/multisig.ts: -------------------------------------------------------------------------------- 1 | import { Input, Output } from './mixin_transaction'; 2 | import { UtxoState } from './utxo'; 3 | 4 | export type MultisigInitAction = 'sign' | 'unlock'; 5 | 6 | export type MultisigAction = MultisigInitAction | 'cancel'; 7 | 8 | export type MultisigState = 'initial' | 'signed' | 'unlocked'; 9 | 10 | export type MultisigOrder = 'created' | 'updated'; 11 | 12 | export interface MultisigRequest { 13 | members: string[]; 14 | threshold: number; 15 | state?: UtxoState; 16 | offset?: string; 17 | limit?: number; 18 | order?: MultisigOrder; 19 | } 20 | 21 | export interface MultisigUtxoResponse { 22 | type: 'multisig_utxo'; 23 | user_id: string; 24 | utxo_id: string; 25 | asset_id: string; 26 | transaction_hash: string; 27 | output_index: number; 28 | amount: string; 29 | threshold: number; 30 | members: string[]; 31 | memo: string; 32 | state: UtxoState; 33 | sender: string; 34 | created_at: string; 35 | updated_at: string; 36 | signed_by: string; 37 | signed_tx: string; 38 | } 39 | 40 | export interface MultisigRequestResponse { 41 | type: 'multisig_request'; 42 | request_id: string; 43 | user_id: string; 44 | asset_id: string; 45 | amount: string; 46 | threshold: string; 47 | senders: string; 48 | receivers: string[]; 49 | signers: string[]; 50 | memo: string[]; 51 | action: MultisigInitAction; 52 | state: MultisigState; 53 | transaction_hash: string; 54 | raw_transaction: string; 55 | created_at: string; 56 | updated_at: string; 57 | code_id: string; 58 | } 59 | 60 | export interface MultisigTransaction { 61 | /** 2 */ 62 | version: number; 63 | /** mixin_id of asset */ 64 | asset: string; 65 | inputs: Input[]; 66 | outputs: Output[]; 67 | extra: string; 68 | } 69 | 70 | export interface SafeTransaction { 71 | /** 5 */ 72 | version: number; 73 | asset: string; 74 | inputs: Input[]; 75 | outputs: Output[]; 76 | extra: Buffer; 77 | references: string[]; 78 | signatureMap?: Record[]; 79 | } 80 | 81 | export interface SafeMultisigsReceiver { 82 | members: string[]; 83 | members_hash: string; 84 | threshold: number; 85 | destination: string; 86 | tag: string; 87 | withdrawal_hash: string; 88 | } 89 | 90 | export interface SafeMultisigsResponse { 91 | type: 'transaction_request'; 92 | request_id: string; 93 | transaction_hash: string; 94 | asset_id: string; // asset uuid 95 | kernel_asset_id: string; // SHA256Hash of asset uuid 96 | amount: string; 97 | receivers: SafeMultisigsReceiver[]; 98 | senders: string[]; 99 | senders_hash: string; 100 | senders_threshold: number; 101 | signers: string[]; 102 | extra: string; 103 | raw_transaction: string; 104 | created_at: string; 105 | updated_at: string; 106 | views: string[]; 107 | } 108 | -------------------------------------------------------------------------------- /src/client/types/network.ts: -------------------------------------------------------------------------------- 1 | import { AssetCommonResponse } from './asset'; 2 | 3 | export interface NetworkInfoResponse { 4 | assets: NetworkAssetResponse[]; 5 | chains: NetworkChainResponse[]; 6 | assets_count: string; 7 | peak_throughput: string; 8 | snapshots_count: string; 9 | type: string; 10 | } 11 | 12 | export interface NetworkChainResponse { 13 | type: 'chain'; 14 | chain_id: string; 15 | name: string; 16 | symbol: string; 17 | icon_url: string; 18 | managed_block_height: string; 19 | deposit_block_height: string; 20 | external_block_height: string; 21 | threshold: number; 22 | withdrawal_timestamp: string; 23 | withdrawal_pending_count: string; 24 | withdrawal_fee: string; 25 | is_synchronized: string; 26 | } 27 | 28 | // for GET /network/assets/:id only 29 | export interface NetworkAssetResponse extends AssetCommonResponse { 30 | amount: string; 31 | fee: string; 32 | snapshots_count: number; 33 | } 34 | 35 | export interface NetworkPriceResponse { 36 | type: string; 37 | price_btc: number; 38 | price_usd: number; 39 | } 40 | 41 | export interface NetworkSnapshotAsset { 42 | asset_id: string; 43 | chain_id: string; 44 | icon_url: string; 45 | name: string; 46 | symbol: string; 47 | type: string; 48 | } 49 | 50 | export interface NetworkSnapshotResponse { 51 | type: 'snapshot'; 52 | amount: number; 53 | asset: NetworkSnapshotAsset; 54 | created_at: string; 55 | snapshot_id: string; 56 | source: string; 57 | state: string; 58 | snapshot_hash: string; 59 | 60 | // 4 private fields that only be returned with correct permission 61 | user_id?: string; 62 | trace_id?: string; 63 | opponent_id?: string; 64 | data?: string; 65 | } 66 | 67 | export interface ExternalTransactionResponse { 68 | transaction_id: string; 69 | created_at: string; 70 | transaction_hash: string; 71 | sender: string; 72 | chain_id: string; 73 | asset_id: string; 74 | amount: string; 75 | destination: string; 76 | tag: string; 77 | confirmations: string; 78 | threshold: string; 79 | } 80 | 81 | export interface ExchangeRateResponse { 82 | code: string; 83 | rate: number; 84 | } 85 | 86 | export interface CheckAddressResponse { 87 | destination: string; 88 | tag: string; 89 | fee: string; 90 | } 91 | 92 | export interface CheckAddressRequest { 93 | asset: string; 94 | destination: string; 95 | tag?: string; 96 | } 97 | 98 | export interface NetworkSnapshotRequest { 99 | limit: number; 100 | offset: string; 101 | asset?: string; 102 | order?: 'ASC' | 'DESC'; 103 | } 104 | -------------------------------------------------------------------------------- /src/client/types/nfo.ts: -------------------------------------------------------------------------------- 1 | export interface NFOMemo { 2 | prefix: string; 3 | version: number; 4 | 5 | mask?: number; 6 | chain?: string /** chain uuid */; 7 | class?: string /** contract address */; 8 | collection?: string /** collection uuid */; 9 | token?: number; 10 | extra: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/client/types/oauth.ts: -------------------------------------------------------------------------------- 1 | import { AppResponse } from './app'; 2 | 3 | export type OAuthScope = 'PROFILE:READ' | 'ASSETS:READ' | 'PHONE:READ' | 'CONTACTS:READ' | 'MESSAGES:REPRESENT' | 'SNAPSHOTS:READ' | 'CIRCLES:READ' | 'CIRCLES:WRITE'; 4 | 5 | export interface AccessTokenResponse { 6 | scope: string; 7 | authorization_id: string; 8 | /** public key from server */ 9 | ed25519: string; 10 | } 11 | export interface AccessTokenRequest { 12 | client_id: string; 13 | code: string; 14 | ed25519: string; 15 | client_secret?: string; 16 | code_verifier?: string; 17 | } 18 | 19 | export interface AuthorizeRequest { 20 | authorization_id: string; 21 | scopes: OAuthScope[]; 22 | pin_base64?: string; 23 | } 24 | 25 | export interface AuthorizationResponse { 26 | type: 'authorization'; 27 | authorization_id: string; 28 | authorization_code: string; 29 | scopes: OAuthScope[]; 30 | code_id: string; 31 | app: Omit; 32 | created_at: string; 33 | accessed_at: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/client/types/payment.ts: -------------------------------------------------------------------------------- 1 | import { UserResponse } from './user'; 2 | import { AssetResponse } from './asset'; 3 | import { AddressResponse } from './address'; 4 | 5 | // todo remove? 6 | // For 1to1 transfer 7 | export interface PaymentResponse { 8 | recipient: UserResponse; 9 | asset: AssetResponse; 10 | address: AddressResponse; 11 | amount: string; 12 | status: string; 13 | } 14 | 15 | // For multisig transfer 16 | export interface PaymentRequestResponse { 17 | type: 'payment'; 18 | asset_id: string; 19 | amount: string; 20 | receivers: string[]; 21 | threshold: number; 22 | memo: string; 23 | trace_id: string; 24 | created_at: string; 25 | status: string; 26 | code_id: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/client/types/safe.ts: -------------------------------------------------------------------------------- 1 | import type { MixAddress } from './address'; 2 | 3 | // field for: 4 | // GET safe/assets 5 | // GET safe/assets/:id 6 | // GET safe/assets/fetch 7 | export interface SafeAsset { 8 | type: 'safe_asset'; 9 | asset_id: string; 10 | chain_id: string; 11 | symbol: string; 12 | name: string; 13 | display_symbol: string; 14 | display_name: string; 15 | icon_url: string; 16 | price_btc: string; 17 | price_usd: string; 18 | change_btc: string; 19 | change_usd: string; 20 | asset_key: string; 21 | precision: number; 22 | dust: string; 23 | confirmations: number; 24 | kernel_asset_id: string; 25 | price_updated_at: string; 26 | } 27 | 28 | export interface SafeDepositEntriesRequest { 29 | members: string[]; 30 | threshold: number; 31 | chain_id: string; 32 | } 33 | 34 | export interface SafeDepositEntryResponse { 35 | type: 'deposit_entry'; 36 | entry_id: string; 37 | chain_id: string; 38 | members: string[]; 39 | threshold: number; 40 | destination: string; 41 | tag: string; 42 | signature: string; 43 | is_primary: boolean; 44 | } 45 | 46 | export interface SafePendingDepositRequest { 47 | asset: string; 48 | destination?: string; 49 | tag?: string; 50 | offset?: string; 51 | limit?: number; 52 | } 53 | 54 | export interface SafePendingDepositResponse { 55 | deposit_id: string; 56 | transaction_hash: string; 57 | amount: string; 58 | confirmations: number; 59 | threshold: number; 60 | created_at: string; 61 | } 62 | 63 | export interface SafeSnapshot { 64 | snapshot_id: string; 65 | type: string; 66 | asset_id: string; 67 | amount: string; 68 | user_id: string; 69 | opponent_id: string; 70 | memo: string; 71 | transaction_hash: string; 72 | created_at: string; 73 | trace_id: string | null; 74 | confirmations: number | null; 75 | opening_balance: string | null; 76 | closing_balance: string | null; 77 | deposit: SafeDeposit | null; 78 | withdrawal: SafeWithdrawal | null; 79 | } 80 | 81 | export interface SafeDeposit { 82 | deposit_hash: string; 83 | } 84 | 85 | export interface SafeWithdrawal { 86 | withdrawal_hash: string; 87 | receiver: string; 88 | } 89 | 90 | export interface SafeWithdrawalRecipient { 91 | amount: string; 92 | destination: string; 93 | tag?: string; 94 | } 95 | 96 | export interface SafeMixinRecipient { 97 | mixAddress: MixAddress; 98 | amount: string; 99 | } 100 | 101 | export type SafeTransactionRecipient = SafeWithdrawalRecipient | SafeMixinRecipient; 102 | 103 | export interface SafeWithdrawalFee { 104 | type: 'withdrawal_fee'; 105 | amount: string; 106 | asset_id: string; 107 | } 108 | -------------------------------------------------------------------------------- /src/client/types/snapshot.ts: -------------------------------------------------------------------------------- 1 | interface BaseSnapshotResponse { 2 | snapshot_id: string; 3 | asset_id: string; 4 | amount: string; 5 | closing_balance: string; 6 | created_at: string; 7 | opening_balance: string; 8 | snapshot_at?: string; 9 | snapshot_hash?: string; 10 | transaction_hash: string; 11 | type: string; 12 | } 13 | 14 | export interface TransferResponse extends BaseSnapshotResponse { 15 | type: 'transfer'; 16 | opponent_id: string; 17 | trace_id: string; 18 | memo: string; 19 | } 20 | 21 | export interface DepositResponse extends BaseSnapshotResponse { 22 | type: 'deposit'; 23 | output_index: number; 24 | sender: string; 25 | } 26 | 27 | export interface RawTransactionResponse extends BaseSnapshotResponse { 28 | type: 'raw'; 29 | opponent_key: string; 30 | opponent_receivers: string[]; 31 | opponent_threshold: number; 32 | trace_id: string; 33 | memo: string; 34 | state: string; 35 | } 36 | 37 | export type SnapshotResponse = DepositResponse | TransferResponse | RawTransactionResponse; 38 | 39 | export interface SnapshotRequest { 40 | limit: number; 41 | offset: string; 42 | order?: 'ASC' | 'DESC'; 43 | asset?: string; 44 | opponent?: string; 45 | /** only for withdrawals */ 46 | destination?: string; 47 | tag?: string; 48 | } 49 | 50 | export interface SafeSnapshotsRequest { 51 | app?: string; 52 | asset?: string; 53 | opponent?: string; 54 | offset?: string; 55 | limit?: number; 56 | } 57 | -------------------------------------------------------------------------------- /src/client/types/transaction.ts: -------------------------------------------------------------------------------- 1 | export interface RawTransactionRequest { 2 | asset_id: string; 3 | amount?: string; 4 | trace_id?: string; 5 | memo?: string; 6 | /** OpponentKey used for raw transaction */ 7 | opponent_key?: string; 8 | opponent_multisig?: { 9 | receivers: string[]; 10 | threshold: number; 11 | }; 12 | 13 | pin?: string; 14 | } 15 | 16 | export interface GhostInputRequest { 17 | receivers: string[]; 18 | index: number; 19 | hint?: string; 20 | } 21 | 22 | export interface GhostKeysResponse { 23 | type: 'ghost_key'; 24 | keys: string[]; 25 | mask: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/client/types/transfer.ts: -------------------------------------------------------------------------------- 1 | export interface TransferRequest { 2 | asset_id: string; 3 | opponent_id: string; 4 | amount?: string; 5 | trace_id?: string; 6 | memo?: string; 7 | 8 | pin?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/client/types/user.ts: -------------------------------------------------------------------------------- 1 | export type Operation = 'ADD' | 'REMOVE' | 'BLOCK' | 'UNBLOCK'; 2 | 3 | export interface UserResponse { 4 | type: 'user'; 5 | user_id: string; 6 | identity_number: string; 7 | /** If there is no `PHONE:READ` permission, it will always be empty string */ 8 | phone: string; 9 | full_name: string; 10 | biography: string; 11 | avatar_url: string; 12 | relationship: string; 13 | mute_until: string; 14 | created_at: string; 15 | is_verified: boolean; 16 | is_scam: boolean; 17 | has_safe: boolean; 18 | } 19 | 20 | export interface AuthenticationUserResponse extends UserResponse { 21 | session_id: string; 22 | pin_token: string; 23 | pin_token_base64: string; 24 | code_id: string; 25 | code_url: string; 26 | device_status: string; 27 | 28 | has_safe: boolean; 29 | has_pin: boolean; 30 | has_emergency_contact: boolean; 31 | receive_message_source: string; 32 | accept_conversation_source: string; 33 | accept_search_source: string; 34 | fiat_currency: string; 35 | transfer_notification_threshold: number; 36 | transfer_confirmation_threshold: number; 37 | 38 | public_key?: string; 39 | private_key?: string; 40 | tip_counter: number; 41 | tip_key_base64: string; 42 | } 43 | 44 | export interface PreferenceRequest { 45 | receive_message_source: string; 46 | accept_conversation_source: string; 47 | accept_search_source: string; 48 | fiat_currency: string; 49 | transfer_notification_threshold?: number; 50 | transfer_confirmation_threshold?: number; 51 | } 52 | 53 | export interface RelationshipRequest { 54 | user_id: string; 55 | action: Operation; 56 | } 57 | 58 | export interface RelationshipAddRequest extends RelationshipRequest { 59 | phone?: string; 60 | full_name?: string; 61 | } 62 | 63 | export interface LogRequest { 64 | category?: string; 65 | offset?: string; 66 | limit?: number; 67 | } 68 | 69 | export interface LogResponse { 70 | type: string; 71 | log_id: string; 72 | code: string; 73 | ip_address: string; 74 | created_at: string; 75 | } 76 | -------------------------------------------------------------------------------- /src/client/types/utxo.ts: -------------------------------------------------------------------------------- 1 | export type UtxoState = 'unspent' | 'signed' | 'spent'; 2 | 3 | export interface UtxoOutput { 4 | utxo_id: string; 5 | transaction_hash: string; 6 | output_index: number; 7 | asset: string; 8 | amount: string; 9 | mask: string; 10 | keys: string[]; 11 | threshold: number; 12 | extra: string; 13 | state: UtxoState; 14 | created_at: string; 15 | updated_at: string; 16 | signed_by: string; 17 | signed_at: string; 18 | spent_at: string; 19 | } 20 | 21 | export interface SafeUtxoOutput extends UtxoOutput { 22 | asset_id: string; 23 | kernel_asset_id: string; 24 | receivers: string[]; 25 | receivers_hash: string; 26 | receivers_threshold: number; 27 | senders: string[]; 28 | senders_hash: string; 29 | senders_threshold: number; 30 | sequence: number; 31 | } 32 | 33 | export interface OutputsRequest { 34 | members: string[]; 35 | threshold: number; 36 | state?: UtxoState; 37 | offset?: string; 38 | limit?: number; 39 | order?: 'ASC' | 'DESC'; 40 | } 41 | 42 | export interface SafeOutputsRequest { 43 | asset?: string; 44 | members?: string[]; 45 | threshold?: number; 46 | state?: UtxoState; 47 | offset?: number; 48 | limit?: number; 49 | } 50 | 51 | export interface SafeBalanceRequest extends SafeOutputsRequest { 52 | asset: string; 53 | } 54 | 55 | export interface GhostKey { 56 | mask: string; 57 | keys: string[]; 58 | } 59 | 60 | export interface GhostKeyRequest { 61 | receivers: string[]; 62 | index: number; 63 | hint: string; 64 | } 65 | 66 | export interface SequencerTransactionRequest { 67 | type: 'kernel_transaction_request'; 68 | request_id: string; 69 | transaction_hash: string; 70 | asset: string; 71 | amount: string; 72 | extra: string; 73 | user_id: string; 74 | state: string; 75 | raw_transaction: string; 76 | created_at: string; 77 | updated_at: string; 78 | snapshot_id: string; 79 | snapshot_hash: string; 80 | snapshot_at: string; 81 | receivers: string[]; 82 | senders: string[]; 83 | senders_hash: string; 84 | senders_threshold: number; 85 | signers: string[]; 86 | views: string[]; 87 | } 88 | 89 | export interface TransactionRequest { 90 | raw: string; 91 | request_id: string; 92 | } 93 | 94 | export interface OutputFetchRequest { 95 | user_id: string; 96 | ids: string[]; 97 | } 98 | 99 | export interface PaymentParams { 100 | uuid?: string; 101 | mainnetAddress?: string; 102 | mixAddress?: string; 103 | xinMembers?: string[]; 104 | uuidMembers?: string[]; 105 | threshold?: number; 106 | 107 | asset?: string; 108 | amount?: string; 109 | memo?: string; 110 | trace?: string; 111 | returnTo?: string; 112 | } 113 | -------------------------------------------------------------------------------- /src/client/user.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { AuthenticationUserResponse, UserResponse, PreferenceRequest, RelationshipRequest, RelationshipAddRequest, LogRequest, LogResponse } from './types/user'; 3 | import type { AtLeastOne } from '../types'; 4 | import { buildClient } from './utils/client'; 5 | 6 | /** Methods to obtain or edit users' profile and relationships */ 7 | export const UserKeystoreClient = (axiosInstance: AxiosInstance) => ({ 8 | /** Get the current user's personal information */ 9 | profile: () => axiosInstance.get(`/me`), 10 | 11 | /** Get the contact list of the users, containing users and bots */ 12 | friends: () => axiosInstance.get(`/friends`), 13 | 14 | /** Get users' block list */ 15 | blockings: () => axiosInstance.get(`/blocking_users`), 16 | 17 | /** Rotate user's code */ 18 | rotateCode: () => axiosInstance.get('/me/code'), 19 | 20 | /** Search users by keyword */ 21 | search: (identityNumberOrPhone: string) => axiosInstance.get(`/search/${identityNumberOrPhone}`), 22 | 23 | /** Get user information by userID */ 24 | fetch: (id: string) => axiosInstance.get(`/users/${id}`), 25 | 26 | /** 27 | * Get users' information by userIDs in bulk 28 | * This API will only return the list of existing users 29 | */ 30 | fetchList: (userIDs: string[]) => axiosInstance.post(`/users/fetch`, userIDs), 31 | 32 | /** Create a network user, can be created by bot only with no permission */ 33 | createBareUser: (fullName: string, sessionSecret: string) => axiosInstance.post('/users', { full_name: fullName, session_secret: sessionSecret }), 34 | 35 | /** Modify current user's personal name and avatar */ 36 | update: (fullName: string, avatarBase64: string) => axiosInstance.post(`/me`, { full_name: fullName, avatar_base64: avatarBase64 }), 37 | 38 | /** update user's preferences */ 39 | updatePreferences: (params: AtLeastOne) => axiosInstance.post(`/me/preferences`, params), 40 | 41 | /** Manage the relationship between two users, one can 'ADD' | 'REMOVE' | 'BLOCK' | 'UNBLOCK' a user */ 42 | updateRelationships: (relationship: RelationshipRequest | RelationshipAddRequest) => axiosInstance.post(`/relationships`, relationship), 43 | 44 | /** Get pin logs of user */ 45 | logs: (params: LogRequest) => axiosInstance.get(`/logs`, { params }), 46 | }); 47 | 48 | export const UserClient = buildClient(UserKeystoreClient); 49 | 50 | export default UserClient; 51 | -------------------------------------------------------------------------------- /src/client/utils/address.ts: -------------------------------------------------------------------------------- 1 | import bs58 from 'bs58'; 2 | import { stringify, parse } from 'uuid'; 3 | import type { MixAddress } from '../types'; 4 | import { newHash } from './uniq'; 5 | import { newKeyFromSeed, publicFromPrivate } from './ed25519'; 6 | 7 | export const MainAddressPrefix = 'XIN'; 8 | export const MixAddressPrefix = 'MIX'; 9 | export const MixAddressVersion = 2; 10 | 11 | export const getPublicFromMainnetAddress = (address: string) => { 12 | try { 13 | if (!address.startsWith(MainAddressPrefix)) return undefined; 14 | 15 | const data = bs58.decode(address.slice(3)); 16 | if (data.length !== 68) return undefined; 17 | 18 | const payload = data.subarray(0, data.length - 4); 19 | const msg = Buffer.concat([Buffer.from(MainAddressPrefix), Buffer.from(payload)]); 20 | const checksum = newHash(msg); 21 | if (!checksum.subarray(0, 4).equals(Buffer.from(data.subarray(data.length - 4)))) return undefined; 22 | return Buffer.from(payload); 23 | } catch { 24 | return undefined; 25 | } 26 | }; 27 | 28 | export const getMainnetAddressFromPublic = (pubKey: Buffer) => { 29 | const msg = Buffer.concat([Buffer.from(MainAddressPrefix), pubKey]); 30 | const checksum = newHash(msg); 31 | const data = Buffer.concat([pubKey, checksum.subarray(0, 4)]); 32 | return `${MainAddressPrefix}${bs58.encode(data)}`; 33 | }; 34 | 35 | export const getMainnetAddressFromSeed = (seed: Buffer) => { 36 | const hash1 = newHash(seed); 37 | const hash2 = newHash(hash1); 38 | const src = Buffer.concat([hash1, hash2]); 39 | const spend = newKeyFromSeed(seed); 40 | const view = newKeyFromSeed(src); 41 | const pub = Buffer.concat([publicFromPrivate(spend), publicFromPrivate(view)]); 42 | return getMainnetAddressFromPublic(pub); 43 | }; 44 | 45 | export const parseMixAddress = (address: string): MixAddress | undefined => { 46 | try { 47 | if (!address.startsWith(MixAddressPrefix)) return undefined; 48 | 49 | const data = bs58.decode(address.slice(3)); 50 | if (data.length < 3 + 16 + 4) { 51 | return undefined; 52 | } 53 | 54 | const payload = data.subarray(0, data.length - 4); 55 | const msg = Buffer.concat([Buffer.from(MixAddressPrefix), Buffer.from(payload)]); 56 | const checksum = newHash(msg); 57 | if (!checksum.subarray(0, 4).equals(Buffer.from(data.subarray(data.length - 4)))) return undefined; 58 | 59 | const version = data.at(0); 60 | const threshold = data.at(1); 61 | const total = data.at(2); 62 | if (version !== 2) return undefined; 63 | if (!threshold || !total || threshold === 0 || threshold > total || total > 64) return undefined; 64 | 65 | const memberData = payload.subarray(3); 66 | const members: string[] = []; 67 | if (memberData.length === total * 16) { 68 | for (let i = 0; i < total; i++) { 69 | const id = stringify(memberData, 16 * i); 70 | members.push(id); 71 | } 72 | return { 73 | version, 74 | uuidMembers: members, 75 | xinMembers: [], 76 | threshold, 77 | }; 78 | } 79 | if (memberData.length === total * 64) { 80 | for (let i = 0; i < total; i++) { 81 | const pub = memberData.subarray(64 * i, 64 * (i + 1)); 82 | const addr = getMainnetAddressFromPublic(Buffer.from(pub)); 83 | members.push(addr); 84 | } 85 | return { 86 | version, 87 | uuidMembers: [], 88 | xinMembers: members, 89 | threshold, 90 | }; 91 | } 92 | 93 | return undefined; 94 | } catch { 95 | return undefined; 96 | } 97 | }; 98 | 99 | export const getMixAddressBuffer = (ma: MixAddress) => { 100 | const members = ma.uuidMembers.length ? ma.uuidMembers : ma.xinMembers; 101 | if (members.length > 255) { 102 | throw new Error(`invalid members length: ${members.length}`); 103 | } 104 | if (ma.threshold === 0 || ma.threshold > members.length) { 105 | throw new Error(`invalid threshold: ${ma.threshold}`); 106 | } 107 | 108 | const prefix = Buffer.concat([Buffer.from([MixAddressVersion]), Buffer.from([ma.threshold]), Buffer.from([members.length])]); 109 | 110 | const memberData: Buffer[] = []; 111 | if (ma.uuidMembers.length) 112 | members.forEach(addr => { 113 | const id = parse(addr); 114 | if (!id) throw new Error(`invalid uuid address: ${addr}`); 115 | memberData.push(Buffer.from(Uint8Array.from(id))); 116 | }); 117 | else if (ma.xinMembers) 118 | members.forEach(addr => { 119 | const pub = getPublicFromMainnetAddress(addr); 120 | if (!pub) throw new Error(`invalid mainnet address: ${addr}`); 121 | memberData.push(pub); 122 | }); 123 | 124 | return Buffer.concat([prefix, ...memberData]); 125 | }; 126 | 127 | export const getMixAddressStringFromBuffer = (data: Buffer) => { 128 | const msg = Buffer.concat([Buffer.from(MixAddressPrefix), data]); 129 | const checksum = newHash(msg); 130 | const buffer = Buffer.concat([data, checksum.subarray(0, 4)]); 131 | return `${MixAddressPrefix}${bs58.encode(buffer)}`; 132 | }; 133 | 134 | export const buildMixAddress = (ma: MixAddress): string => { 135 | const data = getMixAddressBuffer(ma); 136 | return getMixAddressStringFromBuffer(data); 137 | }; 138 | -------------------------------------------------------------------------------- /src/client/utils/amount.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | let zeros = '0'; 4 | while (zeros.length < 256) { 5 | zeros += zeros; 6 | } 7 | 8 | const getMultiplier = (n: number) => BigNumber(`1${zeros.substring(0, n)}`); 9 | 10 | export const formatUnits = (amount: string | number, unit: number) => { 11 | const m = getMultiplier(unit); 12 | return BigNumber(amount).dividedBy(m); 13 | }; 14 | export const parseUnits = (amount: string | number, unit: number) => { 15 | const m = getMultiplier(unit); 16 | return BigNumber(amount).times(m).integerValue(BigNumber.ROUND_FLOOR); 17 | }; 18 | -------------------------------------------------------------------------------- /src/client/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | import { ed25519 } from '@noble/curves/ed25519'; 3 | import { validate } from 'uuid'; 4 | import type { Keystore, AppKeystore, OAuthKeystore, NetworkUserKeystore } from '../types/keystore'; 5 | import { base64RawURLEncode } from './base64'; 6 | import { sha256Hash } from './uniq'; 7 | import { getKeyPair, getRandomBytes } from './ed25519'; 8 | 9 | export const getED25519KeyPair = () => getKeyPair(); 10 | 11 | export const getChallenge = () => { 12 | const seed = getRandomBytes(32); 13 | const verifier = base64RawURLEncode(seed); 14 | const challenge = base64RawURLEncode(sha256Hash(seed)); 15 | return { verifier, challenge }; 16 | }; 17 | 18 | export const signToken = (payload: Object, private_key: string): string => { 19 | const header = base64RawURLEncode(serialize({ alg: 'EdDSA', typ: 'JWT' })); 20 | const payloadStr = base64RawURLEncode(serialize(payload)); 21 | const result = [header, payloadStr]; 22 | 23 | const signData = ed25519.sign(Buffer.from(result.join('.')), private_key); 24 | const sign = base64RawURLEncode(signData); 25 | result.push(sign); 26 | return result.join('.'); 27 | }; 28 | 29 | /** 30 | * sign an authentication token 31 | * sig: sha256(method + uri + params) 32 | */ 33 | export const signAuthenticationToken = (methodRaw: string | undefined, uri: string, params: Object | string, requestID: string, keystore: AppKeystore | NetworkUserKeystore) => { 34 | if (!keystore.session_id || !validate(keystore.session_id)) return ''; 35 | 36 | let method = 'GET'; 37 | if (methodRaw) method = methodRaw.toLocaleUpperCase(); 38 | 39 | let data: string = ''; 40 | if (typeof params === 'object') { 41 | data = serialize(params, { unsafe: true }); 42 | } else if (typeof params === 'string') { 43 | data = params; 44 | } 45 | 46 | const iat = Math.floor(Date.now() / 1000); 47 | const exp = iat + 3600; 48 | const sha256 = sha256Hash(Buffer.from(method + uri + data)).toString('hex'); 49 | 50 | const payload = { 51 | uid: keystore.app_id, 52 | sid: keystore.session_id, 53 | iat, 54 | exp, 55 | jti: requestID, 56 | sig: sha256, 57 | scp: 'FULL', 58 | }; 59 | 60 | return signToken(payload, keystore.session_private_key); 61 | }; 62 | 63 | /** 64 | * Sign an OAuth access token 65 | * Notes: 66 | * requestID should equal the one in header 67 | * scope should be oauth returned 68 | */ 69 | export const signOauthAccessToken = (methodRaw: string | undefined, uri: string, params: Object | string, requestID: string, keystore: OAuthKeystore) => { 70 | if (!keystore.scope) return ''; 71 | 72 | let method = 'GET'; 73 | if (methodRaw) method = methodRaw.toLocaleUpperCase(); 74 | 75 | let data: string = ''; 76 | if (typeof params === 'object') { 77 | data = serialize(params, { unsafe: true }); 78 | } else if (typeof params === 'string') { 79 | data = params; 80 | } 81 | 82 | const iat = Math.floor(Date.now() / 1000); 83 | const exp = iat + 3600; 84 | const sha256 = sha256Hash(Buffer.from(method + uri + data)).toString('hex'); 85 | 86 | const payload = { 87 | iss: keystore.app_id, 88 | aid: keystore.authorization_id, 89 | iat, 90 | exp, 91 | jti: requestID, 92 | sig: sha256, 93 | scp: keystore.scope, 94 | }; 95 | 96 | return signToken(payload, keystore.session_private_key); 97 | }; 98 | 99 | export const signAccessToken = (methodRaw: string | undefined, uri: string, params: Object | string, requestID: string, keystore: Keystore | undefined) => { 100 | if (!keystore || !keystore.app_id || !keystore.session_private_key) return ''; 101 | if (!validate(keystore.app_id)) return ''; 102 | 103 | const privateKey = Buffer.from(keystore.session_private_key, 'hex'); 104 | if (privateKey.byteLength !== 32) return ''; 105 | 106 | if ('authorization_id' in keystore) { 107 | return signOauthAccessToken(methodRaw, uri, params, requestID, keystore); 108 | } 109 | return signAuthenticationToken(methodRaw, uri, params, requestID, keystore); 110 | }; 111 | -------------------------------------------------------------------------------- /src/client/utils/base64.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * mixin uses raw url encoding as default for base64 process 3 | * base64RawURLEncode is the standard raw, unpadded base64 encoding 4 | * base64RawURLDecode is same as encode 5 | * like Golang version https://pkg.go.dev/encoding/base64#Encoding 6 | */ 7 | export const base64RawURLEncode = (raw: Buffer | Uint8Array | string): string => { 8 | let buf = raw; 9 | if (typeof raw === 'string') { 10 | buf = Buffer.from(raw); 11 | } else if (raw instanceof Uint8Array) { 12 | buf = Buffer.from(raw); 13 | } 14 | if (buf.length === 0) { 15 | return ''; 16 | } 17 | return buf.toString('base64').replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_'); 18 | }; 19 | 20 | export const base64RawURLDecode = (raw: string | Buffer): Buffer => { 21 | const data = raw.toString().replaceAll('-', '+').replaceAll('_', '/'); 22 | return Buffer.from(data, 'base64'); 23 | }; 24 | -------------------------------------------------------------------------------- /src/client/utils/client.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { BaseClient, BuildClient, HTTPConfig, KeystoreClient, RequestClient, RequestConfig, UnionKeystoreClient } from '../types/client'; 3 | import type { Keystore } from '../types/keystore'; 4 | import { http } from '../http'; 5 | 6 | export const createAxiosClient = (keystore?: Keystore, requestConfig?: RequestConfig) => http(keystore, requestConfig); 7 | 8 | export const createRequestClient = (axiosInstance: AxiosInstance): RequestClient => ({ 9 | request: config => axiosInstance.request(config), 10 | }); 11 | 12 | export const buildClient: BuildClient = 13 | (KeystoreClient: UnionKeystoreClient): BaseClient => 14 | (config: HTTPConfig = {}): any => { 15 | if (!KeystoreClient) throw new Error('keystore client is required'); 16 | 17 | const { keystore, requestConfig } = config; 18 | const axiosInstance = createAxiosClient(keystore, requestConfig); 19 | const requestClient = createRequestClient(axiosInstance); 20 | const keystoreClient = (KeystoreClient as KeystoreClient)(axiosInstance, keystore); 21 | 22 | return Object.assign(keystoreClient, requestClient); 23 | }; 24 | -------------------------------------------------------------------------------- /src/client/utils/computer.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'uuid'; 2 | import BigNumber from 'bignumber.js'; 3 | import { base64RawURLEncode } from './base64'; 4 | 5 | export const MAX_BASE64_SIZE = 1644; 6 | 7 | export const OperationTypeAddUser = 1; 8 | export const OperationTypeSystemCall = 2; 9 | export const OperationTypeUserDeposit = 3; 10 | 11 | export const userIdToBytes = (uid: string) => { 12 | const x = BigNumber(uid); 13 | const bytes = []; 14 | let i = x; 15 | do { 16 | bytes.unshift(i.mod(256).toNumber()); 17 | i = i.dividedToIntegerBy(256); 18 | } while (!i.isZero()); 19 | do { 20 | bytes.unshift(0); 21 | } while (bytes.length < 8); 22 | return Buffer.from(bytes); 23 | }; 24 | 25 | // bytes of Solana transaction: Buffer.from(tx.serialize()) 26 | export const checkSystemCallSize = (txBuf: Buffer) => txBuf.toString('base64').length < MAX_BASE64_SIZE; 27 | 28 | export const buildSystemCallExtra = (uid: string, cid: string, skipPostProcess: boolean, fid?: string) => { 29 | const flag = skipPostProcess ? 1 : 0; 30 | const ib = userIdToBytes(uid); 31 | const cb = parse(cid); 32 | const data = [ib, cb, Buffer.from([flag])]; 33 | if (fid) data.push(parse(fid)); 34 | return Buffer.concat(data); 35 | }; 36 | 37 | export const buildComputerExtra = (operation: number, extra: Buffer) => Buffer.concat([Buffer.from([operation]), extra]); 38 | 39 | export const encodeMtgExtra = (app_id: string, extra: Buffer) => { 40 | const data = Buffer.concat([parse(app_id), extra]); 41 | return base64RawURLEncode(data); 42 | }; 43 | -------------------------------------------------------------------------------- /src/client/utils/decoder.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from 'uuid'; 2 | import type { Input, Output } from '../types'; 3 | import { magic } from './encoder'; 4 | import { formatUnits } from './amount'; 5 | 6 | export const bytesToInterger = (b: Buffer) => { 7 | let x = 0; 8 | for (let i = 0; i < b.byteLength; i++) { 9 | const byte = b.at(i); 10 | x *= 0x100; 11 | if (byte) x += byte; 12 | } 13 | return x; 14 | }; 15 | 16 | export class Decoder { 17 | buf: Buffer; 18 | 19 | constructor(buf: Buffer) { 20 | this.buf = buf; 21 | } 22 | 23 | subarray(start: number, end?: number) { 24 | return this.buf.subarray(start, end); 25 | } 26 | 27 | read(offset: number) { 28 | this.buf = this.buf.subarray(offset); 29 | } 30 | 31 | readByte() { 32 | const value = this.buf.readUint8(); 33 | this.read(1); 34 | return value; 35 | } 36 | 37 | readBytes() { 38 | const len = this.readByte(); 39 | const value = this.buf.subarray(0, len).toString('hex'); 40 | this.read(len); 41 | return value; 42 | } 43 | 44 | readBytesBuffer() { 45 | const len = this.readByte(); 46 | const value = this.buf.subarray(0, len); 47 | this.read(len); 48 | return value; 49 | } 50 | 51 | readSubarray(end: number) { 52 | const value = this.buf.subarray(0, end); 53 | this.read(end); 54 | return value; 55 | } 56 | 57 | readInt() { 58 | const value = this.buf.readUInt16BE(); 59 | this.read(2); 60 | return value; 61 | } 62 | 63 | readUint32() { 64 | const value = this.buf.readUInt32BE(); 65 | this.read(4); 66 | return value; 67 | } 68 | 69 | readUInt64() { 70 | const value = this.buf.readBigUInt64BE(); 71 | this.read(8); 72 | return value; 73 | } 74 | 75 | readUUID() { 76 | const value = this.buf.subarray(0, 16); 77 | this.read(16); 78 | return stringify(value); 79 | } 80 | 81 | readInteger() { 82 | const len = this.readInt(); 83 | const value = this.buf.subarray(0, len); 84 | this.read(len); 85 | return bytesToInterger(value); 86 | } 87 | 88 | decodeInput() { 89 | const hash = this.subarray(0, 32).toString('hex'); 90 | this.read(32); 91 | const index = this.readInt(); 92 | const input: Input = { 93 | hash, 94 | index, 95 | }; 96 | 97 | const lenGenesis = this.readInt(); 98 | if (lenGenesis > 0) { 99 | input.genesis = this.buf.subarray(0, lenGenesis).toString('hex'); 100 | this.read(lenGenesis); 101 | } 102 | 103 | const depositPrefix = this.subarray(0, 2); 104 | this.read(2); 105 | if (depositPrefix.equals(magic)) { 106 | const chain = this.subarray(0, 32).toString('hex'); 107 | this.read(32); 108 | const asset = this.readBytes(); 109 | const transaction = this.readBytes(); 110 | const index = this.readUInt64(); 111 | const amount = this.readInteger(); 112 | 113 | input.deposit = { 114 | chain, 115 | asset, 116 | transaction, 117 | index, 118 | amount, 119 | }; 120 | } 121 | 122 | const mintPrefix = this.subarray(0, 2); 123 | this.read(2); 124 | if (mintPrefix.equals(magic)) { 125 | const group = this.readBytes(); 126 | const batch = this.readUInt64(); 127 | const amount = this.readInteger(); 128 | 129 | input.mint = { 130 | group, 131 | batch, 132 | amount, 133 | }; 134 | } 135 | 136 | return input; 137 | } 138 | 139 | decodeOutput() { 140 | const t = this.subarray(0, 2); 141 | this.read(2); 142 | if (t.at(0) !== 0) throw new Error(`invalid output type ${t.at(0)}`); 143 | const type = t.at(1); 144 | const amount = this.readInteger(); 145 | 146 | const lenKey = this.readInt(); 147 | const keys = []; 148 | for (let i = 0; i < lenKey; i++) { 149 | const key = this.subarray(0, 32).toString('hex'); 150 | this.read(32); 151 | keys.push(key); 152 | } 153 | const mask = this.subarray(0, 32).toString('hex'); 154 | this.read(32); 155 | const lenScript = this.readInt(); 156 | const script = this.buf.subarray(0, lenScript).toString('hex'); 157 | this.read(lenScript); 158 | 159 | const output: Output = { 160 | type, 161 | amount: formatUnits(amount, 8).toString(), 162 | keys, 163 | mask, 164 | script, 165 | }; 166 | 167 | const prefix = this.subarray(0, 2); 168 | this.read(2); 169 | if (prefix.equals(magic)) { 170 | const address = this.readBytes(); 171 | const tag = this.readBytes(); 172 | output.withdrawal = { 173 | address, 174 | tag, 175 | }; 176 | } 177 | 178 | return output; 179 | } 180 | 181 | decodeSignature() { 182 | const len = this.readInt(); 183 | const sigs: Record = {}; 184 | for (let i = 0; i < len; i++) { 185 | const index = this.readInt(); 186 | const sig = this.buf.subarray(0, 64).toString('hex'); 187 | sigs[index] = sig; 188 | } 189 | return sigs; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/client/utils/ed25519.ts: -------------------------------------------------------------------------------- 1 | import { ed25519, edwardsToMontgomery, edwardsToMontgomeryPriv, x25519 } from '@noble/curves/ed25519'; 2 | import { Field } from '@noble/curves/abstract/modular'; 3 | import { numberToBytesLE, bytesToNumberLE } from '@noble/curves/abstract/utils'; 4 | import { randomBytes } from '@noble/hashes/utils'; 5 | import { blake3Hash, sha512Hash } from './uniq'; 6 | import { putUvarInt } from './encoder'; 7 | 8 | const scMinusOne = Buffer.from('ecd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010', 'hex'); 9 | const base = ed25519.ExtendedPoint.fromHex('5866666666666666666666666666666666666666666666666666666666666666'); 10 | const fn = Field(ed25519.CURVE.n, undefined, true); 11 | 12 | const isReduced = (x: Buffer) => { 13 | for (let i = x.byteLength - 1; i >= 0; i--) { 14 | if (x.at(i)! > scMinusOne.at(i)!) return false; 15 | if (x.at(i)! < scMinusOne.at(i)!) return true; 16 | } 17 | return true; 18 | }; 19 | 20 | const setBytesWithClamping = (x: Buffer) => { 21 | if (x.byteLength !== 32) throw new Error('edwards25519: invalid SetBytesWithClamping input length'); 22 | const wideBytes = Buffer.alloc(64); 23 | x.copy(wideBytes, 0, 0, 32); 24 | wideBytes[0] &= 248; 25 | wideBytes[31] &= 63; 26 | wideBytes[31] |= 64; 27 | const m = fn.create(bytesToNumberLE(wideBytes.subarray(0, 32))); 28 | return m; 29 | }; 30 | 31 | const setUniformBytes = (x: Buffer) => { 32 | if (x.byteLength !== 64) throw new Error('edwards25519: invalid setUniformBytes input length'); 33 | const wideBytes = Buffer.alloc(64); 34 | x.copy(wideBytes); 35 | const m = fn.create(bytesToNumberLE(wideBytes)); 36 | return m; 37 | }; 38 | 39 | const setCanonicalBytes = (x: Buffer) => { 40 | if (x.byteLength !== 32) throw new Error('invalid scalar length'); 41 | if (!isReduced(x)) throw new Error('invalid scalar encoding'); 42 | const s = fn.create(bytesToNumberLE(x)); 43 | return s; 44 | }; 45 | 46 | const scalarBaseMult = (x: bigint) => { 47 | const res = base.multiply(x); 48 | // @ts-ignore 49 | return Buffer.from(res.toRawBytes()); 50 | }; 51 | 52 | const scalarBaseMultToPoint = (x: bigint) => base.multiply(x); 53 | 54 | export const publicFromPrivate = (priv: Buffer) => { 55 | const x = setCanonicalBytes(priv); 56 | const v = scalarBaseMult(x); 57 | return v; 58 | }; 59 | 60 | export const sign = (msg: Buffer, key: Buffer) => { 61 | const digest1 = sha512Hash(key.subarray(0, 32)); 62 | const messageDigest = sha512Hash(Buffer.concat([digest1.subarray(32), msg])); 63 | 64 | const z = setUniformBytes(messageDigest); 65 | const r = scalarBaseMult(z); 66 | 67 | const pub = publicFromPrivate(key); 68 | const hramDigest = sha512Hash(Buffer.concat([r, pub, msg])); 69 | 70 | const x = setUniformBytes(hramDigest); 71 | const y = setCanonicalBytes(key); 72 | const s = numberToBytesLE(fn.add(fn.mul(x, y), z), 32); 73 | return Buffer.concat([r, s]); 74 | }; 75 | 76 | const newPoint = (x: Buffer) => ed25519.ExtendedPoint.fromHex(x.toString('hex')); 77 | 78 | const keyMultPubPriv = (pub: Buffer, priv: Buffer) => { 79 | const q = newPoint(pub); 80 | const x = setCanonicalBytes(priv); 81 | const res = q.multiply(x); 82 | // @ts-ignore 83 | return Buffer.from(res.toRawBytes()); 84 | }; 85 | 86 | const hashScalar = (k: Buffer, index: number) => { 87 | const tmp = Buffer.from(putUvarInt(index)); 88 | const src = Buffer.alloc(64); 89 | let hash = blake3Hash(Buffer.concat([k, tmp])); 90 | hash.copy(src, 0, 0, 32); 91 | hash = blake3Hash(hash); 92 | hash.copy(src, 32, 0, 32); 93 | const s = setUniformBytes(src); 94 | 95 | hash = blake3Hash(Buffer.from(numberToBytesLE(s, 32))); 96 | hash.copy(src, 0, 0, 32); 97 | hash = blake3Hash(hash); 98 | hash.copy(src, 32, 0, 32); 99 | return setUniformBytes(src); 100 | }; 101 | 102 | export const getRandomBytes = (len?: number) => Buffer.from(randomBytes(len ?? ed25519.CURVE.Fp.BYTES)); 103 | 104 | export const getKeyPair = () => { 105 | const seed = getRandomBytes(); 106 | const publicKey = Buffer.from(ed25519.getPublicKey(seed)); 107 | return { 108 | privateKey: Buffer.concat([seed, publicKey]), 109 | publicKey, 110 | seed, 111 | }; 112 | }; 113 | 114 | export const newKeyFromSeed = (seed: Buffer) => { 115 | const s = setUniformBytes(seed); 116 | return Buffer.from(numberToBytesLE(s, 32)); 117 | }; 118 | 119 | export const edwards25519 = { 120 | scalar: fn, 121 | x25519, 122 | edwardsToMontgomery, 123 | edwardsToMontgomeryPriv, 124 | 125 | setBytesWithClamping, 126 | setCanonicalBytes, 127 | setUniformBytes, 128 | 129 | isReduced, 130 | publicFromPrivate, 131 | scalarBaseMult, 132 | scalarBaseMultToPoint, 133 | 134 | newKeyFromSeed, 135 | sign, 136 | 137 | newPoint, 138 | keyMultPubPriv, 139 | hashScalar, 140 | }; 141 | -------------------------------------------------------------------------------- /src/client/utils/encoder.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'uuid'; 2 | import BigNumber from 'bignumber.js'; 3 | import type { Aggregated, Input, Output } from '../types'; 4 | import { parseUnits } from './amount'; 5 | 6 | const MaximumEncodingInt = 0xffff; 7 | 8 | const AggregatedSignaturePrefix = 0xff01; 9 | 10 | export const magic = Buffer.from([0x77, 0x77]); 11 | const empty = Buffer.from([0x00, 0x00]); 12 | 13 | export const integerToBytes = (x: number) => { 14 | const bytes: number[] = []; 15 | if (x === 0) return bytes; 16 | let i = x; 17 | do { 18 | bytes.unshift(i & 255); 19 | i = (i / 2 ** 8) | 0; 20 | } while (i !== 0); 21 | return bytes; 22 | }; 23 | 24 | export const bigNumberToBytes = (x: BigNumber) => { 25 | const bytes = []; 26 | let i = x; 27 | do { 28 | bytes.unshift(i.mod(256).toNumber()); 29 | i = i.dividedToIntegerBy(256); 30 | } while (!i.isZero()); 31 | return Buffer.from(bytes); 32 | }; 33 | 34 | export const putUvarInt = (x: number) => { 35 | const buf = []; 36 | let i = 0; 37 | while (x >= 0x80) { 38 | buf[i] = x | 0x80; 39 | x >>= 7; 40 | i++; 41 | } 42 | buf[i] = x; 43 | return buf; 44 | }; 45 | 46 | export class Encoder { 47 | buf: Buffer; 48 | 49 | constructor(buf: Buffer | undefined) { 50 | this.buf = Buffer.from(''); 51 | if (buf) { 52 | this.buf = buf; 53 | } 54 | } 55 | 56 | buffer() { 57 | return this.buf; 58 | } 59 | 60 | hex() { 61 | return this.buf.toString('hex'); 62 | } 63 | 64 | write(buf: Buffer) { 65 | this.buf = Buffer.concat([this.buf, buf]); 66 | } 67 | 68 | writeBytes(buf: Buffer) { 69 | const len = buf.byteLength; 70 | this.writeInt(len); 71 | this.write(buf); 72 | } 73 | 74 | writeSlice(buf: Buffer) { 75 | const l = buf.length; 76 | if (l > 128) throw new Error(`slice too long, length ${l}, maximum 128`); 77 | this.write(Buffer.from([l])); 78 | this.write(buf); 79 | } 80 | 81 | writeInt(i: number) { 82 | if (i > MaximumEncodingInt) { 83 | throw new Error(`invalid integer ${i}, maximum ${MaximumEncodingInt}`); 84 | } 85 | const buf = Buffer.alloc(2); 86 | buf.writeUInt16BE(i); 87 | this.write(buf); 88 | } 89 | 90 | writeUint16(i: number) { 91 | const buf = Buffer.alloc(2); 92 | buf.writeUInt16BE(i); 93 | this.write(buf); 94 | } 95 | 96 | writeUint32(i: number) { 97 | const buf = Buffer.alloc(4); 98 | buf.writeUInt32BE(i); 99 | this.write(buf); 100 | } 101 | 102 | writeUint64(i: bigint) { 103 | const buf = Buffer.alloc(8); 104 | buf.writeBigUInt64BE(i); 105 | this.write(buf); 106 | } 107 | 108 | writeInteger(i: BigNumber) { 109 | const b = bigNumberToBytes(i); 110 | this.writeInt(b.byteLength); 111 | this.write(b); 112 | } 113 | 114 | // TODO convert array like to array 115 | writeUUID(id: string) { 116 | const uuid: any = parse(id); 117 | for (let i = 0; i < uuid.length; i += 1) { 118 | this.write(Buffer.from([uuid[i]])); 119 | } 120 | } 121 | 122 | encodeInput(input: Input) { 123 | const i = input; 124 | this.write(Buffer.from(i.hash, 'hex')); 125 | this.writeInt(i.index); 126 | 127 | if (!i.genesis) i.genesis = ''; 128 | this.writeInt(i.genesis.length); 129 | this.write(Buffer.from(i.genesis)); 130 | const d = i.deposit; 131 | if (typeof d === 'undefined') { 132 | this.write(empty); 133 | } else { 134 | // TODO... to test... 135 | this.write(magic); 136 | this.write(Buffer.from(d.chain, 'hex')); 137 | 138 | const asset = Buffer.from(d.asset); 139 | this.writeInt(asset.byteLength); 140 | this.write(asset); 141 | 142 | const tx = Buffer.from(d.transaction); 143 | this.writeInt(tx.byteLength); 144 | this.write(tx); 145 | 146 | this.writeUint64(d.index); 147 | this.writeInteger(parseUnits(d.amount, 8)); 148 | } 149 | const m = i.mint; 150 | if (typeof m === 'undefined') { 151 | this.write(empty); 152 | } else { 153 | this.write(magic); 154 | if (!m.group) m.group = ''; 155 | this.writeInt(m.group.length); 156 | this.write(Buffer.from(m.group)); 157 | 158 | this.writeUint64(m.batch); 159 | this.writeInteger(parseUnits(m.amount, 8)); 160 | } 161 | } 162 | 163 | encodeOutput(output: Output) { 164 | const o = output; 165 | if (!o.type) o.type = 0; 166 | this.write(Buffer.from([0x00, o.type])); 167 | this.writeInteger(parseUnits(o.amount, 8)); 168 | 169 | this.writeInt(o.keys.length); 170 | o.keys.forEach(k => this.write(Buffer.from(k, 'hex'))); 171 | 172 | this.write(o.mask ? Buffer.from(o.mask, 'hex') : Buffer.alloc(32, 0)); 173 | 174 | if (!o.script) o.script = ''; 175 | const s = Buffer.from(o.script, 'hex'); 176 | this.writeInt(s.byteLength); 177 | this.write(s); 178 | 179 | const w = o.withdrawal; 180 | if (!w) { 181 | this.write(empty); 182 | } else { 183 | this.write(magic); 184 | 185 | const addr = Buffer.from(w.address); 186 | this.writeInt(addr.byteLength); 187 | this.write(addr); 188 | 189 | const tag = Buffer.from(w.tag); 190 | this.writeInt(tag.byteLength); 191 | this.write(tag); 192 | } 193 | } 194 | 195 | encodeAggregatedSignature(js: Aggregated) { 196 | this.writeInt(MaximumEncodingInt); 197 | this.writeInt(AggregatedSignaturePrefix); 198 | this.write(Buffer.from(js.signature, 'hex')); 199 | 200 | if (js.signers.length === 0) { 201 | this.write(Buffer.from([0x00])); 202 | this.writeInt(0); 203 | return; 204 | } 205 | 206 | js.signers.forEach((m, i) => { 207 | if (i > 0 && m <= js.signers[i - 1]) { 208 | throw new Error('signers not sorted'); 209 | } 210 | if (m > MaximumEncodingInt) { 211 | throw new Error('signer overflow'); 212 | } 213 | }); 214 | 215 | const max = js.signers[js.signers.length - 1]; 216 | 217 | if (((((max / 8) | 0) + 1) | 0) > js.signature.length * 2) { 218 | // TODO... not check... 219 | this.write(Buffer.from([0x01])); 220 | this.writeInt(js.signature.length); 221 | js.signers.forEach(m => this.writeInt(m)); 222 | return; 223 | } 224 | 225 | const masks = Buffer.alloc((((max / 8) | 0) + 1) | 0); 226 | js.signers.forEach(m => { 227 | masks[(m / 8) | 0] ^= 1 << (m % 8 | 0); 228 | }); 229 | this.write(Buffer.from([0x00])); 230 | this.writeInt(masks.length); 231 | this.write(masks); 232 | } 233 | 234 | encodeSignature(sm: { [key: number]: string }) { 235 | const ss = Object.entries(sm) 236 | .map(([k, v]) => ({ index: k, sig: v })) 237 | .sort((a, b) => Number(a.index) - Number(b.index)); 238 | 239 | this.writeInt(ss.length); 240 | ss.forEach(s => { 241 | this.writeUint16(Number(s.index)); 242 | this.write(Buffer.from(s.sig, 'hex')); 243 | }); 244 | } 245 | } 246 | 247 | export default Encoder; 248 | -------------------------------------------------------------------------------- /src/client/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './address'; 2 | export * from './amount'; 3 | export * from './auth'; 4 | export * from './base64'; 5 | export * from './client'; 6 | export * from './computer'; 7 | export * from './decoder'; 8 | export * from './ed25519'; 9 | export * from './encoder'; 10 | export * from './invoice'; 11 | export * from './multisigs'; 12 | export * from './nfo'; 13 | export * from './pin'; 14 | export * from './safe'; 15 | export * from './sleep'; 16 | export * from './tip'; 17 | export * from './uniq'; 18 | -------------------------------------------------------------------------------- /src/client/utils/invoice.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import type { InvoiceEntry, MixinInvoice } from '../types'; 3 | import Encoder from './encoder'; 4 | import { getMixAddressBuffer, getMixAddressStringFromBuffer, parseMixAddress } from './address'; 5 | import { newHash } from './uniq'; 6 | import { base64RawURLDecode, base64RawURLEncode } from './base64'; 7 | import { Decoder } from './decoder'; 8 | import { ExtraSizeGeneralLimit, ExtraSizeStorageCapacity, estimateStorageCost, ReferencesCountLimit } from './safe'; 9 | import { XINAssetID } from '../../constant'; 10 | 11 | export const MixinInvoicePrefix = 'MIN'; 12 | export const MixinInvoiceVersion = 0; 13 | 14 | export const parseMixinInvoice = (s: string) => { 15 | try { 16 | if (!s.startsWith(MixinInvoicePrefix)) return undefined; 17 | 18 | const data = base64RawURLDecode(s.slice(3)); 19 | if (data.length < 3 + 23 + 1) return undefined; 20 | 21 | const payload = data.subarray(0, data.length - 4); 22 | const msg = Buffer.concat([Buffer.from(MixinInvoicePrefix), Buffer.from(payload)]); 23 | const checksum = newHash(msg); 24 | if (!checksum.subarray(0, 4).equals(Buffer.from(data.subarray(data.length - 4)))) return undefined; 25 | 26 | const dec = new Decoder(data); 27 | const version = dec.readByte(); 28 | if (version !== MixinInvoiceVersion) return undefined; 29 | const rl = dec.readInt(); 30 | const rb = dec.readSubarray(rl); 31 | const recipient = getMixAddressStringFromBuffer(rb); 32 | const mi = newMixinInvoice(recipient); 33 | if (!mi) return undefined; 34 | 35 | const el = dec.readByte(); 36 | for (let i = 0; i < el; i++) { 37 | const trace_id = dec.readUUID(); 38 | const asset_id = dec.readUUID(); 39 | const amount = dec.readBytesBuffer().toString(); 40 | const el = dec.readInt(); 41 | const extra = dec.readSubarray(el); 42 | const entry: InvoiceEntry = { 43 | trace_id, 44 | asset_id, 45 | amount, 46 | extra, 47 | index_references: [], 48 | hash_references: [], 49 | }; 50 | const rl = dec.readByte(); 51 | for (let j = 0; j < rl; j++) { 52 | const flag = dec.readByte(); 53 | if (flag === 1) { 54 | const ref = dec.readByte(); 55 | entry.index_references.push(ref); 56 | } else if (flag === 0) { 57 | const hash = dec.readSubarray(32).toString('hex'); 58 | entry.hash_references.push(hash); 59 | } else return undefined; 60 | } 61 | mi.entries.push(entry); 62 | } 63 | 64 | return mi; 65 | } catch { 66 | return undefined; 67 | } 68 | }; 69 | 70 | export const newMixinInvoice = (recipient: string) => { 71 | const r = parseMixAddress(recipient); 72 | if (!r) return r; 73 | return { 74 | version: MixinInvoiceVersion, 75 | recipient: r, 76 | entries: [], 77 | } as MixinInvoice; 78 | }; 79 | 80 | export const attachInvoiceEntry = (invoice: MixinInvoice, entry: InvoiceEntry) => { 81 | if (entry.extra.byteLength >= ExtraSizeGeneralLimit) { 82 | throw new Error('invalid extra length'); 83 | } 84 | if (entry.hash_references.length + entry.index_references.length > ReferencesCountLimit) { 85 | throw new Error('too many references'); 86 | } 87 | entry.index_references.forEach(ref => { 88 | if (ref > invoice.entries.length) { 89 | throw new Error(`invalid entry index reference: ${ref}`); 90 | } 91 | }); 92 | invoice.entries.push(entry); 93 | }; 94 | 95 | export const attachStorageEntry = (invoice: MixinInvoice, trace_id: string, extra: Buffer) => { 96 | const amount = estimateStorageCost(extra).toString(); 97 | const entry: InvoiceEntry = { 98 | trace_id, 99 | asset_id: XINAssetID, 100 | amount, 101 | extra, 102 | index_references: [], 103 | hash_references: [], 104 | }; 105 | invoice.entries.push(entry); 106 | }; 107 | 108 | export const isStorageEntry = (entry: InvoiceEntry) => { 109 | const expect = estimateStorageCost(entry.extra); 110 | const actual = BigNumber(entry.amount); 111 | return entry.asset_id === XINAssetID && entry.extra.byteLength > ExtraSizeGeneralLimit && expect.comparedTo(actual) === 0; 112 | }; 113 | 114 | export const getInvoiceBuffer = (invoice: MixinInvoice) => { 115 | const enc = new Encoder(Buffer.from([invoice.version])); 116 | 117 | const r = getMixAddressBuffer(invoice.recipient); 118 | if (r.byteLength > 1024) { 119 | throw new Error(`invalid recipient length: ${r.byteLength}`); 120 | } 121 | enc.writeUint16(r.byteLength); 122 | enc.write(r); 123 | 124 | if (invoice.entries.length > 128) { 125 | throw new Error(`invalid count of entries: ${r.byteLength}`); 126 | } 127 | enc.write(Buffer.from([invoice.entries.length])); 128 | 129 | invoice.entries.forEach(entry => { 130 | enc.writeUUID(entry.trace_id); 131 | enc.writeUUID(entry.asset_id); 132 | const amount = BigNumber(entry.amount).toFixed(8, BigNumber.ROUND_FLOOR); 133 | if (amount.length > 128) { 134 | throw new Error(`invalid amount of entry: ${amount}`); 135 | } 136 | enc.write(Buffer.from([amount.length])); 137 | enc.write(Buffer.from(amount)); 138 | if (entry.extra.length > ExtraSizeStorageCapacity) { 139 | throw new Error(`invalid extra of entry: ${entry.extra}`); 140 | } 141 | enc.writeUint16(entry.extra.length); 142 | enc.write(entry.extra); 143 | 144 | const rl = entry.index_references.length + entry.hash_references.length; 145 | if (rl > ReferencesCountLimit) { 146 | throw new Error(`invalid count of references: ${entry.index_references.length} ${entry.hash_references.length}`); 147 | } 148 | enc.write(Buffer.from([rl])); 149 | entry.index_references.forEach(ref => { 150 | enc.write(Buffer.from([1, ref])); 151 | }); 152 | entry.hash_references.forEach(ref => { 153 | enc.write(Buffer.concat([Buffer.from([0]), Buffer.from(ref, 'hex')])); 154 | }); 155 | }); 156 | return enc.buffer(); 157 | }; 158 | 159 | export const getInvoiceString = (invoice: MixinInvoice) => { 160 | const payload = getInvoiceBuffer(invoice); 161 | 162 | const msg = Buffer.concat([Buffer.from(MixinInvoicePrefix), payload]); 163 | const checksum = newHash(msg); 164 | const data = Buffer.concat([payload, checksum.subarray(0, 4)]); 165 | return `${MixinInvoicePrefix}${base64RawURLEncode(data)}`; 166 | }; 167 | -------------------------------------------------------------------------------- /src/client/utils/multisigs.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import type { MultisigTransaction, UtxoOutput } from '../types'; 3 | import { Encoder, magic } from './encoder'; 4 | 5 | export const TxVersion = 0x02; 6 | 7 | export const getTotalBalanceFromOutputs = (outputs: UtxoOutput[]) => outputs.reduce((prev, cur) => prev.plus(BigNumber(cur.amount)), BigNumber('0')); 8 | 9 | export const encodeScript = (threshold: number) => { 10 | let s = threshold.toString(16); 11 | if (s.length === 1) s = `0${s}`; 12 | if (s.length > 2) throw new Error(`INVALID THRESHOLD ${threshold}`); 13 | 14 | return `fffe${s}`; 15 | }; 16 | 17 | export const encodeTx = (tx: MultisigTransaction) => { 18 | const enc = new Encoder(Buffer.from([])); 19 | 20 | enc.write(magic); 21 | enc.write(Buffer.from([0x00, tx.version])); 22 | enc.write(Buffer.from(tx.asset, 'hex')); 23 | 24 | enc.writeInt(tx.inputs.length); 25 | tx.inputs.forEach(input => { 26 | enc.encodeInput(input); 27 | }); 28 | 29 | enc.writeInt(tx.outputs.length); 30 | tx.outputs.forEach(output => { 31 | enc.encodeOutput(output); 32 | }); 33 | 34 | const extra = Buffer.from(tx.extra); 35 | enc.writeInt(extra.byteLength); 36 | enc.write(extra); 37 | 38 | enc.writeInt(0); 39 | enc.write(Buffer.from([])); 40 | 41 | return enc.buf.toString('hex'); 42 | }; 43 | 44 | /** 45 | * Generate raw for multi-signature transaction. 46 | * The total amount of input utxos should be equal to the total amount of output utxos. 47 | * */ 48 | export const buildMultiSigsTransaction = (transaction: MultisigTransaction) => { 49 | if (transaction.version !== TxVersion) throw new Error('Invalid Version!'); 50 | 51 | const tx = { 52 | ...transaction, 53 | outputs: transaction.outputs.filter(output => !!output.mask), 54 | }; 55 | return encodeTx(tx); 56 | }; 57 | -------------------------------------------------------------------------------- /src/client/utils/nfo.ts: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | import { parse as UUIDParse, stringify } from 'uuid'; 3 | import type { CollectibleOutputsResponse, NFOMemo } from '../types'; 4 | import type { KeystoreClientReturnType } from '../mixin-client'; 5 | import { buildMultiSigsTransaction, encodeScript } from './multisigs'; 6 | import { Encoder, integerToBytes } from './encoder'; 7 | import { Decoder } from './decoder'; 8 | 9 | const Prefix = 'NFO'; 10 | const Version = 0x00; 11 | 12 | export const DefaultChain = '43d61dcd-e413-450d-80b8-101d5e903357'; 13 | export const DefaultClass = '3c8c161a18ae2c8b14fda1216fff7da88c419b5d'; 14 | export const DefaultNftAssetId = '1700941284a95f31b25ec8c546008f208f88eee4419ccdcdbe6e3195e60128ca'; 15 | 16 | export function buildTokenId(collection_id: string, token: number): string { 17 | const tokenStr = Buffer.from(integerToBytes(token)).toString('hex'); 18 | const msg = DefaultChain.replaceAll('-', '') + DefaultClass + collection_id.replaceAll('-', '') + tokenStr; 19 | const res = md5(Buffer.from(msg, 'hex')); 20 | const bytes = Buffer.from(res, 'hex'); 21 | bytes[6] = (bytes[6] & 0x0f) | 0x30; 22 | bytes[8] = (bytes[8] & 0x3f) | 0x80; 23 | return stringify(bytes); 24 | } 25 | 26 | /** 27 | * Content must be hex string without '0x' 28 | * */ 29 | export function buildCollectibleMemo(content: string, collection_id?: string, token_id?: number): string { 30 | const encoder = new Encoder(Buffer.from(Prefix, 'utf8')); 31 | encoder.write(Buffer.from([Version])); 32 | 33 | if (collection_id && token_id) { 34 | encoder.write(Buffer.from([1])); 35 | encoder.writeUint64(BigInt(1)); 36 | encoder.writeUUID(DefaultChain); 37 | 38 | encoder.writeSlice(Buffer.from(DefaultClass, 'hex')); 39 | encoder.writeSlice(Buffer.from(UUIDParse(collection_id) as Buffer)); 40 | encoder.writeSlice(Buffer.from(integerToBytes(token_id))); 41 | } else { 42 | encoder.write(Buffer.from([0])); 43 | } 44 | 45 | encoder.writeSlice(Buffer.from(content, 'hex')); 46 | return encoder.buf.toString('hex'); 47 | } 48 | 49 | export const decodeNfoMemo = (hexMemo: string) => { 50 | const memo = Buffer.from(hexMemo, 'hex'); 51 | if (memo.byteLength < 4) throw Error(`Invalid NFO memo length: ${memo.byteLength}`); 52 | const prefix = memo.subarray(0, 3).toString(); 53 | if (prefix !== Prefix) throw Error(`Invalid NFO memo prefix: ${prefix}`); 54 | const version = memo.readUint8(3); 55 | if (version !== Version) throw Error(`Invalid NFO memo version: ${version}`); 56 | 57 | const nm: NFOMemo = { 58 | prefix: Prefix, 59 | version: Version, 60 | extra: '', 61 | }; 62 | const decoder = new Decoder(memo.subarray(4)); 63 | const hint = decoder.readByte(); 64 | 65 | if (hint === 1) { 66 | nm.mask = Number(decoder.readUInt64()); 67 | if (nm.mask !== 1) throw Error(`Invalid NFO memo mask: ${nm.mask}`); 68 | 69 | nm.chain = decoder.readUUID(); 70 | if (nm.chain !== DefaultChain) throw Error(`Invalid NFO memo chain: ${nm.chain}`); 71 | 72 | nm.class = decoder.readBytes(); 73 | if (nm.class !== DefaultClass) throw Error(`Invalid NFO memo chain: ${nm.class}`); 74 | 75 | const collection = Buffer.from(decoder.readBytes(), 'hex'); 76 | nm.collection = stringify(collection); 77 | 78 | nm.token = parseInt(decoder.readBytes(), 16); 79 | } 80 | 81 | nm.extra = Buffer.from(decoder.readBytes(), 'hex').toString(); 82 | return nm; 83 | }; 84 | 85 | export const buildNfoTransferRequest = async (client: KeystoreClientReturnType, utxo: CollectibleOutputsResponse, receivers: string[], threshold: number, content = '') => { 86 | const keys = await client.transfer.outputs([ 87 | { 88 | receivers, 89 | index: 0, 90 | }, 91 | ]); 92 | 93 | const raw = buildMultiSigsTransaction({ 94 | version: 2, 95 | asset: DefaultNftAssetId, 96 | inputs: [ 97 | { 98 | hash: utxo.transaction_hash, 99 | index: utxo.output_index, 100 | }, 101 | ], 102 | outputs: [ 103 | { 104 | amount: '1', 105 | mask: keys[0].mask, 106 | keys: keys[0].keys, 107 | script: encodeScript(threshold), 108 | }, 109 | ], 110 | extra: buildCollectibleMemo(content), 111 | }); 112 | return client.collection.request('sign', raw); 113 | }; 114 | -------------------------------------------------------------------------------- /src/client/utils/pin.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { now as nanonow } from 'nano-seconds'; 3 | import { ed25519 } from '@noble/curves/ed25519'; 4 | import { cbc } from '@noble/ciphers/aes'; 5 | import { Uint64LE as Uint64 } from 'int64-buffer'; 6 | import type { Keystore, AppKeystore, NetworkUserKeystore } from '../types/keystore'; 7 | import { base64RawURLDecode, base64RawURLEncode } from './base64'; 8 | import { Encoder } from './encoder'; 9 | import { edwards25519 as ed, getRandomBytes } from './ed25519'; 10 | import { sha256Hash } from './uniq'; 11 | 12 | export const getNanoTime = () => { 13 | const now: number[] = nanonow(); 14 | return now[0] * 1e9 + now[1]; 15 | }; 16 | 17 | export const sharedEd25519Key = (keystore: AppKeystore | NetworkUserKeystore) => { 18 | const pub = 'server_public_key' in keystore ? ed.edwardsToMontgomery(Buffer.from(keystore.server_public_key, 'hex')) : base64RawURLDecode(keystore.pin_token_base64); 19 | const pri = ed.edwardsToMontgomeryPriv(Buffer.from(keystore.session_private_key, 'hex')); 20 | return ed.x25519.getSharedSecret(pri, pub); 21 | }; 22 | 23 | export const getTipPinUpdateMsg = (pub: Buffer, counter: number) => { 24 | const enc = new Encoder(pub); 25 | enc.writeUint64(BigInt(counter)); 26 | return enc.buf; 27 | }; 28 | 29 | export const signEd25519PIN = (pin: string, keystore: Keystore | undefined): string => { 30 | if (!keystore || !keystore.session_private_key) return ''; 31 | if (!('server_public_key' in keystore) && !('pin_token_base64' in keystore)) return ''; 32 | const blockSize = 16; 33 | 34 | const _pin = Buffer.from(pin, 'hex'); 35 | const iterator = Buffer.from(new Uint64(getNanoTime()).toBuffer()); 36 | const time = Buffer.from(new Uint64(Date.now() / 1000).toBuffer()); 37 | let buffer = Buffer.concat([_pin, time, iterator]); 38 | 39 | const paddingLen = blockSize - (buffer.byteLength % blockSize); 40 | const paddings = []; 41 | for (let i = 0; i < paddingLen; i += 1) { 42 | paddings.push(paddingLen); 43 | } 44 | buffer = Buffer.concat([buffer, Buffer.from(paddings)]); 45 | 46 | const iv = getRandomBytes(16); 47 | const sharedKey = sharedEd25519Key(keystore); 48 | 49 | const stream = cbc(sharedKey, iv); 50 | const res = stream.encrypt(buffer); 51 | 52 | const pinBuff = Buffer.concat([iv, res]); 53 | const encryptedBytes = pinBuff.subarray(0, pinBuff.byteLength - blockSize); 54 | return base64RawURLEncode(encryptedBytes); 55 | }; 56 | 57 | export const getCreateAddressTipBody = (asset_id: string, publicKey: string, tag: string, name: string) => { 58 | const msg = `TIP:ADDRESS:ADD:${asset_id + publicKey + tag + name}`; 59 | return sha256Hash(Buffer.from(msg)); 60 | }; 61 | 62 | export const getRemoveAddressTipBody = (address_id: string) => { 63 | const msg = `TIP:ADDRESS:REMOVE:${address_id}`; 64 | return sha256Hash(Buffer.from(msg)); 65 | }; 66 | 67 | export const getVerifyPinTipBody = (timestamp: number) => { 68 | const msg = `TIP:VERIFY:${`${timestamp}`.padStart(32, '0')}`; 69 | return Buffer.from(msg); 70 | }; 71 | 72 | export const signTipBody = (pin: string, msg: Buffer) => { 73 | const signData = Buffer.from(ed25519.sign(msg, pin)); 74 | return signData.toString('hex'); 75 | }; 76 | -------------------------------------------------------------------------------- /src/client/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (n = 500) => 2 | new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(); 5 | }, n); 6 | }); 7 | -------------------------------------------------------------------------------- /src/client/utils/tip.ts: -------------------------------------------------------------------------------- 1 | import { sha256Hash } from './uniq'; 2 | 3 | const TIPSequencerRegister = 'SEQUENCER:REGISTER:'; 4 | 5 | export const TIPBodyForSequencerRegister = (user_id: string, pubKey: string) => tipBody(`${TIPSequencerRegister}${user_id}${pubKey}`); 6 | 7 | export const tipBody = (s: string) => sha256Hash(Buffer.from(s)); 8 | -------------------------------------------------------------------------------- /src/client/utils/uniq.ts: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | import { blake3 } from '@noble/hashes/blake3'; 3 | import { sha3_256 } from '@noble/hashes/sha3'; 4 | import { sha256 } from '@noble/hashes/sha256'; 5 | import { sha512 } from '@noble/hashes/sha512'; 6 | import { stringify as uuidStringify, v4 as uuid } from 'uuid'; 7 | 8 | /** Supporting multisig for tokens & collectibles */ 9 | export const hashMembers = (ids: string[]): string => { 10 | const key = ids.sort().join(''); 11 | return newHash(Buffer.from(key)).toString('hex'); 12 | }; 13 | 14 | /** Generate an unique conversation id for contact */ 15 | export const uniqueConversationID = (userID: string, recipientID: string): string => { 16 | const [minId, maxId] = [userID, recipientID].sort(); 17 | const res = md5(minId + maxId); 18 | const bytes = Buffer.from(res, 'hex'); 19 | 20 | bytes[6] = (bytes[6] & 0x0f) | 0x30; 21 | bytes[8] = (bytes[8] & 0x3f) | 0x80; 22 | 23 | return uuidStringify(bytes); 24 | }; 25 | export const newHash = (data: Buffer) => Buffer.from(sha3_256.create().update(data).digest()); 26 | 27 | export const sha256Hash = (data: Buffer) => Buffer.from(sha256.create().update(data).digest()); 28 | 29 | export const sha512Hash = (data: Buffer) => Buffer.from(sha512.create().update(data).digest()); 30 | 31 | export const blake3Hash = (data: Buffer) => Buffer.from(blake3.create({}).update(data).digest()); 32 | 33 | export const getUuid = () => uuid(); 34 | -------------------------------------------------------------------------------- /src/client/utxo.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'axios'; 2 | import type { 3 | GhostKey, 4 | GhostKeyRequest, 5 | OutputFetchRequest, 6 | OutputsRequest, 7 | SafeOutputsRequest, 8 | SafeBalanceRequest, 9 | SafeUtxoOutput, 10 | TransactionRequest, 11 | SequencerTransactionRequest, 12 | UtxoOutput, 13 | SafeTransactionRecipient, 14 | } from './types'; 15 | import { blake3Hash, buildClient, deriveGhostPublicKey, getPublicFromMainnetAddress, getTotalBalanceFromOutputs, hashMembers, integerToBytes, uniqueConversationID } from './utils'; 16 | import { edwards25519 as ed, newKeyFromSeed } from './utils/ed25519'; 17 | 18 | export const UtxoKeystoreClient = (axiosInstance: AxiosInstance) => ({ 19 | outputs: (params: OutputsRequest): Promise => 20 | axiosInstance.get(`/outputs`, { 21 | params: { 22 | ...params, 23 | members: hashMembers(params.members), 24 | }, 25 | }), 26 | 27 | /** 28 | * Utxos of current user will be returned, if members and threshold are not provided. 29 | */ 30 | safeOutputs: (params: SafeOutputsRequest): Promise => 31 | axiosInstance.get(`/safe/outputs`, { 32 | params: { 33 | ...params, 34 | members: params.members ? hashMembers(params.members) : undefined, 35 | }, 36 | }), 37 | 38 | safeAssetBalance: async (params: SafeBalanceRequest) => { 39 | const outputs = await axiosInstance.get(`/safe/outputs`, { 40 | params: { 41 | ...params, 42 | members: params.members ? hashMembers(params.members) : undefined, 43 | state: 'unspent', 44 | }, 45 | }); 46 | return getTotalBalanceFromOutputs(outputs).toString(); 47 | }, 48 | 49 | fetchSafeOutputs: (params: OutputFetchRequest): Promise => axiosInstance.post('/safe/outputs/fetch', params), 50 | 51 | fetchTransaction: (transactionId: string): Promise => axiosInstance.get(`/safe/transactions/${transactionId}`), 52 | 53 | verifyTransaction: (params: TransactionRequest[]): Promise => 54 | axiosInstance.post('/safe/transaction/requests', params), 55 | 56 | sendTransactions: (params: TransactionRequest[]): Promise => 57 | axiosInstance.post('/safe/transactions', params), 58 | 59 | /** 60 | * Get one-time information to transfer assets to single user or multisigs group, not required for Mixin Kernel Address 61 | * index in GhostKeyRequest MUST be the same with the index of corresponding output 62 | * receivers will be sorted in the function 63 | */ 64 | ghostKey: async (recipients: SafeTransactionRecipient[], trace: string, spendPrivateKey: string): Promise<(GhostKey | undefined)[]> => { 65 | const traceHash = blake3Hash(Buffer.from(trace)); 66 | const privSpend = Buffer.from(spendPrivateKey, 'hex'); 67 | const ghostKeys: (GhostKey | undefined)[] = new Array(recipients.length).fill(undefined); 68 | const uuidRequests: GhostKeyRequest[] = []; 69 | 70 | recipients.forEach((r, i) => { 71 | if ('destination' in r) return; 72 | 73 | const ma = r.mixAddress; 74 | const seedHash = blake3Hash(Buffer.concat([traceHash, Buffer.from(integerToBytes(i))])); 75 | if (ma.xinMembers.length) { 76 | const privHash = blake3Hash(Buffer.concat([seedHash, privSpend])); 77 | const key = newKeyFromSeed(Buffer.concat([traceHash, privHash])); 78 | const mask = ed.publicFromPrivate(key).toString('hex'); 79 | const keys = ma.xinMembers.map(member => { 80 | const pub = getPublicFromMainnetAddress(member); 81 | const spendKey = pub!.subarray(0, 32); 82 | const viewKey = pub!.subarray(32, 64); 83 | const k = deriveGhostPublicKey(key, viewKey, spendKey, i); 84 | return k.toString('hex'); 85 | }); 86 | ghostKeys[i] = { 87 | mask, 88 | keys, 89 | }; 90 | } else { 91 | const hint = uniqueConversationID(traceHash.toString('hex'), seedHash.toString('hex')); 92 | uuidRequests.push({ 93 | receivers: ma.uuidMembers.sort(), 94 | index: i, 95 | hint, 96 | }); 97 | } 98 | }); 99 | if (uuidRequests.length) { 100 | const ghosts = await axiosInstance.post('/safe/keys', uuidRequests); 101 | ghosts.forEach((ghost, i) => { 102 | const { index } = uuidRequests[i]; 103 | ghostKeys[index] = ghost; 104 | }); 105 | } 106 | return ghostKeys; 107 | }, 108 | }); 109 | 110 | export const UtxoClient = buildClient(UtxoKeystoreClient); 111 | 112 | export default UtxoClient; 113 | -------------------------------------------------------------------------------- /src/constant.ts: -------------------------------------------------------------------------------- 1 | export const MixinAssetID = '43d61dcd-e413-450d-80b8-101d5e903357'; 2 | export const MixinCashier = '674d6776-d600-4346-af46-58e77d8df185'; 3 | export const XINAssetID = 'c94ac88f-4671-3976-b60a-09064f1811e8'; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './blaze'; 3 | export * from './webview'; 4 | export * from './constant'; 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type AtLeastOne }> = Partial & U[keyof U]; 2 | -------------------------------------------------------------------------------- /src/webview/client.ts: -------------------------------------------------------------------------------- 1 | import type { Context, WebviewAsset } from './type'; 2 | 3 | export const WebViewApi = () => { 4 | const getMixinContext = () => { 5 | let ctx: Context = {}; 6 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.MixinContext) { 7 | const contextString = prompt('MixinContext.getContext()'); // eslint-disable-line no-alert 8 | if (contextString) { 9 | ctx = JSON.parse(contextString); 10 | ctx.platform = ctx.platform || 'iOS'; 11 | } 12 | } else if (window.MixinContext && typeof window.MixinContext.getContext === 'function') { 13 | ctx = JSON.parse(window.MixinContext.getContext()); 14 | ctx.platform = ctx.platform || 'Android'; 15 | } 16 | 17 | return ctx; 18 | }; 19 | 20 | return { 21 | getMixinContext, 22 | 23 | reloadTheme: () => { 24 | switch (getMixinContext().platform) { 25 | case 'iOS': 26 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.reloadTheme) window.webkit.messageHandlers.reloadTheme.postMessage(''); 27 | break; 28 | case 'Android': 29 | case 'Desktop': 30 | if (window.MixinContext && typeof window.MixinContext.reloadTheme === 'function') window.MixinContext.reloadTheme(); 31 | break; 32 | default: 33 | break; 34 | } 35 | }, 36 | 37 | playlist: (audios: string[]) => { 38 | switch (getMixinContext().platform) { 39 | case 'iOS': 40 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.playlist) window.webkit.messageHandlers.playlist.postMessage(audios); 41 | break; 42 | case 'Android': 43 | case 'Desktop': 44 | if (window.MixinContext && typeof window.MixinContext.playlist === 'function') window.MixinContext.playlist(audios); 45 | break; 46 | default: 47 | break; 48 | } 49 | }, 50 | 51 | close: () => { 52 | switch (getMixinContext().platform) { 53 | case 'iOS': 54 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.close) window.webkit.messageHandlers.close.postMessage(''); 55 | break; 56 | case 'Android': 57 | case 'Desktop': 58 | if (window.MixinContext && typeof window.MixinContext.close === 'function') window.MixinContext.close(); 59 | break; 60 | default: 61 | break; 62 | } 63 | }, 64 | 65 | getAssets: async (assets: string[], cb: (assets: WebviewAsset[]) => void) => { 66 | switch (getMixinContext().platform) { 67 | case 'iOS': 68 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getAssets) { 69 | window.assetsCallbackFunction = cb; 70 | await window.webkit.messageHandlers.getAssets.postMessage([assets, 'assetsCallbackFunction']); 71 | } 72 | break; 73 | case 'Android': 74 | case 'Desktop': 75 | if (window.MixinContext && typeof window.MixinContext.getAssets === 'function') { 76 | window.assetsCallbackFunction = cb; 77 | await window.MixinContext.getAssets(assets, 'assetsCallbackFunction'); 78 | } 79 | break; 80 | default: 81 | break; 82 | } 83 | }, 84 | 85 | getTipAddress: async (chainId: string, cb: (address: string) => void) => { 86 | switch (getMixinContext().platform) { 87 | case 'iOS': 88 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getTipAddress) { 89 | window.tipAddressCallbackFunction = cb; 90 | await window.webkit.messageHandlers.getTipAddress.postMessage([chainId, 'tipAddressCallbackFunction']); 91 | } 92 | break; 93 | case 'Android': 94 | case 'Desktop': 95 | if (window.MixinContext && typeof window.MixinContext.getTipAddress === 'function') { 96 | window.tipAddressCallbackFunction = cb; 97 | await window.MixinContext.getTipAddress(chainId, 'tipAddressCallbackFunction'); 98 | } 99 | break; 100 | default: 101 | break; 102 | } 103 | }, 104 | 105 | tipSign: async (chainId: string, msg: string, cb: (signature: string) => void) => { 106 | switch (getMixinContext().platform) { 107 | case 'iOS': 108 | if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.tipSign) { 109 | window.tipSignCallbackFunction = cb; 110 | await window.webkit.messageHandlers.tipSign.postMessage([chainId, msg, 'tipSignCallbackFunction']); 111 | } 112 | break; 113 | case 'Android': 114 | case 'Desktop': 115 | if (window.MixinContext && typeof window.MixinContext.tipSign === 'function') { 116 | window.tipSignCallbackFunction = cb; 117 | await window.MixinContext.tipSign(chainId, msg, 'tipSignCallbackFunction'); 118 | } 119 | break; 120 | default: 121 | break; 122 | } 123 | }, 124 | }; 125 | }; 126 | -------------------------------------------------------------------------------- /src/webview/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/webview/type.ts: -------------------------------------------------------------------------------- 1 | export interface Context { 2 | app_version?: string; 3 | immersive?: boolean; 4 | appearance?: 'light' | 'dark'; 5 | currency?: string; 6 | locale?: string; 7 | platform?: 'iOS' | 'Android' | 'Desktop'; 8 | conversation_id?: string; 9 | } 10 | 11 | export interface WebviewAsset { 12 | asset_id: string; 13 | balance: string; 14 | chain_id: string; 15 | icon_url: string; 16 | name: string; 17 | symbol: string; 18 | } 19 | 20 | export interface Messengers { 21 | getContext: (args?: string) => string; 22 | playlist: (audio: string[]) => any; 23 | reloadTheme: (args?: string) => void; 24 | close: (args?: string) => void; 25 | getAssets: (assets: string[], globalCallBackFuncName: string) => void; 26 | getTipAddress: (chainId: string, globalCallBackFuncName: string) => void; 27 | tipSign: (chainId: string, message: string, globalCallBackFuncName: string) => void; 28 | } 29 | 30 | declare global { 31 | interface Window { 32 | webkit?: { 33 | messageHandlers?: { 34 | MixinContext?: { postMessage: Messengers['getContext'] }; 35 | playlist?: { postMessage: Messengers['playlist'] }; 36 | reloadTheme?: { postMessage: Messengers['reloadTheme'] }; 37 | close?: { postMessage: Messengers['close'] }; 38 | getAssets?: { postMessage: ([params, globalCallBackFuncName]: [string[], string]) => void }; 39 | getTipAddress?: { postMessage: ([chainId, globalCallBackFuncName]: [string, string]) => void }; 40 | tipSign?: { postMessage: ([chainId, message, globalCallBackFuncName]: [string, string, string]) => void }; 41 | }; 42 | }; 43 | MixinContext?: { 44 | getContext?: Messengers['getContext']; 45 | reloadTheme?: Messengers['reloadTheme']; 46 | playlist?: Messengers['playlist']; 47 | close?: Messengers['close']; 48 | getAssets?: Messengers['getAssets']; 49 | getTipAddress?: Messengers['getTipAddress']; 50 | tipSign?: Messengers['tipSign']; 51 | }; 52 | assetsCallbackFunction?: (res: WebviewAsset[]) => void; 53 | tipAddressCallbackFunction?: (address: string) => void; 54 | tipSignCallbackFunction?: (signature: string) => void; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/address.test.ts: -------------------------------------------------------------------------------- 1 | import { client, app_pin } from './common'; 2 | 3 | describe('address', () => { 4 | const asset_id = '43d61dcd-e413-450d-80b8-101d5e903357'; 5 | const destination = '0xF2e6D6BB9E6D31B873bC23649A25A76f8852e3f5'; 6 | let tmpAddressID = ''; 7 | 8 | it('create address if not exsits', async () => { 9 | const receive = await client.address.create(app_pin, { asset_id, destination, label: 'test' }); 10 | console.log(receive); 11 | const res = await client.address.fetch(receive.address_id); 12 | expect(res.address_id).toEqual(receive.address_id); 13 | tmpAddressID = receive.address_id; 14 | }); 15 | it('read addresses', async () => { 16 | const list = await client.address.fetchListOfChain(asset_id); 17 | const isHave = list.some(item => item.address_id === tmpAddressID); 18 | expect(isHave).toBeTruthy(); 19 | }); 20 | it('delete address', async () => { 21 | const res = await client.address.fetch(tmpAddressID); 22 | expect(res.address_id).toEqual(tmpAddressID); 23 | const t = await client.address.delete(app_pin, tmpAddressID); 24 | expect(t).toBeUndefined(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/common.ts: -------------------------------------------------------------------------------- 1 | import { MixinApi } from '../src'; 2 | import keystore from './keystore'; 3 | 4 | const config = { 5 | requestConfig: { 6 | responseCallback: (err: any) => { 7 | console.log(err); 8 | }, 9 | }, 10 | keystore, 11 | }; 12 | const client = MixinApi(config); 13 | 14 | const app_pin = '9b1b3b1006de4881a6f3c1a8da462444dc3950c76b42d3579a88d4d68bae74be'; 15 | 16 | export { client, keystore, app_pin }; 17 | -------------------------------------------------------------------------------- /test/crypto.ts: -------------------------------------------------------------------------------- 1 | const nodeCrypto = require('crypto'); 2 | 3 | // @ts-ignore 4 | window.crypto = { 5 | getRandomValues: function (buffer) { 6 | return nodeCrypto.randomFillSync(buffer); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/forge.test.ts: -------------------------------------------------------------------------------- 1 | import { pki, util, cipher, md } from 'node-forge'; 2 | import { ed25519 } from '@noble/curves/ed25519'; 3 | import { cbc } from '@noble/ciphers/aes'; 4 | import { Uint64LE as Uint64 } from 'int64-buffer'; 5 | import { v4, stringify } from 'uuid'; 6 | import { getKeyPair, getRandomBytes, sharedEd25519Key, getNanoTime, sha256Hash, uniqueConversationID } from '../src'; 7 | import { app_pin } from './common'; 8 | import keystore from './keystore'; 9 | 10 | const getED25519KeyPair = (seed: Buffer) => { 11 | const keypair = pki.ed25519.generateKeyPair({ seed }); 12 | return { 13 | privateKey: Buffer.from(keypair.privateKey), 14 | publicKey: Buffer.from(keypair.publicKey), 15 | seed, 16 | }; 17 | }; 18 | 19 | const forgeUniqueConversationID = (userID: string, recipientID: string): string => { 20 | const [minId, maxId] = [userID, recipientID].sort(); 21 | const md5 = md.md5.create(); 22 | md5.update(minId); 23 | md5.update(maxId); 24 | const bytes = Buffer.from(md5.digest().bytes(), 'binary'); 25 | 26 | bytes[6] = (bytes[6] & 0x0f) | 0x30; 27 | bytes[8] = (bytes[8] & 0x3f) | 0x80; 28 | 29 | return stringify(bytes); 30 | }; 31 | 32 | describe('forge', () => { 33 | it('sign', async () => { 34 | const nobleKeyPar = getKeyPair(); 35 | const forgeKeyPar = getED25519KeyPair(nobleKeyPar.seed); 36 | expect(nobleKeyPar.privateKey.toString('hex')).toEqual(forgeKeyPar.privateKey.toString('hex')); 37 | expect(nobleKeyPar.publicKey.toString('hex')).toEqual(forgeKeyPar.publicKey.toString('hex')); 38 | expect(nobleKeyPar.seed.toString('hex')).toEqual(forgeKeyPar.seed.toString('hex')); 39 | 40 | const content = getRandomBytes(32); 41 | expect(content.byteLength).toEqual(32); 42 | const sigForge = pki.ed25519.sign({ 43 | message: content.toString('base64'), 44 | encoding: 'utf8', 45 | privateKey: forgeKeyPar.privateKey, 46 | }); 47 | const sig = Buffer.from(ed25519.sign(Buffer.from(content.toString('base64')), nobleKeyPar.seed)); 48 | expect(sigForge.toString('hex')).toEqual(sig.toString('hex')); 49 | }); 50 | 51 | it('cipher', async () => { 52 | const _pin = Buffer.from(app_pin, 'hex'); 53 | const iterator = Buffer.from(new Uint64(getNanoTime()).toBuffer()); 54 | const time = Buffer.from(new Uint64(Date.now() / 1000).toBuffer()); 55 | let buffer = Buffer.concat([_pin, time, iterator]); 56 | 57 | const iv = getRandomBytes(16); 58 | const sharedKey = sharedEd25519Key(keystore); 59 | 60 | const cp = cipher.createCipher('AES-CBC', util.createBuffer(sharedKey, 'raw')); 61 | cp.start({ iv: iv.toString('binary') }); 62 | cp.update(util.createBuffer(buffer)); 63 | cp.finish(); 64 | const resForge = cp.output.getBytes(); 65 | 66 | const stream = cbc(sharedKey, iv); 67 | const resNoble = Buffer.from(stream.encrypt(buffer)); 68 | expect(resNoble.toString('binary')).toEqual(resForge); 69 | }); 70 | 71 | it('md5', async () => { 72 | const id1 = v4(); 73 | const id2 = v4(); 74 | 75 | const resForge = forgeUniqueConversationID(id1, id2); 76 | const res = uniqueConversationID(id1, id2); 77 | expect(res).toEqual(resForge); 78 | }); 79 | 80 | it('sha256', async () => { 81 | const id = v4(); 82 | 83 | const sha256 = md.sha256.create(); 84 | sha256.update(id, 'utf8'); 85 | const resForge = sha256.digest().toHex(); 86 | 87 | const res = sha256Hash(Buffer.from(id)).toString('hex'); 88 | expect(res).toEqual(resForge); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/keystore.ts: -------------------------------------------------------------------------------- 1 | const keystore = { 2 | app_id: '4b79fe76-0d9d-49e6-85fd-0f6be01147da', 3 | session_id: 'a03a4496-3e59-4abd-bd1e-1ab8a2acb550', 4 | server_public_key: '413506980af07695d590bae4b58bafac9f381ab8c6ef02bef85fab3bee21de26', 5 | session_private_key: 'c838d99617cb2543bc6b7eb4ac0a1daede5d132ed1c5442fbbcad8baaf556748', 6 | }; 7 | 8 | export default keystore; 9 | -------------------------------------------------------------------------------- /test/mixin/code.test.ts: -------------------------------------------------------------------------------- 1 | import Code from '../../src/client/code'; 2 | 3 | describe('Tests for codes', () => { 4 | const code = Code(); 5 | test('Test for fetch conversation', async () => { 6 | const resp = await code.fetch('99f90817-9b2a-4d39-ba53-ab2cf0aa2440'); 7 | expect(resp.type).toMatch('conversation'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/mixin/error.test.ts: -------------------------------------------------------------------------------- 1 | import { ResponseError } from '../../src/client/error'; 2 | 3 | describe('Tests for ResponseError', () => { 4 | test('Test response error', () => { 5 | const id = '0f321425-ab1d-4a41-a177-efaa08160df5'; 6 | const err = new ResponseError(100, 'description', 201, { reason: 'invalid' }, id, ''); 7 | expect(err.code).toEqual(100); 8 | expect(err.requestId).toMatch(id); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/mixin/network.test.ts: -------------------------------------------------------------------------------- 1 | import Network from '../../src/client/network'; 2 | 3 | describe('Tests for network', () => { 4 | const network = Network(); 5 | test('Test for fetch network chains', async () => { 6 | const resp = await network.chains(); 7 | console.log(resp.length); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/mixin/nfo.test.ts: -------------------------------------------------------------------------------- 1 | import { buildCollectibleMemo, buildTokenId, decodeNfoMemo } from '../../src'; 2 | 3 | describe('Tests for nfo', () => { 4 | test('Test for fetch conversation', async () => { 5 | const memo = 6 | '4e464f0001000000000000000143d61dcde413450d80b8101d5e903357143c8c161a18ae2c8b14fda1216fff7da88c419b5d103676a640111b42e4923efc4c68d6de400106204d27df6617015c7da6f606106a7f751bc1175b3fcee7ba3eea2e9fec693cff77'; 7 | const nfo = decodeNfoMemo(memo); 8 | 9 | if (nfo.chain && nfo.class && nfo.collection && nfo.token) { 10 | const tokenId = buildTokenId(nfo.collection, nfo.token); 11 | expect(tokenId).toEqual('8048de2d-8092-3ccc-a47d-e30da9764f05'); 12 | 13 | const res = buildCollectibleMemo(Buffer.from('').toString('hex'), nfo.collection, nfo.token); 14 | // Compare without content 15 | expect(res.slice(0, res.length - 2)).toEqual(memo.slice(0, memo.length - 66)); 16 | } 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/mixin/user.test.ts: -------------------------------------------------------------------------------- 1 | import User from '../../src/client/user'; 2 | import Pin from '../../src/client/pin'; 3 | import keystore from '../keystore'; 4 | import { app_pin } from '../common'; 5 | 6 | describe('Tests for users', () => { 7 | const user = User({ keystore }); 8 | test('Test for read user`s profile', async () => { 9 | const resp = await user.profile(); 10 | expect(resp.user_id).toMatch(keystore.app_id); 11 | expect(resp.has_pin).toBe(true); 12 | }); 13 | 14 | test('fetch user by id', async () => { 15 | const resp = await user.fetch(keystore.app_id); 16 | expect(resp.user_id).toMatch(keystore.app_id); 17 | expect(resp.is_verified).toBe(false); 18 | }); 19 | 20 | test('verify user pin', async () => { 21 | const pin = Pin({ keystore }); 22 | const resp = await pin.verifyTipPin(app_pin); 23 | expect(resp.user_id).toMatch(keystore.app_id); 24 | expect(resp.is_verified).toBe(false); 25 | }); 26 | 27 | test('user update me', async () => { 28 | const resp = await user.update('js sdk', ''); 29 | expect(resp.full_name).toMatch('js sdk'); 30 | }); 31 | 32 | test('test for user rotate code', async () => { 33 | const resp = await user.rotateCode(); 34 | expect(resp.user_id).toMatch(keystore.app_id); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/nft.test.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | import { client } from './common'; 3 | import { MintMinimumCost, GroupMembers, GroupThreshold } from '../src/client/collectible'; 4 | import { buildCollectibleMemo, MixinAssetID } from '../src'; 5 | 6 | describe('address', () => { 7 | it('create nft', async () => { 8 | const id = uuid(); 9 | const tr = { 10 | asset_id: MixinAssetID, 11 | amount: MintMinimumCost, 12 | trace_id: id, 13 | memo: buildCollectibleMemo(Buffer.from('test').toString('hex'), id, 1), 14 | opponent_multisig: { 15 | receivers: GroupMembers, 16 | threshold: GroupThreshold, 17 | }, 18 | }; 19 | 20 | const payment = await client.payment.request(tr); 21 | console.log('mint collectibles', id, `mixin://codes/${payment.code_id}`); 22 | expect(payment.trace_id).toEqual(id); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/user.test.ts: -------------------------------------------------------------------------------- 1 | import { client, keystore } from './common'; 2 | 3 | describe('user', () => { 4 | it('profile', async () => { 5 | const user = await client.user.profile(); 6 | expect(user.user_id).toEqual(keystore.app_id); 7 | }); 8 | 9 | it('fetch', async () => { 10 | const user = await client.user.fetch(keystore.app_id); 11 | expect(user.user_id).toEqual(keystore.app_id); 12 | }); 13 | 14 | it('readBlockUsers', async () => { 15 | const user = await client.user.blockings(); 16 | expect(Array.isArray(user)).toBeTruthy(); 17 | }); 18 | 19 | it('fetchList', async () => { 20 | const users = await client.user.fetchList(['e8e8cd79-cd40-4796-8c54-3a13cfe50115']); 21 | expect(users[0].identity_number).toEqual('30265'); 22 | }); 23 | 24 | it('readFriends', async () => { 25 | const user = await client.user.friends(); 26 | expect(Array.isArray(user)).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "jsx": "react", 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "noEmit": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | const replace = require('@rollup/plugin-replace'); 3 | 4 | // Not transpiled with TypeScript or Babel, so use plain Es6/Node.js! 5 | module.exports = { 6 | // This function will run for each entry/format/env combination 7 | rollup(config, opts) { 8 | config.cache = false; 9 | 10 | config.plugins = config.plugins.map(p => 11 | p.name === 'replace' 12 | ? replace({ 13 | 'process.env.NODE_ENV': JSON.stringify(opts.env), 14 | preventAssignment: true, 15 | }) 16 | : p, 17 | ); 18 | return config; // always return a config. 19 | }, 20 | }; 21 | --------------------------------------------------------------------------------