Skip to content

Commit 1466e00

Browse files
committedMar 25, 2021
check error.code exists, get stand alone wallet instance
1 parent 630749b commit 1466e00

File tree

8 files changed

+228
-130
lines changed

8 files changed

+228
-130
lines changed
 

‎CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### Release 2.1.23-beta.8
2+
- Add method to retrieve stand alone wallet instance (instance without a web3 provider)
3+
- check for err.code before making a string comparison check for DATA_CHANNEL_ERROR
4+
15
### Release 2.1.23-beta.7
26
- Use Modified simple-peer library that does not kill connection on datachannel close
37
- Add re-creation of dataChannel when dataChannel errors or closes and connection is still active

‎example/app/src/App.vue

+16-2
Original file line numberDiff line numberDiff line change
@@ -404,13 +404,27 @@ export default {
404404
// Initialize the provider based client
405405
// this.connect = new mewConnect.Provider({windowClosedError: true, rpcUrl: 'ws://127.0.0.1:8545', /*chainId: 1*/});
406406
// 859569f6decc4446a5da1bb680e7e9cf
407+
408+
const newNetworks = {
409+
name: 'matic',
410+
name_long: 'matic',
411+
blockExplorerTX: '',
412+
blockExplorerAddr: '',
413+
chainID: 80001,
414+
currencyName: 'matic',
415+
service: 'matic',
416+
url: 'https://rpc-mumbai.matic.today'
417+
418+
}
407419
this.connect = new mewConnect.Provider({
420+
// newNetworks: [newNetworks],
408421
windowClosedError: true,
409422
// chainId: 1,
410-
chainId: 3,
423+
chainId: 80001,
411424
// rpcUrl: 'https://mainnet.infura.io/v3/' //'wss://mainnet.infura.io/ws/v3/'
412425
// rpcUrl: 'HTTP://127.0.0.1:7545'
413-
rpcUrl: 'https://ropsten.infura.io/v3/c9b249497d074ab59c47a97bdfe6b401'
426+
// rpcUrl: 'https://ropsten.infura.io/v3/c9b249497d074ab59c47a97bdfe6b401'
427+
rpcUrl: 'https://rpc-mumbai.matic.today'
414428
// rpcUrl: 'ws://127.0.0.1:8545'
415429
// infuraId: '7d06294ad2bd432887eada360c5e1986'
416430
});

‎package-lock.json

+57-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"eccrypto": "^1.1.3",
3333
"ethereumjs-common": "^1.5.0",
3434
"ethereumjs-tx": "^2.1.2",
35-
"ethereumjs-util": "^5.2.0",
3635
"ethjs-unit": "^0.1.6",
3736
"events": "^3.1.0",
3837
"isomorphic-ws": "^4.0.1",
@@ -107,5 +106,9 @@
107106
"url-loader": "^3.0.0",
108107
"vue-template-compiler": "^2.6.10",
109108
"yorkie": "^2.0.0"
109+
},
110+
"optionalDependencies": {
111+
"eth-sig-util": "^3.0.1",
112+
"ethereumjs-util": "^5.2.1"
110113
}
111114
}

‎src/connectClient/WebRtcCommunication.js

+26-49
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,6 @@ export default class WebRtcCommunication extends MewConnectCommon {
138138
this.canSignal = !this.canSignal;
139139
this.fallbackTimer();
140140
this.setActivePeerId();
141-
// if (this.p) {
142-
// console.log('PEER EXISTS'); // todo remove dev item
143-
// this.p.destroy();
144-
// delete this.p;
145-
// }
146141
this.p = new this.Peer(simpleOptions);
147142
const peerID = this.getActivePeerId();
148143
this.answerReceived[peerID] = false;
@@ -159,7 +154,7 @@ export default class WebRtcCommunication extends MewConnectCommon {
159154
this.stateChangeListener.bind(this, peerID)
160155
);
161156
this.p._pc.addEventListener('icecandidateerror', event => {
162-
debug('ICE CANIDATE ERROR', event); // todo remove dev item
157+
debug('ICE CANIDATE ERROR', event);
163158
});
164159
}
165160

@@ -244,14 +239,6 @@ export default class WebRtcCommunication extends MewConnectCommon {
244239
// Handle Socket Attempting Turn informative signal
245240
// Provide Notice that initial WebRTC connection failed and the fallback method will be used
246241
willAttemptTurn() {
247-
// let connectionExists = false;
248-
// this.p.getStats((err, stats) => {
249-
// if (err) console.log(err);
250-
// connectionExists = stats.find(stat => stat.state === 'succeeded');
251-
// console.log(connectionExists); // todo remove dev item
252-
// });
253-
254-
// if (connectionExists) return;
255242
this.uiCommunicator(this.lifeCycle.UsingFallback, this.activeInitiatorId);
256243
if (!this.connected && this.tryingTurn && this.usingVersion === 'V2') {
257244
this.refreshQrTimer();
@@ -295,13 +282,6 @@ export default class WebRtcCommunication extends MewConnectCommon {
295282
}
296283
}
297284

298-
// Handle Socket event to initiate turn connection
299-
// Handle Receipt of TURN server details, and begin a WebRTC connection attempt using TURN
300-
// beginTurn(data) {
301-
// console.log('TRYING TURN VALUE SET 3'); // todo remove dev item
302-
// this.tryingTurn = true;
303-
// this.retryViaTurn(data);
304-
// }
305285

306286
// ----- Failure Handlers
307287

@@ -410,29 +390,32 @@ export default class WebRtcCommunication extends MewConnectCommon {
410390
debug('peerID', peerID);
411391
debug(err.code);
412392
debug('error', err);
413-
if (err.code.includes('ERR_DATA_CHANNEL') && this.connected) {
414-
if (this.isAlive() && this.p.createNewDataChannel) {
415-
try {
416-
debug('re-create dataChannel')
417-
this.p.createNewDataChannel(uuid());
418-
if(!this.channelTest && !this.outstandingMobileMessage){
419-
this.channelTest = true;
420-
// this.sendRtcMessage('address', '', '123')
421-
this.channelTestTimer = setTimeout(() => {
422-
if(this.channelTest){
423-
debug('new data channel failed to respond')
424-
this.disconnectRTC();
425-
}
426-
}, 5000)
427-
return;
393+
if(err.code && this.connected){
394+
if (err.code.includes('ERR_DATA_CHANNEL') ) {
395+
if (this.isAlive() && this.p.createNewDataChannel) {
396+
try {
397+
debug('re-create dataChannel')
398+
this.p.createNewDataChannel(uuid());
399+
if(!this.channelTest && !this.outstandingMobileMessage){
400+
this.channelTest = true;
401+
// this.sendRtcMessage('address', '', '123')
402+
this.channelTestTimer = setTimeout(() => {
403+
if(this.channelTest){
404+
debug('new data channel failed to respond')
405+
this.disconnectRTC();
406+
}
407+
}, 5000)
408+
return;
409+
}
410+
} catch (e) {
411+
// eslint-disable-next-line
412+
debug(e);
413+
this.disconnectRTC();
428414
}
429-
} catch (e) {
430-
// eslint-disable-next-line
431-
debug(e);
432-
this.disconnectRTC();
433415
}
434416
}
435417
}
418+
436419
if (!this.connected && !this.tryingTurn && !this.turnDisabled) {
437420
this.useFallback();
438421
} else {
@@ -504,14 +487,8 @@ export default class WebRtcCommunication extends MewConnectCommon {
504487

505488
async rtcSend(arg) {
506489
try {
507-
debug(this.isAlive()); // todo remove dev item
490+
debug(this.isAlive());
508491
if (this.isAlive()) {
509-
// if(!this.sentMessageIds.includes(arg.id)){
510-
// this.sentMessageIds.push(arg.id)
511-
// } else {
512-
// console.log('known id'); // todo remove dev item
513-
// return;
514-
// }
515492
let encryptedSend;
516493
if (typeof arg === 'string') {
517494
encryptedSend = await this.mewCrypto.encrypt(arg);
@@ -532,10 +509,10 @@ export default class WebRtcCommunication extends MewConnectCommon {
532509
}
533510

534511
rtcDestroy() {
535-
debug('rtcDestroy'); // todo remove dev item
512+
debug('rtcDestroy');
536513
if (this.isAlive()) {
537514
this.p.destroy();
538-
debug('DESTROYED'); // todo remove dev item
515+
debug('DESTROYED');
539516
this.connected = false;
540517
this.uiCommunicator(this.lifeCycle.RtcDestroyedEvent);
541518
} else if (!this.p.destroyed) {

‎src/connectClient/initiator/MewConnectInitiator.js

+17-21
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import MewConnectCrypto from '../MewConnectCrypto';
1010
import MewConnectInitiatorV2 from './MewConnectInitiatorV2';
1111
import MewConnectInitiatorV1 from './MewConnectInitiatorV1';
1212

13+
import PopUpCreator from '../../connectWindow/popUpCreator'
14+
import MEWconnectWallet from '../../connectProvider/web3Provider/MEWconnect/index';
15+
import PopUpHandler from '../../connectWindow/popUpHandler';
16+
17+
1318
import WebRtcCommunication from '../WebRtcCommunication';
1419
import { DISCONNECTED, CONNECTED } from '../../config';
1520

@@ -55,7 +60,7 @@ export default class MewConnectInitiator extends MewConnectCommon {
5560

5661
this.mewCrypto = options.cryptoImpl || MewConnectCrypto.create();
5762
this.webRtcCommunication = new WebRtcCommunication(this.mewCrypto);
58-
this.popupCreator = options.popupCreator; // || new PopUpCreator();
63+
this.popupCreator = options.popupCreator ? options.popupCreator : options.newPopupCreator ? new PopUpCreator() : undefined;
5964

6065
debugConnectionState(
6166
'Initial Connection State:',
@@ -88,6 +93,15 @@ export default class MewConnectInitiator extends MewConnectCommon {
8893
return MewConnectInitiator.connectionState;
8994
}
9095

96+
async createWalletOnly(network){
97+
this.popUpHandler = new PopUpHandler();
98+
return MEWconnectWallet(
99+
{ network },
100+
this.popupCreator,
101+
this.popUpHandler
102+
);
103+
}
104+
91105
isAlive() {
92106
if (this.p !== null) {
93107
return this.p.connected && !this.p.destroyed;
@@ -208,24 +222,6 @@ export default class MewConnectInitiator extends MewConnectCommon {
208222
};
209223

210224
debug(qrCodeString);
211-
// if (this.showPopup) {
212-
// if (this.popupCreator.popupWindowOpen) {
213-
// this.popupCreator.updateQrCode(qrCodeString);
214-
// } else {
215-
// this.popupCreator.refreshQrcode = this.initiatorStart.bind(this);
216-
// this.popupCreator.openPopupWindow(qrCodeString);
217-
// // this.popupCreator.container.addEventListener('beforeunload', unloadOrClosed);
218-
// this.popupCreator.container.addEventListener(
219-
// 'mewModalClosed',
220-
// unloadOrClosed,
221-
// { once: true }
222-
// );
223-
// }
224-
// } else {
225-
// this.uiCommunicator(this.lifeCycle.codeDisplay, qrCodeString);
226-
// this.uiCommunicator(this.lifeCycle.checkNumber, privateKey);
227-
// this.uiCommunicator(this.lifeCycle.ConnectionId, this.connId);
228-
// }
229225
} catch (e) {
230226
debug('displayCode error:', e);
231227
}
@@ -364,7 +360,7 @@ Keys
364360
this.showingRefresh = false; // reset refresh
365361
});
366362
const regenerateQRcodeOnClick = () => {
367-
debug('REGENERATE'); // todo remove dev item
363+
debug('REGENERATE');
368364
this.refreshCode();
369365
};
370366

@@ -448,7 +444,7 @@ Keys
448444
this.webRtcCommunication.removeAllListeners(
449445
this.jsonDetails.lifeCycle.RtcConnectedEvent
450446
);
451-
debug('RTC CONNECTED ENVIRONMENT SETUP'); // todo remove dev item
447+
debug('RTC CONNECTED ENVIRONMENT SETUP');
452448
this.emit(this.lifeCycle.RtcConnectedEvent);
453449
this.webRtcCommunication.on('appData', this.dataReceived.bind(this));
454450
this.connected = true;

‎src/connectProvider/index.js

+103-53
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ import EventEmitter from 'events';
1010
import EventNames from './web3Provider/web3-provider/events';
1111
import { Transaction } from 'ethereumjs-tx';
1212
import { messageConstants } from '../messages';
13-
// import parseTokensData from './web3Provider/helpers/parseTokensData';
1413
import debugLogger from 'debug';
1514
import PopUpCreator from '../connectWindow/popUpCreator';
1615
import { nativeCheck, mobileCheck } from './platformDeepLinking';
1716
import { DISCONNECTED, CONNECTING, CONNECTED } from '../config';
18-
import packageJson from '../../package.json'
17+
import packageJson from '../../package.json';
1918

2019
const debugConnectionState = debugLogger('MEWconnect:connection-state');
2120
const debugErrors = debugLogger('MEWconnectError');
@@ -61,33 +60,74 @@ export default class Integration extends EventEmitter {
6160
this.CHAIN_ID = options.chainId || 1;
6261
this.RPC_URL = options.rpcUrl || false;
6362
this.noUrlCheck = options.noUrlCheck || false;
63+
this.newNetworks = options.newNetworks || [];
64+
this.knownNetworks = new Set();
6465
this.lastHash = null;
6566
this.initiator = new Initiator();
6667
this.popUpHandler = new PopUpHandler();
6768
this.connectionState = false;
68-
this.chainIdMapping = this.createChainMapping();
69+
this.chainIdMapping = this.createChainMapping(this.newNetworks);
6970
this.returnPromise = null;
7071
this.disconnectComplete = false;
7172
popUpCreator = new PopUpCreator();
7273
}
7374

74-
closeDataChannelForDemo(){
75+
closeDataChannelForDemo() {
7576
const connection = state.wallet.getConnection();
76-
connection.webRtcCommunication.closeDataChannelForDemo()
77+
connection.webRtcCommunication.closeDataChannelForDemo();
7778
}
7879

79-
createChainMapping() {
80-
return Object.keys(Networks).reduce(
80+
formatNewNetworks(newNetwork) {
81+
return {
82+
type: {
83+
name: newNetwork.name,
84+
name_long: newNetwork.name_long || newNetwork.name,
85+
homePage: newNetwork.homePage || '',
86+
blockExplorerTX: newNetwork.blockExplorerTX || '',
87+
blockExplorerAddr: newNetwork.blockExplorerAddr || '',
88+
chainID: newNetwork.chainId
89+
? newNetwork.chainId
90+
: newNetwork.chainID
91+
? newNetwork.chainID
92+
: this.CHAIN_ID,
93+
tokens: newNetwork.tokens || [],
94+
contracts: [],
95+
currencyName: newNetwork.currencyName || newNetwork.name
96+
},
97+
service: newNetwork.serviceName || newNetwork.name,
98+
url: newNetwork.url || this.RPC_URL,
99+
port: 443,
100+
auth: false,
101+
username: '',
102+
password: ''
103+
};
104+
}
105+
106+
createChainMapping(newNetworks) {
107+
let networks = Networks;
108+
try {
109+
const additional = newNetworks
110+
.map(this.formatNewNetworks)
111+
.reduce((acc, curr) => {
112+
acc[curr.type.name] = curr;
113+
}, {});
114+
networks = { ...networks, ...additional };
115+
} catch (e) {
116+
// eslint-disable-next-line
117+
console.error(e);
118+
}
119+
return Object.keys(networks).reduce(
81120
(acc, curr) => {
82-
if (Networks[curr].length === 0) return acc;
121+
if (networks[curr].length === 0) return acc;
83122
acc.push({
84123
name:
85-
Networks[curr][0].type.name_long === 'Ethereum'
124+
networks[curr][0].type.name_long === 'Ethereum'
86125
? 'mainnet'
87-
: Networks[curr][0].type.name_long.toLowerCase(),
88-
chainId: Networks[curr][0].type.chainID,
89-
key: Networks[curr][0].type.name
126+
: networks[curr][0].type.name_long.toLowerCase(),
127+
chainId: networks[curr][0].type.chainID,
128+
key: networks[curr][0].type.name
90129
});
130+
this.knownNetworks.add(networks[curr][0].type.chainID)
91131
return acc;
92132
},
93133
[{ name: 'mainnet', chainId: 1, key: 'ETH' }]
@@ -122,16 +162,16 @@ export default class Integration extends EventEmitter {
122162
);
123163
}
124164

125-
get getWalletOnly(){
126-
if(state.wallet){
127-
return state.wallet
165+
get getWalletOnly() {
166+
if (state.wallet) {
167+
return state.wallet;
128168
}
129169
}
130170

131171
enable() {
132172
popUpCreator.on('fatalError', () => {
133173
MEWconnectWallet.setConnectionState(DISCONNECTED);
134-
})
174+
});
135175
if (this.runningInApp) {
136176
return new Promise((resolve, reject) => {
137177
state.web3Provider
@@ -185,7 +225,7 @@ export default class Integration extends EventEmitter {
185225
popUpCreator,
186226
this.popUpHandler
187227
);
188-
console.log(`Using MEWconnect v${packageJson.version}`); // todo remove dev item
228+
console.log(`Using MEWconnect v${packageJson.version}`);
189229
this.popUpHandler.showConnectedNotice();
190230
this.popUpHandler.hideNotifier();
191231
this.createDisconnectNotifier();
@@ -203,7 +243,7 @@ export default class Integration extends EventEmitter {
203243
if (state.web3Provider.accountsChanged) {
204244
state.web3Provider.emit('accountsChanged', [
205245
state.wallet.getChecksumAddressString()
206-
])
246+
]);
207247
// state.web3Provider.accountsChanged([
208248
// state.wallet.getChecksumAddressString()
209249
// ]);
@@ -253,9 +293,17 @@ export default class Integration extends EventEmitter {
253293
web3Provider = window.web3.currentProvider;
254294
}
255295
} else {
256-
const chain = this.identifyChain(CHAIN_ID || 1);
257-
const defaultNetwork = Networks[chain.key][0];
258-
state.network = defaultNetwork;
296+
let chain, defaultNetwork;
297+
if(this.knownNetworks.has(CHAIN_ID)){
298+
chain = this.identifyChain(CHAIN_ID || 1);
299+
defaultNetwork = Networks[chain.key][0];
300+
state.network = defaultNetwork;
301+
} else {
302+
chain = {name: 'unknown'}
303+
defaultNetwork = this.formatNewNetworks({name: 'unknown'})
304+
state.network = defaultNetwork
305+
}
306+
259307
if (this.infuraId && !this.RPC_URL) {
260308
RPC_URL = infuraUrlFormater(chain.name, this.infuraId);
261309
}
@@ -267,7 +315,9 @@ export default class Integration extends EventEmitter {
267315
!/[wW]/.test(hostUrl.protocol) &&
268316
!/[htpHTP]/.test(hostUrl.protocol)
269317
) {
270-
throw Error('Invalid rpc endpoint supplied to MEWconnect during setup');
318+
throw Error(
319+
'Invalid rpc endpoint supplied to MEWconnect during setup'
320+
);
271321
}
272322
if (!_noCheck && !this.infuraId) {
273323
if (
@@ -297,7 +347,7 @@ export default class Integration extends EventEmitter {
297347
}
298348

299349
state.enable = this.enable.bind(this);
300-
web3Provider.close = this.disconnect//.bind(this);
350+
web3Provider.close = this.disconnect; //.bind(this);
301351
web3Provider.disconnect = this.disconnect.bind(this);
302352
state.web3Provider = web3Provider;
303353
state.web3 = new Web3(web3Provider);
@@ -329,11 +379,14 @@ export default class Integration extends EventEmitter {
329379
connection.lifeCycle.RtcDisconnectEvent,
330380
() => {
331381
try {
332-
if(this.popupCreator) this.popUpHandler.showNotice(messageConstants.disconnect);
333-
MEWconnectWallet.setConnectionState(connection.lifeCycle.disconnected);
334-
if(state.wallet !== null){
335-
this.emit('close')
336-
this.emit('disconnect')
382+
if (this.popupCreator)
383+
this.popUpHandler.showNotice(messageConstants.disconnect);
384+
MEWconnectWallet.setConnectionState(
385+
connection.lifeCycle.disconnected
386+
);
387+
if (state.wallet !== null) {
388+
this.emit('close');
389+
this.emit('disconnect');
337390
}
338391
if (state.wallet !== null && state.web3Provider) {
339392
state.web3Provider.emit('disconnect');
@@ -347,11 +400,10 @@ export default class Integration extends EventEmitter {
347400
}
348401
state.wallet = null;
349402
this.emit(DISCONNECTED);
350-
this.emit('close')
351-
this.emit('disconnect')
352-
403+
this.emit('close');
404+
this.emit('disconnect');
353405
} catch (e) {
354-
if(this.popUpHandler){
406+
if (this.popUpHandler) {
355407
this.popUpHandler.showNotice(messageConstants.disconnectError);
356408
}
357409
}
@@ -363,10 +415,12 @@ export default class Integration extends EventEmitter {
363415
() => {
364416
try {
365417
this.popUpHandler.showNotice(messageConstants.disconnect);
366-
MEWconnectWallet.setConnectionState(connection.lifeCycle.disconnected);
367-
if(state.wallet !== null){
368-
this.emit('close')
369-
this.emit('disconnect')
418+
MEWconnectWallet.setConnectionState(
419+
connection.lifeCycle.disconnected
420+
);
421+
if (state.wallet !== null) {
422+
this.emit('close');
423+
this.emit('disconnect');
370424
}
371425
if (state.wallet !== null && state.web3Provider) {
372426
state.web3Provider.emit('disconnect');
@@ -381,26 +435,23 @@ export default class Integration extends EventEmitter {
381435

382436
state.wallet = null;
383437
this.emit(connection.lifeCycle.disconnected);
384-
385438
} catch (e) {
386-
if(this.popUpHandler){
439+
if (this.popUpHandler) {
387440
this.popUpHandler.showNotice(messageConstants.disconnectError);
388441
}
389442
}
390443
}
391444
);
392445
}
393446

394-
createCommunicationError(){
447+
createCommunicationError() {
395448
const connection = state.wallet.getConnection();
396-
connection.webRtcCommunication.on(
397-
connection.lifeCycle.decryptError,
398-
() => {
399-
if(this.popupCreator) this.popUpHandler.showNoticePersistentEnter(
449+
connection.webRtcCommunication.on(connection.lifeCycle.decryptError, () => {
450+
if (this.popupCreator)
451+
this.popUpHandler.showNoticePersistentEnter(
400452
messageConstants.communicationError
401453
);
402-
}
403-
);
454+
});
404455
}
405456

406457
disconnect() {
@@ -420,7 +471,7 @@ export default class Integration extends EventEmitter {
420471
return true;
421472
} catch (e) {
422473
debugErrors('disconnect ERROR');
423-
if(this.popUpHandler){
474+
if (this.popUpHandler) {
424475
this.popUpHandler.showNotice(messageConstants.disconnectError);
425476
}
426477
// eslint-disable-next-line
@@ -449,12 +500,11 @@ export default class Integration extends EventEmitter {
449500
state.wallet
450501
.signTransaction(tx)
451502
.then(_response => {
503+
if (!transactionCache.includes(_response.tx.hash)) {
504+
transactionCache.push(_response.tx.hash);
452505

453-
if(!transactionCache.includes(_response.tx.hash)){
454-
transactionCache.push(_response.tx.hash)
455-
456-
this.popUpHandler.showNoticePersistentExit();
457-
resolve(_response);
506+
this.popUpHandler.showNoticePersistentExit();
507+
resolve(_response);
458508
}
459509
})
460510
.catch(err => {
@@ -482,10 +532,10 @@ export default class Integration extends EventEmitter {
482532
}
483533
});
484534
eventHub.on(EventNames.ERROR_NOTIFY, err => {
485-
if(err && err.message){
535+
if (err && err.message) {
486536
this.popUpHandler.showNotice(err.message);
487537
}
488-
})
538+
});
489539
eventHub.on(EventNames.SHOW_MSG_CONFIRM_MODAL, (msg, resolve) => {
490540
if (!state.wallet) {
491541
this.popUpHandler.showNoticePersistentEnter(

‎src/connectProvider/web3Provider/networks/tokens/tokens-eth.json

+1-1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.