UNPKG

edge-core-js

Version:

Edge account & wallet management library

449 lines (392 loc) 12.9 kB
// @flow import type { Cleaner } from "cleaners"; import { asMaybe } from "cleaners"; import { base64 } from "rfc4648"; import { asOtpErrorPayload, asPasswordErrorPayload } from "./server-cleaners"; import type { ChallengeErrorPayload } from "./server-types"; import type { EdgeSwapInfo, EdgeSwapRequest, EdgeTokenId } from "./types"; /* * These are errors the core knows about. * * The GUI should handle these errors in an "intelligent" way, such as by * displaying a localized error message or asking the user for more info. * All these errors have a `name` field, which the GUI can use to select * the appropriate response. * * Other errors are possible, of course, since the Javascript language * itself can generate exceptions. Those errors won't have a `type` field, * and the GUI should just show them with a stack trace & generic message, * since the program has basically crashed at that point. */ /** * Thrown when the login server requires a CAPTCHA. * * After showing the WebView with the challengeUri, * pass the challengeId to the login method * (such as loginWithPassword) to complete the login. * * The challengeUri web page will signal that it is done by navigating * to a new location that ends with either /success or /failure, * such as https://login.edge.app/challenge/success * The login UI can use this as a signal to close the WebView. */ export class ChallengeError extends Error { name: string; challengeId: string; challengeUri: string; constructor( resultsJson: ChallengeErrorPayload, message: string = "Login requires a CAPTCHA", ) { super(message); this.name = "ChallengeError"; this.challengeId = resultsJson.challengeId; this.challengeUri = resultsJson.challengeUri; } } /** * Trying to spend an uneconomically small amount of money. */ export class DustSpendError extends Error { name: string; constructor(message: string = "Please send a larger amount") { super(message); this.name = "DustSpendError"; } } type InsufficientFundsErrorOpts = { // The currency we need more of: tokenId: EdgeTokenId; // If we don't have enough funds for a token send: networkFee?: string; } /** * Trying to spend more money than the wallet contains. */ export class InsufficientFundsError extends Error { name: string; +tokenId: EdgeTokenId; +networkFee: string | void; // Passing a string is deprecated constructor(opts: InsufficientFundsErrorOpts) { const { tokenId = null, networkFee } = opts ?? {}; super(`Insufficient ${tokenId ?? "funds"}`); this.tokenId = tokenId; this.networkFee = networkFee; this.name = "InsufficientFundsError"; } } /** * Could not reach the server at all. */ export class NetworkError extends Error { name: string; constructor(message: string = "Cannot reach the network") { super(message); this.name = "NetworkError"; } } /** * Attempting to create a MakeSpend without specifying an amount of currency to send */ export class NoAmountSpecifiedError extends Error { name: string; constructor(message: string = "Unable to create zero-amount transaction.") { super(message); this.name = "NoAmountSpecifiedError"; } } /** * The endpoint on the server is obsolete, and the app needs to be upgraded. */ export class ObsoleteApiError extends Error { name: string; constructor(message: string = "The application is too old. Please upgrade.") { super(message); this.name = "ObsoleteApiError"; } } /** * The OTP token was missing / incorrect. * * The error object should include a `resetToken` member, * which can be used to reset OTP protection on the account. * * The error object may include a `resetDate` member, * which indicates that an OTP reset is already pending, * and when it will complete. */ export class OtpError extends Error { name: string; +loginId: string | void; // base64, to avoid a breaking change +reason: "ip" | "otp"; +resetDate: Date | void; +resetToken: string | void; +voucherId: string | void; +voucherAuth: string | void; // base64, to avoid a breaking change +voucherActivates: Date | void; constructor(resultsJson: mixed, message: string = "Invalid OTP token") { super(message); this.name = "OtpError"; this.reason = "otp"; const clean = asMaybe(asOtpErrorPayload)(resultsJson); if (clean == null) return; if (clean.login_id != null) { this.loginId = base64.stringify(clean.login_id); } this.resetToken = clean.otp_reset_auth; this.reason = clean.reason; this.resetDate = clean.otp_timeout_date; this.voucherActivates = clean.voucher_activates; if (clean.voucher_auth != null) { this.voucherAuth = base64.stringify(clean.voucher_auth); } this.voucherId = clean.voucher_id; } } /** * The provided authentication is incorrect. * * Reasons could include: * - Password login: wrong password * - PIN login: wrong PIN * - Recovery login: wrong answers * * The error object may include a `wait` member, * which is the number of seconds the user must wait before trying again. */ export class PasswordError extends Error { name: string; +wait: number | void; // seconds constructor(resultsJson: mixed, message: string = "Invalid password") { super(message); this.name = "PasswordError"; const clean = asMaybe(asPasswordErrorPayload)(resultsJson); if (clean == null) return; this.wait = clean.wait_seconds; } } /** * PIN login is not enabled for this account on this device. */ export class PinDisabledError extends Error { name: string; constructor(message: string) { super(message); this.name = "PinDisabledError"; } } /** * Trying to spend funds that are not yet confirmed. */ export class PendingFundsError extends Error { name: string; constructor(message: string = "Not enough confirmed funds") { super(message); this.name = "PendingFundsError"; } } /** * Attempting to shape shift between two wallets of same currency. */ export class SameCurrencyError extends Error { name: string; constructor(message: string = "Wallets can not be the same currency") { super(message); this.name = "SameCurrencyError"; } } /** * Trying to spend to an address of the source wallet */ export class SpendToSelfError extends Error { name: string; constructor(message: string = "Spending to self") { super(message); this.name = "SpendToSelfError"; } } /** * Trying to swap an amount that is either too low or too high. * @param nativeMax the maximum supported amount, in the currency specified * by the direction (defaults to "from" currency) */ export class SwapAboveLimitError extends Error { name: string; /** @deprecated use swapPluginId */ +pluginId: string; +swapPluginId: string; /** This will be '' if the limit is not known */ +nativeMax: string; +direction: "from" | "to"; constructor( swapInfo: EdgeSwapInfo, nativeMax: string | void, direction: "from" | "to" = "from", ) { super("Amount is too high"); this.name = "SwapAboveLimitError"; this.pluginId = swapInfo.pluginId; this.swapPluginId = swapInfo.pluginId; this.nativeMax = nativeMax ?? ""; this.direction = direction; } } /** * Trying to swap an amount that is either too low or too high. * @param nativeMin the minimum supported amount, in the currency specified * by the direction (defaults to "from" currency) */ export class SwapBelowLimitError extends Error { name: string; /** @deprecated use swapPluginId */ +pluginId: string; +swapPluginId: string; /** This will be '' if the limit is not known */ +nativeMin: string; +direction: "from" | "to"; constructor( swapInfo: EdgeSwapInfo, nativeMin: string | void, direction: "from" | "to" = "from", ) { super("Amount is too low"); this.name = "SwapBelowLimitError"; this.pluginId = swapInfo.pluginId; this.swapPluginId = swapInfo.pluginId; this.nativeMin = nativeMin ?? ""; this.direction = direction; } } /** * The swap plugin does not support this currency pair. */ export class SwapCurrencyError extends Error { name: string; +pluginId: string; +fromTokenId: EdgeTokenId; +toTokenId: EdgeTokenId; constructor(swapInfo: EdgeSwapInfo, request: EdgeSwapRequest) { const { fromWallet, toWallet, fromTokenId, toTokenId } = request; const fromPluginId = fromWallet.currencyConfig.currencyInfo.pluginId; const toPluginId = toWallet.currencyConfig.currencyInfo.pluginId; const fromString = `${fromPluginId}:${String(fromTokenId)}`; const toString = `${toPluginId}:${String(toTokenId)}`; super( `${swapInfo.displayName} does not support ${fromString} to ${toString}`, ); this.name = "SwapCurrencyError"; this.pluginId = swapInfo.pluginId; this.fromTokenId = fromTokenId ?? null; this.toTokenId = toTokenId ?? null; } } type SwapPermissionReason = | "geoRestriction" | "noVerification" | "needsActivation"; /** * The user is not allowed to swap these coins for some reason * (no KYC, restricted IP address, etc...). * @param reason A string giving the reason for the denial. * - 'geoRestriction': The IP address is in a restricted region * - 'noVerification': The user needs to provide KYC credentials * - 'needsActivation': The user needs to log into the service. */ export class SwapPermissionError extends Error { name: string; +pluginId: string; +reason: SwapPermissionReason | void; constructor(swapInfo: EdgeSwapInfo, reason?: SwapPermissionReason) { if (reason != null) super(reason); else super("You are not allowed to make this trade"); this.name = "SwapPermissionError"; this.pluginId = swapInfo.pluginId; this.reason = reason; } } // Address requirements for certain swap flows (extend as needed): export type SwapAddressReason = "mustMatch" | "mustBeActivated"; export class SwapAddressError extends Error { name: string; +swapPluginId: string; +reason: SwapAddressReason; constructor(swapInfo: EdgeSwapInfo, opts: { reason: SwapAddressReason }) { const { reason } = opts; switch (reason) { case "mustMatch": super( "This swap requires from and to wallets to have the same address", ); break; case "mustBeActivated": super("The destination wallet must be activated to receive this swap."); break; default: super("Invalid swap address"); } this.name = "SwapAddressError"; this.swapPluginId = swapInfo.pluginId; this.reason = reason; } } /** * Cannot find a login with that id. * * Reasons could include: * - Password login: wrong username * - PIN login: wrong PIN key * - Recovery login: wrong username, or wrong recovery key */ export class UsernameError extends Error { name: string; constructor(message: string = "Invalid username") { super(message); this.name = "UsernameError"; } } function asMaybeError<T>(name: string): Cleaner<T | void> { return function asError(raw) { if (raw instanceof Error && raw.name === name) { const typeHack: any = raw; return typeHack; } }; } export const asMaybeChallengeError = asMaybeError<ChallengeError>("ChallengeError"); export const asMaybeDustSpendError = asMaybeError<DustSpendError>("DustSpendError"); export const asMaybeInsufficientFundsError = asMaybeError<InsufficientFundsError>("InsufficientFundsError"); export const asMaybeNetworkError = asMaybeError<NetworkError>("NetworkError"); export const asMaybeNoAmountSpecifiedError = asMaybeError<NoAmountSpecifiedError>("NoAmountSpecifiedError"); export const asMaybeObsoleteApiError = asMaybeError<ObsoleteApiError>("ObsoleteApiError"); export const asMaybeOtpError = asMaybeError<OtpError>("OtpError"); export const asMaybePasswordError = asMaybeError<PasswordError>("PasswordError"); export const asMaybePinDisabledError = asMaybeError<PinDisabledError>("PinDisabledError"); export const asMaybePendingFundsError = asMaybeError<PendingFundsError>("PendingFundsError"); export const asMaybeSameCurrencyError = asMaybeError<SameCurrencyError>("SameCurrencyError"); export const asMaybeSpendToSelfError = asMaybeError<SpendToSelfError>("SpendToSelfError"); export const asMaybeSwapAboveLimitError = asMaybeError<SwapAboveLimitError>( "SwapAboveLimitError", ); export const asMaybeSwapBelowLimitError = asMaybeError<SwapBelowLimitError>( "SwapBelowLimitError", ); export const asMaybeSwapCurrencyError = asMaybeError<SwapCurrencyError>("SwapCurrencyError"); export const asMaybeSwapPermissionError = asMaybeError<SwapPermissionError>( "SwapPermissionError", ); export const asMaybeSwapAddressError = asMaybeError<SwapAddressError>("SwapAddressError"); export const asMaybeUsernameError = asMaybeError<UsernameError>("UsernameError");