Commit 79ae4bf9 authored by yann300's avatar yann300

modularisation

parent cfe1903a
'use strict'
var React = require('react')
var style = require('./basicStyles')
var codeResolver = require('./codeResolver')
module.exports = React.createClass({
contextTypes: {
traceManager: React.PropTypes.object,
tx: React.PropTypes.object,
web3: React.PropTypes.object
},
getInitialState: function () {
return {
code: [],
selected: -1,
address: '' // selected instruction in the asm
}
},
getDefaultProps: function () {
return {
currentStepIndex: -1
}
},
render: function () {
return (
<select
size='10'
ref='itemsList'
style={style.instructionsList}
value={this.state.selected}>
{this.renderAssemblyItems()}
</select>
)
},
renderAssemblyItems: function () {
if (this.state.code) {
return this.state.code.map(function (item, i) {
return <option key={i} value={i}>{item}</option>
})
}
},
componentWillReceiveProps: function (nextProps) {
console.log('asm' + JSON.stringify(nextProps))
if (nextProps.currentStepIndex < 0) return
codeResolver.setWeb3(this.context.web3)
var self = this
this.context.traceManager.getCurrentCalledAddressAt(nextProps.currentStepIndex, function (address) {
self.ensureCodeLoaded(address, nextProps.currentStepIndex)
})
},
ensureCodeLoaded: function (address, currentStep) {
if (address !== this.state.address) {
this.setState({
code: ['loading...']
})
var self = this
codeResolver.resolveCode(address, currentStep, this.context.tx, function (address, code) {
self.setState({
code: code,
address: address
})
self.setInstructionIndex(address, currentStep)
})
} else {
this.setInstructionIndex(this.state.address, currentStep)
}
},
setInstructionIndex: function (address, step) {
var self = this
this.context.traceManager.getCurrentPC(step, function (instIndex) {
self.setState({
selected: codeResolver.getInstructionIndex(address, instIndex)
})
})
}
})
This diff is collapsed.
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
var React = require('react') var React = require('react')
module.exports = React.createClass({ module.exports = React.createClass({
contextTypes: {
traceManager: React.PropTypes.object
},
propTypes: { propTypes: {
stepIntoBack: React.PropTypes.func.isRequired, stepIntoBack: React.PropTypes.func.isRequired,
stepIntoForward: React.PropTypes.func.isRequired, stepIntoForward: React.PropTypes.func.isRequired,
...@@ -32,7 +36,7 @@ module.exports = React.createClass({ ...@@ -32,7 +36,7 @@ module.exports = React.createClass({
if (incr === -1) { if (incr === -1) {
return this.props.step === 0 ? 'disabled' : '' return this.props.step === 0 ? 'disabled' : ''
} else if (incr === 1) { } else if (incr === 1) {
return this.props.step >= this.props.vmTraceLength - 1 ? 'disabled' : '' return this.props.step >= this.props.max - 1 ? 'disabled' : ''
} }
} }
}) })
'use strict'
var codeUtils = require('./codeUtils')
module.exports = {
web3: null,
codes: {}, // assembly items instructions list by contract addesses
instructionsIndexByBytesOffset: {}, // mapping between bytes offset and instructions index.
setWeb3: function (web3) {
this.web3 = web3
},
resolveCode: function (address, vmTraceIndex, transaction, callBack) {
var cache = this.getExecutingCodeFromCache(address)
if (cache) {
callBack(address, cache.code)
return
}
if (vmTraceIndex === 0 && transaction.to === null) { // start of the trace
callBack(address, this.cacheExecutingCode(address, transaction.input).code)
return
}
var self = this
this.loadCode(address, function (code) {
callBack(address, self.cacheExecutingCode(address, code).code)
})
},
loadCode: function (address, callback) {
console.log('loading new code from web3 ' + address)
this.web3.eth.getCode(address, function (error, result) {
if (error) {
console.log(error)
} else {
callback(result)
}
})
},
cacheExecutingCode: function (address, hexCode) {
var code = codeUtils.nameOpCodes(new Buffer(hexCode.substring(2), 'hex'))
this.codes[address] = code[0]
this.instructionsIndexByBytesOffset[address] = code[1]
return {
code: code[0],
instructionsIndexByBytesOffset: code[1]
}
},
getExecutingCodeFromCache: function (address) {
if (this.codes[address]) {
return {
code: this.codes[address],
instructionsIndexByBytesOffset: this.instructionsIndexByBytesOffset[address]
}
} else {
return null
}
},
getInstructionIndex: function (address, pc) {
return this.getExecutingCodeFromCache(address).instructionsIndexByBytesOffset[pc]
}
}
'use strict' 'use strict'
var React = require('react') var React = require('react')
var TxBrowser = require('./txBrowser') var TxBrowser = require('./txBrowser')
var StepManager = require('./stepManager')
var AssemblyItemsBrowser = require('./assemblyItemsBrowser') var AssemblyItemsBrowser = require('./assemblyItemsBrowser')
var traceManager = require('./traceManager')
var style = require('./basicStyles') var style = require('./basicStyles')
module.exports = React.createClass({ module.exports = React.createClass({
getInitialState: function () { getInitialState: function () {
return { return {
vmTrace: null, tx: null,
state: '', currentStepIndex: -1 // index of the selected item in the vmtrace
currentStep: -1
} }
}, },
childContextTypes: { childContextTypes: {
web3: React.PropTypes.object web3: React.PropTypes.object,
traceManager: React.PropTypes.object,
tx: React.PropTypes.object
}, },
getChildContext: function () { getChildContext: function () {
return { web3: this.props.web3 } return {
web3: this.props.web3,
traceManager: traceManager,
tx: this.state.tx
}
},
componentDidMount: function () {
traceManager.setWeb3(this.props.web3)
}, },
render: function () { render: function () {
return ( return (
<div style={style.wrapper}> <div style={style.wrapper}>
<h1 style={style.container}>Eth Debugger</h1> <h1 style={style.container}>Eth Debugger</h1>
<TxBrowser onNewTxRequested={this.retrieveVmTrace} /> <TxBrowser onNewTxRequested={this.startDebugging} />
<div style={style.container}> <StepManager ref='stepManager' onStepChanged={this.stepChanged} />
{this.state.state} <AssemblyItemsBrowser currentStepIndex={this.state.currentStepIndex} />
</div>
<AssemblyItemsBrowser vmTrace={this.state.vmTrace} transaction={this.state.transaction} />
</div> </div>
) )
}, },
retrieveVmTrace: function (blockNumber, txNumber, tx) { stepChanged: function (stepIndex) {
if (this.state.state !== '') return this.setState({
currentStepIndex: stepIndex
var self = this })
this.setState({state: 'loading...'}) },
this.props.web3.debug.trace(blockNumber, parseInt(txNumber), function (error, result) { startDebugging: function (blockNumber, txIndex, tx) {
if (error) { if (traceManager.isLoading) {
console.log(error) return
} else {
self.setState({vmTrace: result, transaction: tx, state: ''})
} }
console.log('loading trace...')
this.setState({
tx: tx
})
traceManager.setTransaction(tx)
var self = this
traceManager.resolveTrace(blockNumber, txIndex, function (success) {
console.log('trace loaded ' + success)
self.setState({
currentStepIndex: 0
})
self.refs.stepManager.newTraceAvailable()
}) })
} }
}) })
...@@ -3,6 +3,10 @@ var React = require('react') ...@@ -3,6 +3,10 @@ var React = require('react')
var style = require('./sliderStyles') var style = require('./sliderStyles')
module.exports = React.createClass({ module.exports = React.createClass({
contextTypes: {
traceManager: React.PropTypes.object
},
propTypes: { propTypes: {
onChange: React.PropTypes.func.isRequired onChange: React.PropTypes.func.isRequired
}, },
...@@ -10,7 +14,7 @@ module.exports = React.createClass({ ...@@ -10,7 +14,7 @@ module.exports = React.createClass({
getDefaultProps: function () { getDefaultProps: function () {
return { return {
min: 0, min: 0,
max: 500 max: 1
} }
}, },
...@@ -28,6 +32,10 @@ module.exports = React.createClass({ ...@@ -28,6 +32,10 @@ module.exports = React.createClass({
) )
}, },
componentDidMount: function () {
this.setValue(0)
},
onMouseUp: function (event) { onMouseUp: function (event) {
this.props.onChange(parseInt(this.refs.rule.value)) this.props.onChange(parseInt(this.refs.rule.value))
}, },
......
'use strict'
var React = require('react')
var ButtonNavigator = require('./buttonNavigator')
var Slider = require('./slider')
var style = require('./basicStyles')
module.exports = React.createClass({
propTypes: {
onStepChanged: React.PropTypes.func.isRequired
},
contextTypes: {
traceManager: React.PropTypes.object
},
getInitialState: function () {
return {
currentStepIndex: 0,
traceLength: 0
}
},
render: function () {
return (
<div style={style.container}>
<Slider
ref='slider'
onChange={this.sliderMoved}
min='0'
max={this.state.traceLength} />
<ButtonNavigator
stepIntoBack={this.stepIntoBack}
stepIntoForward={this.stepIntoForward}
stepOverBack={this.stepOverBack}
stepOverForward={this.stepOverForward}
jumpToNextCall={this.jumpToNextCall}
max={this.state.traceLength} />
</div>
)
},
init: function () {
this.refs.slider.setValue(0)
},
newTraceAvailable: function () {
this.init()
var self = this
this.context.traceManager.getLength(function (length) {
self.setState({ traceLength: length })
})
},
sliderMoved: function (step) {
this.props.onStepChanged(step)
this.changeState(step)
},
stepIntoForward: function () {
var step = this.state.currentStepIndex + 1
this.props.onStepChanged(step)
this.changeState(step)
},
stepIntoBack: function () {
var step = this.state.currentStepIndex - 1
this.props.onStepChanged(step)
this.refs.slider.setValue(step)
this.changeState(step)
},
stepOverForward: function () {
var step = this.context.traceManager.findStepOverForward(this.state.currentStepIndex)
this.props.onStepChanged(step)
this.refs.slider.setValue(step)
this.changeState(step)
},
stepOverBack: function () {
var step = this.context.traceManager.findStepOverBack(this.state.currentStepIndex)
this.props.onStepChanged(step)
this.refs.slider.setValue(step)
this.changeState(step)
},
jumpToNextCall: function () {
var step = this.context.traceManager.findNextCall(this.state.currentStepIndex)
this.props.onStepChanged(step)
this.refs.slider.setValue(step)
this.changeState(step)
},
changeState: function (step) {
this.setState({
currentStepIndex: step
})
}
})
...@@ -2,9 +2,22 @@ ...@@ -2,9 +2,22 @@
var React = require('react') var React = require('react')
module.exports = React.createClass({ module.exports = React.createClass({
contextTypes: {
traceManager: React.PropTypes.object
},
getDefaultProps: function () { getDefaultProps: function () {
return { return {
data: null currentStepIndex: -1
}
},
getInitialState: function () {
return {
step: '',
addmemory: '',
gas: '',
remainingGas: ''
} }
}, },
...@@ -13,7 +26,38 @@ module.exports = React.createClass({ ...@@ -13,7 +26,38 @@ module.exports = React.createClass({
<div> <div>
<table> <table>
<tbody> <tbody>
{this.renderItems()} <tr key='step'>
<td>
step
</td>
<td>
{this.state.step}
</td>
</tr>
<tr key='addmemory'>
<td>
add memory
</td>
<td>
{this.state.addmemory}
</td>
</tr>
<tr key='gas'>
<td>
gas
</td>
<td>
{this.state.gas}
</td>
</tr>
<tr key='remaininggas'>
<td>
remaining gas
</td>
<td>
{this.state.remaingas}
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
...@@ -21,9 +65,9 @@ module.exports = React.createClass({ ...@@ -21,9 +65,9 @@ module.exports = React.createClass({
}, },
renderItems: function () { renderItems: function () {
if (this.props.data) { if (this.state.data) {
var ret = [] var ret = []
for (var key in this.props.data) { for (var key in this.state.data) {
ret.push( ret.push(
<tr key={key}> <tr key={key}>
<td> <td>
...@@ -34,5 +78,34 @@ module.exports = React.createClass({ ...@@ -34,5 +78,34 @@ module.exports = React.createClass({
return ret return ret
} }
return null return null
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.currentStepIndex < 0) return
var self = this
this.context.traceManager.getCurrentStep(nextProps.currentStepIndex, function (step) {
self.setState({
step: step
})
})
this.context.traceManager.getMemExpand(nextProps.currentStepIndex, function (addmem) {
self.setState({
addmemory: addmem
})
})
this.context.traceManager.getStepCost(nextProps.currentStepIndex, function (gas) {
self.setState({
gas: gas
})
})
this.context.traceManager.getRemainingGas(nextProps.currentStepIndex, function (remaingas) {
self.setState({
remaininGas: remaingas
})
})
} }
}) })
'use strict'
var React = require('react')
module.exports = React.createClass({
contextTypes: {
web3: React.PropTypes.object
},
getInitialState: function () {
return {
storage: {},
storageChanges: [],
vmTraceIndexByStorageChange: {},
vmTraceChangesRef: []
}
},
init: function () {
var defaultState = this.getInitialState()
this.state.storage = defaultState.storage
this.state.storageChanges = defaultState.storageChanges
this.state.vmTraceIndexByStorageChange = defaultState.vmTraceIndexByStorageChange
this.state.vmTraceChangesRef = defaultState.vmTraceChangesRef
},
render: function () {
return null
},
// retrieve the storage of an account just after the execution of txHash
retrieveStorage: function (address, transaction, callBack) {
if (this.state.storage[address]) {
callBack(this.state.storage[address])
}
var self = this
if (transaction) {
this.context.web3.debug.storageAt(transaction.blockNumber.toString(), transaction.transactionIndex, address, function (error, result) {
if (error) {
console.log(error)
} else {
self.state.storage[address] = result
callBack(result)
}
})
} else {
console.log('transaction is not defined')
}
},
trackStorageChange: function (vmTraceIndex, trace) {
var change = false
if (trace.address) {
// new context
this.state.storageChanges.push({ address: trace.address, changes: [] })
change = true
} else if (trace.depth && !trace.address) {
// returned from context
this.state.storageChanges.push({ address: this.state.storageChanges[this.state.storageChanges.length - 1].address, changes: [] })
change = true
} else if (trace.inst === 'SSTORE') {
this.state.storageChanges[this.state.storageChanges.length - 1].changes.push(
{
'key': trace.stack[trace.stack.length - 1],
'value': trace.stack[trace.stack.length - 2]
})
change = true
}
if (change) {
this.state.vmTraceIndexByStorageChange[vmTraceIndex] = {
context: this.state.storageChanges.length - 1,
changes: this.state.storageChanges[this.state.storageChanges.length - 1].changes.length - 1
}
this.state.vmTraceChangesRef.push(vmTraceIndex)
}
},
rebuildStorageAt: function (vmTraceIndex, transaction, callBack) {
var changesLocation = this.retrieveLastChange(vmTraceIndex)
if (!changesLocation) {
console.log('unable to build storage')
callBack({})
} else {
var address = this.state.storageChanges[changesLocation.context].address
this.retrieveStorage(address, transaction, function (storage) {
for (var k = 0; k < changesLocation.context; k++) {
var context = this.state.storageChanges[k]
if (context.address === address) {
for (var i = 0; i < context.changes.length; i++) {
if (i > changesLocation.changes) break
var change = context.changes[i]
storage[change.key] = change.value
}
}
}
callBack(storage)
})
}
},
retrieveLastChange: function (vmTraceIndex) {
var change = this.state.vmTraceIndexByStorageChange[vmTraceIndex]
if (change) {
return change
} else {
for (var k in this.state.vmTraceChangesRef) {
if (this.state.vmTraceChangesRef[k] > vmTraceIndex) {
return this.state.vmTraceIndexByStorageChange[k - 1]
}
}
}
}
})
'use strict'
module.exports = {
isLoading: false,
web3: null,
transaction: null,
trace: null,
// vmtrace changes section
depthChanges: [],
callStack: {},
memoryChanges: [],
callDataChanges: [],
// storage section
storageChanges: [],
vmTraceIndexByStorageChange: {},
vmTraceChangesRef: [],
storages: {},
// init section
setWeb3: function (web3) {
this.web3 = web3
},
setTransaction: function (tx) {
this.transaction = tx
},
resolveTrace: function (blockNumber, txNumber, callback) {
this.isLoading = true
this.init()
if (!this.web3) callback(false)
var self = this
this.web3.debug.trace(blockNumber, parseInt(txNumber), function (error, result) {
if (!error) {
self.computeTrace(result)
callback(true)
} else {
console.log(error)
callback(false)
}
this.isLoading = false
})
},
init: function () {
this.trace = null
this.depthChanges = []
this.memoryChanges = []
this.callDataChanges = []
this.storageChanges = []
this.vmTraceIndexByStorageChange = {}
this.vmTraceChangesRef = []
this.callStack = {}
},
computeTrace: function (trace) {
this.trace = trace
var currentDepth = 0
var currentStorageAddress
var callStack = []
for (var k in this.trace) {
var step = this.trace[k]
this.calldata(k, step)
this.memory(k, step)
currentStorageAddress = this.storage(k, step, currentStorageAddress)
var depth = this.depth(k, step, currentDepth, callStack)
if (depth) {
currentDepth = depth
}
}
},
// compute trace section
calldata: function (index, step) {
if (step.calldata) {
this.callDataChanges.push(index)
}
},
memory: function (index, step) {
if (step.memory) {
this.memoryChanges.push(index)
}
},
storage: function (index, step, currentAddress) {
var change = false
if (step.address) {
// new context
this.storageChanges.push({ address: step.address, changes: [] })
change = true
} else if (step.inst === 'SSTORE') {
this.storageChanges[this.storageChanges.length - 1].changes.push(
{
'key': step.stack[step.stack.length - 1],
'value': step.stack[step.stack.length - 2]
})
change = true
} else if (!step.address && step.depth) {
// returned from context
var address = this.storageChanges[this.storageChanges.length - 2].address
this.storageChanges.push({ address: address, changes: [] })
change = true
}
if (change) {
this.vmTraceIndexByStorageChange[index] = {
context: this.storageChanges.length - 1,
changes: this.storageChanges[this.storageChanges.length - 1].changes.length - 1
}
this.vmTraceChangesRef.push(index)
}
return currentAddress
},
depth: function (index, step, currentDepth, callStack) {
if (step.depth === undefined) return
if (step.depth > currentDepth) {
if (index === 0) {
callStack.push('0x' + step.address) // new context
} else {
// getting the address from the stack
var callTrace = this.trace[index - 1]
var address = callTrace.stack[callTrace.stack.length - 2]
callStack.push(address) // new context
}
} else if (step.depth < currentDepth) {
callStack.pop() // returning from context
}
this.callStack[index] = {
stack: callStack.slice(0),
depth: step.depth,
address: step.address
}
this.depthChanges.push(index)
return step.depth
},
// API section
getLength: function (callback) {
if (!this.trace) callback(0)
callback(this.trace.length)
},
getStorageAt: function (stepIndex, callback) {
var stoChange = this.lastPropertyChange(stepIndex, this.vmTraceChangesRef)
if (!stoChange) {
return {}
}
var changeRefs = this.vmTraceIndexByStorageChange[stoChange]
var address = this.storageChanges[changeRefs.context].address
var self = this
this.retrieveStorage(address, function (storage) {
for (var k = 0; k < changeRefs.context; k++) {
var context = self.storageChanges[k]
if (context.address === address) {
for (var i = 0; i < context.changes.length; i++) {
if (i > changeRefs.changes) break
var change = context.changes[i]
storage[change.key] = change.value
}
}
}
callback(storage)
})
},
getCallDataAt: function (stepIndex, callback) {
var callDataChange = this.lastPropertyChange(stepIndex, this.callDataChanges)
if (!callDataChange) return ['']
callback([this.trace[callDataChange].calldata])
},
getCallStackAt: function (stepIndex, callback) {
var callStackChange = this.lastPropertyChange(stepIndex, this.depthChanges)
if (!callStackChange) return ''
callback(this.callStack[callStackChange].stack)
},
getStackAt: function (stepIndex, callback) {
var stack
if (this.trace[stepIndex].stack) { // there's always a stack
stack = this.trace[stepIndex].stack.slice(0)
stack.reverse()
callback(stack)
}
},
getLastDepthIndexChangeSince: function (stepIndex, callback) {
var depthIndex = this.lastPropertyChange(stepIndex, this.depthChanges)
callback(depthIndex)
},
getCurrentCalledAddressAt: function (stepIndex, callback) {
var self = this
this.getLastDepthIndexChangeSince(stepIndex, function (addressIndex) {
callback(self.resolveAddress(addressIndex))
})
},
getMemoryAt: function (stepIndex, callback) {
var lastChanges = this.lastPropertyChange(stepIndex, this.memoryChanges)
if (!lastChanges) return ''
callback(this.trace[lastChanges].memory)
},
getCurrentPC: function (stepIndex, callback) {
callback(this.trace[stepIndex].pc)
},
getCurrentStep: function (stepIndex, callback) {
callback(this.trace[stepIndex].steps)
},
getMemExpand: function (stepIndex, callback) {
callback(this.trace[stepIndex].memexpand ? this.trace[stepIndex].memexpand : '')
},
getStepCost: function (stepIndex, callback) {
callback(this.trace[stepIndex].gascost)
},
getRemainingGas: function (stepIndex, callback) {
callback(this.trace[stepIndex].gas)
},
// step section
isCallInstruction: function (index) {
var state = this.trace[index]
return state.instname === 'CALL' || state.instname === 'CALLCODE' || state.instname === 'CREATE' || state.instname === 'DELEGATECALL'
},
isReturnInstruction: function (index) {
var state = this.trace[index]
return state.instname === 'RETURN'
},
findStepOverBack: function (currentStep) {
if (this.isReturnInstruction(currentStep - 1)) {
return this.findStepOutBack(currentStep)
} else {
return currentStep - 1
}
},
findStepOverForward: function (currentStep) {
if (this.isCallInstruction(currentStep)) {
return this.findStepOutForward(currentStep)
} else {
return currentStep + 1
}
},
findStepOutBack: function (currentStep) {
var i = currentStep - 1
var depth = 0
while (--i >= 0) {
if (this.isCallInstruction(i)) {
if (depth === 0) {
break
} else {
depth--
}
} else if (this.isReturnInstruction(i)) {
depth++
}
}
return i
},
findStepOutForward: function (currentStep) {
var i = currentStep
var depth = 0
while (++i < this.trace.length) {
if (this.isReturnInstruction(i)) {
if (depth === 0) {
break
} else {
depth--
}
} else if (this.isCallInstruction(i)) {
depth++
}
}
return i + 1
},
// util section
lastPropertyChange: function (target, changes) {
if (changes.length === 1) {
if (changes[0] > target) {
// we only a closest maximum, returning 0
return null
} else {
return changes[0]
}
}
var middle = Math.floor(changes.length / 2)
if (changes[middle] > target) {
return this.lastPropertyChange(target, changes.slice(0, middle))
} else if (changes[middle] < target) {
return this.lastPropertyChange(target, changes.slice(middle, changes.length))
} else {
return changes[middle]
}
},
resolveAddress: function (vmTraceIndex) {
var address = this.trace[vmTraceIndex].address
if (vmTraceIndex > 0) {
var stack = this.trace[vmTraceIndex - 1].stack // callcode, delegatecall, ...
address = stack[stack.length - 2]
}
return address
},
// retrieve the storage of an account just after the execution of tx
retrieveStorage: function (address, callBack) {
if (this.storages[address]) {
callBack(this.storages[address])
}
var self = this
if (this.transaction) {
this.web3.debug.storageAt(this.transaction.blockNumber.toString(), this.transaction.transactionIndex, address, function (error, result) {
if (error) {
console.log(error)
} else {
self.storages[address] = result
callBack(result)
}
})
} else {
console.log('transaction is not defined')
}
}
}
...@@ -42,18 +42,22 @@ module.exports = React.createClass({ ...@@ -42,18 +42,22 @@ module.exports = React.createClass({
Get Get
</button> </button>
<div style={style.transactionInfo}> <div style={style.transactionInfo}>
<div> <table>
Hash: <tbody>
{this.state.hash} <tr>
</div> <td>Hash: </td>
<div> <td>{this.state.hash}</td>
From: </tr>
{this.state.from} <tr>
</div> <td>From: </td>
<div> <td>{this.state.from}</td>
To: </tr>
{this.state.to} <tr>
</div> <td>To: </td>
<td>{this.state.to}</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment