|
|
@ -3,19 +3,20 @@ const ethUtil = require('ethereumjs-util') |
|
|
|
const EthTx = require('ethereumjs-tx') |
|
|
|
const EthTx = require('ethereumjs-tx') |
|
|
|
const ObservableStore = require('obs-store') |
|
|
|
const ObservableStore = require('obs-store') |
|
|
|
const clone = require('clone') |
|
|
|
const clone = require('clone') |
|
|
|
const PendingTransactionWatcher = require('../../app/scripts/lib/pending-tx-watchers') |
|
|
|
const { createStubedProvider } = require('../stub/provider') |
|
|
|
|
|
|
|
const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker') |
|
|
|
const noop = () => true |
|
|
|
const noop = () => true |
|
|
|
const currentNetworkId = 42 |
|
|
|
const currentNetworkId = 42 |
|
|
|
const otherNetworkId = 36 |
|
|
|
const otherNetworkId = 36 |
|
|
|
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') |
|
|
|
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') |
|
|
|
|
|
|
|
|
|
|
|
describe('PendingTransactionWatcher', function () { |
|
|
|
describe('PendingTransactionTracker', function () { |
|
|
|
let pendingTxWatcher, txMeta, txMetaNoHash, txMetaNoRawTx |
|
|
|
let pendingTxTracker, txMeta, txMetaNoHash, txMetaNoRawTx, providerResultStub, provider |
|
|
|
this.timeout(10000) |
|
|
|
this.timeout(10000) |
|
|
|
beforeEach(function () { |
|
|
|
beforeEach(function () { |
|
|
|
txMeta = { |
|
|
|
txMeta = { |
|
|
|
id: 1, |
|
|
|
id: 1, |
|
|
|
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eebBlock', |
|
|
|
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', |
|
|
|
status: 'signed', |
|
|
|
status: 'signed', |
|
|
|
txParams: { |
|
|
|
txParams: { |
|
|
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d', |
|
|
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d', |
|
|
@ -30,13 +31,15 @@ describe('PendingTransactionWatcher', function () { |
|
|
|
txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, |
|
|
|
txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, |
|
|
|
} |
|
|
|
} |
|
|
|
txMetaNoRawTx = { |
|
|
|
txMetaNoRawTx = { |
|
|
|
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eebBlock', |
|
|
|
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', |
|
|
|
status: 'signed', |
|
|
|
status: 'signed', |
|
|
|
txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, |
|
|
|
txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
providerResultStub = {} |
|
|
|
|
|
|
|
provider = createStubedProvider(providerResultStub) |
|
|
|
|
|
|
|
|
|
|
|
pendingTxWatcher = new PendingTransactionWatcher({ |
|
|
|
pendingTxTracker = new PendingTransactionTracker({ |
|
|
|
provider: { sendAsync: noop }, |
|
|
|
provider, |
|
|
|
getBalance: () => {}, |
|
|
|
getBalance: () => {}, |
|
|
|
nonceTracker: { |
|
|
|
nonceTracker: { |
|
|
|
getGlobalLock: async () => { |
|
|
|
getGlobalLock: async () => { |
|
|
@ -47,20 +50,6 @@ describe('PendingTransactionWatcher', function () { |
|
|
|
sufficientBalance: () => {}, |
|
|
|
sufficientBalance: () => {}, |
|
|
|
publishTransaction: () => {}, |
|
|
|
publishTransaction: () => {}, |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
pendingTxWatcher.query = new Proxy({}, { |
|
|
|
|
|
|
|
get: (queryStubResult, key) => { |
|
|
|
|
|
|
|
if (key === 'stubResult') { |
|
|
|
|
|
|
|
return function (method, ...args) { |
|
|
|
|
|
|
|
queryStubResult[method] = args |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
const returnValues = queryStubResult[key] |
|
|
|
|
|
|
|
return () => Promise.resolve(...returnValues) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
describe('#checkForTxInBlock', function () { |
|
|
|
describe('#checkForTxInBlock', function () { |
|
|
@ -68,77 +57,77 @@ describe('PendingTransactionWatcher', function () { |
|
|
|
// throw a type error if it trys to do anything on the block
|
|
|
|
// throw a type error if it trys to do anything on the block
|
|
|
|
// thus failing the test
|
|
|
|
// thus failing the test
|
|
|
|
const block = Proxy.revocable({}, {}).revoke() |
|
|
|
const block = Proxy.revocable({}, {}).revoke() |
|
|
|
pendingTxWatcher.checkForTxInBlock(block) |
|
|
|
pendingTxTracker.checkForTxInBlock(block) |
|
|
|
}) |
|
|
|
}) |
|
|
|
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { |
|
|
|
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { |
|
|
|
const block = Proxy.revocable({}, {}).revoke() |
|
|
|
const block = Proxy.revocable({}, {}).revoke() |
|
|
|
pendingTxWatcher.getPendingTransactions = () => [txMetaNoHash] |
|
|
|
pendingTxTracker.getPendingTransactions = () => [txMetaNoHash] |
|
|
|
pendingTxWatcher.once('txFailed', (txId, err) => { |
|
|
|
pendingTxTracker.once('txFailed', (txId, err) => { |
|
|
|
assert(txId, txMetaNoHash.id, 'should pass txId') |
|
|
|
assert(txId, txMetaNoHash.id, 'should pass txId') |
|
|
|
done() |
|
|
|
done() |
|
|
|
}) |
|
|
|
}) |
|
|
|
pendingTxWatcher.checkForTxInBlock(block) |
|
|
|
pendingTxTracker.checkForTxInBlock(block) |
|
|
|
}) |
|
|
|
}) |
|
|
|
it('should emit \'txConfirmed\' if the tx is in the block', function (done) { |
|
|
|
it('should emit \'txConfirmed\' if the tx is in the block', function (done) { |
|
|
|
const block = { transactions: [txMeta]} |
|
|
|
const block = { transactions: [txMeta]} |
|
|
|
pendingTxWatcher.getPendingTransactions = () => [txMeta] |
|
|
|
pendingTxTracker.getPendingTransactions = () => [txMeta] |
|
|
|
pendingTxWatcher.once('txConfirmed', (txId) => { |
|
|
|
pendingTxTracker.once('txConfirmed', (txId) => { |
|
|
|
assert(txId, txMeta.id, 'should pass txId') |
|
|
|
assert(txId, txMeta.id, 'should pass txId') |
|
|
|
done() |
|
|
|
done() |
|
|
|
}) |
|
|
|
}) |
|
|
|
pendingTxWatcher.once('txFailed', (_, err) => { done(err) }) |
|
|
|
pendingTxTracker.once('txFailed', (_, err) => { done(err) }) |
|
|
|
pendingTxWatcher.checkForTxInBlock(block) |
|
|
|
pendingTxTracker.checkForTxInBlock(block) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
describe('#queryPendingTxs', function () { |
|
|
|
describe('#queryPendingTxs', function () { |
|
|
|
it('should call #_checkPendingTxs if their is no oldBlock', function (done) { |
|
|
|
it('should call #_checkPendingTxs if their is no oldBlock', function (done) { |
|
|
|
let newBlock, oldBlock |
|
|
|
let newBlock, oldBlock |
|
|
|
newBlock = { number: '0x01' } |
|
|
|
newBlock = { number: '0x01' } |
|
|
|
pendingTxWatcher._checkPendingTxs = done |
|
|
|
pendingTxTracker._checkPendingTxs = done |
|
|
|
pendingTxWatcher.queryPendingTxs({oldBlock, newBlock}) |
|
|
|
pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) { |
|
|
|
it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) { |
|
|
|
let newBlock, oldBlock |
|
|
|
let newBlock, oldBlock |
|
|
|
oldBlock = { number: '0x01' } |
|
|
|
oldBlock = { number: '0x01' } |
|
|
|
newBlock = { number: '0x03' } |
|
|
|
newBlock = { number: '0x03' } |
|
|
|
pendingTxWatcher._checkPendingTxs = done |
|
|
|
pendingTxTracker._checkPendingTxs = done |
|
|
|
pendingTxWatcher.queryPendingTxs({oldBlock, newBlock}) |
|
|
|
pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) { |
|
|
|
it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) { |
|
|
|
let newBlock, oldBlock |
|
|
|
let newBlock, oldBlock |
|
|
|
oldBlock = { number: '0x1' } |
|
|
|
oldBlock = { number: '0x1' } |
|
|
|
newBlock = { number: '0x2' } |
|
|
|
newBlock = { number: '0x2' } |
|
|
|
pendingTxWatcher._checkPendingTxs = () => { |
|
|
|
pendingTxTracker._checkPendingTxs = () => { |
|
|
|
const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less') |
|
|
|
const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less') |
|
|
|
done(err) |
|
|
|
done(err) |
|
|
|
} |
|
|
|
} |
|
|
|
pendingTxWatcher.queryPendingTxs({oldBlock, newBlock}) |
|
|
|
pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) |
|
|
|
done() |
|
|
|
done() |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
describe('#_checkPendingTx', function () { |
|
|
|
describe('#_checkPendingTx', function () { |
|
|
|
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { |
|
|
|
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { |
|
|
|
pendingTxWatcher.once('txFailed', (txId, err) => { |
|
|
|
pendingTxTracker.once('txFailed', (txId, err) => { |
|
|
|
assert(txId, txMetaNoHash.id, 'should pass txId') |
|
|
|
assert(txId, txMetaNoHash.id, 'should pass txId') |
|
|
|
done() |
|
|
|
done() |
|
|
|
}) |
|
|
|
}) |
|
|
|
pendingTxWatcher._checkPendingTx(txMetaNoHash) |
|
|
|
pendingTxTracker._checkPendingTx(txMetaNoHash) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
it('should should return if query does not return txParams', function () { |
|
|
|
it('should should return if query does not return txParams', function () { |
|
|
|
pendingTxWatcher.query.stubResult('getTransactionByHash', null) |
|
|
|
providerResultStub.eth_getTransactionByHash = null |
|
|
|
pendingTxWatcher._checkPendingTx(txMeta) |
|
|
|
pendingTxTracker._checkPendingTx(txMeta) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
it('should emit \'txConfirmed\'', function (done) { |
|
|
|
it('should emit \'txConfirmed\'', function (done) { |
|
|
|
pendingTxWatcher.query.stubResult('getTransactionByHash', {blockNumber: '0x01'}) |
|
|
|
providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'} |
|
|
|
pendingTxWatcher.once('txConfirmed', (txId) => { |
|
|
|
pendingTxTracker.once('txConfirmed', (txId) => { |
|
|
|
assert(txId, txMeta.id, 'should pass txId') |
|
|
|
assert(txId, txMeta.id, 'should pass txId') |
|
|
|
done() |
|
|
|
done() |
|
|
|
}) |
|
|
|
}) |
|
|
|
pendingTxWatcher.once('txFailed', (_, err) => { done(err) }) |
|
|
|
pendingTxTracker.once('txFailed', (_, err) => { done(err) }) |
|
|
|
pendingTxWatcher._checkPendingTx(txMeta) |
|
|
|
pendingTxTracker._checkPendingTx(txMeta) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
@ -154,14 +143,14 @@ describe('PendingTransactionWatcher', function () { |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
it('should warp all txMeta\'s in #_checkPendingTx', function (done) { |
|
|
|
it('should warp all txMeta\'s in #_checkPendingTx', function (done) { |
|
|
|
pendingTxWatcher.getPendingTransactions = () => txList |
|
|
|
pendingTxTracker.getPendingTransactions = () => txList |
|
|
|
pendingTxWatcher._checkPendingTx = (tx) => { tx.resolve(tx) } |
|
|
|
pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) } |
|
|
|
const list = txList.map |
|
|
|
const list = txList.map |
|
|
|
Promise.all(txList.map((tx) => tx.processed)) |
|
|
|
Promise.all(txList.map((tx) => tx.processed)) |
|
|
|
.then((txCompletedList) => done()) |
|
|
|
.then((txCompletedList) => done()) |
|
|
|
.catch(done) |
|
|
|
.catch(done) |
|
|
|
|
|
|
|
|
|
|
|
pendingTxWatcher._checkPendingTxs() |
|
|
|
pendingTxTracker._checkPendingTxs() |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
@ -175,15 +164,15 @@ describe('PendingTransactionWatcher', function () { |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
it('should return if no pending transactions', function () { |
|
|
|
it('should return if no pending transactions', function () { |
|
|
|
pendingTxWatcher.resubmitPendingTxs() |
|
|
|
pendingTxTracker.resubmitPendingTxs() |
|
|
|
}) |
|
|
|
}) |
|
|
|
it('should call #_resubmitTx for all pending tx\'s', function (done) { |
|
|
|
it('should call #_resubmitTx for all pending tx\'s', function (done) { |
|
|
|
pendingTxWatcher.getPendingTransactions = () => txList |
|
|
|
pendingTxTracker.getPendingTransactions = () => txList |
|
|
|
pendingTxWatcher._resubmitTx = async (tx) => { tx.resolve(tx) } |
|
|
|
pendingTxTracker._resubmitTx = async (tx) => { tx.resolve(tx) } |
|
|
|
Promise.all(txList.map((tx) => tx.processed)) |
|
|
|
Promise.all(txList.map((tx) => tx.processed)) |
|
|
|
.then((txCompletedList) => done()) |
|
|
|
.then((txCompletedList) => done()) |
|
|
|
.catch(done) |
|
|
|
.catch(done) |
|
|
|
pendingTxWatcher.resubmitPendingTxs() |
|
|
|
pendingTxTracker.resubmitPendingTxs() |
|
|
|
}) |
|
|
|
}) |
|
|
|
it('should not emit \'txFailed\' if the txMeta throws a known txError', function (done) { |
|
|
|
it('should not emit \'txFailed\' if the txMeta throws a known txError', function (done) { |
|
|
|
knownErrors =[ |
|
|
|
knownErrors =[ |
|
|
@ -199,10 +188,10 @@ describe('PendingTransactionWatcher', function () { |
|
|
|
] |
|
|
|
] |
|
|
|
const enoughForAllErrors = txList.concat(txList) |
|
|
|
const enoughForAllErrors = txList.concat(txList) |
|
|
|
|
|
|
|
|
|
|
|
pendingTxWatcher.on('txFailed', (_, err) => done(err)) |
|
|
|
pendingTxTracker.on('txFailed', (_, err) => done(err)) |
|
|
|
|
|
|
|
|
|
|
|
pendingTxWatcher.getPendingTransactions = () => enoughForAllErrors |
|
|
|
pendingTxTracker.getPendingTransactions = () => enoughForAllErrors |
|
|
|
pendingTxWatcher._resubmitTx = async (tx) => { |
|
|
|
pendingTxTracker._resubmitTx = async (tx) => { |
|
|
|
tx.resolve() |
|
|
|
tx.resolve() |
|
|
|
throw new Error(knownErrors.pop()) |
|
|
|
throw new Error(knownErrors.pop()) |
|
|
|
} |
|
|
|
} |
|
|
@ -210,38 +199,38 @@ describe('PendingTransactionWatcher', function () { |
|
|
|
.then((txCompletedList) => done()) |
|
|
|
.then((txCompletedList) => done()) |
|
|
|
.catch(done) |
|
|
|
.catch(done) |
|
|
|
|
|
|
|
|
|
|
|
pendingTxWatcher.resubmitPendingTxs() |
|
|
|
pendingTxTracker.resubmitPendingTxs() |
|
|
|
}) |
|
|
|
}) |
|
|
|
it('should emit \'txFailed\' if it encountered a real error', function (done) { |
|
|
|
it('should emit \'txFailed\' if it encountered a real error', function (done) { |
|
|
|
pendingTxWatcher.once('txFailed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) |
|
|
|
pendingTxTracker.once('txFailed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) |
|
|
|
|
|
|
|
|
|
|
|
pendingTxWatcher.getPendingTransactions = () => txList |
|
|
|
pendingTxTracker.getPendingTransactions = () => txList |
|
|
|
pendingTxWatcher._resubmitTx = async (tx) => { throw new TypeError('im some real error') } |
|
|
|
pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') } |
|
|
|
Promise.all(txList.map((tx) => tx.processed)) |
|
|
|
Promise.all(txList.map((tx) => tx.processed)) |
|
|
|
.then((txCompletedList) => done()) |
|
|
|
.then((txCompletedList) => done()) |
|
|
|
.catch(done) |
|
|
|
.catch(done) |
|
|
|
|
|
|
|
|
|
|
|
pendingTxWatcher.resubmitPendingTxs() |
|
|
|
pendingTxTracker.resubmitPendingTxs() |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
describe('#_resubmitTx with a too-low balance', function () { |
|
|
|
describe('#_resubmitTx with a too-low balance', function () { |
|
|
|
it('should return before publishing the transaction because to low of balance', function (done) { |
|
|
|
it('should return before publishing the transaction because to low of balance', function (done) { |
|
|
|
const lowBalance = '0x0' |
|
|
|
const lowBalance = '0x0' |
|
|
|
pendingTxWatcher.getBalance = (address) => { |
|
|
|
pendingTxTracker.getBalance = (address) => { |
|
|
|
assert.equal(address, txMeta.txParams.from, 'Should pass the address') |
|
|
|
assert.equal(address, txMeta.txParams.from, 'Should pass the address') |
|
|
|
return lowBalance |
|
|
|
return lowBalance |
|
|
|
} |
|
|
|
} |
|
|
|
pendingTxWatcher.publishTransaction = async (rawTx) => { |
|
|
|
pendingTxTracker.publishTransaction = async (rawTx) => { |
|
|
|
done(new Error('tried to publish transaction')) |
|
|
|
done(new Error('tried to publish transaction')) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Stubbing out current account state:
|
|
|
|
// Stubbing out current account state:
|
|
|
|
// Adding the fake tx:
|
|
|
|
// Adding the fake tx:
|
|
|
|
pendingTxWatcher.once('txFailed', (txId, err) => { |
|
|
|
pendingTxTracker.once('txFailed', (txId, err) => { |
|
|
|
assert(err, 'Should have a error') |
|
|
|
assert(err, 'Should have a error') |
|
|
|
done() |
|
|
|
done() |
|
|
|
}) |
|
|
|
}) |
|
|
|
pendingTxWatcher._resubmitTx(txMeta) |
|
|
|
pendingTxTracker._resubmitTx(txMeta) |
|
|
|
.catch((err) => { |
|
|
|
.catch((err) => { |
|
|
|
assert.ifError(err, 'should not throw an error') |
|
|
|
assert.ifError(err, 'should not throw an error') |
|
|
|
done(err) |
|
|
|
done(err) |
|
|
@ -250,17 +239,17 @@ describe('PendingTransactionWatcher', function () { |
|
|
|
|
|
|
|
|
|
|
|
it('should publishing the transaction', function (done) { |
|
|
|
it('should publishing the transaction', function (done) { |
|
|
|
const enoughBalance = '0x100000' |
|
|
|
const enoughBalance = '0x100000' |
|
|
|
pendingTxWatcher.getBalance = (address) => { |
|
|
|
pendingTxTracker.getBalance = (address) => { |
|
|
|
assert.equal(address, txMeta.txParams.from, 'Should pass the address') |
|
|
|
assert.equal(address, txMeta.txParams.from, 'Should pass the address') |
|
|
|
return enoughBalance |
|
|
|
return enoughBalance |
|
|
|
} |
|
|
|
} |
|
|
|
pendingTxWatcher.publishTransaction = async (rawTx) => { |
|
|
|
pendingTxTracker.publishTransaction = async (rawTx) => { |
|
|
|
assert.equal(rawTx, txMeta.rawTx, 'Should pass the rawTx') |
|
|
|
assert.equal(rawTx, txMeta.rawTx, 'Should pass the rawTx') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Stubbing out current account state:
|
|
|
|
// Stubbing out current account state:
|
|
|
|
// Adding the fake tx:
|
|
|
|
// Adding the fake tx:
|
|
|
|
pendingTxWatcher._resubmitTx(txMeta) |
|
|
|
pendingTxTracker._resubmitTx(txMeta) |
|
|
|
.then(() => done()) |
|
|
|
.then(() => done()) |
|
|
|
.catch((err) => { |
|
|
|
.catch((err) => { |
|
|
|
assert.ifError(err, 'should not throw an error') |
|
|
|
assert.ifError(err, 'should not throw an error') |
|
|
|