├── .circleci └── config.yml ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODEOWNERS └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .gitpod.yml ├── .idea └── dictionaries │ └── alexf.xml ├── README.md ├── audits ├── Least-Authority-Ethereum-Foundation-GSN-Final-Audit-Report-v2.pdf └── opengsn-audit-march2021_front-page-summaries.pdf ├── dockers ├── config-sample │ └── gsn-relay-config.json ├── docker-compose.yml └── jsrelay │ ├── .gitignore │ ├── Dockerfile │ ├── config │ └── gsn-relay-config.json │ ├── dbuild.sh │ └── webpack.config.js ├── hardhat.config.ts ├── lerna.json ├── logo.svg ├── package.json ├── packages ├── cli │ ├── .depcheckrc │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── CommandsLogic.ts │ │ ├── ForwarderUtil.ts │ │ ├── GsnTestEnvironment.ts │ │ ├── commands │ │ │ ├── gsn-deploy.ts │ │ │ ├── gsn-paymaster-balance.ts │ │ │ ├── gsn-paymaster-fund.ts │ │ │ ├── gsn-relayer-register.ts │ │ │ ├── gsn-relayer-run.ts │ │ │ ├── gsn-relayer-withdraw.ts │ │ │ ├── gsn-send-request.ts │ │ │ ├── gsn-start.ts │ │ │ └── gsn.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── tsconfig.packages.json ├── common │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── AmountRequired.ts │ │ ├── ConfigResponse.ts │ │ ├── Constants.ts │ │ ├── ContractInteractor.ts │ │ ├── EIP712 │ │ │ ├── ForwardRequest.ts │ │ │ ├── RelayData.ts │ │ │ ├── RelayRequest.ts │ │ │ └── TypedRequestData.ts │ │ ├── ErrorReplacerJSON.ts │ │ ├── GSNContractsDeployment.ts │ │ ├── HttpClient.ts │ │ ├── HttpWrapper.ts │ │ ├── LoggerInterface.ts │ │ ├── PingResponse.ts │ │ ├── RelayCallGasLimitCalculationHelper.ts │ │ ├── StatsResponse.ts │ │ ├── Utils.ts │ │ ├── Version.ts │ │ ├── VersionsManager.ts │ │ ├── dev │ │ │ ├── NetworkSimulatingProvider.ts │ │ │ ├── ProfilingProvider.ts │ │ │ └── WrapperProviderBase.ts │ │ ├── environments │ │ │ ├── AsyncZeroAddressCalldataGasEstimation.ts │ │ │ ├── Environments.ts │ │ │ ├── MainnetCalldataGasEstimation.ts │ │ │ └── OfficialPaymasterDeployments.ts │ │ ├── ethereumjstx │ │ │ └── TxOptions.ts │ │ ├── index.ts │ │ ├── types │ │ │ ├── Aliases.ts │ │ │ ├── AuditRequest.ts │ │ │ ├── GSNContractsDataTypes.ts │ │ │ ├── GSNStatistics.ts │ │ │ ├── GsnTransactionDetails.ts │ │ │ ├── PaymasterConfiguration.ts │ │ │ ├── PenalizerConfiguration.ts │ │ │ ├── RelayFailureInfo.ts │ │ │ ├── RelayHubConfiguration.ts │ │ │ ├── RelayInfo.ts │ │ │ ├── RelayTransactionRequest.ts │ │ │ └── TransactionType.ts │ │ └── web3js │ │ │ ├── FeeHistoryResult.ts │ │ │ ├── JsonRpcPayload.ts │ │ │ ├── JsonRpcResponse.ts │ │ │ ├── RLPEncodedTransaction.ts │ │ │ └── Web3JSUtils.ts │ ├── tsconfig.json │ └── tsconfig.packages.json ├── contracts │ ├── .depcheckrc │ ├── .solhint.json │ ├── LICENSE │ ├── README.md │ ├── contract.hbs │ ├── package.json │ ├── src │ │ ├── BasePaymaster.sol │ │ ├── ERC2771Recipient.sol │ │ ├── Migrations.sol │ │ ├── Penalizer.sol │ │ ├── RelayHub.sol │ │ ├── StakeManager.sol │ │ ├── arbitrum │ │ │ ├── ArbRelayHub.sol │ │ │ └── ArbSys.sol │ │ ├── forwarder │ │ │ ├── Forwarder.sol │ │ │ ├── IForwarder.sol │ │ │ └── test │ │ │ │ ├── TestForwarder.sol │ │ │ │ └── TestForwarderTarget.sol │ │ ├── interfaces │ │ │ ├── IERC20Token.sol │ │ │ ├── IERC2771Recipient.sol │ │ │ ├── IPaymaster.sol │ │ │ ├── IPenalizer.sol │ │ │ ├── IRelayHub.sol │ │ │ ├── IRelayRegistrar.sol │ │ │ └── IStakeManager.sol │ │ ├── test │ │ │ ├── PayableWithEmit.sol │ │ │ ├── TestArbSys.sol │ │ │ ├── TestDecimalsToken.sol │ │ │ ├── TestGatewayForwarder.sol │ │ │ ├── TestPaymasterConfigurableMisbehavior.sol │ │ │ ├── TestPaymasterEverythingAccepted.sol │ │ │ ├── TestPaymasterOwnerSignature.sol │ │ │ ├── TestPaymasterPreconfiguredApproval.sol │ │ │ ├── TestPaymasterStoreContext.sol │ │ │ ├── TestPaymasterVariableGasLimits.sol │ │ │ ├── TestRecipient.sol │ │ │ ├── TestRecipientWithoutFallback.sol │ │ │ ├── TestRelayHub.sol │ │ │ ├── TestRelayHubForRegistrar.sol │ │ │ ├── TestRelayHubValidator.sol │ │ │ ├── TestRelayWorkerContract.sol │ │ │ ├── TestToken.sol │ │ │ ├── TestUtil.sol │ │ │ └── TestWrappedNativeToken.sol │ │ └── utils │ │ │ ├── GsnEip712Library.sol │ │ │ ├── GsnTypes.sol │ │ │ ├── GsnUtils.sol │ │ │ ├── MinLibBytes.sol │ │ │ ├── RLPReader.sol │ │ │ ├── RelayHubValidator.sol │ │ │ └── RelayRegistrar.sol │ ├── truffle.js │ └── tsconfig.json ├── deployer │ ├── deploy │ │ └── deploy.ts │ ├── hardhat.config.ts │ ├── package.json │ ├── src │ │ ├── applyConfig.ts │ │ ├── depinfo.ts │ │ ├── deployUtils.ts │ │ └── exportTask.ts │ ├── test │ │ └── deploy.test.ts │ ├── tsconfig.json │ └── tsconfig.packages.json ├── dev │ ├── .depcheckrc │ ├── LICENSE │ ├── README.md │ ├── migrations │ │ ├── 1_initial_migration.js │ │ └── 2_deploy_contracts.js │ ├── package.json │ ├── replaceWeb3.sh │ ├── src │ │ └── index.ts │ ├── test │ │ ├── AbandonedRelayFlow.test.ts │ │ ├── Flows.test.ts │ │ ├── GasPriceFetcher.test.ts │ │ ├── HttpWrapper.test.ts │ │ ├── KeyManager.test.ts │ │ ├── NetworkSimultaion.test.ts │ │ ├── PaymasterCommitment.test.ts │ │ ├── RegistrationManager.test.ts │ │ ├── RelayHub.test.ts │ │ ├── RelayHubConfiguration.test.ts │ │ ├── RelayHubGasCalculations.test.ts │ │ ├── RelayHubPenalizations.test.ts │ │ ├── RelayHubRegistrationsManagement.test.ts │ │ ├── RelayHubValidator.test.ts │ │ ├── RelayServer.test.ts │ │ ├── RelayServer.webpack.test.ts │ │ ├── RelayServerRequestsProfiling.test.ts │ │ ├── ReputationFlow.test.ts │ │ ├── ReputationManager.test.ts │ │ ├── SampleRecipient.test.ts │ │ ├── ServerConfigParams.test.ts │ │ ├── ServerTestEnvironment.ts │ │ ├── ServerTestUtils.ts │ │ ├── StakeManager.test.ts │ │ ├── StoredTransaction.test.ts │ │ ├── TestUtils.ts │ │ ├── TransactionManager.test.ts │ │ ├── TxStoreManager.test.ts │ │ ├── arbitrum │ │ │ └── ArbRelayHub.test.ts │ │ ├── common │ │ │ ├── ContractInteractor.test.ts │ │ │ ├── TestCliUtils.ts │ │ │ ├── Utils.test.ts │ │ │ └── VersionManager.test.ts │ │ ├── dummies │ │ │ ├── BadContractInteractor.ts │ │ │ ├── BadHttpClient.ts │ │ │ ├── BadRelayClient.ts │ │ │ └── BadRelayedTransactionValidator.ts │ │ ├── forwarder │ │ │ ├── BaseRelayRecipient.test.ts │ │ │ └── Forwarder.test.ts │ │ ├── penalizer │ │ │ ├── EtherscanCachedService.test.ts │ │ │ ├── MockTxByNonceService.ts │ │ │ ├── PenalizationFlow.test.ts │ │ │ └── PenalizerService.test.ts │ │ ├── provider │ │ │ ├── AccountManager.test.ts │ │ │ ├── GsnTestEnvironment.test.ts │ │ │ ├── KnownRelaysManager.test.ts │ │ │ ├── RelayClient.test.ts │ │ │ ├── RelayClientWrapProviders.test.ts │ │ │ ├── RelayProvider.test.ts │ │ │ ├── RelaySelectionManager.test.ts │ │ │ └── RevertMessage.test.ts │ │ ├── regressions │ │ │ └── PayableWithEmit.test.ts │ │ ├── runServer.test.ts │ │ ├── server-config.json │ │ └── utils │ │ │ ├── ERC20BalanceTracker.ts │ │ │ ├── RelayRegistrar.test.ts │ │ │ └── chaiHelper.ts │ ├── truffle.js │ ├── tsconfig.json │ ├── tsconfig.packages.json │ └── types │ │ └── Web3Types.d.ts ├── logger │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── ClientWinstonLogger.ts │ │ ├── CommandsWinstonLogger.ts │ │ └── ServerWinstonLogger.ts │ └── tsconfig.json ├── paymasters │ ├── .depcheckrc │ ├── .solhint.json │ ├── LICENSE │ ├── README.md │ ├── contracts │ │ ├── AcceptEverythingPaymaster.sol │ │ ├── HashcashPaymaster.sol │ │ ├── PermitERC20UniswapV3Paymaster.sol │ │ ├── SingleRecipientPaymaster.sol │ │ ├── SingletonWhitelistPaymaster.sol │ │ ├── TokenPaymaster.sol │ │ ├── VerifyingPaymaster.sol │ │ ├── WhitelistPaymaster.sol │ │ ├── helpers │ │ │ ├── AllEvents.sol │ │ │ ├── ImportsArtifacts.sol │ │ │ ├── SampleRecipient.sol │ │ │ ├── TestCounter.sol │ │ │ ├── TestHub.sol │ │ │ ├── TestProxy.sol │ │ │ ├── TestUniswap.sol │ │ │ ├── TokenGasCalculator.sol │ │ │ └── UniswapV3Helper.sol │ │ └── interfaces │ │ │ ├── IChainlinkOracle.sol │ │ │ ├── IERC725.sol │ │ │ ├── IUniswapV3.sol │ │ │ ├── PermitInterfaceDAI.sol │ │ │ └── PermitInterfaceEIP2612.sol │ ├── deploy │ │ └── deploy.ts │ ├── deployments │ │ ├── deployment-config.ts │ │ └── goerli │ │ │ ├── .chainId │ │ │ ├── PermitERC20UniswapV3Paymaster.json │ │ │ ├── SingletonWhitelistPaymaster.json │ │ │ └── solcInputs │ │ │ ├── 177475e7e223be098190c0c9b442c845.json │ │ │ └── 7787bbb1814de08c189c076febcd7380.json │ ├── hardhat.config.ts │ ├── package.json │ ├── src │ │ ├── HashCashApproval.ts │ │ ├── PermitPaymasterUtils.ts │ │ ├── TokenPaymasterInteractor.ts │ │ ├── TokenPaymasterProvider.ts │ │ ├── VerifyingPaymasterUtils.ts │ │ ├── WrapContract.ts │ │ ├── constants │ │ │ └── MainnetPermitERC20UniswapV3PaymasterConstants.ts │ │ └── index.ts │ ├── test │ │ ├── ForkTestUtils.ts │ │ ├── HashcashPaymaster.test.ts │ │ ├── PermitERC20UniswapV3Paymaster.test.ts │ │ ├── SingletonWhitelistPaymaster.test.ts │ │ ├── TestUniswap.test.ts │ │ ├── TestUtils.ts │ │ ├── TokenPaymaster.test.ts │ │ ├── TokenPaymasterProvider.test.ts │ │ ├── VerifyingPaymaster.test.ts │ │ └── WhitelistPaymaster.test.ts │ ├── truffle.js │ ├── tsconfig.json │ └── tsconfig.packages.json ├── provider │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── AccountManager.ts │ │ ├── ConnectContractToGSN.ts │ │ ├── GSNConfigurator.ts │ │ ├── GsnEvents.ts │ │ ├── KnownRelaysManager.ts │ │ ├── RelayClient.ts │ │ ├── RelayProvider.ts │ │ ├── RelaySelectionManager.ts │ │ ├── RelayedTransactionValidator.ts │ │ ├── VerifierUtils.ts │ │ └── index.ts │ ├── tsconfig.eslint.json │ └── tsconfig.json └── relay │ ├── .depcheckrc │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ ├── GasPriceFetcher.ts │ ├── HttpServer.ts │ ├── KeyManager.ts │ ├── RegistrationManager.ts │ ├── RelayServer.ts │ ├── ReputationEntry.ts │ ├── ReputationManager.ts │ ├── ReputationStoreManager.ts │ ├── ServerConfigParams.ts │ ├── StoredTransaction.ts │ ├── TransactionManager.ts │ ├── TxStoreManager.ts │ ├── Utils.ts │ ├── Web3MethodsBuilder.ts │ ├── penalizer │ │ ├── BlockExplorerInterface.ts │ │ ├── EtherscanCachedService.ts │ │ ├── PenalizerService.ts │ │ ├── PenalizerUtils.ts │ │ └── TransactionDataCache.ts │ └── runServer.ts │ └── tsconfig.json ├── scripts ├── extract_abi.js └── solpp.js ├── tsconfig.json ├── tsconfig.packages.json ├── types ├── chai-bn │ └── index.d.ts ├── date-format │ └── index.d.ts └── openzeppelin__test-helpers │ └── index.d.ts ├── verdaccio ├── README.md ├── verdaccio.yaml ├── yarn └── yarnrc └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | jest: true, 6 | mocha: true, 7 | node: true 8 | }, 9 | globals: { 10 | artifacts: false, 11 | assert: false, 12 | contract: false, 13 | web3: false 14 | }, 15 | extends: 16 | [ 17 | 'standard-with-typescript' 18 | ], 19 | // This is needed to add configuration to rules with type information 20 | parser: '@typescript-eslint/parser', 21 | plugins: ["@typescript-eslint"], 22 | parserOptions: { 23 | // The 'tsconfig.packages.json' is needed to add not-compiled files to the project 24 | project: ['./tsconfig.json', './tsconfig.packages.json'] 25 | }, 26 | ignorePatterns: [ 27 | '**/types/truffle-contracts', 28 | '**/types/ethers-contracts', 29 | 'dist/' 30 | ], 31 | rules: { 32 | 'no-console': 'off', 33 | '@typescript-eslint/no-var-requires': 'off', 34 | '@typescript-eslint/require-array-sort-compare': ['error', 35 | { 36 | ignoreStringArrays: true 37 | } 38 | ] 39 | }, 40 | overrides: [ 41 | { 42 | files: [ 43 | '**/test/**/*.ts' 44 | ], 45 | rules: { 46 | 'no-unused-expressions': 'off', 47 | // chai assertions trigger this rule 48 | '@typescript-eslint/no-unused-expressions': 'off', 49 | '@typescript-eslint/no-non-null-assertion': 'off' 50 | } 51 | }, 52 | { 53 | // otherwise it will raise an error in every JavaScript file 54 | files: ['*.ts'], 55 | rules: { 56 | '@typescript-eslint/ban-ts-comment': 'off', 57 | '@typescript-eslint/prefer-ts-expect-error': 'off', 58 | // allow using '${val}' with numbers, bool and null types 59 | '@typescript-eslint/restrict-template-expressions': [ 60 | 'error', 61 | { 62 | allowNumber: true, 63 | allowBoolean: true, 64 | allowNullish: true 65 | } 66 | ] 67 | } 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.sol ident 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | #https://help.github.com/en/articles/about-code-owners 2 | 3 | * @openeth-dev/dev 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Steps To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Console Logs and/or Screenshots** 24 | If applicable, add outputs of the console and/or screenshots to help explain your problem. 25 | 26 | ** Link to your project's repository ** 27 | In case your repository is open-source and there is already a commit with the code you are having an issue with, it may be helpful to see it to understand what went wrong. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/dev/build/ 2 | .DS_Store 3 | node_modules 4 | /.vscode/settings.json 5 | /.idea/* 6 | !/.idea/dictionaries/ 7 | /secret_mnemonic 8 | /.0x-artifacts/ 9 | /webtools/openeth-webtools.pack.js 10 | /.eslintcache 11 | /packages/common/src/interfaces/ 12 | /packages/paymasters/src/interfaces/ 13 | /ganache.pid 14 | /dist/ 15 | cache 16 | /packages/cli/src/compiled/ 17 | /opengsn-gsn-*.tgz 18 | /RelayClient.ts 19 | /RelayProvider.ts 20 | /GSNConfigurator.ts 21 | /GsnTestEnvironment.ts 22 | artifacts 23 | /yarn-error.log 24 | /jsrelay/gsndata/ 25 | /jsrelay/dist/relayserver.js 26 | /json-input.json 27 | /combined.log 28 | **/dist 29 | tsconfig.tsbuildinfo 30 | /packages/dev/json-input.json 31 | yarn-error.log 32 | /lerna-debug.log 33 | /packages/contracts/build/ 34 | /packages/contracts/json-input.json 35 | /packages/contracts/types/ethers-contracts/ 36 | /packages/contracts/types/truffle-contracts/ 37 | /verdaccio/db/ 38 | /.yarnrc 39 | /build/gsn 40 | /gsn.iml 41 | /packages/paymasters/types/ 42 | /packages/paymasters/build/ 43 | /packages/paymasters/dist/ 44 | /packages/paymasters/json-input.json 45 | hardhat.log 46 | /packages/contracts/solpp/ 47 | /packages/contracts/docs/ 48 | /packages/deployer/contracts-link 49 | /packages/deployer/all-networks.json 50 | /packages/deployer/deployments/ 51 | /Project.xml 52 | /packages/dev/types/truffle-contracts/ 53 | 54 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | ports: 2 | - port: 8545 3 | visibility: public 4 | 5 | tasks: 6 | - name: setup 7 | init: | 8 | yarn 9 | yarn preprocess 10 | -------------------------------------------------------------------------------- /audits/Least-Authority-Ethereum-Foundation-GSN-Final-Audit-Report-v2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengsn/gsn/da4222b76e3ae1968608dc5c5d80074dcac7c4be/audits/Least-Authority-Ethereum-Foundation-GSN-Final-Audit-Report-v2.pdf -------------------------------------------------------------------------------- /audits/opengsn-audit-march2021_front-page-summaries.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengsn/gsn/da4222b76e3ae1968608dc5c5d80074dcac7c4be/audits/opengsn-audit-march2021_front-page-summaries.pdf -------------------------------------------------------------------------------- /dockers/config-sample/gsn-relay-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://", 3 | "workdir": "./app/data", 4 | "relayHubAddress": "", 5 | "managerStakeTokenAddress": "", 6 | "ownerAddress": "
", 7 | "gasPriceFactor": 1, 8 | "ethereumNodeUrl": "https://.infura.io/v3/" 9 | } 10 | -------------------------------------------------------------------------------- /dockers/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # You must have the following environment variable set in .env file: 2 | # HOST | my.example.com | Your Relay Server URL exactly as it is accessed by GSN Clients 3 | 4 | version: '2' 5 | 6 | services: 7 | nginx-proxy: 8 | image: nginxproxy/nginx-proxy 9 | container_name: nginx-proxy 10 | restart: unless-stopped 11 | ports: 12 | - '443:443' 13 | - '80:80' 14 | volumes: 15 | - conf:/etc/nginx/conf.d 16 | - vhost:/etc/nginx/vhost.d 17 | - html:/usr/share/nginx/html 18 | - certs:/etc/nginx/certs:ro 19 | - /var/run/docker.sock:/tmp/docker.sock:ro 20 | logging: 21 | driver: "json-file" 22 | options: 23 | max-size: 10m 24 | max-file: "10" 25 | 26 | acme-companion: 27 | image: nginxproxy/acme-companion 28 | container_name: nginx-proxy-acme 29 | restart: unless-stopped 30 | depends_on: 31 | - nginx-proxy 32 | volumes_from: 33 | - nginx-proxy 34 | volumes: 35 | - certs:/etc/nginx/certs:rw 36 | - acme:/etc/acme.sh 37 | - /var/run/docker.sock:/var/run/docker.sock:ro 38 | 39 | gsn: 40 | container_name: gsn 41 | ports: [ '8080:80' ] #bypass https-portal 42 | image: opengsn/jsrelay:3.0.0-beta.3 43 | restart: on-failure 44 | 45 | # /app/data - relay specific folder 46 | # /app/config - read-only config folder 47 | volumes: 48 | - ./config:/app/config:ro 49 | - ./gsndata:/app/data:rw 50 | 51 | environment: 52 | url: https://${HOST}/ 53 | port: 80 54 | workdir: /app/data 55 | config: /app/config/gsn-relay-config.json 56 | LETSENCRYPT_HOST: $HOST 57 | VIRTUAL_HOST: $HOST 58 | VIRTUAL_PATH: / 59 | VIRTUAL_DEST: / 60 | mem_limit: 100M 61 | logging: 62 | driver: "json-file" 63 | options: 64 | max-size: 10m 65 | max-file: "10" 66 | 67 | volumes: 68 | conf: 69 | vhost: 70 | html: 71 | certs: 72 | acme: 73 | -------------------------------------------------------------------------------- /dockers/jsrelay/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | gsndata 3 | -------------------------------------------------------------------------------- /dockers/jsrelay/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13-buster-slim 2 | COPY dist/relayserver.js app/ 3 | CMD node --no-deprecation /app/relayserver.js 4 | -------------------------------------------------------------------------------- /dockers/jsrelay/config/gsn-relay-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseRelayFee": 70, 3 | "pctRelayFee": 0, 4 | "port": 8090, 5 | "relayHubAddress": "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", 6 | "gasPriceFactor": 1.1, 7 | "ethereumNodeUrl": "http://host.docker.internal:8545", 8 | "devMode": false 9 | } 10 | -------------------------------------------------------------------------------- /dockers/jsrelay/dbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | cd `cd \`dirname $0\`;pwd` 3 | 4 | test -z "$VERSION" && VERSION=`node -e "console.log(require('../../packages/common/dist/Version.js').gsnRuntimeVersion)"` 5 | echo version=$VERSION 6 | 7 | IMAGE=opengsn/jsrelay 8 | 9 | #build docker image of relay 10 | #rebuild if there is a newer src file: 11 | find ./dbuild.sh ../../packages/*/src/ -type f -newer dist/relayserver.js 2>&1 | grep . && { 12 | # yarn preprocess 13 | npx webpack 14 | } 15 | 16 | docker build -t $IMAGE . 17 | docker tag $IMAGE $IMAGE:$VERSION 18 | echo "== To publish" 19 | echo " docker push $IMAGE:latest; docker push $IMAGE:$VERSION" 20 | 21 | -------------------------------------------------------------------------------- /dockers/jsrelay/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { IgnorePlugin } = require('webpack') 3 | 4 | module.exports = { 5 | plugins: [ 6 | // new BundleAnalyzerPlugin() 7 | new IgnorePlugin({ resourceRegExp: /electron/ }), 8 | new IgnorePlugin({ resourceRegExp: /^scrypt$/ }) 9 | ], 10 | target: 'node', 11 | entry: '../../packages/relay/dist/runServer.js', 12 | mode: 'development', 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: 'ts-loader', 18 | exclude: /node_modules/ 19 | } 20 | ] 21 | }, 22 | output: { 23 | path: path.resolve(__dirname, 'dist'), 24 | filename: 'relayserver.js' 25 | }, 26 | stats: 'errors-only' 27 | } 28 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type import('hardhat/config').HardhatUserConfig 3 | */ 4 | const chainId = process.env.CHAINID == null ? 1337 : parseInt(process.env.CHAINID) 5 | module.exports = { 6 | solidity: '0.8.7', 7 | networks: { 8 | hardhat: { chainId }, 9 | npmtest: { // used from "npm test". see package.json 10 | url: 'http://127.0.0.1:8544' 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.0.0-beta.10", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true 5 | } 6 | -------------------------------------------------------------------------------- /packages/cli/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["web3-provider-engine", "@types/web3-provider-engine", "ethereum-protocol", "web3-eth-accounts"] 2 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # GSN command-line tools 2 | 3 | [GSN, the Ethereum Gas Station Network](https://opengsn.org/) abstracts away gas to minimize onboarding & UX friction for dapps. 4 | 5 | This module contains command-line tools made to help developers integrate GSN, test their integrations locally, or bring up relay servers on public Ethereum networks. 6 | 7 | [Complete documentation is available here.](https://docs.opengsn.org/javascript-client/gsn-helpers.html) 8 | 9 | Installation: 10 | 11 | `npm install -g @opengsn/cli` 12 | 13 | Usage: 14 | 15 | `gsn start` - deploys GSN contracts and starts a single relay server on local network. 16 | 17 | `gsn deploy` - deploys the singleton RelayHub instance, as well as other required GSN contracts 18 | 19 | `gsn relayer-register` - fund and register the relay server; you need to run it after you start your own relayer on a public network 20 | 21 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/cli", 3 | "version": "3.0.0-beta.10", 4 | "license": "GPL-3.0-only", 5 | "main": "dist/index.js", 6 | "bin": { 7 | "gsn": "dist/commands/gsn.js", 8 | "run-with-gsn": "scripts/run-with-gsn" 9 | }, 10 | "scripts": { 11 | "tsc": "tsc", 12 | "watch-tsc": "tsc -w --preserveWatchOutput", 13 | "lint": "eslint -f unix .", 14 | "lint-fix": "yarn lint --fix", 15 | "chmod-commands": "chmod a+rx ./dist/commands/*.js", 16 | "rm-dist": "rm -rf tsconfig.tsbuildinfo dist src/compiled" 17 | }, 18 | "files": [ 19 | "dist/*", 20 | "scripts/run-with-gsn", 21 | "README.md" 22 | ], 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "dependencies": { 27 | "@ethereumjs/tx": "^3.2.0", 28 | "@ethersproject/bignumber": "^5.7.0", 29 | "@ethersproject/contracts": "^5.7.0", 30 | "@ethersproject/providers": "^5.7.2", 31 | "@opengsn/common": "^3.0.0-beta.10", 32 | "@opengsn/contracts": "^3.0.0-beta.10", 33 | "@opengsn/logger": "^3.0.0-beta.10", 34 | "@opengsn/provider": "^3.0.0-beta.10", 35 | "@opengsn/relay": "^3.0.0-beta.10", 36 | "@truffle/hdwallet-provider": "^2.0.10", 37 | "@types/node": "^13.0.0", 38 | "@types/web3": "1.0.20", 39 | "@types/web3-provider-engine": "^14.0.1", 40 | "commander": "^6.1.0", 41 | "console-read-write": "^0.1.1", 42 | "ethereum-cryptography": "^1.1.2", 43 | "ethereum-protocol": "^1.0.1", 44 | "ethereumjs-util": "^7.1.0", 45 | "ethereumjs-wallet": "^1.0.2", 46 | "ow": "^0.28.1", 47 | "web3": "^1.7.3", 48 | "web3-core": "^1.7.4", 49 | "web3-provider-engine": "^16.0.4" 50 | }, 51 | "devDependencies": { 52 | "web3-eth-accounts": "^1.7.4", 53 | "web3-eth-contract": "^1.7.4", 54 | "web3-utils": "^1.7.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/cli/src/ForwarderUtil.ts: -------------------------------------------------------------------------------- 1 | import { Contract, type CallOverrides } from '@ethersproject/contracts' 2 | import { Web3Provider } from '@ethersproject/providers' 3 | 4 | import { GsnDomainSeparatorType, GsnRequestType, type LoggerInterface } from '@opengsn/common' 5 | import { type IForwarder } from '@opengsn/contracts/types/ethers-contracts' 6 | 7 | interface TruffleContract { 8 | contract: any 9 | } 10 | 11 | // register a forwarder for use with GSN: the request-type and domain separator we're using. 12 | export async function registerForwarderForGsn ( 13 | domainSeparatorName: string, 14 | forwarderIn: IForwarder | Contract | TruffleContract, 15 | logger?: LoggerInterface, 16 | sendOptions: CallOverrides | undefined = undefined 17 | ): Promise { 18 | let forwarder: Contract 19 | if ((forwarderIn as TruffleContract).contract != null) { 20 | const provider = new Web3Provider((forwarderIn as any).contract.currentProvider) 21 | forwarder = new Contract((forwarderIn as any).address, (forwarderIn as any).abi, provider.getSigner()) 22 | } else { 23 | forwarder = forwarderIn as any 24 | } 25 | 26 | logger?.info(`Registering request type ${GsnRequestType.typeName} with suffix: ${GsnRequestType.typeSuffix}`) 27 | const res = await forwarder.registerRequestType( 28 | GsnRequestType.typeName, 29 | GsnRequestType.typeSuffix, 30 | { ...sendOptions } 31 | ) 32 | logger?.debug(`Transaction broadcast: ${res?.hash as string}`) 33 | 34 | logger?.info(`Registering domain separator ${domainSeparatorName} with version: ${GsnDomainSeparatorType.version}`) 35 | await forwarder.registerDomainSeparator(domainSeparatorName, GsnDomainSeparatorType.version, { ...sendOptions }) 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/src/commands/gsn-paymaster-balance.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import { CommandsLogic } from '../CommandsLogic' 3 | import { getNetworkUrl, getPaymasterAddress, getRelayHubAddress, gsnCommander } from '../utils' 4 | import { createCommandsLogger } from '@opengsn/logger/dist/CommandsWinstonLogger' 5 | 6 | const commander = gsnCommander(['h', 'n']) 7 | .option('--paymaster
', 'address of the paymaster contract') 8 | .parse(process.argv); 9 | 10 | (async () => { 11 | const network: string = commander.network 12 | const nodeURL = getNetworkUrl(network) 13 | 14 | const hub = getRelayHubAddress(commander.hub) 15 | const paymaster = getPaymasterAddress(commander.paymaster) 16 | 17 | if (hub == null || paymaster == null) { 18 | throw new Error(`Contracts not found: hub: ${hub} paymaster: ${paymaster} `) 19 | } 20 | const logger = createCommandsLogger(commander.loglevel) 21 | 22 | const logic = new CommandsLogic(nodeURL, logger, { relayHubAddress: hub }) 23 | await logic.init() 24 | const balance = await logic.getPaymasterBalance(paymaster) 25 | console.log(`Account ${paymaster} has a GSN balance of ${Web3.utils.fromWei(balance.toString())} ETH`) 26 | })().catch( 27 | reason => { 28 | console.error(reason) 29 | process.exit(1) 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /packages/cli/src/commands/gsn-paymaster-fund.ts: -------------------------------------------------------------------------------- 1 | import { ether } from '@opengsn/common' 2 | import { CommandsLogic } from '../CommandsLogic' 3 | import { getMnemonic, getNetworkUrl, getPaymasterAddress, getRelayHubAddress, gsnCommander } from '../utils' 4 | import { createCommandsLogger } from '@opengsn/logger/dist/CommandsWinstonLogger' 5 | 6 | const commander = gsnCommander(['n', 'f', 'h', 'm']) 7 | .option('--paymaster
', 8 | 'address of the paymaster contract (defaults to address from build/gsn/Paymaster.json if exists') 9 | .option('--amount ', 'amount of funds to deposit for the paymaster contract, in wei (defaults to 1 Ether)') 10 | .parse(process.argv); 11 | 12 | (async () => { 13 | const network: string = commander.network 14 | const nodeURL = getNetworkUrl(network) 15 | 16 | const hub = getRelayHubAddress(commander.hub) 17 | const paymaster = getPaymasterAddress(commander.paymaster) 18 | 19 | if (hub == null || paymaster == null) { 20 | throw new Error(`Contracts not found: hub: ${hub} paymaster: ${paymaster} `) 21 | } 22 | 23 | const logger = createCommandsLogger(commander.loglevel) 24 | const mnemonic = getMnemonic(commander.mnemonic) 25 | const logic = new CommandsLogic(nodeURL, logger, { relayHubAddress: hub }, mnemonic, commander.derivationPath, commander.derivationIndex, commander.privateKeyHex) 26 | await logic.init() 27 | const from = commander.from ?? await logic.findWealthyAccount() 28 | const amount = commander.amount ?? ether('1') 29 | 30 | const balance = await logic.fundPaymaster(from, paymaster, amount) 31 | console.log(`Paymaster ${paymaster} balance is now ${balance.toString()} wei`) 32 | })().catch( 33 | reason => { 34 | console.error(reason) 35 | process.exit(1) 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /packages/cli/src/commands/gsn-relayer-register.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | 3 | import { ether } from '@opengsn/common' 4 | 5 | import { CommandsLogic, type RegisterOptions } from '../CommandsLogic' 6 | import { getNetworkUrl, gsnCommander, getMnemonic } from '../utils' 7 | import { toWei } from 'web3-utils' 8 | import { createCommandsLogger } from '@opengsn/logger/dist/CommandsWinstonLogger' 9 | 10 | const commander = gsnCommander(['n', 'f', 'm', 'g']) 11 | .option('--relayUrl ', 'url to advertise the relayer', 'http://localhost:8090') 12 | .option('--stake ', 'amount to stake for the relayer, in ETH', '1') 13 | .option( 14 | '--unstakeDelay ', 15 | 'seconds to wait between unregistering and withdrawing the stake', '15000' 16 | ) 17 | .option( 18 | '--funds ', 19 | 'amount to transfer to the relayer to pay for relayed transactions, in ETH', '2' 20 | ) 21 | .option( 22 | '--sleep ', 23 | 'ms to sleep each time if waiting for RelayServer to set its owner', '10000' 24 | ) 25 | .option( 26 | '--sleepCount ', 27 | 'number of times to sleep before timeout', '5' 28 | ) 29 | .option('-t, --token
', 'Token to be used as a stake, defaults to first registered token') 30 | .option('--wrap', 'Assume token is "Wrapped ETH". If its balance is not enough for stake, then deposit ETH into it.') 31 | .parse(process.argv); 32 | 33 | (async () => { 34 | const host = getNetworkUrl(commander.network) 35 | const mnemonic = getMnemonic(commander.mnemonic) 36 | const logger = createCommandsLogger(commander.loglevel) 37 | const logic = await new CommandsLogic(host, logger, { 38 | managerStakeTokenAddress: commander.token 39 | }, mnemonic, commander.derivationPath, commander.derivationIndex, commander.privateKeyHex).init() 40 | const registerOptions: RegisterOptions = { 41 | sleepMs: parseInt(commander.sleep), 42 | sleepCount: parseInt(commander.sleepCount), 43 | from: commander.from ?? await logic.findWealthyAccount(), 44 | token: commander.token, 45 | stake: commander.stake, 46 | wrap: commander.wrap, 47 | funds: ether(commander.funds), 48 | gasPrice: commander.gasPrice != null ? BigNumber.from(toWei(commander.gasPrice, 'gwei')) : undefined, 49 | relayUrl: commander.relayUrl, 50 | unstakeDelay: commander.unstakeDelay 51 | } 52 | if (registerOptions.from == null) { 53 | console.error('Failed to find a wealthy "from" address') 54 | process.exit(1) 55 | } 56 | 57 | const result = await logic.registerRelay(registerOptions) 58 | if (result.success) { 59 | console.log('Relay registered successfully! Transactions:\n', result.transactions) 60 | process.exit(0) 61 | } else { 62 | console.error('Failed to register relay:', result.error, result) 63 | process.exit(1) 64 | } 65 | })().catch( 66 | reason => { 67 | console.error(reason) 68 | process.exit(1) 69 | } 70 | ) 71 | -------------------------------------------------------------------------------- /packages/cli/src/commands/gsn-relayer-run.ts: -------------------------------------------------------------------------------- 1 | require('@opengsn/relay/dist/runServer') 2 | -------------------------------------------------------------------------------- /packages/cli/src/commands/gsn-start.ts: -------------------------------------------------------------------------------- 1 | import commander from 'commander' 2 | import { gsnCommander, saveDeployment, showDeployment } from '../utils' 3 | import { GsnTestEnvironment } from '../GsnTestEnvironment' 4 | import { createCommandsLogger } from '@opengsn/logger/dist/CommandsWinstonLogger' 5 | 6 | gsnCommander(['n']) 7 | .option('-w, --workdir ', 'relative work directory (defaults to build/gsn/)', 'build/gsn') 8 | .option('--relayUrl ', 'url to advertise the relayer', 'http://127.0.0.1/') 9 | .option('--port ', 'a port for the relayer to listen on. By default, relay will find random available port') 10 | .parse(process.argv); 11 | 12 | (async () => { 13 | const logger = createCommandsLogger(commander.loglevel) 14 | const network: string = commander.network 15 | const localRelayUrl: string = commander.relayUrl 16 | let port: number | undefined 17 | if (commander.port != null) { 18 | port = parseInt(commander.port) 19 | if (isNaN(port)) { 20 | throw new Error('port is NaN') 21 | } 22 | } 23 | const env = await GsnTestEnvironment.startGsn(network, localRelayUrl, port, logger) 24 | saveDeployment(env.contractsDeployment, commander.workdir) 25 | showDeployment(env.contractsDeployment, 'GSN started', logger, undefined) 26 | 27 | logger.info(`Relay is active, URL = ${env.relayUrl} . Press Ctrl-C to abort`) 28 | })().catch( 29 | reason => { 30 | console.error(reason) 31 | process.exit(1) 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /packages/cli/src/commands/gsn.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import commander from 'commander' 4 | import { gsnRuntimeVersion } from '@opengsn/common' 5 | 6 | commander 7 | .version(gsnRuntimeVersion) 8 | .command('start', 'all-on-one: deploy all contracts, start relay') 9 | .command('deploy', 'deploy RelayHub and other GSN contracts instances') 10 | .command('relayer-register', 'stake for a relayer and fund it') 11 | .command('relayer-withdraw', 'Withdraw relayer\'s manager balance from RelayHub to owner') 12 | .command('relayer-run', 'launch a relayer server') 13 | .command('paymaster-fund', 'fund a paymaster contract so it can pay for relayed calls') 14 | .command('paymaster-balance', 'query a paymaster GSN balance') 15 | .command('send-request', 'send a GSN meta-transaction request to a server using a GSN provider') 16 | .parse(process.argv) 17 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GsnTestEnvironment' 2 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "src/compiled/*.json", 6 | "../contracts/types/truffle-contracts/index.d.ts", 7 | "../contracts/types/truffle-contracts/types.d.ts" 8 | ], 9 | "compilerOptions": { 10 | "rootDir": "src", 11 | "outDir": "dist" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "test/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/common/README.md: -------------------------------------------------------------------------------- 1 | # GSN shared components 2 | 3 | [GSN, the Ethereum Gas Station Network](https://opengsn.org/) abstracts away gas to minimize onboarding & UX friction for dapps. 4 | 5 | This module is an internal component of other GSN modules. You are probably looking for one of these: 6 | 7 | `@opengsn/provider` - a Web3 provider for client-facing JavaScript dapps that uses GSN to relay transactions. 8 | 9 | `@opengsn/contracts` - all GSN contracts in Solidity language as well as their TypeScript type declarations. 10 | 11 | `@opengsn/cli` - command-line tools made to help developers integrate GSN, test their integrations locally, or bring up relay servers on public Ethereum networks. 12 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/common", 3 | "version": "3.0.0-beta.10", 4 | "license": "Apache-2.0", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "tsc": "tsc", 8 | "watch-tsc": "tsc -w --preserveWatchOutput", 9 | "lint": "eslint -f unix .", 10 | "lint-fix": "yarn lint --fix", 11 | "rm-dist": "rm -rf tsconfig.tsbuildinfo dist" 12 | }, 13 | "files": [ 14 | "dist/*", 15 | "README.md" 16 | ], 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "dependencies": { 21 | "@ethereumjs/common": "^2.6.5", 22 | "@ethersproject/abi": "^5.7.0", 23 | "@ethersproject/bignumber": "^5.7.0", 24 | "@ethersproject/networks": "^5.7.1", 25 | "@ethersproject/providers": "^5.7.2", 26 | "@metamask/eth-sig-util": "^5.1.0", 27 | "@opengsn/contracts": "^3.0.0-beta.10", 28 | "@types/bn.js": "^5.1.0", 29 | "@types/semver": "^7.3.4", 30 | "axios": "^0.21.1", 31 | "bn.js": "^5.2.0", 32 | "chalk": "^4.1.2", 33 | "ethereumjs-util": "^7.1.0", 34 | "ethers": "^5.7.2", 35 | "ethjs-unit": "^0.1.6", 36 | "number-to-bn": "^1.7.0", 37 | "ow": "^0.28.1", 38 | "semver": "^7.3.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/common/src/Constants.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js' 2 | 3 | import paymasterAbi from './interfaces/IPaymaster.json' 4 | import relayHubAbi from './interfaces/IRelayHub.json' 5 | import forwarderAbi from './interfaces/IForwarder.json' 6 | import stakeManagerAbi from './interfaces/IStakeManager.json' 7 | import penalizerAbi from './interfaces/IPenalizer.json' 8 | import relayRegistrarAbi from './interfaces/IRelayRegistrar.json' 9 | import { getERC165InterfaceID } from './Utils' 10 | import { toBN } from './web3js/Web3JSUtils' 11 | 12 | const dayInSec = 24 * 60 * 60 13 | const weekInSec = dayInSec * 7 14 | const yearInSec = dayInSec * 365 15 | const oneEther = toBN(1e18) 16 | 17 | export const constants = { 18 | dayInSec, 19 | weekInSec, 20 | yearInSec, 21 | oneEther, 22 | ZERO_ADDRESS: '0x0000000000000000000000000000000000000000', 23 | // OpenZeppelin's ERC-20 implementation bans transfer to zero address 24 | BURN_ADDRESS: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF', 25 | // in order to avoid error on insufficient balance for gas, send dry-run call from zero address 26 | DRY_RUN_ADDRESS: '0x0000000000000000000000000000000000000000', 27 | DRY_RUN_KEY: 'DRY-RUN', 28 | ZERO_BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000', 29 | MAX_UINT256: new BN('2').pow(new BN('256')).sub(new BN('1')), 30 | MAX_UINT96: new BN('2').pow(new BN('96')).sub(new BN('1')), 31 | MAX_INT256: new BN('2').pow(new BN('255')).sub(new BN('1')), 32 | MIN_INT256: new BN('2').pow(new BN('255')).mul(new BN('-1')), 33 | 34 | ARBITRUM_ARBSYS: '0x0000000000000000000000000000000000000064' 35 | } 36 | 37 | export const erc165Interfaces = { 38 | forwarder: getERC165InterfaceID(forwarderAbi as any), 39 | paymaster: getERC165InterfaceID(paymasterAbi as any), 40 | penalizer: getERC165InterfaceID(penalizerAbi as any), 41 | relayRegistrar: getERC165InterfaceID(relayRegistrarAbi as any), 42 | relayHub: getERC165InterfaceID(relayHubAbi as any), 43 | stakeManager: getERC165InterfaceID(stakeManagerAbi as any) 44 | } 45 | 46 | export const RelayCallStatusCodes = { 47 | OK: new BN('0'), 48 | RelayedCallFailed: new BN('1'), 49 | RejectedByPreRelayed: new BN('2'), 50 | RejectedByForwarder: new BN('3'), 51 | RejectedByRecipientRevert: new BN('4'), 52 | PostRelayedFailed: new BN('5'), 53 | PaymasterBalanceChanged: new BN('6') 54 | } 55 | -------------------------------------------------------------------------------- /packages/common/src/EIP712/ForwardRequest.ts: -------------------------------------------------------------------------------- 1 | import { type Address, type IntString } from '../types/Aliases' 2 | import { type PrefixedHexString } from 'ethereumjs-util' 3 | 4 | type addresses = 'from' | 'to' 5 | type data = 'data' 6 | type intStrings = 'value' | 'nonce' | 'gas' | 'validUntilTime' 7 | 8 | export type ForwardRequest = Record & Record & Record 9 | -------------------------------------------------------------------------------- /packages/common/src/EIP712/RelayData.ts: -------------------------------------------------------------------------------- 1 | import { type Address, type IntString } from '../types/Aliases' 2 | import { type PrefixedHexString } from 'ethereumjs-util' 3 | 4 | export interface RelayData { 5 | maxFeePerGas: IntString 6 | maxPriorityFeePerGas: IntString 7 | transactionCalldataGasUsed: IntString 8 | relayWorker: Address 9 | paymaster: Address 10 | paymasterData: PrefixedHexString 11 | clientId: IntString 12 | forwarder: Address 13 | } 14 | -------------------------------------------------------------------------------- /packages/common/src/EIP712/RelayRequest.ts: -------------------------------------------------------------------------------- 1 | import { type RelayData } from './RelayData' 2 | import { type ForwardRequest } from './ForwardRequest' 3 | 4 | export interface RelayRequest { 5 | request: ForwardRequest 6 | relayData: RelayData 7 | } 8 | 9 | export function cloneRelayRequest (relayRequest: RelayRequest): RelayRequest { 10 | return { 11 | request: { ...relayRequest.request }, 12 | relayData: { ...relayRequest.relayData } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/common/src/ErrorReplacerJSON.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style 2 | export function replaceErrors (key: string, value: { [key: string]: any }): any { 3 | if (value instanceof Map) { 4 | return { 5 | dataType: 'Map', 6 | value: Array.from(value.entries()) 7 | } 8 | } else if (value instanceof Error) { 9 | // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style 10 | const error: { [key: string]: any } = {} 11 | 12 | // remove "circular referenced" objects we don't really want to log... 13 | Object.getOwnPropertyNames(value).filter(e => !['request', 'response'].includes(e)).forEach(function (key) { 14 | error[key] = 15 | // @ts-ignore 16 | value[key] 17 | }) 18 | 19 | return error 20 | } 21 | 22 | return value 23 | } 24 | -------------------------------------------------------------------------------- /packages/common/src/GSNContractsDeployment.ts: -------------------------------------------------------------------------------- 1 | import { type Address } from './types/Aliases' 2 | 3 | export interface GSNContractsDeployment { 4 | forwarderAddress?: Address 5 | paymasterAddress?: Address 6 | penalizerAddress?: Address 7 | relayRegistrarAddress?: Address 8 | relayHubAddress?: Address 9 | stakeManagerAddress?: Address 10 | managerStakeTokenAddress?: Address 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/src/HttpWrapper.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios' 2 | 3 | const LOGMAXLEN = 120 4 | const DEFAULT_TIMEOUT = 15000 5 | 6 | export class HttpWrapper { 7 | private readonly provider: AxiosInstance 8 | private readonly logreq: boolean 9 | 10 | constructor (opts: AxiosRequestConfig = {}, logreq: boolean = false) { 11 | this.provider = axios.create(Object.assign({ 12 | timeout: DEFAULT_TIMEOUT, 13 | headers: { 'Content-Type': 'application/json' } 14 | }, opts)) 15 | this.logreq = logreq 16 | 17 | if (this.logreq) { 18 | this.provider.interceptors.response.use(function (response) { 19 | console.log('got response:', response.config.url, JSON.stringify(response.data).slice(0, LOGMAXLEN)) 20 | return response 21 | }, async function (error: any): Promise { 22 | const errData = error.response != null ? error.response.data : { error: error.message } 23 | const errStr = ((typeof errData === 'string') ? errData : JSON.stringify(errData)).slice(0, LOGMAXLEN) 24 | const errUrl = error.response != null ? error.response.config.url : error.address 25 | console.log('got response:', errUrl, 'err=', errStr) 26 | return await Promise.reject(error) 27 | }) 28 | } 29 | } 30 | 31 | async sendPromise (url: URL, jsonRequestData?: any): Promise { 32 | if (this.logreq) { 33 | console.log('sending request:', url, JSON.stringify(jsonRequestData ?? {}).slice(0, LOGMAXLEN)) 34 | } 35 | 36 | const response = await this.provider.request({ 37 | url: url.toString(), 38 | method: jsonRequestData != null ? 'POST' : 'GET', 39 | data: jsonRequestData 40 | }) 41 | return response.data 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/common/src/LoggerInterface.ts: -------------------------------------------------------------------------------- 1 | type LogMethod = (msg: string) => void 2 | 3 | export interface LoggerInterface { 4 | error: LogMethod 5 | warn: LogMethod 6 | info: LogMethod 7 | debug: LogMethod 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/src/PingResponse.ts: -------------------------------------------------------------------------------- 1 | import { type Address, type IntString } from './types/Aliases' 2 | 3 | export interface PingResponse { 4 | relayWorkerAddress: Address 5 | relayManagerAddress: Address 6 | relayHubAddress: Address 7 | ownerAddress: Address 8 | maxMaxFeePerGas: IntString 9 | minMaxFeePerGas: IntString 10 | minMaxPriorityFeePerGas: IntString 11 | maxAcceptanceBudget: IntString 12 | networkId?: IntString 13 | chainId?: IntString 14 | ready: boolean 15 | version: string 16 | } 17 | -------------------------------------------------------------------------------- /packages/common/src/StatsResponse.ts: -------------------------------------------------------------------------------- 1 | export interface ReadinessInfo { 2 | runningSince: number 3 | currentStateTimestamp: number 4 | 5 | totalReadyTime: number 6 | totalNotReadyTime: number 7 | totalReadinessChanges: number 8 | } 9 | 10 | export interface StatsResponse extends ReadinessInfo { 11 | totalUptime: number 12 | } 13 | -------------------------------------------------------------------------------- /packages/common/src/Version.ts: -------------------------------------------------------------------------------- 1 | export const gsnRuntimeVersion: string = require('../package.json').version 2 | export const gsnRequiredVersion = '^3.0.0-beta.3' 3 | -------------------------------------------------------------------------------- /packages/common/src/VersionsManager.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver' 2 | 3 | export class VersionsManager { 4 | readonly requiredVersionRange: string 5 | 6 | /** 7 | * @param componentVersion - a semver of a component that uses the VersionsManager 8 | * @param requiredVersionRange - a semver that has to be satisfied by the 9 | */ 10 | constructor (componentVersion: string, requiredVersionRange?: string) { 11 | if (semver.valid(componentVersion) == null) { 12 | throw new Error('Component version is not valid') 13 | } 14 | 15 | if (requiredVersionRange == null) { 16 | const ver = new semver.SemVer(componentVersion) 17 | ver.patch = 0 18 | requiredVersionRange = '^' + ver.format() 19 | } 20 | this.requiredVersionRange = requiredVersionRange 21 | } 22 | 23 | /** 24 | * @param version - the version of a dependency to compare against 25 | * @return true if {@param version} satisfies the {@link requiredVersionRange} 26 | */ 27 | isRequiredVersionSatisfied (version: string): boolean { 28 | // prevent crash with some early paymasters (which are otherwise perfectly valid) 29 | version = version.replace('_', '-') 30 | 31 | return semver.satisfies(version, this.requiredVersionRange, { includePrerelease: true }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/common/src/dev/NetworkSimulatingProvider.ts: -------------------------------------------------------------------------------- 1 | import { type PrefixedHexString } from 'ethereumjs-util' 2 | import { type JsonRpcProvider } from '@ethersproject/providers' 3 | import { utils } from 'ethers' 4 | 5 | import { WrapperProviderBase } from './WrapperProviderBase' 6 | 7 | interface DelayedSend { 8 | method: string 9 | params: any 10 | } 11 | 12 | export class NetworkSimulatingProvider extends WrapperProviderBase { 13 | private isDelayTransactionsOn = false 14 | 15 | mempool = new Map() 16 | 17 | public constructor (provider: JsonRpcProvider) { 18 | super(provider) 19 | } 20 | 21 | setDelayTransactions (delayTransactions: boolean): void { 22 | this.isDelayTransactionsOn = delayTransactions 23 | } 24 | 25 | calculateTxHash (params?: any[]): PrefixedHexString { 26 | const txHash = utils.keccak256(params?.[0]) 27 | if (txHash == null) { 28 | throw new Error('Failed to hash transaction') 29 | } 30 | return txHash 31 | } 32 | 33 | async send (method: string, params: any[]): Promise { 34 | let txHash 35 | switch (method) { 36 | case 'eth_sendRawTransaction': 37 | if (this.isDelayTransactionsOn) { 38 | txHash = this.calculateTxHash(params) 39 | this.mempool.set(txHash, { method, params }) 40 | } 41 | break 42 | } 43 | if (txHash != null) { 44 | return txHash 45 | } else { 46 | return await this.provider.send(method, params) 47 | } 48 | } 49 | 50 | supportsSubscriptions (): boolean { 51 | return false 52 | } 53 | 54 | async mineTransaction (txHash: PrefixedHexString): Promise { 55 | const txPayload: DelayedSend | undefined = this.mempool.get(txHash) 56 | this.mempool.delete(txHash) 57 | if (txPayload == null) { 58 | throw new Error(`Transaction ${txHash} is not in simulated mempool. It must be already mined`) 59 | } 60 | return await this.provider.send(txPayload.method, txPayload.params) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/common/src/dev/ProfilingProvider.ts: -------------------------------------------------------------------------------- 1 | import { StaticJsonRpcProvider } from '@ethersproject/providers' 2 | 3 | export class ProfilingProvider extends StaticJsonRpcProvider { 4 | methodsCount = new Map() 5 | requestsCount = 0 6 | 7 | logTraffic: boolean 8 | 9 | constructor (host: string, logTraffic: boolean = false) { 10 | super(host) 11 | this.logTraffic = logTraffic 12 | } 13 | 14 | async send (method: string, params: any[]): Promise { 15 | this.requestsCount++ 16 | const currentCount = this.methodsCount.get(method) ?? 0 17 | this.methodsCount.set(method, currentCount + 1) 18 | if (this.logTraffic) { 19 | console.log(`>>> payload: "${method}" [${JSON.stringify(params)}]`) 20 | } 21 | try { 22 | const result = await super.send(method, params) 23 | if (this.logTraffic) { 24 | console.log(`<<< result: ${JSON.stringify(result) ?? 'null result'}`) 25 | } 26 | return result 27 | } catch (error: any) { 28 | if (this.logTraffic) { 29 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 30 | console.log(`<<< error: ${error.message ?? 'null error message'}`) 31 | } 32 | throw error 33 | } 34 | } 35 | 36 | reset (): void { 37 | this.requestsCount = 0 38 | this.methodsCount.clear() 39 | } 40 | 41 | log (): void { 42 | console.log('Profiling Provider Stats:') 43 | new Map([...this.methodsCount.entries()].sort(function ([, count1], [, count2]) { 44 | return count2 - count1 45 | })).forEach(function (value, key) { 46 | console.log(`Method: ${key.padEnd(30)} was called: ${value.toString().padEnd(3)} times`) 47 | }) 48 | console.log(`Total RPC calls: ${this.requestsCount}`) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/common/src/dev/WrapperProviderBase.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from '@ethersproject/providers' 2 | 3 | export abstract class WrapperProviderBase extends JsonRpcProvider { 4 | provider: JsonRpcProvider 5 | 6 | protected constructor (provider: JsonRpcProvider) { 7 | super() 8 | this.provider = provider 9 | } 10 | 11 | abstract send (method: string, params: any[]): Promise 12 | } 13 | -------------------------------------------------------------------------------- /packages/common/src/environments/AsyncZeroAddressCalldataGasEstimation.ts: -------------------------------------------------------------------------------- 1 | import { type PrefixedHexString } from 'ethereumjs-util' 2 | 3 | import { type CalldataGasEstimation } from '../types/Aliases' 4 | import { type Environment } from './Environments' 5 | import { constants } from '../Constants' 6 | import { type JsonRpcProvider } from '@ethersproject/providers' 7 | 8 | /** 9 | * In most L2s, the cost of the transaction is dynamic and depends on L1 gas price. 10 | * This function tries to extract calldata cost by requesting an estimate but setting target to zero address. 11 | * As our result must be above the Relay Server's estimate, it makes sense to add some slack to the estimate. 12 | * @param calldata 13 | * @param environment 14 | * @param calldataEstimationSlackFactor 15 | * @param provider 16 | * @constructor 17 | */ 18 | export const AsyncZeroAddressCalldataGasEstimation: CalldataGasEstimation = async ( 19 | calldata: PrefixedHexString, 20 | environment: Environment, 21 | calldataEstimationSlackFactor: number, 22 | provider: JsonRpcProvider 23 | ): Promise => { 24 | const estimateGasCallToZero = await provider.estimateGas({ 25 | to: constants.ZERO_ADDRESS, 26 | data: calldata 27 | }) 28 | return parseInt(estimateGasCallToZero.toString()) * calldataEstimationSlackFactor 29 | } 30 | -------------------------------------------------------------------------------- /packages/common/src/environments/MainnetCalldataGasEstimation.ts: -------------------------------------------------------------------------------- 1 | import { type PrefixedHexString } from 'ethereumjs-util' 2 | 3 | import { type CalldataGasEstimation } from '../types/Aliases' 4 | import { calculateCalldataBytesZeroNonzero } from '../Utils' 5 | import { type Environment } from './Environments' 6 | 7 | /** 8 | * On the Ethereum Mainnet, the transaction cost is currently determined by the EIP-2028. 9 | * In case different coefficients are used later or in different chains, the values are read from the Environment. 10 | * @param calldata 11 | * @param environment 12 | * @constructor 13 | */ 14 | export const MainnetCalldataGasEstimation: CalldataGasEstimation = async ( 15 | calldata: PrefixedHexString, 16 | environment: Environment 17 | ): Promise => { 18 | const { calldataZeroBytes, calldataNonzeroBytes } = calculateCalldataBytesZeroNonzero(calldata) 19 | return environment.mintxgascost + calldataZeroBytes * environment.gtxdatazero + 20 | calldataNonzeroBytes * environment.gtxdatanonzero 21 | } 22 | -------------------------------------------------------------------------------- /packages/common/src/ethereumjstx/TxOptions.ts: -------------------------------------------------------------------------------- 1 | // Copied from "@ethereumjs/tx/dist/types.d.ts" to remove it as dependency 2 | // TODO: stop using 'TxOptions' in server as well 3 | import type Common from '@ethereumjs/common' 4 | 5 | export interface TxOptions { 6 | common?: Common 7 | freeze?: boolean 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AmountRequired' 2 | export * from './ConfigResponse' 3 | export * from './Constants' 4 | export * from './ContractInteractor' 5 | export * from './RelayCallGasLimitCalculationHelper' 6 | export * from './EIP712/ForwardRequest' 7 | export * from './EIP712/RelayData' 8 | export * from './EIP712/RelayRequest' 9 | export * from './EIP712/TypedRequestData' 10 | export * from './GSNContractsDeployment' 11 | export * from './HttpClient' 12 | export * from './HttpWrapper' 13 | export * from './LoggerInterface' 14 | export * from './PingResponse' 15 | export * from './StatsResponse' 16 | export * from './Utils' 17 | export * from './Version' 18 | export * from './VersionsManager' 19 | export * from './environments/AsyncZeroAddressCalldataGasEstimation' 20 | export * from './environments/Environments' 21 | export * from './environments/MainnetCalldataGasEstimation' 22 | export * from './environments/OfficialPaymasterDeployments' 23 | export * from './types/Aliases' 24 | export * from './types/AuditRequest' 25 | export * from './types/GSNContractsDataTypes' 26 | export * from './types/GsnTransactionDetails' 27 | export * from './types/PaymasterConfiguration' 28 | export * from './types/PenalizerConfiguration' 29 | export * from './types/RelayFailureInfo' 30 | export * from './types/RelayHubConfiguration' 31 | export * from './types/RelayInfo' 32 | export * from './types/RelayTransactionRequest' 33 | export * from './types/TransactionType' 34 | export * from './web3js/FeeHistoryResult' 35 | export * from './web3js/JsonRpcPayload' 36 | export * from './web3js/JsonRpcResponse' 37 | export * from './web3js/RLPEncodedTransaction' 38 | export * from './web3js/Web3JSUtils' 39 | -------------------------------------------------------------------------------- /packages/common/src/types/Aliases.ts: -------------------------------------------------------------------------------- 1 | import { type PrefixedHexString } from 'ethereumjs-util' 2 | import { type JsonRpcProvider, type Log } from '@ethersproject/providers' 3 | import { type LogDescription } from '@ethersproject/abi' 4 | 5 | import { type PingResponse } from '../PingResponse' 6 | import { type RelayRequest } from '../EIP712/RelayRequest' 7 | import { type GsnTransactionDetails } from './GsnTransactionDetails' 8 | import { type RegistrarRelayInfo, type PartialRelayInfo } from './RelayInfo' 9 | import { type TypedMessage } from '@metamask/eth-sig-util' 10 | import { type Environment } from '../environments/Environments' 11 | 12 | export type Address = string 13 | export type EventName = string 14 | export type IntString = string 15 | export type SemVerString = string 16 | /** 17 | * For legacy reasons, to filter out the relay this filter has to throw. 18 | * TODO: make ping filtering sane! 19 | */ 20 | export type PingFilter = (pingResponse: PingResponse, gsnTransactionDetails: GsnTransactionDetails) => void 21 | 22 | /** 23 | * As the "PaymasterData" is included in the user-signed request, it cannot have access to the "relayRequestId" value. 24 | */ 25 | export type PaymasterDataCallback = (relayRequest: RelayRequest) => Promise 26 | 27 | export type ApprovalDataCallback = (relayRequest: RelayRequest, relayRequestId: PrefixedHexString) => Promise 28 | 29 | export type SignTypedDataCallback = (signedData: TypedMessage, from: Address) => Promise 30 | 31 | /** 32 | * Different L2 rollups and side-chains have different behavior for the calldata gas cost. 33 | * This means the calldata estimation cannot be hard-coded and new implementations should be easy to add. 34 | * Note that both Relay Client and Relay Server must come to the same number. 35 | * Also, this value does include the base transaction cost (2100 on mainnet). 36 | */ 37 | export type CalldataGasEstimation = (calldata: PrefixedHexString, environment: Environment, calldataEstimationSlackFactor: number, provider: JsonRpcProvider) => Promise 38 | 39 | export type RelayFilter = (registrarRelayInfo: RegistrarRelayInfo) => boolean 40 | 41 | export type EventData = Log & LogDescription 42 | 43 | export function notNull (value: TValue | null | undefined): value is TValue { 44 | return value !== null && value !== undefined 45 | } 46 | 47 | /** 48 | * This is an intersection of NPM log levels and 'loglevel' library methods. 49 | */ 50 | export type NpmLogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug' 51 | 52 | export interface RelaySelectionResult { 53 | relayInfo: PartialRelayInfo 54 | maxDeltaPercent: number 55 | updatedGasFees: EIP1559Fees 56 | } 57 | 58 | export interface EIP1559Fees { 59 | maxFeePerGas: PrefixedHexString 60 | maxPriorityFeePerGas: PrefixedHexString 61 | } 62 | 63 | export type ObjectMap = Record 64 | -------------------------------------------------------------------------------- /packages/common/src/types/AuditRequest.ts: -------------------------------------------------------------------------------- 1 | import ow from 'ow' 2 | import { type PrefixedHexString } from 'ethereumjs-util' 3 | 4 | export interface AuditRequest { 5 | signedTx: PrefixedHexString 6 | } 7 | 8 | export interface AuditResponse { 9 | commitTxHash?: PrefixedHexString 10 | message?: string 11 | } 12 | 13 | export const AuditRequestShape = { 14 | signedTx: ow.string 15 | } 16 | -------------------------------------------------------------------------------- /packages/common/src/types/GsnTransactionDetails.ts: -------------------------------------------------------------------------------- 1 | import { type Address, type IntString } from './Aliases' 2 | import { type PrefixedHexString } from 'ethereumjs-util' 3 | 4 | export interface GsnTransactionDetails { 5 | // Added by the Web3 call stack: 6 | readonly from: Address 7 | readonly data: PrefixedHexString 8 | readonly to: Address 9 | 10 | readonly value?: IntString 11 | gas?: PrefixedHexString 12 | maxFeePerGas: PrefixedHexString 13 | maxPriorityFeePerGas: PrefixedHexString 14 | readonly paymasterData?: PrefixedHexString 15 | readonly clientId?: IntString 16 | 17 | // Optional parameters for RelayProvider only: 18 | /** 19 | * Set to 'false' to create a direct transaction 20 | */ 21 | readonly useGSN?: boolean 22 | } 23 | -------------------------------------------------------------------------------- /packages/common/src/types/PaymasterConfiguration.ts: -------------------------------------------------------------------------------- 1 | export interface PaymasterConfiguration { 2 | forwarderHubOverhead: number 3 | preRelayedCallGasLimit: number 4 | postRelayedCallGasLimit: number 5 | acceptanceBudget: number 6 | calldataSizeLimit: number 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/src/types/PenalizerConfiguration.ts: -------------------------------------------------------------------------------- 1 | export interface PenalizerConfiguration { 2 | penalizeBlockDelay: number 3 | penalizeBlockExpiration: number 4 | } 5 | -------------------------------------------------------------------------------- /packages/common/src/types/RelayFailureInfo.ts: -------------------------------------------------------------------------------- 1 | export interface RelayFailureInfo { 2 | lastErrorTime: number 3 | relayManager: string 4 | relayUrl: string 5 | } 6 | -------------------------------------------------------------------------------- /packages/common/src/types/RelayHubConfiguration.ts: -------------------------------------------------------------------------------- 1 | import type BN from 'bn.js' 2 | 3 | export interface RelayHubConfiguration { 4 | maxWorkerCount: number | BN 5 | gasReserve: number | BN 6 | postOverhead: number | BN 7 | gasOverhead: number | BN 8 | minimumUnstakeDelay: number | BN 9 | devAddress: string 10 | devFee: number | BN | string 11 | baseRelayFee: number | BN | string 12 | pctRelayFee: number | BN | string 13 | } 14 | -------------------------------------------------------------------------------- /packages/common/src/types/RelayInfo.ts: -------------------------------------------------------------------------------- 1 | import { type PingResponse } from '../PingResponse' 2 | import { type RelayInfoUrl } from './GSNContractsDataTypes' 3 | 4 | // Well, I still don't like it 5 | // Some info is known from the event, some from ping 6 | export interface PartialRelayInfo { 7 | relayInfo: RelayInfoUrl 8 | pingResponse: PingResponse 9 | } 10 | 11 | export interface RelayInfo { 12 | pingResponse: PingResponse 13 | relayInfo: RegistrarRelayInfo 14 | } 15 | 16 | export interface RegistrarRelayInfo { 17 | lastSeenBlockNumber: number 18 | lastSeenTimestamp: number 19 | firstSeenBlockNumber: number 20 | firstSeenTimestamp: number 21 | relayUrl: string 22 | relayManager: string 23 | } 24 | -------------------------------------------------------------------------------- /packages/common/src/types/RelayTransactionRequest.ts: -------------------------------------------------------------------------------- 1 | import { type PrefixedHexString } from 'ethereumjs-util' 2 | import ow from 'ow' 3 | 4 | import { type Address } from './Aliases' 5 | import { type RelayRequest } from '../EIP712/RelayRequest' 6 | 7 | export interface RelayMetadata { 8 | approvalData: PrefixedHexString 9 | relayHubAddress: Address 10 | relayLastKnownNonce: number 11 | relayRequestId: PrefixedHexString 12 | relayMaxNonce: number 13 | signature: PrefixedHexString 14 | maxAcceptanceBudget: PrefixedHexString 15 | domainSeparatorName: string 16 | } 17 | 18 | export interface RelayTransactionRequest { 19 | relayRequest: RelayRequest 20 | metadata: RelayMetadata 21 | } 22 | 23 | export const RelayTransactionRequestShape = { 24 | relayRequest: { 25 | request: { 26 | from: ow.string, 27 | to: ow.string, 28 | data: ow.string, 29 | value: ow.string, 30 | nonce: ow.string, 31 | gas: ow.string, 32 | validUntilTime: ow.string 33 | }, 34 | relayData: { 35 | maxPriorityFeePerGas: ow.string, 36 | maxFeePerGas: ow.string, 37 | transactionCalldataGasUsed: ow.string, 38 | relayWorker: ow.string, 39 | paymaster: ow.string, 40 | paymasterData: ow.string, 41 | clientId: ow.string, 42 | forwarder: ow.string 43 | } 44 | }, 45 | metadata: { 46 | domainSeparatorName: ow.string, 47 | relayLastKnownNonce: ow.number, 48 | approvalData: ow.string, 49 | relayHubAddress: ow.string, 50 | relayMaxNonce: ow.number, 51 | relayRequestId: ow.string, 52 | signature: ow.string, 53 | maxAcceptanceBudget: ow.string 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/common/src/types/TransactionType.ts: -------------------------------------------------------------------------------- 1 | export enum TransactionType { 2 | LEGACY, 3 | TYPE_TWO = 2 4 | } 5 | -------------------------------------------------------------------------------- /packages/common/src/web3js/FeeHistoryResult.ts: -------------------------------------------------------------------------------- 1 | export interface FeeHistoryResult { 2 | baseFeePerGas: string[] 3 | gasUsedRatio: number[] 4 | oldestBlock: number 5 | reward: string[][] 6 | } 7 | -------------------------------------------------------------------------------- /packages/common/src/web3js/JsonRpcPayload.ts: -------------------------------------------------------------------------------- 1 | export interface JsonRpcPayload { 2 | jsonrpc: string 3 | method: string 4 | params?: any[] 5 | id?: string | number 6 | } 7 | -------------------------------------------------------------------------------- /packages/common/src/web3js/JsonRpcResponse.ts: -------------------------------------------------------------------------------- 1 | export interface JsonRpcResponse { 2 | jsonrpc: string 3 | id: string | number 4 | result?: any 5 | error?: { 6 | readonly code?: number 7 | readonly data?: unknown 8 | readonly message: string 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/common/src/web3js/RLPEncodedTransaction.ts: -------------------------------------------------------------------------------- 1 | export interface RLPEncodedTransaction { 2 | raw: string 3 | tx: { 4 | nonce: string 5 | gasPrice: string 6 | gas: string 7 | to: string 8 | value: string 9 | input: string 10 | r: string 11 | s: string 12 | v: string 13 | hash: string 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "src/interfaces/*.json" 6 | ], 7 | "compilerOptions": { 8 | "rootDir": "src", 9 | "outDir": "dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/tsconfig.packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "test/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/contracts/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@openzeppelin/contracts", "solc-0.8"] 2 | -------------------------------------------------------------------------------- /packages/contracts/.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "immutable-vars-naming": ["off"], 5 | "custom-errors": ["off"], 6 | "no-global-import": ["off"], 7 | "no-console": ["off"], 8 | "compiler-version": ["error",">=0.6"], 9 | "func-visibility": ["off",{"ignoreConstructors":true}], 10 | "mark-callable-contracts": ["off"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/contracts/README.md: -------------------------------------------------------------------------------- 1 | # GSN smart contracts 2 | 3 | [GSN, the Ethereum Gas Station Network](https://opengsn.org/) abstracts away gas to minimize onboarding & UX friction for dapps. 4 | 5 | This module contains all GSN contracts in Solidity language as well as their TypeScript type declarations. 6 | 7 | [Complete documentation is available here.](https://docs.opengsn.org/contracts/) 8 | -------------------------------------------------------------------------------- /packages/contracts/contract.hbs: -------------------------------------------------------------------------------- 1 | ## `{{name}}` 2 | {{{natspec.title}}} 3 | 4 | {{{natspec.userdoc}}} 5 | 6 | {{{natspec.devdoc}}} 7 | 8 | ## Functions 9 | {{#each ownFunctions}} 10 | ### `{{name}}({{args}})` 11 | {{#if outputs}}` → {{outputs}}`{{/if}} ({{visibility}}) 12 | 13 | {{{natspec.userdoc}}} 14 | 15 | {{{natspec.devdoc}}} 16 | 17 | {{#each natspec.params}} 18 | {{param}}: {{description}} 19 | {{/each}} 20 | 21 | {{#if natspec.returns}}#### Return values{{/if}} 22 | 23 | {{#each natspec.returns}} 24 | {{return}} {{description}} 25 | {{/each}} 26 | 27 | --- 28 | {{/each}} 29 | 30 | {{#if ownEvents}} 31 | ## Events 32 | {{#each ownEvents}} 33 | ### `{{name}}({{args}})` 34 | 35 | {{{natspec.userdoc}}} 36 | 37 | {{{natspec.devdoc}}} 38 | 39 | --- 40 | {{/each}} 41 | {{/if}} 42 | 43 | {{#if ownStructs}} 44 | ## Structs 45 | {{#each ownStructs}} 46 | ### `{{name}}` 47 | 48 | {{#each members}} 49 | 50 | {{type}} {{name}} 51 | 52 | {{/each}} 53 | 54 | --- 55 | {{/each}} 56 | {{/if}} 57 | 58 | {{#if ownEnums}} 59 | ## Enums 60 | {{#each ownEnums}} 61 | ### `{{name}}` 62 | 63 | {{#each members}} 64 | 65 | {{name}} 66 | 67 | {{/each}} 68 | 69 | --- 70 | {{/each}} 71 | {{/if}} 72 | 73 | {{#if ownModifiers}} 74 | ## Modifiers 75 | {{#each ownModifiers}} 76 | ### `{{name}}({{args}})` 77 | 78 | {{{natspec.userdoc}}} 79 | 80 | {{{natspec.devdoc}}} 81 | 82 | --- 83 | {{/each}} 84 | {{/if}} 85 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/contracts", 3 | "license": "GPL-3.0-only", 4 | "version": "3.0.0-beta.10", 5 | "main": "dist/types/ethers-contracts/index.js", 6 | "scripts": { 7 | "tsc": "tsc", 8 | "solpp": "yarn --cwd=\"../..\" solpp", 9 | "solidity-docgen": "rm -rf docs && solidity-docgen --solc-module solc-0.8 --input=src --exclude=src/test,src/forwarder/test --templates=.", 10 | "truffle-compile": "ENABLE_CONSOLE_LOG=1 yarn solpp && truffle compile", 11 | "typechain-generate": "yarn typechain-generate-ethers-v5", 12 | "typechain-generate-ethers-v5": "yarn truffle-compile && typechain --target ethers-v5 '../cli/src/compiled/*.json'", 13 | "lint": "solhint -f unix \"src/**/*.sol\" --max-warnings 0", 14 | "rm-dist": "rm -rf types/truffle-contracts solpp" 15 | }, 16 | "files": [ 17 | "src/*", 18 | "dist/*", 19 | "types/*", 20 | "README.md" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "dependencies": { 26 | "@ethersproject/abi": "^5.7.0", 27 | "@ethersproject/providers": "^5.7.2", 28 | "@openzeppelin/contracts": "^4.2.0", 29 | "ethers": "^5.7.2" 30 | }, 31 | "devDependencies": { 32 | "solc-0.8": "npm:solc@0.8.11", 33 | "solhint": "^3.3.2", 34 | "solidity-docgen": "^0.5.16", 35 | "ts-node": "8.6.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/contracts/src/ERC2771Recipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // solhint-disable no-inline-assembly 3 | pragma solidity >=0.6.9; 4 | 5 | import "./interfaces/IERC2771Recipient.sol"; 6 | 7 | /** 8 | * @title The ERC-2771 Recipient Base Abstract Class - Implementation 9 | * 10 | * @notice Note that this contract was called `BaseRelayRecipient` in the previous revision of the GSN. 11 | * 12 | * @notice A base contract to be inherited by any contract that want to receive relayed transactions. 13 | * 14 | * @notice A subclass must use `_msgSender()` instead of `msg.sender`. 15 | */ 16 | abstract contract ERC2771Recipient is IERC2771Recipient { 17 | 18 | /* 19 | * Forwarder singleton we accept calls from 20 | */ 21 | address private _trustedForwarder; 22 | 23 | /** 24 | * :warning: **Warning** :warning: The Forwarder can have a full control over your Recipient. Only trust verified Forwarder. 25 | * @notice Method is not a required method to allow Recipients to trust multiple Forwarders. Not recommended yet. 26 | * @return forwarder The address of the Forwarder contract that is being used. 27 | */ 28 | function getTrustedForwarder() public virtual view returns (address forwarder){ 29 | return _trustedForwarder; 30 | } 31 | 32 | function _setTrustedForwarder(address _forwarder) internal { 33 | _trustedForwarder = _forwarder; 34 | } 35 | 36 | /// @inheritdoc IERC2771Recipient 37 | function isTrustedForwarder(address forwarder) public virtual override view returns(bool) { 38 | return forwarder == _trustedForwarder; 39 | } 40 | 41 | /// @inheritdoc IERC2771Recipient 42 | function _msgSender() internal override virtual view returns (address ret) { 43 | if (msg.data.length >= 20 && isTrustedForwarder(msg.sender)) { 44 | // At this point we know that the sender is a trusted forwarder, 45 | // so we trust that the last bytes of msg.data are the verified sender address. 46 | // extract sender address from the end of msg.data 47 | assembly { 48 | ret := shr(96,calldataload(sub(calldatasize(),20))) 49 | } 50 | } else { 51 | ret = msg.sender; 52 | } 53 | } 54 | 55 | /// @inheritdoc IERC2771Recipient 56 | function _msgData() internal override virtual view returns (bytes calldata ret) { 57 | if (msg.data.length >= 20 && isTrustedForwarder(msg.sender)) { 58 | return msg.data[0:msg.data.length-20]; 59 | } else { 60 | return msg.data; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/contracts/src/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract Migrations { 5 | address public owner; 6 | // solhint-disable-next-line var-name-mixedcase 7 | uint256 public last_completed_migration; 8 | 9 | constructor() { 10 | owner = msg.sender; 11 | } 12 | 13 | modifier restricted() { 14 | if (msg.sender == owner) _; 15 | } 16 | 17 | function setCompleted(uint256 completed) public restricted { 18 | last_completed_migration = completed; 19 | } 20 | 21 | function upgrade(address newAddress) public restricted { 22 | Migrations upgraded = Migrations(newAddress); 23 | upgraded.setCompleted(last_completed_migration); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/contracts/src/arbitrum/ArbRelayHub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "../RelayHub.sol"; 6 | import "./ArbSys.sol"; 7 | 8 | /** 9 | * @title The RelayHub Implementation for Arbitrum 10 | * @notice This contract implements the `IRelayHub` interface for the Arbitrum-compatible Rollups. 11 | * 12 | * @notice This implementation relies on the `ArbSys` built-ins that do not exist outside of Arbitrum. 13 | */ 14 | contract ArbRelayHub is RelayHub { 15 | 16 | /// @inheritdoc IRelayHub 17 | function versionHub() override public pure returns (string memory){ 18 | return "3.0.0-beta.3+opengsn.arbhub.irelayhub"; 19 | } 20 | 21 | ArbSys public immutable arbsys; 22 | uint256 internal immutable arbCreationBlock; 23 | 24 | /// @notice we accept the `ArbSys` address in the constructor to allow mocking it in tests. 25 | constructor( 26 | ArbSys _arbsys, 27 | IStakeManager _stakeManager, 28 | address _penalizer, 29 | address _batchGateway, 30 | address _relayRegistrar, 31 | RelayHubConfig memory _config 32 | ) RelayHub(_stakeManager, _penalizer, _batchGateway, _relayRegistrar, _config){ 33 | arbsys = _arbsys; 34 | arbCreationBlock = _arbsys.arbBlockNumber(); 35 | } 36 | 37 | /// @inheritdoc IRelayHub 38 | /// @notice Uses `ArbSys` L2 block number specific to the Arbitrum Rollup. 39 | function getCreationBlock() external override virtual view returns (uint256){ 40 | return arbCreationBlock; 41 | } 42 | 43 | /// @return The block number in which the contract has been deployed. 44 | /// @notice Uses original L1 block number. 45 | function getL1CreationBlock() external view returns (uint256){ 46 | return creationBlock; 47 | } 48 | 49 | /// @notice Includes the 'storage gas' specific to the Arbitrum Rollup. 50 | /// @inheritdoc IRelayHub 51 | function aggregateGasleft() public override virtual view returns (uint256){ 52 | return arbsys.getStorageGasAvailable() + gasleft(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/contracts/src/arbitrum/ArbSys.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title Precompiled contract that exists in every Arbitrum chain at address(100), 0x0000000000000000000000000000000000000064. Exposes a variety of system-level functionality. 6 | */ 7 | interface ArbSys { 8 | 9 | /** 10 | * @notice Get Arbitrum block number (distinct from L1 block number; Arbitrum genesis block has block number 0) 11 | * @return block number as int 12 | */ 13 | function arbBlockNumber() external view returns (uint256); 14 | 15 | /** 16 | * @notice get the caller's amount of available storage gas 17 | * @return amount of storage gas available to the caller 18 | */ 19 | function getStorageGasAvailable() external view returns (uint256); 20 | } 21 | -------------------------------------------------------------------------------- /packages/contracts/src/forwarder/test/TestForwarder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "../Forwarder.sol"; 6 | 7 | // helper class for testing the forwarder. 8 | contract TestForwarder { 9 | function callExecute(Forwarder forwarder, Forwarder.ForwardRequest memory req, 10 | bytes32 domainSeparator, bytes32 requestTypeHash, bytes memory suffixData, bytes memory sig) public payable { 11 | (bool success, bytes memory error) = forwarder.execute{value:msg.value}(req, domainSeparator, requestTypeHash, suffixData, sig); 12 | emit Result(success, success ? "" : this.decodeErrorMessage(error)); 13 | } 14 | 15 | event Result(bool success, string error); 16 | 17 | function decodeErrorMessage(bytes calldata ret) external pure returns (string memory message) { 18 | //decode evert string: assume it has a standard Error(string) signature: simply skip the (selector,offset,length) fields 19 | if ( ret.length>4+32+32 ) { 20 | return abi.decode(ret[4:], (string)); 21 | } 22 | //unknown buffer. return as-is 23 | return string(ret); 24 | } 25 | 26 | function getChainId() public view returns (uint256 id){ 27 | /* solhint-disable-next-line no-inline-assembly */ 28 | assembly { id := chainid() } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/contracts/src/forwarder/test/TestForwarderTarget.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../ERC2771Recipient.sol"; 5 | 6 | contract TestForwarderTarget is ERC2771Recipient { 7 | 8 | constructor(address forwarder) { 9 | _setTrustedForwarder(forwarder); 10 | } 11 | 12 | // solhint-disable-next-line no-empty-blocks 13 | receive() external payable {} 14 | 15 | event TestForwarderMessage(string message, bytes realMsgData, address realSender, address msgSender, address origin); 16 | 17 | function emitMessage(string memory message) public { 18 | 19 | // solhint-disable-next-line avoid-tx-origin 20 | emit TestForwarderMessage(message, _msgData(), _msgSender(), msg.sender, tx.origin); 21 | } 22 | 23 | function publicMsgSender() public view returns (address) { 24 | return _msgSender(); 25 | } 26 | 27 | function publicMsgData() public view returns (bytes memory) { 28 | return _msgData(); 29 | } 30 | 31 | function mustReceiveEth(uint256 value) public payable { 32 | require( msg.value == value, "didn't receive value"); 33 | } 34 | 35 | event Reverting(string message); 36 | 37 | function testRevert() public { 38 | require(address(this) == address(0), "always fail"); 39 | emit Reverting("if you see this revert failed..."); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IERC20Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity >=0.7.6; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 8 | 9 | /** 10 | * @notice Extended ERC-20 token interface used internally in OpenGSN modules. 11 | * Renamed to avoid conflict with OZ namespace. Includes IERC20, ERC20Metadata. 12 | * added semi-standard "wrapped eth" access methods deposit() and "withdraw()" 13 | */ 14 | interface IERC20Token is IERC20, IERC20Metadata { 15 | 16 | function deposit() external payable; 17 | function withdraw(uint256 amount) external; 18 | } 19 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IERC2771Recipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.6.0; 3 | 4 | /** 5 | * @title The ERC-2771 Recipient Base Abstract Class - Declarations 6 | * 7 | * @notice A contract must implement this interface in order to support relayed transaction. 8 | * 9 | * @notice It is recommended that your contract inherits from the ERC2771Recipient contract. 10 | */ 11 | abstract contract IERC2771Recipient { 12 | 13 | /** 14 | * :warning: **Warning** :warning: The Forwarder can have a full control over your Recipient. Only trust verified Forwarder. 15 | * @param forwarder The address of the Forwarder contract that is being used. 16 | * @return isTrustedForwarder `true` if the Forwarder is trusted to forward relayed transactions by this Recipient. 17 | */ 18 | function isTrustedForwarder(address forwarder) public virtual view returns(bool); 19 | 20 | /** 21 | * @notice Use this method the contract anywhere instead of msg.sender to support relayed transactions. 22 | * @return sender The real sender of this call. 23 | * For a call that came through the Forwarder the real sender is extracted from the last 20 bytes of the `msg.data`. 24 | * Otherwise simply returns `msg.sender`. 25 | */ 26 | function _msgSender() internal virtual view returns (address); 27 | 28 | /** 29 | * @notice Use this method in the contract instead of `msg.data` when difference matters (hashing, signature, etc.) 30 | * @return data The real `msg.data` of this call. 31 | * For a call that came through the Forwarder, the real sender address was appended as the last 20 bytes 32 | * of the `msg.data` - so this method will strip those 20 bytes off. 33 | * Otherwise (if the call was made directly and not through the forwarder) simply returns `msg.data`. 34 | */ 35 | function _msgData() internal virtual view returns (bytes calldata); 36 | } 37 | -------------------------------------------------------------------------------- /packages/contracts/src/test/PayableWithEmit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "@opengsn/contracts/src/ERC2771Recipient.sol"; 5 | 6 | //make sure that "payable" function that uses _msgSender() still works 7 | // (its not required to use _msgSender(), since the default function 8 | // will never be called through GSN, but still, if someone uses it, 9 | // it should work) 10 | contract PayableWithEmit is ERC2771Recipient { 11 | 12 | event Received(address sender, uint256 value, uint256 gasleft); 13 | 14 | receive () external payable { 15 | 16 | emit Received(_msgSender(), msg.value, gasleft()); 17 | } 18 | 19 | 20 | //helper: send value to another contract 21 | function doSend(address payable target) public payable { 22 | 23 | uint256 before = gasleft(); 24 | // solhint-disable-next-line check-send-result 25 | bool success = target.send(msg.value); 26 | uint256 gasAfter = gasleft(); 27 | emit GasUsed(before-gasAfter, success); 28 | } 29 | event GasUsed(uint256 gasUsed, bool success); 30 | } 31 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestArbSys.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "../arbitrum/ArbSys.sol"; 5 | 6 | /** 7 | * As there is no way to run Arbitrum chain locally, tests currently need to run on simple hardhat node. 8 | * If some behavior is needed from ArbSys, it has to be stubbed here. 9 | */ 10 | contract TestArbSys is ArbSys { 11 | 12 | /** 13 | * @notice Get Arbitrum block number (distinct from L1 block number; Arbitrum genesis block has block number 0) 14 | * @return block number as int 15 | */ 16 | function arbBlockNumber() external override view returns (uint256){ 17 | return block.number * 17; 18 | } 19 | 20 | function getStorageGasAvailable() external override view returns (uint256) { 21 | // we need some really large value as for gasleft but also one that does decrease on every call 22 | return gasleft() * 100; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestDecimalsToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TestDecimalsToken is ERC20 { 7 | 8 | constructor() ERC20("Test Token", "DEC") { 9 | mint(100 ether); 10 | } 11 | 12 | function mint(uint256 amount) public { 13 | _mint(msg.sender, amount); 14 | } 15 | 16 | uint8 private _decimals; 17 | 18 | function decimals() public view virtual override returns (uint8) { 19 | return _decimals; 20 | } 21 | 22 | function setDecimals(uint8 _dec) public { 23 | _decimals = _dec; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestGatewayForwarder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "../forwarder/Forwarder.sol"; 6 | 7 | contract TestGatewayForwarder is Forwarder { 8 | address public trustedRelayHub; 9 | 10 | function setTrustedRelayHub(address _trustedRelayHub) external { 11 | trustedRelayHub = _trustedRelayHub; 12 | } 13 | 14 | function _verifySig( 15 | ForwardRequest calldata req, 16 | bytes32 domainSeparator, 17 | bytes32 requestTypeHash, 18 | bytes calldata suffixData, 19 | bytes calldata sig) 20 | internal 21 | override 22 | view 23 | { 24 | // trustedRelayHub can only be called from a verified Gateway where the signatures are actually checked 25 | // note that if signature field is set, it will be verified in this Forwarder anyway 26 | if (msg.sender != trustedRelayHub || sig.length != 0) { 27 | super._verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestPaymasterEverythingAccepted.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "../forwarder/IForwarder.sol"; 6 | import "../BasePaymaster.sol"; 7 | 8 | contract TestPaymasterEverythingAccepted is BasePaymaster { 9 | 10 | function versionPaymaster() external view override virtual returns (string memory){ 11 | return "3.0.0-beta.3+opengsn.test-pea.ipaymaster"; 12 | } 13 | 14 | event SampleRecipientPreCall(); 15 | event SampleRecipientPostCall(bool success, uint256 actualCharge); 16 | 17 | // solhint-disable-next-line no-empty-blocks 18 | function _verifyValue(GsnTypes.RelayRequest calldata) internal override view{} 19 | 20 | function _preRelayedCall( 21 | GsnTypes.RelayRequest calldata relayRequest, 22 | bytes calldata signature, 23 | bytes calldata approvalData, 24 | uint256 maxPossibleGas 25 | ) 26 | internal 27 | override 28 | virtual 29 | returns (bytes memory, bool) { 30 | (relayRequest, signature); 31 | (approvalData, maxPossibleGas); 32 | emit SampleRecipientPreCall(); 33 | return ("no revert here",false); 34 | } 35 | 36 | function _postRelayedCall( 37 | bytes calldata context, 38 | bool success, 39 | uint256 gasUseWithoutPost, 40 | GsnTypes.RelayData calldata relayData 41 | ) 42 | internal 43 | override 44 | virtual 45 | { 46 | (context, gasUseWithoutPost, relayData); 47 | emit SampleRecipientPostCall(success, gasUseWithoutPost); 48 | } 49 | 50 | function deposit() public payable { 51 | require(address(relayHub) != address(0), "relay hub address not set"); 52 | relayHub.depositFor{value:msg.value}(address(this)); 53 | } 54 | 55 | function withdrawAll(address payable destination) public { 56 | uint256 amount = relayHub.balanceOf(address(this)); 57 | withdrawRelayHubDepositTo(amount, destination); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestPaymasterOwnerSignature.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | 7 | import "./TestPaymasterEverythingAccepted.sol"; 8 | 9 | contract TestPaymasterOwnerSignature is TestPaymasterEverythingAccepted { 10 | using ECDSA for bytes32; 11 | 12 | /** 13 | * @notice This demonstrates how dapps can provide an off-chain signatures to relayed transactions. 14 | */ 15 | function _preRelayedCall( 16 | GsnTypes.RelayRequest calldata relayRequest, 17 | bytes calldata signature, 18 | bytes calldata approvalData, 19 | uint256 maxPossibleGas 20 | ) 21 | internal 22 | view 23 | override 24 | returns (bytes memory, bool) { 25 | (signature, maxPossibleGas); 26 | 27 | address signer = 28 | keccak256(abi.encodePacked("I approve", relayRequest.request.from)) 29 | .toEthSignedMessageHash() 30 | .recover(approvalData); 31 | require(signer == owner(), "test: not approved"); 32 | return ("",false); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestPaymasterPreconfiguredApproval.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "./TestPaymasterEverythingAccepted.sol"; 6 | 7 | contract TestPaymasterPreconfiguredApproval is TestPaymasterEverythingAccepted { 8 | 9 | bytes public expectedApprovalData; 10 | 11 | function setExpectedApprovalData(bytes memory val) public { 12 | expectedApprovalData = val; 13 | } 14 | 15 | function _verifyApprovalData(bytes calldata approvalData) internal override view { 16 | require(keccak256(expectedApprovalData) == keccak256(approvalData), 17 | string(abi.encodePacked( 18 | "test: unexpected approvalData: '", approvalData, "' instead of '", expectedApprovalData, "'"))); 19 | } 20 | 21 | function _preRelayedCall( 22 | GsnTypes.RelayRequest calldata relayRequest, 23 | bytes calldata signature, 24 | bytes calldata approvalData, 25 | uint256 maxPossibleGas 26 | ) 27 | internal 28 | view 29 | override 30 | returns (bytes memory, bool) { 31 | (relayRequest, signature, approvalData, maxPossibleGas); 32 | return ("",false); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestPaymasterStoreContext.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "./TestPaymasterEverythingAccepted.sol"; 6 | 7 | contract TestPaymasterStoreContext is TestPaymasterEverythingAccepted { 8 | 9 | event SampleRecipientPreCallWithValues( 10 | address relay, 11 | address from, 12 | bytes encodedFunction, 13 | uint256 baseRelayFee, 14 | uint256 gasLimit, 15 | bytes approvalData, 16 | uint256 maxPossibleGas 17 | ); 18 | 19 | event SampleRecipientPostCallWithValues( 20 | string context 21 | ); 22 | 23 | /** 24 | * This demonstrates how preRelayedCall can return 'context' data for reuse in postRelayedCall. 25 | */ 26 | function _preRelayedCall( 27 | GsnTypes.RelayRequest calldata relayRequest, 28 | bytes calldata signature, 29 | bytes calldata approvalData, 30 | uint256 maxPossibleGas 31 | ) 32 | internal 33 | override 34 | returns (bytes memory, bool) { 35 | (signature, approvalData, maxPossibleGas); 36 | 37 | emit SampleRecipientPreCallWithValues( 38 | relayRequest.relayData.relayWorker, 39 | relayRequest.request.from, 40 | relayRequest.request.data, 41 | relayRequest.relayData.maxFeePerGas, 42 | relayRequest.request.gas, 43 | approvalData, 44 | maxPossibleGas); 45 | return ("context passed from preRelayedCall to postRelayedCall",false); 46 | } 47 | 48 | function _postRelayedCall( 49 | bytes calldata context, 50 | bool success, 51 | uint256 gasUseWithoutPost, 52 | GsnTypes.RelayData calldata relayData 53 | ) 54 | internal 55 | override 56 | { 57 | (context, success, gasUseWithoutPost, relayData); 58 | emit SampleRecipientPostCallWithValues(string(context)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestPaymasterVariableGasLimits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "./TestPaymasterEverythingAccepted.sol"; 6 | 7 | contract TestPaymasterVariableGasLimits is TestPaymasterEverythingAccepted { 8 | 9 | string public override versionPaymaster = "3.0.0-beta.3+opengsn.test-vgl.ipaymaster"; 10 | 11 | event SampleRecipientPreCallWithValues( 12 | uint256 gasleft, 13 | uint256 maxPossibleGas 14 | ); 15 | 16 | event SampleRecipientPostCallWithValues( 17 | uint256 gasleft, 18 | uint256 gasUseWithoutPost 19 | ); 20 | 21 | function _preRelayedCall( 22 | GsnTypes.RelayRequest calldata relayRequest, 23 | bytes calldata signature, 24 | bytes calldata approvalData, 25 | uint256 maxPossibleGas 26 | ) 27 | internal 28 | override 29 | returns (bytes memory, bool) { 30 | (relayRequest, signature, approvalData); 31 | emit SampleRecipientPreCallWithValues( 32 | gasleft(), maxPossibleGas); 33 | return ("", false); 34 | } 35 | 36 | function _postRelayedCall( 37 | bytes calldata context, 38 | bool success, 39 | uint256 gasUseWithoutPost, 40 | GsnTypes.RelayData calldata relayData 41 | ) 42 | internal 43 | override 44 | { 45 | (context, success, gasUseWithoutPost, relayData); 46 | emit SampleRecipientPostCallWithValues(gasleft(), gasUseWithoutPost); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestRecipientWithoutFallback.sol: -------------------------------------------------------------------------------- 1 | /* solhint-disable avoid-tx-origin */ 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | pragma solidity ^0.8.0; 4 | 5 | import "../utils/GsnUtils.sol"; 6 | import "../ERC2771Recipient.sol"; 7 | import "./TestPaymasterConfigurableMisbehavior.sol"; 8 | 9 | contract TestRecipientWithoutFallback is ERC2771Recipient { 10 | 11 | constructor(address forwarder) { 12 | _setTrustedForwarder(forwarder); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestRelayHub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "../RelayHub.sol"; 5 | 6 | contract TestRelayHub is RelayHub { 7 | 8 | constructor( 9 | IStakeManager _stakeManager, 10 | address _penalizer, 11 | address _batchGateway, 12 | address _relayRegistrar, 13 | RelayHubConfig memory _config 14 | // solhint-disable-next-line no-empty-blocks 15 | ) RelayHub(_stakeManager, _penalizer, _batchGateway, _relayRegistrar, _config) {} 16 | 17 | /// Allow depositing for non-paymaster addresses for Gas Calculations tests 18 | function depositFor(address target) public override payable { 19 | uint256 amount = msg.value; 20 | balances[target] = balances[target] + amount; 21 | emit Deposited(target, msg.sender, amount); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestRelayHubForRegistrar.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "../RelayHub.sol"; 5 | 6 | contract TestRelayHubForRegistrar { 7 | mapping(address => bool) public isStaked; 8 | 9 | function setRelayManagerStaked(address relayManager, bool _isStaked) external { 10 | isStaked[relayManager] = _isStaked; 11 | } 12 | 13 | function verifyCanRegister(address relayManager) external view { 14 | require(isStaked[relayManager], "verifyCanRegister: cannot"); 15 | } 16 | 17 | function verifyRelayManagerStaked(address relayManager) external view { 18 | require(isStaked[relayManager], "verifyRelayManagerStaked: is not"); 19 | } 20 | 21 | function onRelayServerRegistered(address relayManager) external view { 22 | require(isStaked[relayManager], "onRelayServerRegistered no stake"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestRelayHubValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "../utils/RelayHubValidator.sol"; 6 | 7 | contract TestRelayHubValidator { 8 | 9 | //for testing purposes, we must be called from a method with same param signature as RelayCall 10 | function dummyRelayCall( 11 | string calldata domainSeparatorName, 12 | uint256, //paymasterMaxAcceptanceBudget, 13 | GsnTypes.RelayRequest calldata relayRequest, 14 | bytes calldata signature, 15 | bytes calldata approvalData 16 | ) external { 17 | RelayHubValidator.verifyTransactionPacking(domainSeparatorName, relayRequest, signature, approvalData); 18 | } 19 | 20 | // helper method for verifyTransactionPacking 21 | function dynamicParamSize(bytes calldata buf) external pure returns (uint256) { 22 | return RelayHubValidator.dynamicParamSize(buf); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestRelayWorkerContract.sol: -------------------------------------------------------------------------------- 1 | /* solhint-disable avoid-tx-origin */ 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | pragma solidity ^0.8.0; 4 | pragma abicoder v2; 5 | 6 | import "../interfaces/IRelayHub.sol"; 7 | 8 | contract TestRelayWorkerContract { 9 | 10 | function relayCall( 11 | IRelayHub hub, 12 | uint256 maxAcceptanceBudget, 13 | GsnTypes.RelayRequest memory relayRequest, 14 | bytes memory signature) 15 | public 16 | { 17 | hub.relayCall("GSN Relayed Transaction", maxAcceptanceBudget, relayRequest, signature, ""); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TestToken is ERC20 { 7 | 8 | constructor() ERC20("Test Token", "TOK") { 9 | mint(100 ether); 10 | } 11 | 12 | function mint(uint256 amount) public { 13 | _mint(msg.sender, amount); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestUtil.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | 7 | import "../utils/GsnTypes.sol"; 8 | import "../utils/GsnEip712Library.sol"; 9 | import "../utils/GsnUtils.sol"; 10 | 11 | contract TestUtil { 12 | using ECDSA for bytes; 13 | using ECDSA for bytes32; 14 | 15 | function libRelayRequestName() public pure returns (string memory) { 16 | return GsnEip712Library.RELAY_REQUEST_NAME; 17 | } 18 | 19 | function libRelayRequestType() public pure returns (string memory) { 20 | return string(GsnEip712Library.RELAY_REQUEST_TYPE); 21 | } 22 | 23 | function libRelayRequestTypeHash() public pure returns (bytes32) { 24 | return GsnEip712Library.RELAY_REQUEST_TYPEHASH; 25 | } 26 | 27 | function libRelayRequestSuffix() public pure returns (string memory) { 28 | return GsnEip712Library.RELAY_REQUEST_SUFFIX; 29 | } 30 | 31 | //helpers for test to call the library funcs: 32 | function callForwarderVerify( 33 | GsnTypes.RelayRequest calldata relayRequest, 34 | bytes calldata signature 35 | ) 36 | external 37 | view { 38 | GsnEip712Library.verify("GSN Relayed Transaction", relayRequest, signature); 39 | } 40 | 41 | function callForwarderVerifyAndCall( 42 | GsnTypes.RelayRequest calldata relayRequest, 43 | bytes calldata signature 44 | ) 45 | external 46 | returns ( 47 | bool success, 48 | bytes memory ret 49 | ) { 50 | bool forwarderSuccess; 51 | (forwarderSuccess, success, ret) = GsnEip712Library.execute("GSN Relayed Transaction", relayRequest, signature); 52 | if (!forwarderSuccess) { 53 | GsnUtils.revertWithData(ret); 54 | } 55 | emit Called(success, success == false ? ret : bytes("")); 56 | } 57 | 58 | event Called(bool success, bytes error); 59 | 60 | function splitRequest( 61 | GsnTypes.RelayRequest calldata relayRequest 62 | ) 63 | external 64 | pure 65 | returns ( 66 | bytes32 typeHash, 67 | bytes memory suffixData 68 | ) { 69 | (suffixData) = GsnEip712Library.splitRequest(relayRequest); 70 | typeHash = GsnEip712Library.RELAY_REQUEST_TYPEHASH; 71 | } 72 | 73 | function libDomainSeparator(address forwarder) public view returns (bytes32) { 74 | return GsnEip712Library.domainSeparator("GSN Relayed Transaction", forwarder); 75 | } 76 | 77 | function libGetChainID() public view returns (uint256) { 78 | return GsnEip712Library.getChainID(); 79 | } 80 | 81 | function _ecrecover(string memory message, bytes memory signature) public pure returns (address) { 82 | return bytes(message).toEthSignedMessageHash().recover(signature); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/contracts/src/test/TestWrappedNativeToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "../interfaces/IERC20Token.sol"; 6 | 7 | /** 8 | * @notice minimal "wrapped eth" implementation. 9 | */ 10 | contract TestWrappedNativeToken is ERC20, IERC20Token { 11 | 12 | // solhint-disable-next-line no-empty-blocks 13 | constructor() ERC20("Wrapped Native Token", "wnTok") { 14 | } 15 | 16 | receive() external payable { 17 | deposit(); 18 | } 19 | 20 | function deposit() public override payable { 21 | _mint(msg.sender, msg.value); 22 | } 23 | 24 | function withdraw(uint256 amount) public override { 25 | _burn(msg.sender, amount); 26 | // solhint-disable-next-line avoid-low-level-calls 27 | (bool success,) = msg.sender.call{value:amount}(""); 28 | require(success, "transfer failed"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/GsnTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "../forwarder/IForwarder.sol"; 5 | 6 | interface GsnTypes { 7 | /// @notice maxFeePerGas, maxPriorityFeePerGas, pctRelayFee and baseRelayFee must be validated inside of the paymaster's preRelayedCall in order not to overpay 8 | struct RelayData { 9 | uint256 maxFeePerGas; 10 | uint256 maxPriorityFeePerGas; 11 | uint256 transactionCalldataGasUsed; 12 | address relayWorker; 13 | address paymaster; 14 | address forwarder; 15 | bytes paymasterData; 16 | uint256 clientId; 17 | } 18 | 19 | //note: must start with the ForwardRequest to be an extension of the generic forwarder 20 | struct RelayRequest { 21 | IForwarder.ForwardRequest request; 22 | RelayData relayData; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/GsnUtils.sol: -------------------------------------------------------------------------------- 1 | /* solhint-disable no-inline-assembly */ 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | pragma solidity ^0.8.0; 4 | 5 | import "../utils/MinLibBytes.sol"; 6 | import "./GsnTypes.sol"; 7 | 8 | /** 9 | * @title The GSN Solidity Utils Library 10 | * @notice Some library functions used throughout the GSN Solidity codebase. 11 | */ 12 | library GsnUtils { 13 | 14 | bytes32 constant private RELAY_REQUEST_ID_MASK = 0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; 15 | 16 | /** 17 | * @notice Calculate an identifier for the meta-transaction in a format similar to a transaction hash. 18 | * Note that uniqueness relies on signature and may not be enforced if meta-transactions are verified 19 | * with a different algorithm, e.g. when batching. 20 | * @param relayRequest The `RelayRequest` for which an ID is being calculated. 21 | * @param signature The signature for the `RelayRequest`. It is not validated here and may even remain empty. 22 | */ 23 | function getRelayRequestID(GsnTypes.RelayRequest calldata relayRequest, bytes calldata signature) 24 | internal 25 | pure 26 | returns (bytes32) { 27 | return keccak256(abi.encode(relayRequest.request.from, relayRequest.request.nonce, signature)) & RELAY_REQUEST_ID_MASK; 28 | } 29 | 30 | /** 31 | * @notice Extract the method identifier signature from the encoded function call. 32 | */ 33 | function getMethodSig(bytes memory msgData) internal pure returns (bytes4) { 34 | return MinLibBytes.readBytes4(msgData, 0); 35 | } 36 | 37 | /** 38 | * @notice Extract a parameter from encoded-function block. 39 | * see: https://solidity.readthedocs.io/en/develop/abi-spec.html#formal-specification-of-the-encoding 40 | * The return value should be casted to the right type (`uintXXX`/`bytesXXX`/`address`/`bool`/`enum`). 41 | * @param msgData Byte array containing a uint256 value. 42 | * @param index Index in byte array of uint256 value. 43 | * @return result uint256 value from byte array. 44 | */ 45 | function getParam(bytes memory msgData, uint256 index) internal pure returns (uint256 result) { 46 | return MinLibBytes.readUint256(msgData, 4 + index * 32); 47 | } 48 | 49 | /// @notice Re-throw revert with the same revert data. 50 | function revertWithData(bytes memory data) internal pure { 51 | assembly { 52 | revert(add(data,32), mload(data)) 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/RelayHubValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | pragma abicoder v2; 4 | 5 | import "../utils/GsnTypes.sol"; 6 | 7 | /** 8 | * @title The RelayHub Validator Library 9 | * @notice Validates the `msg.data` received by the `RelayHub` does not contain unnecessary bytes. 10 | * Including these extra bytes would allow the Relay Server to inflate transaction costs and overcharge the client. 11 | */ 12 | library RelayHubValidator { 13 | 14 | /// @notice Validate that encoded `relayCall` is properly packed without any extra bytes 15 | function verifyTransactionPacking( 16 | string calldata domainSeparatorName, 17 | GsnTypes.RelayRequest calldata relayRequest, 18 | bytes calldata signature, 19 | bytes calldata approvalData 20 | ) internal pure { 21 | // abicoder v2: https://docs.soliditylang.org/en/latest/abi-spec.html 22 | // each static param/member is 1 word 23 | // struct (with dynamic members) has offset to struct which is 1 word 24 | // dynamic member is 1 word offset to actual value, which is 1-word length and ceil(length/32) words for data 25 | // relayCall has 5 method params, 26 | // relayRequest: 2 members 27 | // relayData 8 members 28 | // ForwardRequest: 7 members 29 | // total 21 32-byte words if all dynamic params are zero-length. 30 | uint256 expectedMsgDataLen = 4 + 22 * 32 + 31 | dynamicParamSize(bytes(domainSeparatorName)) + 32 | dynamicParamSize(signature) + 33 | dynamicParamSize(approvalData) + 34 | dynamicParamSize(relayRequest.request.data) + 35 | dynamicParamSize(relayRequest.relayData.paymasterData); 36 | // zero-length signature is allowed in a batch relay transaction 37 | require(expectedMsgDataLen == msg.data.length, "extra msg.data bytes" ); 38 | } 39 | 40 | // helper method for verifyTransactionPacking: 41 | // size (in bytes) of the given "bytes" parameter. size include the length (32-byte word), 42 | // and actual data size, rounded up to full 32-byte words 43 | function dynamicParamSize(bytes calldata buf) internal pure returns (uint256) { 44 | return 32 + ((buf.length + 31) & (type(uint256).max - 31)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/contracts/truffle.js: -------------------------------------------------------------------------------- 1 | require('ts-node/register/transpile-only') 2 | 3 | function wrapfunc(obj,func) { 4 | const origfunc = obj[func] 5 | obj[func] = function (...args) { 6 | origfunc(...args.map(str => typeof (str) === 'string' ? str.replace(/\/solpp\//g, '/src/') : str)) 7 | } 8 | } 9 | 10 | // make sure compiler errors are on the source file, not solpp-output 11 | wrapfunc(console, 'log') 12 | 13 | module.exports = { 14 | // CLI package needs to deploy contracts from JSON artifacts 15 | contracts_build_directory: '../cli/src/compiled', 16 | contracts_directory: './solpp', 17 | compilers: { 18 | solc: { 19 | version: '0.8.7', 20 | settings: { 21 | evmVersion: 'london', 22 | optimizer: { 23 | enabled: true, 24 | runs: 200 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "types/**/*.ts", 5 | ], 6 | "compilerOptions": { 7 | "rootDir": ".", 8 | "outDir": "dist" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/deployer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/deployer", 3 | "version": "3.0.0-beta.10", 4 | "private": true, 5 | "author": "Dror Tirosh", 6 | "license": "MIT", 7 | "scripts": { 8 | "deploy": "yarn --cwd ../dev solpp; hardhat deploy", 9 | "export": "hardhat export", 10 | "verify": "hardhat etherscan-verify --force-license --license GPL-3.0", 11 | "depinfo": "hardhat run ./src/depinfo.ts", 12 | "applyConfig": "hardhat run ./src/applyConfig.ts", 13 | "lint": "eslint -f unix .", 14 | "lint-fix": "yarn lint --fix", 15 | "test-only": "hardhat test --network npmtest" 16 | }, 17 | "dependencies": { 18 | "@nomicfoundation/hardhat-ethers": "^3.0.4", 19 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", 20 | "@nomiclabs/hardhat-etherscan": "^2.1.8", 21 | "@nomiclabs/hardhat-web3": "^2.0.0", 22 | "@opengsn/common": "^3.0.0-beta.10", 23 | "@opengsn/provider": "^3.0.0-beta.10", 24 | "axios": "^0.27.2", 25 | "chai": "^4.3.6", 26 | "chalk": "^4.1.2", 27 | "ethers": "^6.6.5", 28 | "hardhat": "^2.17.0", 29 | "hardhat-deploy": "^0.11.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/deployer/src/applyConfig.ts: -------------------------------------------------------------------------------- 1 | import { applyDeploymentConfig } from './deployUtils' 2 | import hre from 'hardhat' 3 | 4 | (async () => { 5 | await applyDeploymentConfig(hre) 6 | })() 7 | .catch(e => { console.log(e) }) 8 | .finally(process.exit) 9 | -------------------------------------------------------------------------------- /packages/deployer/src/depinfo.ts: -------------------------------------------------------------------------------- 1 | import { printRelayInfo } from './deployUtils' 2 | import hre from 'hardhat' 3 | 4 | (async () => { 5 | const deployments = await hre.deployments.all() 6 | console.log('Deployed Contracts:') 7 | const addresses = Object.keys(deployments).reduce((set, key) => ({ ...set, [key]: deployments[key].address }), {}) 8 | console.log(addresses) 9 | 10 | await printRelayInfo(hre) 11 | })() 12 | .catch(e => { console.log(e) }) 13 | .finally(process.exit) 14 | -------------------------------------------------------------------------------- /packages/deployer/src/exportTask.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config' 2 | import axios from 'axios' 3 | import fs from 'fs' 4 | 5 | const defaultChainListUrl = 'https://chainid.network/chains.json' 6 | const tmpExportFile = '/tmp/export-all.json' 7 | const defaultExportFile = 'deployments/gsn-networks.json' 8 | 9 | task('export', 'Export all GSN-deployed networks') 10 | .addOptionalParam('chainList', 'Url to fetch chainlist', defaultChainListUrl) 11 | .setAction(async (args, env, runSuper) => { 12 | if (args.export != null) { 13 | throw new Error('only supports --export-all') 14 | } 15 | const exportFile = args.exportAll ?? defaultExportFile 16 | const chainListUrl: string = args.chainList ?? defaultChainListUrl 17 | await runSuper({ exportAll: tmpExportFile }) 18 | console.debug('Fetching global chain list from', chainListUrl) 19 | const chainsResult = await axios.get(args.chainList).catch(e => { 20 | throw new Error(e.response.statusText) 21 | }) 22 | if (chainsResult.data == null || !Array.isArray(chainsResult.data)) { 23 | throw new Error(`failed to get chainlist from ${chainListUrl}`) 24 | } 25 | // chainResult is an array. convert into a map: 26 | const globalChainList = chainsResult.data.reduce((set: any, chainInfo: any) => ({ 27 | ...set, 28 | [chainInfo.chainId]: chainInfo 29 | }), {}) 30 | const exportNetworks = require(tmpExportFile) 31 | // export is an hash of arrays { 3: [ { chainId: 3, ... } ] } 32 | const networks = Object.keys(exportNetworks).reduce((set, chainId) => { 33 | const globalChainInfo = globalChainList[chainId] 34 | if (globalChainInfo == null) { 35 | throw new Error(`Chain ${chainId} not found in ${chainListUrl}`) 36 | } 37 | const chainArray = exportNetworks[chainId].map((chain: any) => { 38 | const ret = { 39 | title: globalChainInfo.name, 40 | symbol: globalChainInfo.nativeCurrency?.symbol, 41 | explorer: globalChainInfo.explorers?.[0].url, 42 | ...chain 43 | } 44 | for (const contract of Object.values(ret.contracts)) { 45 | delete (contract as any).abi 46 | } 47 | return ret 48 | }) 49 | return { 50 | ...set, 51 | [chainId]: chainArray 52 | } 53 | }, {}) 54 | fs.writeFileSync(exportFile, JSON.stringify(networks, null, 2)) 55 | console.log('exported all networks to', exportFile) 56 | }) 57 | -------------------------------------------------------------------------------- /packages/deployer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "deploy/*.ts", 5 | "deployments/*.ts", 6 | "src/**.ts", 7 | "deployment-config.ts", 8 | "hardhat.config.ts", 9 | "../contracts/types/truffle-contracts/index.d.ts", 10 | "../contracts/types/truffle-contracts/types.d.ts" 11 | ], 12 | "compilerOptions": { 13 | "outDir": "dist" 14 | } 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/deployer/tsconfig.packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "truffle.js", 5 | "coverage-prov.js", 6 | "migrations/*.js", 7 | "test/**/*.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/dev/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@openzeppelin/contracts", "hardhat", "truffle-plugin-stdjsonin", "web3-eth-accounts", "@typechain/truffle-v5"] 2 | -------------------------------------------------------------------------------- /packages/dev/README.md: -------------------------------------------------------------------------------- 1 | # GSN developers utils 2 | 3 | [GSN, the Ethereum Gas Station Network](https://opengsn.org/) abstracts away gas to minimize onboarding & UX friction for dapps. 4 | 5 | This module contains `GsnTestEnvironment` that should help integrate GSN with testing frameworks such as Truffle. 6 | 7 | The tutorial will be available soon. 8 | -------------------------------------------------------------------------------- /packages/dev/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require('./Migrations.sol') 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations) 5 | } 6 | -------------------------------------------------------------------------------- /packages/dev/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const RelayHub = artifacts.require('RelayHub') 2 | const StakeManager = artifacts.require('StakeManager') 3 | const Penalizer = artifacts.require('Penalizer') 4 | const SampleRecipient = artifacts.require('TestRecipient') 5 | const Forwarder = artifacts.require('Forwarder') 6 | 7 | module.exports = async function (deployer) { 8 | await deployer.deploy(StakeManager, 30000, 0, 0, '0x0000000000000000000000000000000000000001', '0x0000000000000000000000000000000000000001') 9 | await deployer.deploy(Penalizer, 0, 0) 10 | await deployer.deploy( 11 | RelayHub, 12 | StakeManager.address, 13 | Penalizer.address, 14 | '0x0000000000000000000000000000000000000000', 15 | '0x0000000000000000000000000000000000000000', 16 | [0, 0, 0, 0, 0, '0x0000000000000000000000000000000000000000', 0, 0, 0 17 | ]) 18 | await deployer.deploy(Forwarder) 19 | await deployer.deploy(SampleRecipient, Forwarder.address) 20 | } 21 | -------------------------------------------------------------------------------- /packages/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/dev", 3 | "version": "3.0.0-beta.10", 4 | "license": "GPL-3.0-only", 5 | "main": "dist/src/index.js", 6 | "files": [ 7 | "dist/src/*", 8 | "README.md" 9 | ], 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "scripts": { 14 | "tsc": "tsc && cp -R types dist", 15 | "watch-tsc": "tsc -w --preserveWatchOutput", 16 | "lint": "eslint -f unix .", 17 | "lint-fix": "yarn lint --fix", 18 | "solpp": "yarn --cwd=\"../..\" solpp", 19 | "truffle-console": "truffle console", 20 | "truffle-compile": "truffle compile", 21 | "test-local": "ENABLE_CONSOLE_LOG=1 yarn solpp && npx truffle test --compile-all", 22 | "typechain-generate-truffle": "yarn truffle-compile && typechain --target truffle-v5 '../cli/src/compiled/*.json' && ./replaceWeb3.sh", 23 | "gas-calculations": "ENABLE_CONSOLE_LOG=0 yarn solpp && GAS_CALCULATIONS=1 truffle test test/RelayHubGasCalculations.test.ts test/PaymasterCommitment.test.ts --compile-all --network npmtest", 24 | "test-webpack": "TEST_WEBPACK=1 truffle test test/RelayServer.webpack.test.ts --network npmtest", 25 | "test-only": "truffle test --network npmtest", 26 | "rm-dist": "rm -rf tsconfig.tsbuildinfo dist build" 27 | }, 28 | "dependencies": { 29 | "@opengsn/cli": "^3.0.0-beta.10", 30 | "@opengsn/common": "^3.0.0-beta.10", 31 | "@opengsn/logger": "^3.0.0-beta.10", 32 | "@opengsn/provider": "^3.0.0-beta.10", 33 | "@opengsn/relay": "^3.0.0-beta.10" 34 | }, 35 | "devDependencies": { 36 | "@ethereumjs/common": "^2.6.5", 37 | "@ethereumjs/tx": "^3.2.0", 38 | "@ethersproject/bignumber": "^5.7.0", 39 | "@ethersproject/providers": "^5.7.2", 40 | "@ethersproject/transactions": "^5.7.0", 41 | "@metamask/eth-sig-util": "^5.1.0", 42 | "@openzeppelin/test-helpers": "^0.5.15", 43 | "@truffle/hdwallet-provider": "^2.0.11", 44 | "@typechain/truffle-v5": "^8.0.6", 45 | "@types/chai-as-promised": "^7.1.3", 46 | "@types/sinon": "^9.0.10", 47 | "@types/sinon-chai": "^3.2.5", 48 | "abi-decoder": "^2.3.0", 49 | "async-mutex": "^0.4.0", 50 | "axios": "^0.27.2", 51 | "bn.js": "^5.2.1", 52 | "body-parser": "^1.20.0", 53 | "chai": "^4.2.0", 54 | "chai-as-promised": "^7.1.1", 55 | "ethereumjs-util": "^7.1.0", 56 | "ethereumjs-wallet": "^1.0.2", 57 | "ethers": "^5.7.2", 58 | "ethers-v6": "npm:ethers@^6.6.5", 59 | "express": "^4.18.1", 60 | "hardhat": "^2.6.8", 61 | "rlp": "^3.0.0", 62 | "sinon": "^9.2.3", 63 | "sinon-chai": "^3.5.0", 64 | "truffle-plugin-stdjsonin": "github:mhrsalehi/truffle-plugin-stdjsonin", 65 | "ts-node": "8.6.2", 66 | "web3": "^1.7.4", 67 | "web3-core": "^1.7.4", 68 | "web3-eth-accounts": "^1.7.4", 69 | "web3-eth-contract": "^1.7.4", 70 | "web3-utils": "^1.7.4", 71 | "winston": "^3.8.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/dev/replaceWeb3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | find ./types/truffle-contracts -type f -name '*.d.ts' | xargs sed -i'' -e 's/web3-eth-contract/..\/Web3Types/g' 3 | -------------------------------------------------------------------------------- /packages/dev/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@opengsn/cli/dist/GsnTestEnvironment' 2 | -------------------------------------------------------------------------------- /packages/dev/test/HttpWrapper.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpWrapper } from '@opengsn/common' 2 | import chai from 'chai' 3 | import chaiAsPromised from 'chai-as-promised' 4 | 5 | const { expect, assert } = chai.use(chaiAsPromised) 6 | 7 | describe('HttpWrapper', () => { 8 | it('connect to node, get version', async () => { 9 | const http = new HttpWrapper() 10 | // @ts-ignore 11 | const url = web3.currentProvider.host 12 | const res = await http.sendPromise(url, { 13 | jsonrpc: '2.0', 14 | method: 'net_version', 15 | id: 123 16 | }) 17 | 18 | assert.equal(123, res.id, JSON.stringify(res)) // just verify its a valid response 19 | }) 20 | 21 | it('should fail on connection refused', async () => { 22 | const http = new HttpWrapper() 23 | const res = http.sendPromise(new URL('http://localhost:44321'), { 24 | jsonrpc: '2.0', 25 | method: 'net_version', 26 | id: 123 27 | }) 28 | // @ts-ignore 29 | await expect(res).to.be.eventually.rejectedWith({ error: 'connect ECONNREFUSED 127.0.0.1:44321' }) 30 | }) 31 | 32 | it('should pass timeout to provider', async () => { 33 | // This test should be removed. It checks axios functionality. 34 | const http = new HttpWrapper({ timeout: 1234 }) 35 | // @ts-ignore 36 | assert.equal(http.provider.defaults.timeout, 1234) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/dev/test/KeyManager.test.ts: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | import fs from 'fs' 4 | import { KeyManager, KEYSTORE_FILENAME } from '@opengsn/relay/dist/KeyManager' 5 | 6 | // NOTICE: this dir is removed in 'after', do not use this in any other test 7 | const workdir = '/tmp/gsn/test/key_manager' 8 | const keyStoreFilePath = workdir + '/' + KEYSTORE_FILENAME 9 | 10 | function cleanFolder (): void { 11 | if (fs.existsSync(keyStoreFilePath)) { 12 | fs.unlinkSync(keyStoreFilePath) 13 | } 14 | if (fs.existsSync(workdir)) { 15 | // @ts-ignore 16 | fs.rmSync(workdir, { 17 | recursive: true, 18 | force: true 19 | }) 20 | } 21 | } 22 | 23 | contract('KeyManager', function (accounts) { 24 | describe('in-memory', () => { 25 | let mkm: KeyManager 26 | 27 | before(() => { 28 | mkm = new KeyManager(10, undefined, 'seed1234') 29 | }) 30 | it('should return key', () => { 31 | // for a given seed, the addresses and privkeys are known.. 32 | const k0 = mkm.getAddress(0) 33 | // @ts-ignore 34 | assert.deepEqual(mkm._privateKeys[k0].toString('hex'), 35 | '98bd175008b68dfd5a6aca0584d5a040032f2469656569d5d428161b776d27ff') 36 | assert.equal(k0, '0x56558253d657baa29cfe9f0a808b7d19d5d80b9c') 37 | }) 38 | it('should return another key for different index', () => { 39 | const k1 = mkm.getAddress(1) 40 | // @ts-ignore 41 | assert.equal(mkm._privateKeys[k1].toString('hex'), 42 | 'e52f32d373b0b38be3800ec9070af883a63c4fd2857c5b0f249180a2c303eb7e') 43 | assert.equal(k1, '0xe2ceef58b3e5a8816c52b00067830b8e1afd82da') 44 | }) 45 | }) 46 | describe('file-based KeyManager', () => { 47 | let fkmA: KeyManager 48 | 49 | before('create key manager', function () { 50 | cleanFolder() 51 | fkmA = new KeyManager(20, workdir) 52 | assert.isTrue(fs.existsSync(workdir), 'test keystore dir should exist already') 53 | }) 54 | 55 | it('should get key pair', function () { 56 | const key = fkmA.getAddress(1) 57 | assert.isTrue(web3.utils.isAddress(key)) 58 | // @ts-ignore 59 | assert.equal(fkmA._privateKeys[key].length, 32) 60 | }) 61 | 62 | it('should get the same key when reloading', () => { 63 | const addrA = fkmA.getAddress(0) 64 | const addrA10 = fkmA.getAddress(10) 65 | const fkmB = new KeyManager(20, workdir) 66 | const addrB = fkmB.getAddress(0) 67 | assert.equal(addrA, addrB) 68 | // @ts-ignore 69 | assert.equal(fkmA._privateKeys[addrA].toString('hex'), fkmB._privateKeys[addrB].toString('hex')) 70 | 71 | const addrB10 = fkmB.getAddress(10) 72 | assert.equal(addrA10, addrB10) 73 | // @ts-ignore 74 | assert.equal(fkmA._privateKeys[addrA10].toString('hex'), fkmB._privateKeys[addrB10].toString('hex')) 75 | }) 76 | 77 | after('remove keystore', cleanFolder) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /packages/dev/test/RelayServer.webpack.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-global-assign */ 2 | 3 | import childProcess from 'child_process' 4 | import path from 'path' 5 | import fs from 'fs' 6 | 7 | const describeOrig = describe 8 | if (process.env.TEST_WEBPACK == null) { 9 | // @ts-ignore 10 | describe = describe.skip 11 | } 12 | 13 | describe('RelayServer-webpack', () => { 14 | let oneFileRelayer: string 15 | before('create webpack', function () { 16 | this.timeout(15000) 17 | const jsrelayDir = path.join(__dirname, '../../../dockers', 'jsrelay') 18 | // @ts-ignore 19 | fs.rmSync(path.join(jsrelayDir, 'dist'), { 20 | recursive: true, 21 | force: true 22 | }) 23 | childProcess.execSync('npx webpack', { cwd: jsrelayDir, stdio: 'inherit' }) 24 | oneFileRelayer = path.join(jsrelayDir, 'dist', 'relayserver.js') 25 | }) 26 | 27 | it('should launch (and instantly crash with some parameter missing) to verify it was packed correctly', function () { 28 | try { 29 | childProcess.execSync('node ' + oneFileRelayer, { encoding: 'ascii', stdio: 'pipe' }) 30 | assert.fail('should throw') 31 | } catch (e: any) { 32 | assert.match(e.message.toString(), /missing ethereumNodeUrl/) 33 | } 34 | }) 35 | }) 36 | 37 | // @ts-ignore 38 | describe = describeOrig 39 | -------------------------------------------------------------------------------- /packages/dev/test/common/TestCliUtils.ts: -------------------------------------------------------------------------------- 1 | import { getNetworkUrl } from '@opengsn/cli/dist/utils' 2 | 3 | describe('cli-utils', () => { 4 | describe('#getNetworkUrl', () => { 5 | it('should validate url', function () { 6 | assert.equal(getNetworkUrl('http://localhost:12345'), 'http://localhost:12345') 7 | assert.equal(getNetworkUrl('https://localhost:12345'), 'https://localhost:12345') 8 | }) 9 | it('should require INFURA_ID for valid networks', function () { 10 | expect(() => getNetworkUrl('kovan', {})).to.throw('INFURA_ID not set') 11 | assert.equal(getNetworkUrl('kovan', { INFURA_ID: '' }), 'https://kovan.infura.io/v3/') 12 | }) 13 | it('should reject invalid url', function () { 14 | expect(() => getNetworkUrl('asdasdas', {})).to.throw('network asdasdas is not supported') 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/dev/test/common/VersionManager.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-new */ 2 | import { VersionsManager } from '@opengsn/common' 3 | 4 | describe('VersionManager', function () { 5 | context('constructor', function () { 6 | it('should throw on invalid semver string', function () { 7 | expect(function () { 8 | new VersionsManager('v.1.0') 9 | }).to.throw('Component version is not valid') 10 | }) 11 | it('should not throw on valid semver string', function () { 12 | new VersionsManager('2.0.0-beta.1+opengsn.something') 13 | }) 14 | it('target version with zero patch', function () { 15 | assert.equal(new VersionsManager('2.3.4+opengsn.something').requiredVersionRange, '^2.3.0') 16 | }) 17 | it('target beta version with zero patch', function () { 18 | assert.equal(new VersionsManager('2.3.4-beta.5+opengsn.something').requiredVersionRange, '^2.3.0-beta.5') 19 | }) 20 | }) 21 | 22 | context('#isMinorSameOrNewer()', function () { 23 | const manager = new VersionsManager('1.2.3') 24 | it('should ignore patch level', function () { 25 | assert.isTrue(manager.isRequiredVersionSatisfied('1.2.2')) 26 | assert.isTrue(manager.isRequiredVersionSatisfied('1.2.3')) 27 | assert.isTrue(manager.isRequiredVersionSatisfied('1.2.4')) 28 | }) 29 | 30 | it('should require minor same or equal', function () { 31 | assert.isTrue(manager.isRequiredVersionSatisfied('1.3.0')) 32 | assert.isFalse(manager.isRequiredVersionSatisfied('1.1.3')) 33 | }) 34 | 35 | it('should require exact same major', function () { 36 | assert.isFalse(manager.isRequiredVersionSatisfied('0.2.3')) 37 | assert.isFalse(manager.isRequiredVersionSatisfied('3.2.3')) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/dev/test/dummies/BadContractInteractor.ts: -------------------------------------------------------------------------------- 1 | import { type BigNumber } from '@ethersproject/bignumber' 2 | import { type TransactionResponse } from '@ethersproject/providers' 3 | import { ContractInteractor, type ConstructorParams, type RelayCallABI } from '@opengsn/common' 4 | 5 | export class BadContractInteractor extends ContractInteractor { 6 | static readonly message = 'This is not the contract you are looking for' 7 | static readonly wrongNonceMessage = 'the tx doesn\'t have the correct nonce' 8 | 9 | private readonly failValidateARC: boolean 10 | 11 | constructor (constructorParams: ConstructorParams, failValidateARC: boolean) { 12 | super(constructorParams) 13 | this.failValidateARC = failValidateARC 14 | } 15 | 16 | async validateRelayCall (relayCallABIData: RelayCallABI, viewCallGasLimit: BigNumber, isDryRun: boolean): Promise<{ paymasterAccepted: boolean, returnValue: string, relayHubReverted: boolean, recipientReverted: boolean }> { 17 | if (this.failValidateARC) { 18 | return { 19 | paymasterAccepted: false, 20 | relayHubReverted: true, 21 | recipientReverted: false, 22 | returnValue: BadContractInteractor.message 23 | } 24 | } 25 | return await super.validateRelayCall(relayCallABIData, viewCallGasLimit, isDryRun) 26 | } 27 | 28 | // eslint-disable-next-line @typescript-eslint/require-await 29 | async sendSignedTransaction (rawTx: string): Promise { 30 | throw new Error(BadContractInteractor.wrongNonceMessage) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/dev/test/dummies/BadHttpClient.ts: -------------------------------------------------------------------------------- 1 | import { type PrefixedHexString } from 'ethereumjs-util' 2 | import { HttpClient, HttpWrapper, type PingResponse, type RelayTransactionRequest, type LoggerInterface, type ObjectMap } from '@opengsn/common' 3 | 4 | export class BadHttpClient extends HttpClient { 5 | static readonly message = 'This is not the relay you are looking for' 6 | 7 | private readonly failRelay: boolean 8 | private readonly failPing: boolean 9 | private readonly timeoutRelay: boolean 10 | private readonly stubRelay: string | undefined 11 | private readonly stubPing: PingResponse | undefined 12 | 13 | constructor (logger: LoggerInterface, failPing: boolean, failRelay: boolean, timeoutRelay: boolean, stubPing?: PingResponse, stubRelay?: string) { 14 | super(new HttpWrapper(), logger) 15 | this.failPing = failPing 16 | this.failRelay = failRelay 17 | this.timeoutRelay = timeoutRelay 18 | this.stubRelay = stubRelay 19 | this.stubPing = stubPing 20 | } 21 | 22 | async getPingResponse (relayUrl: string, paymaster?: string): Promise { 23 | if (this.failPing) { 24 | throw new Error(BadHttpClient.message) 25 | } 26 | if (this.stubPing != null) { 27 | return this.stubPing 28 | } 29 | return await super.getPingResponse(relayUrl, paymaster) 30 | } 31 | 32 | async relayTransaction (relayUrl: string, request: RelayTransactionRequest): Promise<{ 33 | signedTx: PrefixedHexString 34 | nonceGapFilled: ObjectMap 35 | }> { 36 | if (this.failRelay) { 37 | throw new Error(BadHttpClient.message) 38 | } 39 | if (this.timeoutRelay) { 40 | throw new Error('some error describing how timeout occurred somewhere') 41 | } 42 | if (this.stubRelay != null) { 43 | return { signedTx: this.stubRelay, nonceGapFilled: {} } 44 | } 45 | return await super.relayTransaction(relayUrl, request) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/dev/test/dummies/BadRelayClient.ts: -------------------------------------------------------------------------------- 1 | import { type GSNUnresolvedConstructorInput, RelayClient, type RelayingResult } from '@opengsn/provider/dist' 2 | import { type GsnTransactionDetails } from '@opengsn/common' 3 | 4 | export class BadRelayClient extends RelayClient { 5 | static readonly message = 'This is not the transaction you are looking for' 6 | 7 | private readonly failRelay: boolean 8 | private readonly returnUndefinedTransaction: boolean 9 | 10 | constructor ( 11 | failRelay: boolean, 12 | returnNullTransaction: boolean, 13 | rawConstructorInput: GSNUnresolvedConstructorInput 14 | ) { 15 | super(rawConstructorInput) 16 | this.failRelay = failRelay 17 | this.returnUndefinedTransaction = returnNullTransaction 18 | } 19 | 20 | async relayTransaction (gsnTransactionDetails: GsnTransactionDetails): Promise { 21 | if (this.failRelay) { 22 | throw new Error(BadRelayClient.message) 23 | } 24 | if (this.returnUndefinedTransaction) { 25 | return { 26 | transaction: undefined, 27 | priceErrors: new Map(), 28 | pingErrors: new Map(), 29 | relayingErrors: new Map() 30 | } 31 | } 32 | return await super.relayTransaction(gsnTransactionDetails) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/dev/test/dummies/BadRelayedTransactionValidator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RelayedTransactionValidator, 3 | type TransactionValidationResult 4 | } from '@opengsn/provider/dist/RelayedTransactionValidator' 5 | import { type ContractInteractor, type RelayTransactionRequest, type LoggerInterface, type ObjectMap } from '@opengsn/common' 6 | import { type GSNConfig } from '@opengsn/provider/dist' 7 | 8 | export class BadRelayedTransactionValidator extends RelayedTransactionValidator { 9 | private readonly failValidation: boolean 10 | 11 | constructor (logger: LoggerInterface, failValidation: boolean, contractInteractor: ContractInteractor, config: GSNConfig) { 12 | super(contractInteractor, logger, config) 13 | this.failValidation = failValidation 14 | } 15 | 16 | validateRelayResponse (transactionJsonRequest: RelayTransactionRequest, returnedTx: string, nonceGapFilled: ObjectMap): TransactionValidationResult { 17 | const superCallResult = super.validateRelayResponse(transactionJsonRequest, returnedTx, nonceGapFilled) 18 | if (this.failValidation) { 19 | superCallResult.isTransactionContentValid = false 20 | } 21 | return superCallResult 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/dev/test/penalizer/EtherscanCachedService.test.ts: -------------------------------------------------------------------------------- 1 | import { EtherscanCachedService } from '@opengsn/relay/dist/penalizer/EtherscanCachedService' 2 | import { TransactionDataCache } from '@opengsn/relay/dist/penalizer/TransactionDataCache' 3 | import { createClientLogger } from '@opengsn/logger/dist/ClientWinstonLogger' 4 | 5 | contract('EtherscanCachedService', function () { 6 | const testApiKey = '22E2FW3YJDPA76RETFSGYB3I41I1JHGSR9' 7 | const transactionHash = '0x968c29171533bcf2c396e6acb74e7b097a266d00b725447e6fd726801991c363' 8 | const account = '0xa975D1DE6d7dA3140E9e293509337373402558bE' 9 | const nonce = 11 10 | let service: EtherscanCachedService 11 | 12 | before(async function () { 13 | const logger = createClientLogger({ logLevel: 'error' }) 14 | const transactionDataCache = new TransactionDataCache(logger, '/tmp/test') 15 | await transactionDataCache.clearAll() 16 | service = new EtherscanCachedService('https://api-goerli.etherscan.io/api', testApiKey, logger, transactionDataCache) 17 | }) 18 | 19 | describe('getTransactionByNonce', function () { 20 | it('should query the Etherscan API for the transaction if account is not cached, and cache the account', async function () { 21 | const transaction = await service.getTransactionByNonce(account, nonce) 22 | const queriedTransactionHash = transaction?.hash 23 | assert.equal(queriedTransactionHash, transactionHash) 24 | }) 25 | 26 | it('should use cached response if possible') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/dev/test/penalizer/MockTxByNonceService.ts: -------------------------------------------------------------------------------- 1 | import { type Transaction } from '@ethereumjs/tx' 2 | 3 | import { type BlockExplorerInterface, type TransactionData } from '@opengsn/relay/dist/penalizer/BlockExplorerInterface' 4 | import { type ContractInteractor, type LoggerInterface, type Address } from '@opengsn/common' 5 | 6 | import { TransactionDataCache } from '@opengsn/relay/dist/penalizer/TransactionDataCache' 7 | 8 | export class MockTxByNonceService implements BlockExplorerInterface { 9 | transactionDataCache: TransactionDataCache 10 | contractInteractor: ContractInteractor 11 | 12 | constructor (contractInteractor: ContractInteractor, logger: LoggerInterface) { 13 | this.transactionDataCache = new TransactionDataCache(logger, '/tmp/test') 14 | this.contractInteractor = contractInteractor 15 | } 16 | 17 | async getTransactionByNonce (account: Address, nonce: number): Promise { 18 | return await this.transactionDataCache.getTransactionByNonce(account, nonce) 19 | } 20 | 21 | async setTransactionByNonce (tx: Transaction, from: Address): Promise { 22 | const txData: TransactionData = { 23 | from, 24 | hash: '0x' + tx.hash().toString('hex'), 25 | nonce: tx.nonce.toString(), 26 | to: '', 27 | gas: '', 28 | gasPrice: '', 29 | value: '', 30 | blockNumber: '', 31 | timeStamp: '', 32 | blockHash: '', 33 | transactionIndex: '', 34 | isError: '', 35 | txreceipt_status: '', 36 | input: '', 37 | contractAddress: '', 38 | cumulativeGasUsed: '', 39 | gasUsed: '', 40 | confirmations: '' 41 | } 42 | await this.transactionDataCache.putTransactions([txData], txData.from, 0) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/dev/test/regressions/PayableWithEmit.test.ts: -------------------------------------------------------------------------------- 1 | const PayableWithEmit = artifacts.require('PayableWithEmit') 2 | 3 | contract('PayableWithEmit', () => { 4 | let sender: any 5 | let receiver: any 6 | 7 | before(async () => { 8 | receiver = await PayableWithEmit.new() 9 | sender = await PayableWithEmit.new() 10 | }) 11 | it('payable that uses _msgSender()', async () => { 12 | const ret = await sender.doSend(receiver.address, { value: 1e18 }) 13 | // console.log({ gasUsed: ret.receipt.gasUsed, log: getLogs(ret) }) 14 | assert.equal(ret.logs.find((e: any) => e.event === 'GasUsed').args.success, true) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/dev/test/server-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "http://localhost:8090", 3 | "port": 8090, 4 | "relayHubAddress": "", 5 | "gasPriceFactor": 1.1, 6 | "ethereumNodeUrl": "http://localhost:8545", 7 | "workdir": "", 8 | "devMode": "true" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /packages/dev/test/utils/ERC20BalanceTracker.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js' 2 | 3 | import { type Address } from '@opengsn/common' 4 | 5 | const TestToken = artifacts.require('TestToken') 6 | 7 | class ERC20BalanceTracker { 8 | private readonly token: Address 9 | private readonly account: Address 10 | private prev?: BN 11 | 12 | constructor (tok: Address, acc: Address) { 13 | this.token = tok 14 | this.account = acc 15 | } 16 | 17 | async delta (): Promise { 18 | if (this.prev == null) { 19 | throw new Error('prev is null') 20 | } 21 | const current = await this.balanceCurrentErc20() 22 | const delta = current.sub(this.prev) 23 | this.prev = current 24 | 25 | return new BN(delta) 26 | } 27 | 28 | async get (): Promise { 29 | this.prev = await this.balanceCurrentErc20() 30 | return new BN(this.prev) 31 | } 32 | 33 | async balanceCurrentErc20 (): Promise { 34 | const token = await TestToken.at(this.token) 35 | return await token.balanceOf(this.account) 36 | } 37 | } 38 | 39 | export async function balanceTrackerErc20 (token: Address, owner: Address): Promise { 40 | const tracker = new ERC20BalanceTracker(token, owner) 41 | await tracker.get() 42 | return tracker 43 | } 44 | -------------------------------------------------------------------------------- /packages/dev/test/utils/chaiHelper.ts: -------------------------------------------------------------------------------- 1 | // remap "eql" function to work nicely with EVM values. 2 | 3 | // cleanup "Result" object (returned on web3/ethers calls) 4 | // remove "array" members, convert values to strings. 5 | // so Result obj like 6 | // { '0': "a", '1': 20, first: "a", second: 20 } 7 | // becomes: 8 | // { first: "a", second: "20" } 9 | // map values inside object using mapping func. 10 | import chai from 'chai' 11 | 12 | export function objValues (obj: Record, mapFunc: (val: any, key?: string) => any): any { 13 | return Object.keys(obj) 14 | .filter(key => key.match(/^[\d_]/) == null) 15 | .reduce((set, key) => ({ 16 | ...set, 17 | [key]: mapFunc(obj[key], key) 18 | }), {}) 19 | } 20 | 21 | /** 22 | * cleanup a value of an object, for easier testing. 23 | * - Result: this is an array which also contains named members. 24 | * - obj.length*2 == Object.keys().length 25 | * - remove the array elements, use just the named ones. 26 | * - recursively handle inner members of object, arrays. 27 | * - attempt toString. but if no normal value, recurse into fields. 28 | */ 29 | export function cleanValue (val: any): any { 30 | if (val == null) return val 31 | if (Array.isArray(val)) { 32 | if (val.length * 2 === Object.keys(val).length) { 33 | // "looks" like a Result object. 34 | return objValues(val, cleanValue) 35 | } 36 | // its a plain array. map each array element 37 | return val.map(val1 => cleanValue(val1)) 38 | } 39 | 40 | const str = val.toString() 41 | if (str !== '[object Object]') { return str } 42 | 43 | return objValues(val, cleanValue) 44 | } 45 | 46 | // use cleanValue for comparing. MUCH easier, since numbers compare well with bignumbers, etc 47 | 48 | chai.Assertion.overwriteMethod('eql', (original) => { 49 | // @ts-ignore 50 | return function (this: any, expected: any) { 51 | const _actual = cleanValue(this._obj) 52 | const _expected = cleanValue(expected) 53 | // original.apply(this,arguments) 54 | this._obj = _actual 55 | original.apply(this, [_expected]) 56 | // assert.deepEqual(_actual, _expected) 57 | // ctx.assert( 58 | // _actual == _expected, 59 | // 'expected #{act} to equal #{exp}', 60 | // 'expected #{act} to be different from #{exp}', 61 | // _expected, 62 | // _actual 63 | // ); 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /packages/dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "deploy/*.ts", 6 | "deployment-config.ts", 7 | "../contracts/hardhat.config.ts", 8 | "test/**/*.ts", 9 | "types/truffle-contracts/index.d.ts", 10 | "types/truffle-contracts/types.d.ts" 11 | ], 12 | "compilerOptions": { 13 | "outDir": "dist" 14 | } 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/dev/tsconfig.packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "truffle.js", 5 | "migrations/*.js", 6 | "test/**/*.ts" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/dev/types/Web3Types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export interface EventData { 4 | returnValues: { 5 | [key: string]: any; 6 | }; 7 | raw: { 8 | data: string; 9 | topics: string[]; 10 | }; 11 | event: string; 12 | signature: string; 13 | logIndex: number; 14 | transactionIndex: number; 15 | transactionHash: string; 16 | blockHash: string; 17 | blockNumber: number; 18 | address: string; 19 | } 20 | 21 | export type BlockNumber = string | number | BN /*| BigNumber*/ | 'latest' | 'pending' | 'earliest' | 'genesis'; 22 | 23 | export interface LogsOptions { 24 | fromBlock?: BlockNumber; 25 | address?: string | string[]; 26 | topics?: Array; 27 | } 28 | 29 | export interface PastLogsOptions extends LogsOptions { 30 | toBlock?: BlockNumber; 31 | } 32 | 33 | export interface Filter { 34 | [key: string]: number | string | string[] | number[]; 35 | } 36 | 37 | export interface EventOptions extends LogsOptions { 38 | filter?: Filter; 39 | } 40 | 41 | export interface PastEventOptions extends PastLogsOptions { 42 | filter?: Filter; 43 | } 44 | -------------------------------------------------------------------------------- /packages/logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/logger", 3 | "version": "3.0.0-beta.10", 4 | "license": "GPL-3.0-only", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist", 8 | "README.md" 9 | ], 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "scripts": { 14 | "tsc": "tsc", 15 | "watch-tsc": "tsc -w --preserveWatchOutput", 16 | "lint": "eslint -f unix .", 17 | "lint-fix": "yarn lint --fix", 18 | "rm-dist": "rm -rf tsconfig.tsbuildinfo dist build" 19 | }, 20 | "dependencies": { 21 | "@opengsn/common": "^3.0.0-beta.10", 22 | "loglevel": "^1.8.0", 23 | "winston": "^3.3.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/logger/src/ClientWinstonLogger.ts: -------------------------------------------------------------------------------- 1 | import log from 'loglevel' 2 | import winston, { type transport } from 'winston' 3 | import { type HttpTransportOptions } from 'winston/lib/winston/transports' 4 | 5 | import { gsnRuntimeVersion, type LoggerInterface, type LoggerConfiguration } from '@opengsn/common' 6 | 7 | const format = winston.format.combine( 8 | winston.format.uncolorize(), 9 | winston.format.timestamp(), 10 | winston.format.simple() 11 | ) 12 | 13 | const service = 'gsn-client' 14 | const userIdKey = 'gsnuser' 15 | 16 | const isBrowser = typeof window !== 'undefined' 17 | 18 | function getOrCreateUserId (): string { 19 | let userId = window.localStorage[userIdKey] 20 | if (userId == null) { 21 | userId = `${userIdKey}${Date.now() % 1000000}` 22 | window.localStorage[userIdKey] = userId 23 | } 24 | return userId 25 | } 26 | 27 | export function createClientLogger (loggerConfiguration?: LoggerConfiguration): LoggerInterface { 28 | loggerConfiguration = loggerConfiguration ?? { logLevel: 'info' } 29 | // eslint-disable-next-line @typescript-eslint/prefer-optional-chain 30 | if (loggerConfiguration.loggerUrl == null || typeof window === 'undefined' || window.localStorage == null) { 31 | log.setLevel(loggerConfiguration.logLevel) 32 | return log 33 | } 34 | 35 | const url = new URL(loggerConfiguration.loggerUrl) 36 | const host = url.host 37 | const path = url.pathname 38 | const ssl = url.protocol === 'https:' 39 | const headers = { 'content-type': 'text/plain' } 40 | const httpTransportOptions: HttpTransportOptions = { 41 | ssl, 42 | format, 43 | host, 44 | path, 45 | headers 46 | } 47 | 48 | const transports: transport[] = [ 49 | new winston.transports.Console({ 50 | format: winston.format.combine( 51 | winston.format.colorize(), 52 | winston.format.simple() 53 | ) 54 | }), 55 | new winston.transports.Http(httpTransportOptions) 56 | ] 57 | let userId: string 58 | if (loggerConfiguration.userId != null) { 59 | userId = loggerConfiguration.userId 60 | } else { 61 | userId = getOrCreateUserId() 62 | } 63 | 64 | const localhostRegExp: RegExp = /http:\/\/(localhost)|\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/ 65 | let applicationId = loggerConfiguration.applicationId 66 | if (loggerConfiguration.applicationId == null && window?.location?.href != null && window.location.href.match(localhostRegExp) == null) { 67 | applicationId = window.location.href 68 | } 69 | const logger = winston.createLogger({ 70 | level: loggerConfiguration.logLevel, 71 | defaultMeta: { 72 | version: gsnRuntimeVersion, 73 | service, 74 | isBrowser, 75 | applicationId, 76 | userId 77 | }, 78 | transports 79 | }) 80 | logger.debug(`Created remote logs collecting logger for userId: ${userId} and applicationId: ${applicationId}`) 81 | if (applicationId == null) { 82 | logger.warn('application ID is not set!') 83 | } 84 | return logger 85 | } 86 | -------------------------------------------------------------------------------- /packages/logger/src/CommandsWinstonLogger.ts: -------------------------------------------------------------------------------- 1 | import winston, { type transport } from 'winston' 2 | 3 | import { type NpmLogLevel, type LoggerInterface } from '@opengsn/common' 4 | 5 | const format = winston.format.combine( 6 | winston.format.cli() 7 | ) 8 | 9 | export function createCommandsLogger (level: NpmLogLevel): LoggerInterface { 10 | const transports: transport[] = [ 11 | new winston.transports.Console({ format }) 12 | ] 13 | return winston.createLogger({ 14 | level, 15 | transports 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /packages/logger/src/ServerWinstonLogger.ts: -------------------------------------------------------------------------------- 1 | import winston, { type Logger, type transport } from 'winston' 2 | import { type ConsoleTransportOptions, type HttpTransportOptions } from 'winston/lib/winston/transports' 3 | 4 | import { gsnRuntimeVersion, type NpmLogLevel } from '@opengsn/common' 5 | 6 | const service = 'gsn-server' 7 | 8 | const format = winston.format.combine( 9 | winston.format.uncolorize(), 10 | winston.format.timestamp(), 11 | winston.format.simple() 12 | ) 13 | 14 | const consoleOptions: ConsoleTransportOptions = { 15 | format: winston.format.combine( 16 | winston.format.cli() 17 | ) 18 | } 19 | 20 | export function createServerLogger (level: NpmLogLevel, loggerUrl: string, userId: string): Logger { 21 | const transports: transport[] = [ 22 | new winston.transports.Console(consoleOptions) 23 | // new winston.transports.File({ format, filename }) 24 | ] 25 | let isCollectingLogs = false 26 | if (loggerUrl.length !== 0 && userId.length !== 0) { 27 | const url = new URL(loggerUrl) 28 | const host = url.host 29 | const path = url.pathname 30 | const ssl = url.protocol === 'https:' 31 | const headers = { 'content-type': 'text/plain' } 32 | isCollectingLogs = true 33 | const httpTransportOptions: HttpTransportOptions = { 34 | ssl, 35 | format, 36 | host, 37 | path, 38 | headers 39 | } 40 | transports.push(new winston.transports.Http(httpTransportOptions)) 41 | } 42 | const logger = winston.createLogger({ 43 | level, 44 | defaultMeta: { 45 | version: gsnRuntimeVersion, 46 | service, 47 | userId 48 | }, 49 | transports 50 | }) 51 | logger.debug(`Created logger for ${userId}; remote logs collecting: ${isCollectingLogs}`) 52 | return logger 53 | } 54 | -------------------------------------------------------------------------------- /packages/logger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "../contracts/types/truffle-contracts/index.d.ts", 6 | "../contracts/types/truffle-contracts/types.d.ts" 7 | ], 8 | "compilerOptions": { 9 | "rootDir": "src", 10 | "outDir": "dist" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/paymasters/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@openzeppelin/contracts", "@uniswap/v3-periphery", "solc-0.8", "web3-eth-accounts"] 2 | -------------------------------------------------------------------------------- /packages/paymasters/.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "custom-errors": ["off"], 5 | "no-global-import": ["off"], 6 | "compiler-version": ["error",">=0.7.5"], 7 | "func-visibility": ["off",{"ignoreConstructors":true}], 8 | "mark-callable-contracts": ["off"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/paymasters/README.md: -------------------------------------------------------------------------------- 1 | # Sample GSN Paymasters 2 | 3 | A GSN paymaster is a contract that actually pays for a relayed request (as the whole point of GSN is that the 4 | calling account require and usually lack eth) 5 | 6 | 7 | ## AcceptEverythingPaymaster 8 | 9 | This is the most naive paymaster: it will accept any request by any client. 10 | Obviously, you don't want to deploy that on mainnet, since any client will be able to make any number 11 | of requests to drain it. 12 | 13 | ## WhitelistPaymaster 14 | 15 | This paymaster accepts only request from specific, known addresses. 16 | This way it is protected from anonymous attack, but requires an extra step of whitelisting all 17 | valid addresses. 18 | 19 | ## HashcashPaymaster 20 | 21 | An example paymaster that tries to mitigate abuse by anonymous clients: 22 | It requires the client to perform a "proof of work" that is verified on-chain. 23 | the "approval data" should contain a hash over the caller's address and nonce. 24 | 25 | ## TokenPaymaster 26 | 27 | A paymaster that requires the calling account to have a specific token. The paymaster will pre-charge 28 | the user with the equivalent value of tokens before making the call (and refund it with the excess after 29 | the call) 30 | 31 | The client doesn't have to have eth, but has to have an `approval` for the paymaster to pull tokens from its account (for an ETH-less account, this can be done using DAI's `permit` method, or using the next paymaster:) 32 | 33 | ## ProxyDeployingPaymaster 34 | 35 | A specific TokenPaymaster, that can also deploy a proxy account. 36 | Since the paymaster also deploys the proxy, it also makes this proxy "approve" the token, so the paymaster 37 | can charge the account with tokens - even for the proxy creation - and then for all future requests. 38 | 39 |

Important notice:

40 | 41 | These contracts are provided as an example, and should NOT be deployed as-is into a real network. 42 | None of them have passed a security audit. 43 | Without a careful configuration, a caller can "grief" the paymaster by making many 44 | anonymous calls, and thus drain the paymaster's balance. 45 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/AcceptEverythingPaymaster.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@opengsn/contracts/src/BasePaymaster.sol"; 6 | 7 | // accept everything. 8 | // this paymaster accepts any request. 9 | // 10 | // NOTE: Do NOT use this contract on a mainnet: it accepts anything, so anyone can "grief" it and drain its account 11 | 12 | contract AcceptEverythingPaymaster is BasePaymaster { 13 | 14 | function versionPaymaster() external view override virtual returns (string memory){ 15 | return "3.0.0-beta.3+opengsn.accepteverything.ipaymaster"; 16 | } 17 | 18 | function _preRelayedCall( 19 | GsnTypes.RelayRequest calldata relayRequest, 20 | bytes calldata signature, 21 | bytes calldata approvalData, 22 | uint256 maxPossibleGas 23 | ) 24 | internal 25 | override 26 | virtual 27 | returns (bytes memory context, bool revertOnRecipientRevert) { 28 | (relayRequest, signature, approvalData, maxPossibleGas); 29 | return ("", false); 30 | } 31 | 32 | function _postRelayedCall( 33 | bytes calldata context, 34 | bool success, 35 | uint256 gasUseWithoutPost, 36 | GsnTypes.RelayData calldata relayData 37 | ) 38 | internal 39 | override 40 | virtual { 41 | (context, success, gasUseWithoutPost, relayData); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/HashcashPaymaster.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "./AcceptEverythingPaymaster.sol"; 6 | 7 | ///A paymaster that requires some calculation from the client before accepting a request. 8 | ///This comes to prevent attack by anonymous clients. 9 | /// Usage: 10 | /// - Create an instance of the HashcashPaymaster, and give it a proper difficulty level. 11 | /// - When creating a RelayProvider, make sure to use the createHashcashAsyncApproval() with 12 | /// the same difficulty level. 13 | /// 14 | /// The "difficulty" level is the number of zero bits at the generated hash. 15 | /// a value of 15 requires roughly 32000 iterations and take ~0.5 second on a normal PC 16 | contract HashcashPaymaster is AcceptEverythingPaymaster { 17 | 18 | function versionPaymaster() external view override virtual returns (string memory){ 19 | return "3.0.0-beta.3+opengsn.hashcash.ipaymaster"; 20 | } 21 | 22 | uint8 public difficulty; 23 | 24 | constructor(uint8 _difficulty) { 25 | difficulty = _difficulty; 26 | } 27 | 28 | function _verifyApprovalData(bytes calldata approvalData) internal virtual override view{ 29 | // solhint-disable-next-line reason-string 30 | require(approvalData.length == 64, "approvalData: invalid length for hash and nonce"); 31 | } 32 | 33 | function _preRelayedCall( 34 | GsnTypes.RelayRequest calldata relayRequest, 35 | bytes calldata signature, 36 | bytes calldata approvalData, 37 | uint256 maxPossibleGas 38 | ) 39 | internal 40 | override 41 | virtual 42 | returns (bytes memory, bool revertOnRecipientRevert) { 43 | (maxPossibleGas, signature); 44 | 45 | (bytes32 hash, uint256 hashNonce) = abi.decode(approvalData, (bytes32, uint256)); 46 | bytes32 calcHash = keccak256(abi.encode( 47 | relayRequest.request.from, 48 | relayRequest.request.nonce, 49 | hashNonce)); 50 | require(hash == calcHash, "wrong hash"); 51 | require(uint256(hash) < (uint256(1) << (256 - difficulty)), "difficulty not met"); 52 | return ("", false); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/SingleRecipientPaymaster.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@opengsn/contracts/src/BasePaymaster.sol"; 6 | 7 | /** 8 | * a paymaster for a single recipient contract. 9 | * - reject requests if destination is not the target contract. 10 | * - reject any request if the target contract reverts. 11 | */ 12 | contract SingleRecipientPaymaster is BasePaymaster { 13 | 14 | address public target; 15 | 16 | event TargetChanged(address oldTarget, address newTarget); 17 | 18 | function versionPaymaster() external view override virtual returns (string memory){ 19 | return "3.0.0-beta.3+opengsn.recipient.ipaymaster"; 20 | } 21 | 22 | function setTarget(address _target) external onlyOwner { 23 | emit TargetChanged(target, _target); 24 | target=_target; 25 | } 26 | 27 | function _preRelayedCall( 28 | GsnTypes.RelayRequest calldata relayRequest, 29 | bytes calldata signature, 30 | bytes calldata approvalData, 31 | uint256 maxPossibleGas 32 | ) 33 | internal 34 | override 35 | virtual 36 | returns (bytes memory context, bool revertOnRecipientRevert) { 37 | (relayRequest, signature, approvalData, maxPossibleGas); 38 | require(relayRequest.request.to==target, "wrong target"); 39 | //returning "true" means this paymaster accepts all requests that 40 | // are not rejected by the recipient contract. 41 | return ("", true); 42 | } 43 | 44 | function _postRelayedCall( 45 | bytes calldata context, 46 | bool success, 47 | uint256 gasUseWithoutPost, 48 | GsnTypes.RelayData calldata relayData 49 | ) 50 | internal 51 | override 52 | virtual { 53 | (context, success, gasUseWithoutPost, relayData); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/AllEvents.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.7; 3 | 4 | /** 5 | * In order to help the Truffle tests to decode events in the transactions' results, 6 | * the events must be declared in a top-level contract. 7 | * Implement this empty interface in order to add event signatures to any contract. 8 | * 9 | */ 10 | interface AllEvents { 11 | event Received(address indexed sender, uint256 eth); 12 | event Withdrawal(address indexed src, uint256 wad); 13 | event Transfer(address indexed from, address indexed to, uint256 value); 14 | event Approval(address indexed owner, address indexed spender, uint256 value); 15 | event TokensCharged(uint256 gasUseWithoutPost, uint256 gasJustPost, uint256 tokenActualCharge, uint256 ethActualCharge); 16 | event UniswapReverted(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin); 17 | 18 | event Swap( 19 | address indexed sender, 20 | address indexed recipient, 21 | int256 amount0, 22 | int256 amount1, 23 | uint160 sqrtPriceX96, 24 | uint128 liquidity, 25 | int24 tick 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/ImportsArtifacts.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | //"import" it into our project for Truffle to generate artifacts 6 | import "@opengsn/contracts/src/forwarder/IForwarder.sol"; 7 | import "@opengsn/contracts/src/forwarder/Forwarder.sol"; 8 | import "@opengsn/contracts/src/StakeManager.sol"; 9 | import "@opengsn/contracts/src/Penalizer.sol"; 10 | import "@opengsn/contracts/src/utils/RelayRegistrar.sol"; 11 | import "@opengsn/contracts/src/test/TestRecipient.sol"; 12 | 13 | import "@uniswap/v3-periphery/contracts/interfaces/IQuoter.sol"; 14 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/SampleRecipient.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@opengsn/contracts/src/ERC2771Recipient.sol"; 6 | 7 | // pass-through paymaster. 8 | // should override it and re-implement acceptRelayedCall. use "super" on success 9 | contract SampleRecipient is ERC2771Recipient { 10 | 11 | event Sender( address _msgSenderFunc, address sender ); 12 | 13 | function setForwarder(address forwarder) public { 14 | _setTrustedForwarder(forwarder); 15 | } 16 | 17 | function something() public { 18 | emit Sender( _msgSender(), msg.sender ); 19 | } 20 | 21 | function nothing() public { 22 | emit Sender( _msgSender(), msg.sender ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/TestCounter.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract TestCounter { 5 | uint256 public count; 6 | 7 | constructor () { 8 | count = 0; 9 | } 10 | 11 | function increment() public payable{ 12 | count = count + 1; 13 | } 14 | 15 | function get() public view returns (uint256) { 16 | return count; 17 | } 18 | 19 | /* solhint-disable no-empty-blocks */ 20 | receive() external payable {} 21 | fallback() external payable {} 22 | } 23 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/TestHub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@opengsn/contracts/src/utils/GsnTypes.sol"; 6 | import "@opengsn/contracts/src/interfaces/IPaymaster.sol"; 7 | 8 | import "@opengsn/contracts/src/RelayHub.sol"; 9 | 10 | import "./AllEvents.sol"; 11 | 12 | /** 13 | * This mock relay hub contract is only used to test the paymaster's 'pre-' and 'postRelayedCall' in isolation. 14 | */ 15 | contract TestHub is RelayHub, AllEvents { 16 | 17 | constructor( 18 | IStakeManager _stakeManager, 19 | address _penalizer, 20 | address _batchGateway, 21 | address _relayRegistrar, 22 | RelayHubConfig memory _config) RelayHub(_stakeManager, 23 | _penalizer, 24 | _batchGateway, 25 | _relayRegistrar, 26 | _config) 27 | // solhint-disable-next-line no-empty-blocks 28 | {} 29 | 30 | function callPreRC( 31 | GsnTypes.RelayRequest calldata relayRequest, 32 | bytes calldata signature, 33 | bytes calldata approvalData, 34 | uint256 maxPossibleGas 35 | ) 36 | external 37 | returns (bytes memory context, bool revertOnRecipientRevert) { 38 | IPaymaster paymaster = IPaymaster(relayRequest.relayData.paymaster); 39 | IPaymaster.GasAndDataLimits memory limits = paymaster.getGasAndDataLimits(); 40 | return paymaster.preRelayedCall{gas: limits.preRelayedCallGasLimit}(relayRequest, signature, approvalData, maxPossibleGas); 41 | } 42 | 43 | function callPostRC( 44 | IPaymaster paymaster, 45 | bytes calldata context, 46 | uint256 gasUseWithoutPost, 47 | GsnTypes.RelayData calldata relayData 48 | ) 49 | external { 50 | IPaymaster.GasAndDataLimits memory limits = paymaster.getGasAndDataLimits(); 51 | paymaster.postRelayedCall{gas: limits.postRelayedCallGasLimit}(context, true, gasUseWithoutPost, relayData); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/TestProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | import "@opengsn/contracts/src/ERC2771Recipient.sol"; 7 | 8 | contract TestProxy is ERC2771Recipient, Ownable { 9 | 10 | constructor(address forwarder) { 11 | _setTrustedForwarder(forwarder); 12 | } 13 | 14 | function isOwner() public view returns (bool) { 15 | return _msgSender() == owner(); 16 | } 17 | 18 | event Test(address _msgSender, address msgSender); 19 | //not a proxy method; just for testing. 20 | function test() public { 21 | emit Test(_msgSender(), msg.sender); 22 | } 23 | 24 | function execute(address target, bytes calldata func) external onlyOwner { 25 | 26 | //solhint-disable-next-line 27 | (bool success, bytes memory ret) = target.call(func); 28 | require(success, string(ret)); 29 | } 30 | 31 | function _msgSender() internal override(Context, ERC2771Recipient) view returns (address) { 32 | return ERC2771Recipient._msgSender(); 33 | } 34 | 35 | function _msgData() internal override(Context, ERC2771Recipient) view returns (bytes memory) { 36 | return ERC2771Recipient._msgData(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/TestUniswap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@opengsn/contracts/src/test/TestToken.sol"; 6 | 7 | import "../interfaces/IUniswapV3.sol"; 8 | 9 | // naive, no-calculation swapper. 10 | //- the exchange rate is fixed at construction 11 | //- mints new tokens at will... 12 | contract TestUniswap is IUniswapV3 { 13 | IERC20 public token; 14 | uint256 public rateMult; 15 | uint256 public rateDiv; 16 | 17 | constructor(uint256 _rateMult, uint256 _rateDiv) payable { 18 | token = new TestToken(); 19 | rateMult = _rateMult; 20 | rateDiv = _rateDiv; 21 | require(msg.value > 0, "must specify liquidity"); 22 | require(rateMult != 0 && rateDiv != 0, "bad mult,div"); 23 | } 24 | 25 | // solhint-disable-next-line no-empty-blocks 26 | receive() external payable {} 27 | 28 | function tokenAddress() external override view returns (address out) { 29 | return address(token); 30 | } 31 | 32 | function tokenToEthSwapOutput(uint256 ethBought, uint256 maxTokens, uint256 deadline) public override returns (uint256 out) { 33 | (maxTokens, deadline); 34 | uint256 tokensToSell = getTokenToEthOutputPrice(ethBought); 35 | require(address(this).balance > ethBought, "not enough liquidity"); 36 | 37 | token.transferFrom(msg.sender, address(this), tokensToSell); 38 | payable(msg.sender).transfer(ethBought); 39 | return tokensToSell; 40 | } 41 | 42 | function getTokenToEthInputPrice(uint256 tokensSold) external override view returns (uint256 out) { 43 | return tokensSold * rateDiv / rateMult; 44 | } 45 | 46 | function tokenToEthTransferOutput(uint256 ethBought, uint256 maxTokens, uint256 deadline, address payable recipient) external override returns (uint256 out) { 47 | (maxTokens, deadline, recipient); 48 | require(address(this).balance > ethBought, "not enough liquidity"); 49 | 50 | uint256 tokensToSell = getTokenToEthOutputPrice(ethBought); 51 | 52 | token.transferFrom(msg.sender, address(this), tokensToSell); 53 | recipient.transfer(ethBought); 54 | return tokensToSell; 55 | } 56 | 57 | function getTokenToEthOutputPrice(uint256 ethBought) public override view returns (uint256 out) { 58 | return ethBought * rateMult / rateDiv; 59 | } 60 | 61 | function exactInputSingle(ExactInputSingleParams calldata) external override payable returns (uint256 amountOut) { 62 | revert("No swap for you"); 63 | } 64 | 65 | // solhint-disable-next-line no-empty-blocks 66 | function unwrapWETH9(uint256, address) external payable {} 67 | } 68 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/TokenGasCalculator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import "@opengsn/contracts/src/RelayHub.sol"; 8 | import "@opengsn/contracts/src/BasePaymaster.sol"; 9 | import "./AllEvents.sol"; 10 | 11 | /** 12 | * Calculate the postRelayedCall gas usage for a TokenPaymaster. 13 | * 14 | */ 15 | contract TokenGasCalculator is RelayHub, AllEvents { 16 | 17 | //(The Paymaster calls back calculateCharge, depositFor in the relayHub, 18 | //so the calculator has to implement them just like a real RelayHub 19 | // solhint-disable-next-line no-empty-blocks 20 | constructor( 21 | IStakeManager _stakeManager, 22 | address _penalizer, 23 | address _batchGateway, 24 | address _relayRegistrar, 25 | RelayHubConfig memory _config) RelayHub(_stakeManager, 26 | _penalizer, 27 | _batchGateway, 28 | _relayRegistrar, 29 | _config) 30 | // solhint-disable-next-line no-empty-blocks 31 | {} 32 | 33 | /** 34 | * calculate actual cost of postRelayedCall. 35 | * usage: 36 | * - create this calculator. 37 | * - create an instance of your TokenPaymaster, with your token's Uniswap instance. 38 | * - move some tokens (1000 "wei") to the calculator (msg.sender is given approval to pull them back at the end) 39 | * - set the calculator as owner of this calculator. 40 | * - call this method. 41 | * - use the returned values to set your real TokenPaymaster.setPostGasUsage() 42 | * the above can be ran on a "forked" network, so that it will have the real token, uniswap instances, 43 | * but still leave no side-effect on the network. 44 | */ 45 | function calculatePostGas( 46 | BasePaymaster paymaster, 47 | bytes memory ctx1, 48 | bytes memory paymasterData 49 | ) public returns (uint256 gasUsedByPost) { 50 | GsnTypes.RelayData memory relayData = GsnTypes.RelayData(1, 1, 0, address(0), address(0), address(0), paymasterData, 0); 51 | 52 | //with precharge 53 | uint256 gas0 = gasleft(); 54 | paymaster.postRelayedCall(ctx1, true, 100, relayData); 55 | uint256 gas1 = gasleft(); 56 | gasUsedByPost = gas0 - gas1; 57 | emit GasUsed(gasUsedByPost); 58 | } 59 | 60 | event GasUsed(uint256 gasUsedByPost); 61 | } 62 | 63 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/helpers/UniswapV3Helper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.7; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; 6 | import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol"; 7 | 8 | library UniswapV3Helper { 9 | event UniswapReverted(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin); 10 | // turn ERC-20 tokens into wrapped ETH at market price 11 | function swapToWeth( 12 | address token, 13 | address weth, 14 | uint256 amountOut, 15 | uint24 fee, 16 | ISwapRouter uniswap 17 | ) internal returns (uint256 amountIn) { 18 | ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams( 19 | token, //tokenIn 20 | weth, //tokenOut 21 | fee, 22 | address(uniswap), //recipient - keep WETH at SwapRouter for withdrawal 23 | // solhint-disable-next-line not-rely-on-time 24 | block.timestamp, //deadline 25 | amountOut, 26 | type(uint256).max, 27 | 0 28 | ); 29 | amountIn = uniswap.exactOutputSingle(params); 30 | } 31 | 32 | function unwrapWeth(ISwapRouter uniswap, uint256 amount) internal { 33 | IPeripheryPayments(address(uniswap)).unwrapWETH9(amount, address(this)); 34 | } 35 | 36 | // swap ERC-20 tokens at market price 37 | function swapToToken( 38 | address tokenIn, 39 | address tokenOut, 40 | uint256 amountIn, 41 | uint256 amountOutMin, 42 | uint24 fee, 43 | ISwapRouter uniswap 44 | ) internal returns (uint256 amountOut) { 45 | ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams( 46 | tokenIn, //tokenIn 47 | tokenOut, //tokenOut 48 | fee, 49 | address(uniswap), 50 | // solhint-disable-next-line not-rely-on-time 51 | block.timestamp, //deadline 52 | amountIn, 53 | amountOutMin, 54 | 0 55 | ); 56 | try uniswap.exactInputSingle(params) returns (uint256 _amountOut) { 57 | amountOut = _amountOut; 58 | } catch { 59 | emit UniswapReverted(tokenIn, tokenOut, amountIn, amountOutMin); 60 | amountOut = 0; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/interfaces/IChainlinkOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.7; 3 | 4 | interface IChainlinkOracle { 5 | function decimals() 6 | external 7 | view 8 | returns ( 9 | uint8 10 | ); 11 | 12 | function latestAnswer() external view returns (int256); 13 | } 14 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/interfaces/IERC725.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC725 { 5 | event DataChanged(bytes32 indexed key, bytes indexed value); 6 | event OwnerChanged(address indexed ownerAddress); 7 | event ContractCreated(address indexed contractAddress); 8 | 9 | function owner() external view returns (address); 10 | 11 | function changeOwner(address _owner) external; 12 | 13 | function getData(bytes32 _key) external view returns (bytes memory _value); 14 | 15 | function setData(bytes32 _key, bytes calldata _value) external; 16 | 17 | function execute(uint256 _operationType, address _to, uint256 _value, bytes calldata _data) external payable; 18 | } 19 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/interfaces/IUniswapV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.7; 3 | 4 | //minimal uniswap we need: 5 | interface IUniswapV3 { 6 | function tokenAddress() external view returns (address); 7 | 8 | function tokenToEthSwapOutput(uint256 ethBought, uint256 maxTokens, uint256 deadline) external returns (uint256 out); 9 | 10 | function tokenToEthTransferOutput(uint256 ethBought, uint256 maxTokens, uint256 deadline, address payable recipient) external returns (uint256 out); 11 | 12 | function getTokenToEthOutputPrice(uint256 ethBought) external view returns (uint256 out); 13 | 14 | function getTokenToEthInputPrice(uint256 tokensSold) external view returns (uint256 out); 15 | 16 | struct ExactInputSingleParams { 17 | address tokenIn; 18 | address tokenOut; 19 | uint24 fee; 20 | address recipient; 21 | uint256 deadline; 22 | uint256 amountIn; 23 | uint256 amountOutMinimum; 24 | uint160 sqrtPriceLimitX96; 25 | } 26 | 27 | function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/interfaces/PermitInterfaceDAI.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 5 | 6 | interface PermitInterfaceDAI is IERC20Metadata { 7 | function nonces(address holder) external view returns (uint256 nonce); 8 | 9 | // --- Approve by signature --- 10 | function permit(address holder, address spender, uint256 nonce, uint256 expiry, 11 | bool allowed, uint8 v, bytes32 r, bytes32 s) external; 12 | } 13 | -------------------------------------------------------------------------------- /packages/paymasters/contracts/interfaces/PermitInterfaceEIP2612.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 5 | 6 | interface PermitInterfaceEIP2612 is IERC20Metadata { 7 | function nonces(address holder) external view returns (uint256 nonce); 8 | 9 | // --- Approve by signature --- 10 | function permit(address owner, address spender, uint256 value, uint256 deadline, 11 | uint8 v, bytes32 r, bytes32 s) external; 12 | } 13 | -------------------------------------------------------------------------------- /packages/paymasters/deployments/goerli/.chainId: -------------------------------------------------------------------------------- 1 | 5 -------------------------------------------------------------------------------- /packages/paymasters/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-web3' 2 | import 'hardhat-deploy' 3 | import '@nomiclabs/hardhat-ethers' 4 | import '@nomiclabs/hardhat-etherscan' 5 | 6 | import fs from 'fs' 7 | 8 | import { type HardhatUserConfig } from 'hardhat/config' 9 | import { type NetworkUserConfig } from 'hardhat/src/types/config' 10 | 11 | // TODO: extract and reuse duplicated code 12 | const mnemonicFileName = process.env.MNEMONIC_FILE 13 | let mnemonic = 'test '.repeat(11) + 'junk' 14 | if (mnemonicFileName != null && fs.existsSync(mnemonicFileName)) { 15 | mnemonic = fs.readFileSync(mnemonicFileName, 'ascii') 16 | } 17 | 18 | function getNetwork (url: string): NetworkUserConfig { 19 | // if "FORK" is set, then "hardhat node --fork" should be active for this chainId 20 | if (process.env.FORK != null) { 21 | return { 22 | url: 'http://localhost:8544', 23 | chainId: parseInt(process.env.FORK) 24 | } 25 | } 26 | return { 27 | url, 28 | accounts: { mnemonic } 29 | } 30 | } 31 | 32 | const infuraUrl = (name: string): string => `https://${name}.infura.io/v3/${process.env.INFURA_ID}` 33 | 34 | function getInfuraNetwork (name: string): NetworkUserConfig { 35 | return getNetwork(infuraUrl(name)) 36 | } 37 | 38 | const config: HardhatUserConfig = { 39 | solidity: { 40 | version: '0.8.7', 41 | settings: { 42 | optimizer: { 43 | enabled: true 44 | } 45 | } 46 | }, 47 | networks: { 48 | aaa: getInfuraNetwork('goerli'), 49 | goerli: getInfuraNetwork('goerli') 50 | } 51 | } 52 | 53 | module.exports = config 54 | -------------------------------------------------------------------------------- /packages/paymasters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/paymasters", 3 | "license": "GPL-3.0-only", 4 | "version": "3.0.0-beta.10", 5 | "scripts": { 6 | "tsc": "tsc", 7 | "deploy": "hardhat deploy", 8 | "verify": "hardhat etherscan-verify --force-license --license GPL-3.0", 9 | "truffle-compile": "truffle compile --compile-all", 10 | "typechain-generate": "yarn truffle-compile && yarn typechain-generate-truffle && yarn typechain-generate-ethers-v5", 11 | "typechain-generate-truffle": "typechain --target truffle-v5 './build/contracts/**/*.json'", 12 | "typechain-generate-ethers-v5": "typechain --target ethers-v5 './build/contracts/**/*.json'", 13 | "lint": "yarn run lint:js && yarn run lint:sol", 14 | "lint:js": "eslint -f unix .", 15 | "lint-fix": "eslint -f unix . --fix", 16 | "lint:sol": "solhint -f unix \"contracts/**/*.sol\" --max-warnings 0", 17 | "test": "yarn tsc && yarn truffle-test-compile-all", 18 | "test-fork": "truffle test test/PermitERC20UniswapV3Paymaster.test.ts test/TokenPaymasterProvider.test.ts --compile-all --network npmtest", 19 | "test-only": "truffle test --network npmtest", 20 | "truffle-test-compile-all": "truffle test --compile-all --network npmtest", 21 | "rm-dist": "rm -rf tsconfig.tsbuildinfo dist build" 22 | }, 23 | "main": "dist/src/index.js", 24 | "files": [ 25 | "src/*", 26 | "dist/src/*", 27 | "dist/build/*", 28 | "dist/types/*", 29 | "types/*", 30 | "contracts/*", 31 | "build/contracts/*", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "dependencies": { 38 | "@metamask/eth-sig-util": "^5.1.0", 39 | "@opengsn/common": "^3.0.0-beta.10", 40 | "@opengsn/contracts": "^3.0.0-beta.10", 41 | "@opengsn/dev": "^3.0.0-beta.10", 42 | "@opengsn/provider": "^3.0.0-beta.10", 43 | "@openzeppelin/contracts": "^4.2.0", 44 | "@uniswap/v3-periphery": "^1.1.1", 45 | "ethereumjs-util": "^7.1.0", 46 | "web3": "^1.7.4", 47 | "web3-eth-abi": "^1.7.4", 48 | "web3-eth-accounts": "^1.7.4", 49 | "web3-eth-contract": "^1.7.4", 50 | "web3-utils": "^1.7.4" 51 | }, 52 | "devDependencies": { 53 | "@ethersproject/abi": "^5.6.4", 54 | "@ethersproject/abstract-signer": "^5.7.0", 55 | "@ethersproject/providers": "^5.7.2", 56 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", 57 | "@nomiclabs/hardhat-etherscan": "^2.1.8", 58 | "@nomiclabs/hardhat-web3": "^2.0.0", 59 | "@opengsn/cli": "^3.0.0-beta.10", 60 | "@openzeppelin/test-helpers": "^0.5.15", 61 | "@truffle/hdwallet-provider": "^2.0.10", 62 | "@types/chai-as-promised": "^7.1.3", 63 | "@types/ethereumjs-util": "^6.1.0", 64 | "@types/web3": "1.2.2", 65 | "bn.js": "^5.2.1", 66 | "chai": "^4.2.0", 67 | "chai-as-promised": "^7.1.1", 68 | "chalk": "^4.1.2", 69 | "ethers": "^5.7.2", 70 | "hardhat": "^2.6.8", 71 | "hardhat-deploy": "^0.11.5", 72 | "solhint": "^3.3.2", 73 | "ts-node": "8.6.2", 74 | "web3-core": "^1.7.4" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/paymasters/src/VerifyingPaymasterUtils.ts: -------------------------------------------------------------------------------- 1 | import { type RelayRequest, type ForwardRequest, type RelayData } from '@opengsn/common' 2 | import { ecsign, keccak256, toRpcSig, type PrefixedHexString } from 'ethereumjs-util' 3 | 4 | import abiCoder, { type AbiCoder } from 'web3-eth-abi' 5 | 6 | const abi: AbiCoder = abiCoder as any 7 | 8 | /** 9 | * sign a relay request, so that VerifyingPaymaster will accept it. 10 | * This method should be called on a server after performing verification of the request. 11 | * the signerPrivateKey is the private-key of the signer passed to VerifyingPaymaster.setSigner() 12 | */ 13 | export function signRelayRequest (relayRequest: RelayRequest, signerPrivateKey: Buffer): PrefixedHexString { 14 | const sig = ecsign(getRequestHash(relayRequest), signerPrivateKey) 15 | return toRpcSig(sig.v, sig.r, sig.s) 16 | } 17 | 18 | export function getRequestHash (relayRequest: RelayRequest): Buffer { 19 | return keccak256(Buffer.concat([ 20 | Buffer.from(packForwardRequest(relayRequest.request).slice(2), 'hex'), 21 | Buffer.from(packRelayData(relayRequest.relayData).slice(2), 'hex') 22 | ])) 23 | } 24 | 25 | export function packForwardRequest (req: ForwardRequest): string { 26 | return abi.encodeParameters( 27 | ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes'], 28 | [req.from, req.to, req.value, req.gas, req.nonce, req.data]) 29 | } 30 | 31 | export function packRelayData (data: RelayData): string { 32 | return abi.encodeParameters( 33 | ['uint256', 'uint256', 'address', 'address', 'bytes', 'uint256'], 34 | [data.maxFeePerGas, data.maxPriorityFeePerGas, data.relayWorker, data.paymaster, data.paymasterData, data.clientId]) 35 | } 36 | -------------------------------------------------------------------------------- /packages/paymasters/src/WrapContract.ts: -------------------------------------------------------------------------------- 1 | import { type Contract, providers } from 'ethers' 2 | import { type ExternalProvider, type JsonRpcProvider } from '@ethersproject/providers' 3 | import { type Signer } from '@ethersproject/abstract-signer' 4 | 5 | import { TokenPaymasterProvider } from './TokenPaymasterProvider' 6 | import { 7 | type Address, 8 | type GSNConfig, 9 | type GSNDependencies, 10 | type GSNUnresolvedConstructorInput, 11 | type SupportedTokenSymbols 12 | } from '@opengsn/provider' 13 | 14 | async function wrapContract ( 15 | contract: Contract, 16 | config: Partial, 17 | overrideDependencies?: Partial 18 | ): Promise { 19 | const signer = await wrapSigner(contract.signer, config, overrideDependencies) 20 | return contract.connect(signer) 21 | } 22 | 23 | async function wrapSigner ( 24 | signer: Signer, 25 | config: Partial, 26 | overrideDependencies?: Partial, 27 | permitERC20TokenForGas?: Address | SupportedTokenSymbols): Promise { 28 | const provider = signer.provider 29 | if (provider == null) { 30 | throw new Error('GSN requires a Signer instance with a provider to wrap it') 31 | } 32 | const input: GSNUnresolvedConstructorInput = { 33 | provider: provider as JsonRpcProvider, 34 | config, 35 | overrideDependencies 36 | } 37 | 38 | const gsnProvider = await TokenPaymasterProvider.newProvider(input).init(permitERC20TokenForGas) 39 | const gsnExternalProvider = gsnProvider as any as ExternalProvider 40 | const ethersProvider = new providers.Web3Provider(gsnExternalProvider) 41 | const address = await signer.getAddress() 42 | return ethersProvider.getSigner(address) 43 | } 44 | 45 | export const TokenPaymasterEthersWrapper = { 46 | wrapContract, 47 | wrapSigner 48 | } 49 | -------------------------------------------------------------------------------- /packages/paymasters/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HashCashApproval' 2 | export * from './PermitPaymasterUtils' 3 | export * from './TokenPaymasterInteractor' 4 | export * from './TokenPaymasterProvider' 5 | export * from './VerifyingPaymasterUtils' 6 | export * from './constants/MainnetPermitERC20UniswapV3PaymasterConstants' 7 | -------------------------------------------------------------------------------- /packages/paymasters/test/ForkTestUtils.ts: -------------------------------------------------------------------------------- 1 | import { toBN } from 'web3-utils' 2 | import { type EIP712Domain } from '@opengsn/common/dist/EIP712/TypedRequestData' 3 | 4 | import { 5 | DAI_CONTRACT_ADDRESS, 6 | UNI_CONTRACT_ADDRESS, 7 | USDC_CONTRACT_ADDRESS 8 | } from '@opengsn/common' 9 | 10 | export function getDaiDomainSeparator (): EIP712Domain { 11 | return { 12 | name: 'Dai Stablecoin', 13 | version: '1', 14 | chainId: 1, 15 | verifyingContract: DAI_CONTRACT_ADDRESS 16 | } 17 | } 18 | 19 | export function getUSDCDomainSeparator (): EIP712Domain { 20 | return { 21 | name: 'USD Coin', 22 | version: '2', 23 | chainId: 1, 24 | verifyingContract: USDC_CONTRACT_ADDRESS 25 | } 26 | } 27 | 28 | export function getUniDomainSeparator (): EIP712Domain { 29 | return { 30 | name: 'Uniswap', 31 | chainId: 1, 32 | verifyingContract: UNI_CONTRACT_ADDRESS 33 | } 34 | } 35 | 36 | export async function detectMainnet (): Promise { 37 | const code = await web3.eth.getCode(DAI_CONTRACT_ADDRESS) 38 | return code !== '0x' 39 | } 40 | 41 | export async function skipWithoutFork (test: any): Promise { 42 | const isMainnet = await detectMainnet() 43 | if (!isMainnet) { 44 | test.skip() 45 | } 46 | } 47 | 48 | export async function impersonateAccount (address: string): Promise { 49 | await new Promise((resolve, reject) => { 50 | // @ts-ignore 51 | web3.currentProvider.send({ 52 | jsonrpc: '2.0', 53 | method: 'hardhat_impersonateAccount', 54 | params: [address], 55 | id: Date.now() 56 | }, (e: Error | null, r: any) => { 57 | if (e != null) { 58 | reject(e) 59 | } else { 60 | resolve(r) 61 | } 62 | }) 63 | }) 64 | } 65 | 66 | // as we are using forked mainnet, we will need to impersonate an account with a lot of DAI & UNI 67 | export const MAJOR_DAI_AND_UNI_HOLDER = '0x28C6c06298d514Db089934071355E5743bf21d60' // '0x47ac0fb4f2d84898e4d9e7b4dab3c24507a6d503' 68 | 69 | export const ETHER = toBN(1e18.toString()) 70 | export const GAS_PRICE = '10000000000' 71 | -------------------------------------------------------------------------------- /packages/paymasters/test/TestUniswap.test.ts: -------------------------------------------------------------------------------- 1 | /* global contract artifacts before it */ 2 | 3 | import { type TestTokenInstance, type TestUniswapInstance } from '../types/truffle-contracts' 4 | import BN from 'bn.js' 5 | import { MAX_INTEGER } from 'ethereumjs-util' 6 | import { toWei } from 'web3-utils' 7 | 8 | const TestUniswap = artifacts.require('TestUniswap') 9 | const TestToken = artifacts.require('TestToken') 10 | 11 | contract('#TestUniswap', ([from]) => { 12 | let uniswap: TestUniswapInstance 13 | let token: TestTokenInstance 14 | 15 | before(async () => { 16 | uniswap = await TestUniswap.new(2, 1, { value: toWei('5') }) 17 | token = await TestToken.at(await uniswap.tokenAddress()) 18 | // approve uniswap to take our tokens. 19 | // @ts-ignore 20 | await token.approve(uniswap.address, MAX_INTEGER) 21 | }) 22 | 23 | it('check exchange rate', async () => { 24 | assert.equal((await uniswap.getTokenToEthOutputPrice(2e10)).toString(), (4e10).toString()) 25 | assert.equal((await uniswap.getTokenToEthInputPrice(2e10)).toString(), (1e10).toString()) 26 | }) 27 | 28 | it.skip('swap token to eth', async () => { 29 | /* 30 | await token.mint(10e18.toString()) 31 | const ethBefore = await web3.eth.getBalance(from) 32 | const tokensBefore = await token.balanceOf(from) 33 | // zero price for easier calculation 34 | await uniswap.tokenToEthSwapOutput(2e18.toString(), -1, -1, { gasPrice: 0 }) 35 | const ethAfter = await web3.eth.getBalance(from) 36 | const tokensAfter = await token.balanceOf(from) 37 | 38 | assert.equal((tokensAfter - tokensBefore) / 1e18, -4) 39 | assert.equal((ethAfter - ethBefore) / 1e18, 2) 40 | */ 41 | }) 42 | 43 | it('swap and transfer', async () => { 44 | await token.mint(10e18.toString()) 45 | const target = '0x' + '1'.repeat(40) 46 | const tokensBefore = await token.balanceOf(from) 47 | const ethBefore = await web3.eth.getBalance(target) 48 | // @ts-ignore 49 | await uniswap.tokenToEthTransferOutput(2e18.toString(), MAX_INTEGER, MAX_INTEGER, target) 50 | const tokensAfter = await token.balanceOf(from) 51 | 52 | const ethAfter = await web3.eth.getBalance(target) 53 | // @ts-ignore 54 | assert.equal((tokensAfter.sub(tokensBefore)).div(new BN((1e18).toString())).toNumber(), -4) 55 | assert.equal((parseInt(ethAfter) - parseInt(ethBefore)) / 1e18, 2) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/paymasters/truffle.js: -------------------------------------------------------------------------------- 1 | require('ts-node/register/transpile-only') 2 | 3 | const HDWalletProvider = require('@truffle/hdwallet-provider') 4 | 5 | let mnemonic = 'digital unknown jealous mother legal hedgehog save glory december universe spread figure custom found six' 6 | 7 | if (process.env.MNEMONIC_FILE !== '' && process.env.MNEMONIC_FILE != null) { 8 | console.error(`== reading mnemonic file: ${process.env.MNEMONIC_FILE}`) 9 | mnemonic = require('fs').readFileSync(process.env.MNEMONIC_FILE, 'utf-8').replace(/(\r\n|\n|\r)/gm, '') 10 | } 11 | 12 | module.exports = { 13 | networks: { 14 | development: { 15 | verbose: process.env.VERBOSE, 16 | host: '127.0.0.1', 17 | port: 8545, 18 | network_id: '*' 19 | }, 20 | npmtest: { // used from "npm test". see package.json 21 | verbose: process.env.VERBOSE, 22 | host: '127.0.0.1', 23 | port: 8544, 24 | network_id: '*' 25 | }, 26 | goerli: { 27 | provider: function () { 28 | return new HDWalletProvider(mnemonic, 'https://goerli.infura.io/v3/f40be2b1a3914db682491dc62a19ad43') 29 | }, 30 | network_id: 5 31 | } 32 | }, 33 | compilers: { 34 | solc: { 35 | version: '0.8.7', 36 | settings: { 37 | evmVersion: 'london', 38 | optimizer: { 39 | enabled: true, 40 | runs: 200 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/paymasters/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "./src/**/*.ts", 5 | "./src/**/*.json", 6 | "./test/**/*.ts", 7 | "./deployments/deployment-config.ts", 8 | "./build/contracts/*.json", 9 | "./types/ethers-contracts/*.ts", 10 | "./types/ethers-contracts/**/*.ts", 11 | "./types/truffle-contracts/index.d.ts", 12 | "./types/truffle-contracts/types.d.ts" 13 | ], 14 | "compilerOptions": { 15 | "outDir": "dist" 16 | } 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/paymasters/tsconfig.packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "truffle.js", 5 | "deploy/deploy.ts", 6 | "hardhat.config.ts", 7 | "../paymasters/test/**/*.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/provider/README.md: -------------------------------------------------------------------------------- 1 | # GSN Web3 Provider 2 | 3 | [GSN, the Ethereum Gas Station Network](https://opengsn.org/) abstracts away gas to minimize onboarding & UX friction for dapps. 4 | 5 | This module contains a Web3 provider for client-facing JavaScript dapps that uses GSN to relay transactions. 6 | 7 | [Please see the tutorial here.](https://docs.opengsn.org/javascript-client/getting-started.html#adding-gsn-support-to-existing-app) 8 | 9 | -------------------------------------------------------------------------------- /packages/provider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/provider", 3 | "version": "3.0.0-beta.10", 4 | "license": "Apache-2.0", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "tsc": "tsc", 8 | "watch-tsc": "tsc -w --preserveWatchOutput", 9 | "lint": "eslint -f unix .", 10 | "lint-fix": "yarn lint --fix", 11 | "rm-dist": "rm -rf tsconfig.tsbuildinfo dist" 12 | }, 13 | "files": [ 14 | "dist/*", 15 | "README.md" 16 | ], 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "dependencies": { 21 | "@ethersproject/abi": "^5.7.0", 22 | "@ethersproject/abstract-signer": "^5.7.0", 23 | "@ethersproject/bignumber": "^5.7.0", 24 | "@ethersproject/providers": "^5.7.2", 25 | "@ethersproject/transactions": "^5.7.0", 26 | "@ethersproject/wallet": "^5.7.0", 27 | "@metamask/eth-sig-util": "^5.1.0", 28 | "@opengsn/common": "^3.0.0-beta.10", 29 | "@opengsn/contracts": "^3.0.0-beta.10", 30 | "ethereumjs-util": "^7.1.0", 31 | "ethereumjs-wallet": "^1.0.2", 32 | "ethers": "^5.7.2", 33 | "ethers-v6": "npm:ethers@^6.6.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/provider/src/ConnectContractToGSN.ts: -------------------------------------------------------------------------------- 1 | import { type Contract } from 'ethers' 2 | import { type Contract as ContractV6, type BaseContract as BaseContractV6 } from 'ethers-v6' 3 | 4 | import { RelayProvider } from './RelayProvider' 5 | import { type GSNConfig, type GSNDependencies } from './GSNConfigurator' 6 | 7 | export async function connectContractToGSN ( 8 | contract: Contract, 9 | config: Partial, 10 | overrideDependencies?: Partial 11 | ): Promise { 12 | const { gsnSigner } = await RelayProvider.newEthersV5Provider({ 13 | provider: contract.signer, config, overrideDependencies 14 | }) 15 | return contract.connect(gsnSigner) 16 | } 17 | 18 | /** 19 | * @experimental support for Ethers.js v6 in GSN is highly experimental! 20 | * Creates a new instance of the GSN Signer and connects the given contract to it. 21 | * @returns a new contract instance 22 | */ 23 | export async function connectContractV6ToGSN ( 24 | contract: ContractV6, 25 | config: Partial, 26 | overrideDependencies?: Partial 27 | ): Promise { 28 | const signer = contract.runner as any 29 | if (signer == null) { 30 | throw new Error('contract not connected!') 31 | } 32 | const { gsnSigner } = await RelayProvider.newEthersV6Provider({ 33 | provider: signer, config, overrideDependencies 34 | }) 35 | return contract.connect(gsnSigner) 36 | } 37 | -------------------------------------------------------------------------------- /packages/provider/src/GSNConfigurator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ApprovalDataCallback, 3 | type ContractInteractor, 4 | type GSNConfig, 5 | type HttpClient, 6 | type LoggerConfiguration, 7 | type LoggerInterface, 8 | type PaymasterDataCallback, 9 | type PingFilter, 10 | type RelayCallGasLimitCalculationHelper, 11 | type RelayFilter, 12 | type SignTypedDataCallback, 13 | defaultEnvironment, 14 | gsnRequiredVersion, 15 | gsnRuntimeVersion 16 | } from '@opengsn/common' 17 | 18 | import { type AccountManager } from './AccountManager' 19 | 20 | import { type KnownRelaysManager } from './KnownRelaysManager' 21 | import { type RelayedTransactionValidator } from './RelayedTransactionValidator' 22 | 23 | export { type GSNConfig } from '@opengsn/common' 24 | 25 | const GAS_PRICE_PERCENT = 20 26 | const GAS_PRICE_SLACK_PERCENT = 80 27 | const MAX_RELAY_NONCE_GAP = 3 28 | const DEFAULT_RELAY_TIMEOUT_GRACE_SEC = 1800 29 | 30 | export const defaultLoggerConfiguration: LoggerConfiguration = { 31 | logLevel: 'info' 32 | } 33 | 34 | export const defaultGsnConfig: GSNConfig = { 35 | calldataEstimationSlackFactor: 1, 36 | preferredRelays: [], 37 | blacklistedRelays: [], 38 | pastEventsQueryMaxPageSize: Number.MAX_SAFE_INTEGER, 39 | pastEventsQueryMaxPageCount: 20, 40 | gasPriceFactorPercent: GAS_PRICE_PERCENT, 41 | gasPriceSlackPercent: GAS_PRICE_SLACK_PERCENT, 42 | getGasFeesBlocks: 5, 43 | getGasFeesPercentile: 50, 44 | gasPriceOracleUrl: '', 45 | gasPriceOraclePath: '', 46 | minMaxPriorityFeePerGas: 1e9, 47 | maxRelayNonceGap: MAX_RELAY_NONCE_GAP, 48 | relayTimeoutGrace: DEFAULT_RELAY_TIMEOUT_GRACE_SEC, 49 | methodSuffix: '_v4', 50 | requiredVersionRange: gsnRequiredVersion, 51 | jsonStringifyRequest: true, 52 | auditorsCount: 0, 53 | skipErc165Check: false, 54 | clientId: '1', 55 | requestValidSeconds: 172800, // 2 days 56 | maxViewableGasLimit: '20000000', 57 | minViewableGasLimit: '300000', 58 | environment: defaultEnvironment, 59 | maxApprovalDataLength: 0, 60 | maxPaymasterDataLength: 0, 61 | clientDefaultConfigUrl: `https://client-config.opengsn.org/${gsnRuntimeVersion}/client-config.json`, 62 | useClientDefaultConfigUrl: true, 63 | performDryRunViewRelayCall: true, 64 | performEstimateGasFromRealSender: false, 65 | paymasterAddress: '', 66 | tokenPaymasterDomainSeparators: {}, 67 | waitForSuccessSliceSize: 3, 68 | waitForSuccessPingGrace: 3000, 69 | domainSeparatorName: 'GSN Relayed Transaction' 70 | } 71 | 72 | export interface GSNDependencies { 73 | httpClient: HttpClient 74 | logger?: LoggerInterface 75 | contractInteractor: ContractInteractor 76 | gasLimitCalculator: RelayCallGasLimitCalculationHelper 77 | knownRelaysManager: KnownRelaysManager 78 | accountManager: AccountManager 79 | transactionValidator: RelayedTransactionValidator 80 | pingFilter: PingFilter 81 | relayFilter: RelayFilter 82 | asyncApprovalData: ApprovalDataCallback 83 | asyncPaymasterData: PaymasterDataCallback 84 | asyncSignTypedData?: SignTypedDataCallback 85 | } 86 | -------------------------------------------------------------------------------- /packages/provider/src/GsnEvents.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * base export class for all events fired by RelayClient. 3 | * for "progress" report, it is enough to test the base export class only. 4 | * subclasses contain some extra information about the events. 5 | * Last event is when we receive response from relayer that event was sent - now we should wait for mining.. 6 | */ 7 | const TOTAL_EVENTS = 7 8 | export class GsnEvent { 9 | total = TOTAL_EVENTS 10 | constructor (readonly event: string, readonly step: number) {} 11 | } 12 | 13 | // initialize client (should be done before all requests. not counted in "total") 14 | export class GsnInitEvent extends GsnEvent { 15 | constructor () { super('init', 0) } 16 | } 17 | 18 | export class GsnRefreshRelaysEvent extends GsnEvent { 19 | constructor () { super('refresh-relays', 1) } 20 | } 21 | 22 | export class GsnDoneRefreshRelaysEvent extends GsnEvent { 23 | constructor (readonly relaysCount: number) { super('refreshed-relays', 2) } 24 | } 25 | 26 | export class GsnNextRelayEvent extends GsnEvent { 27 | constructor (readonly relayUrl: string) { super('next-relay', 3) } 28 | } 29 | 30 | export class GsnSignRequestEvent extends GsnEvent { 31 | constructor () { super('sign-request', 4) } 32 | } 33 | 34 | // before sending the request to the relayer, the client attempt to verify it will succeed. 35 | // validation may fail if the paymaster rejects the request 36 | export class GsnValidateRequestEvent extends GsnEvent { 37 | constructor () { super('validate-request', 5) } 38 | } 39 | 40 | export class GsnSendToRelayerEvent extends GsnEvent { 41 | constructor (readonly relayUrl: string) { super('send-to-relayer', 6) } 42 | } 43 | 44 | export class GsnRelayerResponseEvent extends GsnEvent { 45 | constructor (readonly success: boolean) { super('relayer-response', 7) } 46 | } 47 | -------------------------------------------------------------------------------- /packages/provider/src/VerifierUtils.ts: -------------------------------------------------------------------------------- 1 | import { type PrefixedHexString } from 'ethereumjs-util' 2 | import { type ApprovalDataCallback, type HttpWrapper, type LoggerInterface, type RelayRequest, appendSlashTrim } from '@opengsn/common' 3 | 4 | // TODO: replace with production URL before release 5 | export const DEFAULT_VERIFIER_SERVER_URL = 'https://staging-api.opengsn.org' 6 | export const DEFAULT_VERIFIER_SERVER_APPROVAL_DATA_LENGTH = 65 7 | 8 | export interface ApprovalRequest { 9 | apiKey: string 10 | chainId: number 11 | domainSeparatorName: string 12 | relayRequest: RelayRequest 13 | relayRequestId: string 14 | } 15 | 16 | export function createVerifierApprovalDataCallback ( 17 | httpWrapper: HttpWrapper, 18 | logger: LoggerInterface, 19 | domainSeparatorName: string, 20 | chainId: number, 21 | apiKey: string, 22 | verifierUrl: string 23 | ): ApprovalDataCallback { 24 | return async function defaultVerifierApprovalDataCallback ( 25 | relayRequest: RelayRequest, 26 | relayRequestId: PrefixedHexString 27 | ) { 28 | const approvalRequest: ApprovalRequest = { 29 | apiKey, 30 | chainId, 31 | domainSeparatorName, 32 | relayRequest, 33 | relayRequestId 34 | } 35 | const signRelayRequestResponse = await httpWrapper.sendPromise(new URL('signRelayRequest', appendSlashTrim(verifierUrl)), approvalRequest) 36 | logger.info(`signRelayRequest response: ${JSON.stringify(signRelayRequestResponse)}`) 37 | return signRelayRequestResponse.signature 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/provider/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@opengsn/common' 2 | 3 | export * from './AccountManager' 4 | export * from './GSNConfigurator' 5 | export * from './GsnEvents' 6 | export * from './KnownRelaysManager' 7 | export * from './RelayClient' 8 | export * from './RelayedTransactionValidator' 9 | export * from './RelayProvider' 10 | export * from './RelaySelectionManager' 11 | export * from './ConnectContractToGSN' 12 | -------------------------------------------------------------------------------- /packages/provider/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "**/*.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/provider/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "../contracts/types/truffle-contracts/index.d.ts", 6 | "../contracts/types/truffle-contracts/types.d.ts" 7 | ], 8 | "compilerOptions": { 9 | "rootDir": "src", 10 | "outDir": "dist" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/relay/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@types/nedb", "web3-eth-accounts"] 2 | -------------------------------------------------------------------------------- /packages/relay/README.md: -------------------------------------------------------------------------------- 1 | # GSN Relay Server 2 | 3 | [GSN, the Ethereum Gas Station Network](https://opengsn.org/) abstracts away gas to minimize onboarding & UX friction for dapps. 4 | 5 | This module contains Relay Server source code. Note that it is recommended to use a [Docker container](https://hub.docker.com/repository/docker/opengsn/jsrelay) `opengsn/jsrelay` to run your own Relay Servers. 6 | 7 | [Please see the tutorial here.](https://docs.opengsn.org/relay-server/tutorial.html) 8 | 9 | 10 | You are probably looking for one of these: 11 | 12 | `@opengsn/provider` - a Web3 provider for client-facing JavaScript dapps that uses GSN to relay transactions. 13 | 14 | `@opengsn/contracts` - all GSN contracts in Solidity language as well as their TypeScript type declarations. 15 | 16 | `@opengsn/cli` - command-line tools made to help developers integrate GSN, test their integrations locally, or bring up relay servers on public Ethereum networks. 17 | -------------------------------------------------------------------------------- /packages/relay/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengsn/relay", 3 | "version": "3.0.0-beta.10", 4 | "license": "GPL-3.0-only", 5 | "main": "dist/runServer.js", 6 | "files": [ 7 | "dist/*", 8 | "README.md" 9 | ], 10 | "scripts": { 11 | "tsc": "tsc", 12 | "watch-tsc": "tsc -w --preserveWatchOutput", 13 | "lint": "eslint -f unix .", 14 | "lint-fix": "yarn lint --fix", 15 | "rm-dist": "rm -rf tsconfig.tsbuildinfo dist" 16 | }, 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "dependencies": { 21 | "@ethereumjs/tx": "^3.2.0", 22 | "@ethersproject/bignumber": "^5.7.0", 23 | "@ethersproject/providers": "^5.7.2", 24 | "@opengsn/common": "^3.0.0-beta.10", 25 | "@opengsn/contracts": "^3.0.0-beta.10", 26 | "@opengsn/logger": "^3.0.0-beta.10", 27 | "@seald-io/nedb": "^3.0.0", 28 | "@types/cors": "^2.8.12", 29 | "@types/express": "^4.17.13", 30 | "@types/minimist": "^1.2.0", 31 | "@types/nedb": "^1.8.12", 32 | "abi-decoder": "^2.4.0", 33 | "async-mutex": "^0.4.0", 34 | "axios": "^0.27.2", 35 | "bn.js": "^5.2.1", 36 | "body-parser": "^1.20.0", 37 | "chalk": "^4.1.2", 38 | "cors": "^2.8.5", 39 | "ethereumjs-util": "^7.1.0", 40 | "ethereumjs-wallet": "^1.0.2", 41 | "ethval": "^2.1.1", 42 | "express": "^4.18.1", 43 | "minimist": "^1.2.6", 44 | "ow": "^0.28.1", 45 | "rlp": "^2.2.7", 46 | "web3": "^1.7.4", 47 | "web3-core-helpers": "^1.7.4", 48 | "web3-eth-accounts": "^1.7.4", 49 | "web3-eth-contract": "^1.7.4", 50 | "web3-utils": "^1.7.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/relay/src/GasPriceFetcher.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber' 2 | 3 | import { type LoggerInterface, type ContractInteractor } from '@opengsn/common' 4 | 5 | import axios from 'axios' 6 | 7 | export class GasPriceFetcher { 8 | constructor (readonly gasPriceOracleUrl: string, readonly gasPriceOraclePath: string, 9 | readonly contractInteractor: ContractInteractor, 10 | readonly logger: LoggerInterface) { 11 | } 12 | 13 | // equivalent to `eval("blob"+path)` - but without evil eval 14 | // path is sequence of `.word` , `[number]`, `["string"]` 15 | getJsonElement (blob: any, path: string, origPath = path): string | null { 16 | const m = path.match(/^\.(\w+)|\["([^"]+)"\]|\[(\d+)\]/) 17 | if (m == null) throw new Error(`invalid path: ${origPath}: head of ${path}`) 18 | const rest = path.slice(m[0].length) 19 | const subitem = m[1] ?? m[2] ?? m[3] 20 | const sub = blob[subitem] 21 | if (sub == null) { 22 | return null 23 | } 24 | if (rest === '') { 25 | return sub 26 | } 27 | return this.getJsonElement(sub, rest, origPath) 28 | } 29 | 30 | async getGasPrice (): Promise { 31 | if (this.gasPriceOracleUrl !== '') { 32 | try { 33 | const res = await axios.get(this.gasPriceOracleUrl, { timeout: 2000 }) 34 | const ret = parseFloat(this.getJsonElement(res.data, this.gasPriceOraclePath) ?? '') 35 | if (typeof ret !== 'number' || isNaN(ret)) { 36 | throw new Error(`not a number: ${ret}`) 37 | } 38 | return BigNumber.from(ret * 1e9) 39 | } catch (e: any) { 40 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 41 | this.logger.error(`failed to access gas oracle. using getGasPrice() instead.\n(url=${this.gasPriceOracleUrl} path=${this.gasPriceOraclePath} err: ${e.message})`) 42 | } 43 | } 44 | 45 | return await this.contractInteractor.getGasPrice() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/relay/src/ReputationEntry.ts: -------------------------------------------------------------------------------- 1 | import { type Address } from '@opengsn/common' 2 | 3 | export interface ReputationChange { 4 | blockNumber: number 5 | change: number 6 | } 7 | 8 | export interface ReputationEntry { 9 | paymaster: Address 10 | reputation: number 11 | lastAcceptedRelayRequestTs: number 12 | abuseStartedBlock: number 13 | changes: ReputationChange[] 14 | } 15 | -------------------------------------------------------------------------------- /packages/relay/src/StoredTransaction.ts: -------------------------------------------------------------------------------- 1 | import { Capability, type FeeMarketEIP1559Transaction, type Transaction, type TypedTransaction } from '@ethereumjs/tx' 2 | import * as ethUtils from 'ethereumjs-util' 3 | import { type PrefixedHexString } from 'ethereumjs-util' 4 | import { type Address } from '@opengsn/common' 5 | 6 | export enum ServerAction { 7 | REGISTER_SERVER, 8 | ADD_WORKER, 9 | RELAY_CALL, 10 | VALUE_TRANSFER, 11 | DEPOSIT_WITHDRAWAL, 12 | PENALIZATION, 13 | SET_OWNER, 14 | AUTHORIZE_HUB 15 | } 16 | 17 | export interface StoredTransactionMetadata { 18 | readonly from: Address 19 | readonly attempts: number 20 | readonly serverAction: ServerAction 21 | readonly creationBlock: ShortBlockInfo 22 | readonly boostBlock?: ShortBlockInfo 23 | readonly minedBlock?: ShortBlockInfo 24 | } 25 | 26 | export interface StoredTransactionSerialized { 27 | readonly to: Address 28 | readonly gas: number 29 | maxFeePerGas: number 30 | maxPriorityFeePerGas: number 31 | readonly data: PrefixedHexString 32 | readonly nonce: number 33 | readonly txId: PrefixedHexString 34 | readonly value: PrefixedHexString 35 | readonly rawSerializedTx: PrefixedHexString 36 | } 37 | 38 | export interface NonceSigner { 39 | nonceSigner?: { 40 | nonce: number 41 | signer: Address 42 | } 43 | } 44 | 45 | export interface ShortBlockInfo { 46 | hash: PrefixedHexString 47 | number: number 48 | timestamp: number | string 49 | } 50 | 51 | export type StoredTransaction = StoredTransactionSerialized & StoredTransactionMetadata & NonceSigner 52 | 53 | /** 54 | * Make sure not to pass {@link StoredTransaction} as {@param metadata}, as it will override fields from {@param tx}! 55 | * @param tx 56 | * @param metadata 57 | */ 58 | export function createStoredTransaction (tx: TypedTransaction, metadata: StoredTransactionMetadata): StoredTransaction { 59 | if (tx.to == null) { 60 | throw new Error('tx.to must be defined') 61 | } 62 | const details: Partial = 63 | { 64 | to: ethUtils.bufferToHex(tx.to.toBuffer()), 65 | gas: ethUtils.bufferToInt(tx.gasLimit.toBuffer()), 66 | data: ethUtils.bufferToHex(tx.data), 67 | nonce: ethUtils.bufferToInt(tx.nonce.toBuffer()), 68 | txId: ethUtils.bufferToHex(tx.hash()), 69 | value: ethUtils.bufferToHex(tx.value.toBuffer()), 70 | rawSerializedTx: ethUtils.bufferToHex(tx.serialize()) 71 | } 72 | if (tx.supports(Capability.EIP1559FeeMarket)) { 73 | tx = tx as FeeMarketEIP1559Transaction 74 | details.maxFeePerGas = ethUtils.bufferToInt(tx.maxFeePerGas.toBuffer()) 75 | details.maxPriorityFeePerGas = ethUtils.bufferToInt(tx.maxPriorityFeePerGas.toBuffer()) 76 | } else { 77 | tx = tx as Transaction 78 | details.maxFeePerGas = ethUtils.bufferToInt(tx.gasPrice.toBuffer()) 79 | details.maxPriorityFeePerGas = ethUtils.bufferToInt(tx.gasPrice.toBuffer()) 80 | } 81 | return Object.assign({}, details as StoredTransactionSerialized, metadata) 82 | } 83 | -------------------------------------------------------------------------------- /packages/relay/src/Utils.ts: -------------------------------------------------------------------------------- 1 | import { type EventData } from 'web3-eth-contract' 2 | import { type ServerConfigParams } from './ServerConfigParams' 3 | import { type Address, isSameAddress, packRelayUrlForRegistrar } from '@opengsn/common' 4 | 5 | export function isRegistrationValid (registerEvent: EventData | undefined, config: ServerConfigParams, managerAddress: Address): boolean { 6 | return registerEvent != null && 7 | isSameAddress(registerEvent.returnValues.relayManager, managerAddress) && 8 | packRelayUrlForRegistrar(registerEvent.returnValues.relayUrl) === config.url 9 | } 10 | -------------------------------------------------------------------------------- /packages/relay/src/penalizer/BlockExplorerInterface.ts: -------------------------------------------------------------------------------- 1 | import { type Address } from '@opengsn/common' 2 | 3 | export interface BlockExplorerInterface { 4 | getTransactionByNonce: (account: Address, nonce: number) => Promise 5 | } 6 | 7 | export interface TransactionData { 8 | blockNumber: string 9 | timeStamp: string 10 | hash: string 11 | nonce: string 12 | blockHash: string 13 | transactionIndex: string 14 | from: string 15 | to: string 16 | value: string 17 | gas: string 18 | gasPrice: string 19 | isError: string 20 | txreceipt_status: string 21 | input: string 22 | contractAddress: string 23 | cumulativeGasUsed: string 24 | gasUsed: string 25 | confirmations: string 26 | } 27 | 28 | export interface EtherscanResponse { 29 | status: string 30 | message?: string 31 | result?: TransactionData[] 32 | } 33 | -------------------------------------------------------------------------------- /packages/relay/src/penalizer/PenalizerUtils.ts: -------------------------------------------------------------------------------- 1 | import { encode, type List } from 'rlp' 2 | import { 3 | Capability, 4 | type FeeMarketEIP1559Transaction, 5 | type Transaction, 6 | TransactionFactory, 7 | type TxOptions, 8 | type TypedTransaction 9 | } from '@ethereumjs/tx' 10 | 11 | import { 12 | bnToUnpaddedBuffer, 13 | bufferToHex, 14 | type PrefixedHexString, 15 | toBuffer, 16 | unpadBuffer 17 | } from 'ethereumjs-util' 18 | 19 | import { signatureRSV2Hex } from '@opengsn/common' 20 | 21 | export function getDataAndSignature (tx: TypedTransaction, chainId: number): { data: string, signature: string } { 22 | if (tx.to == null) { 23 | throw new Error('tx.to must be defined') 24 | } 25 | if (tx.s == null || tx.r == null || tx.v == null) { 26 | throw new Error('tx signature must be defined') 27 | } 28 | const input: List = [bnToUnpaddedBuffer(tx.nonce)] 29 | if (!tx.supports(Capability.EIP1559FeeMarket)) { 30 | input.push( 31 | bnToUnpaddedBuffer((tx as Transaction).gasPrice) 32 | ) 33 | } else { 34 | input.push( 35 | bnToUnpaddedBuffer((tx as FeeMarketEIP1559Transaction).maxPriorityFeePerGas), 36 | bnToUnpaddedBuffer((tx as FeeMarketEIP1559Transaction).maxFeePerGas) 37 | ) 38 | } 39 | input.push( 40 | bnToUnpaddedBuffer(tx.gasLimit), 41 | tx.to.toBuffer(), 42 | bnToUnpaddedBuffer(tx.value), 43 | tx.data, 44 | toBuffer(chainId), 45 | unpadBuffer(toBuffer(0)), 46 | unpadBuffer(toBuffer(0)) 47 | ) 48 | let vInt = tx.v.toNumber() 49 | if (vInt > 28) { 50 | vInt -= chainId * 2 + 8 51 | } 52 | const data = `0x${encode(input).toString('hex')}` 53 | const signature = signatureRSV2Hex(tx.r, tx.s, vInt) 54 | return { 55 | data, 56 | signature 57 | } 58 | } 59 | 60 | export function signedTransactionToHash (signedTransaction: PrefixedHexString, transactionOptions: TxOptions): PrefixedHexString { 61 | return bufferToHex(TransactionFactory.fromSerializedData(toBuffer(signedTransaction), transactionOptions).hash()) 62 | } 63 | -------------------------------------------------------------------------------- /packages/relay/src/penalizer/TransactionDataCache.ts: -------------------------------------------------------------------------------- 1 | import Nedb from '@seald-io/nedb' 2 | 3 | import { type Address, type LoggerInterface } from '@opengsn/common' 4 | 5 | import { type TransactionData } from './BlockExplorerInterface' 6 | 7 | export const TX_STORE_FILENAME = 'penalizetxcache.db' 8 | export const TX_PAGES_FILENAME = 'penalizetxpages.db' 9 | 10 | export interface PagesQueried { 11 | sender: Address 12 | page: number 13 | } 14 | 15 | export class TransactionDataCache { 16 | private readonly txstore: Nedb 17 | private readonly pagesStore: Nedb 18 | private readonly logger: LoggerInterface 19 | 20 | constructor (logger: LoggerInterface, workdir: string) { 21 | const filename = `${workdir}/${TX_STORE_FILENAME}` 22 | this.logger = logger 23 | this.txstore = new Nedb({ 24 | filename, 25 | autoload: true, 26 | timestampData: true 27 | }) 28 | this.pagesStore = new Nedb({ 29 | filename: `${workdir}/${TX_PAGES_FILENAME}`, 30 | autoload: true, 31 | timestampData: true 32 | }) 33 | this.txstore.ensureIndex({ fieldName: 'hash', unique: true }) 34 | this.pagesStore.ensureIndex({ fieldName: 'sender', unique: true }) 35 | this.logger.info(`Penalizer cache database location: ${filename}`) 36 | } 37 | 38 | async putTransactions (transactions: TransactionData[], sender: Address, page: number): Promise { 39 | const existing = await this.pagesStore.findOneAsync({ sender }) 40 | if (existing == null) { 41 | await this.pagesStore.insertAsync({ sender, page }) 42 | } else if (existing.page >= page) { 43 | throw new Error(`Trying to cache page ${page} when already have ${existing.page} pages for sender ${sender}`) 44 | } 45 | for (const transaction of transactions) { 46 | transaction.from = transaction.from.toLowerCase() 47 | await this.txstore.insertAsync(transaction) 48 | } 49 | } 50 | 51 | async getLastPageQueried (sender: Address): Promise { 52 | const lastPageQueried = await this.pagesStore.findOneAsync({ sender: sender.toLowerCase() }) 53 | if (lastPageQueried == null) { 54 | return 0 55 | } 56 | return lastPageQueried.page 57 | } 58 | 59 | async getTransactionByNonce (address: Address, nonce: number): Promise { 60 | return await this.txstore.findOneAsync({ 61 | from: address.toLowerCase(), 62 | nonce: nonce.toString() 63 | }) 64 | } 65 | 66 | async clearAll (): Promise { 67 | await this.txstore.removeAsync({}, { multi: true }) 68 | await this.pagesStore.removeAsync({}, { multi: true }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/relay/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packages.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "../contracts/types/truffle-contracts/index.d.ts", 6 | "../contracts/types/truffle-contracts/types.d.ts" 7 | ], 8 | "compilerOptions": { 9 | "rootDir": "src", 10 | "outDir": "dist" 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /scripts/extract_abi.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // extract ABI from truffle-compiled files 4 | // to a file format accepted by TruffleContract constructors 5 | 6 | const fs = require('fs') 7 | const path = require('path') 8 | // const parseArgs = require('minimist') 9 | 10 | // TODO: pass all these things as parameters 11 | // const argv = parseArgs(process.argv, { 12 | // string: filterType(, 'string'), 13 | // // boolean: filterType(ConfigParamsTypes, 'boolean'), 14 | // default: envDefaults 15 | // }) 16 | let outAbiFolder 17 | let contractsFolderToExtract 18 | let files 19 | let jsonFilesLocation 20 | if (process.argv.length >= 2 && process.argv[2] === 'paymasters') { 21 | outAbiFolder = 'packages/paymasters/src/interfaces/' 22 | contractsFolderToExtract = 'packages/paymasters/contracts/interfaces' 23 | files = fs.readdirSync(contractsFolderToExtract) 24 | files.push('PermitERC20UniswapV3Paymaster.sol') 25 | files.concat() 26 | jsonFilesLocation = 'packages/paymasters/build/contracts/' 27 | } else { 28 | outAbiFolder = 'packages/common/src/interfaces/' 29 | contractsFolderToExtract = 'packages/contracts/src/interfaces' 30 | files = fs.readdirSync(contractsFolderToExtract) 31 | files.push('IForwarder.sol') 32 | jsonFilesLocation = 'packages/cli/src/compiled/' 33 | } 34 | 35 | files.forEach(file => { 36 | const c = file.replace(/.sol/, '') 37 | 38 | const outNodeFile = outAbiFolder + '/' + c + '.json' 39 | const jsonFile = `${jsonFilesLocation}/${c.replace(/interfaces./, '')}.json` 40 | const abiStr = JSON.parse(fs.readFileSync(jsonFile, { encoding: 'utf8' })) 41 | fs.mkdirSync(path.dirname(outNodeFile), { recursive: true }) 42 | fs.writeFileSync(outNodeFile, JSON.stringify(abiStr.abi)) 43 | console.log('written "' + outNodeFile + '"') 44 | }) 45 | -------------------------------------------------------------------------------- /scripts/solpp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // apply Solidity preprocessor to the input files 4 | 5 | const fs = require('fs') 6 | const path = require('path') 7 | const solpp = require('solpp') 8 | 9 | const outAbiFolder = 'packages/contracts/solpp' 10 | const contractsFolder = 'packages/contracts/src' 11 | const configuration = { 12 | defs: { 13 | ENABLE_CONSOLE_LOG: process.env.ENABLE_CONSOLE_LOG 14 | }, 15 | noFlatten: true 16 | } 17 | console.log('SOLPP: using configuration', JSON.stringify(configuration)) 18 | 19 | async function preprocess (input, output) { 20 | const processedCode = await solpp.processFile(input, configuration) 21 | fs.mkdirSync(path.dirname(output), { recursive: true }) 22 | if (fs.existsSync(output)) { 23 | fs.chmodSync(output, 0o777) 24 | } 25 | // make generated file read-only 26 | fs.writeFileSync(output, processedCode) 27 | fs.chmodSync(output, 0o444) 28 | // keep original file timestamp 29 | const srcStats = fs.statSync(input) 30 | fs.utimesSync(output, srcStats.atime, srcStats.mtime) 31 | } 32 | 33 | let filesCount = 0 34 | 35 | const recursiveSolidityPreprocess = async function (dirPath) { 36 | const files = fs.readdirSync(dirPath) 37 | 38 | for (const file of files) { 39 | if (fs.statSync(dirPath + '/' + file).isDirectory()) { 40 | await recursiveSolidityPreprocess(dirPath + '/' + file) 41 | } else { 42 | const orig = path.join(dirPath, '/', file) 43 | const dest = orig.replace(contractsFolder, outAbiFolder) 44 | filesCount++ 45 | await preprocess(orig, dest) 46 | } 47 | } 48 | } 49 | console.time('solpp finished') 50 | console.log('solpp started') 51 | err = console.error 52 | console.error = (...params) => { 53 | if ( params[0].match(/INVALID_ALT_NUMBER/) ) return 54 | err(...params) 55 | } 56 | 57 | recursiveSolidityPreprocess(contractsFolder).then(function () { 58 | console.log(`processed ${filesCount} files`) 59 | console.timeEnd('solpp finished') 60 | }) 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./packages/cli" 5 | }, 6 | { 7 | "path": "./packages/common" 8 | }, 9 | { 10 | "path": "./packages/dev" 11 | }, 12 | { 13 | "path": "./packages/relay" 14 | }, 15 | { 16 | "path": "./packages/provider" 17 | } 18 | ], 19 | "compilerOptions": { 20 | "allowSyntheticDefaultImports": true, 21 | "types": [ 22 | "openzeppelin__test-helpers", 23 | "date-format", 24 | "chai-bn" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "**/node_modules", 4 | "**/dist" 5 | ], 6 | "compilerOptions": { 7 | "allowSyntheticDefaultImports": true, 8 | "composite": true, 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "target": "es2017", 13 | "module": "node16", 14 | "strict": true, 15 | "moduleResolution": "node16", 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "declaration": true, 19 | "experimentalDecorators": true, 20 | "emitDecoratorMetadata": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "outDir": "dist", 23 | "lib": [ 24 | "dom", 25 | "es2015", 26 | "esnext.asynciterable" 27 | ], 28 | "sourceMap": true, 29 | "typeRoots": [ 30 | "./node_modules/@types", 31 | "./types" 32 | ], 33 | "types": [ 34 | "openzeppelin__test-helpers", 35 | "date-format", 36 | "chai-bn" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /types/chai-bn/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module Chai { 2 | interface Assertion { 3 | bignumber: any 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /types/date-format/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'date-format' { 2 | export default function asString (format: string, date: Date): string 3 | } 4 | -------------------------------------------------------------------------------- /types/openzeppelin__test-helpers/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@openzeppelin/test-helpers' { 2 | export function ether (value: string): any 3 | 4 | export const expectEvent: any 5 | export const expectRevert: any 6 | export const time: any 7 | export const balance: any 8 | export const constants: any 9 | export const send: any 10 | } 11 | -------------------------------------------------------------------------------- /verdaccio/README.md: -------------------------------------------------------------------------------- 1 | # Using local registry for testing 2 | 3 | - in a background terminal, run `yarn verdaccio-start` 4 | - to publish local build, use `yarn verdaccio-publish`, which will publish into this repo 5 | (note that it requires a clean git, and make local commit - but doesn't push anything) 6 | - use `gsn/verdaccio/yarn` instead of normal `yarn` to initialize a test app 7 | - it will fetch "@opengsn" packages from our verdaccio (with a fallback to npmjs.com) 8 | - all other packages are fetched directly. 9 | - (this script simply does `yarn --use-yarnrc=gsn/verdaccio/yarnrc`) 10 | -------------------------------------------------------------------------------- /verdaccio/verdaccio.yaml: -------------------------------------------------------------------------------- 1 | storage: ./db 2 | uplinks: 3 | npmjs: 4 | url: https://registry.npmjs.org/ 5 | cache: false 6 | packages: 7 | '@*/*': 8 | access: $all 9 | publish: $all 10 | proxy: npmjs 11 | '**': 12 | access: $all 13 | publish: $all 14 | proxy: npmjs 15 | logs: 16 | - {type: stdout, format: pretty, level: info} 17 | publish: 18 | allow_offline: true 19 | -------------------------------------------------------------------------------- /verdaccio/yarn: -------------------------------------------------------------------------------- 1 | #wrapper script, to run yarn with our verdaccio config. 2 | yarn --use-yarnrc `dirname $0`/yarnrc "$@" 3 | -------------------------------------------------------------------------------- /verdaccio/yarnrc: -------------------------------------------------------------------------------- 1 | "@opengsn:registry" "http://localhost:4873" 2 | 3 | --------------------------------------------------------------------------------