diff --git a/README.md b/README.md index a553048..3ee4f88 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,13 @@ It is recommended to add this repository main branch to your project as a submod After adding, you need to run 'npm install' in the root directory of this module in order to install all prerequisites. +## Note +if you get any error about node-gyp or sodium-native. try deleting this two files + + C:\Users\{YourUserName}\AppData\Local\node-gyp + + C:\Users\{YourUserName}\.node-gyp + ## Supported Coins - Bitcoin based coins Like Bitcoin, Litecoin, Bitcoincash, Doge, Bitcoin Gold - Ethereum diff --git a/data/ProkeyCoinsInfo.json b/data/ProkeyCoinsInfo.json index 6f57fb9..a8caeba 100644 --- a/data/ProkeyCoinsInfo.json +++ b/data/ProkeyCoinsInfo.json @@ -35789,5 +35789,39 @@ "https://wallet.prokey.io" ] } + ], + "stellar": [ + { + "name": "Stellar", + "shortcut": "XLM", + "slip44": 148, + "decimals": 0, + "priority": 9, + "on_device": "Stellar", + "test": false, + "support": { + "optimum": "1.8.0" + }, + "tx_url": "https://stellarchain.io/tx/{hash}", + "wallets": [ + "https://wallet.prokey.io" + ] + }, + { + "name": "Stellar Testnet", + "shortcut": "tXLM", + "slip44": 1, + "decimals": 0, + "priority": 100, + "on_device": "Stellar Testnet", + "test": true, + "support": { + "optimum": "1.8.0" + }, + "tx_url": "http://testnet.stellarchain.io/transactions/{hash}", + "wallets": [ + "https://wallet.prokey.io" + ] + } ] } diff --git a/package.json b/package.json index aacefed..5a9602c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "rxjs": "~6.2.0", "typescript-http-client": "^0.10.1", "websocket": "^1.0.34", - "crypto-js": "3.1.9-1" + "crypto-js": "3.1.9-1", + "websocket": "^1.0.34", + "sodium-native": "^3.2.1", + "stellar-base": "^6.0.1" }, "scripts": { "build": "tsc", diff --git a/src/blockchain/servers/prokey/src/ProkeyBaseBlockChain.ts b/src/blockchain/servers/prokey/src/ProkeyBaseBlockChain.ts index e011536..ecc7e98 100644 --- a/src/blockchain/servers/prokey/src/ProkeyBaseBlockChain.ts +++ b/src/blockchain/servers/prokey/src/ProkeyBaseBlockChain.ts @@ -3,39 +3,31 @@ import { Request, import {RequestAddressInfo} from "../../../../models/GenericWalletModel"; export abstract class ProkeyBaseBlockChain { - _url = 'https://blocks.prokey.org/'; + _baseUrl = 'https://blocks.prokey.org/'; - constructor(url?: string) { - if (url) { - this._url = url; - } - } - -// These functions must be implemented in child classes + // These functions must be implemented in child classes public abstract GetAddressInfo(reqAddresses: Array | RequestAddressInfo); public abstract GetTransactions(hash: string); public abstract GetLatestTransactions(trs: Array, count : number, offset: number); public abstract BroadCastTransaction(data: any); + constructor(baseUrl: string = 'https://blocks.prokey.org/') { + this._baseUrl = baseUrl; + } /** * This is a private helper function to GET data from server * @param toServer URL + data * @param changeJson a callback for adjust json before casting */ protected async GetFromServer(toServer: string, changeJson?: (json: string) => string) { - const client = newHttpClient(); - const request = new Request(this._url + toServer, { method: 'GET' }); + const request = new Request(this._baseUrl + toServer, {method: 'GET'}); let json = await client.execute(request); - if (changeJson) { - json = changeJson(json); - } - - return JSON.parse(json) as T; - } + return this.handleJsonResponse(changeJson, json); + } /** * This is a private helper function to POST data to server @@ -43,11 +35,19 @@ export abstract class ProkeyBaseBlockChain { * @param body Request Body * @returns Response data from server */ - protected async PostToServer(toServer: string, body: any): Promise { + protected async PostToServer(toServer: string, body: any, changeJson?: (json: string) => string): Promise { const client = newHttpClient(); - const request = new Request(this._url + toServer, {body: body, method: 'POST'}); + const request = new Request(this._baseUrl + toServer, {body: body, method: 'POST'}); + + let json = await client.execute(request); + return this.handleJsonResponse(changeJson, json); + } - return JSON.parse(await client.execute(request)); + private handleJsonResponse(changeJson: ((json: string) => string) | undefined, json: string) { + if (changeJson) { + json = changeJson(json); + } + return JSON.parse(json) as T; } } diff --git a/src/blockchain/servers/prokey/src/stellar/Stellar.ts b/src/blockchain/servers/prokey/src/stellar/Stellar.ts new file mode 100644 index 0000000..e295cfb --- /dev/null +++ b/src/blockchain/servers/prokey/src/stellar/Stellar.ts @@ -0,0 +1,87 @@ +import {ProkeyBaseBlockChain} from "../ProkeyBaseBlockChain"; +import {RequestAddressInfo} from "../../../../../models/GenericWalletModel"; +import { + StellarAccountInfo, + StellarFee, + StellarTransactionOperationResponse, + StellarTransactionResponse +} from "./StellarModels"; +import {MyConsole} from "../../../../../utils/console"; + +export class StellarBlockchain extends ProkeyBaseBlockChain { + _coinName: string; + + constructor(coinName: string = "Xlm") + { + super(); + this._coinName = coinName; + MyConsole.Info(this._coinName); + } + + /** + * broadcast transaction over network + * @param data stellar signed transaction (base64 format) + * @returns object response of transaction + */ + public async BroadCastTransaction(data: string): Promise { + return await this.PostToServer(`Transaction/advanced-send/${this._coinName}/`, {"SignedTransactionBlob": data}); + } + + /** + * get account information from network + * @param reqAddress address + * @returns StellarAccountInfo account info + */ + public async GetAddressInfo(reqAddress: RequestAddressInfo) : Promise { + try { + return await this.GetFromServer(`address/${this._coinName}/${reqAddress.address}`); + } catch (error) { + return null; + } + } + + /** + * get account transaction list from network + * @param accountAddress address of account + * @param limit number of transactions + * @param cursor used for pagination when you want next page + * @returns StellarAccountInfo list of transactions + */ + public async GetAccountTransactions(accountAddress: string, limit: number = 10, cursor?: string): Promise { + let serverResponse = await this.GetFromServer(`address/transactions/${this._coinName}/${accountAddress}/${limit}`); + if (serverResponse != null && serverResponse.transactions != null) + { + return serverResponse; + } + return null; + } + + /** + * get operations of a specific transaction + * @param transactionId transaction id + * @returns StellarTransactionOperationResponse list of operations + */ + public async GetTransactionOperations(transactionId: string): Promise { + let queryUrl = `transaction/${this._coinName}/${transactionId}`; + let serverResponse = await this.GetFromServer(queryUrl); + if (serverResponse != null && serverResponse.operations != null) + { + return serverResponse; + } + return null; + } + + /** + * get network fee detail + * @returns StellarFee + */ + public async GetCurrentFee(): Promise { + return await this.GetFromServer(`transaction/fee/${this._coinName}`); + } + + GetLatestTransactions(trs: Array, count: number, offset: number) { + } + + GetTransactions(hash: string) { + } +} diff --git a/src/blockchain/servers/prokey/src/stellar/StellarModels.ts b/src/blockchain/servers/prokey/src/stellar/StellarModels.ts new file mode 100644 index 0000000..4ea38c0 --- /dev/null +++ b/src/blockchain/servers/prokey/src/stellar/StellarModels.ts @@ -0,0 +1,164 @@ +/* + * This is part of PROKEY HARDWARE WALLET project + * Copyright (C) Prokey.io + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +export interface StellarFee { + last_ledger: string, + last_ledger_base_fee: string, + ledger_capacity_usage: string, + min_accepted_fee: string, + mode_accepted_fee: string, + p10_accepted_fee: string, + p20_accepted_fee: string, + p30_accepted_fee: string, + p40_accepted_fee: string, + p50_accepted_fee: string, + p60_accepted_fee: string, + p70_accepted_fee: string, + p80_accepted_fee: string, + p90_accepted_fee: string, + p95_accepted_fee: string, + p99_accepted_fee: string, + fee_charged: FeeState, + max_fee: FeeState +} + +export interface StellarAccountInfo { + account_id: string, + sequence: number, + subentry_count: number, + inflation_destination: string, + home_domain: string, + thresholds: Thresholds, + flags: Flags, + balances: Balance[], + signers: Signer[], + num_sponsoring: number, + num_sponsored: number, + Data: object +} + +export interface StellarTransactionResponse { + transactions: StellarTransaction[], + nextPageCursor: string, + prevPageCursor: string +} + +export interface StellarTransaction { + hash: string, + ledger: number, + created_at: string, + source_account: string, + fee_account: string, + successful: boolean, + paging_token: string, + source_account_sequence: number, + fee_charged: number, + max_fee: number, + operation_count: number, + envelope_xdr: string, + result_xdr: string, + result_meta_xdr: string, + signatures: string[], + fee_bump_transaction: FeeBumpTransaction, + inner_transaction: InnerTransaction, + memo_type: string, + memo: string, + memo_bytes: string +} + +export interface StellarTransactionOperationResponse { + operations: StellarTransactionOperation[], + nextPageCursor: string, + prevPageCursor: string +} + +export interface StellarTransactionOperation { + id: number, + source_account: string, + paging_token: string, + type: string, + type_i: number, + created_at: string, + transaction_hash: string, + transaction_successful: boolean + asset_type: string, + from: string, + to: string, + amount: string, + name: string, + value: string, + funder: string, + account: string, + starting_balance: string +} + +interface FeeState { + max: string; + min: string; + mode: string; + p10: string; + p20: string; + p30: string; + p40: string; + p50: string; + p60: string; + p70: string; + p80: string; + p90: string; + p95: string; + p99: string; +} + +export interface FeeBumpTransaction { + hash: string, + signatures: string[] +} + +export interface InnerTransaction { + hash: string, + signatures: string, + max_fee: number +} + +export interface Thresholds { + low_threshold: number, + med_threshold: number, + high_threshold: number +} + +export interface Flags { + auth_required: boolean, + auth_revocable: boolean, + auth_immutable: boolean +} + +export interface Balance { + asset_type: string, + asset_code: string, + asset_issuer: string, + limit: string, + balance: string, + buying_liabilities: string, + selling_liabilities: string, + is_authorized: boolean, + is_authorized_to_maintain_liabilities: boolean +} + +export interface Signer { + key: string, + type: string, + weight: number +} diff --git a/src/coins/CoinInfo.ts b/src/coins/CoinInfo.ts index fceeb74..957a35c 100644 --- a/src/coins/CoinInfo.ts +++ b/src/coins/CoinInfo.ts @@ -1,9 +1,9 @@ /* * This is part of PROKEY HARDWARE WALLET project * Copyright (C) Prokey.io - * + * * Hadi Robati, hadi@prokey.io - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -95,6 +95,9 @@ export class CoinInfo { case CoinBaseType.NEM: c = ProkeyCoinInfoModel.NEM; break; + case CoinBaseType.Stellar: + c = ProkeyCoinInfoModel.stellar; + break; } let f = coinName.toLowerCase(); @@ -141,6 +144,11 @@ export class CoinInfo { ci.id = `nem_${ci.shortcut}`; break; + case CoinBaseType.Stellar: + ci = c.find(obj => obj.name.toLowerCase() == f || obj.shortcut.toLowerCase() == f); + + ci.id = `stellar_${ci.shortcut}`; + break; default: ci = c.find(obj => obj.name.toLowerCase() == f || obj.shortcut.toLowerCase() == f); @@ -248,6 +256,18 @@ export class CoinInfo { } }); + //! For all Stellar base coins + ProkeyCoinInfoModel.stellar.forEach(stellar => { + //! Check the version + if(compareVersions(firmwareVersion, stellar.support.optimum) >= 0) { + list.push({ + ...stellar, + coinBaseType: CoinBaseType.Stellar, + id: `stellar_${stellar.shortcut}`, + }) + } + }); + //! Sort the list by Priority list.sort((a, b) => { if (a.priority > b.priority) diff --git a/src/device/ICoinCommand.ts b/src/device/ICoinCommand.ts index dae3538..1d5baab 100644 --- a/src/device/ICoinCommand.ts +++ b/src/device/ICoinCommand.ts @@ -1,9 +1,9 @@ /* * This is part of PROKEY HARDWARE WALLET project * Copyright (C) Prokey.io - * + * * Hadi Robati, hadi@prokey.io - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -23,6 +23,7 @@ import * as ProkeyResponses from '../models/Prokey'; import { BitcoinTx } from '../models/BitcoinTx'; import { EthereumTx } from '../models/EthereumTx'; import { RippleTransaction } from '../models/Responses-V6'; +import {StellarSignTransactionRequest} from "../models/Prokey"; import {NEMSignTxMessage} from "../models/Prokey"; export interface ICoinCommands { @@ -41,7 +42,7 @@ export interface ICoinCommands { GetAddresses( device: Device, path: Array | string>, - ): Promise; + ProkeyResponses.NEMSignedTx | + string>; SignMessage( device: Device, @@ -84,7 +87,7 @@ export interface ICoinCommands { coinName?: string ): Promise; - + VerifyMessage( device: Device, address: string, diff --git a/src/device/StellarCommands.ts b/src/device/StellarCommands.ts new file mode 100644 index 0000000..c611adf --- /dev/null +++ b/src/device/StellarCommands.ts @@ -0,0 +1,208 @@ +import {ICoinCommands} from "./ICoinCommand"; +import {RippleCoinInfoModel, StellarCoinInfoModel} from "../models/CoinInfoModel"; +import {CoinBaseType, CoinInfo} from "../coins/CoinInfo"; +import {Device} from "./Device"; +import { + MessageSignature, + PublicKey, + StellarAddress, + StellarSignedTx, + StellarSignTransactionRequest, + StellarTxOpRequest, + Success +} from "../models/Prokey"; +import {GeneralErrors, GeneralResponse} from "../models/GeneralResponse"; +import * as PathUtil from "../utils/pathUtils"; +import * as ProkeyResponses from "../models/Prokey"; +import {MyConsole} from "../utils/console"; +import {StrKey} from "stellar-base"; +import * as Utility from "../utils/utils"; + +export class StellarCommands implements ICoinCommands { + private readonly _coinInfo: StellarCoinInfoModel; + + constructor(coinName: string) { + this._coinInfo = CoinInfo.Get(coinName, CoinBaseType.Stellar); + } + + /** + * Get stellar account address based on given path + * @param device Prokey device instance + * @param path BIP path + * @param showOnProkey true means show the address on device display + * @returns StellarAddress stellar unique address + */ + public async GetAddress(device: Device, path: Array | string, showOnProkey: boolean = true): Promise { + if (device == null || path == null) { + return Promise.reject({ success: false, errorCode: GeneralErrors.INVALID_PARAM }); + } + + let address_n: Array; + try { + address_n = this.GetAddressArray(path); + } + catch (e) { + return Promise.reject({ success: false, errorCode: GeneralErrors.PATH_NOT_VALID }); + } + + let param = { + address_n: address_n, + show_display: showOnProkey, + } + + return await device.SendMessage('StellarGetAddress', param, 'StellarAddress'); + } + + /** + * Get a list of account addresses based on given paths + * @param device Prokey device instance + * @param paths List of BIP paths + * @constructor + */ + public async GetAddresses(device: Device, paths: Array | string>): Promise> { + let stellarAddresses: Array = new Array(); + for (const path of paths) { + stellarAddresses.push(await this.GetAddress(device, path, false)); + } + return stellarAddresses; + } + + /** + * Get Coin Info + */ + public GetCoinInfo(): StellarCoinInfoModel { + return this._coinInfo; + } + + /** + * Get Public key + * @param device The prokey device + * @param path BIP path + * @param showOnProkey true means show the public key on prokey display + */ + public async GetPublicKey(device: Device, path: Array | string, showOnProkey: boolean = true): Promise { + if (device == null || path == null) { + return Promise.reject({ success: false, errorCode: GeneralErrors.INVALID_PARAM }); + } + + let address_n: Array; + try { + address_n = this.GetAddressArray(path); + } + catch (e) { + return Promise.reject({ success: false, errorCode: GeneralErrors.PATH_NOT_VALID }); + } + + let param = { + address_n: address_n, + show_display: showOnProkey, + } + + return await device.SendMessage('GetPublicKey', param, 'PublicKey'); + } + + /** + * Sign Message + * @param device Prokey device instance + * @param path array of BIP32/44 Path + * @param message message to be signed + * @param coinName coin name + */ + public async SignMessage(device: Device, path: Array, message: Uint8Array, coinName?: string): Promise { + let scriptType = PathUtil.GetScriptType(path); + + let res = await device.SendMessage('SignMessage', { + address_n: path, + message: message, + coin_name: coinName || 'Stellar', + script_type: scriptType, + }, 'MessageSignature'); + + if (res.signature) { + res.signature = Utility.ByteArrayToHexString(res.signature); + } + + return res; + } + + /** + * Verify Message + * @param device Prokey device instance + * @param address address + * @param message message + * @param signature signature data + * @param coinName coin name + */ + public async VerifyMessage(device: Device, address: string, message: Uint8Array, signature: Uint8Array, coinName?: string): Promise { + return await device.SendMessage('VerifyMessage', { + address: address, + signature: signature, + message: message, + coin_name: coinName || 'Stellar', + }, 'Success'); + } + + /** + * sign transaction + * @param device + * @param transactionForSign a model that containg a transaction model for device and sdk for create signed transaction + * @constructor + */ + public async SignTransaction(device: Device, transactionForSign: StellarSignTransactionRequest): Promise { + MyConsole.Info("StellarSignTx", transactionForSign); + + if (!transactionForSign) { + let e: GeneralResponse = { + success: false, + errorCode: GeneralErrors.INVALID_PARAM, + errorMessage: "StellarCommands::SignTransaction->parameter transaction cannot be null", + } + + throw e; + } + + let firstOperationRequest = await device.SendMessage('StellarSignTx', transactionForSign.signTxMessage, 'StellarTxOpRequest'); + MyConsole.Info("operation request", firstOperationRequest); + + for (let i=0; i < transactionForSign.operations.length - 1; i++) { + let operation = transactionForSign.operations[i]; + let operationRequest = await device.SendMessage(operation.type, operation, 'StellarTxOpRequest'); + MyConsole.Info("operation request", operationRequest); + } + + let operation = transactionForSign.operations[transactionForSign.operations.length - 1]; + let signResponse = await device.SendMessage(operation.type, operation, 'StellarSignedTx'); + return await StellarCommands.prepareTransactionForBroadcast(transactionForSign, signResponse); + } + + /** + * get byte array of path if its serialized + * @param path + * @returns Array account BIP path + */ + public GetAddressArray(path: Array | string) : Array { + if (typeof path == "string") { + return PathUtil.getHDPath(path); + } else { + return path; + } + } + + /** + * prepare signed transaction for sending over network + * @param transactionForSign stellar transaction model + * @param signResponse device sign response + * @private + */ + private static async prepareTransactionForBroadcast(transactionForSign: StellarSignTransactionRequest, signResponse: StellarSignedTx) { + let transactionModel = transactionForSign.transactionModel; + + let stringSignature = Utility.ByteArrayToHexString(signResponse.signature); + let decodedPublicKey = StrKey.encodeEd25519PublicKey(Buffer.from(signResponse.public_key)); + transactionModel.addSignature( + decodedPublicKey, + Buffer.from(stringSignature, 'hex').toString('base64') + ); + return transactionModel.toEnvelope().toXDR().toString("base64"); + } +} diff --git a/src/models/CoinInfoModel.ts b/src/models/CoinInfoModel.ts index f0a4f7c..df5db8a 100644 --- a/src/models/CoinInfoModel.ts +++ b/src/models/CoinInfoModel.ts @@ -28,7 +28,8 @@ export type GeneralCoinInfoModel = BitcoinBaseCoinInfoModel | RippleCoinInfoModel | OmniCoinInfoModel | MiscCoinInfoModel | - NemCoinInfoModel; + NemCoinInfoModel | + StellarCoinInfoModel; export interface BaseCoinInfoModel { name: string, @@ -138,3 +139,8 @@ export interface NemCoinInfoModel extends BaseCoinInfoModel { tx_url: string, priority: number, } + +export interface StellarCoinInfoModel extends BaseCoinInfoModel { + on_device: string, + tx_url: string, +} diff --git a/src/models/Prokey.ts b/src/models/Prokey.ts index b9eabcd..0f3bf5c 100644 --- a/src/models/Prokey.ts +++ b/src/models/Prokey.ts @@ -17,6 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +import {Transaction} from "stellar-base"; export type CipheredKeyValue = { value: string, @@ -446,6 +447,19 @@ export type NEMSignTxMessage = { } // Stellar types +export type StellarDecoratedSignature = { + hint: string, + signature: string +} + +export type StellarTxOpRequest = { +} + +export type StellarSignTransactionRequest = { + signTxMessage: StellarSignTxMessage, + operations: StellarOperationMessage[] + transactionModel: Transaction +} export type StellarAddress = { address: string, diff --git a/src/wallet/BaseWallet.ts b/src/wallet/BaseWallet.ts index 55fbeb9..cf4f8ca 100644 --- a/src/wallet/BaseWallet.ts +++ b/src/wallet/BaseWallet.ts @@ -1,9 +1,9 @@ /* * This is part of PROKEY HARDWARE WALLET project * Copyright (C) Prokey.io - * + * * Hadi Robati, hadi@prokey.io - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -20,11 +20,12 @@ import { Device } from "../device/Device"; import { - BitcoinBaseCoinInfoModel, - EthereumBaseCoinInfoModel, - Erc20BaseCoinInfoModel, - OmniCoinInfoModel, - RippleCoinInfoModel + BitcoinBaseCoinInfoModel, + EthereumBaseCoinInfoModel, + Erc20BaseCoinInfoModel, + OmniCoinInfoModel, + RippleCoinInfoModel, + StellarCoinInfoModel } from '../models/CoinInfoModel' import { CoinBaseType, CoinInfo } from '../coins/CoinInfo' import { ICoinCommands } from '../device/ICoinCommand' @@ -54,6 +55,7 @@ import { CardanoSignedTx, NEMSignedTx, NEMSignTxMessage, + StellarSignTransactionRequest, Success } from "../models/Prokey"; @@ -68,6 +70,7 @@ import { BitcoinTx } from '../models/BitcoinTx'; import { EthereumTx } from '../models/EthereumTx'; import { RippleCommands } from "../device/RippleCommands"; import { RippleSignedTx, RippleTransaction } from "../models/Responses-V6"; +import {StellarCommands} from "../device/StellarCommands"; /** * This is the base class for all implemented wallets @@ -82,9 +85,9 @@ export abstract class BaseWallet { * @param _coinName Coin name, Check /data/ProkeyCoinsInfo.json * @param _coinType Coin type BitcoinBase | EthereumBase | ERC20 | NEM | OMNI | OTHERS */ - constructor(private _device: Device, - coinName: string, - coinType: CoinBaseType, + constructor(private _device: Device, + coinName: string, + coinType: CoinBaseType, chainOrPropertyNumber?: number, coinInfo?: BitcoinBaseCoinInfoModel | EthereumBaseCoinInfoModel | Erc20BaseCoinInfoModel | OmniCoinInfoModel | RippleCoinInfoModel) { if (_device == null) @@ -113,6 +116,10 @@ export abstract class BaseWallet { this._commands = new RippleCommands(coinName); break; + case CoinBaseType.Stellar: + this._commands = new StellarCommands(coinName); + break; + case CoinBaseType.NEM: this._commands = new NemCommands(coinName); break; @@ -126,7 +133,7 @@ export abstract class BaseWallet { /** * Get CoinInfo */ - public GetCoinInfo(): BitcoinBaseCoinInfoModel | EthereumBaseCoinInfoModel | Erc20BaseCoinInfoModel | OmniCoinInfoModel | RippleCoinInfoModel { + public GetCoinInfo(): BitcoinBaseCoinInfoModel | EthereumBaseCoinInfoModel | Erc20BaseCoinInfoModel | OmniCoinInfoModel | RippleCoinInfoModel | StellarCoinInfoModel { return this._coinInfo; } @@ -175,13 +182,13 @@ export abstract class BaseWallet { * @param tx transaction to be signed by device */ public async SignTransaction - (tx: BitcoinTx | EthereumTx | RippleTransaction | NEMSignTxMessage): Promise + (tx: BitcoinTx | EthereumTx | RippleTransaction | NEMSignTxMessage | StellarSignTransactionRequest): Promise { return await this._commands.SignTransaction(this._device, tx) as T; } /** - * Sign Message + * Sign Message * @param path BIP32 Path to sign the message * @param message Message to be signed * @param coinName Optional, Only for Bitcoin based coins @@ -197,7 +204,7 @@ export abstract class BaseWallet { * @param message Signed message * @param signature Signature * @param coinName Optional, Only for Bitcoin based coins - * @returns + * @returns */ public async VerifyMessage(address: string, message: string, signature: string, coinName?: string): Promise { const messageBytes = Util.StringToUint8Array(message); diff --git a/src/wallet/StellarWallet.ts b/src/wallet/StellarWallet.ts new file mode 100644 index 0000000..f6382df --- /dev/null +++ b/src/wallet/StellarWallet.ts @@ -0,0 +1,404 @@ +import {BaseWallet} from "./BaseWallet"; +import {Device} from "../device/Device"; +import {CoinBaseType} from "../coins/CoinInfo"; +import {StellarCoinInfoModel} from "../models/CoinInfoModel"; +import { + AddressModel, + StellarAddress, StellarOperationMessage, StellarPaymentOp, + StellarSignTransactionRequest, + StellarSignTxMessage +} from "../models/Prokey"; +import {StellarBlockchain} from "../blockchain/servers/prokey/src/stellar/Stellar"; +import { + StellarAccountInfo, + StellarFee, StellarTransactionOperationResponse, + StellarTransactionResponse +} from "../blockchain/servers/prokey/src/stellar/StellarModels"; +import { + Account, + Asset, + Keypair, + Memo, MemoType, + Operation, + TransactionBuilder, xdr +} from "stellar-base"; +import * as PathUtil from "../utils/pathUtils"; + +const BigNumber = require('bignumber.js'); + +var WAValidator = require('multicoin-address-validator'); + +export class StellarWallet extends BaseWallet { + private readonly STELLAR_BASE_RESERVE = 0.5; + private readonly _networkPublicPassphrase = "Public Global Stellar Network ; September 2015"; + private readonly _networkTestPassphrase = "Test SDF Network ; September 2015"; + + _block_chain: StellarBlockchain; + _accounts: Array; + + constructor(device: Device, coinName: string) { + super(device, coinName, CoinBaseType.Stellar); + this._block_chain = new StellarBlockchain(this.GetCoinInfo().shortcut); + this._accounts = new Array(); + } + + public IsAddressValid(address: string): boolean { + return WAValidator.validate(address, "xlm"); + } + + /** + * discover network for collecting this device accounts information + * @param accountFindCallBack + * @returns Array an array of accounts information + */ + public async StartDiscovery(accountFindCallBack?: (accountInfo: StellarAccountInfo) => void): Promise> { + return new Promise>(async (resolve, reject) => { + let accountNumber = 0; + do { + let account = await this.GetAccountInfo(accountNumber); + if (account == null) { + return resolve(this._accounts); + } + this._accounts.push(account); + if (accountFindCallBack) { + accountFindCallBack(account); + } + accountNumber++; + } while (true); + }); + } + + /** + * get account info + * @param accountNumber account number in device + * @returns StellarAccountInfo account information + */ + public async GetAccountInfo(accountNumber: number): Promise { + let path = this.GetCoinPath(accountNumber); + let address = await this.GetAddress(path.path, false); + return await this.GetAccountInfoByAddress(address.address); + } + + public async GetAccountInfoByAddress(accountAddress: string): Promise { + return await this._block_chain.GetAddressInfo({address: accountAddress}) + } + + public async GetAccountTransactions(account: string, limit?: number, cursor?: string): Promise { + return await this._block_chain.GetAccountTransactions(account, limit, cursor); + } + + public async GetTransactionOperations(transactionId: string): Promise { + return await this._block_chain.GetTransactionOperations(transactionId); + } + + public async GetCurrentFee(): Promise { + return await this._block_chain.GetCurrentFee(); + } + + /** + * prepare payment transaction request object for sign in device and broadcast over network + * @param toAccount receiver account + * @param amount + * @param accountNumber user account number in device + * @param selectedFee user selected fee + * @returns StellarSignTransactionRequest + */ + public async GenerateTransaction(toAccount: string, amount: number, accountNumber: number, selectedFee: string, memoType: MemoType = "none", memoValue: string = ""): Promise { + // TODO: add memo latter + // Check balance + let balance = this.GetAccountBalance(accountNumber); + this.validateBalance(balance, accountNumber, amount, selectedFee); + let path = this.GetCoinPath(accountNumber).path; + const accountObject = this.GetAccount(accountNumber); + // fetch account for valid sequence + const updatedAccount = await this.GetAccountInfoByAddress(accountObject.account_id); + + if (!updatedAccount) { + throw new Error("your account is not valid"); + } + + let account = new Account(accountObject.account_id, updatedAccount.sequence.toString()); + const stellarTransactionModel = new TransactionBuilder(account, { + fee: selectedFee, + networkPassphrase: this.getNetworkPassphrase() + }) + .addOperation( + // this operation funds the new account with XLM + Operation.payment({ + destination: toAccount, + asset: Asset.native(), + amount: amount.toString() + }) + ) + .addMemo(StellarWallet.getMemo(memoType, memoValue)) + .setTimeout(180) // wait 3 min for transaction + .build(); + + return this.transformTransaction(path, stellarTransactionModel); + } + + /** + * prepare create account transaction request object for sign in device and broadcast over network + * @param toAccount requested account for creation + * @param amount + * @param accountNumber user account number in device + * @param selectedFee user selected fee + * @returns StellarSignTransactionRequest + */ + public async GenerateCreateAccountTransaction(toAccount: string, amount: number, accountNumber: number, selectedFee: string, memoType: MemoType = "none", memoValue: string = ""): Promise { + let balance = this.GetAccountBalance(accountNumber); + let path = this.GetCoinPath(accountNumber).path; + + this.validateBalance(balance, accountNumber, amount, selectedFee); + const accountObject = this.GetAccount(accountNumber); + // fetch account for valid sequence + const updatedAccount = await this.GetAccountInfoByAddress(accountObject.account_id); + + if (!updatedAccount) { + throw new Error("your account is not valid"); + } + let account = new Account(accountObject.account_id, updatedAccount.sequence.toString()); + const stellarTransactionModel = new TransactionBuilder(account, { + fee: selectedFee, + networkPassphrase: this.getNetworkPassphrase() + }) + .addOperation( + Operation.createAccount({ + destination: toAccount, + startingBalance: amount.toString() + }) + ) + .addMemo(StellarWallet.getMemo(memoType, memoValue)) + .setTimeout(180) // wait 3 min for transaction + .build(); + return this.transformTransaction(path, stellarTransactionModel); + } + + /** + * transform stellar sdk transaction to prokey transaction object + * @param path BIP path + * @param transaction stellar sdk transaction + */ + public transformTransaction(path: Array, transaction): StellarSignTransactionRequest { + const amounts = ['amount', 'sendMax', 'destAmount', 'startingBalance', 'limit']; + const assets = ['asset', 'sendAsset', 'destAsset', 'selling', 'buying', 'line']; + + const operations = transaction.operations.map((o, i) => { + const operation = {...o}; + + if (operation.signer) { + operation.signer = this.transformSigner(operation.signer); + } + + if (operation.path) { + operation.path = operation.path.map(this.transformAsset); + } + + if (typeof operation.price === 'string') { + const xdrOperation = transaction.tx.operations()[i]; + operation.price = { + n: xdrOperation.body().value().price().n(), + d: xdrOperation.body().value().price().d(), + }; + } + + amounts.forEach(field => { + if (typeof operation[field] === 'string') { + operation[field] = this.transformAmount(operation[field]); + } + }); + + assets.forEach(field => { + if (operation[field]) { + operation[field] = this.transformAsset(operation[field]); + } + }); + + if (operation.type === 'allowTrust') { + const allowTrustAsset = new Asset(operation.assetCode, operation.trustor); + operation.asset = this.transformAsset(allowTrustAsset); + } + + if (operation.type === 'manageData' && operation.value) { + // stringify is not necessary, Buffer is also accepted + operation.value = operation.value.toString('hex'); + } + + return this.transformType(operation); + }); + + let signTxMessage: StellarSignTxMessage = { + address_n: path, + source_account: transaction.source, + fee: transaction.fee, // todo: if multi operations transaction implemented this value must changes(exm: fee * operationNumber) + sequence_number: transaction.sequence, + network_passphrase: transaction.networkPassphrase, + num_operations: operations.length, + }; + this.transformTimebounds(signTxMessage, transaction.timeBounds); + this.transformMemo(signTxMessage, transaction.memo); + + return { + signTxMessage: signTxMessage, + operations: operations, + transactionModel: transaction + } + } + + public async SendTransaction(tx: string): Promise { + return await this._block_chain.BroadCastTransaction(tx); + } + + public GetAccountBalance(accountNumber: number): number { + let account = this.GetAccount(accountNumber); + let nativeBalance = account.balances.find(balance => balance.asset_type === "native"); + if (nativeBalance) { + return +nativeBalance.balance; + } + return 0; + } + + public GetAccountReserveBalance(accountNumber: number): number { + let account = this.GetAccount(accountNumber); + return (2 + account.subentry_count + account.num_sponsoring - account.num_sponsored) * this.STELLAR_BASE_RESERVE; + } + + private static getMemo(memoType: MemoType, memoValue: string): Memo { + if (memoValue == "") { + return Memo.none(); + } + switch (memoType) { + case "hash": + return Memo.hash(memoValue); + case "id": + return Memo.id(memoValue) + case "none": + return Memo.none(); + case "return": + return Memo.return(memoValue); + case "text": + return Memo.text(memoValue); + } + } + + private validateBalance(balance: number, accountNumber: number, amount: number, selectedFee: string) { + balance = balance - this.GetAccountReserveBalance(accountNumber) - amount - (+selectedFee); + if (balance < 0) + throw new Error(`Insufficient balance you need to hold ${this.GetAccountBalance(accountNumber)} XLM in your account.`); + } + + private getNetworkPassphrase() { + return this.GetCoinInfo().test ? this._networkTestPassphrase : this._networkPublicPassphrase; + } + + private GetAccount(accountNumber: number) { + if (accountNumber >= this._accounts.length) { + throw new Error('Account number is wrong'); + } + return this._accounts[accountNumber]; + } + + public GetCoinPath(accountNumber: number): AddressModel { + return PathUtil.GetBipPath( + CoinBaseType.Stellar, + accountNumber, + super.GetCoinInfo() + ); + } + + public transformSigner(signer) { + let type = 0; + let key; + const {weight} = signer; + if (typeof signer.ed25519PublicKey === 'string') { + const keyPair = Keypair.fromPublicKey(signer.ed25519PublicKey); + key = keyPair.rawPublicKey().toString('hex'); + } + if (signer.preAuthTx instanceof Buffer) { + type = 1; + key = signer.preAuthTx.toString('hex'); + } + if (signer.sha256Hash instanceof Buffer) { + type = 2; + key = signer.sha256Hash.toString('hex'); + } + return { + type, + key, + weight, + }; + } + + public transformAsset(asset) { + if (asset.isNative()) { + return { + type: 0, + code: asset.getCode(), + }; + } + return { + type: asset.getAssetType() === 'credit_alphanum4' ? 1 : 2, + code: asset.getCode(), + issuer: asset.getIssuer(), + }; + } + + public transformAmount(amount) { + return new BigNumber(amount).times(10000000).toString(); + } + + public transformType(operation): StellarOperationMessage | null { + let operationMessage: StellarOperationMessage; + switch (operation.type) { + case 'payment': + return { + type: 'StellarPaymentOp', + destination_account: operation.destination, + asset: operation.asset, + amount: operation.amount, + } + case 'createAccount': + return { + type: 'StellarCreateAccountOp', + new_account: operation.destination, + starting_balance: operation.startingBalance + } + default: + return null; + } + } + + public transformMemo(signMessage: StellarSignTxMessage, memo: Memo) { + if (memo && memo.value) { + switch (memo.type) { + case 'text': + signMessage.memo_type = 1; + signMessage.memo_text = memo.value.toString(); + break; + case 'id': + signMessage.memo_type = 2; + signMessage.memo_id = memo.value.toString(); + break; + case 'hash': + + signMessage.memo_type = 3; + signMessage.memo_hash = Buffer.from(memo.value).toString('hex'); + break; + case 'return': + signMessage.memo_type = 4; + signMessage.memo_hash = Buffer.from(memo.value).toString('hex'); + break; + default: + signMessage.memo_type = 0; + } + } else { + signMessage.memo_type = 0; + } + } + + public transformTimebounds(signMessage: StellarSignTxMessage, timebounds) { + if (!timebounds) return undefined; + signMessage.timebounds_start = Number.parseInt(timebounds.minTime, 10); + signMessage.timebounds_end = Number.parseInt(timebounds.maxTime, 10); + } +}