diff --git a/server/src/server.ts b/server/src/server.ts index 8f2b758..c8d0382 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,290 +1,297 @@ -import axios from 'axios'; +import axios from "axios"; import Big from "big.js"; import { - AuthData, - AliasAuth, - PkeyAuth, - ValidationParams, - BalanceInfo, - TxInfo, -} from './types'; - -import { ZANO_ASSET_ID, ZanoError } from './utils'; -import { APIAsset, APIBalance } from './types'; + AuthData, + AliasAuth, + PkeyAuth, + ValidationParams, + BalanceInfo, + TxInfo, +} from "./types"; +import { ZANO_ASSET_ID, ZanoError } from "./utils"; +import { APIAsset, APIBalance } from "./types"; interface ConstructorParams { - walletUrl: string; - daemonUrl: string; + walletUrl: string; + daemonUrl: string; } interface GetTxsParams { - count: number; - offset: number; - exclude_mining_txs?: boolean; - exclude_unconfirmed?: boolean; - order?: string; - update_provision_info?: boolean; + count: number; + offset: number; + exclude_mining_txs?: boolean; + exclude_unconfirmed?: boolean; + order?: string; + update_provision_info?: boolean; } class ServerWallet { - private walletUrl: string; - private daemonUrl: string; + private walletUrl: string; + private daemonUrl: string; - constructor(params: ConstructorParams) { - this.walletUrl = params.walletUrl; - this.daemonUrl = params.daemonUrl; - } + constructor(params: ConstructorParams) { + this.walletUrl = params.walletUrl; + this.daemonUrl = params.daemonUrl; + } - private async fetchDaemon(method: string, params: any) { - const headers = { "Content-Type": "application/json" }; + private async fetchDaemon(method: string, params: any) { + const headers = { "Content-Type": "application/json" }; - const data = { - jsonrpc: "2.0", - id: 0, - method: method, - params: params - }; - - return axios.post(this.daemonUrl, data, { headers }) - } - - private async fetchWallet(method: string, params: any) { - const headers = { "Content-Type": "application/json" }; - - const data = { - jsonrpc: "2.0", - id: 0, - method: method, - params: params - }; - - return axios.post(this.walletUrl, data, { headers }) - } - - async updateWalletRpcUrl(rpcUrl: string) { - this.walletUrl = rpcUrl; - } - - async updateDaemonRpcUrl(rpcUrl: string) { - this.daemonUrl = rpcUrl; - } - - async getAssetsList() { - const count = 100; // Number of items to fetch per request - let offset = 0; - let allAssets: APIAsset[] = []; - let keepFetching = true; - - while (keepFetching) { - try { - const response = await this.fetchDaemon("get_assets_list", { count, offset }); - - const assets = response.data.result.assets; - if (assets.length < count) { - keepFetching = false; - } - allAssets = allAssets.concat(assets); - offset += count; - } catch (error) { - throw new ZanoError("Failed to fetch assets list", "ASSETS_FETCH_ERROR"); - } - } - - return allAssets as APIAsset[]; + const data = { + jsonrpc: "2.0", + id: 0, + method: method, + params: params, }; - async getAssetDetails(assetId: string) { - const assets = await this.getAssetsList(); - const asset = assets.find((a) => a.asset_id === assetId); + return axios.post(this.daemonUrl, data, { headers }); + } - if (!asset) { - throw new ZanoError( - `Asset with ID ${assetId} not found`, - "ASSET_NOT_FOUND" - ); - } + private async fetchWallet(method: string, params: any) { + const headers = { "Content-Type": "application/json" }; - return asset as APIAsset; - } - - - async getAssetInfo(assetId: string) { - try { - const response = await this.fetchDaemon("get_asset_info", { asset_id: assetId }); - - if (response.data.result) { - return response.data.result; - } else { - throw new ZanoError( - `Error fetching info for asset ID ${assetId}`, - "ASSET_INFO_ERROR" - ); - } - } catch (error) { - console.error(error); - throw new ZanoError("Failed to fetch asset info", "ASSET_INFO_FETCH_ERROR"); - } + const data = { + jsonrpc: "2.0", + id: 0, + method: method, + params: params, }; - async sendTransfer(assetId: string, address: string, amount: string) { - let decimalPoint: number; + return axios.post(this.walletUrl, data, { headers }); + } - if (assetId === ZANO_ASSET_ID) { - decimalPoint = 12; - } else { - const asset = await this.getAssetDetails(assetId); - decimalPoint = asset.decimal_point; - } + async updateWalletRpcUrl(rpcUrl: string) { + this.walletUrl = rpcUrl; + } - const bigAmount = new Big(amount) - .times(new Big(10).pow(decimalPoint)) - .toString(); + async updateDaemonRpcUrl(rpcUrl: string) { + this.daemonUrl = rpcUrl; + } - try { - const response = await this.fetchWallet("transfer", { - destinations: [{ address, amount: bigAmount, asset_id: assetId }], - fee: "10000000000", - mixin: 15, - }); + async getAssetsList() { + const count = 100; + let offset = 0; + let allAssets: APIAsset[] = []; + let keepFetching = true; - if (response.data.result) { - return response.data.result; - } else if ( - response.data.error && - response.data.error.message === "WALLET_RPC_ERROR_CODE_NOT_ENOUGH_MONEY" - ) { - throw new ZanoError("Not enough funds", "NOT_ENOUGH_FUNDS"); - } else { - throw new ZanoError("Error sending transfer", "TRANSFER_ERROR"); - } - - } catch (error) { - if (error instanceof ZanoError) { - throw error; // Re-throw the custom error - } else { - throw new ZanoError("Failed to send transfer", "TRANSFER_SEND_ERROR"); - } - } - }; - - async getAliasByAddress(address: string) { - try { - const response = await this.fetchDaemon("get_alias_by_address", address); - - if (response.data.result) { - return response.data.result; - } else { - throw new ZanoError( - `Error fetching alias for address ${address}`, - "ALIAS_FETCH_ERROR" - ); - } - } catch (error) { - throw new ZanoError("Failed to fetch alias", "ALIAS_FETCH_ERROR"); - } - } - - async getBalances() { - try { - const response = await this.fetchWallet("getbalance", {}); - const balancesData = response.data.result.balances as APIBalance[]; - - - const balances = balancesData.map((asset) => ({ - name: asset.asset_info.full_name, - ticker: asset.asset_info.ticker, - id: asset.asset_info.asset_id, - amount: new Big(asset.unlocked) - .div(new Big(10).pow(asset.asset_info.decimal_point)) - .toString(), - awaiting_in: new Big(asset.awaiting_in).toString(), - awaiting_out: new Big(asset.awaiting_out).toString(), - total: new Big(asset.total).toString(), - unlocked: new Big(asset.unlocked).toString(), - asset_info: asset.asset_info - })); - - return balances.sort((a, b) => { - if (a.id === ZANO_ASSET_ID) - return -1; - if (b.id === ZANO_ASSET_ID ) - return 1; - return 0; - }) as BalanceInfo[]; - - } catch (error) { - throw new ZanoError("Failed to fetch balances", "BALANCES_FETCH_ERROR"); - } - }; - - async validateWallet(authData: AuthData) { - - const { message, address, signature } = authData; - - const alias = (authData as AliasAuth).alias || undefined; - const pkey = (authData as PkeyAuth).pkey || undefined; - - if (!message || (!alias && !pkey) || !signature) { - return false; - } - - const validationParams: ValidationParams = { - "buff": Buffer.from(message).toString("base64"), - "sig": signature - }; - - if (alias) { - validationParams['alias'] = alias; - } else { - validationParams['pkey'] = pkey; - } - - const response = await this.fetchDaemon( - 'validate_signature', - validationParams - ); - - const valid = response?.data?.result?.status === 'OK'; - - if (!valid) { - return false; - } - - if (alias) { - const aliasDetailsResponse = await this.fetchDaemon( - 'get_alias_details', - { - "alias": alias, - } - ); - - const aliasDetails = aliasDetailsResponse?.data?.result?.alias_details; - const aliasAddress = aliasDetails?.address; - - const addressValid = !!aliasAddress && aliasAddress === address; - - if (!addressValid) { - return false; - } - } - - return valid; - } - - async getTxs(params: GetTxsParams) { - const txs = await this.fetchWallet("get_recent_txs_and_info2", { - "count": params.count, - "exclude_mining_txs": params.exclude_mining_txs || false, - "exclude_unconfirmed": params.exclude_unconfirmed || false, - "offset": params.offset, - "order": params.order || "FROM_END_TO_BEGIN", - "update_provision_info": params.update_provision_info || true + while (keepFetching) { + try { + const response = await this.fetchDaemon("get_assets_list", { + count, + offset, }); - return txs.data.result as TxInfo; + const assets = response.data.result.assets; + if (assets.length < count) { + keepFetching = false; + } + allAssets = allAssets.concat(assets); + offset += count; + } catch (error) { + throw new ZanoError( + "Failed to fetch assets list", + "ASSETS_FETCH_ERROR" + ); + } } + + return allAssets as APIAsset[]; + } + + async getAssetDetails(assetId: string) { + const assets = await this.getAssetsList(); + const asset = assets.find((a) => a.asset_id === assetId); + + if (!asset) { + throw new ZanoError( + `Asset with ID ${assetId} not found`, + "ASSET_NOT_FOUND" + ); + } + + return asset as APIAsset; + } + + async getAssetInfo(assetId: string) { + try { + const response = await this.fetchDaemon("get_asset_info", { + asset_id: assetId, + }); + + if (response.data.result) { + return response.data.result; + } else { + throw new ZanoError( + `Error fetching info for asset ID ${assetId}`, + "ASSET_INFO_ERROR" + ); + } + } catch (error) { + console.error(error); + throw new ZanoError( + "Failed to fetch asset info", + "ASSET_INFO_FETCH_ERROR" + ); + } + } + + async sendTransfer(assetId: string, address: string, amount: string) { + let decimalPoint: number; + let auditable: boolean; + + if (assetId === ZANO_ASSET_ID) { + decimalPoint = 12; + } else { + const asset = await this.getAssetDetails(assetId); + decimalPoint = asset.decimal_point; + } + + try { + const response = await this.fetchWallet("getaddress", {}); + auditable = response.data.result.address.startsWith("a"); + } catch (error) { + throw new ZanoError("Failed to fetch address", "ADDRESS_FETCH_ERROR"); + } + + const bigAmount = new Big(amount) + .times(new Big(10).pow(decimalPoint)) + .toString(); + + try { + const response = await this.fetchWallet("transfer", { + destinations: [{ address, amount: bigAmount, asset_id: assetId }], + fee: "10000000000", + mixin: auditable ? 0 : 15, + }); + + if (response.data.result) { + return response.data.result; + } else if ( + response.data.error && + response.data.error.message === "WALLET_RPC_ERROR_CODE_NOT_ENOUGH_MONEY" + ) { + throw new ZanoError("Not enough funds", "NOT_ENOUGH_FUNDS"); + } else { + throw new ZanoError("Error sending transfer", "TRANSFER_ERROR"); + } + } catch (error) { + if (error instanceof ZanoError) { + throw error; + } else { + throw new ZanoError("Failed to send transfer", "TRANSFER_SEND_ERROR"); + } + } + } + + async getAliasByAddress(address: string) { + try { + const response = await this.fetchDaemon("get_alias_by_address", address); + + if (response.data.result) { + return response.data.result; + } else { + throw new ZanoError( + `Error fetching alias for address ${address}`, + "ALIAS_FETCH_ERROR" + ); + } + } catch (error) { + throw new ZanoError("Failed to fetch alias", "ALIAS_FETCH_ERROR"); + } + } + + async getBalances() { + try { + const response = await this.fetchWallet("getbalance", {}); + const balancesData = response.data.result.balances as APIBalance[]; + + const balances = balancesData.map((asset) => ({ + name: asset.asset_info.full_name, + ticker: asset.asset_info.ticker, + id: asset.asset_info.asset_id, + amount: new Big(asset.unlocked) + .div(new Big(10).pow(asset.asset_info.decimal_point)) + .toString(), + awaiting_in: new Big(asset.awaiting_in).toString(), + awaiting_out: new Big(asset.awaiting_out).toString(), + total: new Big(asset.total).toString(), + unlocked: new Big(asset.unlocked).toString(), + asset_info: asset.asset_info, + })); + + return balances.sort((a, b) => { + if (a.id === ZANO_ASSET_ID) return -1; + if (b.id === ZANO_ASSET_ID) return 1; + return 0; + }) as BalanceInfo[]; + } catch (error) { + throw new ZanoError("Failed to fetch balances", "BALANCES_FETCH_ERROR"); + } + } + + async validateWallet(authData: AuthData) { + const { message, address, signature } = authData; + + const alias = (authData as AliasAuth).alias || undefined; + const pkey = (authData as PkeyAuth).pkey || undefined; + + if (!message || (!alias && !pkey) || !signature) { + return false; + } + + const validationParams: ValidationParams = { + buff: Buffer.from(message).toString("base64"), + sig: signature, + }; + + if (alias) { + validationParams["alias"] = alias; + } else { + validationParams["pkey"] = pkey; + } + + const response = await this.fetchDaemon( + "validate_signature", + validationParams + ); + + const valid = response?.data?.result?.status === "OK"; + + if (!valid) { + return false; + } + + if (alias) { + const aliasDetailsResponse = await this.fetchDaemon("get_alias_details", { + alias: alias, + }); + + const aliasDetails = aliasDetailsResponse?.data?.result?.alias_details; + const aliasAddress = aliasDetails?.address; + + const addressValid = !!aliasAddress && aliasAddress === address; + + if (!addressValid) { + return false; + } + } + + return valid; + } + + async getTxs(params: GetTxsParams) { + const txs = await this.fetchWallet("get_recent_txs_and_info2", { + count: params.count, + exclude_mining_txs: params.exclude_mining_txs || false, + exclude_unconfirmed: params.exclude_unconfirmed || false, + offset: params.offset, + order: params.order || "FROM_END_TO_BEGIN", + update_provision_info: params.update_provision_info || true, + }); + + return txs.data.result as TxInfo; + } } - -export default ServerWallet; \ No newline at end of file +export default ServerWallet;