├── .prettierignore ├── .npmignore ├── .gitignore ├── .eslintignore ├── config.default.json ├── .prettierrc.json ├── test ├── common.ts ├── nft.test.ts ├── address.test.ts └── user.test.ts ├── src ├── types │ ├── pin.ts │ ├── client.ts │ ├── attachment.ts │ ├── transaction.ts │ ├── index.ts │ ├── mvm.ts │ ├── oauth.ts │ ├── address.ts │ ├── app.ts │ ├── blaze.ts │ ├── snapshot.ts │ ├── asset.ts │ ├── network_transaction.ts │ ├── network.ts │ ├── transfer.ts │ ├── conversation.ts │ ├── multisigs.ts │ ├── user.ts │ ├── collectibles.ts │ └── message.ts ├── index.ts ├── client │ ├── app.ts │ ├── pin.ts │ ├── asset.ts │ ├── snapshot.ts │ ├── address.ts │ ├── attachment.ts │ ├── transfer.ts │ ├── network.ts │ ├── user.ts │ ├── oauth.ts │ ├── conversation.ts │ ├── multisigs.ts │ ├── collectibles.ts │ ├── blaze.ts │ ├── mvm.ts │ ├── message.ts │ └── index.ts ├── mixin │ ├── keystore.ts │ ├── nfo.ts │ ├── tools.ts │ ├── dump_transaction.ts │ ├── dump_msg.ts │ ├── encoder.ts │ ├── mvm.ts │ └── sign.ts └── services │ └── request.ts ├── .eslintrc ├── tsdx.config.js ├── example ├── mvm_api.js ├── mvm.js ├── nft.js └── multisigns.js ├── tsconfig.json ├── README.zh-CN.md ├── package.json ├── README.md └── LICENSE /.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | config.json 2 | test/index.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | .idea 4 | node_modules 5 | dist 6 | 7 | config.json 8 | test/index.js 9 | .sessions -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /config.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id": "", 3 | "session_id": "", 4 | "pin_token": "", 5 | "private_key": "", 6 | "pin": "", 7 | "client_secret": "" 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 180, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all" 8 | } -------------------------------------------------------------------------------- /test/common.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '../src'; 2 | import fs from 'fs'; 3 | const keystore = JSON.parse(fs.readFileSync(__dirname + '/../config.json', 'utf8')); 4 | const client = new Client(keystore); 5 | 6 | export { client }; 7 | -------------------------------------------------------------------------------- /src/types/pin.ts: -------------------------------------------------------------------------------- 1 | export interface Turn { 2 | url: string; 3 | username: string; 4 | credential: string; 5 | } 6 | 7 | export interface PINClientRequest { 8 | verifyPin: (pin: string) => Promise; 9 | modifyPin: (pin: string, newPin: string) => Promise; 10 | readTurnServers: () => Promise; 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/types/client.ts: -------------------------------------------------------------------------------- 1 | export interface Keystore { 2 | client_id: string; 3 | client_secret: string; 4 | session_id: string; 5 | private_key: string; 6 | pin_token: string; 7 | scope?: string; 8 | pin: string; 9 | } 10 | 11 | export interface ErrorResponse { 12 | status: number; 13 | code: number; 14 | description: string; 15 | extra?: object; 16 | request_id?: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/types/attachment.ts: -------------------------------------------------------------------------------- 1 | export interface Attachment { 2 | attachment_id: string; 3 | upload_url?: string; 4 | view_url: string; 5 | } 6 | 7 | export interface AttachmentClientRequest { 8 | createAttachment: () => Promise; 9 | showAttachment: (attachment_id: string) => Promise; 10 | uploadFile: (file: File) => Promise; 11 | } 12 | 13 | export interface AttachmentRequest { 14 | uploadAttachmentTo: (uploadURL: string, file: File) => Promise; 15 | uploadAttachment: () => Promise; 16 | } 17 | -------------------------------------------------------------------------------- /src/types/transaction.ts: -------------------------------------------------------------------------------- 1 | export interface GhostInput { 2 | receivers: string[]; 3 | index: number; 4 | hint: string; 5 | } 6 | 7 | export interface GhostKeys { 8 | keys: string[]; 9 | mask: string; 10 | } 11 | 12 | export interface TransactionInput { 13 | asset_id: string; 14 | amount?: string; 15 | trace_id?: string; 16 | memo?: string; 17 | // OpponentKey used for raw transaction 18 | opponent_key?: string; 19 | opponent_multisig?: { 20 | receivers: string[]; 21 | threshold: number; 22 | }; 23 | 24 | pin?: string; 25 | } 26 | -------------------------------------------------------------------------------- /test/nft.test.ts: -------------------------------------------------------------------------------- 1 | import { client } from './common'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | describe('address', () => { 5 | it('create nft', async () => { 6 | const id = uuid(); 7 | 8 | const tr = client.newMintCollectibleTransferInput({ 9 | trace_id: id, 10 | collection_id: id, 11 | token_id: id, 12 | content: 'test', 13 | }); 14 | 15 | const payment = await client.verifyPayment(tr); 16 | console.log('mint collectibles', id, `mixin://codes/${payment.code_id}`); 17 | expect(payment.trace_id).toEqual(id); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | const replace = require('@rollup/plugin-replace'); 2 | 3 | // Not transpiled with TypeScript or Babel, so use plain Es6/Node.js! 4 | module.exports = { 5 | // This function will run for each entry/format/env combination 6 | rollup(config, opts) { 7 | config.plugins = config.plugins.map((p) => 8 | p.name === 'replace' 9 | ? replace({ 10 | 'process.env.NODE_ENV': JSON.stringify(opts.env), 11 | preventAssignment: true, 12 | }) 13 | : p 14 | ); 15 | return config; // always return a config. 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './address'; 2 | export * from './app'; 3 | export * from './attachment'; 4 | export * from './asset'; 5 | export * from './client'; 6 | export * from './conversation'; 7 | export * from './message'; 8 | export * from './multisigs'; 9 | export * from './pin'; 10 | 11 | export * from './snapshot'; 12 | export * from './transfer'; 13 | export * from './user'; 14 | export * from './blaze'; 15 | export * from './network_transaction'; 16 | export * from './transaction'; 17 | export * from './collectibles'; 18 | export * from './mvm'; 19 | 20 | export * from './oauth'; 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './client/blaze'; 3 | export * from './client/network'; 4 | export * from './types'; 5 | 6 | export { readAsset, readAssets } from './client/asset'; 7 | export { readConversation } from './client/conversation'; 8 | export { readSnapshot, readSnapshots } from './client/snapshot'; 9 | export { userMe, readBlockUsers, readFriends } from './client/user'; 10 | export { readAddresses } from './client/address'; 11 | export { createCollectibleRequest } from './client/collectibles'; 12 | export { request, mixinRequest } from './services/request'; 13 | 14 | export { getSignPIN } from './mixin/sign'; 15 | 16 | export { decryptAttachment } from './mixin/dump_msg'; -------------------------------------------------------------------------------- /example/mvm_api.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').default; 2 | 3 | const paymentURL = `https://mvm-api.test.mixinbots.com/payment`; 4 | 5 | async function main() { 6 | const { data: txInput } = await axios.post(paymentURL, { 7 | contractAddress: '0x4883Ae7CB5c3Cf9219Aeb02d010F2F6Ef353C40c', 8 | methodName: 'addAny', 9 | types: ['uint256'], 10 | values: ['11'], 11 | payment: { type: 'tx' }, 12 | }); 13 | console.log(txInput); 14 | 15 | const { data: payment } = await axios.post(paymentURL, { 16 | contractAddress: '0x4883Ae7CB5c3Cf9219Aeb02d010F2F6Ef353C40c', 17 | methodName: 'addAny', 18 | types: ['uint256'], 19 | values: ['11'], 20 | }); 21 | console.log(payment); 22 | } 23 | -------------------------------------------------------------------------------- /src/types/mvm.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInput, Payment } from '.'; 2 | 3 | export interface ContractParams { 4 | address: string; 5 | method: string; 6 | types?: string[]; 7 | values?: any[]; 8 | } 9 | 10 | export interface PaymentGenerateParams { 11 | contract?: ContractParams; 12 | contracts?: ContractParams[]; 13 | payment?: { 14 | asset?: string; 15 | amount?: string; 16 | trace?: string; 17 | type?: 'payment' | 'tx'; // payment or tx, default is payment 18 | }; 19 | } 20 | 21 | export interface MvmClientRequest { 22 | // getMvmTransaction: (params: InvokeCodeParams) => Promise; 23 | paymentGeneratorByContract: (params: PaymentGenerateParams) => Promise; 24 | } 25 | -------------------------------------------------------------------------------- /src/client/app.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { App, AppClientRequest, FavoriteApp, UpdateAppRequest } from '../types'; 3 | 4 | export class AppClient implements AppClientRequest { 5 | request!: AxiosInstance; 6 | 7 | updateApp(appID: string, params: UpdateAppRequest): Promise { 8 | return this.request.post(`/apps/${appID}`, params); 9 | } 10 | 11 | readFavoriteApps(userID: string): Promise { 12 | return this.request.get(`/users/${userID}/apps/favorite`); 13 | } 14 | 15 | favoriteApp(appID: string): Promise { 16 | return this.request.post(`/apps/${appID}/favorite`); 17 | } 18 | 19 | unfavoriteApp(appID: string): Promise { 20 | return this.request.post(`/apps/${appID}/unfavorite`); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/client/pin.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { getSignPIN } from '../mixin/sign'; 3 | import { Keystore } from '../types'; 4 | import { PINClientRequest, Turn } from '../types/pin'; 5 | 6 | export class PINClient implements PINClientRequest { 7 | keystore!: Keystore; 8 | request!: AxiosInstance; 9 | 10 | verifyPin(pin?: string): Promise { 11 | pin = getSignPIN(this.keystore, pin); 12 | return this.request.post('/pin/verify', { pin }); 13 | } 14 | 15 | modifyPin(pin: string, old_pin?: string): Promise { 16 | pin = getSignPIN(this.keystore, pin); 17 | if (old_pin) old_pin = getSignPIN(this.keystore, old_pin); 18 | return this.request.post('/pin/update', { old_pin, pin }); 19 | } 20 | 21 | readTurnServers(): Promise { 22 | return this.request.get('/turn'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/types/oauth.ts: -------------------------------------------------------------------------------- 1 | import { App } from './app'; 2 | import { User } from './user'; 3 | 4 | export type Scope = 5 | | 'PROFILE:READ' 6 | | 'ASSETS:READ' 7 | | 'PHONE:READ' 8 | | 'CONTACTS:READ' 9 | | 'MESSAGES:REPRESENT' 10 | | 'SNAPSHOTS:READ' 11 | | 'CIRCLES:READ' 12 | | 'CIRCLES:WRITE' 13 | | 'COLLECTIBLES:READ' 14 | | 'STICKER:READ'; 15 | 16 | export interface AuthData { 17 | code_id: string; 18 | authorization_code: string; 19 | authorization_id: string; 20 | scopes: string[]; 21 | user: User; 22 | app: App; 23 | created_at: string; 24 | } 25 | 26 | export interface OauthClientRequest { 27 | authorizeToken: (code: string, client_secret?: string, code_verifier?: string) => Promise<{ access_token: string; scope: string }>; 28 | getAuthorizeCode: (params: { client_id: string; scopes?: Scope[]; pin?: string }) => Promise; 29 | } 30 | -------------------------------------------------------------------------------- /src/types/address.ts: -------------------------------------------------------------------------------- 1 | export interface Address { 2 | type?: 'address'; 3 | address_id: string; 4 | asset_id: string; 5 | destination: string; 6 | tag: string; 7 | label: string; 8 | fee: string; 9 | dust: string; 10 | } 11 | 12 | export interface AddressCreateParams { 13 | label: string; 14 | asset_id: string; 15 | destination: string; 16 | tag?: string; 17 | pin?: string; 18 | } 19 | 20 | export interface AddressClientRequest { 21 | createAddress: (params: AddressCreateParams, pin?: string) => Promise
; 22 | readAddress: (address_id: string) => Promise
; 23 | readAddresses: (asset_id: string) => Promise; 24 | deleteAddress: (address_id: string, pin?: string) => Promise; 25 | } 26 | 27 | export interface AddressRequest { 28 | readAddress: (token: string, address_id: string) => Promise
; 29 | readAddresses: (token: string, asset_id: string) => Promise; 30 | } 31 | -------------------------------------------------------------------------------- /src/mixin/keystore.ts: -------------------------------------------------------------------------------- 1 | import { sign } from 'jsonwebtoken'; 2 | import { getEd25519Sign, toBuffer } from './sign'; 3 | import { Keystore } from '../types'; 4 | import { v4 } from 'uuid'; 5 | 6 | export class KeystoreAuth { 7 | keystore?: Keystore; 8 | 9 | constructor(keystore?: Keystore) { 10 | this.keystore = keystore; 11 | } 12 | 13 | signToken(signature: string, requestID: string): string { 14 | const { client_id, session_id, private_key, scope } = this.keystore!; 15 | const issuedAt = Math.floor(Date.now() / 1000); 16 | if (!requestID) requestID = v4(); 17 | const payload = { 18 | uid: client_id, 19 | sid: session_id, 20 | iat: issuedAt, 21 | exp: issuedAt + 3600, 22 | jti: requestID, 23 | sig: signature, 24 | scp: scope || 'FULL', 25 | }; 26 | const _privateKey = toBuffer(private_key, 'base64'); 27 | return _privateKey.length === 64 ? getEd25519Sign(payload, _privateKey) : sign(payload, private_key, { algorithm: 'RS512' }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/mixin/nfo.ts: -------------------------------------------------------------------------------- 1 | import { parse as UUIDParse } from 'uuid'; 2 | import { newHash } from './tools'; 3 | import { Encoder } from './encoder'; 4 | import { base64url } from './sign'; 5 | 6 | const Prefix = 'NFO'; 7 | const Version = 0x00; 8 | 9 | const DefaultChain = '43d61dcd-e413-450d-80b8-101d5e903357'; 10 | const DefaultClass = '3c8c161a18ae2c8b14fda1216fff7da88c419b5d'; 11 | 12 | export function buildMintCollectibleMemo(collection_id: string, token_id: string, content: string): string { 13 | const encoder = new Encoder(Buffer.from(Prefix, 'utf8')); 14 | encoder.write(Buffer.from([Version])); 15 | 16 | encoder.write(Buffer.from([1])); 17 | encoder.writeUint64(BigInt(1)); 18 | encoder.writeUUID(DefaultChain); 19 | 20 | encoder.writeSlice(Buffer.from(DefaultClass, 'hex')); 21 | encoder.writeSlice(Buffer.from(UUIDParse(collection_id) as Buffer)); 22 | encoder.writeSlice(Buffer.from(UUIDParse(token_id) as Buffer)); 23 | 24 | encoder.writeSlice(Buffer.from(newHash(content), 'hex')); 25 | return base64url(encoder.buf); 26 | } 27 | -------------------------------------------------------------------------------- /test/address.test.ts: -------------------------------------------------------------------------------- 1 | import { client } 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.createAddress({ asset_id, destination, label: 'test' }, '12345'); 10 | console.log(receive); 11 | const res = await client.readAddress(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.readAddresses(asset_id); 17 | const isHave = list.some(item => item.address_id === tmpAddressID); 18 | expect(isHave).toBeTruthy(); 19 | }); 20 | 21 | it('delete address', async () => { 22 | const res = await client.readAddress(tmpAddressID); 23 | expect(res.address_id).toEqual(tmpAddressID); 24 | const t = await client.deleteAddress(tmpAddressID); 25 | expect(t).toBeUndefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/mixin/tools.ts: -------------------------------------------------------------------------------- 1 | import { SHA3 } from 'sha3'; 2 | import https from 'https'; 3 | 4 | export const delay = (n = 500) => 5 | new Promise(resolve => { 6 | setTimeout(() => { 7 | resolve(); 8 | }, n); 9 | }); 10 | 11 | export function toBuffer(content: any, encoding: any = 'utf8') { 12 | if (typeof content === 'object') { 13 | content = JSON.stringify(content); 14 | } 15 | return Buffer.from(content, encoding); 16 | } 17 | 18 | export const hashMember = (ids: string[]) => newHash(ids.sort((a, b) => (a > b ? 1 : -1)).join('')); 19 | 20 | export const newHash = (str: string) => new SHA3(256).update(str).digest('hex'); 21 | 22 | export const getFileByURL = (url: string): Promise => { 23 | return new Promise((resolve, reject) => { 24 | https 25 | .get(url, res => { 26 | let data: any[] = []; 27 | res.on('data', chunk => { 28 | data.push(chunk); 29 | }); 30 | 31 | res.on('end', () => { 32 | const buffer = Buffer.concat(data); 33 | resolve(buffer); 34 | }); 35 | }) 36 | .on('error', err => { 37 | reject(err); 38 | }); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/client/asset.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { request } from '../services/request'; 3 | import { Asset, AssetClientRequest, ExchangeRate, NetworkTicker } from '../types'; 4 | 5 | export class AssetClient implements AssetClientRequest { 6 | request!: AxiosInstance; 7 | 8 | readAsset(assetID: string): Promise { 9 | return this.request.get(`/assets/${assetID}`); 10 | } 11 | 12 | readAssets(): Promise { 13 | return this.request.get('/assets'); 14 | } 15 | 16 | readAssetFee(assetID: string): Promise { 17 | return this.request.get(`/assets/${assetID}/fee`); 18 | } 19 | 20 | readExchangeRates(): Promise { 21 | return this.request.get('/fiats'); 22 | } 23 | 24 | readAssetNetworkTicker(asset: string, offset?: string): Promise { 25 | return this.request.get(`/network/ticker`, { params: { offset, asset } }); 26 | } 27 | } 28 | 29 | export const readAssets = (token: string): Promise => request(undefined, token).get('/assets'); 30 | 31 | export const readAsset = (token: string, assetID: string): Promise => request(undefined, token).get(`/assets/${assetID}`); 32 | -------------------------------------------------------------------------------- /src/client/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { request } from '../services/request'; 3 | import { Snapshot, SnapshotClientRequest, SnapshotQuery } from '../types'; 4 | 5 | export class SnapshotClient implements SnapshotClientRequest { 6 | request!: AxiosInstance; 7 | 8 | readSnapshots(params?: SnapshotQuery): Promise { 9 | return this.request.get(`/snapshots`, { params }); 10 | } 11 | 12 | readNetworkSnapshots(params?: SnapshotQuery): Promise { 13 | return this.request.get(`/network/snapshots`, { params }); 14 | } 15 | 16 | readSnapshot(snapshot_id: string): Promise { 17 | return this.request.get(`/snapshots/${snapshot_id}`); 18 | } 19 | 20 | readNetworkSnapshot(snapshot_id: string): Promise { 21 | return this.request.get(`/network/snapshots/${snapshot_id}`); 22 | } 23 | } 24 | 25 | export const readSnapshots = (token: string, params: SnapshotQuery): Promise => request(undefined, token).get('/snapshots', { params }); 26 | 27 | export const readSnapshot = (token: string, snapshot_id: string): Promise => request(undefined, token).get(`/snapshots/${snapshot_id}`); 28 | -------------------------------------------------------------------------------- /src/types/app.ts: -------------------------------------------------------------------------------- 1 | export interface App { 2 | updated_at: string; 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 | } 16 | 17 | export interface FavoriteApp { 18 | user_id: string; 19 | app_id: string; 20 | created_at: string; 21 | } 22 | 23 | type Capabilities = 'CONTACT' | 'GROUP' | 'IMMERSIVE' | 'ENCRYPTED'; 24 | 25 | export interface UpdateAppRequest { 26 | redirect_uri: string; 27 | home_uri: string; 28 | name: string; 29 | description: string; 30 | icon_base64: string; 31 | session_secret: string; 32 | category: string; 33 | capabilities: Capabilities[]; 34 | resource_patterns: string[]; 35 | } 36 | 37 | export interface AppClientRequest { 38 | updateApp: (appID: string, params: UpdateAppRequest) => Promise; 39 | readFavoriteApps: (userID: string) => Promise; 40 | favoriteApp: (appID: string) => Promise; 41 | unfavoriteApp: (appID: string) => Promise; 42 | } 43 | -------------------------------------------------------------------------------- /src/client/address.ts: -------------------------------------------------------------------------------- 1 | import { Keystore, Address, AddressCreateParams, AddressClientRequest } from '../types'; 2 | import { AxiosInstance } from 'axios'; 3 | import { getSignPIN } from '../mixin/sign'; 4 | import { request } from '../services/request'; 5 | 6 | export class AddressClient implements AddressClientRequest { 7 | request!: AxiosInstance; 8 | keystore!: Keystore; 9 | 10 | createAddress(params: AddressCreateParams, pin?: string): Promise
{ 11 | params.pin = getSignPIN(this.keystore, pin); 12 | return this.request.post('/addresses', params); 13 | } 14 | 15 | readAddress(addressID: string): Promise
{ 16 | return this.request.get(`/addresses/${addressID}`); 17 | } 18 | 19 | readAddresses(assetID: string): Promise { 20 | return this.request.get(`/assets/${assetID}/addresses`); 21 | } 22 | 23 | deleteAddress(addressID: string, pin?: string): Promise { 24 | pin = getSignPIN(this.keystore, pin); 25 | return this.request.post(`/addresses/${addressID}/delete`, { pin }); 26 | } 27 | } 28 | 29 | export const readAddresses = (token: string, assetID: string): Promise => request(undefined, token).get(`/assets/${assetID}/addresses`); 30 | -------------------------------------------------------------------------------- /src/types/blaze.ts: -------------------------------------------------------------------------------- 1 | import { MessageCategory, MessageStatus, Session } from '.'; 2 | 3 | export interface BlazeMessage { 4 | id: string; 5 | action: string; 6 | params?: { [key: string]: any }; 7 | data?: MessageType; 8 | } 9 | 10 | export type MessageType = MessageView | TransferView | SystemConversationPayload; 11 | 12 | export interface EncryptMessageView { 13 | message_id: string; 14 | recipient_id: string; 15 | state: string; 16 | sessions: Session[]; 17 | } 18 | 19 | export interface MessageView { 20 | type?: 'message'; 21 | representative_id?: string; 22 | quote_message_id?: string; 23 | conversation_id?: string; 24 | user_id?: string; 25 | session_id?: string; 26 | message_id?: string; 27 | category?: MessageCategory; 28 | data?: string; 29 | data_base64?: string; 30 | status?: MessageStatus; 31 | source?: string; 32 | created_at?: string; 33 | updated_at?: string; 34 | } 35 | 36 | export interface TransferView { 37 | type: 'transfer'; 38 | snapshot_id: string; 39 | counter_user_id: string; 40 | asset_id: string; 41 | amount: string; 42 | trace_id: string; 43 | memo: string; 44 | created_at: string; 45 | } 46 | 47 | export interface SystemConversationPayload { 48 | action: string; 49 | participant_id: string; 50 | user_id?: string; 51 | role?: string; 52 | } 53 | -------------------------------------------------------------------------------- /src/types/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from '.'; 2 | 3 | export interface Snapshot { 4 | type: string; 5 | snapshot_id: string; 6 | trace_id: string; 7 | user_id?: string; 8 | asset_id: string; 9 | created_at: string; 10 | opponent_id?: string; 11 | source: string; 12 | amount: string; 13 | memo: string; 14 | chain_id?: string; 15 | opening_balance?: string; 16 | closing_balance?: string; 17 | sender?: string; 18 | receiver?: string; 19 | transaction_hash?: string; 20 | 21 | asset?: Asset; 22 | data?: string; 23 | fee?: { 24 | amount: string; 25 | asset_id: string; 26 | }; 27 | } 28 | 29 | export interface SnapshotQuery { 30 | limit?: number | string; 31 | offset?: string; 32 | asset?: string; 33 | opponent?: string; 34 | tag?: string; 35 | destination?: string; // query external transactions 36 | } 37 | 38 | export interface SnapshotClientRequest { 39 | readSnapshots: (params?: SnapshotQuery) => Promise; 40 | readNetworkSnapshots: (params?: SnapshotQuery) => Promise; 41 | readSnapshot: (snapshot_id: string) => Promise; 42 | readNetworkSnapshot: (snapshot_id: string) => Promise; 43 | } 44 | 45 | export interface SnapshotRequest { 46 | ReadSnapshots: (token: string, params?: SnapshotQuery) => Promise; 47 | ReadSnapshot: (token: string, snapshot_id: string) => Promise; 48 | } 49 | -------------------------------------------------------------------------------- /src/client/attachment.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosResponse } from 'axios'; 2 | import { Attachment, AttachmentClientRequest } from '../types/attachment'; 3 | 4 | export class AttachmentClient implements AttachmentClientRequest { 5 | request!: AxiosInstance; 6 | 7 | createAttachment(): Promise { 8 | return this.request.post(`/attachments`); 9 | } 10 | 11 | showAttachment(attachment_id: string): Promise { 12 | return this.request.get(`/attachments/${attachment_id}`); 13 | } 14 | 15 | async uploadFile(file: File): Promise { 16 | const { view_url, upload_url, attachment_id } = await this.createAttachment(); 17 | await uploadAttachmentTo(upload_url!, file); 18 | return { view_url, attachment_id }; 19 | } 20 | } 21 | 22 | export async function uploadAttachmentTo(uploadURL: string, file: File): Promise { 23 | return axios.create()({ 24 | url: uploadURL, 25 | method: 'PUT', 26 | data: file, 27 | headers: { 28 | 'x-amz-acl': 'public-read', 29 | 'Content-Type': 'application/octet-stream', 30 | }, 31 | maxContentLength: 2147483648, 32 | }); 33 | } 34 | 35 | export function uploadAttachment(attachment: Attachment, file: File): Promise { 36 | if (!attachment.upload_url) return Promise.reject(new Error('No upload URL')); 37 | return uploadAttachmentTo(attachment.upload_url!, file); 38 | } 39 | -------------------------------------------------------------------------------- /src/mixin/dump_transaction.ts: -------------------------------------------------------------------------------- 1 | import { GhostKeys, Transaction } from '../types'; 2 | import { Encoder, magic, maxEncodingInt, OperatorCmp, OperatorSum } from './encoder'; 3 | 4 | export function dumpTransaction(signed: Transaction): string { 5 | const enc = new Encoder(magic); 6 | enc.write(Buffer.from([0x00, signed.version!])); 7 | enc.write(Buffer.from(signed.asset, 'hex')); 8 | 9 | const il = signed.inputs!.length; 10 | enc.writeInt(il); 11 | signed.inputs!.forEach(i => enc.encodeInput(i)); 12 | 13 | const ol = signed.outputs!.length; 14 | enc.writeInt(ol); 15 | signed.outputs!.forEach(o => enc.encodeOutput(o)); 16 | 17 | const e = Buffer.from(signed.extra!, 'hex'); 18 | enc.writeInt(e.byteLength); 19 | enc.write(e); 20 | 21 | if (signed.aggregated) { 22 | enc.encodeAggregatedSignature(signed.aggregated); 23 | } else { 24 | const sl = signed.signatures ? Object.keys(signed.signatures).length : 0; 25 | if (sl == maxEncodingInt) throw new Error('signatures overflow'); 26 | enc.writeInt(sl); 27 | if (sl > 0) { 28 | enc.encodeSignature(signed.signatures!); 29 | } 30 | } 31 | return enc.buf.toString('hex'); 32 | } 33 | 34 | export function DumpOutputFromGhostKey(gi: GhostKeys, amount: string, threshold: number) { 35 | const { mask, keys } = gi; 36 | return { 37 | mask, 38 | keys, 39 | amount: Number(amount).toFixed(8), 40 | script: Buffer.from([OperatorCmp, OperatorSum, threshold]).toString('hex'), 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/types/asset.ts: -------------------------------------------------------------------------------- 1 | export interface Asset { 2 | asset_id: string; 3 | chain_id: string; 4 | asset_key?: string; 5 | mixin_id?: string; 6 | symbol: string; 7 | name: string; 8 | icon_url: string; 9 | price_btc: string; 10 | change_btc: string; 11 | price_usd: string; 12 | change_usd: string; 13 | balance: string; 14 | destination: string; 15 | tag: string; 16 | confirmations: number; 17 | capitalization?: number; 18 | amount?: string; 19 | fee?: string; 20 | liquidity?: string; 21 | snapshots_count: number; 22 | } 23 | 24 | export interface ExchangeRate { 25 | code: string; 26 | rate: string; 27 | } 28 | 29 | export interface NetworkTicker { 30 | type: 'ticker'; 31 | price_usd: string; 32 | price_btc: string; 33 | } 34 | 35 | export interface AssetClientRequest { 36 | readAsset: (asset_id: string) => Promise; 37 | readAssets: () => Promise; 38 | readAssetFee: (asset_id: string) => Promise; 39 | readAssetNetworkTicker: (asset_id: string, offset?: string) => Promise; 40 | readExchangeRates: () => Promise; 41 | } 42 | 43 | export interface AssetRequest { 44 | readAsset: (asset_id: string) => Promise; 45 | readAssets: () => Promise; 46 | readAssetFee: (asset_id: string) => Promise; 47 | ReadExchangeRates: () => Promise; 48 | } 49 | 50 | export interface AssetClient { 51 | readAsset: (token: string, asset_id: string) => Promise; 52 | readAssets: (token: string) => Promise; 53 | } 54 | -------------------------------------------------------------------------------- /src/types/network_transaction.ts: -------------------------------------------------------------------------------- 1 | import { MultisigUTXO } from '.'; 2 | 3 | export interface MintData { 4 | group: string; 5 | batch: bigint; 6 | amount: number; 7 | } 8 | 9 | export interface DepositData { 10 | chain: string; 11 | asset: string; 12 | transaction: string; 13 | index: bigint; 14 | amount: number; 15 | } 16 | 17 | export interface WithdrawData { 18 | chain: string; 19 | asset: string; 20 | address: string; 21 | tag: string; 22 | } 23 | 24 | export interface Input { 25 | hash?: string; 26 | index?: number; 27 | genesis?: string; 28 | deposit?: DepositData; 29 | mint?: MintData; 30 | } 31 | 32 | export interface Output { 33 | type?: number; 34 | amount?: string; 35 | keys?: string[]; 36 | withdrawal?: WithdrawData; 37 | script?: string; 38 | mask?: string; 39 | } 40 | 41 | export interface Aggregated { 42 | signers: number[]; 43 | signature: string; 44 | } 45 | 46 | export interface Transaction { 47 | hash?: string; 48 | snapshot?: string; 49 | signatures?: { 50 | [key: number]: string; 51 | }; 52 | aggregated?: { 53 | signers: number[]; 54 | signature: string; 55 | }; 56 | 57 | version?: number; 58 | asset: string; 59 | inputs?: Input[]; 60 | outputs?: Output[]; 61 | extra: string; 62 | } 63 | 64 | export interface RawTransactionInput { 65 | memo: string; 66 | inputs: MultisigUTXO[]; 67 | outputs: RawTransactionOutput[]; 68 | hint: string; 69 | } 70 | 71 | export interface RawTransactionOutput { 72 | receivers: string[]; 73 | threshold: number; 74 | amount: string; 75 | } 76 | -------------------------------------------------------------------------------- /test/user.test.ts: -------------------------------------------------------------------------------- 1 | import { client } from './common'; 2 | 3 | describe('user', () => { 4 | it('userMe', async () => { 5 | const user = await client.userMe(); 6 | expect(user.user_id).toEqual(client.keystore.client_id); 7 | }); 8 | 9 | it('readUser', async () => { 10 | const user = await client.readUser('30265'); 11 | expect(user.identity_number).toEqual('30265'); 12 | }); 13 | 14 | it('readBlockUsers', async () => { 15 | const user = await client.readBlockUsers(); 16 | expect(Array.isArray(user)).toBeTruthy(); 17 | }); 18 | 19 | it('readUsers', async () => { 20 | const users = await client.readUsers(['e8e8cd79-cd40-4796-8c54-3a13cfe50115']); 21 | expect(users[0].identity_number).toEqual('30265'); 22 | }); 23 | 24 | it('searchUser', async () => { 25 | // const user1 = await client.searchUser("+8613801380138") 26 | // expect(user1.identity_number).toEqual("7000") 27 | const user2 = await client.searchUser('30265'); 28 | expect(user2.identity_number).toEqual('30265'); 29 | }); 30 | 31 | it('readFriends', async () => { 32 | const user = await client.readFriends(); 33 | expect(Array.isArray(user)).toBeTruthy(); 34 | }); 35 | 36 | it('createUser', async () => { 37 | const user = await client.createUser('测试...'); 38 | expect(user.full_name).toEqual('测试...'); 39 | }); 40 | 41 | it('modifyProfile', async () => { 42 | let user = await client.userMe(); 43 | let name = user.full_name; 44 | user = await client.modifyProfile('测试...'); 45 | expect(user.full_name).toEqual('测试...'); 46 | user = await client.modifyProfile(name); 47 | expect(user.full_name).toEqual(name); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": [ 4 | "src", 5 | "types" 6 | ], 7 | "compilerOptions": { 8 | "module": "esnext", 9 | "lib": [ 10 | "dom", 11 | "esnext" 12 | ], 13 | "importHelpers": true, // output .d.ts declaration files for consumers 14 | "declaration": true, 15 | // output .js.map sourcemap files for consumers 16 | "sourceMap": true, 17 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 18 | "rootDir": "./src", 19 | // stricter type-checking for stronger correctness. Recommended by TS 20 | "strict": true, 21 | // linter checks for common issues 22 | "noImplicitReturns": true, 23 | "noFallthroughCasesInSwitch": true, 24 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | // use Node's module resolution algorithm, instead of the legacy TS one 28 | "moduleResolution": "node", 29 | // transpile JSX to React.createElement 30 | "jsx": "react", 31 | // interop between ESM and CJS modules. Recommended by TS 32 | "esModuleInterop": true, 33 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 34 | "skipLibCheck": true, 35 | // error out if import and file system have a casing mismatch. Recommended by TS 36 | "forceConsistentCasingInFileNames": true, 37 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 38 | "noEmit": true 39 | } 40 | } -------------------------------------------------------------------------------- /example/mvm.js: -------------------------------------------------------------------------------- 1 | const { Client, getContractByAssetID, getAssetIDByAddress, getContractByUserIDs, getUserIDByAddress } = require('mixin-node-sdk'); 2 | const keystore = require('./keystore.json'); 3 | const client = new Client(keystore); 4 | 5 | async function main() { 6 | // get tx 7 | const tx = await client.paymentGeneratorByContract({ 8 | contracts: [ 9 | { 10 | address: '0x2E8f70631208A2EcFC6FA47Baf3Fde649963baC7', 11 | method: 'addAny', 12 | types: ['uint256'], 13 | values: ['10'], 14 | }, 15 | ], 16 | payment: { 17 | type: 'tx', 18 | }, 19 | }); 20 | const res = await client.transaction(tx); 21 | console.log(res); 22 | 23 | // get payment 24 | 25 | const payment = await client.paymentGeneratorByContract({ 26 | contracts: [ 27 | { 28 | address: '0x2E8f70631208A2EcFC6FA47Baf3Fde649963baC7', 29 | method: 'addAny', 30 | types: ['uint256'], 31 | values: ['10'], 32 | }, 33 | ], 34 | }); 35 | console.log(`mixin://codes/${payment.code_id}`); 36 | 37 | const BtcAssetID = 'c6d0c728-2624-429b-8e0d-d9d19b6592fa'; 38 | const btcAddress = await getContractByAssetID(BtcAssetID); 39 | console.log('mvm asset_id -> address...', btcAddress); 40 | const btcAssetID = await getAssetIDByAddress(btcAddress); 41 | console.log('mvm address -> asset_id...', btcAssetID); 42 | const UID = 'e8e8cd79-cd40-4796-8c54-3a13cfe50115'; 43 | const userContract = await getContractByUserIDs(UID); 44 | console.log('mvm user_id -> address', userContract); 45 | const uID = await getUserIDByAddress(userContract); 46 | console.log('mvm address -> user_id', uID); 47 | } 48 | 49 | main(); 50 | -------------------------------------------------------------------------------- /src/client/transfer.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { getSignPIN } from '../mixin/sign'; 3 | import { mixinRequest } from '../services/request'; 4 | import { Keystore, Snapshot, TransactionInput } from '../types'; 5 | import { Payment, RawTransaction, TransferClientRequest, TransferInput, WithdrawInput } from '../types/transfer'; 6 | 7 | export class TransferClient implements TransferClientRequest { 8 | keystore!: Keystore; 9 | request!: AxiosInstance; 10 | 11 | verifyPayment(params: TransferInput | TransactionInput): Promise { 12 | return this.request.post('/payments', params); 13 | } 14 | 15 | transfer(params: TransferInput, pin?: string): Promise { 16 | params.pin = getSignPIN(this.keystore, pin); 17 | return this.request.post('/transfers', params); 18 | } 19 | 20 | readTransfer(trace_id: string): Promise { 21 | return this.request.get(`/transfers/trace/${trace_id}`); 22 | } 23 | 24 | transaction(params: TransactionInput, pin?: string): Promise { 25 | params.pin = getSignPIN(this.keystore, pin); 26 | return this.request.post('/transactions', params); 27 | } 28 | 29 | sendRawTransaction(raw: string): Promise<{ hash: string }> { 30 | return this.request.post('/external/proxy', { method: 'sendrawtransaction', params: [raw] }); 31 | } 32 | 33 | withdraw(params: WithdrawInput, pin?: string): Promise { 34 | params.pin = getSignPIN(this.keystore, pin); 35 | return this.request.post('/withdrawals', params); 36 | } 37 | } 38 | 39 | export const verifyPayment = (params: TransactionInput | TransferInput): Promise => mixinRequest.post('/payments', params); 40 | -------------------------------------------------------------------------------- /src/types/network.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from '.'; 2 | 3 | export interface NetworkChain { 4 | chain_id: string; 5 | icon_url: string; 6 | name: string; 7 | type: string; 8 | withdrawal_fee: string; 9 | withdrawal_timestamp: string; 10 | withdrawal_pending_count: string; 11 | deposit_block_height: string; 12 | external_block_height: string; 13 | managed_block_height: string; 14 | is_synchronized: string; 15 | } 16 | 17 | export interface NetworkAsset { 18 | amount: string; 19 | asset_id: string; 20 | icon_url: string; 21 | symbol: string; 22 | 23 | // populated only at ReadNetworkAsset 24 | chain_id?: string; 25 | mixin_id?: string; 26 | name?: string; 27 | snapshot_count?: number; 28 | } 29 | 30 | export interface NetworkInfo { 31 | assets: NetworkAsset[]; 32 | chains: NetworkChain[]; 33 | assets_count: string; 34 | peak_throughput: string; 35 | snapshots_count: string; 36 | type: string; 37 | } 38 | 39 | export interface ExternalTransaction { 40 | transaction_id: string; 41 | created_at: string; 42 | transaction_hash: string; 43 | sender: string; 44 | chain_id: string; 45 | asset_id: string; 46 | amount: string; 47 | destination: string; 48 | tag: string; 49 | confirmations: string; 50 | threshold: string; 51 | } 52 | 53 | export interface NetworkRequest { 54 | readNetworkInfo: () => Promise; 55 | readNetworkAsset: (asset_id: string) => Promise; 56 | readTopNetworkAssets: () => Promise; 57 | 58 | ReadExternalTransactions: (asset_id: string, destination: string, tag: string) => Promise; 59 | sendExternalProxy: (method: string, params: any[]) => Promise; 60 | } 61 | -------------------------------------------------------------------------------- /src/client/network.ts: -------------------------------------------------------------------------------- 1 | import { mixinRequest } from '../services/request'; 2 | import { Asset, NetworkTicker, Snapshot, SnapshotQuery, Transaction } from '../types'; 3 | 4 | export const readNetworkChains = (): Promise => mixinRequest.get('/network/chains'); 5 | 6 | // only support limit/offset/asset/order 7 | export const readNetworkSnapshots = (params: SnapshotQuery): Promise => mixinRequest.get('/network/snapshots', { params }); 8 | 9 | export const readNetworkSnapshot = (id: string): Promise => mixinRequest.get(`/network/snapshots/${id}`); 10 | 11 | export const readExternalTransactions = (params: SnapshotQuery): Promise => mixinRequest.get('/external/transactions', { params }); 12 | 13 | export const readNetworkAssetsTop = (): Promise => mixinRequest.get('/network/assets/top'); 14 | 15 | export const readNetworkAssetsMultisig = (): Promise => mixinRequest.get('/network/assets/multisig'); 16 | 17 | export const readNetworkAsset = (id: string): Promise => mixinRequest.get(`/network/assets/${id}`); 18 | 19 | export const searchNetworkAsset = (assetNameOrSymbol: string): Promise => mixinRequest.get(`/network/assets/search/${assetNameOrSymbol}`); 20 | 21 | export const readExternalAddressesCheck = (params: SnapshotQuery): Promise => mixinRequest.get(`/external/addresses/check`, { params }); 22 | 23 | export const readNetworkTicker = (asset_id: string, offset?: string): Promise => mixinRequest.get(`/network/ticker`, { params: { asset: asset_id, offset } }); 24 | 25 | export const sendExternalProxy = (method: string, params: any[]): Promise => mixinRequest.post(`/external/proxy`, { method, params }); 26 | -------------------------------------------------------------------------------- /src/types/transfer.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInput, Snapshot, Asset, User } from '.'; 2 | 3 | export interface Payment { 4 | recipient: User; 5 | asset: Asset; 6 | asset_id: string; 7 | amount: string; 8 | trace_id: string; 9 | status: string; 10 | memo: string; 11 | receivers: string; 12 | threshold: string; 13 | code_id: string; 14 | } 15 | 16 | // export interface Transaction { 17 | // type: 'transaction' 18 | // transaction_id: string 19 | // transaction_hash: string 20 | // sender: string 21 | // chain_id: string 22 | // asset_id: string 23 | // amount: string 24 | // destination: string 25 | // tag: string 26 | // created_at: string 27 | // output_index: number, 28 | // confirmations: number, 29 | // threshold: number, 30 | // } 31 | 32 | export interface RawTransaction { 33 | type: string; 34 | snapshot: string; 35 | opponent_key: string; 36 | asset_id: string; 37 | amount: string; 38 | trace_id: string; 39 | memo: string; 40 | state: string; 41 | created_at: string; 42 | transaction_hash: string; 43 | snapshot_hash: string; 44 | snapshot_at: string; 45 | } 46 | 47 | export interface TransferInput { 48 | asset_id: string; 49 | opponent_id: string; 50 | amount?: string; 51 | trace_id?: string; 52 | memo?: string; 53 | 54 | pin?: string; 55 | } 56 | 57 | export interface WithdrawInput { 58 | address_id: string; 59 | amount: string; 60 | trace_id?: string; 61 | 62 | memo?: string; 63 | pin?: string; 64 | } 65 | 66 | export interface TransferClientRequest { 67 | verifyPayment: (params: TransferInput | TransactionInput) => Promise; 68 | transfer: (params: TransferInput, pin?: string) => Promise; 69 | readTransfer: (trace_id: string) => Promise; 70 | transaction: (params: TransactionInput, pin?: string) => Promise; 71 | sendRawTransaction: (raw: string) => Promise<{ hash: string }>; 72 | withdraw: (params: WithdrawInput, pin?: string) => Promise; 73 | } 74 | -------------------------------------------------------------------------------- /example/nft.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('mixin-node-sdk'); // >= 3.0.13 2 | const keystore = require('./keystore.json'); 3 | const client = new Client(keystore); 4 | const isMint = true; 5 | 6 | async function main() { 7 | if (isMint) { 8 | const id = client.newUUID(); 9 | const tr = client.newMintCollectibleTransferInput({ 10 | trace_id: id, 11 | collection_id: id, 12 | token_id: id, 13 | content: 'test', 14 | }); 15 | const payment = await client.verifyPayment(tr); 16 | console.log('mint collectibles', id, 'mixin://codes/' + payment.code_id); 17 | return; 18 | } 19 | const outputs = await client.readCollectibleOutputs([client.keystore.client_id], 1); 20 | console.log(outputs); 21 | 22 | outputs.forEach(async output => { 23 | switch (output.state) { 24 | case 'unspent': 25 | const token = await client.readCollectibleToken(output.token_id); 26 | handleUnspentOutput(output, token); 27 | break; 28 | case 'signed': 29 | handleSignedOutput(output); 30 | break; 31 | } 32 | }); 33 | } 34 | 35 | async function handleUnspentOutput(output, token) { 36 | console.log(`handle unspent output ${output.output_id} ${token.token_id}`); 37 | const receivers = ['e8e8cd79-cd40-4796-8c54-3a13cfe50115']; 38 | const signedTx = await client.makeCollectibleTransactionRaw({ 39 | output, 40 | token, 41 | receivers, 42 | threshold: 1, 43 | }); 44 | console.log(signedTx); 45 | const createRes = await client.createCollectibleRequest('sign', signedTx); 46 | console.log('create collectible...', createRes); 47 | const signRes = await client.signCollectibleRequest(createRes.request_id); 48 | console.log('sign finished...', signRes); 49 | } 50 | 51 | async function handleSignedOutput(output) { 52 | console.log(`handle signed output ${output.output_id}`); 53 | const res = await client.sendRawTransaction(output.signed_tx); 54 | console.log('send raw transaction finished...', res); 55 | } 56 | main(); 57 | -------------------------------------------------------------------------------- /src/types/conversation.ts: -------------------------------------------------------------------------------- 1 | export interface Conversation { 2 | conversation_id: string; 3 | creator_id: string; 4 | category: string; 5 | name: string; 6 | icon_url: string; 7 | announcement: string; 8 | created_at: string; 9 | code_id: string; 10 | code_url: string; 11 | 12 | participants: Participant[]; 13 | } 14 | 15 | export type ConversationCategory = 'CONTACT' | 'GROUP'; 16 | export type ConversationAction = 'CREATE' | 'ADD' | 'REMOVE' | 'JOIN' | 'EXIT' | 'ROLE'; 17 | export type ConversationRole = 'OWNER' | 'ADMIN' | ''; 18 | 19 | export interface Participant { 20 | user_id: string; 21 | type?: 'participant'; 22 | role?: ConversationRole; 23 | created_at?: string; 24 | } 25 | 26 | export interface ConversationCreateParams { 27 | category: ConversationCategory; 28 | conversation_id: string; 29 | participants: Participant[]; 30 | name?: string; 31 | } 32 | 33 | export interface ConversationUpdateParams { 34 | name?: string; 35 | announcement?: string; 36 | } 37 | 38 | export interface ConversationClientRequest { 39 | createConversation: (params: ConversationCreateParams) => Promise; 40 | updateConversation: (conversationID: string, params: ConversationUpdateParams) => Promise; 41 | createContactConversation: (userID: string) => Promise; 42 | createGroupConversation: (conversationID: string, name: string, participant: Participant[]) => Promise; 43 | readConversation: (conversationID: string) => Promise; 44 | managerConversation: (conversationID: string, action: ConversationAction, participant: Participant[]) => Promise; 45 | addParticipants: (conversationID: string, userIDs: string[]) => Promise; 46 | removeParticipants: (conversationID: string, userIDs: string[]) => Promise; 47 | adminParticipants: (conversationID: string, userIDs: string[]) => Promise; 48 | rotateConversation: (conversationID: string) => Promise; 49 | } 50 | -------------------------------------------------------------------------------- /src/types/multisigs.ts: -------------------------------------------------------------------------------- 1 | import { GhostKeys, GhostInput, RawTransactionInput } from '.'; 2 | 3 | export type UTXOState = 'unspent' | 'signed' | 'spent'; 4 | 5 | export type MultisigAction = 'sign' | 'unlock'; 6 | 7 | export type MultisigState = 'initial' | 'signed'; 8 | 9 | export interface MultisigUTXO { 10 | type: string; 11 | user_id: string; 12 | utxo_id: string; 13 | asset_id: string; 14 | transaction_hash: string; 15 | output_index: number; 16 | amount: string; 17 | threshold: number; 18 | members: string[]; 19 | memo: string; 20 | state: UTXOState; 21 | created_at: string; 22 | updated_at: string; 23 | signed_by: string; 24 | signed_tx: string; 25 | } 26 | 27 | export interface MultisigRequest { 28 | type: string; 29 | request_id: string; 30 | user_id: string; 31 | asset_id: string; 32 | amount: string; 33 | threshold: string; 34 | senders: string; 35 | receivers: string; 36 | signers: string; 37 | memo: string; 38 | action: MultisigAction; 39 | state: MultisigState; 40 | transaction_hash: string; 41 | raw_transaction: string; 42 | created_at: string; 43 | updated_at: string; 44 | code_id: string; 45 | } 46 | 47 | export interface MultisigClientRequest { 48 | readMultisigs: (offset: string, limit: number) => Promise; 49 | readMultisigOutput(id: string): Promise; 50 | readMultisigOutputs: (members: string[], threshold: number, offset: string, limit: number) => Promise; 51 | createMultisig: (action: MultisigAction, raw: string) => Promise; 52 | signMultisig: (request_id: string, pin?: string) => Promise; 53 | cancelMultisig: (request_id: string) => Promise; 54 | unlockMultisig: (request_id: string, pin?: string) => Promise; 55 | readGhostKeys: (receivers: string[], index: number) => Promise; 56 | batchReadGhostKeys: (inputs: GhostInput[]) => Promise; 57 | makeMultisignTransaction: (txInput: RawTransactionInput) => Promise; 58 | } 59 | -------------------------------------------------------------------------------- /src/services/request.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; 3 | import { KeystoreAuth } from '../mixin/keystore'; 4 | import { signRequest } from '../mixin/sign'; 5 | import { delay } from '../mixin/tools'; 6 | import { v4 as uuid } from 'uuid'; 7 | import { Keystore } from '../types'; 8 | 9 | const hostURL = ['https://mixin-api.zeromesh.net', 'https://api.mixin.one']; 10 | 11 | export const request = (keystore?: Keystore, token = ''): AxiosInstance => { 12 | const ins = axios.create({ 13 | baseURL: hostURL[0], 14 | headers: { 'Content-Type': 'application/json;charset=UTF-8' }, 15 | timeout: 3000, 16 | }); 17 | 18 | let k: KeystoreAuth; 19 | if (keystore) k = new KeystoreAuth(keystore); 20 | 21 | ins.interceptors.request.use((config: AxiosRequestConfig) => { 22 | const { method, data } = config; 23 | const uri = ins.getUri(config); 24 | const requestID = uuid(); 25 | config.headers['x-request-id'] = requestID; 26 | let jwtToken = ''; 27 | if (token) jwtToken = token; 28 | else if (k) jwtToken = k.signToken(signRequest(method!, uri, data), requestID); 29 | config.headers.Authorization = 'Bearer ' + jwtToken; 30 | return config; 31 | }); 32 | 33 | ins.interceptors.response.use( 34 | (res: AxiosResponse) => { 35 | const { data, error } = res.data; 36 | if (error) { 37 | try { 38 | error.request_id = res.headers['x-request-id']; 39 | } catch (e) { 40 | console.log(e, res.config); 41 | } 42 | return error; 43 | } 44 | return data; 45 | }, 46 | async (e: any) => { 47 | if (['ETIMEDOUT', 'ECONNABORTED'].includes(e.code)) { 48 | ins.defaults.baseURL = e.config.baseURL = e.config.baseURL === hostURL[0] ? hostURL[1] : hostURL[0]; 49 | await delay(); 50 | return ins(e.config); 51 | } 52 | return Promise.reject(e); 53 | }, 54 | ); 55 | return ins; 56 | }; 57 | 58 | export const mixinRequest = request(); 59 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # bot-api-nodejs-client 2 | 3 | mixin 的 nodejs 版 sdk 4 | 5 | [中文版文档](https://liuzemei.github.io/mixin-js-sdk-docs/docs/server/intro) 6 | 7 | [中文版视频教程](https://developers.mixinbots.com/courses/1e276ee9-18fb-42e3-915a-54b049679084) 8 | 9 | ## 新版本特性 10 | 11 | 1. 更友好的类型和代码提示 12 | 2. 更规范的函数命名 13 | 3. 更全面的测试覆盖 14 | 15 | ## 安装 16 | 17 | ```shell 18 | npm install mixin-node-sdk 19 | ``` 20 | 21 | 如果你使用 `yarn` 22 | 23 | ```shell 24 | yarn add mixin-node-sdk 25 | ``` 26 | 27 | ## 使用 28 | 29 | 1. 仅使用 Mixin 的 Api 30 | 31 | ```js 32 | const { Client } = require('mixin-node-sdk'); 33 | const client = new Client({ 34 | client_id: '', 35 | session_id: '', 36 | pin_token: '', 37 | private_key: '', 38 | pin: '', 39 | client_secret: '', 40 | }); 41 | // 使用 Promise 42 | client.userMe().then(console.log); 43 | 44 | // 使用 async await 45 | async function getMe() { 46 | const me = await client.userMe(); 47 | console.log(me); 48 | } 49 | ``` 50 | 51 | 2. 使用 Mixin 的消息功能() 52 | 53 | ```js 54 | const { BlazeClient } = require('mixin-node-sdk'); 55 | const client = new BlazeClient( 56 | { 57 | client_id: '', 58 | session_id: '', 59 | pin_token: '', 60 | private_key: '', 61 | pin: '', 62 | client_secret: '', 63 | }, 64 | { parse: true, syncAck: true }, 65 | ); 66 | 67 | client.loopBlaze({ 68 | onMessage(msg) { 69 | console.log(msg); 70 | }, 71 | onAckReceipt(msg) { 72 | console.log('ack', msg); 73 | }, 74 | }); 75 | ``` 76 | 77 | > BlazeClient 直接继承了 Client,所以所有 Client 的方法 BlazeClient 都可以直接调用。 78 | 79 | ## 注意 80 | 81 | 1. 如果你使用的是 `mixin-node-sdk@2.xx.xx` 的版本,请看 [https://github.com/liuzemei/mixin-node-sdk/tree/v2](https://github.com/liuzemei/mixin-node-sdk/tree/v2) 82 | 83 | ## 贡献 84 | 85 | 可接受 PRs. 86 | 87 | ## 相关文章或链接 88 | 89 | > 1. [https://developers.mixin.one/document](https://developers.mixin.one/document) 90 | > 2. [https://github.com/fox-one/mixin-sdk-go](https://github.com/fox-one/mixin-sdk-go) 91 | > 3. [https://mixin.one](https://mixin.one) 92 | 93 | ## License 94 | 95 | MIT © Richard McRichface 96 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | import { App } from '.'; 2 | 3 | export type AcceptSource = 'EVERYBODY' | 'CONTACTS'; 4 | 5 | export interface User { 6 | type: 'user'; 7 | user_id: string; 8 | identity_number: string; 9 | phone: string; 10 | full_name: string; 11 | biography: string; 12 | avatar_url: string; 13 | relationship: string; 14 | mute_until: string; 15 | created_at: string; 16 | is_verified: boolean; 17 | app?: App; 18 | session_id?: string; 19 | pin_token?: string; 20 | pin_token_base64?: string; 21 | code_id?: string; 22 | code_url?: string; 23 | has_pin?: boolean; 24 | has_emergency_contact?: boolean; 25 | receive_message_source?: AcceptSource; 26 | accept_conversation_source?: AcceptSource; 27 | accept_search_source?: AcceptSource; 28 | fiat_currency?: string; 29 | device_status?: string; 30 | 31 | public_key?: string; 32 | private_key?: string; 33 | } 34 | 35 | export interface UpdateUserParams { 36 | full_name?: string; 37 | avatar_base64?: string; 38 | receive_message_source?: AcceptSource; 39 | accept_conversation_source?: AcceptSource; 40 | accept_search_source?: AcceptSource; 41 | biography?: string; 42 | fiat_currency?: string; 43 | transfer_notification_threshold?: number; // default 0 44 | transfer_confirmation_threshold?: number; // default 100 45 | } 46 | 47 | export interface UserClientRequest { 48 | userMe: () => Promise; 49 | readUser: (userIdOrIdentityNumber: string) => Promise; 50 | readBlockUsers: () => Promise; 51 | readUsers: (userIDs: string[]) => Promise; 52 | searchUser: (identityNumberOrPhone: string) => Promise; 53 | readFriends: () => Promise; 54 | createUser: (full_name: string, session_secret?: string) => Promise; 55 | modifyProfile: (full_name: string, avatar_base64: string) => Promise; 56 | modifyRelationships: (relationship: UserRelationship) => Promise; 57 | } 58 | 59 | export type operation = 'ADD' | 'REMOVE' | 'BLOCK' | 'UNBLOCK'; 60 | 61 | export interface UserRelationship { 62 | user_id: string; 63 | action: operation; 64 | full_name?: string; 65 | } 66 | -------------------------------------------------------------------------------- /example/multisigns.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('mixin-node-sdk'); 2 | const keystore = require('./keystore.json'); 3 | const client = new Client(keystore); 4 | async function main() { 5 | const me = await client.userMe(); 6 | const members = [me.app.creator_id, me.user_id]; 7 | const threshold = 1; 8 | const amount = String(1e-4); 9 | // 1. 发送交易给多签账户 10 | const sendMultiTx = await client.transaction({ 11 | asset_id: '965e5c6e-434c-3fa9-b780-c50f43cd955c', 12 | amount, 13 | trace_id: client.newUUID(), 14 | memo: 'send to multisig', 15 | opponent_multisig: { 16 | threshold, 17 | receivers: members, 18 | }, 19 | }); 20 | console.log(sendMultiTx); 21 | let utxo, 22 | offset = ''; 23 | 24 | // 等待交易完成 25 | while (true) { 26 | const outputs = await client.readMultisigOutputs(members, threshold, offset, 10); 27 | // console.log(outputs); 28 | for (const { updated_at, transaction_hash } of outputs) { 29 | offset = updated_at; 30 | console.log('current hash:', transaction_hash); 31 | if (transaction_hash === sendMultiTx.transaction_hash) { 32 | utxo = outputs; 33 | break; 34 | } 35 | } 36 | if (utxo) { 37 | console.log('found hash:', transaction_hash); 38 | break; 39 | } 40 | await new Promise(resolve => setTimeout(resolve, 1000 * 5)); 41 | console.log('waiting for utxo', sendMultiTx.transaction_hash); 42 | } 43 | // 2. 从多签账户转给开发者 44 | const transferTx = await client.makeMultisignTransaction({ 45 | memo: 'multisig test', 46 | inputs: utxo, 47 | outputs: [ 48 | { 49 | receivers: [me.app.creator_id], 50 | threshold: 1, 51 | amount, 52 | }, 53 | ], 54 | hint: client.newUUID(), 55 | }); 56 | // 构建签名交易 57 | console.log('transferTx', transferTx); 58 | const multisig = await client.createMultisig('sign', transferTx); 59 | // 签名c 60 | console.log('multisig', multisig); 61 | const signed = await client.signMultisig(multisig.request_id); 62 | // 发送签名交易 63 | console.log('signed.....'); 64 | console.log(signed); 65 | const txHash = await client.sendRawTransaction(signed.raw_transaction); 66 | console.log(txHash); 67 | } 68 | 69 | main(); 70 | -------------------------------------------------------------------------------- /src/client/user.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { UserClientRequest, User, UserRelationship, UpdateUserParams } from '../types/user'; 3 | import forge from 'node-forge'; 4 | import { request } from '../services/request'; 5 | 6 | export class UserClient implements UserClientRequest { 7 | request!: AxiosInstance; 8 | 9 | userMe(): Promise { 10 | return this.request.get(`/me`); 11 | } 12 | 13 | readUser(userIdOrIdentityNumber: string): Promise { 14 | return this.request.get(`/users/${userIdOrIdentityNumber}`); 15 | } 16 | 17 | readBlockUsers(): Promise { 18 | return this.request.get(`/blocking_users`); 19 | } 20 | 21 | readUsers(userIDs: string[]): Promise { 22 | return this.request.post(`/users/fetch`, userIDs); 23 | } 24 | 25 | readFriends(): Promise { 26 | return this.request.get(`/friends`); 27 | } 28 | 29 | searchUser(identityNumberOrPhone: string): Promise { 30 | return this.request.get(`/search/${identityNumberOrPhone}`); 31 | } 32 | 33 | async createUser(full_name: string, session_secret?: string): Promise { 34 | if (session_secret) return this.request.post(`/users`, { full_name, session_secret }); 35 | const { publicKey, privateKey } = forge.pki.ed25519.generateKeyPair(); 36 | const params = { 37 | full_name, 38 | session_secret: Buffer.from(publicKey).toString('base64'), 39 | }; 40 | const u: User = await this.request.post(`/users`, params); 41 | u.public_key = publicKey.toString('base64'); 42 | u.private_key = privateKey.toString('base64'); 43 | return u; 44 | } 45 | 46 | modifyProfile(full_name: string, avatar_base64: string): Promise { 47 | return this.updateProfile({ full_name, avatar_base64 }); 48 | } 49 | 50 | updateProfile(params: UpdateUserParams): Promise { 51 | return this.request.post(`/me/preferences`, params); 52 | } 53 | 54 | modifyRelationships(relationship: UserRelationship): Promise { 55 | return this.request.post(`/relationships`, relationship); 56 | } 57 | } 58 | 59 | export const userMe = (token: string): Promise => request(undefined, token).get('/me'); 60 | 61 | export const readFriends = (token: string): Promise => request(undefined, token).get(`/friends`); 62 | 63 | export const readBlockUsers = (token: string): Promise => request(undefined, token).get(`/blocking_users`); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mixin-node-sdk", 3 | "version": "3.1.16", 4 | "license": "MIT", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/liuzemei/mixin-node-sdk.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/liuzemei/mixin-node-sdk/issues" 20 | }, 21 | "homepage": "https://github.com/liuzemei/mixin-node-sdk#readme", 22 | "keywords": [ 23 | "mixin", 24 | "node", 25 | "blockchain", 26 | "crypto", 27 | "js" 28 | ], 29 | "scripts": { 30 | "start": "tsdx watch", 31 | "build": "tsdx build", 32 | "build:umd": "tsdx build --format umd", 33 | "test": "tsdx test", 34 | "lint": "tsdx lint", 35 | "prepare": "tsdx build", 36 | "size": "size-limit", 37 | "analyze": "size-limit --why" 38 | }, 39 | "husky": { 40 | "hooks": { 41 | "pre-commit": "tsdx lint" 42 | } 43 | }, 44 | "author": "neooosky@gmail.com", 45 | "module": "dist/mixin-node-sdk.esm.js", 46 | "size-limit": [ 47 | { 48 | "path": "dist/mixin-node-sdk.cjs.production.min.js", 49 | "limit": "10 KB" 50 | }, 51 | { 52 | "path": "dist/mixin-node-sdk.esm.js", 53 | "limit": "10 KB" 54 | } 55 | ], 56 | "devDependencies": { 57 | "@size-limit/preset-small-lib": "^8.2.4", 58 | "@types/pako": "^1.0.2", 59 | "@types/ws": "^7.4.7", 60 | "@typescript-eslint/eslint-plugin": "^5.9.1", 61 | "@typescript-eslint/parser": "^5.9.1", 62 | "eslint": "^8.6.0", 63 | "eslint-config-prettier": "^8.3.0", 64 | "eslint-plugin-prettier": "^4.0.0", 65 | "husky": "^7.0.1", 66 | "prettier": "^2.5.1", 67 | "size-limit": "^5.0.2", 68 | "tsdx": "^0.14.1", 69 | "tslib": "^2.3.1", 70 | "typescript": "^4.5.4" 71 | }, 72 | "dependencies": { 73 | "@noble/ed25519": "^1.7.3", 74 | "@types/axios": "^0.14.0", 75 | "@types/jsonwebtoken": "^8.5.4", 76 | "@types/node-forge": "^0.10.2", 77 | "@types/uuid": "^8.3.1", 78 | "axios": "^0.21.1", 79 | "bignumber.js": "^9.0.2", 80 | "ethers": "^5.6.0", 81 | "int64-buffer": "^1.0.1", 82 | "jsonwebtoken": "^9.0.0", 83 | "node-forge": "^1.3.1", 84 | "pako": "^2.0.4", 85 | "sha3": "^2.1.4", 86 | "tweetnacl": "^1.0.3", 87 | "uuid": "^8.3.2", 88 | "ws": "^8.2.0" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/client/oauth.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { getSignPIN } from '../mixin/sign'; 3 | import { AuthData, Keystore, OauthClientRequest, Scope } from '../types'; 4 | import { gzip, ungzip } from 'pako'; 5 | import WebSocket from 'ws'; 6 | export class OauthClient implements OauthClientRequest { 7 | keystore!: Keystore; 8 | request!: AxiosInstance; 9 | newUUID!: () => string; 10 | 11 | authorizeToken(code: string, client_secret?: string, code_verifier?: string): Promise<{ access_token: string; scope: string }> { 12 | if (!client_secret) client_secret = this.keystore.client_secret; 13 | if (!client_secret) return Promise.reject(new Error('client_secret required')); 14 | return this.request.post('/oauth/token', { 15 | client_secret, 16 | code, 17 | code_verifier, 18 | client_id: this.keystore.client_id, 19 | }); 20 | } 21 | 22 | async getAuthorizeCode(params: { client_id: string; scopes?: Scope[]; pin?: string }): Promise { 23 | const { client_id, scopes: _scopes, pin } = params; 24 | const { authorization_id, scopes } = await this.getAuthorizeData(client_id, _scopes); 25 | const pin_base64 = getSignPIN(this.keystore, pin); 26 | return this.request.post('/oauth/authorize', { authorization_id, scopes, pin_base64 }); 27 | } 28 | 29 | getAuthorizeData(client_id: string, _scope?: Scope[]): Promise { 30 | return new Promise((resolve, reject) => { 31 | let ws = new WebSocket('wss://blaze.mixin.one', 'Mixin-OAuth-1'); 32 | if (!_scope) _scope = []; 33 | if (!_scope.includes('PROFILE:READ')) _scope.push('PROFILE:READ'); 34 | const scope = _scope?.join(' '); 35 | const sendRefreshCode = (authorization_id = '') => { 36 | ws.send( 37 | gzip( 38 | JSON.stringify({ 39 | id: this.newUUID().toUpperCase(), 40 | action: 'REFRESH_OAUTH_CODE', 41 | params: { client_id, scope, authorization_id, code_challenge: '' }, 42 | }), 43 | ), 44 | ); 45 | }; 46 | 47 | ws.addEventListener('message', event => { 48 | const msg = ungzip(event.data, { to: 'string' }); 49 | const authorization: { data: AuthData } = JSON.parse(msg); 50 | ws.close(); 51 | if (!authorization.data) { 52 | return reject(authorization); 53 | } 54 | return resolve(authorization.data); 55 | }); 56 | 57 | ws.addEventListener('open', () => sendRefreshCode()); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/types/collectibles.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInput } from '.'; 2 | 3 | export type CollectibleOutputState = 'unspent' | 'signed' | 'spent'; 4 | 5 | export type CollectibleAction = 'sign' | 'unlock'; 6 | 7 | export type CollectibleRequestState = 'initial' | 'signed'; 8 | 9 | export interface CollectibleOutput { 10 | type?: string; 11 | created_at?: string; 12 | updated_at?: string; 13 | user_id?: string; 14 | output_id?: string; 15 | token_id?: string; 16 | transaction_hash?: string; 17 | output_index?: number; 18 | amount?: string; 19 | senders?: string[]; 20 | senders_threshold?: number; 21 | receivers?: string[]; 22 | receivers_threshold?: number; 23 | state?: string; 24 | signed_by?: string; 25 | signed_tx?: string; 26 | } 27 | 28 | export interface CollectibleRequest { 29 | type?: string; 30 | created_at?: string; 31 | updated_at?: string; 32 | request_id?: string; 33 | user_id?: string; 34 | token_id?: string; 35 | amount?: string; 36 | senders?: string[]; 37 | senders_threshold?: number; 38 | receivers?: string[]; 39 | receivers_threshold?: number; 40 | signers?: string; 41 | action?: string; 42 | state?: string; 43 | transaction_hash?: string; 44 | raw_transaction?: string; 45 | } 46 | 47 | export interface CollectibleTokenMeta { 48 | group?: string; 49 | name?: string; 50 | description?: string; 51 | icon_url?: string; 52 | media_url?: string; 53 | mime?: string; 54 | hash?: string; 55 | } 56 | 57 | export interface CollectibleToken { 58 | type?: string; 59 | created_at?: string; 60 | token_id?: string; 61 | group?: string; 62 | token?: string; 63 | mixin_id?: string; 64 | nfo?: string; 65 | meta?: CollectibleTokenMeta; 66 | } 67 | 68 | export interface CollectiblesParams { 69 | trace_id: string; 70 | collection_id: string; 71 | token_id: string; 72 | content: string; 73 | } 74 | 75 | export interface RawCollectibleInput { 76 | output: CollectibleOutput; 77 | token: CollectibleToken; 78 | receivers: string[]; 79 | threshold: number; 80 | } 81 | 82 | export interface CollectiblesClientRequest { 83 | newMintCollectibleTransferInput: (p: CollectiblesParams) => TransactionInput; 84 | 85 | readCollectibleToken: (id: string) => Promise; 86 | readCollectibleOutputs: (members: string[], threshold: number, offset: string, limit: number) => Promise; 87 | makeCollectibleTransactionRaw: (txInput: RawCollectibleInput) => Promise; 88 | createCollectibleRequest: (action: CollectibleAction, raw: string) => Promise; 89 | signCollectibleRequest: (requestId: string, pin?: string) => Promise; 90 | cancelCollectibleRequest: (requestId: string) => Promise; 91 | unlockCollectibleRequest: (requestId: string, pin?: string) => Promise; 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bot-api-nodejs-client 2 | 3 | [中文版](./README.zh-CN.md) 4 | 5 | [中文版文档](https://liuzemei.github.io/mixin-js-sdk-docs/docs/server/intro) 6 | 7 | [中文版视频教程](https://developers.mixinbots.com/courses/1e276ee9-18fb-42e3-915a-54b049679084) 8 | 9 | The Node.js version of the mixin SDK 10 | 11 | ## New version features 12 | 13 | 1. More friendly type and code hints 14 | 2. More standardized function naming 15 | 3. More comprehensive test coverage 16 | 17 | ## Install 18 | 19 | ```shell 20 | npm install mixin-node-sdk 21 | ``` 22 | 23 | If you use `yarn` 24 | 25 | ```shell 26 | yarn add mixin-node-sdk 27 | ``` 28 | 29 | ## Usage 30 | 31 | 1. Use Mixin API 32 | 33 | ```js 34 | const { Client } = require('mixin-node-sdk'); 35 | const client = new Client({ 36 | client_id: '', 37 | session_id: '', 38 | pin_token: '', 39 | private_key: '', 40 | pin: '', 41 | client_secret: '', 42 | }); 43 | // Use Promise 44 | client.userMe().then(console.log); 45 | 46 | // use async await 47 | async function getMe() { 48 | const me = await client.userMe(); 49 | console.log(me); 50 | } 51 | ``` 52 | 53 | 2. Receive Mixin Messenger messages 54 | 55 | ```js 56 | const { BlazeClient } = require('mixin-node-sdk'); 57 | const client = new BlazeClient( 58 | { 59 | client_id: '', 60 | session_id: '', 61 | pin_token: '', 62 | private_key: '', 63 | pin: '', 64 | client_secret: '', 65 | }, 66 | { parse: true, syncAck: true }, 67 | ); 68 | 69 | client.loopBlaze({ 70 | onMessage(msg) { 71 | console.log(msg); 72 | }, 73 | onAckReceipt(msg) { 74 | console.log('ack', msg); 75 | }, 76 | }); 77 | ``` 78 | 79 | > BlazeClient directly inherits Client, so all Client methods BlazeClient can be called directly. 80 | 81 | ## Note 82 | 83 | 1. If you are using the version of `mixin-node-sdk@2.xx.xx`, please see [https://github.com/liuzemei/mixin-node-sdk/tree/v2](https://github.com/liuzemei/mixin-node-sdk/tree/v2) 84 | 85 | ## Contribute 86 | 87 | Acceptable PRs. 88 | 89 | ## Related articles or links 90 | 91 | > 1. [https://developers.mixin.one/document](https://developers.mixin.one/document) 92 | > 2. [https://github.com/fox-one/mixin-sdk-go](https://github.com/fox-one/mixin-sdk-go) 93 | 94 | ## License 95 | 96 | ``` 97 | Copyright 2021 Mixin. 98 | 99 | Licensed under the Apache License, Version 2.0 (the "License"); 100 | you may not use this file except in compliance with the License. 101 | You may obtain a copy of the License at 102 | 103 | http://www.apache.org/licenses/LICENSE-2.0 104 | 105 | Unless required by applicable law or agreed to in writing, software 106 | distributed under the License is distributed on an "AS IS" BASIS, 107 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 108 | See the License for the specific language governing permissions and 109 | limitations under the License. 110 | ``` 111 | -------------------------------------------------------------------------------- /src/client/conversation.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { request } from '../services/request'; 3 | import { ConversationClientRequest, ConversationCreateParams, Conversation, ConversationUpdateParams, Participant, ConversationAction, Keystore } from '../types'; 4 | 5 | export class ConversationClient implements ConversationClientRequest { 6 | keystore!: Keystore; 7 | request!: AxiosInstance; 8 | uniqueConversationID!: (userID: string, recipientID: string) => string; 9 | 10 | createConversation(params: ConversationCreateParams): Promise { 11 | return this.request.post('/conversations', params); 12 | } 13 | 14 | updateConversation(conversationID: string, params: ConversationUpdateParams): Promise { 15 | return this.request.post(`/conversations/${conversationID}`, params); 16 | } 17 | 18 | createContactConversation(userID: string): Promise { 19 | return this.createConversation({ 20 | category: 'CONTACT', 21 | conversation_id: this.uniqueConversationID(this.keystore.client_id, userID), 22 | participants: [{ user_id: userID }], 23 | }); 24 | } 25 | 26 | createGroupConversation(conversationID: string, name: string, participant: Participant[]): Promise { 27 | return this.createConversation({ 28 | category: 'GROUP', 29 | conversation_id: conversationID, 30 | name, 31 | participants: participant, 32 | }); 33 | } 34 | 35 | readConversation(conversationID: string): Promise { 36 | return this.request.get(`/conversations/${conversationID}`); 37 | } 38 | 39 | managerConversation(conversationID: string, action: ConversationAction, participant: Participant[]): Promise { 40 | return this.request.post(`/conversations/${conversationID}/participants/${action}`, participant); 41 | } 42 | 43 | addParticipants(conversationID: string, userIDs: string[]): Promise { 44 | var participants: Participant[] = userIDs.map(userID => ({ 45 | user_id: userID, 46 | })); 47 | return this.managerConversation(conversationID, 'ADD', participants); 48 | } 49 | 50 | removeParticipants(conversationID: string, userIDs: string[]): Promise { 51 | var participants: Participant[] = userIDs.map(userID => ({ 52 | user_id: userID, 53 | })); 54 | return this.managerConversation(conversationID, 'REMOVE', participants); 55 | } 56 | 57 | adminParticipants(conversationID: string, userIDs: string[]): Promise { 58 | var participants: Participant[] = userIDs.map(userID => ({ 59 | user_id: userID, 60 | role: 'ADMIN', 61 | })); 62 | return this.managerConversation(conversationID, 'ROLE', participants); 63 | } 64 | 65 | rotateConversation(conversationID: string): Promise { 66 | return this.request.post(`/conversations/${conversationID}/rotate`); 67 | } 68 | } 69 | 70 | export const readConversation = (token: string, conversation_id: string): Promise => request(undefined, token).get(`conversations/${conversation_id}`); 71 | -------------------------------------------------------------------------------- /src/client/multisigs.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { base64url, getSignPIN } from '../mixin/sign'; 3 | import { BigNumber } from 'bignumber.js'; 4 | import { Keystore, MultisigClientRequest, MultisigRequest, MultisigUTXO, MultisigAction, RawTransactionInput, Transaction, GhostInput, GhostKeys } from '../types'; 5 | import { DumpOutputFromGhostKey, dumpTransaction } from '../mixin/dump_transaction'; 6 | import { hashMember, newHash } from '../mixin/tools'; 7 | import { TxVersion } from '../mixin/encoder'; 8 | 9 | export class MultisigsClient implements MultisigClientRequest { 10 | keystore!: Keystore; 11 | request!: AxiosInstance; 12 | 13 | readMultisigs(offset: string, limit: number): Promise { 14 | return this.request.get(`/multisigs`, { params: { offset, limit } }); 15 | } 16 | 17 | readMultisigOutput(id: string): Promise { 18 | return this.request.get(`/multisigs/outputs/${id}`); 19 | } 20 | 21 | readMultisigOutputs(members: string[], threshold: number, offset: string, limit: number): Promise { 22 | if ((members.length > 0 && threshold < 1) || threshold > members.length) return Promise.reject(new Error('Invalid threshold or members')); 23 | const params: any = { threshold: Number(threshold), offset, limit }; 24 | params.members = hashMember(members); 25 | return this.request.get(`/multisigs/outputs`, { params }); 26 | } 27 | 28 | createMultisig(action: MultisigAction, raw: string): Promise { 29 | return this.request.post(`/multisigs/requests`, { action, raw }); 30 | } 31 | 32 | signMultisig(request_id: string, pin?: string): Promise { 33 | pin = getSignPIN(this.keystore, pin); 34 | return this.request.post(`/multisigs/requests/${request_id}/sign`, { pin }); 35 | } 36 | 37 | cancelMultisig(request_id: string): Promise { 38 | return this.request.post(`/multisigs/requests/${request_id}/cancel`); 39 | } 40 | 41 | unlockMultisig(request_id: string, pin?: string): Promise { 42 | pin = getSignPIN(this.keystore, pin); 43 | return this.request.post(`/multisigs/requests/${request_id}/unlock`, { 44 | pin, 45 | }); 46 | } 47 | 48 | readGhostKeys(receivers: string[], index: number): Promise { 49 | return this.request.post('/outputs', { receivers, index, hint: '' }); 50 | } 51 | 52 | batchReadGhostKeys(inputs: GhostInput[]): Promise { 53 | return this.request.post(`/outputs`, inputs); 54 | } 55 | 56 | async makeMultisignTransaction(txInput: RawTransactionInput): Promise { 57 | // validate ... 58 | const { inputs, memo, outputs } = txInput; 59 | const tx: Transaction = { 60 | version: TxVersion, 61 | asset: newHash(inputs[0].asset_id), 62 | extra: base64url(Buffer.from(memo)), 63 | inputs: [], 64 | outputs: [], 65 | }; 66 | // add input 67 | for (const input of inputs) { 68 | tx.inputs!.push({ 69 | hash: input.transaction_hash, 70 | index: input.output_index, 71 | }); 72 | } 73 | let change = inputs.reduce((sum, input) => sum.plus(new BigNumber(input.amount)), new BigNumber(0)); 74 | for (const output of outputs) change = change.minus(new BigNumber(output.amount)); 75 | if (change.gt(new BigNumber(0))) 76 | outputs.push({ 77 | receivers: inputs[0].members, 78 | threshold: inputs[0].threshold, 79 | amount: change.toString(), 80 | }); 81 | const ghostInputs: GhostInput[] = []; 82 | outputs.forEach((output, idx) => 83 | ghostInputs.push({ 84 | receivers: output.receivers, 85 | index: idx, 86 | hint: txInput.hint, 87 | }), 88 | ); 89 | // get ghost keys 90 | const ghosts = await this.batchReadGhostKeys(ghostInputs); 91 | outputs.forEach((output, idx) => tx.outputs!.push(DumpOutputFromGhostKey(ghosts[idx], output.amount, output.threshold))); 92 | return dumpTransaction(tx); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/client/collectibles.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { 3 | Keystore, 4 | TransactionInput, 5 | GhostInput, 6 | GhostKeys, 7 | CollectiblesClientRequest, 8 | CollectiblesParams, 9 | CollectibleToken, 10 | RawCollectibleInput, 11 | Transaction, 12 | CollectibleAction, 13 | CollectibleRequest, 14 | CollectibleOutput, 15 | } from '../types'; 16 | import { DumpOutputFromGhostKey, dumpTransaction } from '../mixin/dump_transaction'; 17 | import { hashMember } from '../mixin/tools'; 18 | import { TxVersion } from '../mixin/encoder'; 19 | import { getSignPIN } from '../mixin/sign'; 20 | import { buildMintCollectibleMemo } from '../mixin/nfo'; 21 | import { request } from '../services/request'; 22 | 23 | const MintAssetID = 'c94ac88f-4671-3976-b60a-09064f1811e8'; 24 | const MintMinimumCost = '0.001'; 25 | const GroupMembers = [ 26 | '4b188942-9fb0-4b99-b4be-e741a06d1ebf', 27 | 'dd655520-c919-4349-822f-af92fabdbdf4', 28 | '047061e6-496d-4c35-b06b-b0424a8a400d', 29 | 'acf65344-c778-41ee-bacb-eb546bacfb9f', 30 | 'a51006d0-146b-4b32-a2ce-7defbf0d7735', 31 | 'cf4abd9c-2cfa-4b5a-b1bd-e2b61a83fabd', 32 | '50115496-7247-4e2c-857b-ec8680756bee', 33 | ]; 34 | const GroupThreshold = 5; 35 | 36 | export class CollectiblesClient implements CollectiblesClientRequest { 37 | keystore!: Keystore; 38 | request!: AxiosInstance; 39 | batchReadGhostKeys!: (inputs: GhostInput[]) => Promise; 40 | 41 | newMintCollectibleTransferInput(p: CollectiblesParams): TransactionInput { 42 | const { trace_id, collection_id, token_id, content } = p; 43 | if (!trace_id || !collection_id || !token_id || !content) throw new Error('Missing parameters'); 44 | const input: TransactionInput = { 45 | asset_id: MintAssetID, 46 | amount: MintMinimumCost, 47 | trace_id, 48 | memo: buildMintCollectibleMemo(collection_id, token_id, content), 49 | opponent_multisig: { 50 | receivers: GroupMembers, 51 | threshold: GroupThreshold, 52 | }, 53 | }; 54 | return input; 55 | } 56 | 57 | readCollectibleToken(id: string): Promise { 58 | return this.request.get(`/collectibles/tokens/` + id); 59 | } 60 | 61 | readCollectibleOutputs(_members: string[], threshold: number, offset: string, limit: number): Promise { 62 | const members = hashMember(_members); 63 | return this.request.get(`/collectibles/outputs`, { 64 | params: { members, threshold, offset, limit }, 65 | }); 66 | } 67 | 68 | async makeCollectibleTransactionRaw(txInput: RawCollectibleInput): Promise { 69 | const { token, output, receivers, threshold } = txInput; 70 | const tx: Transaction = { 71 | version: TxVersion, 72 | asset: token.mixin_id!, 73 | extra: token.nfo!, 74 | inputs: [ 75 | { 76 | hash: output.transaction_hash!, 77 | index: output.output_index!, 78 | }, 79 | ], 80 | }; 81 | const ghostInputs = await this.batchReadGhostKeys([ 82 | { 83 | receivers, 84 | index: 0, 85 | hint: output.output_id!, 86 | }, 87 | ]); 88 | tx.outputs = [DumpOutputFromGhostKey(ghostInputs[0], output.amount!, threshold)]; 89 | return dumpTransaction(tx); 90 | } 91 | 92 | createCollectibleRequest(action: CollectibleAction, raw: string): Promise { 93 | return this.request.post(`/collectibles/requests`, { action, raw }); 94 | } 95 | 96 | signCollectibleRequest(requestId: string, pin?: string): Promise { 97 | pin = getSignPIN(this.keystore, pin); 98 | return this.request.post(`/collectibles/requests/${requestId}/sign`, { 99 | pin, 100 | }); 101 | } 102 | 103 | cancelCollectibleRequest(requestId: string): Promise { 104 | return this.request.post(`/collectibles/requests/${requestId}/cancel`); 105 | } 106 | 107 | unlockCollectibleRequest(requestId: string, pin?: string): Promise { 108 | pin = getSignPIN(this.keystore, pin); 109 | return this.request.post(`/collectibles/requests/${requestId}/unlock`, { 110 | pin, 111 | }); 112 | } 113 | } 114 | 115 | export const createCollectibleRequest = (token: string, action: CollectibleAction, raw: string): Promise => 116 | request(undefined, token).post(`/collectibles/requests`, { action, raw }); 117 | -------------------------------------------------------------------------------- /src/mixin/dump_msg.ts: -------------------------------------------------------------------------------- 1 | import { Session } from '../types'; 2 | import { v4 as uuid, parse as uuidParse } from 'uuid'; 3 | import { scalarMult } from 'tweetnacl'; 4 | import crypto from 'crypto'; 5 | import { Point } from '@noble/ed25519'; 6 | 7 | export function privateKeyToCurve25519(privateKey: Buffer) { 8 | const h = crypto.createHash('sha512'); 9 | h.update(privateKey.subarray(0, 32)); 10 | const digest = h.digest(); 11 | 12 | digest[0] &= 248; 13 | digest[31] &= 127; 14 | digest[31] |= 64; 15 | 16 | return digest.subarray(0, 32); 17 | } 18 | 19 | export function generateUserCheckSum(_sessions: Session[]): string { 20 | const sessions = _sessions.map(v => v.session_id); 21 | sessions.sort(); 22 | const md5 = crypto.createHash('md5'); 23 | for (const session of sessions) md5.update(session); 24 | return md5.digest('hex'); 25 | } 26 | 27 | export function decryptMessageData(_data: string, sessionID: string, _privateKey: string): string { 28 | const data = Buffer.from(_data, 'base64'); 29 | const privateKey = Buffer.from(_privateKey, 'base64'); 30 | const size = 16 + 48; 31 | const total = data.length; 32 | if (total < 1 + 2 + 32 + size + 12) throw new Error('invalid data size'); 33 | const sessionLen = data.readUInt16LE(1); 34 | const prefixSize = 35 + sessionLen * size; 35 | let key = Buffer.alloc(0); 36 | for (let i = 35; i < prefixSize; i += size) { 37 | const uid = uuid({ random: data.subarray(i, i + 16) }); 38 | if (uid === sessionID) { 39 | const pub = data.subarray(3, 35); 40 | const priv = privateKeyToCurve25519(privateKey); 41 | const dst = scalarMult(priv, pub); 42 | const iv = data.subarray(i + 16, i + 32); 43 | const decipher = crypto.createDecipheriv('aes-256-cbc', dst, iv); 44 | key = data.subarray(i + 32, i + size); 45 | key = decipher.update(key); 46 | key = Buffer.concat([key, decipher.final()]); 47 | break; 48 | } 49 | } 50 | if (key.length !== 16) throw new Error('session id not found'); 51 | const nonce = data.subarray(prefixSize, prefixSize + 12); 52 | const decipher = crypto.createDecipheriv('aes-128-gcm', key, nonce); 53 | decipher.setAuthTag(data.subarray(total - 16)); 54 | let raw = decipher.update(data.subarray(prefixSize + 12, total - 16)); 55 | raw = Buffer.concat([raw, decipher.final()]); 56 | return raw.toString('base64'); 57 | } 58 | 59 | export const encryptMessageData = (data: Buffer, sessions: Session[], privateKey: Buffer): string => { 60 | let key = crypto.randomBytes(16); 61 | let nonce = crypto.randomBytes(12); 62 | let cipher = crypto.createCipheriv('aes-128-gcm', key, nonce); 63 | const firstBlock = cipher.update(data); 64 | const lastBlock = cipher.final(); 65 | const authTag = cipher.getAuthTag(); 66 | let cipherText = Buffer.concat([firstBlock, lastBlock, authTag]); 67 | let sessionLen = Buffer.alloc(2); 68 | sessionLen.writeUInt16LE(sessions.length); 69 | let pub = Point.fromHex(privateKey.subarray(32)).toX25519(); 70 | let sessionsBytes = Buffer.from([]); 71 | for (let s of sessions) { 72 | let clientPub = Buffer.from(s.public_key!, 'base64'); 73 | const priv = privateKeyToCurve25519(privateKey); 74 | const dst = scalarMult(priv, clientPub); 75 | const iv = crypto.randomBytes(16); 76 | const padText = Buffer.alloc(16).fill(16); 77 | const shared = Buffer.concat([key, padText]); 78 | const cipherText = Buffer.alloc(16 + shared.length); 79 | cipherText.set(iv, 0); 80 | const cipher = crypto.createCipheriv('aes-256-cbc', dst, iv); 81 | let enc = cipher.update(shared); 82 | cipherText.set(enc, 16); 83 | const id = uuidParse(s.session_id); 84 | sessionsBytes = Buffer.concat([sessionsBytes, id as Buffer, cipherText]); 85 | } 86 | let result = Buffer.from([1]); 87 | result = Buffer.concat([result, sessionLen, pub, sessionsBytes, nonce, cipherText]); 88 | return result.toString('base64url'); 89 | }; 90 | 91 | export const decryptAttachment = (data: Buffer, _keys: string) => { 92 | const aesKey = Buffer.from(_keys, 'base64').subarray(0, 32); 93 | const iv = data.subarray(0, 16); 94 | let cipherText = data.subarray(16, data.byteLength - 32); 95 | 96 | const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv); 97 | cipherText = decipher.update(cipherText); 98 | cipherText = Buffer.concat([cipherText, decipher.final()]); 99 | return cipherText; 100 | }; 101 | -------------------------------------------------------------------------------- /src/client/blaze.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '../client'; 2 | import { KeystoreAuth } from '../mixin/keystore'; 3 | import { signRequest } from '../mixin/sign'; 4 | import { AcknowledgementRequest, Keystore } from '../types'; 5 | import WebSocket from 'ws'; 6 | import { BlazeMessage, MessageView } from '../types/blaze'; 7 | import { gzip, ungzip } from 'pako'; 8 | import { decryptMessageData } from '../mixin/dump_msg'; 9 | 10 | const zeromeshUrl = 'wss://mixin-blaze.zeromesh.net'; 11 | const oneUrl = 'wss://blaze.mixin.one/'; 12 | 13 | interface BlazeOptions { 14 | parse?: boolean; // parse message 15 | syncAck?: boolean; // sync ack 16 | } 17 | 18 | interface BlazeHandler { 19 | onMessage: (message: MessageView) => void | Promise; 20 | onAckReceipt?: (message: MessageView) => void | Promise; 21 | onTransfer?: (transfer: MessageView) => void | Promise; 22 | onConversation?: (conversation: MessageView) => void | Promise; 23 | } 24 | 25 | export class BlazeClient extends Client { 26 | ws?: WebSocket; 27 | h!: BlazeHandler; 28 | url = oneUrl; 29 | isAlive = false; 30 | pingInterval: any; 31 | options: BlazeOptions = { 32 | parse: false, 33 | syncAck: false, 34 | }; 35 | sendAcknowledgement!: (message: AcknowledgementRequest) => Promise; 36 | 37 | constructor(keystore?: Keystore, option?: BlazeOptions) { 38 | super(keystore); 39 | if (option) this.options = option; 40 | } 41 | 42 | loopBlaze(h: BlazeHandler) { 43 | if (!h.onMessage) throw new Error('OnMessage not set'); 44 | this.h = h; 45 | this._loopBlaze(); 46 | } 47 | 48 | _loopBlaze() { 49 | const k = new KeystoreAuth(this.keystore); 50 | const headers = { 51 | Authorization: 'Bearer ' + k.signToken(signRequest('GET', '/'), ''), 52 | }; 53 | this.ws = new WebSocket(this.url, 'Mixin-Blaze-1', { 54 | headers, 55 | handshakeTimeout: 3000, 56 | }); 57 | this.ws.onmessage = async event => { 58 | const msg = this.decode(event.data as Uint8Array); 59 | if (!msg) return; 60 | if (msg.category && msg.category.startsWith('ENCRYPTED_')) { 61 | msg.data = decryptMessageData(msg.data_base64!, this.keystore.session_id, this.keystore.private_key); 62 | } 63 | if (this.options?.parse && msg.data) { 64 | msg.data = Buffer.from(msg.data, 'base64').toString(); 65 | if (msg.data) { 66 | try { 67 | msg.data = JSON.parse(msg.data); 68 | } catch (e) {} 69 | } 70 | } 71 | if (msg.source === 'ACKNOWLEDGE_MESSAGE_RECEIPT' && this.h.onAckReceipt) await this.h.onAckReceipt(msg); 72 | else if (msg.category === 'SYSTEM_CONVERSATION' && this.h.onConversation) await this.h.onConversation(msg); 73 | else if (msg.category === 'SYSTEM_ACCOUNT_SNAPSHOT' && this.h.onTransfer) await this.h.onTransfer(msg); 74 | else await this.h.onMessage(msg); 75 | if (this.options.syncAck) await this.sendAcknowledgement({ message_id: msg.message_id!, status: 'READ' }); 76 | }; 77 | this.ws.onclose = () => { 78 | clearInterval(this.pingInterval); 79 | this._loopBlaze(); 80 | }; 81 | this.ws.onerror = e => { 82 | e.message === 'Opening handshake has timed out' && (this.url = this.url === oneUrl ? zeromeshUrl : oneUrl); 83 | }; 84 | this.ws.on('ping', () => { 85 | this.ws!.pong(); 86 | }); 87 | this.ws.on('pong', () => { 88 | this.isAlive = true; 89 | }); 90 | this.ws.onopen = () => { 91 | this.isAlive = true; 92 | this.heartbeat(); 93 | this.send_raw({ id: this.newUUID(), action: 'LIST_PENDING_MESSAGES' }); 94 | }; 95 | } 96 | 97 | heartbeat() { 98 | this.pingInterval = setInterval(() => { 99 | if (this.ws!.readyState === WebSocket.CONNECTING) return; 100 | if (!this.isAlive) return this.ws!.terminate(); 101 | this.isAlive = false; 102 | this.ws!.ping(); 103 | }, 1000 * 30); 104 | } 105 | 106 | decode(data: Uint8Array): MessageView { 107 | const t = ungzip(data, { to: 'string' }); 108 | const msgObj = JSON.parse(t); 109 | return msgObj.data; 110 | } 111 | 112 | send_raw(message: BlazeMessage) { 113 | return new Promise(resolve => { 114 | const buffer = Buffer.from(JSON.stringify(message), 'utf-8'); 115 | const zipped = gzip(buffer); 116 | if (this.ws!.readyState === WebSocket.OPEN) { 117 | this.ws!.send(zipped); 118 | resolve(true); 119 | } else { 120 | resolve(false); 121 | } 122 | }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/types/message.ts: -------------------------------------------------------------------------------- 1 | import { EncryptMessageView, MessageView } from '.'; 2 | 3 | export type MessageCategory = 4 | | 'ENCRYPTED_TEXT' 5 | | 'ENCRYPTED_AUDIO' 6 | | 'ENCRYPTED_POST' 7 | | 'ENCRYPTED_IMAGE' 8 | | 'ENCRYPTED_DATA' 9 | | 'ENCRYPTED_STICKER' 10 | | 'ENCRYPTED_LIVE' 11 | | 'ENCRYPTED_LOCATION' 12 | | 'ENCRYPTED_VIDEO' 13 | | 'ENCRYPTED_CONTACT' 14 | | 'PLAIN_TEXT' 15 | | 'PLAIN_AUDIO' 16 | | 'PLAIN_POST' 17 | | 'PLAIN_IMAGE' 18 | | 'PLAIN_DATA' 19 | | 'PLAIN_STICKER' 20 | | 'PLAIN_LIVE' 21 | | 'PLAIN_LOCATION' 22 | | 'PLAIN_VIDEO' 23 | | 'PLAIN_CONTACT' 24 | | 'APP_CARD' 25 | | 'APP_BUTTON_GROUP' 26 | | 'MESSAGE_RECALL' 27 | | 'SYSTEM_CONVERSATION' 28 | | 'SYSTEM_ACCOUNT_SNAPSHOT'; 29 | 30 | export type MessageStatus = 'SENT' | 'DELIVERED' | 'READ'; 31 | 32 | export interface RecallMessage { 33 | message_id: string; 34 | } 35 | 36 | interface EncryptMsg { 37 | key?: string; 38 | digest?: string; 39 | } 40 | 41 | export interface ImageMessage extends EncryptMsg { 42 | attachment_id: string; 43 | mime_type: string; 44 | width: number; 45 | height: number; 46 | size: number; 47 | thumbnail?: string; 48 | } 49 | 50 | export interface DataMessage extends EncryptMsg { 51 | attachment_id: string; 52 | mime_type: string; 53 | size: number; 54 | name: string; 55 | } 56 | 57 | export interface StickerMessage { 58 | sticker_id: string; 59 | name?: string; 60 | album_id?: string; 61 | } 62 | 63 | export interface ContactMessage { 64 | user_id: string; 65 | } 66 | 67 | export interface AppCardMessage { 68 | app_id: string; 69 | icon_url: string; 70 | title: string; 71 | description: string; 72 | action: string; 73 | shareable?: boolean; 74 | } 75 | 76 | export interface AudioMessage extends EncryptMsg { 77 | attachment_id: string; 78 | mime_type: string; 79 | size: number; 80 | duration: number; 81 | wave_form?: string; 82 | } 83 | 84 | export interface LiveMessage { 85 | width: number; 86 | height: number; 87 | thumb_url: string; 88 | url: string; 89 | shareable?: boolean; 90 | } 91 | 92 | export interface VideoMessage extends EncryptMsg { 93 | attachment_id: string; 94 | mime_type: string; 95 | width: number; 96 | height: number; 97 | size: number; 98 | duration: number; 99 | thumbnail?: string; 100 | } 101 | 102 | export interface Session { 103 | session_id: string; 104 | user_id?: string; 105 | public_key?: string; 106 | } 107 | 108 | export interface LocationMessage { 109 | longitude: number; 110 | latitude: number; 111 | address?: string; 112 | name?: string; 113 | } 114 | 115 | export interface AppButtonMessage { 116 | label: string; 117 | action: string; 118 | color: string; 119 | } 120 | 121 | export interface MessageRequest { 122 | conversation_id: string; 123 | message_id: string; 124 | category: MessageCategory; 125 | data?: string; 126 | data_base64?: string; 127 | recipient_id?: string; 128 | representative_id?: string; 129 | quote_message_id?: string; 130 | 131 | checksum?: string; 132 | recipient_sessions?: Session[]; 133 | } 134 | 135 | export interface AcknowledgementRequest { 136 | message_id: string; 137 | status: string; 138 | } 139 | 140 | export interface MessageClientRequest { 141 | sendAcknowledgements: (messages: AcknowledgementRequest[]) => Promise; 142 | sendAcknowledgement: (message: AcknowledgementRequest) => Promise; 143 | sendMessage: (message: MessageRequest) => Promise; 144 | sendMessages: (messages: MessageRequest[]) => Promise; 145 | 146 | sendEncryptMessage: (message: MessageRequest) => Promise; 147 | sendEncryptMessages: (messages: MessageRequest[]) => Promise; 148 | 149 | sendMessageText: (userID: string, text: string) => Promise; 150 | sendMessagePost: (userID: string, text: string) => Promise; 151 | 152 | sendTextMsg: (userID: string, text: string) => Promise; 153 | sendPostMsg: (userID: string, text: string) => Promise; 154 | sendImageMsg: (userID: string, image: ImageMessage) => Promise; 155 | sendDataMsg: (userID: string, data: DataMessage) => Promise; 156 | sendStickerMsg: (userID: string, sticker: StickerMessage) => Promise; 157 | sendContactMsg: (userID: string, contact: ContactMessage) => Promise; 158 | sendAudioMsg: (userID: string, audio: AudioMessage) => Promise; 159 | sendLiveMsg: (userID: string, live: LiveMessage) => Promise; 160 | sendVideoMsg: (userID: string, video: VideoMessage) => Promise; 161 | sendLocationMsg: (userID: string, location: LocationMessage) => Promise; 162 | 163 | sendEncryptTextMsg: (userID: string, text: string) => Promise; 164 | sendEncryptPostMsg: (userID: string, text: string) => Promise; 165 | sendEncryptImageMsg: (userID: string, image: ImageMessage) => Promise; 166 | sendEncryptDataMsg: (userID: string, data: DataMessage) => Promise; 167 | sendEncryptStickerMsg: (userID: string, sticker: StickerMessage) => Promise; 168 | sendEncryptContactMsg: (userID: string, contact: ContactMessage) => Promise; 169 | sendEncryptAudioMsg: (userID: string, audio: AudioMessage) => Promise; 170 | sendEncryptLiveMsg: (userID: string, live: LiveMessage) => Promise; 171 | sendEncryptVideoMsg: (userID: string, video: VideoMessage) => Promise; 172 | sendEncryptLocationMsg: (userID: string, location: LocationMessage) => Promise; 173 | 174 | getSessions: (userIDs: string[]) => Promise; 175 | } 176 | -------------------------------------------------------------------------------- /src/client/mvm.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInput, ContractParams, PaymentGenerateParams, MvmClientRequest, Payment, TransferInput } from '../types'; 2 | import { parse, stringify } from 'uuid'; 3 | import { Encoder } from '../mixin/encoder'; 4 | import { base64url } from '../mixin/sign'; 5 | import { BigNumber, ethers, utils } from 'ethers'; 6 | import { mvmRPCUri, registryAddress, registryProcess, registryAbi } from '../mixin/mvm'; 7 | // import axios from 'axios'; 8 | 9 | // const OperationPurposeUnknown = 0 10 | const OperationPurposeGroupEvent = 1; 11 | // const OperationPurposeAddProcess = 11 12 | // const OperationPurposeCreditProcess = 12 13 | 14 | // const mvmClient = axios.create({ 15 | // baseURL: 'https://api.test.mvm.dev', 16 | // }); 17 | 18 | const receivers = [ 19 | 'd5a3a450-5619-47af-a3b1-aad08e6e10dd', 20 | '9d4a18aa-9b0a-40ed-ba57-ce8fbbbc6deb', 21 | '2f82a56a-7fae-4bdd-bc4d-aad5005c5041', 22 | 'f7f33be1-399a-4d29-b50c-44e5f01cbb1b', 23 | '23a070df-6b87-4b66-bdd4-f009702770c9', 24 | '2385639c-eac1-4a38-a7f6-597b3f0f5b59', 25 | 'ab357ad7-8828-4173-b3bb-0600c518eab2', 26 | ]; 27 | const threshold = 5; 28 | 29 | const CNBAssetID = '965e5c6e-434c-3fa9-b780-c50f43cd955c'; 30 | const MinAmount = '0.00000001'; 31 | export const getContractByAssetID = (id: string): Promise => getRegistryContract().contracts('0x' + Buffer.from(parse(id) as Buffer).toString('hex')); 32 | 33 | export const getContractByUserIDs = (ids: string | string[], threshold?: number): Promise => { 34 | if (typeof ids === 'string') ids = [ids]; 35 | if (!threshold) threshold = ids.length; 36 | const encoder = new Encoder(Buffer.from([])); 37 | encoder.writeInt(ids.length); 38 | ids.forEach(id => encoder.writeUUID(id)); 39 | encoder.writeInt(threshold); 40 | return getRegistryContract().contracts(utils.keccak256('0x' + encoder.buf.toString('hex'))); 41 | }; 42 | 43 | export const getAssetIDByAddress = async (contract_address: string): Promise => { 44 | const registry = getRegistryContract(); 45 | let res = await registry.assets(contract_address); 46 | res instanceof BigNumber && (res = res._hex); 47 | if (res.length <= 2) return ''; 48 | res = res.slice(2); 49 | return stringify(Buffer.from(res, 'hex')); 50 | }; 51 | 52 | export const getUserIDByAddress = async (contract_address: string): Promise => { 53 | const registry = getRegistryContract(); 54 | let res = await registry.users(contract_address); 55 | res instanceof BigNumber && (res = res._hex); 56 | if (res.length <= 2) return ''; 57 | res = res.slice(6); 58 | res = res.slice(0, 32); 59 | return stringify(Buffer.from(res, 'hex')); 60 | }; 61 | 62 | const getRegistryContract = (address = registryAddress) => new ethers.Contract(address, registryAbi, new ethers.providers.JsonRpcProvider(mvmRPCUri)); 63 | 64 | const getMethodIdByAbi = (methodName: string, types: string[]): string => utils.id(methodName + '(' + types.join(',') + ')').slice(2, 10); 65 | 66 | const encodeMemo = (extra: string, process: string): string => { 67 | if (extra.startsWith('0x')) extra = extra.slice(2); 68 | const enc = new Encoder(Buffer.from([])); 69 | enc.writeInt(OperationPurposeGroupEvent); 70 | enc.writeUUID(process); 71 | enc.writeBytes(Buffer.from([])); 72 | enc.writeBytes(Buffer.from([])); 73 | enc.writeBytes(Buffer.from(extra, 'hex')); 74 | return base64url(enc.buf); 75 | }; 76 | 77 | // address 78 | const getSingleExtra = ({ address, method, types = [], values = [] }: ContractParams) => { 79 | if (types.length !== values.length) return ''; 80 | 81 | let addr = address.toLocaleLowerCase(); 82 | if (addr.startsWith('0x')) addr = addr.slice(2); 83 | let contractInput = getMethodIdByAbi(method, types); 84 | if (types.length != values.length) throw new Error('error: types.length!=values.length'); 85 | if (values.length > 0) { 86 | const abiCoder = new ethers.utils.AbiCoder(); 87 | contractInput += abiCoder.encode(types, values).slice(2); 88 | } 89 | 90 | const inputLength = Buffer.from([0, contractInput.length / 2]).toString('hex'); 91 | const extra = `${addr}${inputLength}${contractInput}`; 92 | return extra; 93 | }; 94 | 95 | /** Get extra for multiple contracts calling, started with number of contracts to be called */ 96 | const getExtra = (contracts: ContractParams[]) => { 97 | if (contracts.length === 0) return ''; 98 | let extra = Buffer.from([0, contracts.length]).toString('hex'); 99 | 100 | for (let i = 0; i < contracts.length; i++) { 101 | const singleExtra = Buffer.from(getSingleExtra(contracts[i])); 102 | extra += singleExtra; 103 | } 104 | 105 | return '0x' + extra; 106 | }; 107 | 108 | export class MvmClient implements MvmClientRequest { 109 | newUUID!: () => string; 110 | verifyPayment!: (params: TransferInput | TransactionInput) => Promise; 111 | async paymentGeneratorByContract(params: PaymentGenerateParams): Promise { 112 | if (!params.contract && !params.contracts) throw new Error('error: contract or contracts is required'); 113 | if (params.contract) params.contracts = [params.contract]; 114 | const extra = getExtra(params.contracts!); 115 | const { asset, amount, trace, type = 'payment' } = params.payment || {}; 116 | let memo = encodeMemo(extra, registryProcess).slice(2); 117 | const txInput = { 118 | asset_id: asset || CNBAssetID, 119 | amount: amount || MinAmount, 120 | trace_id: trace || this.newUUID(), 121 | memo, 122 | opponent_multisig: { receivers, threshold }, 123 | }; 124 | // if (memo.length > 200) { 125 | // // 上传的memo太长,则截取前200个字符 126 | // txInput.memo = extra; 127 | // const data = await mvmClient.post(`/payments`, txInput); 128 | // console.log(data); 129 | // return data.data; 130 | // } 131 | if (type === 'tx') return txInput; 132 | if (type === 'payment') return this.verifyPayment(txInput); 133 | throw new Error('error: type is invalid. type should be tx or payment'); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/mixin/encoder.ts: -------------------------------------------------------------------------------- 1 | import { Aggregated, Input, Output } from '../types'; 2 | import { BigNumber } from 'bignumber.js'; 3 | import { parse } from 'uuid'; 4 | 5 | const aggregatedSignaturePrefix = 0xff01; 6 | const empty = Buffer.from([0x00, 0x00]); 7 | 8 | export const magic = Buffer.from([0x77, 0x77]); 9 | export const maxEncodingInt = 0xffff; 10 | 11 | export const TxVersion = 0x02; 12 | 13 | export const OperatorSum = 0xfe; 14 | export const OperatorCmp = 0xff; 15 | 16 | export class Encoder { 17 | buf: Buffer; 18 | 19 | constructor(buf: Buffer) { 20 | this.buf = buf; 21 | } 22 | 23 | write(buf: Buffer) { 24 | this.buf = Buffer.concat([this.buf, buf]); 25 | } 26 | 27 | writeBytes(buf: Buffer) { 28 | const len = buf.byteLength; 29 | if (len > 65 * 21) { 30 | throw new Error('bytes too long. max length is 21 * 65, current length is ' + len); 31 | } 32 | 33 | this.writeInt(len); 34 | this.write(buf); 35 | } 36 | 37 | writeInt(i: number) { 38 | if (i > maxEncodingInt) { 39 | throw new Error('int overflow'); 40 | } 41 | const buf = Buffer.alloc(2); 42 | buf.writeUInt16BE(i); 43 | this.write(buf); 44 | } 45 | 46 | writeUint64(i: bigint) { 47 | const buf = Buffer.alloc(8); 48 | buf.writeBigUInt64BE(i); 49 | this.write(buf); 50 | } 51 | 52 | writeUint16(i: number) { 53 | const buf = Buffer.alloc(2); 54 | buf.writeUInt16BE(i); 55 | this.write(buf); 56 | } 57 | 58 | writeInteger(i: number) { 59 | const b = getIntBytes(i); 60 | this.writeInt(b.length); 61 | this.write(Buffer.from(b)); 62 | } 63 | 64 | writeSlice(str: Buffer) { 65 | const l = str.length; 66 | if (l > 128) throw new Error('slice too long'); 67 | this.write(Buffer.from([l])); 68 | this.write(str); 69 | } 70 | 71 | writeUUID(id: string) { 72 | const uuid: any = parse(id); 73 | for (let i = 0; i < uuid.length; i++) this.write(Buffer.from([uuid[i]])); 74 | } 75 | 76 | encodeInput(i: Input) { 77 | this.write(Buffer.from(i.hash!, 'hex')); 78 | this.writeInt(i.index!); 79 | 80 | if (!i.genesis) i.genesis = ''; 81 | this.writeInt(i.genesis.length); 82 | this.write(Buffer.from(i.genesis)); 83 | const d = i.deposit; 84 | if (typeof d === 'undefined') { 85 | this.write(empty); 86 | } else { 87 | // TODO... to test... 88 | this.write(magic); 89 | this.write(Buffer.from(d.chain, 'hex')); 90 | 91 | const asset = Buffer.from(d.asset); 92 | this.writeInt(asset.byteLength); 93 | this.write(asset); 94 | 95 | const tx = Buffer.from(d.transaction); 96 | this.writeInt(tx.byteLength); 97 | this.write(tx); 98 | 99 | this.writeUint64(d.index); 100 | this.writeInteger(d.amount); 101 | } 102 | const m = i.mint; 103 | if (typeof m === 'undefined') { 104 | this.write(empty); 105 | } else { 106 | this.write(magic); 107 | if (!m.group) m.group = ''; 108 | this.writeInt(m.group.length); 109 | this.write(Buffer.from(m.group)); 110 | 111 | this.writeUint64(m.batch); 112 | this.writeInteger(m.amount); 113 | } 114 | } 115 | 116 | encodeOutput(o: Output) { 117 | if (!o.type) o.type = 0; 118 | this.write(Buffer.from([0x00, o.type])); 119 | this.writeInteger(new BigNumber(1e8).times(new BigNumber(o.amount!)).toNumber()); 120 | this.writeInt(o.keys!.length); 121 | 122 | o.keys!.forEach(k => this.write(Buffer.from(k, 'hex'))); 123 | 124 | this.write(Buffer.from(o.mask!, 'hex')); 125 | 126 | const s = Buffer.from(o.script!, 'hex'); 127 | this.writeInt(s.byteLength); 128 | this.write(s); 129 | 130 | const w = o.withdrawal; 131 | if (typeof w === 'undefined') { 132 | this.write(empty); 133 | } else { 134 | // TODO... not check... 135 | this.write(magic); 136 | this.write(Buffer.from(w.chain, 'hex')); 137 | 138 | const asset = Buffer.from(w.asset); 139 | this.writeInt(asset.byteLength); 140 | this.write(asset); 141 | 142 | if (!w.address) w.address = ''; 143 | 144 | const addr = Buffer.from(w.address); 145 | this.writeInt(addr.byteLength); 146 | this.write(addr); 147 | 148 | const tag = Buffer.from(w.tag); 149 | this.writeInt(tag.byteLength); 150 | this.write(tag); 151 | } 152 | } 153 | 154 | encodeAggregatedSignature(js: Aggregated) { 155 | this.writeInt(maxEncodingInt); 156 | this.writeInt(aggregatedSignaturePrefix); 157 | this.write(Buffer.from(js.signature, 'hex')); 158 | 159 | if (js.signers.length === 0) { 160 | this.write(Buffer.from([0x00])); 161 | this.writeInt(0); 162 | return; 163 | } 164 | 165 | js.signers.forEach((m, i) => { 166 | if (i > 0 && m <= js.signers[i - 1]) { 167 | throw new Error('signers not sorted'); 168 | } 169 | if (m > maxEncodingInt) { 170 | throw new Error('signer overflow'); 171 | } 172 | }); 173 | 174 | const max = js.signers[js.signers.length - 1]; 175 | 176 | if (((((max / 8) | 0) + 1) | 0) > js.signature.length * 2) { 177 | // TODO... not check... 178 | this.write(Buffer.from([0x01])); 179 | this.writeInt(js.signature.length); 180 | js.signers.forEach(m => this.writeInt(m)); 181 | return; 182 | } 183 | 184 | const masks = Buffer.alloc((((max / 8) | 0) + 1) | 0); 185 | js.signers.forEach(m => (masks[(m / 8) | 0] ^= 1 << (m % 8 | 0))); 186 | this.write(Buffer.from([0x00])); 187 | this.writeInt(masks.length); 188 | this.write(masks); 189 | } 190 | 191 | encodeSignature(sm: { [key: number]: string }) { 192 | const ss = Object.keys(sm) 193 | .map((j, i) => ({ index: j, sig: sm[i] })) 194 | .sort((a, b) => Number(a.index) - Number(b.index)); 195 | 196 | this.writeInt(ss.length); 197 | ss.forEach(s => { 198 | this.writeUint16(Number(s.index)); 199 | this.write(Buffer.from(s.sig, 'hex')); 200 | }); 201 | } 202 | } 203 | 204 | function getIntBytes(x: number) { 205 | const bytes = []; 206 | do { 207 | if (x === 0) break; 208 | bytes.unshift(x & 255); 209 | x = (x / 2 ** 8) | 0; 210 | } while (true); 211 | return bytes; 212 | } 213 | -------------------------------------------------------------------------------- /src/mixin/mvm.ts: -------------------------------------------------------------------------------- 1 | export const mvmRPCUri = 'https://geth.mvm.dev'; 2 | 3 | export const registryAddress = '0x3c84B6C98FBeB813e05a7A7813F0442883450B1F'; 4 | 5 | export const registryProcess = 'bd670872-76ce-3263-b933-3aa337e212a4'; 6 | 7 | export const storageAddress = '0xef241988D19892fE4efF4935256087F4fdc5ecAa'; 8 | 9 | export const registryAbi = [ 10 | { 11 | inputs: [ 12 | { 13 | internalType: 'bytes', 14 | name: 'raw', 15 | type: 'bytes', 16 | }, 17 | { 18 | internalType: 'uint128', 19 | name: 'pid', 20 | type: 'uint128', 21 | }, 22 | ], 23 | stateMutability: 'nonpayable', 24 | type: 'constructor', 25 | }, 26 | { 27 | anonymous: false, 28 | inputs: [ 29 | { 30 | indexed: false, 31 | internalType: 'address', 32 | name: 'at', 33 | type: 'address', 34 | }, 35 | { 36 | indexed: false, 37 | internalType: 'uint256', 38 | name: 'id', 39 | type: 'uint256', 40 | }, 41 | ], 42 | name: 'AssetCreated', 43 | type: 'event', 44 | }, 45 | { 46 | anonymous: false, 47 | inputs: [ 48 | { 49 | components: [ 50 | { 51 | internalType: 'uint64', 52 | name: 'nonce', 53 | type: 'uint64', 54 | }, 55 | { 56 | internalType: 'address', 57 | name: 'user', 58 | type: 'address', 59 | }, 60 | { 61 | internalType: 'address', 62 | name: 'asset', 63 | type: 'address', 64 | }, 65 | { 66 | internalType: 'uint256', 67 | name: 'amount', 68 | type: 'uint256', 69 | }, 70 | { 71 | internalType: 'bytes', 72 | name: 'extra', 73 | type: 'bytes', 74 | }, 75 | { 76 | internalType: 'uint64', 77 | name: 'timestamp', 78 | type: 'uint64', 79 | }, 80 | { 81 | internalType: 'uint256[2]', 82 | name: 'sig', 83 | type: 'uint256[2]', 84 | }, 85 | ], 86 | indexed: false, 87 | internalType: 'struct Registry.Event', 88 | name: 'evt', 89 | type: 'tuple', 90 | }, 91 | ], 92 | name: 'MixinEvent', 93 | type: 'event', 94 | }, 95 | { 96 | anonymous: false, 97 | inputs: [ 98 | { 99 | indexed: false, 100 | internalType: 'bytes', 101 | name: '', 102 | type: 'bytes', 103 | }, 104 | ], 105 | name: 'MixinTransaction', 106 | type: 'event', 107 | }, 108 | { 109 | anonymous: false, 110 | inputs: [ 111 | { 112 | indexed: false, 113 | internalType: 'address', 114 | name: 'at', 115 | type: 'address', 116 | }, 117 | { 118 | indexed: false, 119 | internalType: 'bytes', 120 | name: 'members', 121 | type: 'bytes', 122 | }, 123 | ], 124 | name: 'UserCreated', 125 | type: 'event', 126 | }, 127 | { 128 | inputs: [ 129 | { 130 | internalType: 'uint256', 131 | name: '', 132 | type: 'uint256', 133 | }, 134 | ], 135 | name: 'GROUP', 136 | outputs: [ 137 | { 138 | internalType: 'uint256', 139 | name: '', 140 | type: 'uint256', 141 | }, 142 | ], 143 | stateMutability: 'view', 144 | type: 'function', 145 | }, 146 | { 147 | inputs: [], 148 | name: 'HALTED', 149 | outputs: [ 150 | { 151 | internalType: 'bool', 152 | name: '', 153 | type: 'bool', 154 | }, 155 | ], 156 | stateMutability: 'view', 157 | type: 'function', 158 | }, 159 | { 160 | inputs: [], 161 | name: 'INBOUND', 162 | outputs: [ 163 | { 164 | internalType: 'uint64', 165 | name: '', 166 | type: 'uint64', 167 | }, 168 | ], 169 | stateMutability: 'view', 170 | type: 'function', 171 | }, 172 | { 173 | inputs: [], 174 | name: 'OUTBOUND', 175 | outputs: [ 176 | { 177 | internalType: 'uint64', 178 | name: '', 179 | type: 'uint64', 180 | }, 181 | ], 182 | stateMutability: 'view', 183 | type: 'function', 184 | }, 185 | { 186 | inputs: [], 187 | name: 'PID', 188 | outputs: [ 189 | { 190 | internalType: 'uint128', 191 | name: '', 192 | type: 'uint128', 193 | }, 194 | ], 195 | stateMutability: 'view', 196 | type: 'function', 197 | }, 198 | { 199 | inputs: [], 200 | name: 'VERSION', 201 | outputs: [ 202 | { 203 | internalType: 'uint256', 204 | name: '', 205 | type: 'uint256', 206 | }, 207 | ], 208 | stateMutability: 'view', 209 | type: 'function', 210 | }, 211 | { 212 | inputs: [ 213 | { 214 | internalType: 'uint256', 215 | name: '', 216 | type: 'uint256', 217 | }, 218 | ], 219 | name: 'addresses', 220 | outputs: [ 221 | { 222 | internalType: 'address', 223 | name: '', 224 | type: 'address', 225 | }, 226 | ], 227 | stateMutability: 'view', 228 | type: 'function', 229 | }, 230 | { 231 | inputs: [ 232 | { 233 | internalType: 'address', 234 | name: '', 235 | type: 'address', 236 | }, 237 | ], 238 | name: 'assets', 239 | outputs: [ 240 | { 241 | internalType: 'uint128', 242 | name: '', 243 | type: 'uint128', 244 | }, 245 | ], 246 | stateMutability: 'view', 247 | type: 'function', 248 | }, 249 | { 250 | inputs: [ 251 | { 252 | internalType: 'uint128', 253 | name: '', 254 | type: 'uint128', 255 | }, 256 | ], 257 | name: 'balances', 258 | outputs: [ 259 | { 260 | internalType: 'uint256', 261 | name: '', 262 | type: 'uint256', 263 | }, 264 | ], 265 | stateMutability: 'view', 266 | type: 'function', 267 | }, 268 | { 269 | inputs: [ 270 | { 271 | internalType: 'address', 272 | name: 'user', 273 | type: 'address', 274 | }, 275 | { 276 | internalType: 'uint256', 277 | name: 'amount', 278 | type: 'uint256', 279 | }, 280 | ], 281 | name: 'burn', 282 | outputs: [ 283 | { 284 | internalType: 'bool', 285 | name: '', 286 | type: 'bool', 287 | }, 288 | ], 289 | stateMutability: 'nonpayable', 290 | type: 'function', 291 | }, 292 | { 293 | inputs: [ 294 | { 295 | internalType: 'address', 296 | name: 'asset', 297 | type: 'address', 298 | }, 299 | { 300 | internalType: 'uint256', 301 | name: 'amount', 302 | type: 'uint256', 303 | }, 304 | ], 305 | name: 'claim', 306 | outputs: [ 307 | { 308 | internalType: 'bool', 309 | name: '', 310 | type: 'bool', 311 | }, 312 | ], 313 | stateMutability: 'nonpayable', 314 | type: 'function', 315 | }, 316 | { 317 | inputs: [ 318 | { 319 | internalType: 'uint256', 320 | name: '', 321 | type: 'uint256', 322 | }, 323 | ], 324 | name: 'contracts', 325 | outputs: [ 326 | { 327 | internalType: 'address', 328 | name: '', 329 | type: 'address', 330 | }, 331 | ], 332 | stateMutability: 'view', 333 | type: 'function', 334 | }, 335 | { 336 | inputs: [ 337 | { 338 | internalType: 'uint256', 339 | name: '', 340 | type: 'uint256', 341 | }, 342 | ], 343 | name: 'deposits', 344 | outputs: [ 345 | { 346 | internalType: 'uint128', 347 | name: '', 348 | type: 'uint128', 349 | }, 350 | ], 351 | stateMutability: 'view', 352 | type: 'function', 353 | }, 354 | { 355 | inputs: [ 356 | { 357 | internalType: 'bytes', 358 | name: 'raw', 359 | type: 'bytes', 360 | }, 361 | ], 362 | name: 'evolve', 363 | outputs: [], 364 | stateMutability: 'nonpayable', 365 | type: 'function', 366 | }, 367 | { 368 | inputs: [ 369 | { 370 | internalType: 'bytes', 371 | name: 'raw', 372 | type: 'bytes', 373 | }, 374 | ], 375 | name: 'halt', 376 | outputs: [], 377 | stateMutability: 'nonpayable', 378 | type: 'function', 379 | }, 380 | { 381 | inputs: [ 382 | { 383 | internalType: 'bytes', 384 | name: 'raw', 385 | type: 'bytes', 386 | }, 387 | ], 388 | name: 'iterate', 389 | outputs: [], 390 | stateMutability: 'nonpayable', 391 | type: 'function', 392 | }, 393 | { 394 | inputs: [ 395 | { 396 | internalType: 'bytes', 397 | name: 'raw', 398 | type: 'bytes', 399 | }, 400 | ], 401 | name: 'mixin', 402 | outputs: [ 403 | { 404 | internalType: 'bool', 405 | name: '', 406 | type: 'bool', 407 | }, 408 | ], 409 | stateMutability: 'nonpayable', 410 | type: 'function', 411 | }, 412 | { 413 | inputs: [ 414 | { 415 | internalType: 'address', 416 | name: '', 417 | type: 'address', 418 | }, 419 | ], 420 | name: 'users', 421 | outputs: [ 422 | { 423 | internalType: 'bytes', 424 | name: '', 425 | type: 'bytes', 426 | }, 427 | ], 428 | stateMutability: 'view', 429 | type: 'function', 430 | }, 431 | { 432 | inputs: [ 433 | { 434 | internalType: 'address', 435 | name: '_storageContract', 436 | type: 'address', 437 | }, 438 | { 439 | internalType: 'bytes32', 440 | name: '_key', 441 | type: 'bytes32', 442 | }, 443 | { 444 | internalType: 'bytes', 445 | name: 'raw', 446 | type: 'bytes', 447 | }, 448 | ], 449 | name: 'writeValue', 450 | outputs: [], 451 | stateMutability: 'nonpayable', 452 | type: 'function', 453 | }, 454 | ]; 455 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/client/message.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { writeFileSync, mkdirSync, existsSync } from 'fs'; 3 | import { resolve } from 'path'; 4 | import { 5 | AcknowledgementRequest, 6 | Keystore, 7 | MessageCategory, 8 | MessageClientRequest, 9 | MessageRequest, 10 | MessageView, 11 | ImageMessage, 12 | DataMessage, 13 | StickerMessage, 14 | ContactMessage, 15 | AppCardMessage, 16 | AudioMessage, 17 | LiveMessage, 18 | LocationMessage, 19 | VideoMessage, 20 | AppButtonMessage, 21 | RecallMessage, 22 | Session, 23 | EncryptMessageView, 24 | Attachment, 25 | } from '../types'; 26 | import { decryptAttachment, encryptMessageData, generateUserCheckSum } from '../mixin/dump_msg'; 27 | import { getFileByURL } from '../mixin/tools'; 28 | 29 | let _sessionCache: { [key: string]: Session[] } = {}; 30 | 31 | export class MessageClient implements MessageClientRequest { 32 | keystore!: Keystore; 33 | request!: AxiosInstance; 34 | newUUID!: () => string; 35 | uniqueConversationID!: (userID: string, recipientID: string) => string; 36 | showAttachment!: (attachment_id: string) => Promise; 37 | 38 | sendAcknowledgements(messages: AcknowledgementRequest[]): Promise { 39 | return this.request.post('/acknowledgements', messages); 40 | } 41 | 42 | sendAcknowledgement(message: AcknowledgementRequest): Promise { 43 | return this.sendAcknowledgements([message]); 44 | } 45 | 46 | sendMessage(message: MessageRequest): Promise { 47 | return this.request.post('/messages', message); 48 | } 49 | 50 | sendMessages(messages: MessageRequest[]): Promise { 51 | return this.request.post('/messages', messages); 52 | } 53 | 54 | sendMsg(recipient_id: string, category: MessageCategory, data: any): Promise { 55 | if (typeof data === 'object') data = JSON.stringify(data); 56 | return this.sendMessage({ 57 | category, 58 | recipient_id, 59 | conversation_id: this.uniqueConversationID(this.keystore.client_id, recipient_id), 60 | message_id: this.newUUID(), 61 | data: Buffer.from(data).toString('base64url'), 62 | }); 63 | } 64 | 65 | sendMessageText(userID: string, text: string): Promise { 66 | return this.sendMsg(userID, 'PLAIN_TEXT', text); 67 | } 68 | 69 | sendMessagePost(userID: string, text: string): Promise { 70 | return this.sendMsg(userID, 'PLAIN_POST', text); 71 | } 72 | 73 | sendTextMsg(userID: string, text: string): Promise { 74 | return this.sendMsg(userID, 'PLAIN_TEXT', text); 75 | } 76 | 77 | sendPostMsg(userID: string, text: string): Promise { 78 | return this.sendMsg(userID, 'PLAIN_POST', text); 79 | } 80 | 81 | sendImageMsg(userID: string, image: ImageMessage): Promise { 82 | return this.sendMsg(userID, 'PLAIN_IMAGE', image); 83 | } 84 | 85 | sendDataMsg(userID: string, data: DataMessage): Promise { 86 | return this.sendMsg(userID, 'PLAIN_DATA', data); 87 | } 88 | 89 | sendStickerMsg(userID: string, sticker: StickerMessage): Promise { 90 | return this.sendMsg(userID, 'PLAIN_STICKER', sticker); 91 | } 92 | 93 | sendContactMsg(userID: string, contact: ContactMessage): Promise { 94 | return this.sendMsg(userID, 'PLAIN_CONTACT', contact); 95 | } 96 | 97 | sendAppCardMsg(userID: string, appCard: AppCardMessage): Promise { 98 | return this.sendMsg(userID, 'APP_CARD', appCard); 99 | } 100 | 101 | sendAudioMsg(userID: string, audio: AudioMessage): Promise { 102 | return this.sendMsg(userID, 'PLAIN_AUDIO', audio); 103 | } 104 | 105 | sendLiveMsg(userID: string, live: LiveMessage): Promise { 106 | return this.sendMsg(userID, 'PLAIN_LIVE', live); 107 | } 108 | 109 | sendVideoMsg(userID: string, video: VideoMessage): Promise { 110 | return this.sendMsg(userID, 'PLAIN_VIDEO', video); 111 | } 112 | 113 | sendLocationMsg(userID: string, location: LocationMessage): Promise { 114 | return this.sendMsg(userID, 'PLAIN_LOCATION', location); 115 | } 116 | 117 | sendAppButtonMsg(userID: string, appButton: AppButtonMessage[]): Promise { 118 | return this.sendMsg(userID, 'APP_BUTTON_GROUP', appButton); 119 | } 120 | 121 | sendRecallMsg(userID: string, message: RecallMessage): Promise { 122 | return this.sendMsg(userID, 'MESSAGE_RECALL', message); 123 | } 124 | 125 | sendEncryptMessage(message: MessageRequest): Promise { 126 | return this._sendEncryptMsg(message.recipient_id!, message.category, message.data, false, message.message_id, message.conversation_id); 127 | } 128 | 129 | async _sendEncryptMsg(userID: string, category: MessageCategory, data: any, isRetry = false, message_id?: string, conversation_id?: string): Promise { 130 | if (!category.startsWith('ENCRYPTED_')) return Promise.reject('category must start with ENCRYPTED_'); 131 | if (typeof data === 'object') data = JSON.stringify(data); 132 | const sessions = await this.getSessionsWithCache([userID]); 133 | if (sessions[userID].length === 0) return Promise.reject(`${userID} has no active session`); 134 | const data_base64 = encryptMessageData(Buffer.from(data), sessions[userID], Buffer.from(this.keystore.private_key, 'base64')); 135 | const checksum = generateUserCheckSum(sessions[userID] || []); 136 | const recipient_sessions = sessions[userID].map(v => ({ session_id: v.session_id })); 137 | if (!message_id) message_id = this.newUUID(); 138 | if (!conversation_id) conversation_id = this.uniqueConversationID(this.keystore.client_id, userID); 139 | const [res] = await this.sendEncryptMessagesRaw([{ category, recipient_id: userID, conversation_id, message_id, data_base64, checksum, recipient_sessions }]); 140 | if (res.state === 'SUCCESS' || res.sessions.length === 0 || isRetry) return res; 141 | this.cacheSession(res.sessions); 142 | return this._sendEncryptMsg(userID, category, data, true, message_id); 143 | } 144 | 145 | sendEncryptMessagesRaw(messages: MessageRequest[]): Promise { 146 | return this.request.post('/encrypted_messages', messages); 147 | } 148 | 149 | sendEncryptTextMsg(userID: string, text: string): Promise { 150 | return this._sendEncryptMsg(userID, 'ENCRYPTED_TEXT', text); 151 | } 152 | 153 | sendEncryptPostMsg(userID: string, text: string): Promise { 154 | return this._sendEncryptMsg(userID, 'ENCRYPTED_POST', text); 155 | } 156 | 157 | sendEncryptImageMsg(userID: string, image: ImageMessage): Promise { 158 | return this._sendEncryptMsg(userID, 'ENCRYPTED_IMAGE', image); 159 | } 160 | 161 | sendEncryptDataMsg(userID: string, data: DataMessage): Promise { 162 | return this._sendEncryptMsg(userID, 'ENCRYPTED_DATA', data); 163 | } 164 | 165 | sendEncryptStickerMsg(userID: string, sticker: StickerMessage): Promise { 166 | return this._sendEncryptMsg(userID, 'ENCRYPTED_STICKER', sticker); 167 | } 168 | 169 | sendEncryptContactMsg(userID: string, contact: ContactMessage): Promise { 170 | return this._sendEncryptMsg(userID, 'ENCRYPTED_CONTACT', contact); 171 | } 172 | 173 | sendEncryptAudioMsg(userID: string, audio: AudioMessage): Promise { 174 | return this._sendEncryptMsg(userID, 'ENCRYPTED_AUDIO', audio); 175 | } 176 | 177 | sendEncryptLiveMsg(userID: string, live: LiveMessage): Promise { 178 | return this._sendEncryptMsg(userID, 'ENCRYPTED_LIVE', live); 179 | } 180 | 181 | sendEncryptVideoMsg(userID: string, video: VideoMessage): Promise { 182 | return this._sendEncryptMsg(userID, 'ENCRYPTED_VIDEO', video); 183 | } 184 | 185 | sendEncryptLocationMsg(userID: string, location: LocationMessage): Promise { 186 | return this._sendEncryptMsg(userID, 'ENCRYPTED_LOCATION', location); 187 | } 188 | 189 | async sendEncryptMessages(_messages: MessageRequest[]): Promise { 190 | return this._sendEncryptMsgs(_messages); 191 | } 192 | 193 | async _sendEncryptMsgs(_messages: MessageRequest[], isRetry = false, resp: EncryptMessageView[] = []): Promise { 194 | let userIDs = _messages.map(v => v.recipient_id!); 195 | const sessions = await this.getSessionsWithCache(userIDs); 196 | const msgs: MessageRequest[] = []; 197 | const msgMap = new Map(); 198 | for (const msg of _messages) { 199 | msgMap.set(msg.message_id!, msg); 200 | let { category, recipient_id, data, message_id, conversation_id } = msg; 201 | if (!category.startsWith('ENCRYPTED_')) throw new Error('category must start with ENCRYPTED_'); 202 | let session = sessions[recipient_id!]; 203 | if (session.length === 0) continue; 204 | const data_base64 = encryptMessageData(Buffer.from(data!), session, Buffer.from(this.keystore.private_key, 'base64')); 205 | const checksum = generateUserCheckSum(session || []); 206 | const recipient_sessions = session.map(v => ({ session_id: v.session_id })); 207 | if (!message_id) message_id = this.newUUID(); 208 | if (!conversation_id) conversation_id = this.uniqueConversationID(this.keystore.client_id, recipient_id!); 209 | msgs.push({ category, recipient_id, conversation_id, message_id, data_base64, checksum, recipient_sessions }); 210 | } 211 | if (msgs.length === 0) return resp; 212 | 213 | const res = await this.sendEncryptMessagesRaw(msgs); 214 | let unsent: MessageRequest[] = []; 215 | for (const s of res) { 216 | if (s.state === 'SUCCESS' || isRetry) resp.push(s); 217 | else { 218 | this.cacheSession(s.sessions); 219 | unsent.push(msgMap.get(s.message_id)!); 220 | } 221 | } 222 | if (unsent.length === 0 || isRetry) return resp; 223 | return this._sendEncryptMsgs(unsent, true, resp); 224 | } 225 | 226 | async getSessions(userIDs: string[]): Promise { 227 | return this.request.post('/sessions/fetch', userIDs); 228 | } 229 | 230 | async getSessionsWithCache(userIDs: string[]): Promise<{ [key: string]: Session[] }> { 231 | let needFetch: string[] = []; 232 | let result: { [key: string]: Session[] } = {}; 233 | for (const userID of userIDs) { 234 | const session = this.getSessionByCache(userID); 235 | if (session) { 236 | result[userID] = session; 237 | continue; 238 | } else { 239 | needFetch.push(userID); 240 | } 241 | } 242 | if (needFetch.length === 0) return result; 243 | const sessions: Session[] = await this.request.post('/sessions/fetch', needFetch); 244 | 245 | const _result = this.cacheSession(sessions); 246 | result = { ...result, ..._result }; 247 | return result; 248 | } 249 | 250 | cacheSession(sessions: Session[]) { 251 | if (!existsSync('.sessions')) mkdirSync('.sessions'); 252 | let result: { [key: string]: Session[] } = {}; 253 | sessions.forEach(session => { 254 | if (!session.user_id) return; 255 | if (!result[session.user_id]) result[session.user_id] = []; 256 | result[session.user_id].push(session); 257 | }); 258 | for (const userID in result) { 259 | _sessionCache[userID] = result[userID]; 260 | const sessionPath = resolve(process.cwd(), '.sessions', `${userID}.json`); 261 | writeFileSync(sessionPath, JSON.stringify(result[userID])); 262 | } 263 | return result; 264 | } 265 | 266 | async decryptAttachmentByMsgData(msgData: { attachment_id: string; key?: string }): Promise { 267 | if (typeof msgData !== 'object') return Promise.reject(`msgData should be an object...`); 268 | if (!msgData.attachment_id) return Promise.reject(`msgData should contain attachment_id...`); 269 | const { view_url } = await this.showAttachment(msgData.attachment_id); 270 | const raw = await getFileByURL(view_url); 271 | if (!msgData.key) return raw; 272 | return decryptAttachment(raw, msgData.key); 273 | } 274 | 275 | getSessionByCache(userID: string): Session[] | undefined { 276 | return _sessionCache[userID]; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | import { v4 as uuid } from 'uuid'; 3 | import { AxiosInstance } from 'axios'; 4 | import { mixinRequest, request } from '../services/request'; 5 | import { UserClient } from './user'; 6 | import { AddressClient } from './address'; 7 | import { 8 | AddressClientRequest, 9 | AddressCreateParams, 10 | Address, 11 | AppClientRequest, 12 | UpdateAppRequest, 13 | App, 14 | FavoriteApp, 15 | AssetClientRequest, 16 | Asset, 17 | ExchangeRate, 18 | NetworkTicker, 19 | Attachment, 20 | AttachmentClientRequest, 21 | CollectiblesParams, 22 | CollectibleToken, 23 | CollectibleAction, 24 | CollectibleRequest, 25 | CollectibleOutput, 26 | RawCollectibleInput, 27 | ConversationClientRequest, 28 | ConversationCreateParams, 29 | Conversation, 30 | ConversationUpdateParams, 31 | Participant, 32 | ConversationAction, 33 | MessageClientRequest, 34 | AcknowledgementRequest, 35 | MessageRequest, 36 | MessageView, 37 | ImageMessage, 38 | DataMessage, 39 | StickerMessage, 40 | ContactMessage, 41 | AppCardMessage, 42 | AudioMessage, 43 | LiveMessage, 44 | LocationMessage, 45 | VideoMessage, 46 | AppButtonMessage, 47 | RecallMessage, 48 | TransactionInput, 49 | RawTransactionInput, 50 | MultisigClientRequest, 51 | MultisigRequest, 52 | MultisigUTXO, 53 | PINClientRequest, 54 | Turn, 55 | SnapshotClientRequest, 56 | Snapshot, 57 | SnapshotQuery, 58 | TransferClientRequest, 59 | TransferInput, 60 | Payment, 61 | GhostInput, 62 | GhostKeys, 63 | WithdrawInput, 64 | RawTransaction, 65 | UserClientRequest, 66 | User, 67 | UserRelationship, 68 | Keystore, 69 | MvmClientRequest, 70 | PaymentGenerateParams, 71 | OauthClientRequest, 72 | AuthData, 73 | Scope, 74 | EncryptMessageView, 75 | Session, 76 | } from '../types'; 77 | import { AppClient } from './app'; 78 | import { AssetClient } from './asset'; 79 | import { AttachmentClient } from './attachment'; 80 | import { ConversationClient } from './conversation'; 81 | import { MessageClient } from './message'; 82 | import { MultisigsClient } from './multisigs'; 83 | import { PINClient } from './pin'; 84 | import { SnapshotClient } from './snapshot'; 85 | import { TransferClient } from './transfer'; 86 | import { OauthClient } from './oauth'; 87 | 88 | export { verifyPayment } from './transfer'; 89 | import { CollectiblesClient } from './collectibles'; 90 | import { MvmClient } from './mvm'; 91 | 92 | export * from './mvm'; 93 | 94 | export class Client 95 | implements 96 | AddressClientRequest, 97 | AppClientRequest, 98 | AssetClientRequest, 99 | AttachmentClientRequest, 100 | CollectiblesClient, 101 | ConversationClientRequest, 102 | MessageClientRequest, 103 | MultisigClientRequest, 104 | PINClientRequest, 105 | SnapshotClientRequest, 106 | TransferClientRequest, 107 | UserClientRequest, 108 | MvmClientRequest, 109 | OauthClientRequest 110 | { 111 | request: AxiosInstance; 112 | keystore: Keystore; 113 | 114 | constructor(keystore?: Keystore, token?: string) { 115 | if (!keystore && !token) throw new Error('keystore or token required'); 116 | this.keystore = keystore!; 117 | this.request = request(keystore, token); 118 | } 119 | 120 | // Address... 121 | createAddress!: (params: AddressCreateParams, pin?: string) => Promise
; 122 | readAddress!: (address_id: string) => Promise
; 123 | readAddresses!: (asset_id: string) => Promise; 124 | deleteAddress!: (address_id: string, pin?: string) => Promise; 125 | 126 | // App... 127 | updateApp!: (appID: string, params: UpdateAppRequest) => Promise; 128 | readFavoriteApps!: (userID: string) => Promise; 129 | favoriteApp!: (appID: string) => Promise; 130 | unfavoriteApp!: (appID: string) => Promise; 131 | 132 | // Asset... 133 | readAsset!: (asset_id: string) => Promise; 134 | readAssets!: () => Promise; 135 | readAssetFee!: (asset_id: string) => Promise; 136 | readAssetNetworkTicker!: (asset_id: string, offset?: string) => Promise; 137 | 138 | readExchangeRates!: () => Promise; 139 | 140 | // Attachment... 141 | createAttachment!: () => Promise; 142 | showAttachment!: (attachment_id: string) => Promise; 143 | uploadFile!: (file: File) => Promise; 144 | 145 | // Collectibles... 146 | newMintCollectibleTransferInput!: (p: CollectiblesParams) => TransactionInput; 147 | 148 | readCollectibleToken!: (id: string) => Promise; 149 | readCollectibleOutputs!: (members: string[], threshold: number, offset: string, limit: number) => Promise; 150 | makeCollectibleTransactionRaw!: (txInput: RawCollectibleInput) => Promise; 151 | createCollectibleRequest!: (action: CollectibleAction, raw: string) => Promise; 152 | signCollectibleRequest!: (requestId: string, pin?: string) => Promise; 153 | cancelCollectibleRequest!: (requestId: string) => Promise; 154 | unlockCollectibleRequest!: (requestId: string, pin?: string) => Promise; 155 | 156 | // Conversation... 157 | createConversation!: (params: ConversationCreateParams) => Promise; 158 | updateConversation!: (conversationID: string, params: ConversationUpdateParams) => Promise; 159 | createContactConversation!: (userID: string) => Promise; 160 | createGroupConversation!: (conversationID: string, name: string, participant: Participant[]) => Promise; 161 | readConversation!: (conversationID: string) => Promise; 162 | managerConversation!: (conversationID: string, action: ConversationAction, participant: Participant[]) => Promise; 163 | addParticipants!: (conversationID: string, userIDs: string[]) => Promise; 164 | removeParticipants!: (conversationID: string, userIDs: string[]) => Promise; 165 | adminParticipants!: (conversationID: string, userIDs: string[]) => Promise; 166 | rotateConversation!: (conversationID: string) => Promise; 167 | 168 | // Message... 169 | sendAcknowledgements!: (messages: AcknowledgementRequest[]) => Promise; 170 | sendAcknowledgement!: (message: AcknowledgementRequest) => Promise; 171 | sendMessage!: (message: MessageRequest) => Promise; 172 | sendMessages!: (messages: MessageRequest[]) => Promise; 173 | sendMessageText!: (userID: string, text: string) => Promise; 174 | sendMessagePost!: (userID: string, text: string) => Promise; 175 | sendTextMsg!: (userID: string, text: string) => Promise; 176 | sendPostMsg!: (userID: string, text: string) => Promise; 177 | sendImageMsg!: (userID: string, image: ImageMessage) => Promise; 178 | sendDataMsg!: (userID: string, data: DataMessage) => Promise; 179 | sendStickerMsg!: (userID: string, sticker: StickerMessage) => Promise; 180 | sendContactMsg!: (userID: string, contact: ContactMessage) => Promise; 181 | sendAppCardMsg!: (userID: string, appCard: AppCardMessage) => Promise; 182 | sendAudioMsg!: (userID: string, audio: AudioMessage) => Promise; 183 | sendLiveMsg!: (userID: string, live: LiveMessage) => Promise; 184 | sendVideoMsg!: (userID: string, video: VideoMessage) => Promise; 185 | sendLocationMsg!: (userID: string, location: LocationMessage) => Promise; 186 | sendAppButtonMsg!: (userID: string, appButton: AppButtonMessage[]) => Promise; 187 | sendRecallMsg!: (userID: string, message: RecallMessage) => Promise; 188 | 189 | sendEncryptMessage!: (message: MessageRequest) => Promise; 190 | sendEncryptMessages!: (messages: MessageRequest[]) => Promise; 191 | sendEncryptTextMsg!: (userID: string, text: string) => Promise; 192 | sendEncryptPostMsg!: (userID: string, text: string) => Promise; 193 | sendEncryptImageMsg!: (userID: string, image: ImageMessage) => Promise; 194 | sendEncryptDataMsg!: (userID: string, data: DataMessage) => Promise; 195 | sendEncryptStickerMsg!: (userID: string, sticker: StickerMessage) => Promise; 196 | sendEncryptContactMsg!: (userID: string, contact: ContactMessage) => Promise; 197 | sendEncryptAudioMsg!: (userID: string, audio: AudioMessage) => Promise; 198 | sendEncryptLiveMsg!: (userID: string, live: LiveMessage) => Promise; 199 | sendEncryptVideoMsg!: (userID: string, video: VideoMessage) => Promise; 200 | sendEncryptLocationMsg!: (userID: string, location: LocationMessage) => Promise; 201 | 202 | getSessions!: (userIDs: string[]) => Promise; 203 | 204 | // Multisigs... 205 | readMultisigs!: (offset: string, limit: number) => Promise; 206 | readMultisigOutput!: (id: string) => Promise; 207 | readMultisigOutputs!: (members: string[], threshold: number, offset: string, limit: number) => Promise; 208 | createMultisig!: (action: string, raw: string) => Promise; 209 | signMultisig!: (request_id: string, pin?: string) => Promise; 210 | cancelMultisig!: (request_id: string) => Promise; 211 | unlockMultisig!: (request_id: string, pin?: string) => Promise; 212 | sendRawTransaction!: (raw: string) => Promise<{ hash: string }>; 213 | readGhostKeys!: (receivers: string[], index: number) => Promise; 214 | batchReadGhostKeys!: (inputs: GhostInput[]) => Promise; 215 | makeMultisignTransaction!: (txInput: RawTransactionInput) => Promise; 216 | 217 | // Mvm... 218 | paymentGeneratorByContract!: (params: PaymentGenerateParams) => Promise; 219 | 220 | // Pin... 221 | verifyPin!: (pin: string) => Promise; 222 | modifyPin!: (pin: string, oldPin?: string) => Promise; 223 | readTurnServers!: () => Promise; 224 | 225 | // Snapshot... 226 | readSnapshots!: (params?: SnapshotQuery) => Promise; 227 | readNetworkSnapshots!: (params?: SnapshotQuery) => Promise; 228 | readSnapshot!: (snapshot_id: string) => Promise; 229 | readNetworkSnapshot!: (snapshot_id: string) => Promise; 230 | 231 | // Transfer... 232 | verifyPayment!: (params: TransferInput | TransactionInput) => Promise; 233 | transfer!: (params: TransferInput, pin?: string) => Promise; 234 | readTransfer!: (trace_id: string) => Promise; 235 | transaction!: (params: TransactionInput, pin?: string) => Promise; 236 | withdraw!: (params: WithdrawInput, pin?: string) => Promise; 237 | 238 | // User... 239 | userMe!: () => Promise; 240 | readUser!: (userIdOrIdentityNumber: string) => Promise; 241 | readUsers!: (userIDs: string[]) => Promise; 242 | searchUser!: (identityNumberOrPhone: string) => Promise; 243 | readFriends!: () => Promise; 244 | createUser!: (full_name: string, session_secret?: string) => Promise; 245 | modifyProfile!: (full_name?: string, avatar_base64?: string) => Promise; 246 | modifyRelationships!: (relationship: UserRelationship) => Promise; 247 | readBlockUsers!: () => Promise; 248 | 249 | // Oauth... 250 | authorizeToken!: (code: string, client_secret?: string, code_verifier?: string) => Promise<{ access_token: string; scope: string }>; 251 | getAuthorizeCode!: (params: { client_id: string; scopes?: Scope[]; pin?: string }) => Promise; 252 | 253 | newUUID(): string { 254 | return uuid(); 255 | } 256 | 257 | uniqueConversationID(userID: string, recipientID: string): string { 258 | let [minId, maxId] = [userID, recipientID]; 259 | if (minId > maxId) { 260 | [minId, maxId] = [recipientID, userID]; 261 | } 262 | 263 | const hash = crypto.createHash('md5'); 264 | hash.update(minId); 265 | hash.update(maxId); 266 | const bytes = hash.digest(); 267 | 268 | bytes[6] = (bytes[6] & 0x0f) | 0x30; 269 | bytes[8] = (bytes[8] & 0x3f) | 0x80; 270 | 271 | const digest = Array.from(bytes, byte => `0${(byte & 0xff).toString(16)}`.slice(-2)).join(''); 272 | return `${digest.slice(0, 8)}-${digest.slice(8, 12)}-${digest.slice(12, 16)}-${digest.slice(16, 20)}-${digest.slice(20, 32)}`; 273 | } 274 | } 275 | 276 | [ 277 | AddressClient, 278 | AppClient, 279 | AssetClient, 280 | AttachmentClient, 281 | ConversationClient, 282 | MessageClient, 283 | MultisigsClient, 284 | PINClient, 285 | SnapshotClient, 286 | TransferClient, 287 | UserClient, 288 | CollectiblesClient, 289 | MvmClient, 290 | OauthClient, 291 | ].forEach(client => _extends(Client, client)); 292 | 293 | function _extends(origin: any, target: any) { 294 | for (const key in target.prototype) { 295 | origin.prototype[key] = target.prototype[key]; 296 | } 297 | } 298 | 299 | export const authorizeToken = (client_id: string, code: string, client_secret: string, code_verifier?: string): Promise<{ access_token: string; scope: string }> => 300 | mixinRequest.post('/oauth/token', { 301 | client_id, 302 | code, 303 | code_verifier, 304 | client_secret, 305 | }); 306 | -------------------------------------------------------------------------------- /src/mixin/sign.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes, createCipheriv, createHash } from 'crypto'; 2 | import { pki, util, md } from 'node-forge'; 3 | import { Uint64LE } from 'int64-buffer'; 4 | import { Keystore } from '../types'; 5 | 6 | export const getSignPIN = (keystore: Keystore, pin?: any, iterator?: any) => { 7 | const { session_id, private_key, pin_token, pin: _pin } = keystore; 8 | pin = pin || _pin; 9 | if (!pin) throw new Error('PIN is required'); 10 | const blockSize = 16; 11 | 12 | let _privateKey: any = toBuffer(private_key, 'base64'); 13 | let pinKey = _privateKey.length === 64 ? signEncryptEd25519PIN(pin_token, _privateKey) : signPin(pin_token, private_key, session_id); 14 | 15 | let time = new Uint64LE((Date.now() / 1000) | 0).toBuffer(); 16 | if (iterator == undefined || iterator === '') { 17 | iterator = Date.now() * 1000000; 18 | } 19 | iterator = new Uint64LE(iterator).toBuffer(); 20 | pin = Buffer.from(pin, 'utf8'); 21 | let buf: any = Buffer.concat([pin, Buffer.from(time), Buffer.from(iterator)]); 22 | let padding = blockSize - (buf.length % blockSize); 23 | let paddingArray = []; 24 | for (let i = 0; i < padding; i++) { 25 | paddingArray.push(padding); 26 | } 27 | buf = Buffer.concat([buf, Buffer.from(paddingArray)]); 28 | let iv16 = randomBytes(16); 29 | let cipher = createCipheriv('aes-256-cbc', pinKey, iv16); 30 | cipher.setAutoPadding(false); 31 | let encrypted_pin_buff = cipher.update(buf, 'utf-8'); 32 | encrypted_pin_buff = Buffer.concat([iv16, encrypted_pin_buff]); 33 | return Buffer.from(encrypted_pin_buff).toString('base64'); 34 | }; 35 | 36 | export function toBuffer(content: any, encoding: any = 'utf8') { 37 | if (typeof content === 'object') { 38 | content = JSON.stringify(content); 39 | } 40 | return Buffer.from(content, encoding); 41 | } 42 | 43 | export function getEd25519Sign(payload: any, privateKey: any) { 44 | const header = toBuffer({ alg: 'EdDSA', typ: 'JWT' }).toString('base64'); 45 | payload = base64url(toBuffer(payload)); 46 | const result = [header, payload]; 47 | const sign = base64url(Buffer.from(pki.ed25519.sign({ message: result.join('.'), encoding: 'utf8', privateKey }))); 48 | result.push(sign); 49 | return result.join('.'); 50 | } 51 | 52 | export const signRequest = (method: string, url: string, body: object | string = ''): string => { 53 | if (url.startsWith('https://api.mixin.one')) url = url.replace('https://api.mixin.one', ''); 54 | if (url.startsWith('https://mixin-api.zeromesh.net')) url = url.replace('https://mixin-api.zeromesh.net', ''); 55 | if (typeof body === 'object') body = JSON.stringify(body); 56 | method = method.toUpperCase(); 57 | return md.sha256 58 | .create() 59 | .update(method + url + body, 'utf8') 60 | .digest() 61 | .toHex(); 62 | }; 63 | 64 | function signEncryptEd25519PIN(pinToken: any, privateKey: string) { 65 | pinToken = Buffer.from(pinToken, 'base64'); 66 | return scalarMult(privateKeyToCurve25519(privateKey), pinToken.slice(0, 32)); 67 | } 68 | 69 | function signPin(pin_token: any, private_key: any, session_id: any) { 70 | pin_token = Buffer.from(pin_token, 'base64'); 71 | private_key = pki.privateKeyFromPem(private_key); 72 | const pinKey = private_key.decrypt(pin_token, 'RSA-OAEP', { 73 | md: md.sha256.create(), 74 | label: session_id, 75 | }); 76 | return hexToBytes(util.binary.hex.encode(pinKey)); 77 | } 78 | 79 | function hexToBytes(hex: any) { 80 | const bytes = new Uint8Array(32); 81 | for (let c = 0; c < hex.length; c += 2) { 82 | bytes[c / 2] = parseInt(hex.substr(c, 2), 16); 83 | } 84 | return bytes; 85 | } 86 | 87 | function scalarMult(curvePriv: any, publicKey: any) { 88 | curvePriv[0] &= 248; 89 | curvePriv[31] &= 127; 90 | curvePriv[31] |= 64; 91 | let sharedKey = new Uint8Array(32); 92 | crypto_scalarmult(sharedKey, curvePriv, publicKey); 93 | return sharedKey; 94 | } 95 | 96 | export function base64url(buffer: Buffer) { 97 | return Buffer.from(buffer).toString('base64').replace(/\=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); 98 | } 99 | 100 | function privateKeyToCurve25519(privateKey: any) { 101 | const seed = privateKey.slice(0, 32); 102 | const sha512 = createHash('sha512'); 103 | sha512.write(seed, 'binary'); 104 | const digest = sha512.digest(); 105 | digest[0] &= 248; 106 | digest[31] &= 127; 107 | digest[31] |= 64; 108 | return digest.slice(0, 32); 109 | } 110 | 111 | function crypto_scalarmult(q: any, n: any, p: any) { 112 | let z = new Uint8Array(32); 113 | let x = new Float64Array(80), 114 | r, 115 | i; 116 | let a = gf(), 117 | b = gf(), 118 | c = gf(), 119 | d = gf(), 120 | e = gf(), 121 | f = gf(); 122 | for (i = 0; i < 31; i++) { 123 | z[i] = n[i]; 124 | } 125 | z[31] = (n[31] & 127) | 64; 126 | z[0] &= 248; 127 | unpack25519(x, p); 128 | for (i = 0; i < 16; i++) { 129 | b[i] = x[i]; 130 | d[i] = a[i] = c[i] = 0; 131 | } 132 | a[0] = d[0] = 1; 133 | for (i = 254; i >= 0; --i) { 134 | r = (z[i >>> 3] >>> (i & 7)) & 1; 135 | sel25519(a, b, r); 136 | sel25519(c, d, r); 137 | A(e, a, c); 138 | Z(a, a, c); 139 | A(c, b, d); 140 | Z(b, b, d); 141 | S(d, e); 142 | S(f, a); 143 | M(a, c, a); 144 | M(c, b, e); 145 | A(e, a, c); 146 | Z(a, a, c); 147 | S(b, a); 148 | Z(c, d, f); 149 | M(a, c, gf([0xdb41, 1])); 150 | A(a, a, d); 151 | M(c, c, a); 152 | M(a, d, f); 153 | M(d, b, x); 154 | S(b, e); 155 | sel25519(a, b, r); 156 | sel25519(c, d, r); 157 | } 158 | for (i = 0; i < 16; i++) { 159 | x[i + 16] = a[i]; 160 | x[i + 32] = c[i]; 161 | x[i + 48] = b[i]; 162 | x[i + 64] = d[i]; 163 | } 164 | let x32 = x.subarray(32); 165 | let x16 = x.subarray(16); 166 | inv25519(x32, x32); 167 | M(x16, x16, x32); 168 | pack25519(q, x16); 169 | return 0; 170 | } 171 | 172 | function gf(init: any = undefined) { 173 | let i, 174 | r = new Float64Array(16); 175 | if (init) { 176 | for (i = 0; i < init.length; i++) { 177 | r[i] = init[i]; 178 | } 179 | } 180 | return r; 181 | } 182 | 183 | function unpack25519(o: any, n: any) { 184 | let i; 185 | for (i = 0; i < 16; i++) { 186 | o[i] = n[2 * i] + (n[2 * i + 1] << 8); 187 | } 188 | o[15] &= 0x7fff; 189 | } 190 | 191 | function sel25519(p: any, q: any, b: any) { 192 | let t, 193 | c = ~(b - 1); 194 | for (let i = 0; i < 16; i++) { 195 | t = c & (p[i] ^ q[i]); 196 | p[i] ^= t; 197 | q[i] ^= t; 198 | } 199 | } 200 | 201 | function A(o: any, a: any, b: any) { 202 | for (let i = 0; i < 16; i++) { 203 | o[i] = a[i] + b[i]; 204 | } 205 | } 206 | 207 | function Z(o: any, a: any, b: any) { 208 | for (let i = 0; i < 16; i++) { 209 | o[i] = a[i] - b[i]; 210 | } 211 | } 212 | 213 | function M(o: any, a: any, b: any) { 214 | let v, 215 | c, 216 | t0 = 0, 217 | t1 = 0, 218 | t2 = 0, 219 | t3 = 0, 220 | t4 = 0, 221 | t5 = 0, 222 | t6 = 0, 223 | t7 = 0, 224 | t8 = 0, 225 | t9 = 0, 226 | t10 = 0, 227 | t11 = 0, 228 | t12 = 0, 229 | t13 = 0, 230 | t14 = 0, 231 | t15 = 0, 232 | t16 = 0, 233 | t17 = 0, 234 | t18 = 0, 235 | t19 = 0, 236 | t20 = 0, 237 | t21 = 0, 238 | t22 = 0, 239 | t23 = 0, 240 | t24 = 0, 241 | t25 = 0, 242 | t26 = 0, 243 | t27 = 0, 244 | t28 = 0, 245 | t29 = 0, 246 | t30 = 0, 247 | b0 = b[0], 248 | b1 = b[1], 249 | b2 = b[2], 250 | b3 = b[3], 251 | b4 = b[4], 252 | b5 = b[5], 253 | b6 = b[6], 254 | b7 = b[7], 255 | b8 = b[8], 256 | b9 = b[9], 257 | b10 = b[10], 258 | b11 = b[11], 259 | b12 = b[12], 260 | b13 = b[13], 261 | b14 = b[14], 262 | b15 = b[15]; 263 | v = a[0]; 264 | t0 += v * b0; 265 | t1 += v * b1; 266 | t2 += v * b2; 267 | t3 += v * b3; 268 | t4 += v * b4; 269 | t5 += v * b5; 270 | t6 += v * b6; 271 | t7 += v * b7; 272 | t8 += v * b8; 273 | t9 += v * b9; 274 | t10 += v * b10; 275 | t11 += v * b11; 276 | t12 += v * b12; 277 | t13 += v * b13; 278 | t14 += v * b14; 279 | t15 += v * b15; 280 | v = a[1]; 281 | t1 += v * b0; 282 | t2 += v * b1; 283 | t3 += v * b2; 284 | t4 += v * b3; 285 | t5 += v * b4; 286 | t6 += v * b5; 287 | t7 += v * b6; 288 | t8 += v * b7; 289 | t9 += v * b8; 290 | t10 += v * b9; 291 | t11 += v * b10; 292 | t12 += v * b11; 293 | t13 += v * b12; 294 | t14 += v * b13; 295 | t15 += v * b14; 296 | t16 += v * b15; 297 | v = a[2]; 298 | t2 += v * b0; 299 | t3 += v * b1; 300 | t4 += v * b2; 301 | t5 += v * b3; 302 | t6 += v * b4; 303 | t7 += v * b5; 304 | t8 += v * b6; 305 | t9 += v * b7; 306 | t10 += v * b8; 307 | t11 += v * b9; 308 | t12 += v * b10; 309 | t13 += v * b11; 310 | t14 += v * b12; 311 | t15 += v * b13; 312 | t16 += v * b14; 313 | t17 += v * b15; 314 | v = a[3]; 315 | t3 += v * b0; 316 | t4 += v * b1; 317 | t5 += v * b2; 318 | t6 += v * b3; 319 | t7 += v * b4; 320 | t8 += v * b5; 321 | t9 += v * b6; 322 | t10 += v * b7; 323 | t11 += v * b8; 324 | t12 += v * b9; 325 | t13 += v * b10; 326 | t14 += v * b11; 327 | t15 += v * b12; 328 | t16 += v * b13; 329 | t17 += v * b14; 330 | t18 += v * b15; 331 | v = a[4]; 332 | t4 += v * b0; 333 | t5 += v * b1; 334 | t6 += v * b2; 335 | t7 += v * b3; 336 | t8 += v * b4; 337 | t9 += v * b5; 338 | t10 += v * b6; 339 | t11 += v * b7; 340 | t12 += v * b8; 341 | t13 += v * b9; 342 | t14 += v * b10; 343 | t15 += v * b11; 344 | t16 += v * b12; 345 | t17 += v * b13; 346 | t18 += v * b14; 347 | t19 += v * b15; 348 | v = a[5]; 349 | t5 += v * b0; 350 | t6 += v * b1; 351 | t7 += v * b2; 352 | t8 += v * b3; 353 | t9 += v * b4; 354 | t10 += v * b5; 355 | t11 += v * b6; 356 | t12 += v * b7; 357 | t13 += v * b8; 358 | t14 += v * b9; 359 | t15 += v * b10; 360 | t16 += v * b11; 361 | t17 += v * b12; 362 | t18 += v * b13; 363 | t19 += v * b14; 364 | t20 += v * b15; 365 | v = a[6]; 366 | t6 += v * b0; 367 | t7 += v * b1; 368 | t8 += v * b2; 369 | t9 += v * b3; 370 | t10 += v * b4; 371 | t11 += v * b5; 372 | t12 += v * b6; 373 | t13 += v * b7; 374 | t14 += v * b8; 375 | t15 += v * b9; 376 | t16 += v * b10; 377 | t17 += v * b11; 378 | t18 += v * b12; 379 | t19 += v * b13; 380 | t20 += v * b14; 381 | t21 += v * b15; 382 | v = a[7]; 383 | t7 += v * b0; 384 | t8 += v * b1; 385 | t9 += v * b2; 386 | t10 += v * b3; 387 | t11 += v * b4; 388 | t12 += v * b5; 389 | t13 += v * b6; 390 | t14 += v * b7; 391 | t15 += v * b8; 392 | t16 += v * b9; 393 | t17 += v * b10; 394 | t18 += v * b11; 395 | t19 += v * b12; 396 | t20 += v * b13; 397 | t21 += v * b14; 398 | t22 += v * b15; 399 | v = a[8]; 400 | t8 += v * b0; 401 | t9 += v * b1; 402 | t10 += v * b2; 403 | t11 += v * b3; 404 | t12 += v * b4; 405 | t13 += v * b5; 406 | t14 += v * b6; 407 | t15 += v * b7; 408 | t16 += v * b8; 409 | t17 += v * b9; 410 | t18 += v * b10; 411 | t19 += v * b11; 412 | t20 += v * b12; 413 | t21 += v * b13; 414 | t22 += v * b14; 415 | t23 += v * b15; 416 | v = a[9]; 417 | t9 += v * b0; 418 | t10 += v * b1; 419 | t11 += v * b2; 420 | t12 += v * b3; 421 | t13 += v * b4; 422 | t14 += v * b5; 423 | t15 += v * b6; 424 | t16 += v * b7; 425 | t17 += v * b8; 426 | t18 += v * b9; 427 | t19 += v * b10; 428 | t20 += v * b11; 429 | t21 += v * b12; 430 | t22 += v * b13; 431 | t23 += v * b14; 432 | t24 += v * b15; 433 | v = a[10]; 434 | t10 += v * b0; 435 | t11 += v * b1; 436 | t12 += v * b2; 437 | t13 += v * b3; 438 | t14 += v * b4; 439 | t15 += v * b5; 440 | t16 += v * b6; 441 | t17 += v * b7; 442 | t18 += v * b8; 443 | t19 += v * b9; 444 | t20 += v * b10; 445 | t21 += v * b11; 446 | t22 += v * b12; 447 | t23 += v * b13; 448 | t24 += v * b14; 449 | t25 += v * b15; 450 | v = a[11]; 451 | t11 += v * b0; 452 | t12 += v * b1; 453 | t13 += v * b2; 454 | t14 += v * b3; 455 | t15 += v * b4; 456 | t16 += v * b5; 457 | t17 += v * b6; 458 | t18 += v * b7; 459 | t19 += v * b8; 460 | t20 += v * b9; 461 | t21 += v * b10; 462 | t22 += v * b11; 463 | t23 += v * b12; 464 | t24 += v * b13; 465 | t25 += v * b14; 466 | t26 += v * b15; 467 | v = a[12]; 468 | t12 += v * b0; 469 | t13 += v * b1; 470 | t14 += v * b2; 471 | t15 += v * b3; 472 | t16 += v * b4; 473 | t17 += v * b5; 474 | t18 += v * b6; 475 | t19 += v * b7; 476 | t20 += v * b8; 477 | t21 += v * b9; 478 | t22 += v * b10; 479 | t23 += v * b11; 480 | t24 += v * b12; 481 | t25 += v * b13; 482 | t26 += v * b14; 483 | t27 += v * b15; 484 | v = a[13]; 485 | t13 += v * b0; 486 | t14 += v * b1; 487 | t15 += v * b2; 488 | t16 += v * b3; 489 | t17 += v * b4; 490 | t18 += v * b5; 491 | t19 += v * b6; 492 | t20 += v * b7; 493 | t21 += v * b8; 494 | t22 += v * b9; 495 | t23 += v * b10; 496 | t24 += v * b11; 497 | t25 += v * b12; 498 | t26 += v * b13; 499 | t27 += v * b14; 500 | t28 += v * b15; 501 | v = a[14]; 502 | t14 += v * b0; 503 | t15 += v * b1; 504 | t16 += v * b2; 505 | t17 += v * b3; 506 | t18 += v * b4; 507 | t19 += v * b5; 508 | t20 += v * b6; 509 | t21 += v * b7; 510 | t22 += v * b8; 511 | t23 += v * b9; 512 | t24 += v * b10; 513 | t25 += v * b11; 514 | t26 += v * b12; 515 | t27 += v * b13; 516 | t28 += v * b14; 517 | t29 += v * b15; 518 | v = a[15]; 519 | t15 += v * b0; 520 | t16 += v * b1; 521 | t17 += v * b2; 522 | t18 += v * b3; 523 | t19 += v * b4; 524 | t20 += v * b5; 525 | t21 += v * b6; 526 | t22 += v * b7; 527 | t23 += v * b8; 528 | t24 += v * b9; 529 | t25 += v * b10; 530 | t26 += v * b11; 531 | t27 += v * b12; 532 | t28 += v * b13; 533 | t29 += v * b14; 534 | t30 += v * b15; 535 | t0 += 38 * t16; 536 | t1 += 38 * t17; 537 | t2 += 38 * t18; 538 | t3 += 38 * t19; 539 | t4 += 38 * t20; 540 | t5 += 38 * t21; 541 | t6 += 38 * t22; 542 | t7 += 38 * t23; 543 | t8 += 38 * t24; 544 | t9 += 38 * t25; 545 | t10 += 38 * t26; 546 | t11 += 38 * t27; 547 | t12 += 38 * t28; 548 | t13 += 38 * t29; 549 | t14 += 38 * t30; 550 | // t15 left as is 551 | // first car 552 | c = 1; 553 | v = t0 + c + 65535; 554 | c = Math.floor(v / 65536); 555 | t0 = v - c * 65536; 556 | v = t1 + c + 65535; 557 | c = Math.floor(v / 65536); 558 | t1 = v - c * 65536; 559 | v = t2 + c + 65535; 560 | c = Math.floor(v / 65536); 561 | t2 = v - c * 65536; 562 | v = t3 + c + 65535; 563 | c = Math.floor(v / 65536); 564 | t3 = v - c * 65536; 565 | v = t4 + c + 65535; 566 | c = Math.floor(v / 65536); 567 | t4 = v - c * 65536; 568 | v = t5 + c + 65535; 569 | c = Math.floor(v / 65536); 570 | t5 = v - c * 65536; 571 | v = t6 + c + 65535; 572 | c = Math.floor(v / 65536); 573 | t6 = v - c * 65536; 574 | v = t7 + c + 65535; 575 | c = Math.floor(v / 65536); 576 | t7 = v - c * 65536; 577 | v = t8 + c + 65535; 578 | c = Math.floor(v / 65536); 579 | t8 = v - c * 65536; 580 | v = t9 + c + 65535; 581 | c = Math.floor(v / 65536); 582 | t9 = v - c * 65536; 583 | v = t10 + c + 65535; 584 | c = Math.floor(v / 65536); 585 | t10 = v - c * 65536; 586 | v = t11 + c + 65535; 587 | c = Math.floor(v / 65536); 588 | t11 = v - c * 65536; 589 | v = t12 + c + 65535; 590 | c = Math.floor(v / 65536); 591 | t12 = v - c * 65536; 592 | v = t13 + c + 65535; 593 | c = Math.floor(v / 65536); 594 | t13 = v - c * 65536; 595 | v = t14 + c + 65535; 596 | c = Math.floor(v / 65536); 597 | t14 = v - c * 65536; 598 | v = t15 + c + 65535; 599 | c = Math.floor(v / 65536); 600 | t15 = v - c * 65536; 601 | t0 += c - 1 + 37 * (c - 1); 602 | // second car 603 | c = 1; 604 | v = t0 + c + 65535; 605 | c = Math.floor(v / 65536); 606 | t0 = v - c * 65536; 607 | v = t1 + c + 65535; 608 | c = Math.floor(v / 65536); 609 | t1 = v - c * 65536; 610 | v = t2 + c + 65535; 611 | c = Math.floor(v / 65536); 612 | t2 = v - c * 65536; 613 | v = t3 + c + 65535; 614 | c = Math.floor(v / 65536); 615 | t3 = v - c * 65536; 616 | v = t4 + c + 65535; 617 | c = Math.floor(v / 65536); 618 | t4 = v - c * 65536; 619 | v = t5 + c + 65535; 620 | c = Math.floor(v / 65536); 621 | t5 = v - c * 65536; 622 | v = t6 + c + 65535; 623 | c = Math.floor(v / 65536); 624 | t6 = v - c * 65536; 625 | v = t7 + c + 65535; 626 | c = Math.floor(v / 65536); 627 | t7 = v - c * 65536; 628 | v = t8 + c + 65535; 629 | c = Math.floor(v / 65536); 630 | t8 = v - c * 65536; 631 | v = t9 + c + 65535; 632 | c = Math.floor(v / 65536); 633 | t9 = v - c * 65536; 634 | v = t10 + c + 65535; 635 | c = Math.floor(v / 65536); 636 | t10 = v - c * 65536; 637 | v = t11 + c + 65535; 638 | c = Math.floor(v / 65536); 639 | t11 = v - c * 65536; 640 | v = t12 + c + 65535; 641 | c = Math.floor(v / 65536); 642 | t12 = v - c * 65536; 643 | v = t13 + c + 65535; 644 | c = Math.floor(v / 65536); 645 | t13 = v - c * 65536; 646 | v = t14 + c + 65535; 647 | c = Math.floor(v / 65536); 648 | t14 = v - c * 65536; 649 | v = t15 + c + 65535; 650 | c = Math.floor(v / 65536); 651 | t15 = v - c * 65536; 652 | t0 += c - 1 + 37 * (c - 1); 653 | o[0] = t0; 654 | o[1] = t1; 655 | o[2] = t2; 656 | o[3] = t3; 657 | o[4] = t4; 658 | o[5] = t5; 659 | o[6] = t6; 660 | o[7] = t7; 661 | o[8] = t8; 662 | o[9] = t9; 663 | o[10] = t10; 664 | o[11] = t11; 665 | o[12] = t12; 666 | o[13] = t13; 667 | o[14] = t14; 668 | o[15] = t15; 669 | } 670 | 671 | function S(o: any, a: any) { 672 | M(o, a, a); 673 | } 674 | 675 | function inv25519(o: any, i: any) { 676 | let c = gf(); 677 | let a; 678 | for (a = 0; a < 16; a++) { 679 | c[a] = i[a]; 680 | } 681 | for (a = 253; a >= 0; a--) { 682 | S(c, c); 683 | if (a !== 2 && a !== 4) { 684 | M(c, c, i); 685 | } 686 | } 687 | for (a = 0; a < 16; a++) { 688 | o[a] = c[a]; 689 | } 690 | } 691 | 692 | function pack25519(o: any, n: any) { 693 | let i, j, b; 694 | let m = gf(), 695 | t = gf(); 696 | for (i = 0; i < 16; i++) { 697 | t[i] = n[i]; 698 | } 699 | car25519(t); 700 | car25519(t); 701 | car25519(t); 702 | for (j = 0; j < 2; j++) { 703 | m[0] = t[0] - 0xffed; 704 | for (i = 1; i < 15; i++) { 705 | m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1); 706 | m[i - 1] &= 0xffff; 707 | } 708 | m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1); 709 | b = (m[15] >> 16) & 1; 710 | m[14] &= 0xffff; 711 | sel25519(t, m, 1 - b); 712 | } 713 | for (i = 0; i < 16; i++) { 714 | o[2 * i] = t[i] & 0xff; 715 | o[2 * i + 1] = t[i] >> 8; 716 | } 717 | } 718 | 719 | function car25519(o: any) { 720 | let i, 721 | v, 722 | c = 1; 723 | for (i = 0; i < 16; i++) { 724 | v = o[i] + c + 65535; 725 | c = Math.floor(v / 65536); 726 | o[i] = v - c * 65536; 727 | } 728 | o[0] += c - 1 + 37 * (c - 1); 729 | } 730 | --------------------------------------------------------------------------------