From 297100f7d98692cb4859cd55da8ae0e5808abc23 Mon Sep 17 00:00:00 2001 From: lgalende Date: Mon, 3 Nov 2025 17:08:14 -0300 Subject: [PATCH 1/3] examples: add DCA task --- .../eslint.config.mjs | 89 +++++++++++++++++ .../10-dollar-cost-averaging/manifest.yaml | 10 ++ .../10-dollar-cost-averaging/package.json | 31 ++++++ examples/10-dollar-cost-averaging/src/task.ts | 41 ++++++++ .../tests/task.spec.ts | 97 +++++++++++++++++++ .../tests/tsconfig.json | 21 ++++ .../10-dollar-cost-averaging/tsconfig.json | 6 ++ 7 files changed, 295 insertions(+) create mode 100644 examples/10-dollar-cost-averaging/eslint.config.mjs create mode 100644 examples/10-dollar-cost-averaging/manifest.yaml create mode 100644 examples/10-dollar-cost-averaging/package.json create mode 100644 examples/10-dollar-cost-averaging/src/task.ts create mode 100644 examples/10-dollar-cost-averaging/tests/task.spec.ts create mode 100644 examples/10-dollar-cost-averaging/tests/tsconfig.json create mode 100644 examples/10-dollar-cost-averaging/tsconfig.json diff --git a/examples/10-dollar-cost-averaging/eslint.config.mjs b/examples/10-dollar-cost-averaging/eslint.config.mjs new file mode 100644 index 0000000..90af2c8 --- /dev/null +++ b/examples/10-dollar-cost-averaging/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/10-dollar-cost-averaging/manifest.yaml b/examples/10-dollar-cost-averaging/manifest.yaml new file mode 100644 index 0000000..b1790a2 --- /dev/null +++ b/examples/10-dollar-cost-averaging/manifest.yaml @@ -0,0 +1,10 @@ +version: 1.0.0 +name: DCA +description: Dollar-cost averaging task that swaps tokenIn to tokenOut +inputs: + - chainId: uint32 + - tokenIn: address + - tokenOut: address + - amountDecimal: string # "1.5" = 1.5 tokenIn + - slippageBps: uint16 # 100 = 1% + - recipient: address diff --git a/examples/10-dollar-cost-averaging/package.json b/examples/10-dollar-cost-averaging/package.json new file mode 100644 index 0000000..5afa2d4 --- /dev/null +++ b/examples/10-dollar-cost-averaging/package.json @@ -0,0 +1,31 @@ +{ + "name": "@mimicprotocol/10-dollar-cost-averaging", + "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/10-dollar-cost-averaging/src/task.ts b/examples/10-dollar-cost-averaging/src/task.ts new file mode 100644 index 0000000..03d1c66 --- /dev/null +++ b/examples/10-dollar-cost-averaging/src/task.ts @@ -0,0 +1,41 @@ +import { BigInt, ERC20Token, log, SwapBuilder, TokenAmount } from '@mimicprotocol/lib-ts' + +import { inputs } from './types' + +export default function main(): void { + // Log input parameters + log.info('Starting DCA swap: amountFromToken={}, slippageBps={}, chainId={}, recipient={}', [ + inputs.amountDecimal, + inputs.slippageBps.toString(), + inputs.chainId.toString(), + inputs.recipient.toString(), + ]) + + // Create token instances + const tokenIn = ERC20Token.fromAddress(inputs.tokenIn, inputs.chainId) + const tokenOut = ERC20Token.fromAddress(inputs.tokenOut, inputs.chainId) + + // Create amount from decimal string and estimatate amount out + const amountIn = TokenAmount.fromStringDecimal(tokenIn, inputs.amountDecimal) + const amountOut = amountIn.toTokenAmount(tokenOut) + + // Apply slippage to calculate the expected minimum amount out + const basisPoints = BigInt.fromU16(10000) + const slippageBps = basisPoints.minus(BigInt.fromU16(inputs.slippageBps)) + const minAmountOut = amountOut.times(slippageBps).div(basisPoints) + + log.info('Calculated minOut: {} (equivalent={}, slippageBps={})', [ + minAmountOut.toString(), + amountOut.toString(), + inputs.slippageBps.toString(), + ]) + + // Create and execute swap + SwapBuilder.forChain(inputs.chainId) + .addTokenInFromTokenAmount(amountIn) + .addTokenOutFromTokenAmount(minAmountOut, inputs.recipient) + .build() + .send() + + log.info('DCA swap executed successfully') +} diff --git a/examples/10-dollar-cost-averaging/tests/task.spec.ts b/examples/10-dollar-cost-averaging/tests/task.spec.ts new file mode 100644 index 0000000..8a7d395 --- /dev/null +++ b/examples/10-dollar-cost-averaging/tests/task.spec.ts @@ -0,0 +1,97 @@ +import { Chains, fp, OpType } from '@mimicprotocol/sdk' +import { Context, ContractCallMock, GetPriceMock, runTask, Swap } from '@mimicprotocol/test-ts' +import { expect } from 'chai' + +describe('Task', () => { + const taskDir = './build' + + const chainId = Chains.Base + const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' + const WETH = '0x4200000000000000000000000000000000000006' + + const context: Context = { + user: '0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0', + settlers: [{ address: '0x609d831c0068844e11ef85a273c7f356212fd6d1', chainId }], + timestamp: Date.now(), + } + + const inputs = { + chainId, + tokenIn: USDC, + tokenOut: WETH, + amountDecimal: '10.5', // 10.5 USDC + slippageBps: 100, // 1% + recipient: context.user!, + } + + const prices: GetPriceMock[] = [ + { + request: { + token: USDC, + chainId, + }, + response: [fp(1).toString()], // 1 USDC = 1 USD + }, + { + request: { + token: WETH, + chainId, + }, + response: [fp(4200).toString()], // 1 WETH = 4200 USD + }, + ] + + const calls: ContractCallMock[] = [ + // USDC + { + request: { to: USDC, chainId, fnSelector: '0x313ce567' }, // `decimals` + response: { value: '6', abiType: 'uint8' }, + }, + { + request: { to: USDC, chainId, fnSelector: '0x95d89b41' }, // `symbol` + response: { value: 'USDC', abiType: 'string' }, + }, + // WETH + { + request: { to: WETH, chainId, fnSelector: '0x313ce567' }, // `decimals` + response: { value: '18', abiType: 'uint8' }, + }, + { + request: { to: WETH, chainId, fnSelector: '0x95d89b41' }, // `symbol` + response: { value: 'WETH', abiType: 'string' }, + }, + ] + + it('produces the expected intents', async () => { + const result = await runTask(taskDir, context, { inputs, prices, calls }) + expect(result.success).to.be.true + expect(result.timestamp).to.be.equal(context.timestamp) + + const intents = result.intents as Swap[] + expect(intents).to.have.lengthOf(1) + + expect(intents[0].op).to.be.equal(OpType.Swap) + expect(intents[0].settler).to.be.equal(context.settlers?.[0].address) + expect(intents[0].user).to.be.equal(context.user) + expect(intents[0].sourceChain).to.be.equal(inputs.chainId) + expect(intents[0].destinationChain).to.be.equal(inputs.chainId) + + expect(intents[0].tokensIn).to.have.lengthOf(1) + expect(intents[0].tokensIn[0].token).to.be.equal(inputs.tokenIn) + expect(intents[0].tokensIn[0].amount).to.be.equal(fp(10.5, 6).toString()) + + expect(intents[0].tokensOut).to.have.lengthOf(1) + expect(intents[0].tokensOut[0].token).to.be.equal(inputs.tokenOut) + expect(intents[0].tokensOut[0].minAmount).to.be.equal(fp(0.002475).toString()) // amountIn / wethPrice * (1 - slippage) = 10.5 / 4200 * 0.99 = 0.002475 + expect(intents[0].tokensOut[0].recipient).to.be.equal(context.user) + + expect(result.logs).to.have.lengthOf(3) + expect(result.logs[0]).to.be.equal( + `[Info] Starting DCA swap: amountFromToken=${inputs.amountDecimal}, slippageBps=${inputs.slippageBps}, chainId=${chainId}, recipient=${context.user}` + ) + expect(result.logs[1]).to.be.equal( + `[Info] Calculated minOut: 0.002475 WETH (equivalent=0.0025 WETH, slippageBps=${inputs.slippageBps})` + ) + expect(result.logs[2]).to.be.equal('[Info] DCA swap executed successfully') + }) +}) diff --git a/examples/10-dollar-cost-averaging/tests/tsconfig.json b/examples/10-dollar-cost-averaging/tests/tsconfig.json new file mode 100644 index 0000000..821e603 --- /dev/null +++ b/examples/10-dollar-cost-averaging/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/10-dollar-cost-averaging/tsconfig.json b/examples/10-dollar-cost-averaging/tsconfig.json new file mode 100644 index 0000000..dd7ad20 --- /dev/null +++ b/examples/10-dollar-cost-averaging/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./src/**/*.ts"], + "exclude": ["tests/**/*"], + "references": [{ "path": "./tests" }] +} From be4719b467a66cc0b1049f2c1ee8eebd083960be Mon Sep 17 00:00:00 2001 From: lgalende Date: Wed, 5 Nov 2025 09:20:07 -0300 Subject: [PATCH 2/3] chore: embelish test --- examples/10-dollar-cost-averaging/tests/task.spec.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/examples/10-dollar-cost-averaging/tests/task.spec.ts b/examples/10-dollar-cost-averaging/tests/task.spec.ts index 8a7d395..b1ad193 100644 --- a/examples/10-dollar-cost-averaging/tests/task.spec.ts +++ b/examples/10-dollar-cost-averaging/tests/task.spec.ts @@ -26,17 +26,11 @@ describe('Task', () => { const prices: GetPriceMock[] = [ { - request: { - token: USDC, - chainId, - }, + request: { token: USDC, chainId }, response: [fp(1).toString()], // 1 USDC = 1 USD }, { - request: { - token: WETH, - chainId, - }, + request: { token: WETH, chainId }, response: [fp(4200).toString()], // 1 WETH = 4200 USD }, ] From 8181ca6725c87c1cca1e63cec946b960d8e4c21d Mon Sep 17 00:00:00 2001 From: lgalende Date: Thu, 6 Nov 2025 12:34:16 -0300 Subject: [PATCH 3/3] chore: rename amountDecimal as amount --- examples/10-dollar-cost-averaging/manifest.yaml | 4 ++-- examples/10-dollar-cost-averaging/src/task.ts | 4 ++-- examples/10-dollar-cost-averaging/tests/task.spec.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/10-dollar-cost-averaging/manifest.yaml b/examples/10-dollar-cost-averaging/manifest.yaml index b1790a2..34e7a03 100644 --- a/examples/10-dollar-cost-averaging/manifest.yaml +++ b/examples/10-dollar-cost-averaging/manifest.yaml @@ -5,6 +5,6 @@ inputs: - chainId: uint32 - tokenIn: address - tokenOut: address - - amountDecimal: string # "1.5" = 1.5 tokenIn - - slippageBps: uint16 # 100 = 1% + - amount: string # "1.5" = 1.5 tokenIn + - slippageBps: uint16 # 100 = 1% - recipient: address diff --git a/examples/10-dollar-cost-averaging/src/task.ts b/examples/10-dollar-cost-averaging/src/task.ts index 03d1c66..368e5ac 100644 --- a/examples/10-dollar-cost-averaging/src/task.ts +++ b/examples/10-dollar-cost-averaging/src/task.ts @@ -5,7 +5,7 @@ import { inputs } from './types' export default function main(): void { // Log input parameters log.info('Starting DCA swap: amountFromToken={}, slippageBps={}, chainId={}, recipient={}', [ - inputs.amountDecimal, + inputs.amount, inputs.slippageBps.toString(), inputs.chainId.toString(), inputs.recipient.toString(), @@ -16,7 +16,7 @@ export default function main(): void { const tokenOut = ERC20Token.fromAddress(inputs.tokenOut, inputs.chainId) // Create amount from decimal string and estimatate amount out - const amountIn = TokenAmount.fromStringDecimal(tokenIn, inputs.amountDecimal) + const amountIn = TokenAmount.fromStringDecimal(tokenIn, inputs.amount) const amountOut = amountIn.toTokenAmount(tokenOut) // Apply slippage to calculate the expected minimum amount out diff --git a/examples/10-dollar-cost-averaging/tests/task.spec.ts b/examples/10-dollar-cost-averaging/tests/task.spec.ts index b1ad193..a8d8b82 100644 --- a/examples/10-dollar-cost-averaging/tests/task.spec.ts +++ b/examples/10-dollar-cost-averaging/tests/task.spec.ts @@ -19,7 +19,7 @@ describe('Task', () => { chainId, tokenIn: USDC, tokenOut: WETH, - amountDecimal: '10.5', // 10.5 USDC + amount: '10.5', // 10.5 USDC slippageBps: 100, // 1% recipient: context.user!, } @@ -81,7 +81,7 @@ describe('Task', () => { expect(result.logs).to.have.lengthOf(3) expect(result.logs[0]).to.be.equal( - `[Info] Starting DCA swap: amountFromToken=${inputs.amountDecimal}, slippageBps=${inputs.slippageBps}, chainId=${chainId}, recipient=${context.user}` + `[Info] Starting DCA swap: amountFromToken=${inputs.amount}, slippageBps=${inputs.slippageBps}, chainId=${chainId}, recipient=${context.user}` ) expect(result.logs[1]).to.be.equal( `[Info] Calculated minOut: 0.002475 WETH (equivalent=0.0025 WETH, slippageBps=${inputs.slippageBps})`