diff --git a/examples/08-relevant-tokens-query/abis/ERC20.json b/examples/08-relevant-tokens-query/abis/ERC20.json new file mode 100644 index 0000000..405d6b3 --- /dev/null +++ b/examples/08-relevant-tokens-query/abis/ERC20.json @@ -0,0 +1,222 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] diff --git a/examples/08-relevant-tokens-query/eslint.config.mjs b/examples/08-relevant-tokens-query/eslint.config.mjs new file mode 100644 index 0000000..90af2c8 --- /dev/null +++ b/examples/08-relevant-tokens-query/eslint.config.mjs @@ -0,0 +1,89 @@ +import eslintPluginTypeScript from "@typescript-eslint/eslint-plugin" +import eslintParserTypeScript from "@typescript-eslint/parser" +import eslintPluginImport from "eslint-plugin-import" +import eslintPluginSimpleImportSort from "eslint-plugin-simple-import-sort" +import eslintConfigPrettier from "eslint-config-prettier" +import eslintPluginPrettier from "eslint-plugin-prettier" + +export default [ + { + ignores: ["node_modules/**", "**/dist/**", "**/build/**", "**/.prettierrc.*", "./src/types/**"] + }, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + parser: eslintParserTypeScript, + parserOptions: { + project: "./tsconfig.json" + } + }, + plugins: { + "@typescript-eslint": eslintPluginTypeScript, + prettier: eslintPluginPrettier, + import: eslintPluginImport, + "simple-import-sort": eslintPluginSimpleImportSort + }, + rules: { + ...eslintPluginTypeScript.configs.recommended.rules, + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-unused-vars": ["error"], + "@typescript-eslint/explicit-function-return-type": "error", + "@typescript-eslint/no-explicit-any": "error", + + "prettier/prettier": [ + "error", + { + "semi": false, + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "always", + "bracketSpacing": true, + "printWidth": 120, + "tabWidth": 2, + "useTabs": false + } + ], + + "simple-import-sort/imports": [ + "error", + { + groups: [ + ["^@?\\w"], + ["^\\.\\.(?!/?$)", "^\\.\\./?$"], + ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"] + ] + } + ], + "simple-import-sort/exports": "error", + + "comma-spacing": ["error", { before: false, after: true }], + "no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }] + }, + settings: { + "import/resolver": { + typescript: { + alwaysTryTypes: true, + project: "./tsconfig.json" + } + } + } + }, + // configuration for test files + { + files: ["tests/**/*.{ts,tsx}", "**/*.spec.{ts,tsx}", "**/*.test.{ts,tsx}"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + parser: eslintParserTypeScript, + parserOptions: { + project: "./tests/tsconfig.json" + } + }, + rules: { + "@typescript-eslint/no-unused-expressions": "off" + } + }, + eslintConfigPrettier +] \ No newline at end of file diff --git a/examples/08-relevant-tokens-query/manifest.yaml b/examples/08-relevant-tokens-query/manifest.yaml new file mode 100644 index 0000000..d56ebde --- /dev/null +++ b/examples/08-relevant-tokens-query/manifest.yaml @@ -0,0 +1,7 @@ +version: 1.0.0 +name: Relevant tokens query example +description: Example of how to use the relevant tokens query +inputs: + - chainId: int32 + - feeAmountUsd: string # e.g., '1.5' = 1.5 USD + - recipient: address diff --git a/examples/08-relevant-tokens-query/package.json b/examples/08-relevant-tokens-query/package.json new file mode 100644 index 0000000..89e98f5 --- /dev/null +++ b/examples/08-relevant-tokens-query/package.json @@ -0,0 +1,31 @@ +{ + "name": "@mimicprotocol/08-relevant-tokens-query", + "version": "0.0.1", + "license": "Unlicensed", + "private": true, + "type": "module", + "scripts": { + "build": "yarn codegen && yarn compile", + "codegen": "mimic codegen", + "compile": "mimic compile", + "test": "mimic test", + "lint": "eslint ." + }, + "devDependencies": { + "@mimicprotocol/cli": "latest", + "@mimicprotocol/lib-ts": "latest", + "@mimicprotocol/sdk": "latest", + "@mimicprotocol/test-ts": "latest", + "@types/chai": "^5.2.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.5", + "assemblyscript": "0.27.36", + "chai": "^4.3.7", + "eslint": "^9.10.0", + "json-as": "1.1.7", + "mocha": "^10.2.0", + "tsx": "^4.20.3", + "typescript": "^5.8.3", + "visitor-as": "0.11.4" + } +} \ No newline at end of file diff --git a/examples/08-relevant-tokens-query/src/task.ts b/examples/08-relevant-tokens-query/src/task.ts new file mode 100644 index 0000000..d631e96 --- /dev/null +++ b/examples/08-relevant-tokens-query/src/task.ts @@ -0,0 +1,22 @@ +import { DenominationToken, environment, ListType, log, TokenAmount, TransferBuilder, USD } from '@mimicprotocol/lib-ts' + +import { inputs } from './types' + +export default function main(): void { + const context = environment.getContext() + const tokens = environment.getRelevantTokens(context.user, [inputs.chainId], USD.zero(), [], ListType.DenyList) + const builder = TransferBuilder.forChain(inputs.chainId) + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + builder.addTransferFromTokenAmount(token, inputs.recipient) + log.info(`Adding transfer for ${token} on chain ${inputs.chainId}`) + } + + if (builder.transfers.length == 0) { + log.info(`No tokens found on chain ${inputs.chainId}`) + return + } + + builder.addMaxFee(TokenAmount.fromStringDecimal(DenominationToken.USD(), inputs.feeAmountUsd)).build().send() +} diff --git a/examples/08-relevant-tokens-query/tests/task.spec.ts b/examples/08-relevant-tokens-query/tests/task.spec.ts new file mode 100644 index 0000000..494cbb3 --- /dev/null +++ b/examples/08-relevant-tokens-query/tests/task.spec.ts @@ -0,0 +1,110 @@ +import { NATIVE_TOKEN_ADDRESS, OpType, randomEvmAddress } from '@mimicprotocol/sdk' +import { ContractCallMock, GetRelevantTokensMock, runTask, Transfer } from '@mimicprotocol/test-ts' +import { expect } from 'chai' + +describe('Task', () => { + const taskDir = './build' + + const chainId = 1 + const USDC = randomEvmAddress() + + const context = { + user: randomEvmAddress(), + settlers: [{ address: randomEvmAddress(), chainId }], + timestamp: 1438223173000, + } + + const inputs = { + chainId, + feeAmountUsd: '0.1', + recipient: randomEvmAddress(), + } + + const calls: ContractCallMock[] = [ + { + request: { chainId, to: USDC, fnSelector: '0x313ce567' }, + response: { value: '6', abiType: 'uint8' }, + }, + { + request: { chainId, to: USDC, fnSelector: '0x95d89b41' }, + response: { value: 'USDC', abiType: 'string' }, + }, + ] + + describe('when the user has some balance for the requested tokens', () => { + const relevantTokens: GetRelevantTokensMock[] = [ + { + request: { + owner: context.user, + chainIds: [chainId], + usdMinAmount: '0', + tokenFilter: 1, + tokens: [], + }, + response: [ + { + timestamp: Date.now(), + balances: [ + { token: { address: USDC, chainId }, balance: '10' }, + { token: { address: NATIVE_TOKEN_ADDRESS, chainId }, balance: '100' }, + ], + }, + ], + }, + ] + + it('produces the expected intents for multiple tokens', async () => { + const result = await runTask(taskDir, context, { inputs, calls, relevantTokens }) + expect(result.success).to.be.true + expect(result.timestamp).to.be.equal(context.timestamp) + + const intents = result.intents as Transfer[] + expect(intents).to.have.lengthOf(1) + + expect(intents[0].op).to.be.equal(OpType.Transfer) + expect(intents[0].chainId).to.equal(chainId) + expect(intents[0].maxFees.length).to.equal(1) + expect(intents[0].maxFees[0].token).to.equal('0x0000000000000000000000000000000000000348') + expect(intents[0].maxFees[0].amount).to.equal('100000000000000000') + expect(intents[0].user).to.equal(context.user) + expect(intents[0].transfers).to.have.lengthOf(2) + + const firstTransfer = intents[0].transfers[0] + expect(firstTransfer.token).to.equal(USDC) + expect(firstTransfer.amount).to.equal('10') + expect(firstTransfer.recipient).to.be.equal(inputs.recipient) + + const secondTransfer = intents[0].transfers[1] + expect(secondTransfer.token).to.equal(NATIVE_TOKEN_ADDRESS.toLowerCase()) + expect(secondTransfer.amount).to.equal('100') + expect(secondTransfer.recipient).to.be.equal(inputs.recipient) + }) + }) + + describe('when the user does not have balance for the requested tokens', () => { + const relevantTokens: GetRelevantTokensMock[] = [ + { + request: { + owner: context.user, + chainIds: [chainId], + usdMinAmount: '0', + tokenFilter: 1, + tokens: [], + }, + response: [ + { + timestamp: Date.now(), + balances: [], + }, + ], + }, + ] + + it('does not produce any intents', async () => { + const result = await runTask(taskDir, context, { inputs, calls, relevantTokens }) + expect(result.success).to.be.true + expect(result.timestamp).to.be.equal(context.timestamp) + expect(result.intents).to.be.empty + }) + }) +}) diff --git a/examples/08-relevant-tokens-query/tests/tsconfig.json b/examples/08-relevant-tokens-query/tests/tsconfig.json new file mode 100644 index 0000000..821e603 --- /dev/null +++ b/examples/08-relevant-tokens-query/tests/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "declaration": true, + "composite": true, + "outDir": "./dist", + "rootDir": "./", + "resolveJsonModule": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2020", "DOM"], + "types": ["mocha", "chai", "node"] + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/08-relevant-tokens-query/tsconfig.json b/examples/08-relevant-tokens-query/tsconfig.json new file mode 100644 index 0000000..dd7ad20 --- /dev/null +++ b/examples/08-relevant-tokens-query/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./src/**/*.ts"], + "exclude": ["tests/**/*"], + "references": [{ "path": "./tests" }] +}