@elevenyellow.com/ark-switchain-plugin
Version:
Switchain plugin on Ark Desktop Wallet
1,138 lines (1,118 loc) • 45.8 kB
JavaScript
const ApiWorker = require("../apiWorker");
const style = require("./mainPageStyles");
const stepperStyle = require("./stepperStyles");
const { defaultFrom, defaultTo } = require("../constants");
const {
errorType,
longName,
statuses,
finishedStatuses,
exchangingStatuses,
symbolsWithTag
} = require("../constants");
const {
valiateAddress,
valiateExternalId,
pairToObject
} = require("../utils/validators");
const { getLink } = require("../utils/links");
const exchangeInterval = 5000;
const {
pluginContainer,
mainPageHeader,
exchangeInputTitle,
sequenceBlock,
formContainer,
toggleButton,
coinIcon,
selectFromWrapper,
searchInput,
currencyListContainer,
currencyList,
currencyItem,
coinName,
coinTicker,
inputLoader,
sreachIcon,
subName
} = style;
const {
arrow,
Stepper,
stepContainer,
stepHeader,
stepNumber,
stepName,
stepBody,
formBlock,
input,
inputWrapper,
exchangeInputSearch,
circle,
line,
exchangeSequence,
selectContainer,
addressInput,
addressInputBody,
inputSuccesValid,
addressInputLabel,
addressInputWrapper,
stepButton,
buttonGreen,
buttonWhite,
disabledButton,
confirmInfoLabel,
confirmInfoSub,
confirmInfoData,
confirmInfoAmount,
confirmArrow,
confirmCheckboxWrapper,
checkboxBody,
checkbox,
checkboxChecked,
buttonsBlock,
refundButton,
inputError,
exchangeInputError,
stepThreeBlock,
infoHeader,
infoContent,
bigLoader,
smallStep,
smallStepHeader,
smallStepNumber,
smallStepName,
stepHeaderText,
smallStepInfoIcon,
smallStepInfoItem,
stepInfoHead,
transactionSuccessIcon
} = stepperStyle;
const {
faArrowsAltV,
faSpinner,
faLongArrowAltDown,
faLongArrowAltUp,
faSearch,
faArrowRight,
faCheck,
faCheckCircle,
faTimesCircle,
faCircleNotch,
faCircle,
faExternalLinkAlt
} = walletApi.fontAwesomeIcons;
module.exports = {
template: `
<div class="rounded-lg px-0 py-2 flex flex-col" style="${pluginContainer}" @click="outSideClick">
<div style="${mainPageHeader}">
<img src="https://www.switchain.com/img/logo.svg" id="sw-logo">
<div class="sm:block hidden" style="width: 1px; height: 40px; margin: 0 15px 0 10px;"></div>
<div class="sm:block hidden" style="color: #FFF; font-size: 14px; margin-top:10px">best cryptocurrency exchange rates</div>
</div>
<div style="${formContainer}" class="px-3">
<div v-if="initializing">
<Loader />
</div>
<div v-else style="${Stepper}">
<div v-if="currentStep === 1" style="${stepContainer}">
<div style="${stepHeader}">
<div style="${stepNumber}">1</div>
<span style="${stepName}">Send To</span>
</div>
<div style="${stepBody}">
<div style="${formBlock}">
<div style="${inputWrapper}">
<div v-if="amountError" style="${exchangeInputError}">{{renderFromLabel}}</div>
<div v-else style="${exchangeInputTitle}">You send</div>
<input type="text" v-model.number="amount" @keyup="startRecount" style="${input}" @input="isNumber($event)"/>
<div class="cursor-pointer" style="${exchangeInputSearch}" ref="fromSearchBtn" @click="openSelectFrom">
<span v-if="from">
{{fromTicker}}
</span>
<span v-else class='currency-coin-ticker coin-ticker-to'>
{{fromTicker}}
</span>
<div class="currencies-container currencies-to-container"></div>
<div style="${arrow}"></div>
</div>
<div style="${selectContainer} display: none;" ref="currencySelectFrom">
<div style="${sreachIcon}"><font-awesome-icon :icon="faSearch" size="lg"/></div>
<input type="text" style="${searchInput}" ref="searchFrom" v-model="fromFilter">
<div style="${currencyListContainer}">
<ul v-if="isListFromOpen" style="${currencyList}">
<li v-for="fromCurrency in filtredFrom" style="${currencyItem}" v-bind:key="fromCurrency"
class="hover:shadow-md" @click="() => selectCoinFrom(fromCurrency)">
<span style="${coinTicker}">{{fromCurrency}}</span>
<span style="${coinName}">{{fromCurrency}}</span>
</li>
</ul>
</div>
</div>
</div>
<div style="${sequenceBlock}">
<div style="${circle}"></div>
<div style="${line}"></div>
<span style="${exchangeSequence}">{{sequence}}</span>
<v-popover
offset="1"
>
<button class="pl-3" style="padding-bottom: 4px; color: #3bee81; font-size: 12px;">Expected rate</button>
<template slot="popover" >
<div style="background-color: white; max-width: 250px; padding: 20px; border-radius: 3px; box-shadow: 0 4px 20px rgba(0,0,0,.45);">
<h4 style="color: #5c5780; font-size: 16px; margin-bottom: 10px;">This is an expected rate</h4>
<p style="color: #2b2b37; font-size: 14px; margin: 20px 0;">
Switchain will pick the best rate for you during the moment of the exchange.
</p>
<a href="https://www.switchain.com/faq"
style="color: #3bee81; font-size: 12px;" target="_blank">
<div class="flex items-center">
Learn More
<div style="font-size: 6px; margin-left: 3px; padding-bottom: 2px;">
<font-awesome-icon :icon="faExternalLinkAlt" size="lg"/>
</div>
</div>
</a>
</div>
</template>
</v-popover>
<div style="${toggleButton}" @click="toggleCurrancies">
<font-awesome-icon :icon="upArrow" size="lg"/>
<font-awesome-icon :icon="downArrow" size="lg"/>
</div>
</div>
<div style="${inputWrapper}">
<div style="${exchangeInputTitle}">You get</div>
<input type="text" disabled style="${input}" :value="isCounting ? '' : amountTo"/>
<span v-if="isCounting" style="${inputLoader}">
<font-awesome-icon :icon="spinner" size="lg" rotation="180" spin/>
</span>
<div class="cursor-pointer" style="${exchangeInputSearch}" ref="toSearchBtn" @click="openSelectTo">
<span v-if="to">
{{toTicker}}
</span>
<span v-else>
{{toTicker}}
</span>
<div class="currencies-container currencies-to-container"></div>
<div style="${arrow}"></div>
</div>
<div style="${selectContainer} display: none;" ref="currencySelectTo">
<div style="${sreachIcon}"><font-awesome-icon :icon="faSearch" size="lg"/></div>
<input type="text" style="${searchInput}" ref="searchTo" v-model="toFilter">
<div style="${currencyListContainer}">
<ul v-if="isListToOpen" style="${currencyList}">
<li v-for="toCurrency in filtredTo" style="${currencyItem}" v-bind:key="toCurrency"
class="hover:shadow-md" @click="() => selectCoinTo(toCurrency)">
<span style="${coinTicker}">{{toCurrency}}</span>
<span style="${coinName}">{{toCurrency}}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div v-if="to === 'ark' && arkWallets.length" class="relative" style="${addressInputBody}">
<span>Recipient Wallet</span>
<InputSelect :items="arkWallets" label="" name="ArkWallets" v-model="selectValue" v-on:input="setArkAddress"/>
<p v-if="recipientWallet && !isValidRecipient && !recipientFocus"
class="text-xs"
style="${inputError}">
This address is not valid
</p>
</div>
<div v-else class="relative" style="${addressInputBody}">
<span style="${addressInputLabel}">Recipient Wallet</span>
<div style="${addressInputWrapper}">
<input
type="text"
v-model="recipientWallet"
@blur="() => recipientFocus = false"
@focus="() => recipientFocus = true"
class="border border-solid focus:border-green border-gray-400"
style="${addressInput}"
:placeholder="recipientPlace"/>
<div v-if="recipientWallet && isValidRecipient" style="${inputSuccesValid}"><font-awesome-icon :icon="faCheck" size="lg"/></div>
</div>
<p v-if="recipientWallet && !isValidRecipient && !recipientFocus"
class="text-xs"
style="${inputError}">
This address is not valid
</p>
</div>
<div v-if="to && toHasExternalId" class="relative" style="${addressInputBody}">
<div style="${addressInputWrapper}">
<input
type="text"
v-model="toExternalId"
@blur="() => externalIdFocus = false"
@focus="() => externalIdFocus = true"
class="border border-solid focus:border-green border-gray-400"
style="${addressInput}"
placeholder="(Memo, tag...)"/>
<div v-if="to && isValidToExternalId" style="${inputSuccesValid}"><font-awesome-icon :icon="faCheck" size="lg"/></div>
</div>
<p v-if="toExternalId && !isValidToExternalId && !externalIdFocus"
class="text-xs" style="${inputError}">{{externalIdValidError}}</p>
</div>
<div v-if="from === 'ark' && arkWallets.length" class="relative" style="${addressInputBody}">
<span>Refund Wallet</span>
<InputSelect :items="arkWallets" label="" name="ArkWallets" v-model="selectRefundValue" v-on:input="setRefundArkAddress"/>
<p v-if="refundWallet && !isValidRefund && !refundFocus"
class="text-xs"
style="${inputError}">
This address is not valid
</p>
</div>
<div v-if="from" class="relative" style="${addressInputBody}">
<span style="${addressInputLabel}">Refund Wallet</span>
<div style="${addressInputWrapper}">
<input
type="text"
v-model="refundWallet"
@blur="() => refundFocus = false"
@focus="() => refundFocus = true"
style="${addressInput}"
class="border border-solid focus:border-green border-gray-400"
:placeholder="refundPlace"/>
<div v-if="refundWallet && isValidRefund" style="${inputSuccesValid}"><font-awesome-icon :icon="faCheck" size="lg"/></div>
</div>
<p v-if="refundWallet && !isValidRefund && !refundFocus"
class="text-xs" style="${inputError}">This address is not valid</p>
</div>
<div v-if="from && fromHasExternalId" class="relative" style="${addressInputBody}">
<div style="${addressInputWrapper}">
<input
type="text"
v-model="fromExternalId"
@blur="() => externalIdFocus = false"
@focus="() => externalIdFocus = true"
class="border border-solid focus:border-green border-gray-400"
style="${addressInput}"
placeholder="(Memo, tag...)"/>
<div v-if="from && isValidFromExternalId" style="${inputSuccesValid}"><font-awesome-icon :icon="faCheck" size="lg"/></div>
</div>
<p v-if="fromExternalId && !isValidFromExternalId && !externalIdFocus"
class="text-xs" style="${inputError}">{{externalIdValidError}}</p>
</div>
</div>
<div style="${buttonsBlock}">
<button v-if="!validParams" style="${stepButton} ${disabledButton}"
class="hover:opacity-75 disabled:bg-gray" :disabled="!validParams"
>Next</button>
<button v-else style="${stepButton} ${buttonGreen}"
class="hover:opacity-75 disabled:bg-gray" :disabled="!validParams"
@click.prevent="switchToTwoStep">Next</button>
<router-link :to="{ name: 'switchain'}">
<button class="hover:opacity-75" style="${stepButton} ${buttonWhite}">Back</button>
</router-link>
</div>
</div>
<div v-if="currentStep === 2" style="${stepContainer}">
<div style="${stepHeader}">
<div style="${stepNumber}">2</div>
<span style="${stepName}">Confirmation</span>
</div>
<div style="${stepBody}">
<div class="flex flex-col md:flex-row md:items-center">
<div style="${confirmInfoData}" class="pr-6">
<span style="${confirmInfoLabel}">You Send</span>
<span style="${confirmInfoAmount}">{{amount}} {{fromTicker}}</span>
<span style="${confirmInfoSub}">{{sequence}}</span>
</div>
<div style="${confirmArrow}" class="md:block hidden">
<font-awesome-icon :icon="faArrowRight" size="lg"/>
</div>
<div style="${confirmInfoData}" class="md:pl-6">
<span style="${confirmInfoLabel}">You Get</span>
<span style="${confirmInfoAmount}">≈ {{amountTo}} {{toTicker}}</span>
<span style="${confirmInfoSub}">{{recipientWallet}}</span>
</div>
</div>
<div style="margin: 10px 0;" class="flex">
<div style="margin-right: 30px;">
<p style="${confirmInfoLabel} margin-bottom: 3px;">Estimated Arrival</p>
<p style="${confirmInfoSub}">≈ {{transactionTime}} minutes</p>
</div>
</div>
</div>
<div style="${confirmCheckboxWrapper}">
<label style="${checkboxBody}" class="cursor-pointer">
<input type="checkbox" v-model="confirm" style="${checkbox}">
<span v-if="confirm" style="${checkboxChecked}"><font-awesome-icon :icon="faCheck" size="lg"/></span>
</label>
<div style="confirmText">
<span>I've read and agree to the Switchain
<a class="no-underline" style="color: #3bee81;" href="https://www.switchain.com/tos" target="blank">Terms of Use</a> and
<a class="no-underline" style="color: #3bee81;" href="https://www.switchain.com/policy" target="blank">KYC/AML Policy</a>
</span>
</div>
</div>
<div style="${buttonsBlock}">
<button v-if="!confirm || creating" style="${stepButton} ${disabledButton}" :disabled="!confirm">Confirm</button>
<button v-else style="${stepButton} ${buttonGreen}" @click.prevent="createExchange">Confirm</button>
<button style="${stepButton} ${buttonWhite}" @click.prevent="switchToOneStep" :disabled="creating">Back</button>
</div>
<div v-if="creating" style="${bigLoader}">
<font-awesome-icon :icon="faCircleNotch" size="lg" rotation="180" spin style="color: #3bee81;"/>
</div>
</div>
<div v-if="currentStep === 3 && transaction" style="${stepContainer}">
<div style="${stepHeader} font-size: 16px;" class="relative">
<div style="${stepNumber}">3</div>
<span style="${stepName} color: #a4a3aa">Sending</span>
<span class="m-4" style="color: #a4a3aa; font-size: 16px;">Transaction Id: {{transaction.orderId}}</span>
<button style="margin-left: auto; color: #3bee81;" @click="startNewTransaction">Start new transaction</button>
</div>
<div style="padding: 5px 0;">
<div style="border: 2px solid #3bee81; padding: 5px 65px 5px 10px; max-width: 650px;" class="mb-1">
<div style="${stepThreeBlock}">
<p style="${infoHeader}">You send</p>
<p style="${infoContent} text-transform:uppercase;">{{transaction.fromAmount}} {{fromTicker}}</p>
</div>
<div style="${stepThreeBlock}">
<p style="${infoHeader}">To address</p>
<p style="${infoContent}">{{transaction.exchangeAddress}} <ButtonClipboard :value="transaction.exchangeAddress" class="text-theme-page-text-light mx-2"/></p>
</div>
<div style="${stepThreeBlock}" v-if="transaction.exchangeAddressTag">
<p style="${infoContent}"><small>Tag/memo:</small> {{transaction.exchangeAddressTag}} <ButtonClipboard :value="transaction.exchangeAddressTag" class="text-theme-page-text-light mx-2"/></p>
</div>
</div>
<div style="padding: 5px 65px 5px 10px;">
<div style="${stepThreeBlock}">
<p style="${infoHeader}">You get</p>
<p style="${infoHeader} font-size: 18px; text-transform:uppercase;"> ≈ {{transaction.rate}} {{toTicker}}</p>
</div>
<div style="${stepThreeBlock}">
<p style="${infoHeader}">To address</p>
<p style="${infoHeader} font-size: 18px; word-break: break-all;">{{transaction.toAddress}}</p>
</div>
<div v-if="transaction.toAddressTag" style="${stepThreeBlock}">
<p style="${infoHeader} font-size: 18px; word-break: break-all;"><small>Tag/memo:</small> {{transaction.toAddressTag}}</p>
</div>
</div>
<div v-if="!isExchangeFinished" style="exchangeStatuses" class="flex items-center justify-center flex-col md:flex-row">
<div style="height: 35px; border: 2px solid rgba(61,61,112,.04);" class="md:w-1/3 w-full mb-1 md:mx-1 flex items-center justify-center">
<font-awesome-icon v-if="receivedStatus" :icon="faCheckCircle" size="lg" style="color: #3bee81;"/>
<font-awesome-icon v-else :icon="spinner" size="lg" rotation="180" spin style="color: #3bee81;"/>
<span class="ml-2">{{receivedStatus ? 'Deposit received' : 'Awaiting deposit'}}</span>
</div>
<div style="height: 35px; border: 2px solid rgba(61,61,112,.04);" class="md:w-1/3 w-full mb-1 md:mx-1 flex items-center justify-center">
<font-awesome-icon v-if="confirmingStatus" :icon="faCheckCircle" size="lg" style="color: #3bee81;"/>
<font-awesome-icon v-else-if="exchangingStatus" :icon="spinner" size="lg" rotation="180" spin style="color: #3bee81;"/>
<font-awesome-icon v-else :icon="faCircleNotch" size="lg" style="color: #E9E7EF;"/>
<span class="ml-2">{{confirmingStatus ? 'Exchanged' : 'Exchanging'}}</span>
</div>
<div style="height: 35px; border: 2px solid rgba(61,61,112,.04);" class="md:w-1/3 w-full mb-1 md:mx-1 flex items-center justify-center">
<font-awesome-icon v-if="isExchangeFinishedSuccess" :icon="faCheckCircle" size="lg" style="color: #3bee81;"/>
<font-awesome-icon v-else-if="sendingStatus" :icon="spinner" size="lg" rotation="180" spin style="color: #3bee81;"/>
<font-awesome-icon v-else :icon="faCircleNotch" size="lg" style="color: #E9E7EF;"/>
<span class="ml-2">{{isExchangeFinishedSuccess ? 'Sent to your wallet' : 'Sending to your wallet'}}</span>
</div>
</div>
<div v-if="transaction.status === statuses.failed" class="px-4 py-3 rounded my-1" style="background-color: #fff5f5; ">
<span class="block sm:inline" style="color: #e53e3e;">Error during exchange. Please contact support.</span>
</div>
<div v-if="transaction.status === statuses.expired" class="px-4 py-3 rounded my-1" style="background-color: #fff5f5; ">
<span class="block sm:inline" style="color: #e53e3e;">This exchange has expired. Please start a new exchange.</span>
</div>
<div class="px-2 py-1 rounded my-1" style="background-color: rgba(61,61,112,.04);">
<p class="mb-1" style="color: #333;">If you have any questions about your exchange, please contact our support team via email.</p>
<a style="color: #3bee81;" href="mailto: help@switchain.com">help@switchain.com</a>
</div>
</div>
</div>
<div v-if="currentStep === 4 && transaction" class="lg:w-11/12">
<div class="relative px-2 py-2 rounded my-1 flex flex-col justify-center items-center" style="background-color: rgba(61,61,112,.04);">
<div style="${transactionSuccessIcon}">
<img src="https://changenow.io/images/exchange/check.svg"/>
</div>
<p style="font-size: 26px; font-weight: 700; margin-botom: 10px; color:white">Exchange is completed!</p>
<button class="absolute" style="margin-left: auto; color: #3bee81; right: 10px; top: 10px;" @click="startNewTransaction">Start new exchange</button>
</div>
<div style="${smallStep}">
<div style="${smallStepHeader}">
<div style="${smallStepNumber}">1</div>
<p style="${smallStepName}">Your {{transaction.from}} Wallet</p>
<span style="${stepHeaderText}">{{ parseDate(transaction.createdAt) }}</span>
</div>
<div class="flex">
<div style="${smallStepInfoIcon}">
<img style="width: 52px" src="https://changenow.io/images/exchange/wallet-icon.svg"/>
</div>
<div style="padding-left: 33px;">
<div style="${smallStepInfoItem}">
<p style="${stepInfoHead} width: 240px;">Input Transaction Hash</p>
<p style="font-size: 15px; letter-spacing: .3px; word-break: break-all;">
<a style="color: #3bee81; word-break: break-all; user-select: all;" :href="payinHashLink" target="_blank">
{{transaction.depositTxId}}
</a>
<ButtonClipboard :value="transaction.depositTxId" class="text-theme-page-text-light mx-2"/>
</p>
</div>
<div style="${smallStepInfoItem}">
<p style="${stepInfoHead} width: 240px;">Switchain Address</p>
<p style="font-size: 15px; letter-spacing: .3px; word-break: break-all;">
<a style="color: #3bee81; word-break: break-all; user-select: all;" :href="payinAddressLink" target="_blank">
{{transaction.exchangeAddress}}
</a>
<ButtonClipboard :value="transaction.exchangeAddress" class="text-theme-page-text-light mx-2"/>
</p>
</div>
<div style="${smallStepInfoItem}">
<p style="${stepInfoHead} font-weight: 700; width: 240px;">Amount Sent</p>
<p style="font-size: 15px; letter-spacing: .3px; word-break: break-all; font-weight: 700; word-break: break-all;">
{{transaction.fromAmount}} {{transaction.from}}
</p>
</div>
</div>
</div>
</div>
<div style="${smallStep}">
<div style="${smallStepHeader}">
<div style="${smallStepNumber}">2</div>
<p style="${smallStepName}">Your {{toTicker}} Wallet</p>
<span style="${stepHeaderText}">{{ parseDate(transaction.updatedAt) }}</span>
</div>
<div class="flex">
<div style="${smallStepInfoIcon}">
<img style="width: 52px" src="https://changenow.io/images/exchange/exchange-icon.svg"/>
</div>
<div style="padding-left: 33px;">
<div style="${smallStepInfoItem}">
<p style="${stepInfoHead} width: 240px;">Output Transaction Hash</p>
<p style="font-size: 15px; letter-spacing: .3px; word-break: break-all;">
<a style="color: #3bee81; word-break: break-all; user-select: all;" :href="payoutHashLink" target="_blank">
{{transaction.toTx}}
</a>
<ButtonClipboard :value="transaction.toTx" class="text-theme-page-text-light mx-2"/>
</p>
</div>
<div style="${smallStepInfoItem}">
<p style="${stepInfoHead} width: 240px;">Your {{toTicker}} Address</p>
<p style="font-size: 15px; letter-spacing: .3px; word-break: break-all;">
<a style="color: #3bee81; word-break: break-all; user-select: all;" target="_blank"
:href="payoutAddressLink">
{{transaction.toAddress}}
</a>
<ButtonClipboard :value="transaction.toAddress" class="text-theme-page-text-light mx-2"/>
</p>
</div>
<div style="${smallStepInfoItem}">
<p style="${stepInfoHead} font-weight: 700; width: 240px;">Amount Received</p>
<p style="font-size: 15px; letter-spacing: .3px; word-break: break-all; font-weight: 700; word-break: break-all;">
{{transaction.rate}} {{toTicker}}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`,
components: {},
data() {
return {
faCheck: faCheck,
spinner: faSpinner,
arrow: faArrowsAltV,
upArrow: faLongArrowAltUp,
downArrow: faLongArrowAltDown,
faSearch,
faArrowRight,
faCheckCircle,
faTimesCircle,
faCircleNotch,
faCircle,
faExternalLinkAlt,
amount: 0.1,
amountTo: 0,
currentStep: 1,
currencies: [],
from: null,
to: null,
fullTo: null,
fullFrom: null,
api: {},
isCounting: false,
recountTimer: null,
counter: 1,
recountTimeout: null,
arkWallets: [],
recipientWallet: "",
recipientWalletTag: "",
refundWallet: "",
refundWalletTag: "",
fromHasExternalId: false,
toHasExternalId: false,
fromExternalId: "",
toExternalId: "",
externalId: "",
initializing: true,
confirm: false,
fromFilter: "",
toFilter: "",
recipientFocus: false,
refundFocus: false,
externalIdFocus: false,
isListFromOpen: false,
isListToOpen: false,
fromAddressTag: "",
toAddressTag: "",
hasError: false,
amountError: false,
amountMinError: false,
amountMaxError: false,
minAmount: 0,
transactionTime: "15",
longName: {},
isEnabled: false,
selectValue: "",
selectRefundValue: "",
// Step 3
transaction: null,
creating: false,
statusTimer: null,
finishedStatuses,
statuses,
counter: 0,
sequence: ""
};
},
computed: {
isValidRecipient() {
return this.to ? valiateAddress(this.to, this.recipientWallet) : false;
},
isValidRefund() {
return this.from ? valiateAddress(this.from, this.refundWallet) : false;
},
isValidFromExternalId() {
return this.from
? valiateExternalId(this.from, this.fromExternalId)
: false;
},
isValidToExternalId() {
return this.to ? valiateExternalId(this.to, this.toExternalId) : false;
},
renderFromLabel() {
const hasError = this.amountMaxError || this.amountMinError;
const error = this.amountMaxError
? `Maximum amount ${this.maxAmount} ${this.from}`
: `Minimum amount ${this.minAmount} ${this.from}`;
return this.from && hasError ? error : "";
},
externalIdValidError() {
return `This is not a valid memo, tag...`;
},
recipientPlace() {
return this.to ? `Enter the recipient's ${this.to} address` : "";
},
externalIdName() {
return this.addressTag || "Extra Id";
},
refundPlace() {
return this.from
? `Enter ${this.from} refund address (${
this.from ? "required" : "optional"
})`
: "";
},
fromTicker() {
return this.from || defaultFrom;
},
toTicker() {
return this.to || defaultTo;
},
filtredFrom() {
const filter = this.currencies.filter(currency => currency !== this.from);
return filter;
},
filtredTo() {
const filter = this.currencies.filter(currency => currency !== this.to);
return filter;
},
validParams() {
if (this.from && this.to && this.amount) {
const isValidRecipient =
this.recipientWallet && valiateAddress(this.to, this.recipientWallet);
const isValidRefund =
this.from || this.refundWallet
? valiateAddress(this.from, this.refundWallet)
: true;
const isValidExternalId =
(this.to && !this.toHasExternalId) || this.toExternalId
? valiateExternalId(this.to, this.toExternalId)
: true;
return Boolean(
isValidRecipient &&
isValidRefund &&
isValidExternalId &&
!this.hasError &&
!this.amountError
);
}
return false;
},
receivedStatus() {
if (this.transaction) {
const { status } = this.transaction;
return status !== statuses.waiting;
}
return false;
},
exchangingStatus() {
if (this.transaction) {
const { status } = this.transaction;
return exchangingStatuses.includes(status);
}
return false;
},
confirmingStatus() {
if (this.transaction) {
const { status } = this.transaction;
return status === statuses.confirming || status === statuses.confirmed;
}
return false;
},
sendingStatus() {
if (this.transaction) {
const { status } = this.transaction;
return status === statuses.confirming;
}
return false;
},
isExchangeFinished() {
if (this.transaction) {
const { status } = this.transaction;
return this.finishedStatuses.includes(status);
}
return false;
},
isExchangeFinishedSuccess() {
if (this.transaction) {
const { status } = this.transaction;
return status === statuses.confirmed;
}
return false;
},
payinHashLink() {
if (this.transaction && this.transaction.status === statuses.confirmed) {
const { depositTxId, pair } = this.transaction;
const { from } = pairToObject({ pair });
const link = getLink({ asset: from, path: "tx", hash: depositTxId });
return link;
}
return "";
},
payinAddressLink() {
if (this.transaction && this.transaction.status === statuses.confirmed) {
const { exchangeAddress, pair } = this.transaction;
const { from } = pairToObject({ pair });
const link = getLink({
asset: from,
path: "address",
hash: exchangeAddress
});
return link;
}
return "";
},
payoutAddressLink() {
if (this.transaction && this.transaction.status === statuses.confirmed) {
const { toAddress, pair } = this.transaction;
const { to } = pairToObject({ pair });
const link = getLink({
asset: to,
path: "address",
hash: toAddress
});
return link;
}
return "";
},
payoutHashLink() {
if (this.transaction && this.transaction.status === statuses.confirmed) {
const { toTx, pair } = this.transaction;
const { to } = pairToObject({ pair });
const link = getLink({
asset: to,
path: "tx",
hash: toTx
});
return link;
}
return "";
},
exchangeRate() {
if (this.transaction && this.transaction.status === statuses.confirmed) {
const { rate, fromAmount, pair } = this.transaction;
const { from, to } = pairToObject({ pair });
const quote = Number(rate) / Number(fromAmount);
return `1 ${from} ≈ ${quote.toFixed(7)} ${to}`;
}
return "";
}
},
methods: {
parseDate(date) {
const time = new Date(date);
return time.toLocaleString();
},
outSideClick(event) {
const domElements = event.path;
const cfl = this.refs.currencySelectFrom;
const ctl = this.refs.currencySelectTo;
if (!cfl || !ctl) {
return;
}
if (
!domElements.includes(cfl) &&
!domElements.includes(this.refs.fromSearchBtn)
) {
cfl.style.display = "none";
this.fromFilter = "";
this.isListFromOpen = false;
}
if (
!event.path.includes(ctl) &&
!domElements.includes(this.refs.toSearchBtn)
) {
ctl.style.display = "none";
this.toFilter = "";
this.isListToOpen = false;
}
},
countSequence() {
const price =
this.amountTo && this.amountTo !== "-" && this.amount
? Number(this.amountTo / this.amount).toFixed(7)
: 0;
return `1 ${this.from ? this.from : defaultFrom} ≈ ${price || ""} ${
this.to ? this.to : "ETH"
}`;
},
toggleRefund() {
this.needRefund = !this.needRefund;
this.refundWallet = "";
},
async getAllCurrencies() {
try {
const marketInfo = await this.api.getAllCurrencies();
walletApi.storage.set("marketInfo", marketInfo);
const currencies = new Set();
marketInfo.forEach(({ pair }) => {
const { from, to } = pairToObject({ pair });
currencies.add(from);
currencies.add(to);
});
this.currencies = [...currencies];
return [...currencies];
} catch (error) {
walletApi.alert.error(error);
}
},
async recountTo() {
const marketInfo = walletApi.storage.get("marketInfo");
const { from, to } = this;
if (marketInfo.length && from && to) {
this.isCounting = true;
const currentPair = `${from}-${to}`;
const amount = this.amount;
if (this.arkWallets.length && this.to === "ARK") {
this.setArkAddress();
}
if (this.arkWallets.length && this.from === "ARK") {
this.setRefundArkAddress();
}
try {
const { quote, minerFee, maxLimit, minLimit } = marketInfo.find(
({ pair }) => pair === currentPair
);
this.minAmount = minLimit;
this.maxAmount = maxLimit;
if (minLimit > amount) {
this.amountMinError = true;
this.amountError = true;
return;
}
if (maxLimit < amount) {
this.amountError = true;
this.amountMaxError = true;
return;
}
this.amountMinError = false;
this.amountMaxError = false;
this.amountError = false;
const amountTo = amount * quote - minerFee;
this.amountTo = amountTo.toFixed(8);
this.hasError = false;
} catch (error) {
this.amountTo = 0;
this.hasError = true;
if (error.body) {
const errorData = JSON.parse(error.body);
if (errorData.error === errorType.SMALL_DEPOSIT) {
this.amountMinError = true;
this.amountError = true;
return;
}
if (errorData.error === errorType.BIG_DEPOSIT) {
this.amountMaxError = true;
this.amountError = true;
return;
}
if (errorData.error === errorType.INACTIVE) {
const errorMessage = `The ${this.from}/${this.to}
pair is temporarily unavailable for exchanges.`;
walletApi.alert.error(errorMessage);
return;
}
}
if (error.message) {
walletApi.alert.error(
`Fail to fetch available currencies. Reason: ${error.message}.`
);
return;
}
walletApi.alert.error("Unknown error.");
} finally {
this.sequence = this.countSequence();
this.isCounting = false;
}
}
},
startRecount() {
if (this.recountTimeout) {
walletApi.timers.clearTimeout(this.recountTimeout);
}
this.recountTimeout = walletApi.timers.setTimeout(() => {
this.recountTo();
}, 500);
},
toggleCurrancies() {
const prevFrom = this.from;
const prevFromEx = this.fromHasExternalId;
const prevRecipient = this.recipientWallet;
this.from = this.to;
this.to = prevFrom;
this.fromHasExternalId = this.toHasExternalId;
this.toHasExternalId = prevFromEx;
this.recipientWallet = this.refundWallet;
this.refundWallet = prevRecipient;
this.fromExternalId = "";
this.toExternalId = "";
this.recountTo();
},
openSelectFrom() {
if (this.currencies.length) {
this.refs.currencySelectFrom.style.display = "block";
this.refs.searchFrom.focus();
this.isListFromOpen = true;
}
},
openSelectTo() {
if (this.currencies.length) {
this.refs.currencySelectTo.style.display = "block";
this.refs.searchTo.focus();
this.isListToOpen = true;
}
},
selectCoinFrom(asset) {
this.from = asset;
walletApi.storage.set("fromCurrency", asset);
this.recountTo();
this.refs.currencySelectFrom.style.display = "none";
this.isListFromOpen = false;
this.fromFilter = "";
this.fromHasExternalId = symbolsWithTag.includes(asset);
},
selectCoinTo(asset) {
this.to = asset;
walletApi.storage.set("toCurrency", asset);
this.recountTo();
this.refs.currencySelectTo.style.display = "none";
this.isListToOpen = false;
this.toFilter = "";
this.toHasExternalId = symbolsWithTag.includes(asset);
},
isNumber(event) {
const value = event.target.value.trim();
const amount = Number(event.target.value);
if (Number.isNaN(amount)) {
event.preventDefault();
event.target.value = value.slice(0, -1);
this.amount = value.slice(0, -1);
return false;
} else {
return true;
}
},
async createExchange() {
if (this.validParams) {
const { from, to } = this;
const pair = `${from}-${to}`;
const params = {
pair,
toAddress: this.recipientWallet,
refundAddress: this.refundWallet,
fromAmount: String(this.amount)
};
if (this.toExternalId) {
params.toAddressTag = this.toExternalId;
}
if (this.fromExternalId) {
params.refundAddressTag = this.fromExternalId;
}
this.creating = true;
try {
this.statusTimer = walletApi.timers.setInterval(() => {
this.checkTransactionStatus();
}, exchangeInterval);
const transaction = await this.api.createOrder(params);
walletApi.storage.set("transactionId", transaction.orderId);
this.transaction = transaction;
await this.checkTransactionStatus();
this.currentStep = 3;
} catch (error) {
walletApi.alert.error(`Fail to create transaction.`);
} finally {
this.creating = false;
}
}
},
async checkTransactionStatus() {
if (!this.transaction) {
return;
}
const { orderId, pair } = this.transaction;
try {
const transactionData = await this.api.getTransactionStatus(orderId);
this.transaction = transactionData;
if (pair) {
const { from, to } = pairToObject({ pair });
this.transaction.from = from;
this.transaction.to = to;
}
this.transaction.updatedAt = new Date();
if (finishedStatuses.includes(transactionData.status)) {
if (transactionData.status === statuses.confirmed) {
this.currentStep = 4;
}
walletApi.storage.set("transactionId", null);
walletApi.timers.clearInterval(this.statusTimer);
}
} catch (error) {
walletApi.alert.error(`Fail to fetch transaction data.`);
}
},
async initialize() {
this.initializing = true;
const storageFrom = walletApi.storage.get("fromCurrency");
const storageAmount = walletApi.storage.get("amount");
const storageTo = walletApi.storage.get("toCurrency");
const lastId = walletApi.storage.get("transactionId");
const profile = walletApi.profiles.getCurrent();
this.arkWallets = profile.wallets.map(wallet => {
return wallet.name ? wallet.name : wallet.address;
});
if (storageFrom) {
this.from = storageFrom;
this.fromHasExternalId = symbolsWithTag.includes(storageFrom);
}
if (storageTo) {
this.to = storageTo;
this.toHasExternalId = symbolsWithTag.includes(storageTo);
}
if (storageAmount) {
this.amount = storageAmount;
}
if (this.arkWallets.length && this.to && this.to === defaultTo) {
this.setArkAddress();
}
if (this.arkWallets.length && this.from && this.from === "ARK") {
this.setRefundArkAddress();
}
try {
if (lastId) {
this.statusTimer = walletApi.timers.setInterval(() => {
this.checkTransactionStatus();
}, exchangeInterval);
this.transaction = {
orderId: lastId
};
await this.checkTransactionStatus();
this.currentStep = 3;
this.initializing = false;
return;
}
await this.getAllCurrencies();
await this.recountTo();
this.initializing = false;
} catch (error) {
if (error.message) {
walletApi.alert.error(
`Fail to fetch available currencies. Reason: ${error.message}.`
);
return;
}
if (error.body) {
const { message } = JSON.parse(error.body);
walletApi.alert.error(message);
return;
}
walletApi.alert.error("Unknown error.");
} finally {
this.initializing = false;
}
},
switchToTwoStep() {
this.currentStep = 2;
},
switchToOneStep() {
this.currentStep = 1;
},
async startNewTransaction() {
walletApi.storage.set("transactionId", null);
walletApi.route.goTo("switchain");
},
setArkAddress(value) {
if (!value) {
value = this.arkWallets[0];
}
const profile = walletApi.profiles.getCurrent();
const selectedWallet = profile.wallets.find(wallet => {
return wallet.name === value || wallet.address === value;
});
this.selectValue = value;
if (!selectedWallet) {
this.recipientWallet = profile.wallets[0].address;
return;
}
this.recipientWallet = selectedWallet.address;
},
setRefundArkAddress(value) {
if (!value) {
value = this.arkWallets[0];
}
const profile = walletApi.profiles.getCurrent();
const selectedWallet = profile.wallets.find(wallet => {
return wallet.name === value || wallet.address === value;
});
this.selectRefundValue = value;
if (!selectedWallet) {
this.refundWallet = profile.wallets[0].address;
return;
}
this.refundWallet = selectedWallet.address;
}
},
created() {
this.api = new ApiWorker(walletApi.http);
},
async mounted() {
this.longName = longName;
await this.initialize();
}
};