diff --git a/examples/8-rebalancing-tokens/abis/ERC20.json b/examples/8-rebalancing-tokens/abis/ERC20.json new file mode 100644 index 0000000..405d6b3 --- /dev/null +++ b/examples/8-rebalancing-tokens/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/8-rebalancing-tokens/eslint.config.mjs b/examples/8-rebalancing-tokens/eslint.config.mjs new file mode 100644 index 0000000..90af2c8 --- /dev/null +++ b/examples/8-rebalancing-tokens/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/8-rebalancing-tokens/manifest.yaml b/examples/8-rebalancing-tokens/manifest.yaml new file mode 100644 index 0000000..3d4bc1f --- /dev/null +++ b/examples/8-rebalancing-tokens/manifest.yaml @@ -0,0 +1,14 @@ +version: 1.0.0 +name: Rebalance to USD target ratios (3 tokens) +description: Automated task that rebalances a 3-token portfolio to target basis-point weights using USD valuations and slippage-protected swaps +inputs: + - chainId: uint32 + - tokenA: address + - tokenB: address + - tokenC: address + - targetBpsA: uint16 # e.g., 5000 = 50% + - targetBpsB: uint16 # must sum with A & C to 10000 + - targetBpsC: uint16 + - slippageBps: uint16 # e.g., 50 = 0.50% +abis: + - ERC20: ./abis/ERC20.json diff --git a/examples/8-rebalancing-tokens/package.json b/examples/8-rebalancing-tokens/package.json new file mode 100644 index 0000000..0b1d4c1 --- /dev/null +++ b/examples/8-rebalancing-tokens/package.json @@ -0,0 +1,30 @@ +{ + "name": "@mimicprotocol/8-rebalancing-tokens", + "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/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/8-rebalancing-tokens/src/task.ts b/examples/8-rebalancing-tokens/src/task.ts new file mode 100644 index 0000000..0590305 --- /dev/null +++ b/examples/8-rebalancing-tokens/src/task.ts @@ -0,0 +1,109 @@ +import { Address, BigInt, environment, ERC20Token, log, Swap, TokenAmount, USD } from '@mimicprotocol/lib-ts' + +import { ERC20 } from './types/ERC20' +import { inputs } from './types' + +const BPS_DENOMINATOR = BigInt.fromI32(10_000) + +function usdMin(left: USD, right: USD): USD { + return left.lt(right) ? left : right +} + +function shareByBps(amountUSD: USD, bps: i32): USD { + const numerator = amountUSD.times(BigInt.fromI32(bps)) + return numerator.div(BPS_DENOMINATOR) +} + +function getTokenAmount(chainId: u32, tokenAddress: Address): TokenAmount { + const me = environment.getContext().user + const contract = new ERC20(tokenAddress, chainId) + const balance = contract.balanceOf(me) + const token = ERC20Token.fromAddress(tokenAddress, chainId) + return TokenAmount.fromBigInt(token, balance) +} + +class Bucket { + constructor( + public index: i32, + public amountUSD: USD + ) {} +} + +export default function main(): void { + const tokenAddresses = [inputs.tokenA, inputs.tokenB, inputs.tokenC] + const targetBps = [inputs.targetBpsA as i32, inputs.targetBpsB as i32, inputs.targetBpsC as i32] + + const totalTargetBps = targetBps[0] + targetBps[1] + targetBps[2] + if (totalTargetBps != 10_000) throw new Error('Targets BPS must sum to 10000') + + const tokensMetadata = [ + ERC20Token.fromAddress(tokenAddresses[0], inputs.chainId), + ERC20Token.fromAddress(tokenAddresses[1], inputs.chainId), + ERC20Token.fromAddress(tokenAddresses[2], inputs.chainId), + ] + + const tokenAmounts = [ + getTokenAmount(inputs.chainId, tokenAddresses[0]), + getTokenAmount(inputs.chainId, tokenAddresses[1]), + getTokenAmount(inputs.chainId, tokenAddresses[2]), + ] + + const currentBalancesUsd = [tokenAmounts[0].toUsd(), tokenAmounts[1].toUsd(), tokenAmounts[2].toUsd()] + const totalPortfolioUSD = currentBalancesUsd[0].plus(currentBalancesUsd[1]).plus(currentBalancesUsd[2]) + if (totalPortfolioUSD.le(USD.zero())) { + log.info('No rebalance needed (total USD is zero)') + return + } + + const desiredBalancesUsd = [ + shareByBps(totalPortfolioUSD, targetBps[0]), + shareByBps(totalPortfolioUSD, targetBps[1]), + shareByBps(totalPortfolioUSD, targetBps[2]), + ] + + const surpluses = new Array() + const deficits = new Array() + for (let i: i32 = 0; i < 3; i++) { + if (currentBalancesUsd[i].gt(desiredBalancesUsd[i])) { + surpluses.push(new Bucket(i, currentBalancesUsd[i].minus(desiredBalancesUsd[i]))) + } else if (desiredBalancesUsd[i].gt(currentBalancesUsd[i])) { + deficits.push(new Bucket(i, desiredBalancesUsd[i].minus(currentBalancesUsd[i]))) + } + } + + if (surpluses.length == 0 || deficits.length == 0) { + log.info('No rebalance needed (target ratios matched)') + return + } + + let surplusIndex: i32 = 0 + let deficitIndex: i32 = 0 + while (surplusIndex < surpluses.length && deficitIndex < deficits.length) { + const movedUSD = usdMin(surpluses[surplusIndex].amountUSD, deficits[deficitIndex].amountUSD) + + const surplusTokenIndex = surpluses[surplusIndex].index + const deficitTokenIndex = deficits[deficitIndex].index + + const amountInToken = movedUSD.toTokenAmount(tokensMetadata[surplusTokenIndex]) + const expectedOutToken = movedUSD.toTokenAmount(tokensMetadata[deficitTokenIndex]) + + const slippageFactor = BPS_DENOMINATOR.minus(BigInt.fromI32(inputs.slippageBps as i32)) + const minimumOutAmount = expectedOutToken.amount.times(slippageFactor).div(BPS_DENOMINATOR) + + Swap.create( + inputs.chainId, + tokensMetadata[surplusTokenIndex], + amountInToken.amount, + tokensMetadata[deficitTokenIndex], + minimumOutAmount + ).send() + + surpluses[surplusIndex].amountUSD = surpluses[surplusIndex].amountUSD.minus(movedUSD) + deficits[deficitIndex].amountUSD = deficits[deficitIndex].amountUSD.minus(movedUSD) + + if (surpluses[surplusIndex].amountUSD.le(USD.zero())) surplusIndex++ + if (deficits[deficitIndex].amountUSD.le(USD.zero())) deficitIndex++ + } + + log.info('Rebalance executed') +} diff --git a/examples/8-rebalancing-tokens/tests/task.spec.ts b/examples/8-rebalancing-tokens/tests/task.spec.ts new file mode 100644 index 0000000..06b3b31 --- /dev/null +++ b/examples/8-rebalancing-tokens/tests/task.spec.ts @@ -0,0 +1,131 @@ +import { ContractCall, runTask, Swap } from '@mimicprotocol/test-ts' +import { expect } from 'chai' + +describe('Task', () => { + const taskDir = './' + + const context = { + user: '0x756f45e3fa69347a9a973a725e3c98bc4db0b5a0', + settlers: [{ address: '0xdcf1d9d12a0488dfb70a8696f44d6d3bc303963d', chainId: 10 }], + timestamp: Date.now(), + } + + const WBTC = '0x1111111111111111111111111111111111111111' // 8 decimals + const WETH = '0x2222222222222222222222222222222222222222' // 18 decimals + const DAI = '0x3333333333333333333333333333333333333333' // 18 decimals + + const inputs = { + chainId: 10, + tokenA: WBTC, + tokenB: WETH, + tokenC: DAI, + targetBpsA: 5000, // BTC 50% + targetBpsB: 3000, // ETH 30% + targetBpsC: 2000, // DAI 20% + slippageBps: 50, // 0.50% + } + + const buildErc20Calls = (balanceWBTC: string, balanceWETH: string, balanceDAI: string): ContractCall[] => [ + // WBTC + { request: { to: WBTC, chainId: 10, data: '0x70a08231' }, response: { value: balanceWBTC, abiType: 'uint256' } }, // balanceOf(user) + { request: { to: WBTC, chainId: 10, data: '0x313ce567' }, response: { value: '8', abiType: 'uint8' } }, // decimals + // WETH + { request: { to: WETH, chainId: 10, data: '0x70a08231' }, response: { value: balanceWETH, abiType: 'uint256' } }, + { request: { to: WETH, chainId: 10, data: '0x313ce567' }, response: { value: '18', abiType: 'uint8' } }, + // DAI + { request: { to: DAI, chainId: 10, data: '0x70a08231' }, response: { value: balanceDAI, abiType: 'uint256' } }, + { request: { to: DAI, chainId: 10, data: '0x313ce567' }, response: { value: '18', abiType: 'uint8' } }, + ] + + describe('when there are some balances', () => { + // Prices: BTC=$60k, ETH=$3k, DAI=$1 — all with 1e18 USD precision + const prices = [ + { request: { token: WBTC, chainId: 10 }, response: ['60000000000000000000000'] }, // 60000 * 1e18 + { request: { token: WETH, chainId: 10 }, response: ['3000000000000000000000'] }, // 3000 * 1e18 + { request: { token: DAI, chainId: 10 }, response: ['1000000000000000000'] }, // 1 * 1e18 + ] + + describe('when rebalancing is needed (ETH surplus → BTC & DAI deficits)', () => { + // Holdings: 0.8 BTC (8d), 20 WETH (18d), 5000 DAI (18d) + // USD: BTC 48k, ETH 60k, DAI 5k => total 113k + // Targets 50/30/20 => BTC 56.5k, ETH 33.9k, DAI 22.6k + // Deltas: BTC -8.5k, ETH +26.1k, DAI -17.6k + // Swaps: + // 1) ETH -> BTC $8,500 => amountIn(WETH)=2.833333333333333333e18 => 2833333333333333333 + // minOut(WBTC)=14,095,832 (sat) after 50 bps slippage + // 2) ETH -> DAI $17,600 => amountIn(WETH)=5.866666666666666666e18 => 5866666666666666666 + // minOut(DAI)=17,512 * 1e18 + const calls = buildErc20Calls( + '80000000', // 0.8 * 1e8 WBTC + '20000000000000000000', // 20 * 1e18 WETH + '5000000000000000000000' // 5000 * 1e18 DAI + ) + + it('emits two swap intents with correct legs and slippage protections', async () => { + const intents = (await runTask(taskDir, context, { inputs, calls, prices })) as Swap[] + expect(intents).to.have.lengthOf(2) + + // ---- First swap: ETH -> BTC ($8,500) ---- + const firstSwap = intents[0] + expect(firstSwap.type).to.equal('swap') + expect(firstSwap.settler).to.equal(context.settlers[0].address) + expect(firstSwap.user).to.equal(context.user) + expect(firstSwap.sourceChain).to.equal(inputs.chainId) + expect(firstSwap.destinationChain).to.equal(inputs.chainId) + + expect(firstSwap.tokensIn).to.have.lengthOf(1) + expect(firstSwap.tokensIn[0].token).to.equal(WETH) + expect(firstSwap.tokensIn[0].amount).to.equal('2833333333333333333') // 2.833333333333333333 WETH + + expect(firstSwap.tokensOut).to.have.lengthOf(1) + expect(firstSwap.tokensOut[0].token).to.equal(WBTC) + expect(firstSwap.tokensOut[0].minAmount).to.equal('14095832') // 14,095,832 sat (8d) + expect(firstSwap.tokensOut[0].recipient).to.equal(context.user) + + // ---- Second swap: ETH -> DAI ($17,600) ---- + const secondSwap = intents[1] + expect(secondSwap.type).to.equal('swap') + expect(secondSwap.settler).to.equal(context.settlers[0].address) + expect(secondSwap.user).to.equal(context.user) + expect(secondSwap.sourceChain).to.equal(inputs.chainId) + expect(secondSwap.destinationChain).to.equal(inputs.chainId) + + expect(secondSwap.tokensIn).to.have.lengthOf(1) + expect(secondSwap.tokensIn[0].token).to.equal(WETH) + expect(secondSwap.tokensIn[0].amount).to.equal('5866666666666666666') // 5.866666666666666666 WETH + + expect(secondSwap.tokensOut).to.have.lengthOf(1) + expect(secondSwap.tokensOut[0].token).to.equal(DAI) + expect(secondSwap.tokensOut[0].minAmount).to.equal('17512000000000000000000') // 17,512 DAI (18d) + expect(secondSwap.tokensOut[0].recipient).to.equal(context.user) + }) + }) + + describe('when the portfolio already matches target ratios', () => { + // Choose a total USD (BigInt) that’s divisible for all tokens and prices. + // With T = 1.2e21 (i.e., 1200 USD), targets are: + // - BTC 50% → 600 USD → 0.01 BTC → 1,000,000 sat + // - ETH 30% → 360 USD → 0.12 ETH → 120,000,000,000,000,000 wei + // - DAI 20% → 240 USD → 240 DAI → 240,000,000,000,000,000,000 wei + const calls = buildErc20Calls( + '1000000', // WBTC: 0.01 BTC (8 decimals) + '120000000000000000', // WETH: 0.12 ETH (18 decimals) + '240000000000000000000' // DAI: 240 DAI (18 decimals) + ) + + it('does not produce any intents', async () => { + const intents = await runTask(taskDir, context, { inputs: inputs, calls, prices }) + expect(intents).to.be.empty + }) + }) + }) + + describe('when total USD is zero (no balances)', () => { + const calls = buildErc20Calls('0', '0', '0') + + it('does not produce any intents', async () => { + const intents = await runTask(taskDir, context, { inputs: inputs, calls: calls, prices: [] }) + expect(intents).to.be.an('array').that.is.empty + }) + }) +}) diff --git a/examples/8-rebalancing-tokens/tests/tsconfig.json b/examples/8-rebalancing-tokens/tests/tsconfig.json new file mode 100644 index 0000000..821e603 --- /dev/null +++ b/examples/8-rebalancing-tokens/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/8-rebalancing-tokens/tsconfig.json b/examples/8-rebalancing-tokens/tsconfig.json new file mode 100644 index 0000000..dd7ad20 --- /dev/null +++ b/examples/8-rebalancing-tokens/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./src/**/*.ts"], + "exclude": ["tests/**/*"], + "references": [{ "path": "./tests" }] +} diff --git a/yarn.lock b/yarn.lock index cae9603..2a2da55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -364,9 +364,9 @@ integrity sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw== "@mimicprotocol/cli@latest": - version "0.0.1-rc.20" - resolved "https://registry.yarnpkg.com/@mimicprotocol/cli/-/cli-0.0.1-rc.20.tgz#8f3b228b75b65477ea28b7b07b5642eb87ed68af" - integrity sha512-SkqULGjLg3ZwWLpRqvw+nGxEDy2bwG54hkTkTwm8rVfEnrORwpMhnTH1u/PCz9y0R5mX2CxvO3jdAcWHXywHZg== + version "0.0.1-rc.21" + resolved "https://registry.yarnpkg.com/@mimicprotocol/cli/-/cli-0.0.1-rc.21.tgz#2a898c2fe30c5cd44502506234572fefa0c454a9" + integrity sha512-QXyOKyNQXLLFIp7pdsKuhtMmtEeBneNuuT6oS6m8sgT4dSQBtzxAuHqCqDLROJtR9NICybh6Cjukpqkw/3UMXQ== dependencies: "@inquirer/prompts" "^7.2.4" "@oclif/core" "^4.2.2" @@ -379,9 +379,9 @@ zod "^3.24.1" "@mimicprotocol/lib-ts@latest": - version "0.0.1-rc.20" - resolved "https://registry.yarnpkg.com/@mimicprotocol/lib-ts/-/lib-ts-0.0.1-rc.20.tgz#87d222997a12f97cc5fa075d66e8ce76fe1ec8b7" - integrity sha512-VywXCiVs2A488rkq9okMY/YVXOBoHdWtkEd41cQWcwRRoh7fIp3p2p9TwYS3tGw6owVwN2MHoMGlmMDpRIzTKw== + version "0.0.1-rc.21" + resolved "https://registry.yarnpkg.com/@mimicprotocol/lib-ts/-/lib-ts-0.0.1-rc.21.tgz#11114c3d359199c0c84524e78ec33eed922892e5" + integrity sha512-EueyYbNVg+0SoZBvCPUb682OmyZprfiosQQjFzBIY3QQfGojdgab1jG0G2P5Fhgp2owXjuzGlfCrePJ3zwFP3Q== dependencies: as-base58 "^0.1.1" eslint-config-mimic "^0.0.3" @@ -389,9 +389,9 @@ visitor-as "0.11.4" "@mimicprotocol/test-ts@latest": - version "0.0.1-rc.20" - resolved "https://registry.yarnpkg.com/@mimicprotocol/test-ts/-/test-ts-0.0.1-rc.20.tgz#fe9c099ffba6541822bf16d8dd5d3ac86481fa19" - integrity sha512-b6y5qQElzytouCGfj4/9ctvf+wMLpI4ElxjDsFhdKZAMDMqVGMjZ7u6aHhooWYGC8tdV+vP4XhQbm6DkjBUKbg== + version "0.0.1-rc.21" + resolved "https://registry.yarnpkg.com/@mimicprotocol/test-ts/-/test-ts-0.0.1-rc.21.tgz#c42bcd0718f1d6b202f386a42f4736f778b629c9" + integrity sha512-+vGVR6BVuWjdBgbJk6ZsRsXa6Rsd2qo736ooDn01xERvewMtQB2t6ovmE4E2dbCbtO4zjZSyJyH/2xlVrbF26A== dependencies: zod "^3.24.1"