diff --git a/.tool-versions b/.tool-versions index 0718df60..e761a701 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -nodejs 14.20.0 +nodejs 16.20.0 yarn 1.22.10 golang 1.18 diff --git a/package.json b/package.json index 4ab87129..465fd31f 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@cosmos-client/core": "0.45.13", "@cosmos-client/cosmwasm": "^0.20.1", "@cosmos-client/ibc": "^1.2.1", - "@neutron-org/neutronjsplus": "^0.0.9", + "@neutron-org/neutronjsplus": "^0.0.15", "@types/lodash": "^4.14.182", "@types/long": "^4.0.2", "axios": "^0.27.2", @@ -86,4 +86,4 @@ "engines": { "node": ">=11.0 <17" } -} \ No newline at end of file +} diff --git a/src/testcases/parallel/overrule.test.ts b/src/testcases/parallel/overrule.test.ts index 2f2fa841..1312eebf 100644 --- a/src/testcases/parallel/overrule.test.ts +++ b/src/testcases/parallel/overrule.test.ts @@ -54,6 +54,7 @@ describe('Neutron / Subdao', () => { daoContracts.core.address, daoContracts.proposals.overrule?.pre_propose?.address || '', neutronAccount1.wallet.address.toString(), + false, // do not close proposal on failure since otherwise we wont get an error exception from submsgs ); subdaoMember1 = new dao.DaoMember(neutronAccount1, subDao); diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index 925e7619..b640c2dd 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -9,7 +9,9 @@ import { TestStateLocalCosmosTestNet, types, wait, + proposal, } from '@neutron-org/neutronjsplus'; +import Long from 'long'; const config = require('../../config.json'); @@ -113,7 +115,7 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.msgs).toHaveLength(1); }); - test('execute timelocked: nonexistant ', async () => { + test('execute timelocked: nonexistant', async () => { await expect( subdaoMember1.executeTimelockedProposal(1_000_000), ).rejects.toThrow(/SingleChoiceProposal not found/); @@ -135,6 +137,9 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.id).toEqual(proposalId); expect(timelockedProp.status).toEqual('execution_failed'); expect(timelockedProp.msgs).toHaveLength(1); + + const error = await subDao.getTimelockedProposalError(proposalId); + expect(error).toEqual('codespace: sdk, code: 5'); // 'insufficient funds' error }); test('execute timelocked(ExecutionFailed): WrongStatus error', async () => { @@ -148,6 +153,173 @@ describe('Neutron / Subdao', () => { overruleTimelockedProposalMock(subdaoMember1, proposalId), ).rejects.toThrow(/Wrong proposal status \(execution_failed\)/); }); + + let proposalId2: number; + test('proposal timelock 2 with two messages, one of them fails', async () => { + // pack two messages in one proposal + const failMessage = proposal.paramChangeProposal({ + title: 'paramchange', + description: 'paramchange', + subspace: 'icahost', + key: 'HostEnabled', + value: '123123123', // expected boolean, provided number + }); + const goodMessage = proposal.sendProposal({ + to: neutronAccount2.wallet.address.toString(), + denom: NEUTRON_DENOM, + amount: '100', + }); + const fee = { + gas_limit: Long.fromString('4000000'), + amount: [{ denom: NEUTRON_DENOM, amount: '10000' }], + }; + proposalId2 = await subdaoMember1.submitSingleChoiceProposal( + 'proposal2', + 'proposal2', + [goodMessage, failMessage], + '1000', + 'single', + fee, + ); + + const timelockedProp = await subdaoMember1.supportAndExecuteProposal( + proposalId2, + ); + + expect(timelockedProp.id).toEqual(proposalId2); + expect(timelockedProp.status).toEqual('timelocked'); + expect(timelockedProp.msgs).toHaveLength(1); + }); + + test('execute timelocked 2: execution failed', async () => { + await neutronAccount1.msgSend(subDao.contracts.core.address, '100000'); // fund the subdao treasury + const balance2 = await neutronAccount2.queryDenomBalance(NEUTRON_DENOM); + + //wait for timelock durations + await wait.waitSeconds(20); + // timelocked proposal execution failed due to invalid param value + await subdaoMember1.executeTimelockedProposal(proposalId2); + const timelockedProp = await subDao.getTimelockedProposal(proposalId2); + expect(timelockedProp.id).toEqual(proposalId2); + expect(timelockedProp.status).toEqual('execution_failed'); + expect(timelockedProp.msgs).toHaveLength(1); + + const error = await subDao.getTimelockedProposalError(proposalId2); + expect(error).toEqual('codespace: undefined, code: 1'); + + // check that goodMessage failed as well + const balance2After = await neutronAccount2.queryDenomBalance( + NEUTRON_DENOM, + ); + expect(balance2After).toEqual(balance2); + + // cannot execute failed proposal with closeOnProposalExecutionFailed=true + await expect( + subdaoMember1.executeTimelockedProposal(proposalId2), + ).rejects.toThrow(/Wrong proposal status \(execution_failed\)/); + await neutronChain.blockWaiter.waitBlocks(2); + }); + + test('change subdao proposal config with closeOnProposalExecutionFailed = false', async () => { + const subdaoConfig = + await neutronChain.queryContract( + subDao.contracts.proposals.single.address, + { + config: {}, + }, + ); + expect(subdaoConfig.close_proposal_on_execution_failure).toEqual(true); + subdaoConfig.close_proposal_on_execution_failure = false; + + const proposalId = await subdaoMember1.submitUpdateConfigProposal( + 'updateconfig', + 'updateconfig', + subdaoConfig, + '1000', + ); + const timelockedProp = await subdaoMember1.supportAndExecuteProposal( + proposalId, + ); + expect(timelockedProp.status).toEqual('timelocked'); + //wait for timelock durations + await wait.waitSeconds(20); + await subdaoMember1.executeTimelockedProposal(proposalId); // should execute no problem + + await neutronChain.blockWaiter.waitBlocks(2); + + const subdaoConfigAfter = + await neutronChain.queryContract( + subDao.contracts.proposals.single.address, + { + config: {}, + }, + ); + expect(subdaoConfigAfter.close_proposal_on_execution_failure).toEqual( + false, + ); + }); + + let proposalId3: number; + test('proposal timelock 3 with not enough funds initially to resubmit later', async () => { + proposalId3 = await subdaoMember1.submitSendProposal('send', 'send', [ + { + recipient: demo2Addr.toString(), + amount: 200000, + denom: neutronChain.denom, + }, + ]); + + const timelockedProp = await subdaoMember1.supportAndExecuteProposal( + proposalId3, + ); + + expect(timelockedProp.id).toEqual(proposalId3); + expect(timelockedProp.status).toEqual('timelocked'); + expect(timelockedProp.msgs).toHaveLength(1); + }); + + test('execute timelocked 3: execution failed at first and then successful after funds sent', async () => { + const subdaoConfig = + await neutronChain.queryContract( + subDao.contracts.proposals.single.address, + { + config: {}, + }, + ); + expect(subdaoConfig.close_proposal_on_execution_failure).toEqual(false); + + //wait for timelock durations + await wait.waitSeconds(20); + // timelocked proposal execution failed due to insufficient funds + await expect( + subdaoMember1.executeTimelockedProposal(proposalId3), + ).rejects.toThrow(/insufficient funds/); + const timelockedProp = await subDao.getTimelockedProposal(proposalId3); + expect(timelockedProp.id).toEqual(proposalId3); + expect(timelockedProp.status).toEqual('timelocked'); + expect(timelockedProp.msgs).toHaveLength(1); + + const error = await subDao.getTimelockedProposalError(proposalId3); + // do not have an error because we did not have reply + expect(error).toEqual(null); + + await neutronAccount1.msgSend(subDao.contracts.core.address, '300000'); + + // now that we have funds should execute without problems + + const balanceBefore = await neutronChain.queryDenomBalance( + demo2Addr.toString(), + NEUTRON_DENOM, + ); + await subdaoMember1.executeTimelockedProposal(proposalId3); + await neutronChain.blockWaiter.waitBlocks(2); + const balanceAfter = await neutronChain.queryDenomBalance( + demo2Addr.toString(), + NEUTRON_DENOM, + ); + + expect(balanceAfter - balanceBefore).toEqual(200000); + }); }); describe('Timelock: Succeed execution', () => { @@ -832,10 +1004,12 @@ describe('Neutron / Subdao', () => { await subdaoMember1.supportAndExecuteProposal(proposalId); await wait.waitSeconds(20); - await subdaoMember1.executeTimelockedProposal(proposalId); + await expect( + subdaoMember1.executeTimelockedProposal(proposalId), + ).rejects.toThrow(/config name cannot be empty/); const timelockedProp = await subDao.getTimelockedProposal(proposalId); expect(timelockedProp.id).toEqual(proposalId); - expect(timelockedProp.status).toEqual('execution_failed'); + expect(timelockedProp.status).toEqual('timelocked'); expect(timelockedProp.msgs).toHaveLength(1); const configAfter = await neutronChain.queryContract( subDao.contracts.core.address, diff --git a/yarn.lock b/yarn.lock index 335e4d65..91985c67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1591,15 +1591,16 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@neutron-org/neutronjsplus@^0.0.9": - version "0.0.9" - resolved "https://registry.yarnpkg.com/@neutron-org/neutronjsplus/-/neutronjsplus-0.0.9.tgz#d4aea3ddc828438b22cd60e0f5746063f957614d" - integrity sha512-yAzfPiwgE9500lbD02zhQXGbgzwl9OkMWC58y1oMKav0LIHLN9In8a9t4Wg6QGFg0eGxW0teQZrXudGNufW4qQ== +"@neutron-org/neutronjsplus@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@neutron-org/neutronjsplus/-/neutronjsplus-0.0.15.tgz#0bcebb8138fc380bed1210474ec9189340ec874d" + integrity sha512-kF82bsu74QrXPZya7rS8/+Ziu+sht9nvjKTfKBIsExVr2U+VePA8D/CJkqx7ts4yi1D7ECATgGFCA8GJW8I4VQ== dependencies: "@cosmos-client/core" "0.45.13" "@cosmos-client/cosmwasm" "^0.20.1" "@cosmos-client/ibc" "^1.2.1" axios "^0.27.2" + long "^5.2.1" merkletreejs "^0.3.9" "@nodelib/fs.scandir@2.1.5":