ton-evm-bridge 0.0.1-security → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ton-evm-bridge might be problematic. Click here for more details.
- package/.editorconfig +13 -0
- package/.eslintrc.js +16 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yaml +108 -0
- package/.nvmrc +1 -0
- package/.prettierrc +4 -0
- package/@types/global.d.ts +1 -0
- package/@types/params.d.ts +24 -0
- package/@types/tonweb.d.ts +44 -0
- package/LICENSE +674 -0
- package/README.md +71 -5
- package/assets/WTON.json +1 -0
- package/assets/pics/arrow.svg +44 -0
- package/assets/pics/done.svg +38 -0
- package/assets/pics/link.svg +15 -0
- package/assets/pics/swap.svg +331 -0
- package/assets/styles/reboot.css +163 -0
- package/components/BridgeProcessor.vue +977 -0
- package/docs/.nojekyll +0 -0
- package/docs/200.html +9 -0
- package/docs/_nuxt/1f2ac62.js +1 -0
- package/docs/_nuxt/1f2ac62.js.br +0 -0
- package/docs/_nuxt/1f2ac62.js.gz +0 -0
- package/docs/_nuxt/5cc51a5.js +1 -0
- package/docs/_nuxt/5cc51a5.js.br +0 -0
- package/docs/_nuxt/5cc51a5.js.gz +0 -0
- package/docs/_nuxt/7767db0.js +2 -0
- package/docs/_nuxt/7767db0.js.br +0 -0
- package/docs/_nuxt/7767db0.js.gz +0 -0
- package/docs/_nuxt/9ead974.js +2 -0
- package/docs/_nuxt/9ead974.js.br +0 -0
- package/docs/_nuxt/9ead974.js.gz +0 -0
- package/docs/_nuxt/LICENSES +98 -0
- package/docs/_nuxt/a46cdc5.js +1 -0
- package/docs/_nuxt/a46cdc5.js.br +0 -0
- package/docs/_nuxt/a46cdc5.js.gz +0 -0
- package/docs/_nuxt/b3f7827.js +1 -0
- package/docs/_nuxt/b3f7827.js.br +0 -0
- package/docs/_nuxt/b3f7827.js.gz +0 -0
- package/docs/_nuxt/b5d388d.js +1 -0
- package/docs/_nuxt/b5d388d.js.br +0 -0
- package/docs/_nuxt/b5d388d.js.gz +0 -0
- package/docs/_nuxt/fe8ca79.js +2 -0
- package/docs/_nuxt/fe8ca79.js.br +0 -0
- package/docs/_nuxt/fe8ca79.js.gz +0 -0
- package/docs/_nuxt/img/arrow.69e1e70.svg +44 -0
- package/docs/_nuxt/img/arrow.69e1e70.svg.br +0 -0
- package/docs/_nuxt/img/arrow.69e1e70.svg.gz +0 -0
- package/docs/_nuxt/img/swap.b8b4b2f.svg +331 -0
- package/docs/_nuxt/img/swap.b8b4b2f.svg.br +0 -0
- package/docs/_nuxt/img/swap.b8b4b2f.svg.gz +0 -0
- package/docs/favicon.ico +0 -0
- package/docs/index.html +9 -0
- package/index.js +66 -0
- package/lang/en/bridge.json +105 -0
- package/lang/en-US.js +7 -0
- package/layouts/default.vue +53 -0
- package/modules/i18n.js +44 -0
- package/nuxt.config.js +73 -0
- package/package.json +61 -6
- package/pages/index.vue +635 -0
- package/static/favicon.ico +0 -0
- package/tsconfig.json +39 -0
- package/utils/constants.ts +65 -0
- package/utils/helpers.ts +37 -0
- package/vue-shim.d.ts +4 -0
@@ -0,0 +1,977 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="BridgeProcessor">
|
3
|
+
<button
|
4
|
+
class="BridgeProcessor-transfer"
|
5
|
+
v-if="state.step === 0"
|
6
|
+
@click="onTransferClick">{{$t('Bridge.transfer')}}</button>
|
7
|
+
|
8
|
+
<div class="BridgeProcessor-infoWrapper" v-else>
|
9
|
+
<div class="BridgeProcessor-infoLine">
|
10
|
+
<div
|
11
|
+
class="BridgeProcessor-info-icon"
|
12
|
+
:class="{'none': state.step < 1, 'pending': state.step === 1, 'done': state.step > 1}"></div>
|
13
|
+
<div class="BridgeProcessor-info-text" v-if="!getStepInfoText1.isOnlyText">
|
14
|
+
{{ getStepInfoText1.sendAmount }}<br/>
|
15
|
+
<a :href="getStepInfoText1.url" target="_blank">{{ getStepInfoText1.url }}</a>
|
16
|
+
<div class="note">{{ getStepInfoText1.sendFromPersonal }} <b>{{ getStepInfoText1.sendNotFromExchanges }}</b></div>
|
17
|
+
</div>
|
18
|
+
<div class="BridgeProcessor-info-text" v-else>{{ getStepInfoText1.text }}</div>
|
19
|
+
</div>
|
20
|
+
<div class="BridgeProcessor-infoLine" v-if="!isFromTon">
|
21
|
+
<div
|
22
|
+
class="BridgeProcessor-info-icon"
|
23
|
+
:class="{'none': state.step < 2, 'pending': state.step === 2, 'done': state.step > 2}"></div>
|
24
|
+
<div class="BridgeProcessor-info-text">{{ getStepInfoText2 }}</div>
|
25
|
+
</div>
|
26
|
+
<div class="BridgeProcessor-infoLine">
|
27
|
+
<div
|
28
|
+
class="BridgeProcessor-info-icon"
|
29
|
+
:class="{'none': state.step < 3, 'pending': state.step === 3, 'done': state.step > 3}"></div>
|
30
|
+
<div class="BridgeProcessor-info-text">{{ getStepInfoText3 }}</div>
|
31
|
+
</div>
|
32
|
+
<div class="BridgeProcessor-infoLine">
|
33
|
+
<div
|
34
|
+
class="BridgeProcessor-info-icon"
|
35
|
+
:class="{'none': state.step < 4, 'pending': state.step === 4, 'done': state.step > 4}"></div>
|
36
|
+
<div class="BridgeProcessor-info-text">{{ getStepInfoText4 }}</div>
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<button
|
41
|
+
v-if="isGetTonCoinVisible"
|
42
|
+
class="BridgeProcessor-getTonCoin"
|
43
|
+
@click="mint">{{$t('Bridge.getToncoin')}}</button>
|
44
|
+
|
45
|
+
<button
|
46
|
+
v-if="isDoneVisible"
|
47
|
+
class="BridgeProcessor-done"
|
48
|
+
@click="onDoneClick">{{$t('Bridge.done')}}</button>
|
49
|
+
|
50
|
+
<button
|
51
|
+
v-if="isCancelVisible"
|
52
|
+
class="BridgeProcessor-cancel"
|
53
|
+
@click="onCancelClick">{{$t('Bridge.cancel')}}</button>
|
54
|
+
</div>
|
55
|
+
</template>
|
56
|
+
|
57
|
+
<script lang="ts">
|
58
|
+
import Vue from 'vue'
|
59
|
+
import Web3 from 'web3';
|
60
|
+
import TonWeb from 'tonweb';
|
61
|
+
import WTON from '~/assets/WTON.json';
|
62
|
+
import {ethers} from "ethers";
|
63
|
+
import {Contract} from 'web3-eth-contract';
|
64
|
+
import {AbiItem} from 'web3-utils';
|
65
|
+
import { toUnit, fromUnit, getNumber, getBool, decToHex, parseAddressFromDec } from '~/utils/helpers';
|
66
|
+
import {PARAMS} from '~/utils/constants';
|
67
|
+
|
68
|
+
const BN = TonWeb.utils.BN;
|
69
|
+
|
70
|
+
declare interface IEthToTon {
|
71
|
+
transactionHash: string,
|
72
|
+
logIndex: number,
|
73
|
+
to: {
|
74
|
+
workchain: number,
|
75
|
+
address_hash: string
|
76
|
+
}
|
77
|
+
value: number,
|
78
|
+
blockTime: number,
|
79
|
+
blockHash: string,
|
80
|
+
blockNumber: number,
|
81
|
+
from: string
|
82
|
+
}
|
83
|
+
|
84
|
+
declare interface ISwapData {
|
85
|
+
type: string,
|
86
|
+
receiver: string,
|
87
|
+
amount: string,
|
88
|
+
tx: {
|
89
|
+
address_: {
|
90
|
+
workchain: number,
|
91
|
+
address_hash: string
|
92
|
+
},
|
93
|
+
tx_hash: string,
|
94
|
+
lt: number
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
declare interface IVoteEth {
|
99
|
+
publicKey: string,
|
100
|
+
r: string,
|
101
|
+
s: string,
|
102
|
+
v: number | undefined
|
103
|
+
}
|
104
|
+
|
105
|
+
declare interface IProvider {
|
106
|
+
oraclesTotal: number,
|
107
|
+
blockNumber: number,
|
108
|
+
myEthAddress: string,
|
109
|
+
wtonContract: Contract,
|
110
|
+
web3: Web3,
|
111
|
+
tonweb: TonWeb,
|
112
|
+
feeFlat: typeof BN,
|
113
|
+
feeFactor: typeof BN,
|
114
|
+
feeBase: typeof BN
|
115
|
+
}
|
116
|
+
|
117
|
+
declare interface IState {
|
118
|
+
swapId: string,
|
119
|
+
queryId: string,
|
120
|
+
fromCurrencySent: boolean,
|
121
|
+
toCurrencySent: boolean,
|
122
|
+
step: number,
|
123
|
+
votes: IVoteEth[] | number[] | null,
|
124
|
+
swapData: ISwapData | null,
|
125
|
+
createTime: number,
|
126
|
+
blockNumber: number,
|
127
|
+
}
|
128
|
+
|
129
|
+
declare interface IComponentData {
|
130
|
+
newBlockHeadersSubscription: any,
|
131
|
+
updateStateInterval: null | ReturnType<typeof setInterval>,
|
132
|
+
provider: IProvider | null,
|
133
|
+
state: IState,
|
134
|
+
ethToTon: IEthToTon | null
|
135
|
+
}
|
136
|
+
|
137
|
+
export default Vue.extend({
|
138
|
+
props: {
|
139
|
+
isTestnet: {
|
140
|
+
type: Boolean,
|
141
|
+
required: true
|
142
|
+
},
|
143
|
+
isRecover: {
|
144
|
+
type: Boolean,
|
145
|
+
required: true
|
146
|
+
},
|
147
|
+
lt: {
|
148
|
+
type: Number,
|
149
|
+
required: true
|
150
|
+
},
|
151
|
+
hash: {
|
152
|
+
type: String,
|
153
|
+
required: true
|
154
|
+
},
|
155
|
+
isFromTon: {
|
156
|
+
type: Boolean,
|
157
|
+
required: true
|
158
|
+
},
|
159
|
+
pair: {
|
160
|
+
type: String,
|
161
|
+
required: true
|
162
|
+
},
|
163
|
+
amount: {
|
164
|
+
type: Number
|
165
|
+
},
|
166
|
+
toAddress: {
|
167
|
+
type: String,
|
168
|
+
required: true
|
169
|
+
},
|
170
|
+
},
|
171
|
+
|
172
|
+
data(): IComponentData {
|
173
|
+
return {
|
174
|
+
newBlockHeadersSubscription: null,
|
175
|
+
updateStateInterval: null,
|
176
|
+
provider: null,
|
177
|
+
ethToTon: null,
|
178
|
+
|
179
|
+
state: {
|
180
|
+
swapId: '',
|
181
|
+
queryId: '0',
|
182
|
+
fromCurrencySent: false,
|
183
|
+
toCurrencySent: false,
|
184
|
+
step: 0,
|
185
|
+
votes: null,
|
186
|
+
swapData: null,
|
187
|
+
createTime: 0,
|
188
|
+
blockNumber: 0
|
189
|
+
}
|
190
|
+
}
|
191
|
+
},
|
192
|
+
|
193
|
+
computed: {
|
194
|
+
netTypeName(): string {
|
195
|
+
return this.isTestnet ? 'test' : 'main';
|
196
|
+
},
|
197
|
+
params(): IParamsNetwork {
|
198
|
+
const pairParams = PARAMS.networks[this.pair];
|
199
|
+
return pairParams[this.netTypeName as keyof typeof pairParams];
|
200
|
+
},
|
201
|
+
isGetTonCoinVisible(): boolean {
|
202
|
+
return this.isFromTon && !this.state.toCurrencySent && this.state.step === 4;
|
203
|
+
},
|
204
|
+
isDoneVisible(): boolean {
|
205
|
+
return this.state.step > 4;
|
206
|
+
},
|
207
|
+
isCancelVisible(): boolean {
|
208
|
+
return this.isFromTon && this.state.step === 1;
|
209
|
+
},
|
210
|
+
fromCoin(): string {
|
211
|
+
return this.isFromTon ?
|
212
|
+
this.$t(`Bridge.networks.ton.${this.netTypeName}.coinShort`) as string :
|
213
|
+
this.$t(`Bridge.networks.${this.pair}.${this.netTypeName}.coinShort`) as string;
|
214
|
+
},
|
215
|
+
toCoin(): string {
|
216
|
+
return !this.isFromTon ?
|
217
|
+
this.$t(`Bridge.networks.ton.${this.netTypeName}.coinShort`) as string :
|
218
|
+
this.$t(`Bridge.networks.${this.pair}.${this.netTypeName}.coinShort`) as string;
|
219
|
+
},
|
220
|
+
toNetwork(): string {
|
221
|
+
const pair = this.isFromTon ? this.pair : 'ton';
|
222
|
+
return this.$t(`Bridge.networks.${pair}.${this.netTypeName}.name`) as string;
|
223
|
+
},
|
224
|
+
getStepInfoText1(): object {
|
225
|
+
if (this.state.step === 1) {
|
226
|
+
if (this.isFromTon) {
|
227
|
+
const url = PARAMS.tonTransferUrl
|
228
|
+
.replace('<BRIDGE_ADDRESS>', this.params.tonBridgeAddress)
|
229
|
+
.replace('<AMOUNT>', String(toUnit(this.amount)))
|
230
|
+
.replace('<TO_ADDRESS>', this.toAddress);
|
231
|
+
|
232
|
+
const sendAmount = (this.$t(`Bridge.networks.ton.transactionSendAmount`) as string)
|
233
|
+
.replace('<AMOUNT>', String(this.amount))
|
234
|
+
.replace('<FROM_COIN>', this.fromCoin);
|
235
|
+
|
236
|
+
return {
|
237
|
+
isOnlyText: false,
|
238
|
+
sendAmount,
|
239
|
+
url,
|
240
|
+
sendFromPersonal: this.$t(`Bridge.networks.ton.transactionSendFromPersonal`) as string,
|
241
|
+
sendNotFromExchanges: this.$t(`Bridge.networks.ton.transactionSendNotFromExchanges`) as string
|
242
|
+
}
|
243
|
+
} else {
|
244
|
+
return {
|
245
|
+
isOnlyText: true,
|
246
|
+
text: this.state.fromCurrencySent ?
|
247
|
+
this.$t(`Bridge.networks.${this.pair}.transactionWait`) as string :
|
248
|
+
this.$t(`Bridge.networks.${this.pair}.transactionSend`) as string
|
249
|
+
}
|
250
|
+
}
|
251
|
+
} else {
|
252
|
+
const pair = this.isFromTon ? 'ton' : this.pair;
|
253
|
+
return {
|
254
|
+
isOnlyText: true,
|
255
|
+
text: this.$t(`Bridge.networks.${pair}.transactionCompleted`) as string
|
256
|
+
}
|
257
|
+
}
|
258
|
+
},
|
259
|
+
getStepInfoText2(): string {
|
260
|
+
if (this.isFromTon) {
|
261
|
+
return '';
|
262
|
+
}
|
263
|
+
|
264
|
+
if (this.state.step === 2) {
|
265
|
+
let blocksConfirmations = (this.provider?.blockNumber || this.state.blockNumber) - this.state.blockNumber;
|
266
|
+
blocksConfirmations = Math.min(blocksConfirmations, this.params.blocksConfirmations);
|
267
|
+
|
268
|
+
return (this.$t(`Bridge.networks.${this.pair}.blocksConfirmations`) as string)
|
269
|
+
.replace('<COUNT>', String(blocksConfirmations) + '/' + String(this.params.blocksConfirmations));
|
270
|
+
} else if (this.state.step > 2) {
|
271
|
+
return this.$t('Bridge.blocksConfirmationsCollected') as string;
|
272
|
+
} else {
|
273
|
+
return this.$t('Bridge.blocksConfirmationsWaiting') as string;
|
274
|
+
}
|
275
|
+
},
|
276
|
+
getStepInfoText3(): string {
|
277
|
+
if (this.state.step === 3) {
|
278
|
+
const votesConfirmations = (this.state.votes?.length || 0) + '/' + (this.provider?.oraclesTotal || 0);
|
279
|
+
|
280
|
+
return (this.$t(`Bridge.oraclesConfirmations`) as string)
|
281
|
+
.replace('<COUNT>', String(votesConfirmations));
|
282
|
+
} else if (this.state.step > 3) {
|
283
|
+
return this.$t('Bridge.oraclesConfirmationsCollected') as string;
|
284
|
+
} else {
|
285
|
+
return this.$t('Bridge.oraclesConfirmationsWaiting') as string;
|
286
|
+
}
|
287
|
+
},
|
288
|
+
getStepInfoText4(): string {
|
289
|
+
if (this.state.step === 4) {
|
290
|
+
if (this.isFromTon) {
|
291
|
+
return this.state.toCurrencySent ?
|
292
|
+
this.$t(`Bridge.networks.${this.pair}.transactionWait`) as string :
|
293
|
+
(this.$t(`Bridge.getCoinsByMetamask`) as string)
|
294
|
+
.replace('<TO_COIN>', this.toCoin);
|
295
|
+
|
296
|
+
} else {
|
297
|
+
return (this.$t(`Bridge.coinsSent`) as string)
|
298
|
+
.replace('<TO_COIN>', this.toCoin);
|
299
|
+
}
|
300
|
+
} else if (this.state.step > 4) {
|
301
|
+
return (this.$t(`Bridge.coinsSent`) as string)
|
302
|
+
.replace('<TO_COIN>', this.toCoin);
|
303
|
+
} else {
|
304
|
+
return 'Get ' + this.toCoin + 's in ' + this.toNetwork;
|
305
|
+
return (this.$t(`Bridge.getCoins`) as string)
|
306
|
+
.replace('<TO_COIN>', this.toCoin)
|
307
|
+
.replace('<TO_NETWORK>', this.toNetwork);
|
308
|
+
}
|
309
|
+
}
|
310
|
+
},
|
311
|
+
|
312
|
+
watch: {
|
313
|
+
'state.step': {
|
314
|
+
immediate: true,
|
315
|
+
handler(val): void {
|
316
|
+
this.$emit('state-changed');
|
317
|
+
this.$emit('interface-blocked', val > 0);
|
318
|
+
}
|
319
|
+
}
|
320
|
+
},
|
321
|
+
|
322
|
+
mounted(): void {
|
323
|
+
this.updateState();
|
324
|
+
this.updateStateInterval = setInterval(this.updateState, 5000);
|
325
|
+
},
|
326
|
+
|
327
|
+
beforeDestroy(): void {
|
328
|
+
clearInterval(this.updateStateInterval as ReturnType<typeof setInterval>);
|
329
|
+
|
330
|
+
if (this.newBlockHeadersSubscription) {
|
331
|
+
this.newBlockHeadersSubscription.unsubscribe();
|
332
|
+
}
|
333
|
+
|
334
|
+
const ethereum = window.ethereum;
|
335
|
+
|
336
|
+
if (ethereum) {
|
337
|
+
ethereum.removeListener('accountsChanged', this.onAccountChanged);
|
338
|
+
}
|
339
|
+
},
|
340
|
+
|
341
|
+
methods: {
|
342
|
+
resetState(): void {
|
343
|
+
this.state.swapId = '';
|
344
|
+
this.state.queryId = '0';
|
345
|
+
this.state.fromCurrencySent = false;
|
346
|
+
this.state.toCurrencySent = false;
|
347
|
+
this.state.step = 0;
|
348
|
+
this.state.votes = null;
|
349
|
+
this.state.swapData = null;
|
350
|
+
this.state.createTime = 0;
|
351
|
+
this.state.blockNumber = 0;
|
352
|
+
|
353
|
+
this.$emit('reset-state');
|
354
|
+
},
|
355
|
+
async loadState(processingState: IState): Promise<void> {
|
356
|
+
if (!processingState) {
|
357
|
+
return;
|
358
|
+
}
|
359
|
+
|
360
|
+
this.provider = await this.initProvider();
|
361
|
+
|
362
|
+
if (!this.provider) {
|
363
|
+
return;
|
364
|
+
}
|
365
|
+
Object.assign(this.state, processingState);
|
366
|
+
|
367
|
+
await this.updateState();
|
368
|
+
},
|
369
|
+
saveState(): void {
|
370
|
+
this.$emit('save-state', this.state);
|
371
|
+
},
|
372
|
+
deleteState(): void {
|
373
|
+
this.$emit('delete-state');
|
374
|
+
},
|
375
|
+
async updateState(): Promise<void> {
|
376
|
+
if (this.state.step === 1 && this.isFromTon) {
|
377
|
+
const swap = await this.getSwap(this.amount, this.toAddress, this.state.createTime);
|
378
|
+
if (swap) {
|
379
|
+
this.state.swapId = this.getSwapTonToEthId(this.provider!.web3, swap);
|
380
|
+
this.state.swapData = swap;
|
381
|
+
this.state.step = 3;
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
if (this.state.step === 2 && !this.isFromTon) {
|
386
|
+
const blocksConfirmations = (this.provider?.blockNumber || this.state.blockNumber) - this.state.blockNumber;
|
387
|
+
|
388
|
+
if (blocksConfirmations > this.params.blocksConfirmations) {
|
389
|
+
const block = await this.provider!.web3.eth.getBlock(this.state.blockNumber);
|
390
|
+
|
391
|
+
this.ethToTon!.blockTime = Number(block.timestamp);
|
392
|
+
this.ethToTon!.blockHash = block.hash;
|
393
|
+
|
394
|
+
this.state.queryId = this.getQueryId(this.ethToTon!).toString();
|
395
|
+
this.state.step = 3;
|
396
|
+
}
|
397
|
+
}
|
398
|
+
|
399
|
+
if (this.state.step === 3) {
|
400
|
+
this.state.votes = this.isFromTon ? await this.getEthVote(this.state.swapId) : await this.getTonVote(this.state.queryId);
|
401
|
+
if (this.state.votes && this.state.votes!.length >= this.provider!.oraclesTotal * 2 / 3) {
|
402
|
+
this.state.step = this.isFromTon ? 4 : 5;
|
403
|
+
}
|
404
|
+
}
|
405
|
+
},
|
406
|
+
getSwapTonToEthId(web3: any, d: ISwapData): string {
|
407
|
+
let encodedParams;
|
408
|
+
|
409
|
+
if (this.pair === 'eth' && !this.isTestnet) {
|
410
|
+
encodedParams = web3.eth.abi.encodeParameters(
|
411
|
+
['int', 'address', 'uint256', 'int8', 'bytes32', 'bytes32', 'uint64'],
|
412
|
+
[0xDA7A, d.receiver, d.amount, d.tx.address_.workchain, d.tx.address_.address_hash, d.tx.tx_hash, d.tx.lt]
|
413
|
+
)
|
414
|
+
}
|
415
|
+
|
416
|
+
if (this.pair === 'bsc' || this.isTestnet) {
|
417
|
+
encodedParams = web3.eth.abi.encodeParameters(
|
418
|
+
['int', 'address', 'address', 'uint256', 'int8', 'bytes32', 'bytes32', 'uint64'],
|
419
|
+
[0xDA7A, this.params.wTonAddress, d.receiver, d.amount, d.tx.address_.workchain, d.tx.address_.address_hash, d.tx.tx_hash, d.tx.lt]
|
420
|
+
)
|
421
|
+
}
|
422
|
+
|
423
|
+
return Web3.utils.sha3(encodedParams) as string;
|
424
|
+
},
|
425
|
+
serializeEthToTon(ethToTon: IEthToTon) {
|
426
|
+
const bits = new TonWeb.boc.BitString(8 + 256 + 16 + 8 + 256 + 64);
|
427
|
+
bits.writeUint(0, 8); // vote op
|
428
|
+
bits.writeUint(new BN(ethToTon.transactionHash.substr(2), 16), 256);
|
429
|
+
bits.writeInt(ethToTon.logIndex, 16);
|
430
|
+
bits.writeUint(ethToTon.to.workchain, 8);
|
431
|
+
bits.writeUint(new BN(ethToTon.to.address_hash, 16), 256);
|
432
|
+
bits.writeUint(new BN(ethToTon.value), 64);
|
433
|
+
return bits.array;
|
434
|
+
},
|
435
|
+
getQueryId(ethToTon: IEthToTon): typeof BN {
|
436
|
+
|
437
|
+
// web3@1.3.4 has an error in the algo for computing SHA
|
438
|
+
// it doesn't strictly check input string for valid HEX relying only for 0x prefix
|
439
|
+
// but the query string is formed that way: 0xBLOCKHASH + '_' + 0xTRANSACTIONHASH + '_' + LOGINDEX
|
440
|
+
// the keccak algo splits string to pairs of symbols, and treats them as hex bytes
|
441
|
+
// so _0 becames NaN, x7 becames NaN, d_ becames 13 (it only sees first d and skips invalid _)
|
442
|
+
// web3@1.6.1 has this error fixed, but for our case this means that we've got different hashes for different web3 versions
|
443
|
+
// and getLegacyQueryString code transforms query string in the way, that SHA from web3@1.6.1 can return the same exact value as web3@1.3.4
|
444
|
+
// for example:
|
445
|
+
// old one: 0xcad62a0e0090e30e0133586f86ed8b7d0d2eac5fa8ded73b8180931ff379b113_0x77e5617841b2d355fe588716b6f8f506b683e985fc98fdb819ddf566594d4cfd_64
|
446
|
+
// new one: 0xcad62a0e0090e30e0133586f86ed8b7d0d2eac5fa8ded73b8180931ff379b11300007e5617841b2d355fe588716b6f8f506b683e985fc98fdb819ddf566594d4cf0d64
|
447
|
+
// diff : ^^^^ ^^
|
448
|
+
function getLegacyQueryString(str: string): string {
|
449
|
+
const strArr = str.split('');
|
450
|
+
strArr[66] = '0';
|
451
|
+
strArr[67] = '0';
|
452
|
+
strArr[68] = '0';
|
453
|
+
strArr[69] = '0';
|
454
|
+
strArr[133] = strArr[132];
|
455
|
+
strArr[132] = '0';
|
456
|
+
return strArr.join('');
|
457
|
+
}
|
458
|
+
|
459
|
+
const MULTISIG_QUERY_TIMEOUT = 30 * 24 * 60 * 60; // 30 days
|
460
|
+
const VERSION = 2;
|
461
|
+
const timeout = ethToTon.blockTime + MULTISIG_QUERY_TIMEOUT + VERSION;
|
462
|
+
|
463
|
+
const query_id = Web3.utils.sha3(getLegacyQueryString(ethToTon.blockHash + '_' + ethToTon.transactionHash + '_' + String(ethToTon.logIndex)))!.substr(2, 8); // get first 32 bit
|
464
|
+
|
465
|
+
return new BN(timeout).mul(new BN(4294967296)).add(new BN(query_id, 16));
|
466
|
+
},
|
467
|
+
getFeeAmount(amount: typeof BN): string {
|
468
|
+
const rest = new BN(amount).sub(this.provider!.feeFlat);
|
469
|
+
const percentFee = rest.mul(this.provider!.feeFactor).div(this.provider!.feeBase);
|
470
|
+
return this.provider!.feeFlat.add(percentFee)
|
471
|
+
},
|
472
|
+
makeAddress(address: string): string {
|
473
|
+
if (!address.startsWith('0x')) throw new Error('Invalid address ' + address);
|
474
|
+
let hex = address.substr(2);
|
475
|
+
while (hex.length < 40) {
|
476
|
+
hex = '0' + hex;
|
477
|
+
}
|
478
|
+
return '0x' + hex;
|
479
|
+
},
|
480
|
+
async getSwap(myAmount: number, myToAddress: string, myCreateTime: number): Promise<null | ISwapData> {
|
481
|
+
console.log('getTransactions', this.params.tonBridgeAddress, this.lt && this.hash ? 1 : (this.isRecover ? 200 : 40), this.lt || undefined, this.hash || undefined, undefined, this.lt && this.hash ? true : undefined);
|
482
|
+
const transactions = await this.provider!.tonweb.provider.getTransactions(this.params.tonBridgeAddress, this.lt && this.hash ? 1 : (this.isRecover ? 200 : 40), this.lt || undefined, this.hash || undefined, undefined, this.lt && this.hash ? true : undefined);
|
483
|
+
console.log('ton txs', transactions.length);
|
484
|
+
|
485
|
+
const findLogOutMsg = (outMessages?: any[]): any => {
|
486
|
+
if (!outMessages) return null;
|
487
|
+
for (const outMsg of outMessages) {
|
488
|
+
if (outMsg.destination === '') return outMsg;
|
489
|
+
}
|
490
|
+
return null;
|
491
|
+
}
|
492
|
+
|
493
|
+
const getRawMessageBytes = (logMsg: any): Uint8Array | null => {
|
494
|
+
const message = logMsg.message.substr(0, logMsg.message.length - 1); // remove '\n' from end
|
495
|
+
const bytes = TonWeb.utils.base64ToBytes(message);
|
496
|
+
if (bytes.length !== 28) {
|
497
|
+
return null;
|
498
|
+
}
|
499
|
+
return bytes;
|
500
|
+
}
|
501
|
+
|
502
|
+
const getTextMessageBytes = (logMsg: any): Uint8Array | null => {
|
503
|
+
const message = logMsg.msg_data?.text;
|
504
|
+
const textBytes = TonWeb.utils.base64ToBytes(message);
|
505
|
+
const bytes = new Uint8Array(textBytes.length + 4);
|
506
|
+
bytes.set(textBytes, 4);
|
507
|
+
return bytes;
|
508
|
+
}
|
509
|
+
|
510
|
+
const getMessageBytes = (logMsg: any): Uint8Array | null => {
|
511
|
+
const msgType = logMsg.msg_data['@type'];
|
512
|
+
if (msgType === 'msg.dataText') {
|
513
|
+
return getTextMessageBytes(logMsg);
|
514
|
+
} else if (msgType === 'msg.dataRaw') {
|
515
|
+
return getRawMessageBytes(logMsg);
|
516
|
+
} else {
|
517
|
+
console.error('Unknown log msg type ' + msgType);
|
518
|
+
return null;
|
519
|
+
}
|
520
|
+
}
|
521
|
+
|
522
|
+
for (const t of transactions) {
|
523
|
+
const logMsg = findLogOutMsg(t.out_msgs);
|
524
|
+
if (logMsg) {
|
525
|
+
if (!this.isRecover && !(this.lt && this.hash)) {
|
526
|
+
if (t.utime * 1000 < myCreateTime) continue;
|
527
|
+
}
|
528
|
+
const bytes = getMessageBytes(logMsg);
|
529
|
+
if (bytes === null) {
|
530
|
+
continue;
|
531
|
+
}
|
532
|
+
|
533
|
+
const destinationAddress = this.makeAddress('0x' + TonWeb.utils.bytesToHex(bytes.slice(0, 20)));
|
534
|
+
const amountHex = TonWeb.utils.bytesToHex(bytes.slice(20, 28));
|
535
|
+
const amount = new BN(amountHex, 16);
|
536
|
+
const senderAddress = new TonWeb.utils.Address(t.in_msg.source);
|
537
|
+
|
538
|
+
const addressFromInMsg = t.in_msg.message.slice('swapTo#'.length);
|
539
|
+
if (destinationAddress.toLowerCase() !== addressFromInMsg.toLowerCase()) {
|
540
|
+
console.error('address from in_msg doesnt match ', addressFromInMsg, destinationAddress);
|
541
|
+
continue;
|
542
|
+
}
|
543
|
+
const amountFromInMsg = new BN(t.in_msg.value);
|
544
|
+
const amountFromInMsgAfterFee = amountFromInMsg.sub(this.getFeeAmount(amountFromInMsg));
|
545
|
+
if (!amount.eq(amountFromInMsgAfterFee)) {
|
546
|
+
console.error('amount from in_msg doesnt match ', amount.toString(), amountFromInMsgAfterFee.toString(), amountFromInMsg.toString());
|
547
|
+
continue;
|
548
|
+
}
|
549
|
+
|
550
|
+
const event: ISwapData = {
|
551
|
+
type: 'SwapTonToEth',
|
552
|
+
receiver: destinationAddress,
|
553
|
+
amount: amount.toString(),
|
554
|
+
tx: {
|
555
|
+
address_: { // sender address
|
556
|
+
workchain: senderAddress.wc,
|
557
|
+
address_hash: '0x' + TonWeb.utils.bytesToHex(senderAddress.hashPart),
|
558
|
+
},
|
559
|
+
tx_hash: '0x' + TonWeb.utils.bytesToHex(TonWeb.utils.base64ToBytes(t.transaction_id.hash)),
|
560
|
+
lt: t.transaction_id.lt,
|
561
|
+
}
|
562
|
+
};
|
563
|
+
console.log(JSON.stringify(event));
|
564
|
+
|
565
|
+
const myAmountNano = new BN(myAmount * 1e9);
|
566
|
+
const amountAfterFee = myAmountNano.sub(this.getFeeAmount(myAmountNano));
|
567
|
+
|
568
|
+
if (amount.eq(amountAfterFee) && event.receiver.toLowerCase() === myToAddress.toLowerCase()) {
|
569
|
+
return event;
|
570
|
+
}
|
571
|
+
}
|
572
|
+
}
|
573
|
+
return null;
|
574
|
+
},
|
575
|
+
parseEthSignature(data: any) {
|
576
|
+
const tuple = data.tuple.elements;
|
577
|
+
const publicKey = this.makeAddress(decToHex(tuple[0].number.number));
|
578
|
+
|
579
|
+
const rsv = tuple[1].tuple.elements;
|
580
|
+
const r = decToHex(rsv[0].number.number);
|
581
|
+
const s = decToHex(rsv[1].number.number);
|
582
|
+
const v = Number(rsv[2].number.number);
|
583
|
+
return {
|
584
|
+
publicKey,
|
585
|
+
r,
|
586
|
+
s,
|
587
|
+
v
|
588
|
+
}
|
589
|
+
},
|
590
|
+
async getEthVote(voteId: string): Promise<null | IVoteEth[]> {
|
591
|
+
console.log('getEthVote ', voteId);
|
592
|
+
|
593
|
+
const result = await this.provider!.tonweb.provider.call(this.params.tonCollectorAddress, 'get_external_voting_data', [['num', voteId]]);
|
594
|
+
if (result.exit_code === 309) {
|
595
|
+
return null;
|
596
|
+
}
|
597
|
+
|
598
|
+
let list;
|
599
|
+
try {
|
600
|
+
list = result.stack[0][1].elements;
|
601
|
+
} catch (e) {
|
602
|
+
console.log('getEthVote, corrupted result', result);
|
603
|
+
return null;
|
604
|
+
}
|
605
|
+
|
606
|
+
const status = {
|
607
|
+
signatures: list.map(this.parseEthSignature)
|
608
|
+
};
|
609
|
+
|
610
|
+
return status.signatures;
|
611
|
+
},
|
612
|
+
async getTonVote(queryId: string): Promise<null | number[]> {
|
613
|
+
console.log('getTonVote ', queryId);
|
614
|
+
|
615
|
+
const result = await this.provider!.tonweb.provider.call(this.params.tonMultisigAddress, 'get_query_state', [['num', queryId]]);
|
616
|
+
|
617
|
+
let a, b;
|
618
|
+
try {
|
619
|
+
a = getNumber(result.stack[0]);
|
620
|
+
b = getNumber(result.stack[1]);
|
621
|
+
} catch (e) {
|
622
|
+
console.log('getTonVote, corrupted result', result);
|
623
|
+
return null;
|
624
|
+
}
|
625
|
+
console.log('getTonVote', result, a, b);
|
626
|
+
|
627
|
+
const arr = [];
|
628
|
+
const count = a === -1 ? this.provider!.oraclesTotal : b.toString(2).split('0').join('').length; // count of bits
|
629
|
+
for (let i = 0; i < count; i++) {
|
630
|
+
arr.push(1);
|
631
|
+
}
|
632
|
+
return arr;
|
633
|
+
},
|
634
|
+
async mint(): Promise<any> {
|
635
|
+
let receipt;
|
636
|
+
try {
|
637
|
+
let signatures = (this.state.votes! as IVoteEth[]).map(v => {
|
638
|
+
return {
|
639
|
+
signer: v.publicKey,
|
640
|
+
signature: ethers.utils.joinSignature({r: v.r, s: v.s, v: v.v})
|
641
|
+
}
|
642
|
+
})
|
643
|
+
|
644
|
+
signatures = signatures.sort((a, b) => {
|
645
|
+
return new BN(a.signer.substr(2), 16).cmp(new BN(b.signer.substr(2), 16));
|
646
|
+
});
|
647
|
+
|
648
|
+
console.log('voteForMinting', JSON.stringify(this.state.swapData!), JSON.stringify(signatures));
|
649
|
+
|
650
|
+
receipt = await this.provider!.wtonContract.methods.voteForMinting(this.state.swapData!, signatures).send({from: this.provider!.myEthAddress})
|
651
|
+
.on('transactionHash', () => {
|
652
|
+
this.state.toCurrencySent = true;
|
653
|
+
this.deleteState();
|
654
|
+
});
|
655
|
+
} catch (e) {
|
656
|
+
console.error(e);
|
657
|
+
return;
|
658
|
+
}
|
659
|
+
|
660
|
+
if (receipt.status) {
|
661
|
+
this.state.step = 5;
|
662
|
+
this.deleteState();
|
663
|
+
} else {
|
664
|
+
console.error('transaction fail', receipt);
|
665
|
+
}
|
666
|
+
},
|
667
|
+
async burn(): Promise<void> {
|
668
|
+
const fromAddress = this.provider!.myEthAddress;
|
669
|
+
const toAddress = this.toAddress;
|
670
|
+
const amount = this.amount;
|
671
|
+
|
672
|
+
const addressTon = new TonWeb.utils.Address(toAddress);
|
673
|
+
const wc = addressTon.wc;
|
674
|
+
const hashPart = TonWeb.utils.bytesToHex(addressTon.hashPart);
|
675
|
+
const amountUnit = toUnit(amount);
|
676
|
+
|
677
|
+
let receipt;
|
678
|
+
|
679
|
+
try {
|
680
|
+
receipt = await this.provider!.wtonContract.methods.burn(amountUnit, {
|
681
|
+
workchain: wc,
|
682
|
+
address_hash: '0x' + hashPart
|
683
|
+
}).send({from: fromAddress})
|
684
|
+
.on('transactionHash', () => {
|
685
|
+
this.state.fromCurrencySent = true;
|
686
|
+
});
|
687
|
+
} catch (e) {
|
688
|
+
console.error(e);
|
689
|
+
this.resetState();
|
690
|
+
return;
|
691
|
+
}
|
692
|
+
|
693
|
+
if (receipt.status) {
|
694
|
+
console.log('receipt', receipt);
|
695
|
+
|
696
|
+
this.state.blockNumber = receipt.blockNumber;
|
697
|
+
this.ethToTon = {
|
698
|
+
transactionHash: receipt.transactionHash,
|
699
|
+
logIndex: receipt.events.SwapEthToTon.logIndex,
|
700
|
+
blockNumber: this.state.blockNumber,
|
701
|
+
blockTime: 0,
|
702
|
+
blockHash: '',
|
703
|
+
from: fromAddress,
|
704
|
+
to: {
|
705
|
+
workchain: wc,
|
706
|
+
address_hash: hashPart
|
707
|
+
},
|
708
|
+
value: amountUnit
|
709
|
+
};
|
710
|
+
|
711
|
+
this.state.step = 2;
|
712
|
+
} else {
|
713
|
+
console.error('transaction fail', receipt);
|
714
|
+
}
|
715
|
+
},
|
716
|
+
onDoneClick(): void {
|
717
|
+
this.resetState();
|
718
|
+
},
|
719
|
+
onCancelClick(): void {
|
720
|
+
this.deleteState();
|
721
|
+
this.resetState();
|
722
|
+
},
|
723
|
+
async onAccountChanged(accounts: Array<any>): Promise<void> {
|
724
|
+
console.log('accountsChanged', accounts);
|
725
|
+
const address: string = accounts[0] as string;
|
726
|
+
if (!this.provider) {
|
727
|
+
return;
|
728
|
+
}
|
729
|
+
if (!(new BN(await this.provider!.web3.eth.getBalance(address)).gt(new BN('0')))) {
|
730
|
+
alert(this.$t(`Bridge.networks.${this.pair}.errors.lowBalance`) as string);
|
731
|
+
return;
|
732
|
+
}
|
733
|
+
this.provider!.myEthAddress = address;
|
734
|
+
console.log('address is', this.provider!.myEthAddress);
|
735
|
+
},
|
736
|
+
async initProvider(): Promise<IProvider | null> {
|
737
|
+
const ethereum = window.ethereum;
|
738
|
+
|
739
|
+
if (!ethereum) {
|
740
|
+
alert(this.$t('Bridge.errors.installMetamask') as string);
|
741
|
+
return null;
|
742
|
+
} else {
|
743
|
+
let myEthAddress;
|
744
|
+
try {
|
745
|
+
const accounts = (await ethereum.send('eth_requestAccounts')).result;
|
746
|
+
myEthAddress = accounts[0];
|
747
|
+
console.log('address is', myEthAddress);
|
748
|
+
} catch (error) {
|
749
|
+
console.log(error);
|
750
|
+
return null;
|
751
|
+
}
|
752
|
+
|
753
|
+
ethereum.addListener('accountsChanged', this.onAccountChanged);
|
754
|
+
|
755
|
+
if (ethereum.networkVersion as string !== String(this.params.chainId)) {
|
756
|
+
//eth
|
757
|
+
const error = (this.$t('Bridge.errors.wrongMetamaskNetwork') as string)
|
758
|
+
.replace('<NETWORK>', this.$t(`Bridge.networks.${this.pair}.${this.netTypeName}.full`) as string)
|
759
|
+
alert(error);
|
760
|
+
return null;
|
761
|
+
}
|
762
|
+
|
763
|
+
const web3 = new Web3(ethereum);
|
764
|
+
const wtonContract = new web3.eth.Contract(WTON as AbiItem[], this.params.wTonAddress);
|
765
|
+
const oraclesTotal = (await wtonContract.methods.getFullOracleSet().call()).length;
|
766
|
+
|
767
|
+
if (!(oraclesTotal > 0)) {
|
768
|
+
return null;
|
769
|
+
}
|
770
|
+
|
771
|
+
if (!(new BN(await web3.eth.getBalance(myEthAddress)).gt(new BN('0')))) {
|
772
|
+
alert(this.$t(`Bridge.networks.${this.pair}.errors.lowBalance`) as string);
|
773
|
+
return null;
|
774
|
+
}
|
775
|
+
|
776
|
+
this.newBlockHeadersSubscription = web3.eth.subscribe('newBlockHeaders')
|
777
|
+
.on('data', (blockHeader) => {
|
778
|
+
this.provider!.blockNumber = blockHeader.number;
|
779
|
+
})
|
780
|
+
.on('error', (error) => {
|
781
|
+
console.error("Error on newBlockHeaders", error);
|
782
|
+
});
|
783
|
+
|
784
|
+
const tonweb = new TonWeb(new TonWeb.HttpProvider(this.params.tonCenterUrl, {apiKey: 'ba68682c292bf1ad6150319d94670d36a81313f08fe67592c99e43c8f718d298'}));
|
785
|
+
|
786
|
+
const bridgeData = (await tonweb.provider.call(this.params.tonBridgeAddress, 'get_bridge_data', [])).stack;
|
787
|
+
|
788
|
+
if (bridgeData.length !== 8) throw new Error('Invalid bridge data')
|
789
|
+
const stateFlags = getNumber(bridgeData[0]);
|
790
|
+
const totalLocked = getNumber(bridgeData[1]);
|
791
|
+
const collectorWc = getNumber(bridgeData[2]);
|
792
|
+
const collectorAddr = bridgeData[3][1]; // string
|
793
|
+
const feeFlat = new BN(getNumber(bridgeData[4]));
|
794
|
+
const feeNetwork = new BN(getNumber(bridgeData[5]));
|
795
|
+
const feeFactor = new BN(getNumber(bridgeData[6]));
|
796
|
+
const feeBase = new BN(getNumber(bridgeData[7]));
|
797
|
+
|
798
|
+
const res: IProvider = {
|
799
|
+
blockNumber: 0,
|
800
|
+
myEthAddress,
|
801
|
+
web3,
|
802
|
+
wtonContract,
|
803
|
+
tonweb,
|
804
|
+
oraclesTotal,
|
805
|
+
feeFlat: feeFlat.add(feeNetwork),
|
806
|
+
feeFactor,
|
807
|
+
feeBase
|
808
|
+
};
|
809
|
+
|
810
|
+
return res;
|
811
|
+
}
|
812
|
+
},
|
813
|
+
async onTransferClick(): Promise<void> {
|
814
|
+
if (isNaN(this.amount)) {
|
815
|
+
alert(this.$t('Bridge.errors.notValidAmount') as string);
|
816
|
+
return;
|
817
|
+
}
|
818
|
+
if (this.amount < 10) {
|
819
|
+
alert(this.$t('Bridge.errors.amountBelow10') as string);
|
820
|
+
return;
|
821
|
+
}
|
822
|
+
|
823
|
+
if (this.toAddress.toLowerCase() === this.params.wTonAddress.toLowerCase() ||
|
824
|
+
this.toAddress.toLowerCase() === this.params.tonBridgeAddress.toLowerCase()) {
|
825
|
+
alert(this.$t('Bridge.errors.needPersonalAddress') as string);
|
826
|
+
return;
|
827
|
+
}
|
828
|
+
|
829
|
+
if (this.isFromTon) {
|
830
|
+
if (!Web3.utils.isAddress(this.toAddress)) {
|
831
|
+
alert(this.$t(`Bridge.networks.${this.pair}.errors.invalidAddress`) as string);
|
832
|
+
return;
|
833
|
+
}
|
834
|
+
} else {
|
835
|
+
if (!TonWeb.utils.Address.isValid(this.toAddress)) {
|
836
|
+
alert(this.$t(`Bridge.networks.ton.errors.invalidAddress`) as string);
|
837
|
+
return;
|
838
|
+
}
|
839
|
+
}
|
840
|
+
|
841
|
+
if (!this.provider) {
|
842
|
+
this.provider = await this.initProvider();
|
843
|
+
if (!this.provider) {
|
844
|
+
return;
|
845
|
+
}
|
846
|
+
}
|
847
|
+
|
848
|
+
if (!this.isFromTon) {
|
849
|
+
const userErcBalance = fromUnit(Number(await (this.provider!.wtonContract.methods.balanceOf(this.provider!.myEthAddress).call())));
|
850
|
+
if (this.amount > userErcBalance) {
|
851
|
+
alert((this.$t('Bridge.errors.toncoinBalance') as string).replace('<BALANCE>', String(userErcBalance)));
|
852
|
+
return;
|
853
|
+
}
|
854
|
+
}
|
855
|
+
|
856
|
+
this.state.createTime = Date.now();
|
857
|
+
this.state.step = 1;
|
858
|
+
|
859
|
+
if (this.isFromTon) {
|
860
|
+
this.saveState();
|
861
|
+
} else {
|
862
|
+
await this.burn();
|
863
|
+
}
|
864
|
+
}
|
865
|
+
}
|
866
|
+
})
|
867
|
+
</script>
|
868
|
+
|
869
|
+
|
870
|
+
<style lang="less" scoped>
|
871
|
+
@r: .BridgeProcessor;
|
872
|
+
|
873
|
+
@{r} {
|
874
|
+
&-transfer,
|
875
|
+
&-getTonCoin,
|
876
|
+
&-done,
|
877
|
+
&-cancel {
|
878
|
+
-webkit-appearance: none;
|
879
|
+
background-color: #1d98dc;
|
880
|
+
border-radius: 25px;
|
881
|
+
color: white;
|
882
|
+
font-size: 16px;
|
883
|
+
line-height: 19px;
|
884
|
+
border: none;
|
885
|
+
padding: 15px 35px 14px;
|
886
|
+
margin-top: 20px;
|
887
|
+
|
888
|
+
.isPointer &:hover,
|
889
|
+
.isTouch &:active {
|
890
|
+
background-color: #5fb8ea;
|
891
|
+
}
|
892
|
+
}
|
893
|
+
|
894
|
+
&-infoWrapper {
|
895
|
+
text-align: left;
|
896
|
+
width: fit-content;
|
897
|
+
font-size: 18px;
|
898
|
+
|
899
|
+
@media (max-width: 800px) {
|
900
|
+
font-size: 16px;
|
901
|
+
}
|
902
|
+
}
|
903
|
+
|
904
|
+
&-infoLine {
|
905
|
+
margin-top: 20px;
|
906
|
+
display: flex;
|
907
|
+
align-items: center;
|
908
|
+
}
|
909
|
+
|
910
|
+
&-info-icon {
|
911
|
+
flex-shrink: 0;
|
912
|
+
|
913
|
+
&.done {
|
914
|
+
width: 18px;
|
915
|
+
height: 18px;
|
916
|
+
background-image: url('~assets/pics/done.svg');
|
917
|
+
background-size: contain;
|
918
|
+
background-repeat: no-repeat;
|
919
|
+
background-position: center;
|
920
|
+
margin-right: 10px;
|
921
|
+
}
|
922
|
+
|
923
|
+
&.pending {
|
924
|
+
width: 12px;
|
925
|
+
height: 12px;
|
926
|
+
border: 3px solid #1d98dc;
|
927
|
+
border-left: 3px solid transparent;
|
928
|
+
border-radius: 50%;
|
929
|
+
animation: rotating 2s linear infinite;
|
930
|
+
margin-right: 13px;
|
931
|
+
margin-left: 3px;
|
932
|
+
}
|
933
|
+
|
934
|
+
&.none {
|
935
|
+
width: 8px;
|
936
|
+
height: 8px;
|
937
|
+
margin-left: 4px;
|
938
|
+
margin-right: 14px;
|
939
|
+
background-color: #1d98dc;
|
940
|
+
border-radius: 50%;
|
941
|
+
}
|
942
|
+
}
|
943
|
+
|
944
|
+
&-info-text {
|
945
|
+
a {
|
946
|
+
color: #1d98dc;
|
947
|
+
text-decoration: underline;
|
948
|
+
|
949
|
+
.isPointer &:hover,
|
950
|
+
.isTouch &:active {
|
951
|
+
text-decoration: none;
|
952
|
+
}
|
953
|
+
}
|
954
|
+
|
955
|
+
.note {
|
956
|
+
margin-top: 8px;
|
957
|
+
}
|
958
|
+
}
|
959
|
+
|
960
|
+
@keyframes rotating {
|
961
|
+
from {
|
962
|
+
-ms-transform: rotate(0deg);
|
963
|
+
-moz-transform: rotate(0deg);
|
964
|
+
-webkit-transform: rotate(0deg);
|
965
|
+
-o-transform: rotate(0deg);
|
966
|
+
transform: rotate(0deg);
|
967
|
+
}
|
968
|
+
to {
|
969
|
+
-ms-transform: rotate(360deg);
|
970
|
+
-moz-transform: rotate(360deg);
|
971
|
+
-webkit-transform: rotate(360deg);
|
972
|
+
-o-transform: rotate(360deg);
|
973
|
+
transform: rotate(360deg);
|
974
|
+
}
|
975
|
+
}
|
976
|
+
}
|
977
|
+
</style>
|