diff --git a/.github/workflows/smart-guessr.yml b/.github/workflows/smart-guessr.yml new file mode 100644 index 000000000..fd1bf4176 --- /dev/null +++ b/.github/workflows/smart-guessr.yml @@ -0,0 +1,92 @@ +on: + push: + branches: + - 'main' + tags: + - 'smart-guessr/v*' + paths: + - smart-guessr/** + - .github/workflows/smart-guessr.yml + pull_request: + paths: + - smart-guessr/** + - .github/workflows/smart-guessr.yml + +name: Test and docker (smart-guessr) + +env: + REGISTRY: ghcr.io + IMAGE_NAME: blockscout/smart-guessr + +defaults: + run: + working-directory: smart-guessr + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install bun + uses: oven-sh/setup-bun@v1 + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test + if: success() || failure() + + push: + name: Docker build and docker push + needs: + - test + if: | + always() && + (needs.test.result == 'success' || needs.test.result == 'cancelled') + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions-ecosystem/action-regex-match@v2 + id: regex + with: + text: ${{ github.ref }} + regex: '^(refs\/tags\/smart-guessr\/(v\d+\.\d+\.\d+))|(refs\/heads\/(main))$' + + - name: Extract tag name + id: tags_extractor + run: | + t=${{ steps.regex.outputs.group2 }} + m=${{ steps.regex.outputs.group4 }} + (if ! [[ "$t" == "" ]]; then echo tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$t, ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest; elif ! [[ "$m" == "" ]]; then echo tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$m; else echo tags=; fi) >> $GITHUB_OUTPUT + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: "smart-guessr" + file: "smart-guessr/Dockerfile" + push: ${{ steps.tags_extractor.outputs.tags != '' }} + tags: ${{ steps.tags_extractor.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:build-cache + cache-to: ${{ github.ref == 'refs/heads/main' && format('type=registry,ref={0}/{1}:build-cache,mode=max', env.REGISTRY, env.IMAGE_NAME) || '' }} diff --git a/smart-guessr/.gitignore b/smart-guessr/.gitignore new file mode 100644 index 000000000..87e56100f --- /dev/null +++ b/smart-guessr/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +**/*.trace +**/*.zip +**/*.tar.gz +**/*.tgz +**/*.log +package-lock.json +**/*.bun \ No newline at end of file diff --git a/smart-guessr/Dockerfile b/smart-guessr/Dockerfile new file mode 100644 index 000000000..ec136179f --- /dev/null +++ b/smart-guessr/Dockerfile @@ -0,0 +1,14 @@ +FROM oven/bun + +WORKDIR /app + +COPY package.json . +COPY bun.lockb . + +RUN bun install --production + +COPY src src +COPY tsconfig.json . + +ENV NODE_ENV production +CMD ["bun", "src/index.ts"] diff --git a/smart-guessr/README.md b/smart-guessr/README.md new file mode 100644 index 000000000..688c87e69 --- /dev/null +++ b/smart-guessr/README.md @@ -0,0 +1,15 @@ +# Elysia with Bun runtime + +## Getting Started +To get started with this template, simply paste this command into your terminal: +```bash +bun create elysia ./elysia-example +``` + +## Development +To start the development server run: +```bash +bun run dev +``` + +Open http://localhost:3000/ with your browser to see the result. \ No newline at end of file diff --git a/smart-guessr/bun.lockb b/smart-guessr/bun.lockb new file mode 100755 index 000000000..1e540da0f Binary files /dev/null and b/smart-guessr/bun.lockb differ diff --git a/smart-guessr/package.json b/smart-guessr/package.json new file mode 100644 index 000000000..265933777 --- /dev/null +++ b/smart-guessr/package.json @@ -0,0 +1,23 @@ +{ + "name": "smart-guessr", + "version": "1.0.50", + "scripts": { + "dev": "bun --watch src/index.ts", + "build": "bun build src/index.ts", + "start": "NODE_ENV=production bun src/index.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@bogeychan/elysia-logger": "^0.0.20", + "@elysiajs/eden": "^1.0.7", + "@elysiajs/swagger": "^1.0.3", + "@shazow/whatsabi": "^0.11.0", + "elysia": "latest", + "ethers": "^6.11.1", + "import": "^0.0.6" + }, + "devDependencies": { + "bun-types": "latest" + }, + "module": "src/index.js" +} \ No newline at end of file diff --git a/smart-guessr/src/index.ts b/smart-guessr/src/index.ts new file mode 100644 index 000000000..8db605a02 --- /dev/null +++ b/smart-guessr/src/index.ts @@ -0,0 +1,75 @@ +import { Elysia, t } from "elysia"; +import { swagger } from '@elysiajs/swagger' +import { whatsabi } from "@shazow/whatsabi"; +import { ethers } from "ethers"; +import { logger } from "@bogeychan/elysia-logger"; + +export function getPort() { + let port = process.env.SMART_GUESSR__PORT; + if (port == undefined) { + port = "3000"; + } + return port; +} + +const signatureLookup = new whatsabi.loaders.MultiSignatureLookup([ + new whatsabi.loaders.OpenChainSignatureLookup(), + new whatsabi.loaders.FourByteSignatureLookup(), +]); + +export function initApp(port: string | number) { + return new Elysia() + .use(swagger({ + path: '/api/v1/swagger' + })) + .use( + logger({ + level: "info", + }) + ) + .get("/api/v1/abi", async ( { request, log, query }) => { + log.info(request, "New request"); + const result = await processAbi(query.address, query.provider); + log.info(request, `Found ${result.length} abi items`); + return result; + }, { + query: t.Object({ + address: t.String(), + provider: t.String(), + }), + beforeHandle({ error, query }) { + query.address = normalizeAddress(query.address); + if (!ethers.isAddress(query.address)) { + return error(400, "Invalid address value") + } + } + }) + .listen(port); +} + +const port = getPort(); +let app = initApp(port); + +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +); + +async function processAbi(address: string, provider_url: string) { + const provider = ethers.getDefaultProvider(provider_url); + + let abi = await whatsabi.autoload(address, { + provider: provider, + signatureLookup: signatureLookup, + abiLoader: false + }); + + return abi.abi +} + +function normalizeAddress(address: string) { + let normalized = address.toLowerCase(); + if (!normalized.startsWith('0x')) { + normalized = '0x' + normalized; + } + return normalized; +} \ No newline at end of file diff --git a/smart-guessr/test/index.test.ts b/smart-guessr/test/index.test.ts new file mode 100644 index 000000000..0700288e0 --- /dev/null +++ b/smart-guessr/test/index.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'bun:test' +import {getPort, initApp} from "../src"; + +const port = getPort(); +const app = initApp(port); + +describe('Processing abi requests', () => { + async function makeRequest(address: string | null, provider: string | null) { + let url = `http://localhost:${port}/api/v1/abi?`; + if (address != null) { + url = url.concat(`address=${address}&`); + } + if (provider != null) { + url = url.concat(`provider=${provider}`); + } + return await app.handle(new Request(url)) + } + + it('returns an abi with blockscout as provider', async () => { + const response = await makeRequest( + "0xe8ef418ed75d744e2868c0d2f898c6a41bb17d6e", + "https://eth.blockscout.com/api/eth-rpc" + ); + expect(response.status).toBe(200); + + const expected = JSON.parse("[{\"type\":\"function\",\"selector\":\"0xeced3873\",\"sig\":\"publicSaleDate()\",\"name\":\"publicSaleDate\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0xedec5f27\",\"sig\":\"whitelistUsers(address[])\",\"name\":\"whitelistUsers\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address[]\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xf2c4ce1e\",\"sig\":\"setNotRevealedURI(string)\",\"name\":\"setNotRevealedURI\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"string\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xf2fde38b\",\"sig\":\"transferOwnership(address)\",\"name\":\"transferOwnership\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xd0eb26b0\",\"sig\":\"setNftPerAddressLimit(uint256)\",\"name\":\"setNftPerAddressLimit\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xd5abeb01\",\"sig\":\"maxSupply()\",\"name\":\"maxSupply\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0xda3ef23f\",\"sig\":\"setBaseExtension(string)\",\"name\":\"setBaseExtension\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"string\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xe985e9c5\",\"sig\":\"isApprovedForAll(address,address)\",\"name\":\"isApprovedForAll\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"},{\"type\":\"address\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xba7d2c76\",\"sig\":\"nftPerAddressLimit()\",\"name\":\"nftPerAddressLimit\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0xc6682862\",\"sig\":\"baseExtension()\",\"name\":\"baseExtension\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0xc87b56dd\",\"sig\":\"tokenURI(uint256)\",\"name\":\"tokenURI\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xcc9ff9c6\",\"sig\":\"preSaleCost()\",\"name\":\"preSaleCost\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0xa22cb465\",\"sig\":\"setApprovalForAll(address,bool)\",\"name\":\"setApprovalForAll\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"},{\"type\":\"bool\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xa475b5dd\",\"sig\":\"reveal()\",\"name\":\"reveal\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0xb88d4fde\",\"sig\":\"safeTransferFrom(address,address,uint256,bytes)\",\"name\":\"safeTransferFrom\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"},{\"type\":\"address\",\"name\":\"\"},{\"type\":\"uint256\",\"name\":\"\"},{\"type\":\"bytes\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x8fdcf942\",\"sig\":\"setPresaleCost(uint256)\",\"name\":\"setPresaleCost\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x95d89b41\",\"sig\":\"symbol()\",\"name\":\"symbol\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0xa0712d68\",\"sig\":\"mint(uint256)\",\"name\":\"mint\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0xa18116f1\",\"sig\":\"preSaleMaxSupply()\",\"name\":\"preSaleMaxSupply\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x7f00c7a6\",\"sig\":\"setmaxMintAmount(uint256)\",\"name\":\"setmaxMintAmount\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x831e60de\",\"sig\":\"getCurrentCost()\",\"name\":\"getCurrentCost\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x83a076be\",\"sig\":\"gift(uint256,address)\",\"name\":\"gift\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"},{\"type\":\"address\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x8da5cb5b\",\"sig\":\"owner()\",\"name\":\"owner\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x715018a6\",\"sig\":\"renounceOwnership()\",\"name\":\"renounceOwnership\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x743c7f6b\",\"sig\":\"setPreSaleDate(uint256)\",\"name\":\"setPreSaleDate\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x7967a50a\",\"sig\":\"preSaleEndDate()\",\"name\":\"preSaleEndDate\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x7effc032\",\"sig\":\"maxMintAmountPresale()\",\"name\":\"maxMintAmountPresale\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x6f9fb98a\",\"sig\":\"getContractBalance()\",\"name\":\"getContractBalance\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x70a08231\",\"sig\":\"balanceOf(address)\",\"name\":\"balanceOf\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x714c5398\",\"sig\":\"getBaseURI()\",\"name\":\"getBaseURI\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x5c975abb\",\"sig\":\"paused()\",\"name\":\"paused\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x6352211e\",\"sig\":\"ownerOf(uint256)\",\"name\":\"ownerOf\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x669736c0\",\"sig\":\"setmaxMintAmountPreSale(uint256)\",\"name\":\"setmaxMintAmountPreSale\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x6f8b44b0\",\"sig\":\"setMaxSupply(uint256)\",\"name\":\"setMaxSupply\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x44a0d68a\",\"sig\":\"setCost(uint256)\",\"name\":\"setCost\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x4f6ccce7\",\"sig\":\"tokenByIndex(uint256)\",\"name\":\"tokenByIndex\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x51830227\",\"sig\":\"revealed()\",\"name\":\"revealed\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x55f804b3\",\"sig\":\"setBaseURI(string)\",\"name\":\"setBaseURI\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"string\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x3ccfd60b\",\"sig\":\"withdraw()\",\"name\":\"withdraw\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x42842e0e\",\"sig\":\"safeTransferFrom(address,address,uint256)\",\"name\":\"safeTransferFrom\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"},{\"type\":\"address\",\"name\":\"\"},{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x42f0ca0d\",\"sig\":\"setPreSaleEndDate(uint256)\",\"name\":\"setPreSaleEndDate\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x438b6300\",\"sig\":\"walletOfOwner(address)\",\"name\":\"walletOfOwner\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x2e09282e\",\"sig\":\"nftPerAddressLimitPresale()\",\"name\":\"nftPerAddressLimitPresale\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x2f745c59\",\"sig\":\"tokenOfOwnerByIndex(address,uint256)\",\"name\":\"tokenOfOwnerByIndex\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"},{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x3af32abf\",\"sig\":\"isWhitelisted(address)\",\"name\":\"isWhitelisted\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x18cae269\",\"sig\":\"addressMintedBalance(address)\",\"name\":\"addressMintedBalance\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x1985cc65\",\"sig\":\"preSaleDate()\",\"name\":\"preSaleDate\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x239c70ae\",\"sig\":\"maxMintAmount()\",\"name\":\"maxMintAmount\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x23b872dd\",\"sig\":\"transferFrom(address,address,uint256)\",\"name\":\"transferFrom\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"},{\"type\":\"address\",\"name\":\"\"},{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x0a50716b\",\"sig\":\"setNftPerAddressLimitPreSale(uint256)\",\"name\":\"setNftPerAddressLimitPreSale\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x0e54a883\",\"sig\":\"setPublicSaleDate(uint256)\",\"name\":\"setPublicSaleDate\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x13faede6\",\"sig\":\"cost()\",\"name\":\"cost\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x18160ddd\",\"sig\":\"totalSupply()\",\"name\":\"totalSupply\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x081812fc\",\"sig\":\"getApproved(uint256)\",\"name\":\"getApproved\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x081c8c44\",\"sig\":\"notRevealedUri()\",\"name\":\"notRevealedUri\",\"constant\":false,\"payable\":false,\"inputs\":[]},{\"type\":\"function\",\"selector\":\"0x095ea7b3\",\"sig\":\"approve(address,uint256)\",\"name\":\"approve\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"address\",\"name\":\"\"},{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x0a403f04\",\"sig\":\"setPresaleMaxSupply(uint256)\",\"name\":\"setPresaleMaxSupply\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"uint256\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x01ffc9a7\",\"sig\":\"supportsInterface(bytes4)\",\"name\":\"supportsInterface\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"bytes4\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x02329a29\",\"sig\":\"pause(bool)\",\"name\":\"pause\",\"constant\":false,\"payable\":false,\"inputs\":[{\"type\":\"bool\",\"name\":\"\"}]},{\"type\":\"function\",\"selector\":\"0x06fdde03\",\"sig\":\"name()\",\"name\":\"name\",\"constant\":false,\"payable\":false,\"inputs\":[]}]") + expect(await response.json()).toEqual(expected); + }) + + it('support different hex representations of address', async () => { + // '0x' prefixed lowercase + let response = await makeRequest( + "0xe8ef418ed75d744e2868c0d2f898c6a41bb17d6e", + "https://eth.blockscout.com/api/eth-rpc" + ); + expect(response.status).toBe(200); + + // Without '0x' prefix + response = await makeRequest( + "e8ef418ed75d744e2868c0d2f898c6a41bb17d6e", + "https://eth.blockscout.com/api/eth-rpc" + ); + expect(response.status).toBe(200); + + // Random case letters also work + response = await makeRequest( + "0xe8EF418ED75d744e2868c0d2f898c6a41bb17d6e", + "https://eth.blockscout.com/api/eth-rpc" + ); + expect(response.status).toBe(200); + + // Invalid address returns 'Bad Request' + response = await makeRequest( + "0xcafe", + "https://eth.blockscout.com/api/eth-rpc" + ); + expect(response.status).toBe(400); + expect(await response.text()).toContain("Invalid address"); + }) + + it('returns an empty result if address is not a contract', async () => { + // Address is an EOA + let response = await makeRequest( + "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8", + "https://eth.blockscout.com/api/eth-rpc" + ); + expect(response.status).toBe(200); + expect(await response.json()).toEqual([]) + + // Address is not in the list of existing active addresses at all + response = await makeRequest( + "0x8438932513461375132897139713853424534523", + "https://eth.blockscout.com/api/eth-rpc" + ); + expect(response.status).toBe(200); + expect(await response.json()).toEqual([]) + }) +}) \ No newline at end of file diff --git a/smart-guessr/tsconfig.json b/smart-guessr/tsconfig.json new file mode 100644 index 000000000..1ca2350ae --- /dev/null +++ b/smart-guessr/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ES2022", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}