diff --git a/package.json b/package.json index 798c685..12f84a4 100644 --- a/package.json +++ b/package.json @@ -51,20 +51,27 @@ }, "devDependencies": { "@openeth/truffle-typings": "0.0.6", - "@types/chai": "^4.3.1", "@types/eth-sig-util": "2.1.0", "@types/mocha": "^9.1.1", + "@types/sinon": "^10.0.13", + "@types/sinon-chai": "^3.2.8", + "@types/chai": "^4.3.1", + "@types/chai-as-promised": "^7.1.5", "@types/node": "~13.13.4", "@types/semver": "~7.3.4", "@typescript-eslint/eslint-plugin": "~4.28.3", "@typescript-eslint/parser": "~4.28.3", "chai": "^4.3.6", + "chai-as-promised": "^7.1.1", "eslint": "~7.30.0", "husky": "~6.0.0", "mocha": "^10.0.0", "prettier": "2.3.0", + "sinon": "^14.0.0", + "sinon-chai": "^3.7.0", "ts-loader": "~9.2.3", "ts-node": "8.6.2", + "ts-sinon": "^2.0.2", "typescript": "4.3.5", "webpack": "~5.43.0", "webpack-cli": "~4.7.2", diff --git a/src/ContractInteractor.ts b/src/ContractInteractor.ts index 34545e1..aa29c3c 100644 --- a/src/ContractInteractor.ts +++ b/src/ContractInteractor.ts @@ -877,6 +877,17 @@ export default class ContractInteractor { `No receipt found for this transaction ${transactionHash}` ); } + + async verifyForwarder( + suffixData: string, + request: RelayRequest, + signature: string + ): Promise { + const forwarder = await this._createForwarder( + request.relayData.callForwarder + ); + await forwarder.verify(suffixData, request.request, signature); + } } /** diff --git a/test/ContractInteractor.test.ts b/test/ContractInteractor.test.ts new file mode 100644 index 0000000..0ad1f52 --- /dev/null +++ b/test/ContractInteractor.test.ts @@ -0,0 +1,199 @@ +import sinon, { stubInterface } from 'ts-sinon'; +import { expect, use, assert } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import sinonChai from 'sinon-chai'; +import { IForwarderInstance } from '@rsksmart/rif-relay-contracts/types/truffle-contracts'; +import { + constants, + ContractInteractor, + EnvelopingConfig, + Web3Provider +} from '../src'; +import { + ForwardRequest, + RelayData, + RelayRequest +} from '@rsksmart/rif-relay-contracts'; + +use(sinonChai); +use(chaiAsPromised); + +const GAS_PRICE_PERCENT = 0; // +const MAX_RELAY_NONCE_GAP = 3; +const DEFAULT_RELAY_TIMEOUT_GRACE_SEC = 1800; +const DEFAULT_LOOKUP_WINDOW_BLOCKS = 60000; +const DEFAULT_CHAIN_ID = 33; + +describe('ContractInteractor', () => { + const defaultConfig: EnvelopingConfig = { + preferredRelays: [], + onlyPreferredRelays: false, + relayLookupWindowParts: 1, + relayLookupWindowBlocks: DEFAULT_LOOKUP_WINDOW_BLOCKS, + gasPriceFactorPercent: GAS_PRICE_PERCENT, + minGasPrice: 60000000, // 0.06 GWei + maxRelayNonceGap: MAX_RELAY_NONCE_GAP, + sliceSize: 3, + relayTimeoutGrace: DEFAULT_RELAY_TIMEOUT_GRACE_SEC, + methodSuffix: '', + jsonStringifyRequest: false, + chainId: DEFAULT_CHAIN_ID, + relayHubAddress: constants.ZERO_ADDRESS, + deployVerifierAddress: constants.ZERO_ADDRESS, + relayVerifierAddress: constants.ZERO_ADDRESS, + forwarderAddress: constants.ZERO_ADDRESS, + smartWalletFactoryAddress: constants.ZERO_ADDRESS, + logLevel: 0, + clientId: '1' + }; + let mockWeb3Provider: Web3Provider; + let contractInteractor: ContractInteractor; + + before(() => { + mockWeb3Provider = stubInterface(); + contractInteractor = new ContractInteractor( + mockWeb3Provider, + defaultConfig + ); + }); + + describe('verifyForwarder', () => { + let _createForwarderStub: sinon.SinonStub; + let fakeIForwarderInstance: sinon.SinonStubbedInstance & + IForwarderInstance; + const fakeSuffixData = 'fakeSuffix'; + const fakeRelayRequest: RelayRequest = { + request: { + to: 'fake_address', + data: 'fake_data', + gas: '1' + } as ForwardRequest, + relayData: { + gasPrice: '0', + callForwarder: 'fake_address' + } as RelayData + }; + const fakeSignature = 'fake_signature'; + + before(() => { + fakeIForwarderInstance = stubInterface(); + _createForwarderStub = sinon + .stub(contractInteractor, '_createForwarder') + .callsFake(() => Promise.resolve(fakeIForwarderInstance)); + }); + + it('should verify EOA and call once _createForwarder', async () => { + await expect( + contractInteractor.verifyForwarder( + fakeSuffixData, + fakeRelayRequest, + fakeSignature + ) + ).to.eventually.be.undefined; + expect(contractInteractor._createForwarder).to.have.been.calledOnce; + }); + + it('should fail if EOA is not the owner', async () => { + const error = new TypeError( + 'VM Exception while processing transaction: revert Not the owner of the SmartWallet' + ); + fakeIForwarderInstance.verify.throwsException(error); + await assert.isRejected( + contractInteractor.verifyForwarder( + fakeSuffixData, + fakeRelayRequest, + fakeSignature + ), + error.message + ); + }); + + it('should fail if nonce mismatch', async () => { + const error = new TypeError( + 'VM Exception while processing transaction: revert nonce mismatch' + ); + fakeIForwarderInstance.verify.throwsException(error); + await assert.isRejected( + contractInteractor.verifyForwarder( + fakeSuffixData, + fakeRelayRequest, + fakeSignature + ), + error.message + ); + }); + + it('should fail if signature mismatch', async () => { + const error = new TypeError( + 'VM Exception while processing transaction: revert Signature mismatch' + ); + fakeIForwarderInstance.verify.throwsException(error); + await assert.isRejected( + contractInteractor.verifyForwarder( + fakeSuffixData, + fakeRelayRequest, + fakeSignature + ), + error.message + ); + }); + + it('should fail if suffixData is null', async () => { + const error = new TypeError( + "Cannot read properties of null (reading 'substring')" + ); + fakeIForwarderInstance.verify.throwsException(error); + await assert.isRejected( + contractInteractor.verifyForwarder( + null, + fakeRelayRequest, + fakeSignature + ), + error.message + ); + }); + + it('should fail if RelayRequest is null', async () => { + const error = new TypeError( + "Cannot read properties of null (reading 'relayData')" + ); + fakeIForwarderInstance.verify.throwsException(error); + await assert.isRejected( + contractInteractor.verifyForwarder( + fakeSuffixData, + null, + fakeSignature + ), + error.message + ); + }); + + it('should fail if Signature is null', async () => { + const error = new TypeError( + "Cannot read properties of null (reading 'length')" + ); + fakeIForwarderInstance.verify.throwsException(error); + await assert.isRejected( + contractInteractor.verifyForwarder( + fakeSuffixData, + fakeRelayRequest, + null + ), + error.message + ); + }); + + it('should fail if callForwarder is null', async () => { + _createForwarderStub.restore(); + fakeRelayRequest.relayData.callForwarder = null; + await assert.isRejected( + contractInteractor.verifyForwarder( + fakeSuffixData, + fakeRelayRequest, + fakeSignature + ), + 'Invalid address passed to IForwarder.at(): null' + ); + }); + }); +});