Skip to content
28 changes: 24 additions & 4 deletions src/server/blockchain/Web3Wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ export class Web3Wallet {
try {
log.info('WalletInit: Obtained AdminWallet address', { adminWalletAddress, network: this.network })

// Reset per-run so re-running init() doesn't keep stacking addresses found by a previous run.
this.filledAddresses = []

const adminWalletContractBalance = await this.web3.eth.getBalance(adminWalletAddress)
log.info(`WalletInit: AdminWallet contract balance`, { adminWalletContractBalance, adminWalletAddress })

Expand Down Expand Up @@ -400,7 +403,22 @@ export class Web3Wallet {
break
}
}
// this.address = this.filledAddresses[0]

// No configured wallet met the funding threshold above. Fall back to the first
// configured address instead of leaving `this.address` undefined, otherwise the
// getBalance/getTransactionCount calls below crash the process on an invalid
// (undefined) address even though the low-funds condition is already alerted on
// via Slack a few lines down. If there is no configured address at all, that's a
// genuine misconfiguration (no mnemonic/privateKey/KMS wallets set up) - fail loudly
// through the existing catch below instead of silently continuing with no address.
if (!this.address) {
if (this.addresses.length === 0) {
throw new Error('WalletInit: no admin wallet addresses configured (missing mnemonic/privateKey/KMS wallets)')
}

this.address = this.addresses[0]
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
}

this.proxyContract = new this.web3.eth.Contract(AdminWalletABI, adminWalletAddress, { from: this.address })

if (this.conf.topAdminsOnStartup) {
Expand Down Expand Up @@ -498,9 +516,11 @@ export class Web3Wallet {
} catch (e) {
log.error('WalletInit: Error initializing wallet', e.message, e)

if (this.conf.env !== 'test' && this.conf.env !== 'development') {
process.exit(-1)
}
await sendSlackAlert({
msg: `CRITICAL: AdminWallet init failed - ${e.message} ${this.name}`
})

return false
}

return true
Expand Down
20 changes: 20 additions & 0 deletions src/server/blockchain/__tests__/adminWallet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,24 @@ describe('adminwallet', () => {
const { release } = await txManager.lock(unverifiedAddresses)
await release()
})

test('adminWallet init falls back to a default address instead of crashing when no wallet is funded', async () => {
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {})
const isVerifiedAdminSpy = jest.spyOn(AdminWallet, 'isVerifiedAdmin').mockResolvedValue(false)

try {
// init() only sets this.address when a wallet qualifies, so clear the address the
// real (unmocked) init from beforeAll already found before re-running it here.
AdminWallet.address = undefined

await AdminWallet.init()

expect(AdminWallet.address).toBe(AdminWallet.addresses[0])
expect(AdminWallet.filledAddresses).toEqual([])
expect(exitSpy).not.toHaveBeenCalled()
} finally {
isVerifiedAdminSpy.mockRestore()
exitSpy.mockRestore()
}
})
Comment on lines +146 to +164

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Use try/finally around mocks to guarantee cleanup even if the test fails.

Currently mockRestore() is only reached if all assertions pass. If an earlier assertion throws, the mocked process.exit and AdminWallet.isVerifiedAdmin will persist into later tests. Please wrap the test body in try { ... } finally { isVerifiedAdminSpy.mockRestore(); exitSpy.mockRestore(); } so mocks are always restored, even on failure, and don’t leak between tests.

Suggested change
test('adminWallet init falls back to a default address instead of crashing when no wallet is funded', async () => {
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {})
const isVerifiedAdminSpy = jest.spyOn(AdminWallet, 'isVerifiedAdmin').mockResolvedValue(false)
// init() appends to filledAddresses instead of resetting it, so clear out whatever
// the real (unmocked) init from beforeAll already populated before re-running it here.
AdminWallet.filledAddresses = []
AdminWallet.address = undefined
await AdminWallet.init()
expect(AdminWallet.address).toBe(AdminWallet.addresses[0])
expect(AdminWallet.filledAddresses).toEqual([])
expect(exitSpy).not.toHaveBeenCalled()
isVerifiedAdminSpy.mockRestore()
exitSpy.mockRestore()
})
test('adminWallet init falls back to a default address instead of crashing when no wallet is funded', async () => {
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {})
const isVerifiedAdminSpy = jest
.spyOn(AdminWallet, 'isVerifiedAdmin')
.mockResolvedValue(false)
try {
// init() appends to filledAddresses instead of resetting it, so clear out whatever
// the real (unmocked) init from beforeAll already populated before re-running it here.
AdminWallet.filledAddresses = []
AdminWallet.address = undefined
await AdminWallet.init()
expect(AdminWallet.address).toBe(AdminWallet.addresses[0])
expect(AdminWallet.filledAddresses).toEqual([])
expect(exitSpy).not.toHaveBeenCalled()
} finally {
isVerifiedAdminSpy.mockRestore()
exitSpy.mockRestore()
}
})

})
Loading