From 43d3e8b7f3f9865fadbbcb590944d6225912b6d7 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Thu, 15 Jan 2026 08:33:52 -0800 Subject: [PATCH 001/174] npm install frontend, and git ignore node modules Signed-off-by: John Shutt --- .gitignore | 6 +- frontend/package-lock.json | 9834 ++++++++++++++++++++++++++++++++++++ frontend/package.json | 2 +- 3 files changed, 9840 insertions(+), 2 deletions(-) create mode 100644 frontend/package-lock.json diff --git a/.gitignore b/.gitignore index 02854013..e02e932e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,8 @@ docs/ # Anvil files anvil.log -anvil.pid \ No newline at end of file +anvil.pid + +# Node modules +node_modules +frontend/node_modules \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..150e10f3 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,9834 @@ +{ + "name": "og-deployer-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "og-deployer-frontend", + "version": "0.1.0", + "dependencies": { + "@rainbow-me/rainbowkit": "^2.1.6", + "@tanstack/react-query": "^5.56.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "viem": "^2.20.0", + "wagmi": "^2.12.7" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^7.3.1" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@base-org/account": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@base-org/account/-/account-2.4.0.tgz", + "integrity": "sha512-A4Umpi8B9/pqR78D1Yoze4xHyQaujioVRqqO3d6xuDFw9VRtjg6tK3bPlwE0aW+nVH/ntllCpPa2PbI8Rnjcug==", + "license": "Apache-2.0", + "dependencies": { + "@coinbase/cdp-sdk": "^1.0.0", + "@noble/hashes": "1.4.0", + "clsx": "1.2.1", + "eventemitter3": "5.0.1", + "idb-keyval": "6.2.1", + "ox": "0.6.9", + "preact": "10.24.2", + "viem": "^2.31.7", + "zustand": "5.0.3" + } + }, + "node_modules/@base-org/account/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@base-org/account/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@base-org/account/node_modules/ox": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", + "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@base-org/account/node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@coinbase/cdp-sdk": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@coinbase/cdp-sdk/-/cdp-sdk-1.43.0.tgz", + "integrity": "sha512-Fre1tvoIi4HAoC8/PgBoLsuZ9mt7K0R50EEC6i+6FaipW7oO3MABCx+vGAcM7EpcbVa7E6hTFe2/a0UdoajvYQ==", + "license": "MIT", + "dependencies": { + "@solana-program/system": "^0.10.0", + "@solana-program/token": "^0.9.0", + "@solana/kit": "^5.1.0", + "@solana/web3.js": "^1.98.1", + "abitype": "1.0.6", + "axios": "^1.12.2", + "axios-retry": "^4.5.0", + "jose": "^6.0.8", + "md5": "^2.3.0", + "uncrypto": "^0.1.3", + "viem": "^2.21.26", + "zod": "^3.24.4" + } + }, + "node_modules/@coinbase/cdp-sdk/node_modules/abitype": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", + "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@coinbase/wallet-sdk": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.3.6.tgz", + "integrity": "sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA==", + "license": "Apache-2.0", + "dependencies": { + "@noble/hashes": "1.4.0", + "clsx": "1.2.1", + "eventemitter3": "5.0.1", + "idb-keyval": "6.2.1", + "ox": "0.6.9", + "preact": "10.24.2", + "viem": "^2.27.2", + "zustand": "5.0.3" + } + }, + "node_modules/@coinbase/wallet-sdk/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@coinbase/wallet-sdk/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@coinbase/wallet-sdk/node_modules/ox": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", + "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@coinbase/wallet-sdk/node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ecies/ciphers": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.5.tgz", + "integrity": "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==", + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/common": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", + "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", + "license": "MIT", + "dependencies": { + "@ethereumjs/util": "^8.1.0", + "crc-32": "^1.2.0" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethereumjs/tx": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", + "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/common": "^3.2.0", + "@ethereumjs/rlp": "^4.0.1", + "@ethereumjs/util": "^8.1.0", + "ethereum-cryptography": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@gemini-wallet/core": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@gemini-wallet/core/-/core-0.3.2.tgz", + "integrity": "sha512-Z4aHi3ECFf5oWYWM3F1rW83GJfB9OvhBYPTmb5q+VyK3uvzvS48lwo+jwh2eOoCRWEuT/crpb9Vwp2QaS5JqgQ==", + "license": "MIT", + "dependencies": { + "@metamask/rpc-errors": "7.0.2", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "viem": ">=2.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@metamask/eth-json-rpc-provider": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-1.0.1.tgz", + "integrity": "sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==", + "dependencies": { + "@metamask/json-rpc-engine": "^7.0.0", + "@metamask/safe-event-emitter": "^3.0.0", + "@metamask/utils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/json-rpc-engine": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.3.tgz", + "integrity": "sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==", + "license": "ISC", + "dependencies": { + "@metamask/rpc-errors": "^6.2.1", + "@metamask/safe-event-emitter": "^3.0.0", + "@metamask/utils": "^8.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/json-rpc-engine/node_modules/@metamask/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.0.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/rpc-errors": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.4.0.tgz", + "integrity": "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==", + "license": "MIT", + "dependencies": { + "@metamask/utils": "^9.0.0", + "fast-safe-stringify": "^2.0.6" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/rpc-errors/node_modules/@metamask/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.1.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", + "integrity": "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.1.2", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "semver": "^7.3.8", + "superstruct": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@metamask/eth-json-rpc-provider/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/eth-json-rpc-provider/node_modules/superstruct": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", + "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@metamask/eth-json-rpc-provider/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@metamask/json-rpc-engine": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-8.0.2.tgz", + "integrity": "sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA==", + "license": "ISC", + "dependencies": { + "@metamask/rpc-errors": "^6.2.1", + "@metamask/safe-event-emitter": "^3.0.0", + "@metamask/utils": "^8.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/json-rpc-engine/node_modules/@metamask/rpc-errors": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.4.0.tgz", + "integrity": "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==", + "license": "MIT", + "dependencies": { + "@metamask/utils": "^9.0.0", + "fast-safe-stringify": "^2.0.6" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/json-rpc-engine/node_modules/@metamask/rpc-errors/node_modules/@metamask/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.1.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/json-rpc-engine/node_modules/@metamask/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.0.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/json-rpc-engine/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/json-rpc-engine/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@metamask/json-rpc-middleware-stream": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-7.0.2.tgz", + "integrity": "sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg==", + "license": "ISC", + "dependencies": { + "@metamask/json-rpc-engine": "^8.0.2", + "@metamask/safe-event-emitter": "^3.0.0", + "@metamask/utils": "^8.3.0", + "readable-stream": "^3.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/json-rpc-middleware-stream/node_modules/@metamask/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.0.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/json-rpc-middleware-stream/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/json-rpc-middleware-stream/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@metamask/object-multiplex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@metamask/object-multiplex/-/object-multiplex-2.1.0.tgz", + "integrity": "sha512-4vKIiv0DQxljcXwfpnbsXcfa5glMj5Zg9mqn4xpIWqkv6uJ2ma5/GtUfLFSxhlxnR8asRMv8dDmWya1Tc1sDFA==", + "license": "ISC", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.2" + }, + "engines": { + "node": "^16.20 || ^18.16 || >=20" + } + }, + "node_modules/@metamask/onboarding": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@metamask/onboarding/-/onboarding-1.0.1.tgz", + "integrity": "sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==", + "license": "MIT", + "dependencies": { + "bowser": "^2.9.0" + } + }, + "node_modules/@metamask/providers": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@metamask/providers/-/providers-16.1.0.tgz", + "integrity": "sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g==", + "license": "MIT", + "dependencies": { + "@metamask/json-rpc-engine": "^8.0.1", + "@metamask/json-rpc-middleware-stream": "^7.0.1", + "@metamask/object-multiplex": "^2.0.0", + "@metamask/rpc-errors": "^6.2.1", + "@metamask/safe-event-emitter": "^3.1.1", + "@metamask/utils": "^8.3.0", + "detect-browser": "^5.2.0", + "extension-port-stream": "^3.0.0", + "fast-deep-equal": "^3.1.3", + "is-stream": "^2.0.0", + "readable-stream": "^3.6.2", + "webextension-polyfill": "^0.10.0" + }, + "engines": { + "node": "^18.18 || >=20" + } + }, + "node_modules/@metamask/providers/node_modules/@metamask/rpc-errors": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.4.0.tgz", + "integrity": "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==", + "license": "MIT", + "dependencies": { + "@metamask/utils": "^9.0.0", + "fast-safe-stringify": "^2.0.6" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/providers/node_modules/@metamask/rpc-errors/node_modules/@metamask/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.1.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/providers/node_modules/@metamask/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.0.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/providers/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/providers/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@metamask/rpc-errors": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-7.0.2.tgz", + "integrity": "sha512-YYYHsVYd46XwY2QZzpGeU4PSdRhHdxnzkB8piWGvJW2xbikZ3R+epAYEL4q/K8bh9JPTucsUdwRFnACor1aOYw==", + "license": "MIT", + "dependencies": { + "@metamask/utils": "^11.0.1", + "fast-safe-stringify": "^2.0.6" + }, + "engines": { + "node": "^18.20 || ^20.17 || >=22" + } + }, + "node_modules/@metamask/safe-event-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.2.tgz", + "integrity": "sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA==", + "license": "ISC", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@metamask/sdk": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@metamask/sdk/-/sdk-0.33.1.tgz", + "integrity": "sha512-1mcOQVGr9rSrVcbKPNVzbZ8eCl1K0FATsYH3WJ/MH4WcZDWGECWrXJPNMZoEAkLxWiMe8jOQBumg2pmcDa9zpQ==", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@metamask/onboarding": "^1.0.1", + "@metamask/providers": "16.1.0", + "@metamask/sdk-analytics": "0.0.5", + "@metamask/sdk-communication-layer": "0.33.1", + "@metamask/sdk-install-modal-web": "0.32.1", + "@paulmillr/qr": "^0.2.1", + "bowser": "^2.9.0", + "cross-fetch": "^4.0.0", + "debug": "4.3.4", + "eciesjs": "^0.4.11", + "eth-rpc-errors": "^4.0.3", + "eventemitter2": "^6.4.9", + "obj-multiplex": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^3.6.2", + "socket.io-client": "^4.5.1", + "tslib": "^2.6.0", + "util": "^0.12.4", + "uuid": "^8.3.2" + } + }, + "node_modules/@metamask/sdk-analytics": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@metamask/sdk-analytics/-/sdk-analytics-0.0.5.tgz", + "integrity": "sha512-fDah+keS1RjSUlC8GmYXvx6Y26s3Ax1U9hGpWb6GSY5SAdmTSIqp2CvYy6yW0WgLhnYhW+6xERuD0eVqV63QIQ==", + "license": "MIT", + "dependencies": { + "openapi-fetch": "^0.13.5" + } + }, + "node_modules/@metamask/sdk-communication-layer": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.33.1.tgz", + "integrity": "sha512-0bI9hkysxcfbZ/lk0T2+aKVo1j0ynQVTuB3sJ5ssPWlz+Z3VwveCkP1O7EVu1tsVVCb0YV5WxK9zmURu2FIiaA==", + "dependencies": { + "@metamask/sdk-analytics": "0.0.5", + "bufferutil": "^4.0.8", + "date-fns": "^2.29.3", + "debug": "4.3.4", + "utf-8-validate": "^5.0.2", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "cross-fetch": "^4.0.0", + "eciesjs": "*", + "eventemitter2": "^6.4.9", + "readable-stream": "^3.6.2", + "socket.io-client": "^4.5.1" + } + }, + "node_modules/@metamask/sdk-communication-layer/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@metamask/sdk-communication-layer/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/@metamask/sdk-install-modal-web": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.32.1.tgz", + "integrity": "sha512-MGmAo6qSjf1tuYXhCu2EZLftq+DSt5Z7fsIKr2P+lDgdTPWgLfZB1tJKzNcwKKOdf6q9Qmmxn7lJuI/gq5LrKw==", + "dependencies": { + "@paulmillr/qr": "^0.2.1" + } + }, + "node_modules/@metamask/sdk/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@metamask/sdk/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/@metamask/superstruct": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.2.1.tgz", + "integrity": "sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/utils": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-11.9.0.tgz", + "integrity": "sha512-wRnoSDD9jTWOge/+reFviJQANhS+uy8Y+OEwRanp5mQeGTjBFmK1r2cTOnei2UCZRV1crXHzeJVSFEoDDcgRbA==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@metamask/superstruct": "^3.1.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "@types/lodash": "^4.17.20", + "debug": "^4.3.4", + "lodash": "^4.17.21", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": "^18.18 || ^20.14 || >=22" + } + }, + "node_modules/@metamask/utils/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/utils/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paulmillr/qr": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@paulmillr/qr/-/qr-0.2.1.tgz", + "integrity": "sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==", + "deprecated": "The package is now available as \"qr\": npm install qr", + "license": "(MIT OR Apache-2.0)", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@rainbow-me/rainbowkit": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@rainbow-me/rainbowkit/-/rainbowkit-2.2.10.tgz", + "integrity": "sha512-8+E4die1A2ovN9t3lWxWnwqTGEdFqThXDQRj+E4eDKuUKyymYD+66Gzm6S9yfg8E95c6hmGlavGUfYPtl1EagA==", + "license": "MIT", + "dependencies": { + "@vanilla-extract/css": "1.17.3", + "@vanilla-extract/dynamic": "2.1.4", + "@vanilla-extract/sprinkles": "1.6.4", + "clsx": "2.1.1", + "cuer": "0.0.3", + "react-remove-scroll": "2.6.2", + "ua-parser-js": "^1.0.37" + }, + "engines": { + "node": ">=12.4" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "react": ">=18", + "react-dom": ">=18", + "viem": "2.x", + "wagmi": "^2.9.0" + } + }, + "node_modules/@reown/appkit": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.7.8.tgz", + "integrity": "sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit-common": "1.7.8", + "@reown/appkit-controllers": "1.7.8", + "@reown/appkit-pay": "1.7.8", + "@reown/appkit-polyfills": "1.7.8", + "@reown/appkit-scaffold-ui": "1.7.8", + "@reown/appkit-ui": "1.7.8", + "@reown/appkit-utils": "1.7.8", + "@reown/appkit-wallet": "1.7.8", + "@walletconnect/types": "2.21.0", + "@walletconnect/universal-provider": "2.21.0", + "bs58": "6.0.0", + "valtio": "1.13.2", + "viem": ">=2.29.0" + } + }, + "node_modules/@reown/appkit-common": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.7.8.tgz", + "integrity": "sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==", + "license": "Apache-2.0", + "dependencies": { + "big.js": "6.2.2", + "dayjs": "1.11.13", + "viem": ">=2.29.0" + } + }, + "node_modules/@reown/appkit-controllers": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.7.8.tgz", + "integrity": "sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA==", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit-common": "1.7.8", + "@reown/appkit-wallet": "1.7.8", + "@walletconnect/universal-provider": "2.21.0", + "valtio": "1.13.2", + "viem": ">=2.29.0" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@noble/ciphers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", + "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/core": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", + "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.33.0", + "events": "3.3.0", + "uint8arrays": "3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/sign-client": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", + "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/core": "2.21.0", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "2.1.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/types": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", + "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/universal-provider": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", + "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/jsonrpc-http-connection": "1.0.8", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "@walletconnect/sign-client": "2.21.0", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "es-toolkit": "1.33.0", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/utils": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", + "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", + "license": "Apache-2.0", + "dependencies": { + "@noble/ciphers": "1.2.1", + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "bs58": "6.0.0", + "detect-browser": "5.3.0", + "query-string": "7.1.3", + "uint8arrays": "3.1.0", + "viem": "2.23.2" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/utils/node_modules/viem": { + "version": "2.23.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", + "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@scure/bip32": "1.6.2", + "@scure/bip39": "1.5.4", + "abitype": "1.0.8", + "isows": "1.0.6", + "ox": "0.6.7", + "ws": "8.18.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-controllers/node_modules/abitype": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", + "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-controllers/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/@reown/appkit-controllers/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/isows": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", + "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/@reown/appkit-controllers/node_modules/ox": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", + "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-controllers/node_modules/unstorage": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", + "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-controllers/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-pay": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.7.8.tgz", + "integrity": "sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw==", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit-common": "1.7.8", + "@reown/appkit-controllers": "1.7.8", + "@reown/appkit-ui": "1.7.8", + "@reown/appkit-utils": "1.7.8", + "lit": "3.3.0", + "valtio": "1.13.2" + } + }, + "node_modules/@reown/appkit-polyfills": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.7.8.tgz", + "integrity": "sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "6.0.3" + } + }, + "node_modules/@reown/appkit-scaffold-ui": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.7.8.tgz", + "integrity": "sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ==", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit-common": "1.7.8", + "@reown/appkit-controllers": "1.7.8", + "@reown/appkit-ui": "1.7.8", + "@reown/appkit-utils": "1.7.8", + "@reown/appkit-wallet": "1.7.8", + "lit": "3.3.0" + } + }, + "node_modules/@reown/appkit-ui": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.7.8.tgz", + "integrity": "sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow==", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit-common": "1.7.8", + "@reown/appkit-controllers": "1.7.8", + "@reown/appkit-wallet": "1.7.8", + "lit": "3.3.0", + "qrcode": "1.5.3" + } + }, + "node_modules/@reown/appkit-utils": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.7.8.tgz", + "integrity": "sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw==", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit-common": "1.7.8", + "@reown/appkit-controllers": "1.7.8", + "@reown/appkit-polyfills": "1.7.8", + "@reown/appkit-wallet": "1.7.8", + "@walletconnect/logger": "2.1.2", + "@walletconnect/universal-provider": "2.21.0", + "valtio": "1.13.2", + "viem": ">=2.29.0" + }, + "peerDependencies": { + "valtio": "1.13.2" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@noble/ciphers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", + "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@walletconnect/core": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", + "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.33.0", + "events": "3.3.0", + "uint8arrays": "3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-utils/node_modules/@walletconnect/sign-client": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", + "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/core": "2.21.0", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "2.1.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@walletconnect/types": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", + "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@walletconnect/universal-provider": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", + "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/jsonrpc-http-connection": "1.0.8", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "@walletconnect/sign-client": "2.21.0", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "es-toolkit": "1.33.0", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@walletconnect/utils": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", + "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", + "license": "Apache-2.0", + "dependencies": { + "@noble/ciphers": "1.2.1", + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "bs58": "6.0.0", + "detect-browser": "5.3.0", + "query-string": "7.1.3", + "uint8arrays": "3.1.0", + "viem": "2.23.2" + } + }, + "node_modules/@reown/appkit-utils/node_modules/@walletconnect/utils/node_modules/viem": { + "version": "2.23.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", + "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@scure/bip32": "1.6.2", + "@scure/bip39": "1.5.4", + "abitype": "1.0.8", + "isows": "1.0.6", + "ox": "0.6.7", + "ws": "8.18.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-utils/node_modules/abitype": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", + "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-utils/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/@reown/appkit-utils/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/@reown/appkit-utils/node_modules/isows": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", + "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/@reown/appkit-utils/node_modules/ox": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", + "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-utils/node_modules/unstorage": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", + "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-utils/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@reown/appkit-wallet": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.7.8.tgz", + "integrity": "sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A==", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit-common": "1.7.8", + "@reown/appkit-polyfills": "1.7.8", + "@walletconnect/logger": "2.1.2", + "zod": "3.22.4" + } + }, + "node_modules/@reown/appkit-wallet/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@reown/appkit/node_modules/@noble/ciphers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", + "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit/node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit/node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit/node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@reown/appkit/node_modules/@walletconnect/core": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", + "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.33.0", + "events": "3.3.0", + "uint8arrays": "3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@reown/appkit/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@reown/appkit/node_modules/@walletconnect/sign-client": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", + "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/core": "2.21.0", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "2.1.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit/node_modules/@walletconnect/types": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", + "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit/node_modules/@walletconnect/universal-provider": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", + "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/jsonrpc-http-connection": "1.0.8", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "@walletconnect/sign-client": "2.21.0", + "@walletconnect/types": "2.21.0", + "@walletconnect/utils": "2.21.0", + "es-toolkit": "1.33.0", + "events": "3.3.0" + } + }, + "node_modules/@reown/appkit/node_modules/@walletconnect/utils": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", + "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", + "license": "Apache-2.0", + "dependencies": { + "@noble/ciphers": "1.2.1", + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.0", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "bs58": "6.0.0", + "detect-browser": "5.3.0", + "query-string": "7.1.3", + "uint8arrays": "3.1.0", + "viem": "2.23.2" + } + }, + "node_modules/@reown/appkit/node_modules/@walletconnect/utils/node_modules/viem": { + "version": "2.23.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", + "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@scure/bip32": "1.6.2", + "@scure/bip39": "1.5.4", + "abitype": "1.0.8", + "isows": "1.0.6", + "ox": "0.6.7", + "ws": "8.18.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@reown/appkit/node_modules/abitype": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", + "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@reown/appkit/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/@reown/appkit/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/@reown/appkit/node_modules/isows": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", + "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/@reown/appkit/node_modules/ox": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", + "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@reown/appkit/node_modules/unstorage": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", + "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@reown/appkit/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@safe-global/safe-apps-provider": { + "version": "0.18.6", + "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.6.tgz", + "integrity": "sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==", + "license": "MIT", + "dependencies": { + "@safe-global/safe-apps-sdk": "^9.1.0", + "events": "^3.3.0" + } + }, + "node_modules/@safe-global/safe-apps-sdk": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz", + "integrity": "sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==", + "license": "MIT", + "dependencies": { + "@safe-global/safe-gateway-typescript-sdk": "^3.5.3", + "viem": "^2.1.1" + } + }, + "node_modules/@safe-global/safe-gateway-typescript-sdk": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.23.1.tgz", + "integrity": "sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@solana-program/system": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@solana-program/system/-/system-0.10.0.tgz", + "integrity": "sha512-Go+LOEZmqmNlfr+Gjy5ZWAdY5HbYzk2RBewD9QinEU/bBSzpFfzqDRT55JjFRBGJUvMgf3C2vfXEGT4i8DSI4g==", + "license": "Apache-2.0", + "peerDependencies": { + "@solana/kit": "^5.0" + } + }, + "node_modules/@solana-program/token": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@solana-program/token/-/token-0.9.0.tgz", + "integrity": "sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==", + "license": "Apache-2.0", + "peerDependencies": { + "@solana/kit": "^5.0" + } + }, + "node_modules/@solana/accounts": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/accounts/-/accounts-5.4.0.tgz", + "integrity": "sha512-qHtAtwCcCFTXcya6JOOG1nzYicivivN/JkcYNHr10qOp9b4MVRkfW1ZAAG1CNzjMe5+mwtEl60RwdsY9jXNb+Q==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/rpc-spec": "5.4.0", + "@solana/rpc-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/addresses": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/addresses/-/addresses-5.4.0.tgz", + "integrity": "sha512-YRHiH30S8qDV4bZ+mtEk589PGfBuXHzD/fK2Z+YI5f/+s+yi/5le/fVw7PN6LxnnmVQKiRCDUiNF+WmFFKi6QQ==", + "license": "MIT", + "dependencies": { + "@solana/assertions": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/nominal-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/assertions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/assertions/-/assertions-5.4.0.tgz", + "integrity": "sha512-8EP7mkdnrPc9y67FqWeAPzdWq2qAOkxsuo+ZBIXNWtIixDtXIdHrgjZ/wqbWxLgSTtXEfBCjpZU55Xw2Qfbwyg==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-5.4.0.tgz", + "integrity": "sha512-IbDCUvNX0MrkQahxiXj9rHzkd/fYfp1F2nTJkHGH8v+vPfD+YPjl007ZBM38EnCeXj/Xn+hxqBBivPvIHP29dA==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "5.4.0", + "@solana/codecs-data-structures": "5.4.0", + "@solana/codecs-numbers": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/options": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-5.4.0.tgz", + "integrity": "sha512-rQ5jXgiDe2vIU+mYCHDjgwMd9WdzZfh4sc5H6JgYleAUjeTUX6mx8hTV2+pcXvvn27LPrgrt9jfxswbDb8O8ww==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-data-structures": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-5.4.0.tgz", + "integrity": "sha512-LVssbdQ1GfY6upnxW3mufYsNfvTWKnHNk5Hx2gHuOYJhm3HZlp+Y8zvuoY65G1d1xAXkPz5YVGxaSeVIRWLGWg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "5.4.0", + "@solana/codecs-numbers": "5.4.0", + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-5.4.0.tgz", + "integrity": "sha512-z6LMkY+kXWx1alrvIDSAxexY5QLhsso638CjM7XI1u6dB7drTLWKhifyjnm1vOQc1VPVFmbYxTgKKpds8TY8tg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "5.4.0", + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/codecs-strings": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-5.4.0.tgz", + "integrity": "sha512-w0trrjfQDhkCVz7O1GTmHBk9m+MkljKx2uNBbQAD3/yW2Qn9dYiTrZ1/jDVq0/+lPPAUkbT3s3Yo7HUZ2QFmHw==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "5.4.0", + "@solana/codecs-numbers": "5.4.0", + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "fastestsmallesttextencoderdecoder": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/errors": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-5.4.0.tgz", + "integrity": "sha512-hNoAOmlZAszaVBrAy1Jf7amHJ8wnUnTU0BqhNQXknbSvirvsYr81yEud2iq18YiCqhyJ9SuQ5kWrSAT0x7S0oA==", + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "commander": "14.0.2" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/fast-stable-stringify": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/fast-stable-stringify/-/fast-stable-stringify-5.4.0.tgz", + "integrity": "sha512-KB7PUL7yalPvbWCezzyUDVRDp39eHLPH7OJ6S8VFT8YNIFUANwwj5ctui50Fim76kvSYDdYJOclXV45O2gfQ8Q==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/functional": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/functional/-/functional-5.4.0.tgz", + "integrity": "sha512-32ghHO0bg6GgX/7++0/7Lps6RgeXD2gKF1okiuyEGuVfKENIapgaQdcGhUwb3q6D6fv6MRAVn/Yve4jopGVNMQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/instruction-plans": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/instruction-plans/-/instruction-plans-5.4.0.tgz", + "integrity": "sha512-5xbJ+I/pP2aWECmK75bEM1zCnIITlohAK83dVN+t5X2vBFrr6M9gifo8r4Opdnibsgo6QVVkKPxRo5zow5j0ig==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0", + "@solana/instructions": "5.4.0", + "@solana/keys": "5.4.0", + "@solana/promises": "5.4.0", + "@solana/transaction-messages": "5.4.0", + "@solana/transactions": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/instructions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/instructions/-/instructions-5.4.0.tgz", + "integrity": "sha512-//a7jpHbNoAgTqy3YyqG1X6QhItJLKzJa6zuYJGCwaAAJye7BxS9pxJBgb2mUt7CGidhUksf+U8pmLlxCNWYyg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "5.4.0", + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/keys": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/keys/-/keys-5.4.0.tgz", + "integrity": "sha512-zQVbAwdoXorgXjlhlVTZaymFG6N8n1zn2NT+xI6S8HtbrKIB/42xPdXFh+zIihGzRw+9k8jzU7Axki/IPm6qWQ==", + "license": "MIT", + "dependencies": { + "@solana/assertions": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/nominal-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/kit": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/kit/-/kit-5.4.0.tgz", + "integrity": "sha512-aVjN26jOEzJA6UBYxSTQciZPXgTxWnO/WysHrw+yeBL/5AaTZnXEgb4j5xV6cUFzOlVxhJBrx51xtoxSqJ0u3g==", + "license": "MIT", + "dependencies": { + "@solana/accounts": "5.4.0", + "@solana/addresses": "5.4.0", + "@solana/codecs": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/functional": "5.4.0", + "@solana/instruction-plans": "5.4.0", + "@solana/instructions": "5.4.0", + "@solana/keys": "5.4.0", + "@solana/offchain-messages": "5.4.0", + "@solana/plugin-core": "5.4.0", + "@solana/programs": "5.4.0", + "@solana/rpc": "5.4.0", + "@solana/rpc-api": "5.4.0", + "@solana/rpc-parsed-types": "5.4.0", + "@solana/rpc-spec-types": "5.4.0", + "@solana/rpc-subscriptions": "5.4.0", + "@solana/rpc-types": "5.4.0", + "@solana/signers": "5.4.0", + "@solana/sysvars": "5.4.0", + "@solana/transaction-confirmation": "5.4.0", + "@solana/transaction-messages": "5.4.0", + "@solana/transactions": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/nominal-types": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/nominal-types/-/nominal-types-5.4.0.tgz", + "integrity": "sha512-h4dTRQwTerzksE5B1WmObN6TvLo8dYUd7kpUUynGd8WJjK0zz3zkDhq0MkA3aF6A1C2C82BSGqSsN9EN0E6Exg==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/offchain-messages": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/offchain-messages/-/offchain-messages-5.4.0.tgz", + "integrity": "sha512-DjdlYJCcKfgh4dkdk+owH1bP+Q4BRqCs55mgWWp9PTwm/HHy/a5vcMtCi1GyIQXfhtNNvKBLbXrUE0Fxej8qlg==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/codecs-data-structures": "5.4.0", + "@solana/codecs-numbers": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/keys": "5.4.0", + "@solana/nominal-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/options": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-5.4.0.tgz", + "integrity": "sha512-h4vTWRChEXPhaHo9i1pCyQBWWs+NqYPQRXSAApqpUYvHb9Kct/C6KbHjfyaRMyqNQnDHLcJCX7oW9tk0iRDzIg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "5.4.0", + "@solana/codecs-data-structures": "5.4.0", + "@solana/codecs-numbers": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/plugin-core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/plugin-core/-/plugin-core-5.4.0.tgz", + "integrity": "sha512-e1aLGLldW7C5113qTOjFYSGq95a4QC9TWb77iq+8l6h085DcNj+195r4E2zKaINrevQjQTwvxo00oUyHP7hSJA==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/programs": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/programs/-/programs-5.4.0.tgz", + "integrity": "sha512-Sc90WK9ZZ7MghOflIvkrIm08JwsFC99yqSJy28/K+hDP2tcx+1x+H6OFP9cumW9eUA1+JVRDeKAhA8ak7e/kUA==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/promises": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/promises/-/promises-5.4.0.tgz", + "integrity": "sha512-23mfgNBbuP6Q+4vsixGy+GkyZ7wBLrxTBNXqrG/XWrJhjuuSkjEUGaK4Fx5o7LIrBi6KGqPknKxmTlvqnJhy2Q==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc/-/rpc-5.4.0.tgz", + "integrity": "sha512-S6GRG+usnubDs0JSpgc0ZWEh9IPL5KPWMuBoD8ggGVOIVWntp53FpvhYslNzbxWBXlTvJecr2todBipGVM/AqQ==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0", + "@solana/fast-stable-stringify": "5.4.0", + "@solana/functional": "5.4.0", + "@solana/rpc-api": "5.4.0", + "@solana/rpc-spec": "5.4.0", + "@solana/rpc-spec-types": "5.4.0", + "@solana/rpc-transformers": "5.4.0", + "@solana/rpc-transport-http": "5.4.0", + "@solana/rpc-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-api": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-api/-/rpc-api-5.4.0.tgz", + "integrity": "sha512-FJL6KaAsQ4DhfhLKKMcqbTpToNFwHlABCemIpOunE3OSqJFDrmc/NbsEaLIoeHyIg3d1Imo49GIUOn2TEouFUA==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/keys": "5.4.0", + "@solana/rpc-parsed-types": "5.4.0", + "@solana/rpc-spec": "5.4.0", + "@solana/rpc-transformers": "5.4.0", + "@solana/rpc-types": "5.4.0", + "@solana/transaction-messages": "5.4.0", + "@solana/transactions": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-parsed-types": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-parsed-types/-/rpc-parsed-types-5.4.0.tgz", + "integrity": "sha512-IRQuSzx+Sj1A3XGiIzguNZlMjMMybXTTjV/RnTwBgnJQPd/H4us4pfPD94r+/yolWDVfGjJRm04hnKVMjJU8Rg==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-spec": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-spec/-/rpc-spec-5.4.0.tgz", + "integrity": "sha512-XMhxBb1GuZ3Kaeu5WNHB5KteCQ/aVuMByZmUKPqaanD+gs5MQZr0g62CvN7iwRlFU7GC18Q73ROWR3/JjzbXTA==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0", + "@solana/rpc-spec-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-spec-types": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-spec-types/-/rpc-spec-types-5.4.0.tgz", + "integrity": "sha512-JU9hC5/iyJx30ym17gpoXDtT9rCbO6hLpB6UDhSFFoNeirxtTVb4OdnKtsjJDfXAiXsynJRsZRwfj3vGxRLgQw==", + "license": "MIT", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions/-/rpc-subscriptions-5.4.0.tgz", + "integrity": "sha512-051t1CEjjAzM9ohjj2zb3ED70yeS3ZY8J5wSytL6tthTGImw/JB2a0D9DWMOKriFKt496n95IC+IdpJ35CpBWA==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0", + "@solana/fast-stable-stringify": "5.4.0", + "@solana/functional": "5.4.0", + "@solana/promises": "5.4.0", + "@solana/rpc-spec-types": "5.4.0", + "@solana/rpc-subscriptions-api": "5.4.0", + "@solana/rpc-subscriptions-channel-websocket": "5.4.0", + "@solana/rpc-subscriptions-spec": "5.4.0", + "@solana/rpc-transformers": "5.4.0", + "@solana/rpc-types": "5.4.0", + "@solana/subscribable": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-api": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-api/-/rpc-subscriptions-api-5.4.0.tgz", + "integrity": "sha512-euAFIG6ruEsqK+MsrL1tGSMbbOumm8UAyGzlD/kmXsAqqhcVsSeZdv5+BMIHIBsQ93GHcloA8UYw1BTPhpgl9w==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/keys": "5.4.0", + "@solana/rpc-subscriptions-spec": "5.4.0", + "@solana/rpc-transformers": "5.4.0", + "@solana/rpc-types": "5.4.0", + "@solana/transaction-messages": "5.4.0", + "@solana/transactions": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-channel-websocket": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-channel-websocket/-/rpc-subscriptions-channel-websocket-5.4.0.tgz", + "integrity": "sha512-kWCmlW65MccxqXwKsIz+LkXUYQizgvBrrgYOkyclJHPa+zx4gqJjam87+wzvO9cfbDZRer3wtJBaRm61gTHNbw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0", + "@solana/functional": "5.4.0", + "@solana/rpc-subscriptions-spec": "5.4.0", + "@solana/subscribable": "5.4.0", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-channel-websocket/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-subscriptions-spec": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-spec/-/rpc-subscriptions-spec-5.4.0.tgz", + "integrity": "sha512-ELaV9Z39GtKyUO0++he00ymWleb07QXYJhSfA0e1N5Q9hXu/Y366kgXHDcbZ/oUJkT3ylNgTupkrsdtiy8Ryow==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0", + "@solana/promises": "5.4.0", + "@solana/rpc-spec-types": "5.4.0", + "@solana/subscribable": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-transformers": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-transformers/-/rpc-transformers-5.4.0.tgz", + "integrity": "sha512-dZ8keYloLW+eRAwAPb471uWCFs58yHloLoI+QH0FulYpsSJ7F2BNWYcdnjSS/WiggsNcU6DhpWzYAzlEY66lGQ==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0", + "@solana/functional": "5.4.0", + "@solana/nominal-types": "5.4.0", + "@solana/rpc-spec-types": "5.4.0", + "@solana/rpc-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-transport-http": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-transport-http/-/rpc-transport-http-5.4.0.tgz", + "integrity": "sha512-vidA+Qtqrnqp3QSVumWHdWJ/986yCr5+qX3fbc9KPm9Ofoto88OMWB/oLJvi2Tfges1UBu/jl+lJdsVckCM1bA==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0", + "@solana/rpc-spec": "5.4.0", + "@solana/rpc-spec-types": "5.4.0", + "undici-types": "^7.18.2" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/rpc-types": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/rpc-types/-/rpc-types-5.4.0.tgz", + "integrity": "sha512-+C4N4/5AYzBdt3Y2yzkScknScy/jTx6wfvuJIY9XjOXtdDyZ8TmrnMwdPMTZPGLdLuHplJwlwy1acu/4hqmrBQ==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/codecs-numbers": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/nominal-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/signers": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/signers/-/signers-5.4.0.tgz", + "integrity": "sha512-s+fZxpi6UPr6XNk2pH/R84WjNRoSktrgG8AGNfsj/V8MJ++eKX7hhIf4JsHZtnnQXXrHmS3ozB2oHlc8yEJvCQ==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/instructions": "5.4.0", + "@solana/keys": "5.4.0", + "@solana/nominal-types": "5.4.0", + "@solana/offchain-messages": "5.4.0", + "@solana/transaction-messages": "5.4.0", + "@solana/transactions": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/subscribable": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/subscribable/-/subscribable-5.4.0.tgz", + "integrity": "sha512-72LmfNX7UENgA24sn/xjlWpPAOsrxkWb9DQhuPZxly/gq8rl/rvr7Xu9qBkvFF2po9XpdUrKlccqY4awvfpltA==", + "license": "MIT", + "dependencies": { + "@solana/errors": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/sysvars": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-5.4.0.tgz", + "integrity": "sha512-A5NES7sOlFmpnsiEts5vgyL3NXrt/tGGVSEjlEGvsgwl5EDZNv+xWnNA400uMDqd9O3a5PmH7p/6NsgR+kUzSg==", + "license": "MIT", + "dependencies": { + "@solana/accounts": "5.4.0", + "@solana/codecs": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/rpc-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transaction-confirmation": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/transaction-confirmation/-/transaction-confirmation-5.4.0.tgz", + "integrity": "sha512-EdSDgxs84/4gkjQw2r7N+Kgus8x9U+NFo0ufVG+48V8Hzy2t0rlBuXgIxwx0zZwUuTIgaKhpIutJgVncwZ5koA==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/keys": "5.4.0", + "@solana/promises": "5.4.0", + "@solana/rpc": "5.4.0", + "@solana/rpc-subscriptions": "5.4.0", + "@solana/rpc-types": "5.4.0", + "@solana/transaction-messages": "5.4.0", + "@solana/transactions": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transaction-messages": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/transaction-messages/-/transaction-messages-5.4.0.tgz", + "integrity": "sha512-qd/3kZDaPiHM0amhn3vXnupfcsFTVz6CYuHXvq9HFv/fq32+5Kp1FMLnmHwoSxQxdTMDghPdOhC4vhNhuWmuVQ==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/codecs-data-structures": "5.4.0", + "@solana/codecs-numbers": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/functional": "5.4.0", + "@solana/instructions": "5.4.0", + "@solana/nominal-types": "5.4.0", + "@solana/rpc-types": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/transactions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@solana/transactions/-/transactions-5.4.0.tgz", + "integrity": "sha512-OuY4M4x/xna8KZQIrz8tSrI9EEul9Od97XejqFmGGkEjbRsUOfJW8705TveTW8jU3bd5RGecFYscPgS2F+m7jQ==", + "license": "MIT", + "dependencies": { + "@solana/addresses": "5.4.0", + "@solana/codecs-core": "5.4.0", + "@solana/codecs-data-structures": "5.4.0", + "@solana/codecs-numbers": "5.4.0", + "@solana/codecs-strings": "5.4.0", + "@solana/errors": "5.4.0", + "@solana/functional": "5.4.0", + "@solana/instructions": "5.4.0", + "@solana/keys": "5.4.0", + "@solana/nominal-types": "5.4.0", + "@solana/rpc-types": "5.4.0", + "@solana/transaction-messages": "5.4.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.17", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.17.tgz", + "integrity": "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.17", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.17.tgz", + "integrity": "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.17" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz", + "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vanilla-extract/css": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.17.3.tgz", + "integrity": "sha512-jHivr1UPoJTX5Uel4AZSOwrCf4mO42LcdmnhJtUxZaRWhW4FviFbIfs0moAWWld7GOT+2XnuVZjjA/K32uUnMQ==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.8", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.0.7", + "dedent": "^1.5.3", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "lru-cache": "^10.4.3", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", + "picocolors": "^1.0.0" + } + }, + "node_modules/@vanilla-extract/dynamic": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.4.tgz", + "integrity": "sha512-7+Ot7VlP3cIzhJnTsY/kBtNs21s0YD7WI1rKJJKYP56BkbDxi/wrQUWMGEczKPUDkJuFcvbye+E2ub1u/mHH9w==", + "license": "MIT", + "dependencies": { + "@vanilla-extract/private": "^1.0.8" + } + }, + "node_modules/@vanilla-extract/private": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.9.tgz", + "integrity": "sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==", + "license": "MIT" + }, + "node_modules/@vanilla-extract/sprinkles": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/sprinkles/-/sprinkles-1.6.4.tgz", + "integrity": "sha512-lW3MuIcdIeHKX81DzhTnw68YJdL1ial05exiuvTLJMdHXQLKcVB93AncLPajMM6mUhaVVx5ALZzNHMTrq/U9Hg==", + "license": "MIT", + "peerDependencies": { + "@vanilla-extract/css": "^1.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@wagmi/connectors": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-6.2.0.tgz", + "integrity": "sha512-2NfkbqhNWdjfibb4abRMrn7u6rPjEGolMfApXss6HCDVt9AW2oVC6k8Q5FouzpJezElxLJSagWz9FW1zaRlanA==", + "license": "MIT", + "dependencies": { + "@base-org/account": "2.4.0", + "@coinbase/wallet-sdk": "4.3.6", + "@gemini-wallet/core": "0.3.2", + "@metamask/sdk": "0.33.1", + "@safe-global/safe-apps-provider": "0.18.6", + "@safe-global/safe-apps-sdk": "9.1.0", + "@walletconnect/ethereum-provider": "2.21.1", + "cbw-sdk": "npm:@coinbase/wallet-sdk@3.9.3", + "porto": "0.2.35" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@wagmi/core": "2.22.1", + "typescript": ">=5.0.4", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wagmi/connectors/node_modules/ox": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.17.tgz", + "integrity": "sha512-rKAnhzhRU3Xh3hiko+i1ZxywZ55eWQzeS/Q4HRKLx2PqfHOolisZHErSsJVipGlmQKHW5qwOED/GighEw9dbLg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wagmi/connectors/node_modules/porto": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/porto/-/porto-0.2.35.tgz", + "integrity": "sha512-gu9FfjjvvYBgQXUHWTp6n3wkTxVtEcqFotM7i3GEZeoQbvLGbssAicCz6hFZ8+xggrJWwi/RLmbwNra50SMmUQ==", + "license": "MIT", + "dependencies": { + "hono": "^4.10.3", + "idb-keyval": "^6.2.1", + "mipd": "^0.0.7", + "ox": "^0.9.6", + "zod": "^4.1.5", + "zustand": "^5.0.1" + }, + "bin": { + "porto": "dist/cli/bin/index.js" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.59.0", + "@wagmi/core": ">=2.16.3", + "expo-auth-session": ">=7.0.8", + "expo-crypto": ">=15.0.7", + "expo-web-browser": ">=15.0.8", + "react": ">=18", + "react-native": ">=0.81.4", + "typescript": ">=5.4.0", + "viem": ">=2.37.0", + "wagmi": ">=2.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/react-query": { + "optional": true + }, + "expo-auth-session": { + "optional": true + }, + "expo-crypto": { + "optional": true + }, + "expo-web-browser": { + "optional": true + }, + "react": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + }, + "wagmi": { + "optional": true + } + } + }, + "node_modules/@wagmi/connectors/node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@wagmi/core": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.22.1.tgz", + "integrity": "sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg==", + "license": "MIT", + "dependencies": { + "eventemitter3": "5.0.1", + "mipd": "0.0.7", + "zustand": "5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5.0.0", + "typescript": ">=5.0.4", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@tanstack/query-core": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@wagmi/core/node_modules/zustand": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", + "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/@walletconnect/core": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.1.tgz", + "integrity": "sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.1", + "@walletconnect/utils": "2.21.1", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.33.0", + "events": "3.3.0", + "uint8arrays": "3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@walletconnect/core/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/core/node_modules/unstorage": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", + "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/environment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", + "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/environment/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/ethereum-provider": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@walletconnect/ethereum-provider/-/ethereum-provider-2.21.1.tgz", + "integrity": "sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit": "1.7.8", + "@walletconnect/jsonrpc-http-connection": "1.0.8", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/sign-client": "2.21.1", + "@walletconnect/types": "2.21.1", + "@walletconnect/universal-provider": "2.21.1", + "@walletconnect/utils": "2.21.1", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/ethereum-provider/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/ethereum-provider/node_modules/unstorage": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", + "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", + "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", + "license": "MIT", + "dependencies": { + "keyvaluestorage-interface": "^1.0.0", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/events/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/heartbeat": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", + "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "license": "MIT", + "dependencies": { + "@walletconnect/events": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-http-connection": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.8.tgz", + "integrity": "sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.1", + "cross-fetch": "^3.1.4", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-http-connection/node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/@walletconnect/jsonrpc-provider": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", + "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", + "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "keyvaluestorage-interface": "^1.0.0" + } + }, + "node_modules/@walletconnect/jsonrpc-utils": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", + "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", + "license": "MIT", + "dependencies": { + "@walletconnect/environment": "^1.0.1", + "@walletconnect/jsonrpc-types": "^1.0.3", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/jsonrpc-ws-connection": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", + "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0", + "ws": "^7.5.1" + } + }, + "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@walletconnect/logger": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-2.1.2.tgz", + "integrity": "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "7.11.0" + } + }, + "node_modules/@walletconnect/relay-api": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", + "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-types": "^1.0.2" + } + }, + "node_modules/@walletconnect/relay-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", + "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.0", + "@noble/hashes": "1.7.0", + "@walletconnect/safe-json": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "uint8arrays": "^3.0.0" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/safe-json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", + "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/safe-json/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/sign-client": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.1.tgz", + "integrity": "sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/core": "2.21.1", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "2.1.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.1", + "@walletconnect/utils": "2.21.1", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", + "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/time/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/types": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.1.tgz", + "integrity": "sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/types/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/types/node_modules/unstorage": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", + "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/universal-provider": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.1.tgz", + "integrity": "sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==", + "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", + "license": "Apache-2.0", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/jsonrpc-http-connection": "1.0.8", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "2.1.2", + "@walletconnect/sign-client": "2.21.1", + "@walletconnect/types": "2.21.1", + "@walletconnect/utils": "2.21.1", + "es-toolkit": "1.33.0", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/unstorage": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", + "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.1.tgz", + "integrity": "sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==", + "license": "Apache-2.0", + "dependencies": { + "@noble/ciphers": "1.2.1", + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.21.1", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "bs58": "6.0.0", + "detect-browser": "5.3.0", + "query-string": "7.1.3", + "uint8arrays": "3.1.0", + "viem": "2.23.2" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/ciphers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", + "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/abitype": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", + "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/@walletconnect/utils/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/@walletconnect/utils/node_modules/isows": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", + "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/@walletconnect/utils/node_modules/ox": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", + "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/unstorage": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.3.tgz", + "integrity": "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^4.0.3", + "destr": "^2.0.5", + "h3": "^1.15.4", + "lru-cache": "^10.4.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.1" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6.0.3 || ^7.0.0", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/viem": { + "version": "2.23.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", + "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@scure/bip32": "1.6.2", + "@scure/bip39": "1.5.4", + "abitype": "1.0.8", + "isows": "1.0.6", + "ox": "0.6.7", + "ws": "8.18.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@walletconnect/window-getters": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", + "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-getters/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/window-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", + "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", + "license": "MIT", + "dependencies": { + "@walletconnect/window-getters": "^1.0.1", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-metadata/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/async-mutex": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.6.tgz", + "integrity": "sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/big.js": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/bowser": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cbw-sdk": { + "name": "@coinbase/wallet-sdk", + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-3.9.3.tgz", + "integrity": "sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.1", + "buffer": "^6.0.3", + "clsx": "^1.2.1", + "eth-block-tracker": "^7.1.0", + "eth-json-rpc-filters": "^6.0.0", + "eventemitter3": "^5.0.1", + "keccak": "^3.0.3", + "preact": "^10.16.0", + "sha.js": "^2.4.11" + } + }, + "node_modules/cbw-sdk/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cuer": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/cuer/-/cuer-0.0.3.tgz", + "integrity": "sha512-f/UNxRMRCYtfLEGECAViByA3JNflZImOk11G9hwSd+44jvzrc99J35u5l+fbdQ2+ZG441GvOpaeGYBmWquZsbQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "qr": "~0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/derive-valtio": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.1.0.tgz", + "integrity": "sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==", + "license": "MIT", + "peerDependencies": { + "valtio": "*" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", + "license": "MIT" + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eciesjs": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.16.tgz", + "integrity": "sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==", + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.4", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + } + }, + "node_modules/eciesjs/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.33.0.tgz", + "integrity": "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eth-block-tracker": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-7.1.0.tgz", + "integrity": "sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg==", + "license": "MIT", + "dependencies": { + "@metamask/eth-json-rpc-provider": "^1.0.0", + "@metamask/safe-event-emitter": "^3.0.0", + "@metamask/utils": "^5.0.1", + "json-rpc-random-id": "^1.0.1", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/eth-block-tracker/node_modules/@metamask/utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", + "integrity": "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==", + "license": "ISC", + "dependencies": { + "@ethereumjs/tx": "^4.1.2", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "semver": "^7.3.8", + "superstruct": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/eth-block-tracker/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eth-block-tracker/node_modules/superstruct": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", + "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/eth-json-rpc-filters": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/eth-json-rpc-filters/-/eth-json-rpc-filters-6.0.1.tgz", + "integrity": "sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig==", + "license": "ISC", + "dependencies": { + "@metamask/safe-event-emitter": "^3.0.0", + "async-mutex": "^0.2.6", + "eth-query": "^2.1.2", + "json-rpc-engine": "^6.1.0", + "pify": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/eth-json-rpc-filters/node_modules/pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eth-query": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz", + "integrity": "sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA==", + "license": "ISC", + "dependencies": { + "json-rpc-random-id": "^1.0.0", + "xtend": "^4.0.1" + } + }, + "node_modules/eth-rpc-errors": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz", + "integrity": "sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "^2.0.6" + } + }, + "node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/extension-port-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/extension-port-stream/-/extension-port-stream-3.0.0.tgz", + "integrity": "sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==", + "license": "ISC", + "dependencies": { + "readable-stream": "^3.6.2 || ^4.4.2", + "webextension-polyfill": ">=0.10.0 <1.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/h3": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.4.tgz", + "integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.5", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.2", + "radix3": "^1.1.2", + "ufo": "^1.6.1", + "uncrypto": "^0.1.3" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-rpc-engine": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz", + "integrity": "sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==", + "license": "ISC", + "dependencies": { + "@metamask/safe-event-emitter": "^2.0.0", + "eth-rpc-errors": "^4.0.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/json-rpc-engine/node_modules/@metamask/safe-event-emitter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", + "integrity": "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==", + "license": "ISC" + }, + "node_modules/json-rpc-random-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz", + "integrity": "sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==", + "license": "ISC" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/keyvaluestorage-interface": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", + "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", + "license": "MIT" + }, + "node_modules/lit": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/media-query-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", + "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + } + }, + "node_modules/micro-ftch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", + "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mipd": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz", + "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/modern-ahocorasick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz", + "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obj-multiplex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/obj-multiplex/-/obj-multiplex-1.0.0.tgz", + "integrity": "sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==", + "license": "ISC", + "dependencies": { + "end-of-stream": "^1.4.0", + "once": "^1.4.0", + "readable-stream": "^2.3.3" + } + }, + "node_modules/obj-multiplex/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/obj-multiplex/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/obj-multiplex/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/obj-multiplex/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/on-exit-leak-free": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", + "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-fetch": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.13.8.tgz", + "integrity": "sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "license": "MIT" + }, + "node_modules/ox": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.11.3.tgz", + "integrity": "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pino": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", + "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.0.0", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.15.1" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/pony-cause": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.24.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", + "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", + "license": "MIT" + }, + "node_modules/proxy-compare": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz", + "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qr": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/qr/-/qr-0.5.4.tgz", + "integrity": "sha512-gjVMHOt7CX+BQd7JLQ9fnS4kJK4Lj4u+Conq52tcCbW7YH3mATTtBbTMA+7cQ1rKOkDo61olFHJReawe+XFxIA==", + "license": "(MIT OR Apache-2.0)", + "engines": { + "node": ">= 20.19.0" + } + }, + "node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", + "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rpc-websockets": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.2.tgz", + "integrity": "sha512-VuW2xJDnl1k8n8kjbdRSWawPRkwaVqUQNjE1TdeTawf0y0abGhtVJFTXCLfgpgGDBkO/Fj6kny8Dc/nvOW78MA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^8.3.2", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", + "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/thread-stream": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", + "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.1.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ufo": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz", + "integrity": "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==", + "license": "MIT" + }, + "node_modules/uint8arrays": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz", + "integrity": "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/valtio": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz", + "integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==", + "license": "MIT", + "dependencies": { + "derive-valtio": "0.1.0", + "proxy-compare": "2.6.0", + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/valtio/node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/viem": { + "version": "2.44.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.44.2.tgz", + "integrity": "sha512-nHY872t/T3flLpVsnvQT/89bwbrJwxaL917FDv7Oxy4E5FWIFkokRQOKXG3P+hgl30QYVZxi9o2SUHLnebycxw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.11.3", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/wagmi": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.19.5.tgz", + "integrity": "sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==", + "license": "MIT", + "dependencies": { + "@wagmi/connectors": "6.2.0", + "@wagmi/core": "2.22.1", + "use-sync-external-store": "1.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "react": ">=18", + "typescript": ">=5.0.4", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/webextension-polyfill": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", + "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==", + "license": "MPL-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json index ad1adb8e..3d0d7580 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,6 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.3.1", - "vite": "^5.4.1" + "vite": "^7.3.1" } } From 2e2fbb6a21372d453f2aec8af27ffc658dfa14c1 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Thu, 15 Jan 2026 08:38:33 -0800 Subject: [PATCH 002/174] update usdc defaults in frontend Signed-off-by: John Shutt --- frontend/src/App.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3cf255a6..a018dcf8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -128,6 +128,8 @@ const defaults = { ogMasterCopy: '0x28CeBFE94a03DbCA9d17143e9d2Bd1155DC26D5d', ogIdentifier: 'ASSERT_TRUTH2', ogLiveness: '172800', + collateral: '0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + bondAmount: (250n * 10n ** 6n).toString(), safeSaltNonce: '1', ogSaltNonce: '1', }; @@ -139,8 +141,8 @@ function App() { const { data: walletClient } = useWalletClient(); const [form, setForm] = useState({ rules: '', - collateral: '', - bondAmount: '', + collateral: defaults.collateral, + bondAmount: defaults.bondAmount, liveness: defaults.ogLiveness, identifier: defaults.ogIdentifier, safeSaltNonce: defaults.safeSaltNonce, From 70dc56ebca58e2f11d7543188c7616bef2878ad2 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Thu, 15 Jan 2026 09:37:21 -0800 Subject: [PATCH 003/174] set burn address as owner after deployment, so all txs go through og Signed-off-by: John Shutt --- frontend/src/App.jsx | 209 +++++++++++++++++- script/DeploySafeWithOptimisticGovernor.s.sol | 61 ++++- 2 files changed, 257 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a018dcf8..89ce49ca 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,8 +3,10 @@ import { ConnectButton } from '@rainbow-me/rainbowkit'; import { encodeAbiParameters, encodeFunctionData, + concatHex, hexToSignature, isAddress, + numberToHex, signatureToHex, stringToHex, zeroAddress, @@ -99,6 +101,27 @@ const safeAbi = [ ], outputs: [{ type: 'bool' }], }, + { + type: 'function', + name: 'addOwnerWithThreshold', + stateMutability: 'nonpayable', + inputs: [ + { name: 'owner', type: 'address' }, + { name: '_threshold', type: 'uint256' }, + ], + outputs: [], + }, + { + type: 'function', + name: 'removeOwner', + stateMutability: 'nonpayable', + inputs: [ + { name: 'prevOwner', type: 'address' }, + { name: 'owner', type: 'address' }, + { name: '_threshold', type: 'uint256' }, + ], + outputs: [], + }, ]; const enableModuleAbi = [ @@ -121,6 +144,30 @@ const ogSetupAbi = [ }, ]; +const MODULE_PROXY_FACTORY_BYTECODE = + '0x60808060405234610016576102e4908161001b8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63f1ab873c14610024575f80fd5b346100ce5760603660031901126100ce576004356001600160a01b03811681036100ce5760243567ffffffffffffffff81116100ce57366023820112156100ce5780600401359161007483610129565b6100816040519182610107565b83815236602485850101116100ce575f6020856100ca9660246100b09701838601378301015260443591610174565b6040516001600160a01b0390911681529081906020820190565b0390f35b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff82111761010257604052565b6100d2565b90601f8019910116810190811067ffffffffffffffff82111761010257604052565b67ffffffffffffffff811161010257601f01601f191660200190565b3d1561016f573d9061015682610129565b916101646040519384610107565b82523d5f602084013e565b606090565b90929183519060208501918220604091825190602082019283528382015282815261019e816100e6565b5190206001600160a01b0384811694909190851561029657835172602d8060093d393df3363d3d373d3d3d363d7360681b6020820190815260609290921b6bffffffffffffffffffffffff191660338201526e5af43d82803e903d91602b57fd5bf360881b604782015260368152610215816100e6565b51905ff590811692831561027457815f92918380939951925af1610237610145565b501561026457507f2150ada912bf189ed721c44211199e270903fc88008c2a1e1e889ef30fe67c5f5f80a3565b51637dabd39960e01b8152600490fd5b50905163371e9e8960e21b81526001600160a01b039091166004820152602490fd5b8351633202e20d60e21b815260048101879052602490fdfea26469706673582212208f37f4bfb66727d4e6c07c613af0febf39dcd35dcf8d6037c9da73384d61b55764736f6c63430008170033'; + +function readEnv(key) { + if (typeof process !== 'undefined' && process?.env?.[key]) { + return process.env[key]; + } + + if (typeof import.meta !== 'undefined') { + const metaEnv = import.meta?.env; + if (metaEnv?.[key]) { + return metaEnv[key]; + } + } + + return undefined; +} + +function readEnvWithPrefixes(key) { + return readEnv(key) ?? readEnv(`VITE_${key}`) ?? readEnv(`NEXT_PUBLIC_${key}`); +} + +const defaultModuleProxyFactory = readEnvWithPrefixes('MODULE_PROXY_FACTORY') ?? ''; + const defaults = { safeSingleton: '0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552', safeProxyFactory: '0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2', @@ -128,13 +175,16 @@ const defaults = { ogMasterCopy: '0x28CeBFE94a03DbCA9d17143e9d2Bd1155DC26D5d', ogIdentifier: 'ASSERT_TRUTH2', ogLiveness: '172800', - collateral: '0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + collateral: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', bondAmount: (250n * 10n ** 6n).toString(), safeSaltNonce: '1', ogSaltNonce: '1', + moduleProxyFactory: defaultModuleProxyFactory, }; const zeroLike = '0x0000000000000000000000000000000000000000'; +const BURN_OWNER = '0x000000000000000000000000000000000000dEaD'; +const SENTINEL_OWNERS = '0x0000000000000000000000000000000000000001'; function App() { const publicClient = usePublicClient(); @@ -151,7 +201,7 @@ function App() { safeProxyFactory: defaults.safeProxyFactory, safeFallbackHandler: defaults.safeFallbackHandler, ogMasterCopy: defaults.ogMasterCopy, - moduleProxyFactory: '', + moduleProxyFactory: defaults.moduleProxyFactory, }); const [deployment, setDeployment] = useState({ moduleProxyFactory: '', @@ -159,6 +209,7 @@ function App() { ogModule: '', }); const [txHashes, setTxHashes] = useState({ + moduleProxyFactory: '', safeProxy: '', ogModule: '', enableModule: '', @@ -223,16 +274,11 @@ function App() { return; } - if (!form.moduleProxyFactory) { - setError('ModuleProxyFactory address is required (deploy one externally or set MODULE_PROXY_FACTORY).'); - return; - } - setIsSubmitting(true); setError(''); setStatus('Preparing deployment...'); setDeployment({ moduleProxyFactory: '', safe: '', ogModule: '' }); - setTxHashes({ safeProxy: '', ogModule: '', enableModule: '' }); + setTxHashes({ moduleProxyFactory: '', safeProxy: '', ogModule: '', enableModule: '' }); try { const account = walletClient.account.address; @@ -241,6 +287,20 @@ function App() { const bondAmount = BigInt(form.bondAmount || '0'); const liveness = BigInt(form.liveness || '0'); const identifier = stringToHex(form.identifier, { size: 32 }); + let moduleProxyFactory = form.moduleProxyFactory; + + if (!moduleProxyFactory) { + setStatus('Deploying ModuleProxyFactory...'); + const deployTx = await walletClient.deployContract({ + abi: moduleProxyFactoryAbi, + bytecode: MODULE_PROXY_FACTORY_BYTECODE, + account, + }); + setTxHashes((prev) => ({ ...prev, moduleProxyFactory: deployTx })); + const receipt = await publicClient.waitForTransactionReceipt({ hash: deployTx }); + moduleProxyFactory = receipt.contractAddress ?? ''; + setForm((prev) => ({ ...prev, moduleProxyFactory })); + } const safeInitializer = encodeFunctionData({ abi: safeAbi, @@ -291,7 +351,7 @@ function App() { const ogSimulation = await publicClient.simulateContract({ account, - address: form.moduleProxyFactory, + address: moduleProxyFactory, abi: moduleProxyFactoryAbi, functionName: 'deployModule', args: [form.ogMasterCopy, ogInitializerCall, ogSaltNonce], @@ -334,7 +394,8 @@ function App() { const signature = await walletClient.signMessage({ message: { raw: txHash } }); const { r, s, v } = hexToSignature(signature); - const packedSignature = signatureToHex({ r, s, v }); + const safeV = (v >= 27n ? v : v + 27n) + 4n; // eth_sign flavor + const packedSignature = concatHex([r, s, numberToHex(safeV, { size: 1 })]); setStatus('Enabling module on the Safe...'); const execSimulation = await publicClient.simulateContract({ @@ -360,12 +421,136 @@ function App() { setTxHashes((prev) => ({ ...prev, enableModule: enableTxHash })); await publicClient.waitForTransactionReceipt({ hash: enableTxHash }); + setStatus('Setting burn address as sole Safe owner...'); + const addOwnerCallData = encodeFunctionData({ + abi: safeAbi, + functionName: 'addOwnerWithThreshold', + args: [BURN_OWNER, 1n], + }); + + const addOwnerNonce = await publicClient.readContract({ + address: safeProxy, + abi: safeAbi, + functionName: 'nonce', + }); + + const addOwnerTxHash = await publicClient.readContract({ + address: safeProxy, + abi: safeAbi, + functionName: 'getTransactionHash', + args: [ + safeProxy, + 0n, + addOwnerCallData, + 0, + 0n, + 0n, + 0n, + zeroAddress, + zeroAddress, + addOwnerNonce, + ], + }); + + const addOwnerSignature = await walletClient.signMessage({ message: { raw: addOwnerTxHash } }); + const { r: addOwnerR, s: addOwnerS, v: addOwnerVRaw } = hexToSignature(addOwnerSignature); + const addOwnerV = (addOwnerVRaw >= 27n ? addOwnerVRaw : addOwnerVRaw + 27n) + 4n; + const addOwnerPackedSignature = concatHex([ + addOwnerR, + addOwnerS, + numberToHex(addOwnerV, { size: 1 }), + ]); + + const addOwnerExec = await publicClient.simulateContract({ + account, + address: safeProxy, + abi: safeAbi, + functionName: 'execTransaction', + args: [ + safeProxy, + 0n, + addOwnerCallData, + 0, + 0n, + 0n, + 0n, + zeroAddress, + zeroAddress, + addOwnerPackedSignature, + ], + }); + + const addOwnerTx = await walletClient.writeContract(addOwnerExec.request); + await publicClient.waitForTransactionReceipt({ hash: addOwnerTx }); + + setStatus('Removing deployer from Safe owners...'); + const removeOwnerCallData = encodeFunctionData({ + abi: safeAbi, + functionName: 'removeOwner', + args: [SENTINEL_OWNERS, account, 1n], + }); + + const removeOwnerNonce = await publicClient.readContract({ + address: safeProxy, + abi: safeAbi, + functionName: 'nonce', + }); + + const removeOwnerTxHash = await publicClient.readContract({ + address: safeProxy, + abi: safeAbi, + functionName: 'getTransactionHash', + args: [ + safeProxy, + 0n, + removeOwnerCallData, + 0, + 0n, + 0n, + 0n, + zeroAddress, + zeroAddress, + removeOwnerNonce, + ], + }); + + const removeOwnerSignature = await walletClient.signMessage({ message: { raw: removeOwnerTxHash } }); + const { r: removeOwnerR, s: removeOwnerS, v: removeOwnerVRaw } = hexToSignature(removeOwnerSignature); + const removeOwnerV = (removeOwnerVRaw >= 27n ? removeOwnerVRaw : removeOwnerVRaw + 27n) + 4n; + const removeOwnerPackedSignature = concatHex([ + removeOwnerR, + removeOwnerS, + numberToHex(removeOwnerV, { size: 1 }), + ]); + + const removeOwnerExec = await publicClient.simulateContract({ + account, + address: safeProxy, + abi: safeAbi, + functionName: 'execTransaction', + args: [ + safeProxy, + 0n, + removeOwnerCallData, + 0, + 0n, + 0n, + 0n, + zeroAddress, + zeroAddress, + removeOwnerPackedSignature, + ], + }); + + const removeOwnerTx = await walletClient.writeContract(removeOwnerExec.request); + await publicClient.waitForTransactionReceipt({ hash: removeOwnerTx }); + setDeployment({ - moduleProxyFactory: form.moduleProxyFactory, + moduleProxyFactory, safe: safeProxy, ogModule, }); - setStatus('Deployment complete.'); + setStatus('Deployment complete (burn address is sole Safe owner).'); } catch (err) { setError(err?.shortMessage || err?.message || 'Deployment failed.'); setStatus(''); diff --git a/script/DeploySafeWithOptimisticGovernor.s.sol b/script/DeploySafeWithOptimisticGovernor.s.sol index 56cf544a..2cdd7388 100644 --- a/script/DeploySafeWithOptimisticGovernor.s.sol +++ b/script/DeploySafeWithOptimisticGovernor.s.sol @@ -109,6 +109,8 @@ contract DeploySafeWithOptimisticGovernor is Script { uint256 ogSaltNonce; } + address internal constant BURN_OWNER = 0x000000000000000000000000000000000000dEaD; + function run() external { // --------- // Required @@ -131,6 +133,7 @@ contract DeploySafeWithOptimisticGovernor is Script { address ogModule = deployOptimisticGovernor(config, moduleProxyFactory, safeProxy, rules); enableModule(DEPLOYER_PK, safeProxy, ogModule); + burnOwner(DEPLOYER_PK, safeProxy); vm.stopBroadcast(); @@ -247,7 +250,7 @@ contract DeploySafeWithOptimisticGovernor is Script { ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(deployerPk, txHash); - bytes memory sig = abi.encodePacked(r, s, v); + bytes memory sig = abi.encodePacked(r, s, v + 4); // eth_sign flavor bool ok = safe.execTransaction( safeProxy, 0, enableModuleCalldata, OP_CALL, 0, 0, 0, address(0), payable(address(0)), sig @@ -255,6 +258,62 @@ contract DeploySafeWithOptimisticGovernor is Script { require(ok, "enableModule execTransaction failed"); } + function burnOwner(uint256 deployerPk, address safeProxy) internal { + ISafe safe = ISafe(safeProxy); + + // Add burn owner + bytes memory addOwnerCalldata = abi.encodeWithSignature( + "addOwnerWithThreshold(address,uint256)", BURN_OWNER, 1 + ); + + bytes32 addOwnerTxHash = safe.getTransactionHash( + safeProxy, + 0, + addOwnerCalldata, + OP_CALL, + 0, + 0, + 0, + address(0), + address(0), + safe.nonce() + ); + + (uint8 addV, bytes32 addR, bytes32 addS) = vm.sign(deployerPk, addOwnerTxHash); + bytes memory addSig = abi.encodePacked(addR, addS, addV + 4); + + bool addOk = safe.execTransaction( + safeProxy, 0, addOwnerCalldata, OP_CALL, 0, 0, 0, address(0), payable(address(0)), addSig + ); + require(addOk, "add burn owner failed"); + + // Remove deployer owner + bytes memory removeOwnerCalldata = abi.encodeWithSignature( + "removeOwner(address,address,uint256)", safe.SENTINEL_OWNERS(), vm.addr(deployerPk), 1 + ); + + bytes32 removeOwnerTxHash = safe.getTransactionHash( + safeProxy, + 0, + removeOwnerCalldata, + OP_CALL, + 0, + 0, + 0, + address(0), + address(0), + safe.nonce() + ); + + (uint8 remV, bytes32 remR, bytes32 remS) = vm.sign(deployerPk, removeOwnerTxHash); + bytes memory remSig = abi.encodePacked(remR, remS, remV + 4); + + bool remOk = safe.execTransaction( + safeProxy, 0, removeOwnerCalldata, OP_CALL, 0, 0, 0, address(0), payable(address(0)), remSig + ); + require(remOk, "remove deployer owner failed"); + } + function logDeployment(address moduleProxyFactory, address safeProxy, address ogModule, Config memory config) internal { From e4502da3eb3b7bc0e0b4b8d7827539e891cd0608 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sat, 31 Jan 2026 12:02:27 -0800 Subject: [PATCH 004/174] Fix Safe removal and signatures --- script/DeploySafeWithOptimisticGovernor.s.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/script/DeploySafeWithOptimisticGovernor.s.sol b/script/DeploySafeWithOptimisticGovernor.s.sol index 2cdd7388..51db2442 100644 --- a/script/DeploySafeWithOptimisticGovernor.s.sol +++ b/script/DeploySafeWithOptimisticGovernor.s.sol @@ -110,6 +110,7 @@ contract DeploySafeWithOptimisticGovernor is Script { } address internal constant BURN_OWNER = 0x000000000000000000000000000000000000dEaD; + address internal constant SENTINEL_OWNERS = 0x0000000000000000000000000000000000000001; function run() external { // --------- @@ -250,7 +251,7 @@ contract DeploySafeWithOptimisticGovernor is Script { ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(deployerPk, txHash); - bytes memory sig = abi.encodePacked(r, s, v + 4); // eth_sign flavor + bytes memory sig = abi.encodePacked(r, s, v); // EIP-712 signature (no eth_sign prefix) bool ok = safe.execTransaction( safeProxy, 0, enableModuleCalldata, OP_CALL, 0, 0, 0, address(0), payable(address(0)), sig @@ -280,7 +281,7 @@ contract DeploySafeWithOptimisticGovernor is Script { ); (uint8 addV, bytes32 addR, bytes32 addS) = vm.sign(deployerPk, addOwnerTxHash); - bytes memory addSig = abi.encodePacked(addR, addS, addV + 4); + bytes memory addSig = abi.encodePacked(addR, addS, addV); bool addOk = safe.execTransaction( safeProxy, 0, addOwnerCalldata, OP_CALL, 0, 0, 0, address(0), payable(address(0)), addSig @@ -289,7 +290,7 @@ contract DeploySafeWithOptimisticGovernor is Script { // Remove deployer owner bytes memory removeOwnerCalldata = abi.encodeWithSignature( - "removeOwner(address,address,uint256)", safe.SENTINEL_OWNERS(), vm.addr(deployerPk), 1 + "removeOwner(address,address,uint256)", BURN_OWNER, vm.addr(deployerPk), 1 ); bytes32 removeOwnerTxHash = safe.getTransactionHash( @@ -306,7 +307,7 @@ contract DeploySafeWithOptimisticGovernor is Script { ); (uint8 remV, bytes32 remR, bytes32 remS) = vm.sign(deployerPk, removeOwnerTxHash); - bytes memory remSig = abi.encodePacked(remR, remS, remV + 4); + bytes memory remSig = abi.encodePacked(remR, remS, remV); bool remOk = safe.execTransaction( safeProxy, 0, removeOwnerCalldata, OP_CALL, 0, 0, 0, address(0), payable(address(0)), remSig From 254f458aab0141ff60585fa9396d9c335edba1e4 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sat, 31 Jan 2026 13:50:45 -0800 Subject: [PATCH 005/174] Add agent scaffold and frontend env generator --- .gitignore | 4 +- README.md | 30 +++++ agent/.env.example | 22 ++++ agent/README.md | 33 +++++ agent/package-lock.json | 232 +++++++++++++++++++++++++++++++++++ agent/package.json | 13 ++ agent/src/index.js | 259 ++++++++++++++++++++++++++++++++++++++++ frontend/src/App.jsx | 189 ++++++++++++++++++++++++++++- frontend/src/styles.css | 37 ++++++ 9 files changed, 817 insertions(+), 2 deletions(-) create mode 100644 agent/.env.example create mode 100644 agent/README.md create mode 100644 agent/package-lock.json create mode 100644 agent/package.json create mode 100644 agent/src/index.js diff --git a/.gitignore b/.gitignore index e02e932e..b7c69a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ docs/ .env .env.mainnet .env.sepolia +agent/.env # Anvil files anvil.log @@ -22,4 +23,5 @@ anvil.pid # Node modules node_modules -frontend/node_modules \ No newline at end of file +frontend/node_modules +agent/node_modules diff --git a/README.md b/README.md index 0cd0d6cd..bf143ed9 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,29 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis --private-key ``` +## Commitment Agent Tooling + +Use the offchain agent scaffold in `agent/` to serve commitments by posting bonds, proposing transactions, monitoring deposits, and making deposits on behalf of the commitment. It ships only generic tools; add commitment-specific behavior in your own prompts or handlers. + +### Setup & Run + +```shell +cd agent +npm install +cp .env.example .env # fill in RPC_URL, PRIVATE_KEY, COMMITMENT_SAFE, OG_MODULE, WATCH_ASSETS +npm start +``` + +The loop polls every `POLL_INTERVAL_MS` (default 60s) for ERC20 transfers into the commitment (and optional native balance increases). If nothing changes, the LLM/decision hook is not invoked. When signals are found, `decideOnSignals` is called—extend that function to route context into your system prompt and custom tools. + +### Built-in Agent Tools + +- `postBondAndPropose`: Approves the Optimistic Oracle for the module bond and calls `proposeTransactions` on the Optimistic Governor. +- `makeDeposit`: Sends ERC20 or native deposits into the commitment Safe using the agent key. +- `pollCommitmentChanges`: Watches configured assets (plus the OG collateral by default) for new deposits. + +Add more tools for a specific commitment beside these generics; keep the default agent lean. + ## Required Environment Variables - `DEPLOYER_PK`: Private key for the deployer. @@ -40,6 +63,8 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis The web frontend is a lightweight UI for filling in Safe + Optimistic Governor parameters and launching the same deployment flow as the script. It can be hosted as a static site and uses RPC endpoints to read chain state and craft the deployment payloads. +It also prepares `.env` files for the offchain agent scaffold (in `agent/`) so you can deploy a commitment and immediately configure an agent to serve it. + ### Dependencies - Node.js 18+ (or newer) @@ -54,6 +79,11 @@ npm install npm run dev ``` +Agent `.env` prep (in the UI): + +- Fill RPC, agent key, and addresses (Safe + OG) — the UI auto-fills addresses from the deployment section. +- Copy or download the rendered `.env`, then run the agent locally: `cd agent && npm install && npm start`. + ### Required Environment Variables Expose these values to the frontend build (for example via `.env` in the frontend project) so the UI can prefill defaults and target the correct network: diff --git a/agent/.env.example b/agent/.env.example new file mode 100644 index 00000000..97933364 --- /dev/null +++ b/agent/.env.example @@ -0,0 +1,22 @@ +# RPC endpoint and signing key +RPC_URL=http://127.0.0.1:8545 +PRIVATE_KEY=0xabc123... + +# Commitment + Optimistic Governor addresses +COMMITMENT_SAFE=0x0000000000000000000000000000000000000000 +OG_MODULE=0x0000000000000000000000000000000000000000 + +# Assets to watch for deposits (comma-separated ERC20 addresses) +WATCH_ASSETS=0x0000000000000000000000000000000000000000 + +# Polling loop +POLL_INTERVAL_MS=60000 +# Optional: start from a specific block number +# START_BLOCK=12345678 + +# Watch native ETH balance increases (true|false) +WATCH_NATIVE_BALANCE=true + +# Optional convenience defaults for deposits +# DEFAULT_DEPOSIT_ASSET=0x0000000000000000000000000000000000000000 +# DEFAULT_DEPOSIT_AMOUNT_WEI=0 diff --git a/agent/README.md b/agent/README.md new file mode 100644 index 00000000..ba4e063a --- /dev/null +++ b/agent/README.md @@ -0,0 +1,33 @@ +# Commitment Agent Scaffold + +Generic offchain agent wiring for monitoring a commitment and acting through the Optimistic Governor. It exposes only the core tools needed to serve commitments; add commitment-specific logic, prompts, and extra tools as needed. + +## Prerequisites + +- Node.js 18+ +- RPC endpoint the agent can reach +- Private key funded for gas and permissions to propose through the Optimistic Governor + +## Configure + +1. Copy `.env.example` to `.env` and fill in: + - `RPC_URL`: RPC the agent should use + - `PRIVATE_KEY`: agent signer (never commit this) + - `COMMITMENT_SAFE`: Safe address holding assets + - `OG_MODULE`: Optimistic Governor module address + - `WATCH_ASSETS`: Comma-separated ERC20s to monitor (the OG collateral is auto-added) + - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*` +2. Install deps and start the loop: + +```bash +npm install +npm start +``` + +## What the Agent Does + +- **Polls for deposits**: Checks ERC20 `Transfer` logs into the commitment and (optionally) native balance increases. If nothing changed, no LLM/decision code runs. +- **Bonds + proposes**: `postBondAndPropose` approves the OG collateral bond and calls `proposeTransactions` on the module. +- **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. + +All other behavior is intentionally left out. Implement your own `decideOnSignals` in `src/index.js` to call an LLM or custom tools when new signals appear. diff --git a/agent/package-lock.json b/agent/package-lock.json new file mode 100644 index 00000000..ddc4aca4 --- /dev/null +++ b/agent/package-lock.json @@ -0,0 +1,232 @@ +{ + "name": "og-commitment-agent", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "og-commitment-agent", + "version": "0.1.0", + "dependencies": { + "dotenv": "^16.4.5", + "viem": "^2.20.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/ox": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.11.3.tgz", + "integrity": "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem": { + "version": "2.45.1", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.45.1.tgz", + "integrity": "sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.11.3", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/agent/package.json b/agent/package.json new file mode 100644 index 00000000..8b40a000 --- /dev/null +++ b/agent/package.json @@ -0,0 +1,13 @@ +{ + "name": "og-commitment-agent", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start": "node ./src/index.js" + }, + "dependencies": { + "dotenv": "^16.4.5", + "viem": "^2.20.0" + } +} diff --git a/agent/src/index.js b/agent/src/index.js new file mode 100644 index 00000000..6f37652d --- /dev/null +++ b/agent/src/index.js @@ -0,0 +1,259 @@ +import 'dotenv/config'; +import { + createPublicClient, + createWalletClient, + erc20Abi, + getAddress, + http, + parseAbi, + parseAbiItem, + zeroAddress, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +const optimisticGovernorAbi = parseAbi([ + 'function proposeTransactions((address to,uint256 value,bytes data,uint8 operation)[] transactions) returns (bytes32 proposalHash)', + 'function collateral() view returns (address)', + 'function bondAmount() view returns (uint256)', + 'function optimisticOracleV3() view returns (address)', +]); + +const transferEvent = parseAbiItem( + 'event Transfer(address indexed from, address indexed to, uint256 value)' +); + +function mustGetEnv(key) { + const value = process.env[key]; + if (!value) { + throw new Error(`Missing required env var ${key}`); + } + + return value; +} + +function parseAddressList(list) { + if (!list) return []; + return list + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + .map(getAddress); +} + +const config = { + rpcUrl: mustGetEnv('RPC_URL'), + privateKey: mustGetEnv('PRIVATE_KEY'), + commitmentSafe: getAddress(mustGetEnv('COMMITMENT_SAFE')), + ogModule: getAddress(mustGetEnv('OG_MODULE')), + pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 60_000), + startBlock: process.env.START_BLOCK ? BigInt(process.env.START_BLOCK) : undefined, + watchAssets: parseAddressList(process.env.WATCH_ASSETS), + watchNativeBalance: + process.env.WATCH_NATIVE_BALANCE === undefined + ? true + : process.env.WATCH_NATIVE_BALANCE.toLowerCase() !== 'false', + defaultDepositAsset: process.env.DEFAULT_DEPOSIT_ASSET + ? getAddress(process.env.DEFAULT_DEPOSIT_ASSET) + : undefined, + defaultDepositAmountWei: process.env.DEFAULT_DEPOSIT_AMOUNT_WEI + ? BigInt(process.env.DEFAULT_DEPOSIT_AMOUNT_WEI) + : undefined, +}; + +const account = privateKeyToAccount(config.privateKey); +const publicClient = createPublicClient({ transport: http(config.rpcUrl) }); +const walletClient = createWalletClient({ account, transport: http(config.rpcUrl) }); + +const trackedAssets = new Set(config.watchAssets); +let lastCheckedBlock = config.startBlock; +let lastNativeBalance; + +async function loadOptimisticGovernorDefaults() { + const collateral = await publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'collateral', + }); + + trackedAssets.add(getAddress(collateral)); +} + +async function primeBalances(blockNumber) { + if (!config.watchNativeBalance) return; + + lastNativeBalance = await publicClient.getBalance({ + address: config.commitmentSafe, + blockNumber, + }); +} + +async function postBondAndPropose(transactions) { + const [collateral, bondAmount, optimisticOracle] = await Promise.all([ + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'collateral', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'bondAmount', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'optimisticOracleV3', + }), + ]); + + if (bondAmount > 0n) { + await walletClient.writeContract({ + address: collateral, + abi: erc20Abi, + functionName: 'approve', + args: [optimisticOracle, bondAmount], + }); + } + + const proposalHash = await walletClient.writeContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'proposeTransactions', + args: [transactions], + }); + + return { proposalHash, bondAmount, collateral, optimisticOracle }; +} + +async function makeDeposit({ asset, amountWei }) { + const depositAsset = asset ? getAddress(asset) : config.defaultDepositAsset; + const depositAmount = + amountWei !== undefined ? amountWei : config.defaultDepositAmountWei; + + if (!depositAsset || depositAmount === undefined) { + throw new Error('Deposit requires asset and amount (wei).'); + } + + if (depositAsset === zeroAddress) { + return walletClient.sendTransaction({ + account, + to: config.commitmentSafe, + value: BigInt(depositAmount), + }); + } + + return walletClient.writeContract({ + address: depositAsset, + abi: erc20Abi, + functionName: 'transfer', + args: [config.commitmentSafe, BigInt(depositAmount)], + }); +} + +async function pollCommitmentChanges() { + const latestBlock = await publicClient.getBlockNumber(); + if (lastCheckedBlock === undefined) { + lastCheckedBlock = latestBlock; + await primeBalances(latestBlock); + return []; + } + + if (latestBlock <= lastCheckedBlock) { + return []; + } + + const fromBlock = lastCheckedBlock + 1n; + const toBlock = latestBlock; + const deposits = []; + + for (const asset of trackedAssets) { + const logs = await publicClient.getLogs({ + address: asset, + event: transferEvent, + args: { to: config.commitmentSafe }, + fromBlock, + toBlock, + }); + + for (const log of logs) { + deposits.push({ + kind: 'erc20Deposit', + asset, + from: log.args.from, + amount: log.args.value, + blockNumber: log.blockNumber, + transactionHash: log.transactionHash, + }); + } + } + + if (config.watchNativeBalance) { + const nativeBalance = await publicClient.getBalance({ + address: config.commitmentSafe, + blockNumber: toBlock, + }); + + if (lastNativeBalance !== undefined && nativeBalance > lastNativeBalance) { + deposits.push({ + kind: 'nativeDeposit', + asset: zeroAddress, + from: 'unknown', + amount: nativeBalance - lastNativeBalance, + blockNumber: toBlock, + transactionHash: undefined, + }); + } + + lastNativeBalance = nativeBalance; + } + + lastCheckedBlock = toBlock; + return deposits; +} + +async function decideOnSignals(signals) { + // Hook for LLM/decision making; left generic on purpose. + console.log(`[agent] ${signals.length} change(s) detected; plug in decision logic here.`); + for (const signal of signals) { + console.log(signal); + } +} + +async function agentLoop() { + try { + const signals = await pollCommitmentChanges(); + + if (signals.length > 0) { + await decideOnSignals(signals); + } + } catch (error) { + console.error('[agent] loop error', error); + } + + setTimeout(agentLoop, config.pollIntervalMs); +} + +async function startAgent() { + console.log('[agent] initializing...'); + await loadOptimisticGovernorDefaults(); + + if (lastCheckedBlock === undefined) { + lastCheckedBlock = await publicClient.getBlockNumber(); + } + + await primeBalances(lastCheckedBlock); + + console.log('[agent] watching assets:', [...trackedAssets].join(', ')); + console.log('[agent] starting loop with interval', config.pollIntervalMs, 'ms'); + + agentLoop(); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + startAgent().catch((error) => { + console.error('[agent] failed to start', error); + process.exit(1); + }); +} + +export { makeDeposit, postBondAndPropose, startAgent }; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 89ce49ca..5d983edb 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { ConnectButton } from '@rainbow-me/rainbowkit'; import { encodeAbiParameters, @@ -208,6 +208,19 @@ function App() { safe: '', ogModule: '', }); + const [agentForm, setAgentForm] = useState({ + rpcUrl: '', + privateKey: '', + commitmentSafe: '', + ogModule: '', + watchAssets: '', + watchNativeBalance: true, + pollIntervalMs: '60000', + startBlock: '', + defaultDepositAsset: '', + defaultDepositAmountWei: '', + }); + const [agentCopyStatus, setAgentCopyStatus] = useState(''); const [txHashes, setTxHashes] = useState({ moduleProxyFactory: '', safeProxy: '', @@ -227,6 +240,14 @@ function App() { setForm((prev) => ({ ...prev, [name]: value })); }; + const onAgentChange = (event) => { + const { name, type, checked, value } = event.target; + setAgentForm((prev) => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value, + })); + }; + const validatedAddresses = useMemo(() => { const required = { collateral: form.collateral, @@ -559,6 +580,68 @@ function App() { } }; + useEffect(() => { + setAgentForm((prev) => ({ + ...prev, + commitmentSafe: prev.commitmentSafe || deployment.safe, + ogModule: prev.ogModule || deployment.ogModule, + })); + }, [deployment.safe, deployment.ogModule]); + + const agentEnvPreview = useMemo(() => { + const lines = [ + `RPC_URL=${agentForm.rpcUrl || ''}`, + `PRIVATE_KEY=${agentForm.privateKey || ''}`, + `COMMITMENT_SAFE=${agentForm.commitmentSafe || deployment.safe || ''}`, + `OG_MODULE=${agentForm.ogModule || deployment.ogModule || ''}`, + `WATCH_ASSETS=${agentForm.watchAssets || ''}`, + `POLL_INTERVAL_MS=${agentForm.pollIntervalMs || '60000'}`, + `WATCH_NATIVE_BALANCE=${agentForm.watchNativeBalance ? 'true' : 'false'}`, + ]; + + if (agentForm.startBlock) { + lines.push(`START_BLOCK=${agentForm.startBlock}`); + } + + if (agentForm.defaultDepositAsset) { + lines.push(`DEFAULT_DEPOSIT_ASSET=${agentForm.defaultDepositAsset}`); + } + + if (agentForm.defaultDepositAmountWei) { + lines.push(`DEFAULT_DEPOSIT_AMOUNT_WEI=${agentForm.defaultDepositAmountWei}`); + } + + return lines.join('\n'); + }, [ + agentForm.commitmentSafe, + agentForm.defaultDepositAmountWei, + agentForm.defaultDepositAsset, + agentForm.ogModule, + agentForm.pollIntervalMs, + agentForm.privateKey, + agentForm.rpcUrl, + agentForm.startBlock, + agentForm.watchAssets, + agentForm.watchNativeBalance, + deployment.ogModule, + deployment.safe, + ]); + + const copyAgentEnv = async () => { + try { + await navigator.clipboard.writeText(agentEnvPreview); + setAgentCopyStatus('Copied env to clipboard.'); + setTimeout(() => setAgentCopyStatus(''), 2000); + } catch (err) { + setAgentCopyStatus('Copy failed.'); + setTimeout(() => setAgentCopyStatus(''), 2000); + } + }; + + const agentEnvDownloadUrl = useMemo(() => { + return `data:text/plain;charset=utf-8,${encodeURIComponent(agentEnvPreview)}`; + }, [agentEnvPreview]); + return (
@@ -682,6 +765,110 @@ function App() {
+ +
+

Agent Config (offchain)

+

+ Prepare the `.env` for the agent scaffold in agent/. Secrets stay in your browser; copy or download + the file and run the agent locally. +

+
+ + + + + + + + + + +
+ +
+
+
{agentEnvPreview}
+
+
+ + + Download .env + +
+

Run: cd agent && npm install && npm start

+ {agentCopyStatus &&

{agentCopyStatus}

} +
+
+
+
); } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index dc90ef1d..da116350 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -140,6 +140,43 @@ button:disabled { font-weight: 600; } +.agent-actions { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 12px; +} + +.agent-env { + background: #0f172a; + color: #e2e8f0; + border-radius: 12px; + padding: 12px; + overflow: auto; +} + +.agent-env pre { + margin: 0; + font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.85rem; + white-space: pre-wrap; +} + +.button-link { + text-decoration: none; + border-radius: 10px; + background: #0ea5e9; + color: white; + padding: 12px 20px; + font-weight: 600; +} + +.checkbox-row { + flex-direction: row; + align-items: center; + gap: 10px; +} + @media (max-width: 720px) { .header { flex-direction: column; From 6685c8bf5a336da64f3471bc19edab137f1e08aa Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sat, 31 Jan 2026 14:23:18 -0800 Subject: [PATCH 006/174] forge fmt Signed-off-by: John Shutt --- agent/src/index.js | 1 + script/DeploySafeWithOptimisticGovernor.s.sol | 31 +++---------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 6f37652d..a0aa795b 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -221,6 +221,7 @@ async function decideOnSignals(signals) { async function agentLoop() { try { + console.log(`[agent] loop tick @ ${new Date().toISOString()}`); const signals = await pollCommitmentChanges(); if (signals.length > 0) { diff --git a/script/DeploySafeWithOptimisticGovernor.s.sol b/script/DeploySafeWithOptimisticGovernor.s.sol index 51db2442..4af587c9 100644 --- a/script/DeploySafeWithOptimisticGovernor.s.sol +++ b/script/DeploySafeWithOptimisticGovernor.s.sol @@ -263,21 +263,10 @@ contract DeploySafeWithOptimisticGovernor is Script { ISafe safe = ISafe(safeProxy); // Add burn owner - bytes memory addOwnerCalldata = abi.encodeWithSignature( - "addOwnerWithThreshold(address,uint256)", BURN_OWNER, 1 - ); + bytes memory addOwnerCalldata = abi.encodeWithSignature("addOwnerWithThreshold(address,uint256)", BURN_OWNER, 1); bytes32 addOwnerTxHash = safe.getTransactionHash( - safeProxy, - 0, - addOwnerCalldata, - OP_CALL, - 0, - 0, - 0, - address(0), - address(0), - safe.nonce() + safeProxy, 0, addOwnerCalldata, OP_CALL, 0, 0, 0, address(0), address(0), safe.nonce() ); (uint8 addV, bytes32 addR, bytes32 addS) = vm.sign(deployerPk, addOwnerTxHash); @@ -289,21 +278,11 @@ contract DeploySafeWithOptimisticGovernor is Script { require(addOk, "add burn owner failed"); // Remove deployer owner - bytes memory removeOwnerCalldata = abi.encodeWithSignature( - "removeOwner(address,address,uint256)", BURN_OWNER, vm.addr(deployerPk), 1 - ); + bytes memory removeOwnerCalldata = + abi.encodeWithSignature("removeOwner(address,address,uint256)", BURN_OWNER, vm.addr(deployerPk), 1); bytes32 removeOwnerTxHash = safe.getTransactionHash( - safeProxy, - 0, - removeOwnerCalldata, - OP_CALL, - 0, - 0, - 0, - address(0), - address(0), - safe.nonce() + safeProxy, 0, removeOwnerCalldata, OP_CALL, 0, 0, 0, address(0), address(0), safe.nonce() ); (uint8 remV, bytes32 remR, bytes32 remS) = vm.sign(deployerPk, removeOwnerTxHash); From 411dea3ab84aa4ca9ac85c03556ab3abdad58c5a Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sat, 31 Jan 2026 19:03:39 -0800 Subject: [PATCH 007/174] Fix OG deploy test for burn owner flow --- test/DeploySafeWithOptimisticGovernor.t.sol | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/DeploySafeWithOptimisticGovernor.t.sol b/test/DeploySafeWithOptimisticGovernor.t.sol index cde16756..5a23217a 100644 --- a/test/DeploySafeWithOptimisticGovernor.t.sol +++ b/test/DeploySafeWithOptimisticGovernor.t.sol @@ -32,6 +32,23 @@ contract SafeMock { lastEnabledModule = module; } + function addOwnerWithThreshold(address owner, uint256 _threshold) external { + owners.push(owner); + threshold = _threshold; + } + + function removeOwner(address, address owner, uint256 _threshold) external { + // remove matching owner (simple linear scan for mock) + for (uint256 i = 0; i < owners.length; i++) { + if (owners[i] == owner) { + owners[i] = owners[owners.length - 1]; + owners.pop(); + break; + } + } + threshold = _threshold; + } + function getTransactionHash( address, uint256, @@ -100,6 +117,8 @@ contract OptimisticGovernorMock { } contract DeploySafeWithOptimisticGovernorTest is Test { + address internal constant BURN_OWNER = 0x000000000000000000000000000000000000dEaD; + SafeProxyFactoryMock private safeProxyFactory; OptimisticGovernorMock private ogMasterCopy; ModuleProxyFactory private moduleProxyFactory; @@ -135,8 +154,8 @@ contract DeploySafeWithOptimisticGovernorTest is Test { assertTrue(safeProxy != address(0)); SafeMock safe = SafeMock(safeProxy); - assertEq(safe.owners(0), vm.addr(1)); - assertEq(safe.threshold(), 1); + assertEq(safe.owners(0), BURN_OWNER); + assertEq(safe.threshold(), 1); // burn address set as sole owner assertEq(safe.fallbackHandler(), address(0xFA11)); address module = safe.lastEnabledModule(); From 41e01ea322164e8220b59c19b26c387f4443e870 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sat, 31 Jan 2026 19:39:57 -0800 Subject: [PATCH 008/174] add reasoning model recommendation to agent Signed-off-by: John Shutt --- agent/.env.example | 5 ++ agent/README.md | 4 +- agent/src/index.js | 159 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 163 insertions(+), 5 deletions(-) diff --git a/agent/.env.example b/agent/.env.example index 97933364..f875d9d0 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -20,3 +20,8 @@ WATCH_NATIVE_BALANCE=true # Optional convenience defaults for deposits # DEFAULT_DEPOSIT_ASSET=0x0000000000000000000000000000000000000000 # DEFAULT_DEPOSIT_AMOUNT_WEI=0 + +# Optional: OpenAI Responses API integration +# OPENAI_API_KEY=sk-... +# OPENAI_MODEL=gpt-4.1-mini +# OPENAI_BASE_URL=https://api.openai.com/v1 diff --git a/agent/README.md b/agent/README.md index ba4e063a..8a1be325 100644 --- a/agent/README.md +++ b/agent/README.md @@ -17,6 +17,7 @@ Generic offchain agent wiring for monitoring a commitment and acting through the - `OG_MODULE`: Optimistic Governor module address - `WATCH_ASSETS`: Comma-separated ERC20s to monitor (the OG collateral is auto-added) - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*` + - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` 2. Install deps and start the loop: ```bash @@ -29,5 +30,6 @@ npm start - **Polls for deposits**: Checks ERC20 `Transfer` logs into the commitment and (optionally) native balance increases. If nothing changed, no LLM/decision code runs. - **Bonds + proposes**: `postBondAndPropose` approves the OG collateral bond and calls `proposeTransactions` on the module. - **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. +- **Optional LLM decisions**: If `OPENAI_API_KEY` is set, `decideOnSignals` will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions. -All other behavior is intentionally left out. Implement your own `decideOnSignals` in `src/index.js` to call an LLM or custom tools when new signals appear. +All other behavior is intentionally left out. Implement your own `decideOnSignals` in `src/index.js` to add commitment-specific logic and tool use. diff --git a/agent/src/index.js b/agent/src/index.js index a0aa795b..20b0184a 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -16,6 +16,9 @@ const optimisticGovernorAbi = parseAbi([ 'function collateral() view returns (address)', 'function bondAmount() view returns (uint256)', 'function optimisticOracleV3() view returns (address)', + 'function rules() view returns (string)', + 'function identifier() view returns (bytes32)', + 'function liveness() view returns (uint64)', ]); const transferEvent = parseAbiItem( @@ -58,6 +61,9 @@ const config = { defaultDepositAmountWei: process.env.DEFAULT_DEPOSIT_AMOUNT_WEI ? BigInt(process.env.DEFAULT_DEPOSIT_AMOUNT_WEI) : undefined, + openAiApiKey: process.env.OPENAI_API_KEY, + openAiModel: process.env.OPENAI_MODEL ?? 'gpt-4.1-mini', + openAiBaseUrl: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1', }; const account = privateKeyToAccount(config.privateKey); @@ -67,6 +73,7 @@ const walletClient = createWalletClient({ account, transport: http(config.rpcUrl const trackedAssets = new Set(config.watchAssets); let lastCheckedBlock = config.startBlock; let lastNativeBalance; +let ogContext; async function loadOptimisticGovernorDefaults() { const collateral = await publicClient.readContract({ @@ -78,6 +85,50 @@ async function loadOptimisticGovernorDefaults() { trackedAssets.add(getAddress(collateral)); } +async function loadOgContext() { + const [collateral, bondAmount, optimisticOracle, rules, identifier, liveness] = await Promise.all([ + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'collateral', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'bondAmount', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'optimisticOracleV3', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'rules', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'identifier', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'liveness', + }), + ]); + + ogContext = { + collateral, + bondAmount, + optimisticOracle, + rules, + identifier, + liveness, + }; +} + async function primeBalances(blockNumber) { if (!config.watchNativeBalance) return; @@ -212,10 +263,29 @@ async function pollCommitmentChanges() { } async function decideOnSignals(signals) { - // Hook for LLM/decision making; left generic on purpose. - console.log(`[agent] ${signals.length} change(s) detected; plug in decision logic here.`); - for (const signal of signals) { - console.log(signal); + console.log(`[agent] ${signals.length} change(s) detected.`); + + if (!config.openAiApiKey) { + console.log('[agent] OPENAI_API_KEY not set; logging signals only.'); + for (const signal of signals) { + console.log(signal); + } + return; + } + + if (!ogContext) { + await loadOgContext(); + } + + try { + const decision = await callAgent(signals, ogContext); + console.log('[agent] Agent decision:', decision); + // Map decision to actions here (e.g., postBondAndPropose/makeDeposit) after validation. + } catch (error) { + console.error('[agent] Agent call failed; logging signals', error); + for (const signal of signals) { + console.log(signal); + } } } @@ -237,6 +307,7 @@ async function agentLoop() { async function startAgent() { console.log('[agent] initializing...'); await loadOptimisticGovernorDefaults(); + await loadOgContext(); if (lastCheckedBlock === undefined) { lastCheckedBlock = await publicClient.getBlockNumber(); @@ -250,6 +321,86 @@ async function startAgent() { agentLoop(); } +async function callAgent(signals, context) { + const systemPrompt = + 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Given signals and rules, recommend a course of action. Prefer no-op when unsure. Output strict JSON with keys: action (propose|deposit|ignore|other), rationale (string), transactions (optional array of {to,value,data,operation}), deposit (optional {asset,amountWei}).'; + + const safeSignals = signals.map((signal) => ({ + ...signal, + amount: signal.amount !== undefined ? signal.amount.toString() : undefined, + blockNumber: signal.blockNumber !== undefined ? signal.blockNumber.toString() : undefined, + transactionHash: signal.transactionHash ? String(signal.transactionHash) : undefined, + })); + + const safeContext = { + rules: context?.rules, + identifier: context?.identifier ? String(context.identifier) : undefined, + liveness: context?.liveness !== undefined ? context.liveness.toString() : undefined, + collateral: context?.collateral, + bondAmount: context?.bondAmount !== undefined ? context.bondAmount.toString() : undefined, + optimisticOracle: context?.optimisticOracle, + }; + + const payload = { + model: config.openAiModel, + input: [ + { + role: 'system', + content: systemPrompt, + }, + { + role: 'user', + content: JSON.stringify({ + commitmentSafe: config.commitmentSafe, + ogModule: config.ogModule, + ogContext: safeContext, + signals: safeSignals, + }), + }, + ], + text: { format: { type: 'json_object' } }, + }; + + const res = await fetch(`${config.openAiBaseUrl}/responses`, { + method: 'POST', + headers: { + Authorization: `Bearer ${config.openAiApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`OpenAI API error: ${res.status} ${text}`); + } + + const json = await res.json(); + const raw = extractFirstText(json); + try { + return JSON.parse(raw); + } catch (error) { + throw new Error(`Failed to parse OpenAI JSON: ${raw}`); + } +} + +function extractFirstText(responseJson) { + // Responses API structure: output -> [{ content: [{ type: 'output_text', text: '...' }, ...] }, ...] + const outputs = responseJson?.output; + if (!Array.isArray(outputs)) return ''; + + for (const item of outputs) { + if (!item?.content) continue; + for (const chunk of item.content) { + if (chunk?.text) return chunk.text; + if (chunk?.output_text) return chunk.output_text?.text ?? ''; + if (chunk?.text?.value) return chunk.text.value; // older shape + } + } + + return ''; +} + if (import.meta.url === `file://${process.argv[1]}`) { startAgent().catch((error) => { console.error('[agent] failed to start', error); From 77c79a2f5ea71bf1eb1a461c316a86ee24119da3 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sat, 31 Jan 2026 19:54:49 -0800 Subject: [PATCH 009/174] add tool calling Signed-off-by: John Shutt --- agent/src/index.js | 222 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 215 insertions(+), 7 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 20b0184a..3de4de9d 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -279,8 +279,21 @@ async function decideOnSignals(signals) { try { const decision = await callAgent(signals, ogContext); - console.log('[agent] Agent decision:', decision); - // Map decision to actions here (e.g., postBondAndPropose/makeDeposit) after validation. + if (decision.toolCalls.length > 0) { + console.log('[agent] Agent tool calls:', decision.toolCalls); + const toolOutputs = await executeToolCalls(decision.toolCalls); + if (decision.responseId && toolOutputs.length > 0) { + const explanation = await explainToolCalls( + decision.responseId, + toolOutputs + ); + if (explanation) { + console.log('[agent] Agent explanation:', explanation); + } + } + return; + } + console.log('[agent] Agent decision:', decision.textDecision); } catch (error) { console.error('[agent] Agent call failed; logging signals', error); for (const signal of signals) { @@ -323,7 +336,7 @@ async function startAgent() { async function callAgent(signals, context) { const systemPrompt = - 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Given signals and rules, recommend a course of action. Prefer no-op when unsure. Output strict JSON with keys: action (propose|deposit|ignore|other), rationale (string), transactions (optional array of {to,value,data,operation}), deposit (optional {asset,amountWei}).'; + 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Given signals and rules, recommend a course of action. Prefer no-op when unsure. If an onchain action is needed, call a tool. If no action is needed, output strict JSON with keys: action (propose|deposit|ignore|other) and rationale (string).'; const safeSignals = signals.map((signal) => ({ ...signal, @@ -358,6 +371,9 @@ async function callAgent(signals, context) { }), }, ], + tools: toolDefinitions(), + tool_choice: 'auto', + parallel_tool_calls: false, text: { format: { type: 'json_object' } }, }; @@ -376,12 +392,18 @@ async function callAgent(signals, context) { } const json = await res.json(); + const toolCalls = extractToolCalls(json); const raw = extractFirstText(json); - try { - return JSON.parse(raw); - } catch (error) { - throw new Error(`Failed to parse OpenAI JSON: ${raw}`); + let textDecision; + if (raw) { + try { + textDecision = JSON.parse(raw); + } catch (error) { + throw new Error(`Failed to parse OpenAI JSON: ${raw}`); + } } + + return { toolCalls, textDecision, responseId: json?.id }; } function extractFirstText(responseJson) { @@ -401,6 +423,192 @@ function extractFirstText(responseJson) { return ''; } +function extractToolCalls(responseJson) { + const outputs = responseJson?.output; + if (!Array.isArray(outputs)) return []; + + const toolCalls = []; + for (const item of outputs) { + if (item?.type === 'tool_call' || item?.type === 'function_call') { + toolCalls.push({ + name: item?.name ?? item?.function?.name, + arguments: item?.arguments ?? item?.function?.arguments, + callId: item?.call_id ?? item?.id, + }); + continue; + } + + if (Array.isArray(item?.tool_calls)) { + for (const call of item.tool_calls) { + toolCalls.push({ + name: call?.name ?? call?.function?.name, + arguments: call?.arguments ?? call?.function?.arguments, + callId: call?.call_id ?? call?.id, + }); + } + } + } + + return toolCalls.filter((call) => call.name); +} + +function toolDefinitions() { + return [ + { + type: 'function', + name: 'make_deposit', + description: + 'Deposit funds into the commitment Safe. Use asset=0x000...000 for native ETH. amountWei must be a string of the integer wei amount.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + asset: { + type: 'string', + description: + 'Asset address (ERC20) or 0x0000000000000000000000000000000000000000 for native.', + }, + amountWei: { + type: 'string', + description: 'Amount in wei as a string.', + }, + }, + required: ['amountWei'], + }, + }, + { + type: 'function', + name: 'post_bond_and_propose', + description: + 'Post bond (if required) and propose transactions to the Optimistic Governor.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + transactions: { + type: 'array', + description: + 'Safe transaction batch to propose. Use value as string wei.', + items: { + type: 'object', + additionalProperties: false, + properties: { + to: { type: 'string' }, + value: { type: 'string' }, + data: { type: 'string' }, + operation: { type: 'integer' }, + }, + required: ['to', 'value', 'data', 'operation'], + }, + }, + }, + required: ['transactions'], + }, + }, + ]; +} + +async function executeToolCalls(toolCalls) { + const outputs = []; + for (const call of toolCalls) { + const args = parseToolArguments(call.arguments); + if (!args) { + console.warn('[agent] Skipping tool call with invalid args:', call); + continue; + } + + if (call.name === 'make_deposit') { + const txHash = await makeDeposit({ + asset: args.asset, + amountWei: BigInt(args.amountWei), + }); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'submitted', + transactionHash: String(txHash), + }), + }); + continue; + } + + if (call.name === 'post_bond_and_propose') { + const transactions = args.transactions.map((tx) => ({ + to: getAddress(tx.to), + value: BigInt(tx.value), + data: tx.data, + operation: Number(tx.operation), + })); + const result = await postBondAndPropose(transactions); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'submitted', + ...result, + }), + }); + continue; + } + + console.warn('[agent] Unknown tool call:', call.name); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ status: 'skipped', reason: 'unknown tool' }), + }); + } + return outputs.filter((item) => item.callId); +} + +function parseToolArguments(raw) { + if (!raw) return null; + if (typeof raw === 'object') return raw; + if (typeof raw === 'string') { + try { + return JSON.parse(raw); + } catch (error) { + return null; + } + } + return null; +} + +async function explainToolCalls(previousResponseId, toolOutputs) { + const input = [ + ...toolOutputs.map((item) => ({ + type: 'function_call_output', + call_id: item.callId, + output: item.output, + })), + { + type: 'input_text', + text: 'Summarize the actions you took and why.', + }, + ]; + + const res = await fetch(`${config.openAiBaseUrl}/responses`, { + method: 'POST', + headers: { + Authorization: `Bearer ${config.openAiApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: config.openAiModel, + previous_response_id: previousResponseId, + input, + }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`OpenAI API error: ${res.status} ${text}`); + } + + const json = await res.json(); + return extractFirstText(json); +} + if (import.meta.url === `file://${process.argv[1]}`) { startAgent().catch((error) => { console.error('[agent] failed to start', error); From da85e4132f30938fbb1cc431bc492fdf5eb8b280 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sat, 31 Jan 2026 20:00:24 -0800 Subject: [PATCH 010/174] fix deposit tool build Signed-off-by: John Shutt --- agent/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/index.js b/agent/src/index.js index 3de4de9d..960963b4 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -474,7 +474,7 @@ function toolDefinitions() { description: 'Amount in wei as a string.', }, }, - required: ['amountWei'], + required: ['asset', 'amountWei'], }, }, { From bcc0964d4b67d670792cc8df2768ef2352f3f846 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sat, 31 Jan 2026 20:41:23 -0800 Subject: [PATCH 011/174] add helper tool for constructing og proposals Signed-off-by: John Shutt --- agent/src/index.js | 152 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) diff --git a/agent/src/index.js b/agent/src/index.js index 960963b4..68f78dbb 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -2,6 +2,7 @@ import 'dotenv/config'; import { createPublicClient, createWalletClient, + encodeFunctionData, erc20Abi, getAddress, http, @@ -336,7 +337,7 @@ async function startAgent() { async function callAgent(signals, context) { const systemPrompt = - 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Given signals and rules, recommend a course of action. Prefer no-op when unsure. If an onchain action is needed, call a tool. If no action is needed, output strict JSON with keys: action (propose|deposit|ignore|other) and rationale (string).'; + 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Given signals and rules, recommend a course of action. Prefer no-op when unsure. If an onchain action is needed, call a tool. Use build_og_transactions to construct proposal payloads, then post_bond_and_propose. If no action is needed, output strict JSON with keys: action (propose|deposit|ignore|other) and rationale (string).'; const safeSignals = signals.map((signal) => ({ ...signal, @@ -454,6 +455,70 @@ function extractToolCalls(responseJson) { function toolDefinitions() { return [ + { + type: 'function', + name: 'build_og_transactions', + description: + 'Build Optimistic Governor transaction payloads from high-level intents. Returns array of {to,value,data,operation} with value as string wei.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + actions: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + kind: { + type: 'string', + description: + 'Action type: erc20_transfer | native_transfer | contract_call', + }, + token: { + type: 'string', + description: + 'ERC20 token address for erc20_transfer.', + }, + to: { + type: 'string', + description: 'Recipient or target contract address.', + }, + amountWei: { + type: 'string', + description: + 'Amount in wei as a string. For erc20_transfer and native_transfer.', + }, + valueWei: { + type: 'string', + description: + 'ETH value to send in contract_call (default 0).', + }, + abi: { + type: 'string', + description: + 'Function signature for contract_call, e.g. "setOwner(address)".', + }, + args: { + type: 'array', + description: + 'Arguments for contract_call in order, JSON-serializable.', + items: {}, + }, + operation: { + type: 'integer', + description: + 'Safe operation (0=CALL,1=DELEGATECALL). Defaults to 0.', + }, + }, + required: ['kind'], + }, + }, + }, + required: ['actions'], + }, + }, { type: 'function', name: 'make_deposit', @@ -519,6 +584,25 @@ async function executeToolCalls(toolCalls) { continue; } + if (call.name === 'build_og_transactions') { + try { + const transactions = buildOgTransactions(args.actions ?? []); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ status: 'ok', transactions }), + }); + } catch (error) { + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'error', + message: error?.message ?? String(error), + }), + }); + } + continue; + } + if (call.name === 'make_deposit') { const txHash = await makeDeposit({ asset: args.asset, @@ -561,6 +645,72 @@ async function executeToolCalls(toolCalls) { return outputs.filter((item) => item.callId); } +function buildOgTransactions(actions) { + if (!Array.isArray(actions) || actions.length === 0) { + throw new Error('actions must be a non-empty array'); + } + + return actions.map((action) => { + const operation = action.operation !== undefined ? Number(action.operation) : 0; + + if (action.kind === 'erc20_transfer') { + if (!action.token || !action.to || action.amountWei === undefined) { + throw new Error('erc20_transfer requires token, to, amountWei'); + } + + const data = encodeFunctionData({ + abi: erc20Abi, + functionName: 'transfer', + args: [getAddress(action.to), BigInt(action.amountWei)], + }); + + return { + to: getAddress(action.token), + value: '0', + data, + operation, + }; + } + + if (action.kind === 'native_transfer') { + if (!action.to || action.amountWei === undefined) { + throw new Error('native_transfer requires to, amountWei'); + } + + return { + to: getAddress(action.to), + value: BigInt(action.amountWei).toString(), + data: '0x', + operation, + }; + } + + if (action.kind === 'contract_call') { + if (!action.to || !action.abi) { + throw new Error('contract_call requires to, abi'); + } + + const abi = parseAbi([`function ${action.abi}`]); + const args = Array.isArray(action.args) ? action.args : []; + const data = encodeFunctionData({ + abi, + functionName: action.abi.split('(')[0], + args, + }); + const value = action.valueWei !== undefined ? BigInt(action.valueWei).toString() : '0'; + + return { + to: getAddress(action.to), + value, + data, + operation, + }; + } + + throw new Error(`Unknown action kind: ${action.kind}`); + }); +} + function parseToolArguments(raw) { if (!raw) return null; if (typeof raw === 'object') return raw; From 38473f089cf7adbc970308ac349996e83cc54e64 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sun, 1 Feb 2026 09:50:29 -0800 Subject: [PATCH 012/174] agent updates to logging and polling Signed-off-by: John Shutt --- agent/src/index.js | 293 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 260 insertions(+), 33 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 68f78dbb..43e00cca 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -12,7 +12,7 @@ import { } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -const optimisticGovernorAbi = parseAbi([ +const optimisticGovernorAbiV1 = parseAbi([ 'function proposeTransactions((address to,uint256 value,bytes data,uint8 operation)[] transactions) returns (bytes32 proposalHash)', 'function collateral() view returns (address)', 'function bondAmount() view returns (uint256)', @@ -22,6 +22,20 @@ const optimisticGovernorAbi = parseAbi([ 'function liveness() view returns (uint64)', ]); +const optimisticGovernorAbiV2 = parseAbi([ + 'function proposeTransactions((address to,uint256 value,bytes data,uint8 operation)[] transactions, bytes explanation) returns (bytes32 proposalHash)', + 'function collateral() view returns (address)', + 'function bondAmount() view returns (uint256)', + 'function optimisticOracleV3() view returns (address)', + 'function rules() view returns (string)', + 'function identifier() view returns (bytes32)', + 'function liveness() view returns (uint64)', +]); + +const optimisticOracleAbi = parseAbi([ + 'function getMinimumBond(address collateral) view returns (uint256)', +]); + const transferEvent = parseAbiItem( 'event Transfer(address indexed from, address indexed to, uint256 value)' ); @@ -49,7 +63,7 @@ const config = { privateKey: mustGetEnv('PRIVATE_KEY'), commitmentSafe: getAddress(mustGetEnv('COMMITMENT_SAFE')), ogModule: getAddress(mustGetEnv('OG_MODULE')), - pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 60_000), + pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 10_000), startBlock: process.env.START_BLOCK ? BigInt(process.env.START_BLOCK) : undefined, watchAssets: parseAddressList(process.env.WATCH_ASSETS), watchNativeBalance: @@ -62,12 +76,14 @@ const config = { defaultDepositAmountWei: process.env.DEFAULT_DEPOSIT_AMOUNT_WEI ? BigInt(process.env.DEFAULT_DEPOSIT_AMOUNT_WEI) : undefined, + bondSpender: (process.env.BOND_SPENDER ?? 'og').toLowerCase(), openAiApiKey: process.env.OPENAI_API_KEY, openAiModel: process.env.OPENAI_MODEL ?? 'gpt-4.1-mini', openAiBaseUrl: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1', }; const account = privateKeyToAccount(config.privateKey); +const agentAddress = account.address; const publicClient = createPublicClient({ transport: http(config.rpcUrl) }); const walletClient = createWalletClient({ account, transport: http(config.rpcUrl) }); @@ -79,7 +95,7 @@ let ogContext; async function loadOptimisticGovernorDefaults() { const collateral = await publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'collateral', }); @@ -90,32 +106,32 @@ async function loadOgContext() { const [collateral, bondAmount, optimisticOracle, rules, identifier, liveness] = await Promise.all([ publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'collateral', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'bondAmount', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'optimisticOracleV3', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'rules', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'identifier', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'liveness', }), ]); @@ -130,6 +146,56 @@ async function loadOgContext() { }; } +async function logOgFundingStatus() { + try { + const [collateral, bondAmount, optimisticOracle] = await Promise.all([ + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbiV1, + functionName: 'collateral', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbiV1, + functionName: 'bondAmount', + }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbiV1, + functionName: 'optimisticOracleV3', + }), + ]); + const minimumBond = await publicClient.readContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'getMinimumBond', + args: [collateral], + }); + + const requiredBond = bondAmount > minimumBond ? bondAmount : minimumBond; + const collateralBalance = await publicClient.readContract({ + address: collateral, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }); + const nativeBalance = await publicClient.getBalance({ address: account.address }); + + console.log('[agent] OG funding status:', { + proposer: account.address, + collateral, + bondAmount: bondAmount.toString(), + minimumBond: minimumBond.toString(), + requiredBond: requiredBond.toString(), + collateralBalance: collateralBalance.toString(), + nativeBalance: nativeBalance.toString(), + optimisticOracle, + }); + } catch (error) { + console.warn('[agent] Failed to log OG funding status:', error); + } +} + async function primeBalances(blockNumber) { if (!config.watchNativeBalance) return; @@ -140,40 +206,177 @@ async function primeBalances(blockNumber) { } async function postBondAndPropose(transactions) { + const proposerBalance = await publicClient.getBalance({ address: account.address }); const [collateral, bondAmount, optimisticOracle] = await Promise.all([ publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'collateral', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'bondAmount', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbi, + abi: optimisticGovernorAbiV1, functionName: 'optimisticOracleV3', }), ]); - if (bondAmount > 0n) { - await walletClient.writeContract({ + let minimumBond = 0n; + try { + minimumBond = await publicClient.readContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'getMinimumBond', + args: [collateral], + }); + } catch (error) { + console.warn('[agent] Failed to fetch minimum bond from optimistic oracle:', error); + } + + const requiredBond = bondAmount > minimumBond ? bondAmount : minimumBond; + + if (requiredBond > 0n) { + const collateralBalance = await publicClient.readContract({ address: collateral, abi: erc20Abi, - functionName: 'approve', - args: [optimisticOracle, bondAmount], + functionName: 'balanceOf', + args: [account.address], }); + if (collateralBalance < requiredBond) { + throw new Error( + `Insufficient bond collateral balance: need ${requiredBond.toString()} wei, have ${collateralBalance.toString()}.` + ); + } + const spenders = []; + if (config.bondSpender === 'og' || config.bondSpender === 'both') { + spenders.push(config.ogModule); + } + if (config.bondSpender === 'oo' || config.bondSpender === 'both') { + spenders.push(optimisticOracle); + } + + for (const spender of spenders) { + const approveHash = await walletClient.writeContract({ + address: collateral, + abi: erc20Abi, + functionName: 'approve', + args: [spender, requiredBond], + }); + await publicClient.waitForTransactionReceipt({ hash: approveHash }); + const allowance = await publicClient.readContract({ + address: collateral, + abi: erc20Abi, + functionName: 'allowance', + args: [account.address, spender], + }); + if (allowance < requiredBond) { + throw new Error( + `Insufficient bond allowance: need ${requiredBond.toString()} wei, have ${allowance.toString()} for spender ${spender}.` + ); + } + } } - const proposalHash = await walletClient.writeContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'proposeTransactions', - args: [transactions], + if (proposerBalance === 0n) { + throw new Error( + `Proposer ${account.address} has 0 native balance; cannot pay gas to propose.` + ); + } + + const [allowanceOg, allowanceOo] = await Promise.all([ + publicClient.readContract({ + address: collateral, + abi: erc20Abi, + functionName: 'allowance', + args: [account.address, config.ogModule], + }), + publicClient.readContract({ + address: collateral, + abi: erc20Abi, + functionName: 'allowance', + args: [account.address, optimisticOracle], + }), + ]); + + console.log('[agent] Propose preflight:', { + proposer: account.address, + ogModule: config.ogModule, + optimisticOracle, + collateral, + bondAmount: bondAmount.toString(), + minimumBond: minimumBond.toString(), + requiredBond: requiredBond.toString(), + collateralBalance: ( + await publicClient.readContract({ + address: collateral, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }) + ).toString(), + allowanceOg: allowanceOg.toString(), + allowanceOo: allowanceOo.toString(), }); + const proposalContext = { + ogModule: config.ogModule, + proposer: account.address, + collateral, + bondAmount: bondAmount.toString(), + minimumBond: minimumBond.toString(), + requiredBond: requiredBond.toString(), + optimisticOracle, + }; + + let proposalHash; + const explanation = 'Agent serving Oya commitment.'; + try { + console.log('[agent] Propose signature: V2 (transactions, explanation)'); + await publicClient.simulateContract({ + address: config.ogModule, + abi: optimisticGovernorAbiV2, + functionName: 'proposeTransactions', + args: [transactions, explanation], + account: account.address, + }); + proposalHash = await walletClient.writeContract({ + address: config.ogModule, + abi: optimisticGovernorAbiV2, + functionName: 'proposeTransactions', + args: [transactions, explanation], + }); + } catch (errorV2) { + try { + console.log('[agent] Propose signature: V1 (transactions)'); + await publicClient.simulateContract({ + address: config.ogModule, + abi: optimisticGovernorAbiV1, + functionName: 'proposeTransactions', + args: [transactions], + account: account.address, + }); + proposalHash = await walletClient.writeContract({ + address: config.ogModule, + abi: optimisticGovernorAbiV1, + functionName: 'proposeTransactions', + args: [transactions], + }); + } catch (errorV1) { + const message = + errorV1?.shortMessage ?? + errorV1?.message ?? + errorV2?.shortMessage ?? + errorV2?.message ?? + String(errorV1 ?? errorV2); + console.warn('[agent] Propose simulation context:', proposalContext); + throw new Error(`Propose simulation failed: ${message}`); + } + } + return { proposalHash, bondAmount, collateral, optimisticOracle }; } @@ -322,6 +525,7 @@ async function startAgent() { console.log('[agent] initializing...'); await loadOptimisticGovernorDefaults(); await loadOgContext(); + await logOgFundingStatus(); if (lastCheckedBlock === undefined) { lastCheckedBlock = await publicClient.getBlockNumber(); @@ -337,7 +541,7 @@ async function startAgent() { async function callAgent(signals, context) { const systemPrompt = - 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Given signals and rules, recommend a course of action. Prefer no-op when unsure. If an onchain action is needed, call a tool. Use build_og_transactions to construct proposal payloads, then post_bond_and_propose. If no action is needed, output strict JSON with keys: action (propose|deposit|ignore|other) and rationale (string).'; + 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Your own address is provided in the input as agentAddress; use it when rules refer to “the agent/themselves”. Given signals and rules, recommend a course of action. Prefer no-op when unsure. If an onchain action is needed, call a tool. Use build_og_transactions to construct proposal payloads, then post_bond_and_propose. If no action is needed, output strict JSON with keys: action (propose|deposit|ignore|other) and rationale (string).'; const safeSignals = signals.map((signal) => ({ ...signal, @@ -367,6 +571,7 @@ async function callAgent(signals, context) { content: JSON.stringify({ commitmentSafe: config.commitmentSafe, ogModule: config.ogModule, + agentAddress, ogContext: safeContext, signals: safeSignals, }), @@ -477,42 +682,51 @@ function toolDefinitions() { 'Action type: erc20_transfer | native_transfer | contract_call', }, token: { - type: 'string', + type: ['string', 'null'], description: 'ERC20 token address for erc20_transfer.', }, to: { - type: 'string', + type: ['string', 'null'], description: 'Recipient or target contract address.', }, amountWei: { - type: 'string', + type: ['string', 'null'], description: 'Amount in wei as a string. For erc20_transfer and native_transfer.', }, valueWei: { - type: 'string', + type: ['string', 'null'], description: 'ETH value to send in contract_call (default 0).', }, abi: { - type: 'string', + type: ['string', 'null'], description: 'Function signature for contract_call, e.g. "setOwner(address)".', }, args: { - type: 'array', + type: ['array', 'null'], description: 'Arguments for contract_call in order, JSON-serializable.', - items: {}, + items: { type: 'string' }, }, operation: { - type: 'integer', + type: ['integer', 'null'], description: 'Safe operation (0=CALL,1=DELEGATECALL). Defaults to 0.', }, }, - required: ['kind'], + required: [ + 'kind', + 'token', + 'to', + 'amountWei', + 'valueWei', + 'abi', + 'args', + 'operation', + ], }, }, }, @@ -577,6 +791,8 @@ function toolDefinitions() { async function executeToolCalls(toolCalls) { const outputs = []; + const hasPostProposal = toolCalls.some((call) => call.name === 'post_bond_and_propose'); + let builtTransactions; for (const call of toolCalls) { const args = parseToolArguments(call.arguments); if (!args) { @@ -587,6 +803,7 @@ async function executeToolCalls(toolCalls) { if (call.name === 'build_og_transactions') { try { const transactions = buildOgTransactions(args.actions ?? []); + builtTransactions = transactions; outputs.push({ callId: call.callId, output: JSON.stringify({ status: 'ok', transactions }), @@ -642,6 +859,10 @@ async function executeToolCalls(toolCalls) { output: JSON.stringify({ status: 'skipped', reason: 'unknown tool' }), }); } + if (builtTransactions && !hasPostProposal) { + const result = await postBondAndPropose(builtTransactions); + console.log('[agent] Auto-proposed via OG:', result); + } return outputs.filter((item) => item.callId); } @@ -732,8 +953,14 @@ async function explainToolCalls(previousResponseId, toolOutputs) { output: item.output, })), { - type: 'input_text', - text: 'Summarize the actions you took and why.', + type: 'message', + role: 'user', + content: [ + { + type: 'input_text', + text: 'Summarize the actions you took and why.', + }, + ], }, ]; From e3ed55f373e6f5da0feb95e6f5760bf95d0f207c Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sun, 1 Feb 2026 10:10:23 -0800 Subject: [PATCH 013/174] use ASSERT_TRUTH on Sepolia and ASSERT_TRUTH2 elsewhere Signed-off-by: John Shutt --- agent/src/index.js | 22 ++++++++++++++++++- script/DeploySafeWithOptimisticGovernor.s.sol | 11 ++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 43e00cca..22af6abc 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -8,6 +8,7 @@ import { http, parseAbi, parseAbiItem, + stringToHex, zeroAddress, } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; @@ -148,7 +149,12 @@ async function loadOgContext() { async function logOgFundingStatus() { try { - const [collateral, bondAmount, optimisticOracle] = await Promise.all([ + const chainId = await publicClient.getChainId(); + const expectedIdentifierStr = + chainId === 11155111 ? 'ASSERT_TRUTH' : 'ASSERT_TRUTH2'; + const expectedIdentifier = stringToHex(expectedIdentifierStr, { size: 32 }); + + const [collateral, bondAmount, optimisticOracle, identifier] = await Promise.all([ publicClient.readContract({ address: config.ogModule, abi: optimisticGovernorAbiV1, @@ -164,6 +170,11 @@ async function logOgFundingStatus() { abi: optimisticGovernorAbiV1, functionName: 'optimisticOracleV3', }), + publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbiV1, + functionName: 'identifier', + }), ]); const minimumBond = await publicClient.readContract({ address: optimisticOracle, @@ -190,7 +201,16 @@ async function logOgFundingStatus() { collateralBalance: collateralBalance.toString(), nativeBalance: nativeBalance.toString(), optimisticOracle, + chainId, + identifier, + expectedIdentifier, }); + + if (identifier !== expectedIdentifier) { + console.warn( + `[agent] OG identifier mismatch: expected ${expectedIdentifierStr}, onchain ${identifier}` + ); + } } catch (error) { console.warn('[agent] Failed to log OG funding status:', error); } diff --git a/script/DeploySafeWithOptimisticGovernor.s.sol b/script/DeploySafeWithOptimisticGovernor.s.sol index 4af587c9..d30dfc81 100644 --- a/script/DeploySafeWithOptimisticGovernor.s.sol +++ b/script/DeploySafeWithOptimisticGovernor.s.sol @@ -173,8 +173,8 @@ contract DeploySafeWithOptimisticGovernor is Script { config.bondAmount = vm.envUint("OG_BOND_AMOUNT"); config.liveness = uint64(vm.envOr("OG_LIVENESS", uint256(2 days))); // seconds - // Identifier (bytes32). Default "ASSERT_TRUTH2" for UMA Optimistic Oracle. :contentReference[oaicite:14]{index=14} - string memory identifierStr = vm.envOr("OG_IDENTIFIER_STR", string("ASSERT_TRUTH2")); + // Identifier (bytes32). Default "ASSERT_TRUTH" on Sepolia, "ASSERT_TRUTH2" elsewhere. :contentReference[oaicite:14]{index=14} + string memory identifierStr = vm.envOr("OG_IDENTIFIER_STR", defaultIdentifierStr()); config.identifier = bytes32(bytes(identifierStr)); // "ZODIAC" fits in 32 bytes. // salts @@ -182,6 +182,13 @@ contract DeploySafeWithOptimisticGovernor is Script { config.ogSaltNonce = vm.envOr("OG_SALT_NONCE", uint256(1)); } + function defaultIdentifierStr() internal view returns (string memory) { + if (block.chainid == 11155111) { + return "ASSERT_TRUTH"; + } + return "ASSERT_TRUTH2"; + } + function resolveModuleProxyFactory() internal returns (address moduleProxyFactory) { // 1) Deploy (or use) ModuleProxyFactory // (You can also set MODULE_PROXY_FACTORY env and skip deployment if you prefer.) From ee0387b6c354913efdae1e09a0bfaa6911f926a5 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Sun, 1 Feb 2026 13:16:56 -0800 Subject: [PATCH 014/174] update agent and add codexignore Signed-off-by: John Shutt --- .codexignore | 3 + agent/src/index.js | 397 ++++++++++++++++--------- script/ProposeCommitmentTransfer.s.sol | 2 +- 3 files changed, 268 insertions(+), 134 deletions(-) create mode 100644 .codexignore diff --git a/.codexignore b/.codexignore new file mode 100644 index 00000000..ca05dd84 --- /dev/null +++ b/.codexignore @@ -0,0 +1,3 @@ +.env +.env.* +**/*.env**/*.env.* \ No newline at end of file diff --git a/agent/src/index.js b/agent/src/index.js index 22af6abc..9e5e5a13 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -13,24 +13,16 @@ import { } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -const optimisticGovernorAbiV1 = parseAbi([ - 'function proposeTransactions((address to,uint256 value,bytes data,uint8 operation)[] transactions) returns (bytes32 proposalHash)', - 'function collateral() view returns (address)', - 'function bondAmount() view returns (uint256)', - 'function optimisticOracleV3() view returns (address)', - 'function rules() view returns (string)', - 'function identifier() view returns (bytes32)', - 'function liveness() view returns (uint64)', -]); - -const optimisticGovernorAbiV2 = parseAbi([ - 'function proposeTransactions((address to,uint256 value,bytes data,uint8 operation)[] transactions, bytes explanation) returns (bytes32 proposalHash)', +const optimisticGovernorAbi = parseAbi([ + 'function proposeTransactions((address to,uint8 operation,uint256 value,bytes data)[] transactions, bytes explanation)', + 'function executeProposal((address to,uint8 operation,uint256 value,bytes data)[] transactions)', 'function collateral() view returns (address)', 'function bondAmount() view returns (uint256)', 'function optimisticOracleV3() view returns (address)', 'function rules() view returns (string)', 'function identifier() view returns (bytes32)', 'function liveness() view returns (uint64)', + 'function assertionIds(bytes32) view returns (bytes32)', ]); const optimisticOracleAbi = parseAbi([ @@ -40,6 +32,15 @@ const optimisticOracleAbi = parseAbi([ const transferEvent = parseAbiItem( 'event Transfer(address indexed from, address indexed to, uint256 value)' ); +const transactionsProposedEvent = parseAbiItem( + 'event TransactionsProposed(address indexed proposer,uint256 indexed proposalTime,bytes32 indexed assertionId,((address to,uint8 operation,uint256 value,bytes data)[] transactions,uint256 requestTime) proposal,bytes32 proposalHash,bytes explanation,string rules,uint256 challengeWindowEnds)' +); +const proposalExecutedEvent = parseAbiItem( + 'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' +); +const proposalDeletedEvent = parseAbiItem( + 'event ProposalDeleted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' +); function mustGetEnv(key) { const value = process.env[key]; @@ -81,6 +82,11 @@ const config = { openAiApiKey: process.env.OPENAI_API_KEY, openAiModel: process.env.OPENAI_MODEL ?? 'gpt-4.1-mini', openAiBaseUrl: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1', + allowProposeOnSimulationFail: true, + proposeGasLimit: process.env.PROPOSE_GAS_LIMIT + ? BigInt(process.env.PROPOSE_GAS_LIMIT) + : 2_000_000n, + executeRetryMs: Number(process.env.EXECUTE_RETRY_MS ?? 60_000), }; const account = privateKeyToAccount(config.privateKey); @@ -90,13 +96,16 @@ const walletClient = createWalletClient({ account, transport: http(config.rpcUrl const trackedAssets = new Set(config.watchAssets); let lastCheckedBlock = config.startBlock; +let lastProposalCheckedBlock = config.startBlock; let lastNativeBalance; let ogContext; +const proposalsByHash = new Map(); +const zeroBytes32 = `0x${'0'.repeat(64)}`; async function loadOptimisticGovernorDefaults() { const collateral = await publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'collateral', }); @@ -107,32 +116,32 @@ async function loadOgContext() { const [collateral, bondAmount, optimisticOracle, rules, identifier, liveness] = await Promise.all([ publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'collateral', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'bondAmount', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'optimisticOracleV3', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'rules', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'identifier', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'liveness', }), ]); @@ -157,22 +166,22 @@ async function logOgFundingStatus() { const [collateral, bondAmount, optimisticOracle, identifier] = await Promise.all([ publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'collateral', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'bondAmount', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'optimisticOracleV3', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'identifier', }), ]); @@ -192,20 +201,6 @@ async function logOgFundingStatus() { }); const nativeBalance = await publicClient.getBalance({ address: account.address }); - console.log('[agent] OG funding status:', { - proposer: account.address, - collateral, - bondAmount: bondAmount.toString(), - minimumBond: minimumBond.toString(), - requiredBond: requiredBond.toString(), - collateralBalance: collateralBalance.toString(), - nativeBalance: nativeBalance.toString(), - optimisticOracle, - chainId, - identifier, - expectedIdentifier, - }); - if (identifier !== expectedIdentifier) { console.warn( `[agent] OG identifier mismatch: expected ${expectedIdentifierStr}, onchain ${identifier}` @@ -226,25 +221,25 @@ async function primeBalances(blockNumber) { } async function postBondAndPropose(transactions) { + const normalizedTransactions = normalizeOgTransactions(transactions); const proposerBalance = await publicClient.getBalance({ address: account.address }); const [collateral, bondAmount, optimisticOracle] = await Promise.all([ publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'collateral', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'bondAmount', }), publicClient.readContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'optimisticOracleV3', }), ]); - let minimumBond = 0n; try { minimumBond = await publicClient.readContract({ @@ -307,97 +302,104 @@ async function postBondAndPropose(transactions) { ); } - const [allowanceOg, allowanceOo] = await Promise.all([ - publicClient.readContract({ - address: collateral, - abi: erc20Abi, - functionName: 'allowance', - args: [account.address, config.ogModule], - }), - publicClient.readContract({ - address: collateral, - abi: erc20Abi, - functionName: 'allowance', - args: [account.address, optimisticOracle], - }), - ]); - - console.log('[agent] Propose preflight:', { - proposer: account.address, - ogModule: config.ogModule, - optimisticOracle, - collateral, - bondAmount: bondAmount.toString(), - minimumBond: minimumBond.toString(), - requiredBond: requiredBond.toString(), - collateralBalance: ( - await publicClient.readContract({ - address: collateral, - abi: erc20Abi, - functionName: 'balanceOf', - args: [account.address], - }) - ).toString(), - allowanceOg: allowanceOg.toString(), - allowanceOo: allowanceOo.toString(), - }); - - const proposalContext = { - ogModule: config.ogModule, - proposer: account.address, - collateral, - bondAmount: bondAmount.toString(), - minimumBond: minimumBond.toString(), - requiredBond: requiredBond.toString(), - optimisticOracle, - }; - let proposalHash; const explanation = 'Agent serving Oya commitment.'; + const explanationBytes = stringToHex(explanation); + const proposalData = encodeFunctionData({ + abi: optimisticGovernorAbi, + functionName: 'proposeTransactions', + args: [normalizedTransactions, explanationBytes], + }); + let simulationError; + let submissionError; try { - console.log('[agent] Propose signature: V2 (transactions, explanation)'); await publicClient.simulateContract({ address: config.ogModule, - abi: optimisticGovernorAbiV2, + abi: optimisticGovernorAbi, functionName: 'proposeTransactions', - args: [transactions, explanation], + args: [normalizedTransactions, explanationBytes], account: account.address, }); - proposalHash = await walletClient.writeContract({ - address: config.ogModule, - abi: optimisticGovernorAbiV2, - functionName: 'proposeTransactions', - args: [transactions, explanation], - }); - } catch (errorV2) { - try { - console.log('[agent] Propose signature: V1 (transactions)'); - await publicClient.simulateContract({ - address: config.ogModule, - abi: optimisticGovernorAbiV1, - functionName: 'proposeTransactions', - args: [transactions], - account: account.address, + } catch (error) { + simulationError = error; + if (!config.allowProposeOnSimulationFail) { + throw error; + } + console.warn('[agent] Simulation failed; attempting to propose anyway.'); + } + + try { + if (simulationError) { + proposalHash = await walletClient.sendTransaction({ + account, + to: config.ogModule, + data: proposalData, + value: 0n, + gas: config.proposeGasLimit, }); + } else { proposalHash = await walletClient.writeContract({ address: config.ogModule, - abi: optimisticGovernorAbiV1, + abi: optimisticGovernorAbi, functionName: 'proposeTransactions', - args: [transactions], + args: [normalizedTransactions, explanationBytes], }); - } catch (errorV1) { - const message = - errorV1?.shortMessage ?? - errorV1?.message ?? - errorV2?.shortMessage ?? - errorV2?.message ?? - String(errorV1 ?? errorV2); - console.warn('[agent] Propose simulation context:', proposalContext); - throw new Error(`Propose simulation failed: ${message}`); } + } catch (error) { + submissionError = error; + const message = + error?.shortMessage ?? + error?.message ?? + simulationError?.shortMessage ?? + simulationError?.message ?? + String(error ?? simulationError); + console.warn('[agent] Propose submission failed:', message); + } + + if (proposalHash) { + console.log('[agent] Proposal submitted:', proposalHash); + } + + return { + proposalHash, + bondAmount, + collateral, + optimisticOracle, + submissionError: submissionError ? summarizeViemError(submissionError) : null, + }; +} + +function normalizeOgTransactions(transactions) { + if (!Array.isArray(transactions)) { + throw new Error('transactions must be an array'); } - return { proposalHash, bondAmount, collateral, optimisticOracle }; + return transactions.map((tx, index) => { + if (!tx || !tx.to) { + throw new Error(`transactions[${index}] missing to`); + } + + return { + to: getAddress(tx.to), + value: BigInt(tx.value ?? 0), + data: tx.data ?? '0x', + operation: Number(tx.operation ?? 0), + }; + }); +} + +function summarizeViemError(error) { + if (!error) return null; + + return { + name: error.name, + shortMessage: error.shortMessage, + message: error.message, + details: error.details, + metaMessages: error.metaMessages, + data: error.data ?? error.cause?.data, + cause: error.cause?.shortMessage ?? error.cause?.message ?? error.cause, + }; } async function makeDeposit({ asset, amountWei }) { @@ -486,14 +488,146 @@ async function pollCommitmentChanges() { return deposits; } -async function decideOnSignals(signals) { - console.log(`[agent] ${signals.length} change(s) detected.`); +async function pollProposalChanges() { + const latestBlock = await publicClient.getBlockNumber(); + if (lastProposalCheckedBlock === undefined) { + lastProposalCheckedBlock = latestBlock; + return; + } - if (!config.openAiApiKey) { - console.log('[agent] OPENAI_API_KEY not set; logging signals only.'); - for (const signal of signals) { - console.log(signal); + if (latestBlock <= lastProposalCheckedBlock) { + return; + } + + const fromBlock = lastProposalCheckedBlock + 1n; + const toBlock = latestBlock; + + const [proposedLogs, executedLogs, deletedLogs] = await Promise.all([ + publicClient.getLogs({ + address: config.ogModule, + event: transactionsProposedEvent, + fromBlock, + toBlock, + }), + publicClient.getLogs({ + address: config.ogModule, + event: proposalExecutedEvent, + fromBlock, + toBlock, + }), + publicClient.getLogs({ + address: config.ogModule, + event: proposalDeletedEvent, + fromBlock, + toBlock, + }), + ]); + + for (const log of proposedLogs) { + const proposalHash = log.args?.proposalHash; + const assertionId = log.args?.assertionId; + const proposal = log.args?.proposal; + const challengeWindowEnds = log.args?.challengeWindowEnds; + if (!proposalHash || !proposal?.transactions) continue; + + const transactions = proposal.transactions.map((tx) => ({ + to: getAddress(tx.to), + operation: Number(tx.operation ?? 0), + value: BigInt(tx.value ?? 0), + data: tx.data ?? '0x', + })); + + proposalsByHash.set(proposalHash, { + proposalHash, + assertionId, + challengeWindowEnds: BigInt(challengeWindowEnds ?? 0), + transactions, + lastAttemptMs: 0, + }); + } + + for (const log of executedLogs) { + const proposalHash = log.args?.proposalHash; + if (proposalHash) { + proposalsByHash.delete(proposalHash); + } + } + + for (const log of deletedLogs) { + const proposalHash = log.args?.proposalHash; + if (proposalHash) { + proposalsByHash.delete(proposalHash); + } + } + + lastProposalCheckedBlock = toBlock; +} + +async function executeReadyProposals() { + if (proposalsByHash.size === 0) return; + + const latestBlock = await publicClient.getBlockNumber(); + const block = await publicClient.getBlock({ blockNumber: latestBlock }); + const now = BigInt(block.timestamp); + const nowMs = Date.now(); + + for (const proposal of proposalsByHash.values()) { + if (!proposal?.transactions?.length) continue; + if (proposal.challengeWindowEnds === undefined) continue; + if (now < proposal.challengeWindowEnds) continue; + if (proposal.lastAttemptMs && nowMs - proposal.lastAttemptMs < config.executeRetryMs) { + continue; } + + proposal.lastAttemptMs = nowMs; + + let assertionId; + try { + assertionId = await publicClient.readContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'assertionIds', + args: [proposal.proposalHash], + }); + } catch (error) { + console.warn('[agent] Failed to read assertionId:', error); + continue; + } + + if (!assertionId || assertionId === zeroBytes32) { + proposalsByHash.delete(proposal.proposalHash); + continue; + } + + try { + await publicClient.simulateContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'executeProposal', + args: [proposal.transactions], + account: account.address, + }); + } catch (error) { + console.warn('[agent] Proposal not executable yet:', proposal.proposalHash); + continue; + } + + try { + const txHash = await walletClient.writeContract({ + address: config.ogModule, + abi: optimisticGovernorAbi, + functionName: 'executeProposal', + args: [proposal.transactions], + }); + console.log('[agent] Proposal execution submitted:', txHash); + } catch (error) { + console.warn('[agent] Proposal execution failed:', error?.shortMessage ?? error?.message ?? error); + } + } +} + +async function decideOnSignals(signals) { + if (!config.openAiApiKey) { return; } @@ -504,7 +638,6 @@ async function decideOnSignals(signals) { try { const decision = await callAgent(signals, ogContext); if (decision.toolCalls.length > 0) { - console.log('[agent] Agent tool calls:', decision.toolCalls); const toolOutputs = await executeToolCalls(decision.toolCalls); if (decision.responseId && toolOutputs.length > 0) { const explanation = await explainToolCalls( @@ -517,23 +650,21 @@ async function decideOnSignals(signals) { } return; } - console.log('[agent] Agent decision:', decision.textDecision); } catch (error) { - console.error('[agent] Agent call failed; logging signals', error); - for (const signal of signals) { - console.log(signal); - } + console.error('[agent] Agent call failed', error); } } async function agentLoop() { try { - console.log(`[agent] loop tick @ ${new Date().toISOString()}`); const signals = await pollCommitmentChanges(); + await pollProposalChanges(); if (signals.length > 0) { await decideOnSignals(signals); } + + await executeReadyProposals(); } catch (error) { console.error('[agent] loop error', error); } @@ -542,7 +673,6 @@ async function agentLoop() { } async function startAgent() { - console.log('[agent] initializing...'); await loadOptimisticGovernorDefaults(); await loadOgContext(); await logOgFundingStatus(); @@ -550,11 +680,13 @@ async function startAgent() { if (lastCheckedBlock === undefined) { lastCheckedBlock = await publicClient.getBlockNumber(); } + if (lastProposalCheckedBlock === undefined) { + lastProposalCheckedBlock = lastCheckedBlock; + } await primeBalances(lastCheckedBlock); - console.log('[agent] watching assets:', [...trackedAssets].join(', ')); - console.log('[agent] starting loop with interval', config.pollIntervalMs, 'ms'); + console.log('[agent] running...'); agentLoop(); } @@ -881,7 +1013,6 @@ async function executeToolCalls(toolCalls) { } if (builtTransactions && !hasPostProposal) { const result = await postBondAndPropose(builtTransactions); - console.log('[agent] Auto-proposed via OG:', result); } return outputs.filter((item) => item.callId); } diff --git a/script/ProposeCommitmentTransfer.s.sol b/script/ProposeCommitmentTransfer.s.sol index 6d3dc855..5c9a866f 100644 --- a/script/ProposeCommitmentTransfer.s.sol +++ b/script/ProposeCommitmentTransfer.s.sol @@ -10,9 +10,9 @@ interface IERC20 { interface IOptimisticGovernor { struct Transaction { address to; + uint8 operation; uint256 value; bytes data; - uint8 operation; } function proposeTransactions(Transaction[] calldata transactions) external returns (bytes32 proposalHash); From 643c4aeebb873f8d17aba2a6fe2520081b1f477b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:22:19 +0000 Subject: [PATCH 015/174] Bump h3 from 1.15.4 to 1.15.5 in /frontend Bumps [h3](https://github.com/h3js/h3) from 1.15.4 to 1.15.5. - [Release notes](https://github.com/h3js/h3/releases) - [Changelog](https://github.com/h3js/h3/blob/v1.15.5/CHANGELOG.md) - [Commits](https://github.com/h3js/h3/compare/v1.15.4...v1.15.5) --- updated-dependencies: - dependency-name: h3 dependency-version: 1.15.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 150e10f3..f851ed09 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7612,9 +7612,9 @@ } }, "node_modules/h3": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.4.tgz", - "integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", + "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", @@ -7622,9 +7622,9 @@ "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.2", + "node-mock-http": "^1.0.4", "radix3": "^1.1.2", - "ufo": "^1.6.1", + "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, @@ -9291,9 +9291,9 @@ } }, "node_modules/ufo": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz", - "integrity": "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT" }, "node_modules/uint8arrays": { From 2b301d6838687f422d5ada8deadff35dd41ab757 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:22:50 +0000 Subject: [PATCH 016/174] Bump lodash from 4.17.21 to 4.17.23 in /frontend Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.17.23 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 150e10f3..1f4d3ee9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8083,9 +8083,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/loose-envify": { From ab033c347fee8cdf32a6b0b0baf9978f8761d8cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:22:58 +0000 Subject: [PATCH 017/174] Bump hono from 4.11.4 to 4.11.7 in /frontend Bumps [hono](https://github.com/honojs/hono) from 4.11.4 to 4.11.7. - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.11.4...v4.11.7) --- updated-dependencies: - dependency-name: hono dependency-version: 4.11.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 150e10f3..18c11610 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7680,9 +7680,9 @@ } }, "node_modules/hono": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", - "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", "engines": { "node": ">=16.9.0" From c32909038d7e8d82642de40cb21574fce8b99257 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 2 Feb 2026 10:31:08 -0800 Subject: [PATCH 018/174] add more signing and key management options Signed-off-by: John Shutt --- .env.mainnet.example | 14 ++- README.md | 43 ++++++++-- agent/.env.example | 54 ++++++++---- agent/README.md | 7 +- agent/package-lock.json | 107 +++++++++++++++++++++++ agent/package.json | 1 + agent/src/index.js | 138 +++++++++++++++++++++++++++-- frontend/src/App.jsx | 186 ---------------------------------------- 8 files changed, 333 insertions(+), 217 deletions(-) diff --git a/.env.mainnet.example b/.env.mainnet.example index 8be73f11..40c24031 100644 --- a/.env.mainnet.example +++ b/.env.mainnet.example @@ -1,9 +1,21 @@ # Fork source MAINNET_RPC_URL=... -# Deployer +# Deployer (use DEPLOYER_PK directly or resolve it at runtime via agent/with-signer.mjs) DEPLOYER_PK=0x... +# Optional signer resolution (used with agent/with-signer.mjs) +# SIGNER_TYPE=env +# PRIVATE_KEY=0x... +# KEYSTORE_PATH=./keys/deployer.json +# KEYSTORE_PASSWORD=... +# KEYCHAIN_SERVICE=og-deployer +# KEYCHAIN_ACCOUNT=deployer +# VAULT_ADDR=https://vault.example.com +# VAULT_TOKEN=... +# VAULT_SECRET_PATH=secret/data/og-deployer +# VAULT_SECRET_KEY=private_key + # Optimistic Governor config OG_COLLATERAL=0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 OG_BOND_AMOUNT=250000000 diff --git a/README.md b/README.md index bf143ed9..5cc97bcc 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Add more tools for a specific commitment beside these generics; keep the default The web frontend is a lightweight UI for filling in Safe + Optimistic Governor parameters and launching the same deployment flow as the script. It can be hosted as a static site and uses RPC endpoints to read chain state and craft the deployment payloads. -It also prepares `.env` files for the offchain agent scaffold (in `agent/`) so you can deploy a commitment and immediately configure an agent to serve it. +It mirrors the deploy script flow with a UI-driven parameter set. ### Dependencies @@ -79,10 +79,7 @@ npm install npm run dev ``` -Agent `.env` prep (in the UI): - -- Fill RPC, agent key, and addresses (Safe + OG) — the UI auto-fills addresses from the deployment section. -- Copy or download the rendered `.env`, then run the agent locally: `cd agent && npm install && npm start`. +Agent setup is documented separately in `agent/README.md`. ### Required Environment Variables @@ -145,6 +142,42 @@ SAFE_SINGLETON=0x6666666666666666666666666666666666666666 SAFE_FALLBACK_HANDLER=0x7777777777777777777777777777777777777777 ``` +### Signer Options (CLI Scripts) + +Forge scripts still require a private key env var (e.g., `DEPLOYER_PK`, `PROPOSER_PK`, `EXECUTOR_PK`). If you don't want to store raw keys in `.env`, use `agent/with-signer.mjs` to resolve a signer at runtime and inject the env var: + +```shell +# Private key from env +SIGNER_TYPE=env PRIVATE_KEY=0x... \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# Encrypted keystore +SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/deployer.json KEYSTORE_PASSWORD=... \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# OS keychain +SIGNER_TYPE=keychain KEYCHAIN_SERVICE=og-deployer KEYCHAIN_ACCOUNT=deployer \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# Vault KV (private key stored as a secret) +SIGNER_TYPE=vault VAULT_ADDR=https://vault.example.com VAULT_TOKEN=... VAULT_SECRET_PATH=secret/data/og-deployer \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast +``` + +For KMS/Vault signing without exporting private keys, use an RPC signer proxy that exposes `eth_sendTransaction` (set `SIGNER_RPC_URL` and `SIGNER_ADDRESS`). The agent supports this directly (see `agent/README.md`); Forge scripts need a proxy that can export or inject keys. + ## Common Commands ```shell diff --git a/agent/.env.example b/agent/.env.example index f875d9d0..1a4c7a3e 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -1,27 +1,43 @@ -# RPC endpoint and signing key -RPC_URL=http://127.0.0.1:8545 -PRIVATE_KEY=0xabc123... +# Required +RPC_URL=https://... +COMMITMENT_SAFE=0x... +OG_MODULE=0x... +WATCH_ASSETS=0xToken1,0xToken2 -# Commitment + Optimistic Governor addresses -COMMITMENT_SAFE=0x0000000000000000000000000000000000000000 -OG_MODULE=0x0000000000000000000000000000000000000000 +# Signer selection (default: env) +SIGNER_TYPE=env +PRIVATE_KEY=0x... -# Assets to watch for deposits (comma-separated ERC20 addresses) -WATCH_ASSETS=0x0000000000000000000000000000000000000000 +# Encrypted keystore +# SIGNER_TYPE=keystore +# KEYSTORE_PATH=./keys/agent.json +# KEYSTORE_PASSWORD=... -# Polling loop -POLL_INTERVAL_MS=60000 -# Optional: start from a specific block number -# START_BLOCK=12345678 +# OS keychain (macOS Keychain or Linux Secret Service) +# SIGNER_TYPE=keychain +# KEYCHAIN_SERVICE=og-deployer +# KEYCHAIN_ACCOUNT=agent -# Watch native ETH balance increases (true|false) -WATCH_NATIVE_BALANCE=true +# Vault KV (private key stored as a secret) +# SIGNER_TYPE=vault +# VAULT_ADDR=https://vault.example.com +# VAULT_TOKEN=... +# VAULT_SECRET_PATH=secret/data/og-deployer +# VAULT_SECRET_KEY=private_key + +# KMS/Vault signer via RPC (eth_sendTransaction) +# SIGNER_TYPE=rpc +# SIGNER_RPC_URL=https://signer.example.com +# SIGNER_ADDRESS=0x... -# Optional convenience defaults for deposits -# DEFAULT_DEPOSIT_ASSET=0x0000000000000000000000000000000000000000 -# DEFAULT_DEPOSIT_AMOUNT_WEI=0 +# Optional tuning +POLL_INTERVAL_MS=60000 +WATCH_NATIVE_BALANCE=true +# START_BLOCK= +# DEFAULT_DEPOSIT_ASSET= +# DEFAULT_DEPOSIT_AMOUNT_WEI= -# Optional: OpenAI Responses API integration -# OPENAI_API_KEY=sk-... +# Optional LLM +# OPENAI_API_KEY= # OPENAI_MODEL=gpt-4.1-mini # OPENAI_BASE_URL=https://api.openai.com/v1 diff --git a/agent/README.md b/agent/README.md index 8a1be325..78a1bb56 100644 --- a/agent/README.md +++ b/agent/README.md @@ -12,10 +12,15 @@ Generic offchain agent wiring for monitoring a commitment and acting through the 1. Copy `.env.example` to `.env` and fill in: - `RPC_URL`: RPC the agent should use - - `PRIVATE_KEY`: agent signer (never commit this) - `COMMITMENT_SAFE`: Safe address holding assets - `OG_MODULE`: Optimistic Governor module address - `WATCH_ASSETS`: Comma-separated ERC20s to monitor (the OG collateral is auto-added) + - Signer selection: `SIGNER_TYPE` (default `env`) + - `env`: `PRIVATE_KEY` + - `keystore`: `KEYSTORE_PATH`, `KEYSTORE_PASSWORD` + - `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` (macOS Keychain or Linux Secret Service) + - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) + - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*` - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` 2. Install deps and start the loop: diff --git a/agent/package-lock.json b/agent/package-lock.json index ddc4aca4..7b267484 100644 --- a/agent/package-lock.json +++ b/agent/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "dotenv": "^16.4.5", + "ethers": "^6.12.0", "viem": "^2.20.0" } }, @@ -93,6 +94,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/abitype": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", @@ -114,6 +124,12 @@ } } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -126,6 +142,85 @@ "url": "https://dotenvx.com" } }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -177,6 +272,18 @@ } } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/viem": { "version": "2.45.1", "resolved": "https://registry.npmjs.org/viem/-/viem-2.45.1.tgz", diff --git a/agent/package.json b/agent/package.json index 8b40a000..78a95352 100644 --- a/agent/package.json +++ b/agent/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "dotenv": "^16.4.5", + "ethers": "^6.12.0", "viem": "^2.20.0" } } diff --git a/agent/src/index.js b/agent/src/index.js index 9e5e5a13..e3b0157c 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -1,4 +1,8 @@ import 'dotenv/config'; +import { readFile } from 'node:fs/promises'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { Wallet } from 'ethers'; import { createPublicClient, createWalletClient, @@ -11,7 +15,7 @@ import { stringToHex, zeroAddress, } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; +import { privateKeyToAccount, toAccount } from 'viem/accounts'; const optimisticGovernorAbi = parseAbi([ 'function proposeTransactions((address to,uint8 operation,uint256 value,bytes data)[] transactions, bytes explanation)', @@ -51,6 +55,79 @@ function mustGetEnv(key) { return value; } +function normalizePrivateKey(value) { + if (!value) return value; + return value.startsWith('0x') ? value : `0x${value}`; +} + +const execFileAsync = promisify(execFile); + +async function loadPrivateKeyFromKeystore() { + const keystorePath = mustGetEnv('KEYSTORE_PATH'); + const keystorePassword = mustGetEnv('KEYSTORE_PASSWORD'); + const keystoreJson = await readFile(keystorePath, 'utf8'); + const wallet = await Wallet.fromEncryptedJson(keystoreJson, keystorePassword); + return wallet.privateKey; +} + +async function loadPrivateKeyFromKeychain() { + const service = mustGetEnv('KEYCHAIN_SERVICE'); + const account = mustGetEnv('KEYCHAIN_ACCOUNT'); + + if (process.platform === 'darwin') { + const { stdout } = await execFileAsync('security', [ + 'find-generic-password', + '-s', + service, + '-a', + account, + '-w', + ]); + return stdout.trim(); + } + + if (process.platform === 'linux') { + const { stdout } = await execFileAsync('secret-tool', [ + 'lookup', + 'service', + service, + 'account', + account, + ]); + return stdout.trim(); + } + + throw new Error('Keychain lookup not supported on this platform.'); +} + +async function loadPrivateKeyFromVault() { + const vaultAddr = mustGetEnv('VAULT_ADDR').replace(/\/+$/, ''); + const vaultToken = mustGetEnv('VAULT_TOKEN'); + const vaultPath = mustGetEnv('VAULT_SECRET_PATH').replace(/^\/+/, ''); + const vaultNamespace = process.env.VAULT_NAMESPACE; + const vaultKeyField = process.env.VAULT_SECRET_KEY ?? 'private_key'; + + const response = await fetch(`${vaultAddr}/v1/${vaultPath}`, { + headers: { + 'X-Vault-Token': vaultToken, + ...(vaultNamespace ? { 'X-Vault-Namespace': vaultNamespace } : {}), + }, + }); + + if (!response.ok) { + throw new Error(`Vault request failed (${response.status}).`); + } + + const payload = await response.json(); + const data = payload?.data?.data ?? payload?.data ?? {}; + const value = data[vaultKeyField]; + if (!value) { + throw new Error(`Vault secret missing key '${vaultKeyField}'.`); + } + + return value; +} + function parseAddressList(list) { if (!list) return []; return list @@ -62,7 +139,6 @@ function parseAddressList(list) { const config = { rpcUrl: mustGetEnv('RPC_URL'), - privateKey: mustGetEnv('PRIVATE_KEY'), commitmentSafe: getAddress(mustGetEnv('COMMITMENT_SAFE')), ogModule: getAddress(mustGetEnv('OG_MODULE')), pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 10_000), @@ -89,10 +165,62 @@ const config = { executeRetryMs: Number(process.env.EXECUTE_RETRY_MS ?? 60_000), }; -const account = privateKeyToAccount(config.privateKey); -const agentAddress = account.address; const publicClient = createPublicClient({ transport: http(config.rpcUrl) }); -const walletClient = createWalletClient({ account, transport: http(config.rpcUrl) }); + +async function createSignerClient() { + const signerType = (process.env.SIGNER_TYPE ?? 'env').toLowerCase(); + + if (signerType === 'env') { + const privateKey = normalizePrivateKey(mustGetEnv('PRIVATE_KEY')); + const account = privateKeyToAccount(privateKey); + return { + account, + walletClient: createWalletClient({ account, transport: http(config.rpcUrl) }), + }; + } + + if (signerType === 'keystore') { + const privateKey = normalizePrivateKey(await loadPrivateKeyFromKeystore()); + const account = privateKeyToAccount(privateKey); + return { + account, + walletClient: createWalletClient({ account, transport: http(config.rpcUrl) }), + }; + } + + if (signerType === 'keychain') { + const privateKey = normalizePrivateKey(await loadPrivateKeyFromKeychain()); + const account = privateKeyToAccount(privateKey); + return { + account, + walletClient: createWalletClient({ account, transport: http(config.rpcUrl) }), + }; + } + + if (signerType === 'vault') { + const privateKey = normalizePrivateKey(await loadPrivateKeyFromVault()); + const account = privateKeyToAccount(privateKey); + return { + account, + walletClient: createWalletClient({ account, transport: http(config.rpcUrl) }), + }; + } + + if (['kms', 'vault-signer', 'signer-rpc', 'rpc', 'json-rpc'].includes(signerType)) { + const signerRpcUrl = mustGetEnv('SIGNER_RPC_URL'); + const signerAddress = getAddress(mustGetEnv('SIGNER_ADDRESS')); + const account = toAccount(signerAddress); + return { + account, + walletClient: createWalletClient({ account, transport: http(signerRpcUrl) }), + }; + } + + throw new Error(`Unsupported SIGNER_TYPE '${signerType}'.`); +} + +const { account, walletClient } = await createSignerClient(); +const agentAddress = account.address; const trackedAssets = new Set(config.watchAssets); let lastCheckedBlock = config.startBlock; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5d983edb..e58f4ca2 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -208,19 +208,6 @@ function App() { safe: '', ogModule: '', }); - const [agentForm, setAgentForm] = useState({ - rpcUrl: '', - privateKey: '', - commitmentSafe: '', - ogModule: '', - watchAssets: '', - watchNativeBalance: true, - pollIntervalMs: '60000', - startBlock: '', - defaultDepositAsset: '', - defaultDepositAmountWei: '', - }); - const [agentCopyStatus, setAgentCopyStatus] = useState(''); const [txHashes, setTxHashes] = useState({ moduleProxyFactory: '', safeProxy: '', @@ -240,14 +227,6 @@ function App() { setForm((prev) => ({ ...prev, [name]: value })); }; - const onAgentChange = (event) => { - const { name, type, checked, value } = event.target; - setAgentForm((prev) => ({ - ...prev, - [name]: type === 'checkbox' ? checked : value, - })); - }; - const validatedAddresses = useMemo(() => { const required = { collateral: form.collateral, @@ -580,68 +559,6 @@ function App() { } }; - useEffect(() => { - setAgentForm((prev) => ({ - ...prev, - commitmentSafe: prev.commitmentSafe || deployment.safe, - ogModule: prev.ogModule || deployment.ogModule, - })); - }, [deployment.safe, deployment.ogModule]); - - const agentEnvPreview = useMemo(() => { - const lines = [ - `RPC_URL=${agentForm.rpcUrl || ''}`, - `PRIVATE_KEY=${agentForm.privateKey || ''}`, - `COMMITMENT_SAFE=${agentForm.commitmentSafe || deployment.safe || ''}`, - `OG_MODULE=${agentForm.ogModule || deployment.ogModule || ''}`, - `WATCH_ASSETS=${agentForm.watchAssets || ''}`, - `POLL_INTERVAL_MS=${agentForm.pollIntervalMs || '60000'}`, - `WATCH_NATIVE_BALANCE=${agentForm.watchNativeBalance ? 'true' : 'false'}`, - ]; - - if (agentForm.startBlock) { - lines.push(`START_BLOCK=${agentForm.startBlock}`); - } - - if (agentForm.defaultDepositAsset) { - lines.push(`DEFAULT_DEPOSIT_ASSET=${agentForm.defaultDepositAsset}`); - } - - if (agentForm.defaultDepositAmountWei) { - lines.push(`DEFAULT_DEPOSIT_AMOUNT_WEI=${agentForm.defaultDepositAmountWei}`); - } - - return lines.join('\n'); - }, [ - agentForm.commitmentSafe, - agentForm.defaultDepositAmountWei, - agentForm.defaultDepositAsset, - agentForm.ogModule, - agentForm.pollIntervalMs, - agentForm.privateKey, - agentForm.rpcUrl, - agentForm.startBlock, - agentForm.watchAssets, - agentForm.watchNativeBalance, - deployment.ogModule, - deployment.safe, - ]); - - const copyAgentEnv = async () => { - try { - await navigator.clipboard.writeText(agentEnvPreview); - setAgentCopyStatus('Copied env to clipboard.'); - setTimeout(() => setAgentCopyStatus(''), 2000); - } catch (err) { - setAgentCopyStatus('Copy failed.'); - setTimeout(() => setAgentCopyStatus(''), 2000); - } - }; - - const agentEnvDownloadUrl = useMemo(() => { - return `data:text/plain;charset=utf-8,${encodeURIComponent(agentEnvPreview)}`; - }, [agentEnvPreview]); - return (
@@ -766,109 +683,6 @@ function App() {
-
-

Agent Config (offchain)

-

- Prepare the `.env` for the agent scaffold in agent/. Secrets stay in your browser; copy or download - the file and run the agent locally. -

-
- - - - - - - - - - -
- -
-
-
{agentEnvPreview}
-
-
- - - Download .env - -
-

Run: cd agent && npm install && npm start

- {agentCopyStatus &&

{agentCopyStatus}

} -
-
-
-
); } From f60df3ea1eda2d8b9e8f32ba53080a132c9ee4e2 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 2 Feb 2026 10:34:28 -0800 Subject: [PATCH 019/174] remove agent builder from frontend Signed-off-by: John Shutt --- agent/with-signer.mjs | 156 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 agent/with-signer.mjs diff --git a/agent/with-signer.mjs b/agent/with-signer.mjs new file mode 100644 index 00000000..2ae21874 --- /dev/null +++ b/agent/with-signer.mjs @@ -0,0 +1,156 @@ +#!/usr/bin/env node +import { readFile } from 'node:fs/promises'; +import { execFile, spawn } from 'node:child_process'; +import { promisify } from 'node:util'; +import { Wallet } from 'ethers'; + +const execFileAsync = promisify(execFile); + +function normalizePrivateKey(value) { + if (!value) return value; + return value.startsWith('0x') ? value : `0x${value}`; +} + +function mustGetEnv(key) { + const value = process.env[key]; + if (!value) { + throw new Error(`Missing required env var ${key}`); + } + return value; +} + +async function loadPrivateKeyFromKeystore() { + const keystorePath = mustGetEnv('KEYSTORE_PATH'); + const keystorePassword = mustGetEnv('KEYSTORE_PASSWORD'); + const keystoreJson = await readFile(keystorePath, 'utf8'); + const wallet = await Wallet.fromEncryptedJson(keystoreJson, keystorePassword); + return wallet.privateKey; +} + +async function loadPrivateKeyFromKeychain() { + const service = mustGetEnv('KEYCHAIN_SERVICE'); + const account = mustGetEnv('KEYCHAIN_ACCOUNT'); + + if (process.platform === 'darwin') { + const { stdout } = await execFileAsync('security', [ + 'find-generic-password', + '-s', + service, + '-a', + account, + '-w', + ]); + return stdout.trim(); + } + + if (process.platform === 'linux') { + const { stdout } = await execFileAsync('secret-tool', [ + 'lookup', + 'service', + service, + 'account', + account, + ]); + return stdout.trim(); + } + + throw new Error('Keychain lookup not supported on this platform.'); +} + +async function loadPrivateKeyFromVault() { + const vaultAddr = mustGetEnv('VAULT_ADDR').replace(/\/+$/, ''); + const vaultToken = mustGetEnv('VAULT_TOKEN'); + const vaultPath = mustGetEnv('VAULT_SECRET_PATH').replace(/^\/+/, ''); + const vaultNamespace = process.env.VAULT_NAMESPACE; + const vaultKeyField = process.env.VAULT_SECRET_KEY ?? 'private_key'; + + const response = await fetch(`${vaultAddr}/v1/${vaultPath}`, { + headers: { + 'X-Vault-Token': vaultToken, + ...(vaultNamespace ? { 'X-Vault-Namespace': vaultNamespace } : {}), + }, + }); + + if (!response.ok) { + throw new Error(`Vault request failed (${response.status}).`); + } + + const payload = await response.json(); + const data = payload?.data?.data ?? payload?.data ?? {}; + const value = data[vaultKeyField]; + if (!value) { + throw new Error(`Vault secret missing key '${vaultKeyField}'.`); + } + + return value; +} + +async function resolvePrivateKey() { + const signerType = (process.env.SIGNER_TYPE ?? 'env').toLowerCase(); + + if (signerType === 'env') { + return mustGetEnv('PRIVATE_KEY'); + } + + if (signerType === 'keystore') { + return loadPrivateKeyFromKeystore(); + } + + if (signerType === 'keychain') { + return loadPrivateKeyFromKeychain(); + } + + if (signerType === 'vault') { + return loadPrivateKeyFromVault(); + } + + throw new Error(`Signer type '${signerType}' does not expose a private key.`); +} + +function parseArgs(args) { + const options = { envVar: 'DEPLOYER_PK' }; + const separatorIndex = args.indexOf('--'); + if (separatorIndex === -1) { + throw new Error('Usage: node agent/with-signer.mjs [--env VAR] -- '); + } + + const optionArgs = args.slice(0, separatorIndex); + const commandArgs = args.slice(separatorIndex + 1); + + for (let i = 0; i < optionArgs.length; i += 1) { + if (optionArgs[i] === '--env') { + options.envVar = optionArgs[i + 1]; + i += 1; + continue; + } + } + + if (!options.envVar) { + throw new Error('Missing env var name for --env.'); + } + + if (commandArgs.length === 0) { + throw new Error('Missing command after --.'); + } + + return { options, commandArgs }; +} + +const { options, commandArgs } = parseArgs(process.argv.slice(2)); +const privateKey = normalizePrivateKey(await resolvePrivateKey()); + +const [command, ...commandRest] = commandArgs; +const child = await new Promise((resolve, reject) => { + const proc = spawn(command, commandRest, { + stdio: 'inherit', + env: { + ...process.env, + [options.envVar]: privateKey, + }, + }); + + proc.on('exit', (code) => resolve(code ?? 1)); + proc.on('error', reject); +}); + +process.exitCode = child; From 25a663381eaa39b3078854ad7148bc78bb48922e Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 2 Feb 2026 10:59:37 -0800 Subject: [PATCH 020/174] readme updates Signed-off-by: John Shutt --- README.md | 170 +++++++++++---------------------------------- agent/README.md | 4 +- frontend/README.md | 40 +++++++++++ 3 files changed, 82 insertions(+), 132 deletions(-) create mode 100644 frontend/README.md diff --git a/README.md b/README.md index 5cc97bcc..5837e50e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,32 @@ -## OG Deployer +# Oya Commitments Monorepo -Command-line tooling for deploying onchain Commitments: a Safe with an Optimistic Governor module configured with natural language rules. Use it to spin up a new Safe, connect the Optimistic Governor module, and set the rules/bond/collateral in one script run. +This repo contains everything needed to set up **Oya commitments**: smart contracts controlled by natural language rules and the agents that serve them. It includes the Solidity contracts, deployment scripts, an optional web UI, and an offchain agent scaffold. -## What This Repo Does +## What Is a Commitment? -- Deploys a Safe and the Optimistic Governor module in one flow. -- Encodes natural language rules into the module configuration. -- Supports env-var overrides for Safe and OG parameters. -- Uses Foundry for scripting, testing, and deployments. +A commitment is a Safe controlled by an Optimistic Governor module. The commitment rules are written in natural language (stored onchain or via a URI) and enforced through the Optimistic Governor challenge process. Agents can observe incoming deposits and propose compliant transfers. -## Quick Start +## Concepts (How It Works) + +1. **Rules**: You define natural language rules for what the commitment may do. +2. **Control**: A Safe is deployed and wired to an Optimistic Governor module with those rules. +3. **Proposals**: An agent (or user) proposes transfers via the module and posts the bond. +4. **Challenge Window**: If no one challenges during the period, the proposal can be executed. +5. **Execution**: The Safe executes the approved transfer. + +## Repo Layout + +- `src/` Solidity contracts +- `script/` Foundry deployment and ops scripts +- `test/` Foundry tests +- `agent/` Offchain agent scaffold +- `frontend/` Web UI for configuring and deploying commitments +- `lib/` External dependencies (Foundry) + +## Quick Start (Deploy a Commitment) 1. Install Foundry: https://book.getfoundry.sh/ -2. Set environment variables in your shell or `.env` file (load via `direnv` or `dotenvx` if desired). +2. Set required environment variables. 3. Run the deployment script. ```shell @@ -22,29 +36,6 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis --private-key ``` -## Commitment Agent Tooling - -Use the offchain agent scaffold in `agent/` to serve commitments by posting bonds, proposing transactions, monitoring deposits, and making deposits on behalf of the commitment. It ships only generic tools; add commitment-specific behavior in your own prompts or handlers. - -### Setup & Run - -```shell -cd agent -npm install -cp .env.example .env # fill in RPC_URL, PRIVATE_KEY, COMMITMENT_SAFE, OG_MODULE, WATCH_ASSETS -npm start -``` - -The loop polls every `POLL_INTERVAL_MS` (default 60s) for ERC20 transfers into the commitment (and optional native balance increases). If nothing changes, the LLM/decision hook is not invoked. When signals are found, `decideOnSignals` is called—extend that function to route context into your system prompt and custom tools. - -### Built-in Agent Tools - -- `postBondAndPropose`: Approves the Optimistic Oracle for the module bond and calls `proposeTransactions` on the Optimistic Governor. -- `makeDeposit`: Sends ERC20 or native deposits into the commitment Safe using the agent key. -- `pollCommitmentChanges`: Watches configured assets (plus the OG collateral by default) for new deposits. - -Add more tools for a specific commitment beside these generics; keep the default agent lean. - ## Required Environment Variables - `DEPLOYER_PK`: Private key for the deployer. @@ -59,62 +50,34 @@ Add more tools for a specific commitment beside these generics; keep the default - `OG_MASTER_COPY`, `SAFE_SINGLETON`, `SAFE_FALLBACK_HANDLER` - `MODULE_PROXY_FACTORY` -## Web Frontend +## Offchain Agent (Serve a Commitment) -The web frontend is a lightweight UI for filling in Safe + Optimistic Governor parameters and launching the same deployment flow as the script. It can be hosted as a static site and uses RPC endpoints to read chain state and craft the deployment payloads. +The agent in `agent/` can watch deposits and propose or execute transfers via the Optimistic Governor module. It ships with generic tools; customize the decision logic to match your commitment rules. -It mirrors the deploy script flow with a UI-driven parameter set. +```shell +cd agent +npm install +cp .env.example .env # fill in RPC_URL, PRIVATE_KEY, COMMITMENT_SAFE, OG_MODULE, WATCH_ASSETS +npm start +``` -### Dependencies +Built-in tools include: -- Node.js 18+ (or newer) -- npm, pnpm, or yarn for package management +- `postBondAndPropose` +- `makeDeposit` +- `pollCommitmentChanges` -### Local Development +## Web Frontend -From the web frontend directory (if you keep it alongside this repo), install dependencies and start the dev server: +`frontend/` provides a lightweight UI for entering Safe + Optimistic Governor parameters and generating the same deployment flow as the script. It uses the connected wallet to submit transactions. ```shell +cd frontend npm install npm run dev ``` -Agent setup is documented separately in `agent/README.md`. - -### Required Environment Variables - -Expose these values to the frontend build (for example via `.env` in the frontend project) so the UI can prefill defaults and target the correct network: - -- `MAINNET_RPC_URL` or `SEPOLIA_RPC_URL` (or another network-specific RPC URL) -- Default addresses (optional but recommended for prefill): - - `SAFE_SINGLETON` - - `SAFE_PROXY_FACTORY` - - `SAFE_FALLBACK_HANDLER` - - `OG_MASTER_COPY` - - `MODULE_PROXY_FACTORY` - -### Form Fields → On-Chain Parameters - -Use the same inputs as the deploy script; the UI should map them directly to the on-chain deployment parameters: - -- **Safe Owners** → `SAFE_OWNERS` -- **Safe Threshold** → `SAFE_THRESHOLD` -- **Safe Salt Nonce** → `SAFE_SALT_NONCE` -- **OG Collateral Token** → `OG_COLLATERAL` -- **OG Bond Amount** → `OG_BOND_AMOUNT` -- **OG Rules (Natural Language)** → `OG_RULES` -- **OG Challenge Period** → `OG_CHALLENGE_PERIOD` -- **OG Rules URI** → `OG_RULES_URI` -- **OG Salt Nonce** → `OG_SALT_NONCE` -- **Safe Singleton** → `SAFE_SINGLETON` -- **Safe Proxy Factory** → `SAFE_PROXY_FACTORY` -- **Safe Fallback Handler** → `SAFE_FALLBACK_HANDLER` -- **OG Master Copy** → `OG_MASTER_COPY` -- **Module Proxy Factory** → `MODULE_PROXY_FACTORY` - -### Deployment Note - -Build output is static (e.g., `dist/` or `build/`, depending on your frontend tooling) and can be hosted on any static host (Netlify, Vercel static output, S3/CloudFront, etc.). Ensure the RPC URLs and default addresses are configured for the target network before deploying the static bundle. +Environment overrides are minimal today. The UI supports `MODULE_PROXY_FACTORY` (optionally with `VITE_` or `NEXT_PUBLIC_` prefixes). Other defaults are currently hardcoded in `frontend/src/App.jsx` and can be edited there or wired to env vars. ## Example `.env` @@ -142,42 +105,6 @@ SAFE_SINGLETON=0x6666666666666666666666666666666666666666 SAFE_FALLBACK_HANDLER=0x7777777777777777777777777777777777777777 ``` -### Signer Options (CLI Scripts) - -Forge scripts still require a private key env var (e.g., `DEPLOYER_PK`, `PROPOSER_PK`, `EXECUTOR_PK`). If you don't want to store raw keys in `.env`, use `agent/with-signer.mjs` to resolve a signer at runtime and inject the env var: - -```shell -# Private key from env -SIGNER_TYPE=env PRIVATE_KEY=0x... \ - node agent/with-signer.mjs --env DEPLOYER_PK -- \ - forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast - -# Encrypted keystore -SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/deployer.json KEYSTORE_PASSWORD=... \ - node agent/with-signer.mjs --env DEPLOYER_PK -- \ - forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast - -# OS keychain -SIGNER_TYPE=keychain KEYCHAIN_SERVICE=og-deployer KEYCHAIN_ACCOUNT=deployer \ - node agent/with-signer.mjs --env DEPLOYER_PK -- \ - forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast - -# Vault KV (private key stored as a secret) -SIGNER_TYPE=vault VAULT_ADDR=https://vault.example.com VAULT_TOKEN=... VAULT_SECRET_PATH=secret/data/og-deployer \ - node agent/with-signer.mjs --env DEPLOYER_PK -- \ - forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast -``` - -For KMS/Vault signing without exporting private keys, use an RPC signer proxy that exposes `eth_sendTransaction` (set `SIGNER_RPC_URL` and `SIGNER_ADDRESS`). The agent supports this directly (see `agent/README.md`); Forge scripts need a proxy that can export or inject keys. - ## Common Commands ```shell @@ -186,7 +113,7 @@ forge test forge fmt ``` -## Local Testing +## Local Testing (Anvil) Dry-run (no broadcast): @@ -245,23 +172,6 @@ Optional overrides: - `TRANSFER_OPERATION` (default `0` for `CALL`) - `TRANSFER_VALUE` (default `0`) -### Anvil Test Key + USDC Funding (Fork) - -Start Anvil with the default test mnemonic and grab one of the printed private keys: - -```shell -anvil --fork-url $MAINNET_RPC_URL --mnemonic "test test test test test test test test test test test junk" -``` - -Fund the test account with USDC by impersonating a whale on the fork: - -```shell -cast rpc anvil_impersonateAccount -cast rpc anvil_setBalance 0x3635C9ADC5DEA00000 -cast send "transfer(address,uint256)" --from -cast rpc anvil_stopImpersonatingAccount -``` - ## Network Env Files You can keep per-network env files and load them with a tool like `dotenvx` or `direnv`. diff --git a/agent/README.md b/agent/README.md index 78a1bb56..b193b454 100644 --- a/agent/README.md +++ b/agent/README.md @@ -1,6 +1,6 @@ -# Commitment Agent Scaffold +# Oya Commitment Agent -Generic offchain agent wiring for monitoring a commitment and acting through the Optimistic Governor. It exposes only the core tools needed to serve commitments; add commitment-specific logic, prompts, and extra tools as needed. +Generic offchain agent wiring for monitoring an Oya commitment and acting through the Optimistic Governor. It exposes only the core tools needed to serve commitments; add commitment-specific logic, prompts, and extra tools as needed. ## Prerequisites diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..6997713e --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,40 @@ +# Oya Commitments Frontend + +Lightweight web UI for configuring and deploying a commitment (Safe + Optimistic Governor module). It mirrors the Foundry deployment flow and helps craft the same onchain calls from a browser wallet. + +## Prerequisites + +- Node.js 18+ +- npm (or pnpm/yarn) + +## Install & Run + +```shell +npm install +npm run dev +``` + +## Environment Variables + +The app reads env values directly or with `VITE_` / `NEXT_PUBLIC_` prefixes. + +Supported today: + +- `MODULE_PROXY_FACTORY` (optional; overrides the module proxy factory address) + +All other Safe / Optimistic Governor defaults are currently hardcoded in `src/App.jsx` (mainnet defaults). If you want to make those configurable, update the defaults or wire in additional env keys. + +## Build + +```shell +npm run build +npm run preview +``` + +## What It Does + +- Collects Safe + Optimistic Governor parameters (rules, collateral, bond, liveness, addresses). +- Deploys a Safe proxy and an Optimistic Governor module. +- Enables the module on the Safe. + +If you need the CLI-based flow instead, use the Foundry scripts in `script/` from the repo root. From 90cc9c35b0676c4c156f0f28dbaa7e4dfd57f5d1 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 2 Feb 2026 12:36:15 -0800 Subject: [PATCH 021/174] add beta disclaimer Signed-off-by: John Shutt --- README.md | 4 ++++ agent/README.md | 4 ++++ frontend/README.md | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 5837e50e..a024d49d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ This repo contains everything needed to set up **Oya commitments**: smart contracts controlled by natural language rules and the agents that serve them. It includes the Solidity contracts, deployment scripts, an optional web UI, and an offchain agent scaffold. +## Beta Disclaimer + +This is beta software provided “as is.” Use at your own risk. No guarantees of safety, correctness, or fitness for any purpose. + ## What Is a Commitment? A commitment is a Safe controlled by an Optimistic Governor module. The commitment rules are written in natural language (stored onchain or via a URI) and enforced through the Optimistic Governor challenge process. Agents can observe incoming deposits and propose compliant transfers. diff --git a/agent/README.md b/agent/README.md index b193b454..ddc4c129 100644 --- a/agent/README.md +++ b/agent/README.md @@ -2,6 +2,10 @@ Generic offchain agent wiring for monitoring an Oya commitment and acting through the Optimistic Governor. It exposes only the core tools needed to serve commitments; add commitment-specific logic, prompts, and extra tools as needed. +## Beta Disclaimer + +This is beta software provided “as is.” Use at your own risk. No guarantees of safety, correctness, or fitness for any purpose. + ## Prerequisites - Node.js 18+ diff --git a/frontend/README.md b/frontend/README.md index 6997713e..9ad921bd 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -2,6 +2,10 @@ Lightweight web UI for configuring and deploying a commitment (Safe + Optimistic Governor module). It mirrors the Foundry deployment flow and helps craft the same onchain calls from a browser wallet. +## Beta Disclaimer + +This is beta software provided “as is.” Use at your own risk. No guarantees of safety, correctness, or fitness for any purpose. + ## Prerequisites - Node.js 18+ From c6d6affe39ecc76014f8d3e153aff6375999bf75 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 2 Feb 2026 12:38:21 -0800 Subject: [PATCH 022/174] Update commitment definition in README Clarify the role of agents in the commitment process. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a024d49d..28de5a94 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees ## What Is a Commitment? -A commitment is a Safe controlled by an Optimistic Governor module. The commitment rules are written in natural language (stored onchain or via a URI) and enforced through the Optimistic Governor challenge process. Agents can observe incoming deposits and propose compliant transfers. +A commitment is a Safe controlled by an Optimistic Governor module. The commitment rules are written in natural language (stored onchain or via a URI) and enforced through the Optimistic Governor challenge process. Agents (which can be either AI-driven or deterministic) can interpret onchain and offchain signals and propose valid transactions baed on the commitment's rules. ## Concepts (How It Works) From 990a93e51ff464b1fa107ea9558320601b437b6d Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 2 Feb 2026 12:40:06 -0800 Subject: [PATCH 023/174] Clarify offchain agent functionality and community contributions --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 28de5a94..0c8b5383 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis ## Offchain Agent (Serve a Commitment) -The agent in `agent/` can watch deposits and propose or execute transfers via the Optimistic Governor module. It ships with generic tools; customize the decision logic to match your commitment rules. +The agent in `agent/` can propose and execute transactions via the Optimistic Governor module. It ships with generic tools; customize the decision logic, signal monitoring, and overall behavior to match your commitment rules. ```shell cd agent @@ -71,6 +71,8 @@ Built-in tools include: - `makeDeposit` - `pollCommitmentChanges` +We will be building a library of agents showcasing different types of commitments, and welcome community contributions! + ## Web Frontend `frontend/` provides a lightweight UI for entering Safe + Optimistic Governor parameters and generating the same deployment flow as the script. It uses the connected wallet to submit transactions. From 221826b5ce514e82221d2e51c2d2eee147e63c35 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 2 Feb 2026 12:47:31 -0800 Subject: [PATCH 024/174] document alternative signing methods Signed-off-by: John Shutt --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ agent/README.md | 22 ++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/README.md b/README.md index 0c8b5383..87a13122 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,65 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis - `OG_BOND_AMOUNT`: Bond amount for challenges. - `OG_RULES`: Natural language rules for the commitment. +## Alternative Signing Methods + +You can avoid storing raw private keys in `.env` by using the agent’s signer helpers and injecting the key at runtime for Forge scripts. + +Supported signer types: + +- `env`: `PRIVATE_KEY` +- `keystore`: `KEYSTORE_PATH`, `KEYSTORE_PASSWORD` +- `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` +- `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` +- `kms` / `vault-signer` / `rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (RPC signer that accepts `eth_sendTransaction`) + +### Use With Forge Scripts (Deployments + Interactions) + +The `agent/with-signer.mjs` helper resolves a signer and injects it as an env var (e.g., `DEPLOYER_PK`, `PROPOSER_PK`, `EXECUTOR_PK`) for any Forge script. + +```shell +# Private key from env +SIGNER_TYPE=env PRIVATE_KEY=0x... \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# Encrypted keystore +SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/deployer.json KEYSTORE_PASSWORD=... \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# OS keychain +SIGNER_TYPE=keychain KEYCHAIN_SERVICE=og-deployer KEYCHAIN_ACCOUNT=deployer \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# Vault KV (private key stored as a secret) +SIGNER_TYPE=vault VAULT_ADDR=https://vault.example.com VAULT_TOKEN=... VAULT_SECRET_PATH=secret/data/og-deployer \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast +``` + +For interactions, swap the env var: + +```shell +# Propose a transfer with a non-env signer +SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/proposer.json KEYSTORE_PASSWORD=... \ + node agent/with-signer.mjs --env PROPOSER_PK -- \ + forge script script/ProposeCommitmentTransfer.s.sol:ProposeCommitmentTransfer \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast +``` + +Forge scripts still expect a private key env var, so for KMS/Vault signing without exporting private keys you’ll need an RPC signer proxy that can provide `eth_sendTransaction` (set `SIGNER_RPC_URL` and `SIGNER_ADDRESS`). + ## Optional Overrides - `SAFE_SALT_NONCE`, `SAFE_THRESHOLD`, `SAFE_OWNERS` diff --git a/agent/README.md b/agent/README.md index ddc4c129..892ad351 100644 --- a/agent/README.md +++ b/agent/README.md @@ -34,6 +34,28 @@ npm install npm start ``` +## Alternative Signing Methods for Forge Scripts + +You can reuse the agent’s signer helpers to inject a private key env var for Forge scripts without storing raw keys in `.env`. + +```shell +# Private key from env +SIGNER_TYPE=env PRIVATE_KEY=0x... \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# Encrypted keystore +SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/deployer.json KEYSTORE_PASSWORD=... \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast +``` + +For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For signing without exporting a key, use an RPC signer proxy (`SIGNER_RPC_URL`, `SIGNER_ADDRESS`) that supports `eth_sendTransaction`. + ## What the Agent Does - **Polls for deposits**: Checks ERC20 `Transfer` logs into the commitment and (optionally) native balance increases. If nothing changed, no LLM/decision code runs. From 43fe422fcd72f1d0e989cacc884122e3f4f8d945 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 09:35:28 -0800 Subject: [PATCH 025/174] add tool for disputes Signed-off-by: John Shutt --- agent/README.md | 3 + agent/src/index.js | 237 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 226 insertions(+), 14 deletions(-) diff --git a/agent/README.md b/agent/README.md index 892ad351..00fc4669 100644 --- a/agent/README.md +++ b/agent/README.md @@ -26,6 +26,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*` + - Optional disputes: `DISPUTE_ENABLED` (default true), `DISPUTE_RETRY_MS` (default 60000) - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` 2. Install deps and start the loop: @@ -60,6 +61,8 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig - **Polls for deposits**: Checks ERC20 `Transfer` logs into the commitment and (optionally) native balance increases. If nothing changed, no LLM/decision code runs. - **Bonds + proposes**: `postBondAndPropose` approves the OG collateral bond and calls `proposeTransactions` on the module. +- **Monitors proposals**: Watches for Optimistic Governor proposals and routes them to the LLM for rule checks. +- **Disputes assertions**: When the LLM flags a proposal as violating the rules, the agent posts the Oracle V3 bond and disputes the associated assertion. A human-readable rationale is logged locally. - **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. - **Optional LLM decisions**: If `OPENAI_API_KEY` is set, `decideOnSignals` will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions. diff --git a/agent/src/index.js b/agent/src/index.js index e3b0157c..481ec44e 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -9,6 +9,7 @@ import { encodeFunctionData, erc20Abi, getAddress, + hexToString, http, parseAbi, parseAbiItem, @@ -30,7 +31,9 @@ const optimisticGovernorAbi = parseAbi([ ]); const optimisticOracleAbi = parseAbi([ + 'function disputeAssertion(bytes32 assertionId, address disputer)', 'function getMinimumBond(address collateral) view returns (uint256)', + 'function getAssertion(bytes32 assertionId) view returns ((bool arbitrateViaEscalationManager,bool discardOracle,bool validateDisputers,address assertingCaller,address escalationManager) escalationManagerSettings,address asserter,uint64 assertionTime,bool settled,address currency,uint64 expirationTime,bool settlementResolution,bytes32 domainId,bytes32 identifier,uint256 bond,address callbackRecipient,address disputer)', ]); const transferEvent = parseAbiItem( @@ -163,6 +166,11 @@ const config = { ? BigInt(process.env.PROPOSE_GAS_LIMIT) : 2_000_000n, executeRetryMs: Number(process.env.EXECUTE_RETRY_MS ?? 60_000), + disputeEnabled: + process.env.DISPUTE_ENABLED === undefined + ? true + : process.env.DISPUTE_ENABLED.toLowerCase() !== 'false', + disputeRetryMs: Number(process.env.DISPUTE_RETRY_MS ?? 60_000), }; const publicClient = createPublicClient({ transport: http(config.rpcUrl) }); @@ -620,11 +628,11 @@ async function pollProposalChanges() { const latestBlock = await publicClient.getBlockNumber(); if (lastProposalCheckedBlock === undefined) { lastProposalCheckedBlock = latestBlock; - return; + return []; } if (latestBlock <= lastProposalCheckedBlock) { - return; + return []; } const fromBlock = lastProposalCheckedBlock + 1n; @@ -651,12 +659,24 @@ async function pollProposalChanges() { }), ]); + const newProposals = []; for (const log of proposedLogs) { const proposalHash = log.args?.proposalHash; const assertionId = log.args?.assertionId; const proposal = log.args?.proposal; const challengeWindowEnds = log.args?.challengeWindowEnds; if (!proposalHash || !proposal?.transactions) continue; + const proposer = log.args?.proposer; + const explanationHex = log.args?.explanation; + const rules = log.args?.rules; + let explanation; + if (explanationHex && typeof explanationHex === 'string') { + try { + explanation = hexToString(explanationHex); + } catch (error) { + explanation = undefined; + } + } const transactions = proposal.transactions.map((tx) => ({ to: getAddress(tx.to), @@ -665,13 +685,19 @@ async function pollProposalChanges() { data: tx.data ?? '0x', })); - proposalsByHash.set(proposalHash, { + const proposalRecord = { proposalHash, assertionId, + proposer: proposer ? getAddress(proposer) : undefined, challengeWindowEnds: BigInt(challengeWindowEnds ?? 0), transactions, lastAttemptMs: 0, - }); + disputeAttemptMs: 0, + rules, + explanation, + }; + proposalsByHash.set(proposalHash, proposalRecord); + newProposals.push(proposalRecord); } for (const log of executedLogs) { @@ -689,6 +715,7 @@ async function pollProposalChanges() { } lastProposalCheckedBlock = toBlock; + return newProposals; } async function executeReadyProposals() { @@ -754,6 +781,111 @@ async function executeReadyProposals() { } } +async function postBondAndDispute({ assertionId, explanation }) { + if (!config.disputeEnabled) { + throw new Error('Disputes disabled via DISPUTE_ENABLED.'); + } + + if (!ogContext) { + await loadOgContext(); + } + + const proposerBalance = await publicClient.getBalance({ address: account.address }); + if (proposerBalance === 0n) { + throw new Error( + `Disputer ${account.address} has 0 native balance; cannot pay gas to dispute.` + ); + } + + const optimisticOracle = ogContext?.optimisticOracle; + if (!optimisticOracle) { + throw new Error('Missing optimistic oracle address.'); + } + + const assertion = await publicClient.readContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'getAssertion', + args: [assertionId], + }); + + const nowBlock = await publicClient.getBlock(); + const now = BigInt(nowBlock.timestamp); + const expirationTime = BigInt(assertion.expirationTime ?? 0); + const disputer = assertion.disputer ? getAddress(assertion.disputer) : zeroAddress; + const settled = Boolean(assertion.settled); + if (settled) { + throw new Error(`Assertion ${assertionId} already settled.`); + } + if (expirationTime !== 0n && now >= expirationTime) { + throw new Error(`Assertion ${assertionId} expired at ${expirationTime}.`); + } + if (disputer !== zeroAddress) { + throw new Error(`Assertion ${assertionId} already disputed by ${disputer}.`); + } + + const bond = BigInt(assertion.bond ?? 0); + const currency = assertion.currency ? getAddress(assertion.currency) : zeroAddress; + if (currency === zeroAddress) { + throw new Error('Assertion currency is zero address; cannot post bond.'); + } + + if (bond > 0n) { + const collateralBalance = await publicClient.readContract({ + address: currency, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }); + if (collateralBalance < bond) { + throw new Error( + `Insufficient dispute bond balance: need ${bond.toString()} wei, have ${collateralBalance.toString()}.` + ); + } + + const approveHash = await walletClient.writeContract({ + address: currency, + abi: erc20Abi, + functionName: 'approve', + args: [optimisticOracle, bond], + }); + await publicClient.waitForTransactionReceipt({ hash: approveHash }); + } + + let disputeHash; + try { + await publicClient.simulateContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'disputeAssertion', + args: [assertionId, account.address], + account: account.address, + }); + disputeHash = await walletClient.writeContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'disputeAssertion', + args: [assertionId, account.address], + }); + } catch (error) { + const message = error?.shortMessage ?? error?.message ?? String(error); + throw new Error(`Dispute submission failed: ${message}`); + } + + if (explanation) { + console.log(`[agent] Dispute rationale: ${explanation}`); + } + + console.log('[agent] Dispute submitted:', disputeHash); + + return { + disputeHash, + bondAmount: bond, + collateral: currency, + optimisticOracle, + }; +} + async function decideOnSignals(signals) { if (!config.openAiApiKey) { return; @@ -786,10 +918,22 @@ async function decideOnSignals(signals) { async function agentLoop() { try { const signals = await pollCommitmentChanges(); - await pollProposalChanges(); + const proposalSignals = await pollProposalChanges(); + const combinedSignals = signals.concat( + proposalSignals.map((proposal) => ({ + kind: 'proposal', + proposalHash: proposal.proposalHash, + assertionId: proposal.assertionId, + proposer: proposal.proposer, + challengeWindowEnds: proposal.challengeWindowEnds, + transactions: proposal.transactions, + rules: proposal.rules, + explanation: proposal.explanation, + })) + ); - if (signals.length > 0) { - await decideOnSignals(signals); + if (combinedSignals.length > 0) { + await decideOnSignals(combinedSignals); } await executeReadyProposals(); @@ -821,14 +965,32 @@ async function startAgent() { async function callAgent(signals, context) { const systemPrompt = - 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Your own address is provided in the input as agentAddress; use it when rules refer to “the agent/themselves”. Given signals and rules, recommend a course of action. Prefer no-op when unsure. If an onchain action is needed, call a tool. Use build_og_transactions to construct proposal payloads, then post_bond_and_propose. If no action is needed, output strict JSON with keys: action (propose|deposit|ignore|other) and rationale (string).'; + 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Your own address is provided in the input as agentAddress; use it when rules refer to “the agent/themselves”. Given signals and rules, recommend a course of action. Default to disputing proposals that violate the rules; prefer no-op when unsure. If an onchain action is needed, call a tool. Use build_og_transactions to construct proposal payloads, then post_bond_and_propose. Use dispute_assertion with a short human-readable explanation when disputing. If no action is needed, output strict JSON with keys: action (propose|deposit|dispute|ignore|other) and rationale (string).'; - const safeSignals = signals.map((signal) => ({ - ...signal, - amount: signal.amount !== undefined ? signal.amount.toString() : undefined, - blockNumber: signal.blockNumber !== undefined ? signal.blockNumber.toString() : undefined, - transactionHash: signal.transactionHash ? String(signal.transactionHash) : undefined, - })); + const safeSignals = signals.map((signal) => { + if (signal?.kind === 'proposal') { + return { + ...signal, + challengeWindowEnds: + signal.challengeWindowEnds !== undefined + ? signal.challengeWindowEnds.toString() + : undefined, + transactions: Array.isArray(signal.transactions) + ? signal.transactions.map((tx) => ({ + ...tx, + value: tx.value !== undefined ? tx.value.toString() : undefined, + })) + : undefined, + }; + } + + return { + ...signal, + amount: signal.amount !== undefined ? signal.amount.toString() : undefined, + blockNumber: signal.blockNumber !== undefined ? signal.blockNumber.toString() : undefined, + transactionHash: signal.transactionHash ? String(signal.transactionHash) : undefined, + }; + }); const safeContext = { rules: context?.rules, @@ -1066,6 +1228,28 @@ function toolDefinitions() { required: ['transactions'], }, }, + { + type: 'function', + name: 'dispute_assertion', + description: + 'Post bond (if required) and dispute an assertion on the Optimistic Oracle. Provide a short human-readable explanation.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + assertionId: { + type: 'string', + description: 'Assertion ID to dispute.', + }, + explanation: { + type: 'string', + description: 'Short human-readable dispute rationale.', + }, + }, + required: ['assertionId', 'explanation'], + }, + }, ]; } @@ -1133,6 +1317,31 @@ async function executeToolCalls(toolCalls) { continue; } + if (call.name === 'dispute_assertion') { + try { + const result = await postBondAndDispute({ + assertionId: args.assertionId, + explanation: args.explanation, + }); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'submitted', + ...result, + }), + }); + } catch (error) { + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'error', + message: error?.message ?? String(error), + }), + }); + } + continue; + } + console.warn('[agent] Unknown tool call:', call.name); outputs.push({ callId: call.callId, From fe588587ddcd4f345147477ff2e059f4aafd6b21 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 09:49:48 -0800 Subject: [PATCH 026/174] add tests and doc updates for disputes Signed-off-by: John Shutt --- agent/.env.example | 1 + agent/README.md | 22 ++++ agent/scripts/simulate-dispute.mjs | 163 ++++++++++++++++++++++++++ agent/src/index.js | 29 ++++- test/OptimisticOracleV3Dispute.t.sol | 57 +++++++++ test/mocks/MockERC20.sol | 53 +++++++++ test/mocks/MockOptimisticGovernor.sol | 27 +++++ test/mocks/MockOptimisticOracleV3.sol | 80 +++++++++++++ 8 files changed, 430 insertions(+), 2 deletions(-) create mode 100644 agent/scripts/simulate-dispute.mjs create mode 100644 test/OptimisticOracleV3Dispute.t.sol create mode 100644 test/mocks/MockERC20.sol create mode 100644 test/mocks/MockOptimisticGovernor.sol create mode 100644 test/mocks/MockOptimisticOracleV3.sol diff --git a/agent/.env.example b/agent/.env.example index 1a4c7a3e..3da80ac3 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -33,6 +33,7 @@ PRIVATE_KEY=0x... # Optional tuning POLL_INTERVAL_MS=60000 WATCH_NATIVE_BALANCE=true +# DISPUTE_ENABLED=true # START_BLOCK= # DEFAULT_DEPOSIT_ASSET= # DEFAULT_DEPOSIT_AMOUNT_WEI= diff --git a/agent/README.md b/agent/README.md index 00fc4669..dcfae540 100644 --- a/agent/README.md +++ b/agent/README.md @@ -67,3 +67,25 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig - **Optional LLM decisions**: If `OPENAI_API_KEY` is set, `decideOnSignals` will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions. All other behavior is intentionally left out. Implement your own `decideOnSignals` in `src/index.js` to add commitment-specific logic and tool use. + +## Local Dispute Simulation + +Use this to validate the dispute path against local mock contracts. + +```bash +# 1) Start anvil in another terminal +anvil + +# 2) Build the Solidity artifacts (includes mock OO/OG/ERC20) +forge build + +# 3) Run the no-dispute case (assertion remains undisputed) +RPC_URL=http://127.0.0.1:8545 \ +PRIVATE_KEY= \ +node agent/scripts/simulate-dispute.mjs --case=no-dispute + +# 4) Run the dispute case (assertion disputed, bond transferred) +RPC_URL=http://127.0.0.1:8545 \ +PRIVATE_KEY= \ +node agent/scripts/simulate-dispute.mjs --case=dispute +``` diff --git a/agent/scripts/simulate-dispute.mjs b/agent/scripts/simulate-dispute.mjs new file mode 100644 index 00000000..283381c3 --- /dev/null +++ b/agent/scripts/simulate-dispute.mjs @@ -0,0 +1,163 @@ +import 'dotenv/config'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + createPublicClient, + createWalletClient, + getAddress, + http, + parseAbi, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '../..'); + +function mustGetEnv(key) { + const value = process.env[key]; + if (!value) { + throw new Error(`Missing required env var ${key}`); + } + return value; +} + +function loadArtifact(relativePath) { + return readFile(path.join(repoRoot, relativePath), 'utf8').then((raw) => JSON.parse(raw)); +} + +async function deployContract({ walletClient, publicClient, abi, bytecode, args }) { + const hash = await walletClient.deployContract({ + abi, + bytecode, + args, + }); + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + if (!receipt.contractAddress) { + throw new Error('Deployment failed (no contractAddress).'); + } + return receipt.contractAddress; +} + +async function main() { + const rpcUrl = mustGetEnv('RPC_URL'); + const privateKey = mustGetEnv('PRIVATE_KEY'); + const caseArg = process.argv.find((arg) => arg.startsWith('--case=')); + const scenario = caseArg ? caseArg.split('=')[1] : 'dispute'; + if (!['dispute', 'no-dispute'].includes(scenario)) { + throw new Error('Case must be one of: dispute, no-dispute.'); + } + + const account = privateKeyToAccount(privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`); + const publicClient = createPublicClient({ transport: http(rpcUrl) }); + const walletClient = createWalletClient({ account, transport: http(rpcUrl) }); + + const [erc20Artifact, ooArtifact, ogArtifact] = await Promise.all([ + loadArtifact('out/MockERC20.sol/MockERC20.json'), + loadArtifact('out/MockOptimisticOracleV3.sol/MockOptimisticOracleV3.json'), + loadArtifact('out/MockOptimisticGovernor.sol/MockOptimisticGovernor.json'), + ]); + + const erc20 = await deployContract({ + walletClient, + publicClient, + abi: erc20Artifact.abi, + bytecode: erc20Artifact.bytecode.object ?? erc20Artifact.bytecode, + args: ['Bond', 'BOND', 18], + }); + + const oo = await deployContract({ + walletClient, + publicClient, + abi: ooArtifact.abi, + bytecode: ooArtifact.bytecode.object ?? ooArtifact.bytecode, + args: [], + }); + + const og = await deployContract({ + walletClient, + publicClient, + abi: ogArtifact.abi, + bytecode: ogArtifact.bytecode.object ?? ogArtifact.bytecode, + args: [ + getAddress(erc20), + 0n, + getAddress(oo), + 'Mock rules', + '0x' + '11'.repeat(32), + 3600, + ], + }); + + const erc20Client = { + address: erc20, + abi: erc20Artifact.abi, + }; + const ooClient = { + address: oo, + abi: ooArtifact.abi, + }; + + const assertionId = `0x${'aa'.repeat(32)}`; + const now = BigInt((await publicClient.getBlock()).timestamp); + + const mintHash = await walletClient.writeContract({ + ...erc20Client, + functionName: 'mint', + args: [account.address, 1_000_000n], + }); + await publicClient.waitForTransactionReceipt({ hash: mintHash }); + + const assertionHash = await walletClient.writeContract({ + ...ooClient, + functionName: 'setAssertionSimple', + args: [ + assertionId, + account.address, + Number(now), + false, + erc20, + Number(now + 3600n), + '0x' + '22'.repeat(32), + 100_000n, + ], + }); + await publicClient.waitForTransactionReceipt({ hash: assertionHash }); + + const seeded = await publicClient.readContract({ + ...ooClient, + functionName: 'getAssertion', + args: [assertionId], + }); + console.log('[sim] Seeded assertion currency:', seeded.currency); + + process.env.COMMITMENT_SAFE = account.address; + process.env.OG_MODULE = og; + process.env.WATCH_ASSETS = erc20; + process.env.OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? ''; + + const { postBondAndDispute } = await import('../src/index.js'); + + if (scenario === 'dispute') { + await postBondAndDispute({ + assertionId, + explanation: 'Simulation dispute: proposal violates rules.', + }); + } else { + console.log('[sim] No-dispute case: leaving assertion undisputed.'); + } + + const updated = await publicClient.readContract({ + ...ooClient, + functionName: 'getAssertion', + args: [assertionId], + }); + + console.log('[sim] Assertion disputer:', updated.disputer); +} + +main().catch((error) => { + console.error('[sim] failed', error); + process.exit(1); +}); diff --git a/agent/src/index.js b/agent/src/index.js index 481ec44e..32758031 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -802,12 +802,13 @@ async function postBondAndDispute({ assertionId, explanation }) { throw new Error('Missing optimistic oracle address.'); } - const assertion = await publicClient.readContract({ + const assertionRaw = await publicClient.readContract({ address: optimisticOracle, abi: optimisticOracleAbi, functionName: 'getAssertion', args: [assertionId], }); + const assertion = normalizeAssertion(assertionRaw); const nowBlock = await publicClient.getBlock(); const now = BigInt(nowBlock.timestamp); @@ -886,6 +887,30 @@ async function postBondAndDispute({ assertionId, explanation }) { }; } +function normalizeAssertion(assertion) { + if (!assertion) return {}; + if (typeof assertion === 'object' && !Array.isArray(assertion)) { + return assertion; + } + + // viem can return tuple arrays; map indices to named fields. + const tuple = Array.isArray(assertion) ? assertion : []; + return { + escalationManagerSettings: tuple[0], + asserter: tuple[1], + assertionTime: tuple[2], + settled: tuple[3], + currency: tuple[4], + expirationTime: tuple[5], + settlementResolution: tuple[6], + domainId: tuple[7], + identifier: tuple[8], + bond: tuple[9], + callbackRecipient: tuple[10], + disputer: tuple[11], + }; +} + async function decideOnSignals(signals) { if (!config.openAiApiKey) { return; @@ -1481,4 +1506,4 @@ if (import.meta.url === `file://${process.argv[1]}`) { }); } -export { makeDeposit, postBondAndPropose, startAgent }; +export { makeDeposit, postBondAndDispute, postBondAndPropose, startAgent }; diff --git a/test/OptimisticOracleV3Dispute.t.sol b/test/OptimisticOracleV3Dispute.t.sol new file mode 100644 index 00000000..7d95bd14 --- /dev/null +++ b/test/OptimisticOracleV3Dispute.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "./mocks/MockERC20.sol"; +import "./mocks/MockOptimisticOracleV3.sol"; + +contract OptimisticOracleV3DisputeTest is Test { + MockERC20 private token; + MockOptimisticOracleV3 private oracle; + address private disputer; + + function setUp() public { + token = new MockERC20("Bond", "BOND", 18); + oracle = new MockOptimisticOracleV3(); + disputer = address(0xB00D); + token.mint(disputer, 1_000 ether); + } + + function test_DisputeRequiresBondApproval() public { + bytes32 assertionId = keccak256("assertion"); + + MockOptimisticOracleV3.Assertion memory assertion = MockOptimisticOracleV3.Assertion({ + escalationManagerSettings: MockOptimisticOracleV3.EscalationManagerSettings({ + arbitrateViaEscalationManager: false, + discardOracle: false, + validateDisputers: false, + assertingCaller: address(0), + escalationManager: address(0) + }), + asserter: address(0xA11CE), + assertionTime: uint64(block.timestamp), + settled: false, + currency: token, + expirationTime: uint64(block.timestamp + 1 days), + settlementResolution: false, + domainId: bytes32(0), + identifier: bytes32("ASSERT_TRUTH"), + bond: 100 ether, + callbackRecipient: address(0), + disputer: address(0) + }); + + oracle.setAssertion(assertionId, assertion); + + vm.startPrank(disputer); + vm.expectRevert("allowance"); + oracle.disputeAssertion(assertionId, disputer); + + token.approve(address(oracle), 100 ether); + oracle.disputeAssertion(assertionId, disputer); + vm.stopPrank(); + + MockOptimisticOracleV3.Assertion memory updated = oracle.getAssertion(assertionId); + assertEq(updated.disputer, disputer); + } +} diff --git a/test/mocks/MockERC20.sol b/test/mocks/MockERC20.sol new file mode 100644 index 00000000..ceb9a179 --- /dev/null +++ b/test/mocks/MockERC20.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract MockERC20 { + string public name; + string public symbol; + uint8 public immutable decimals; + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + } + + function mint(address to, uint256 amount) external { + totalSupply += amount; + balanceOf[to] += amount; + emit Transfer(address(0), to, amount); + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transfer(address to, uint256 amount) external returns (bool) { + _transfer(msg.sender, to, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + require(allowed >= amount, "allowance"); + allowance[from][msg.sender] = allowed - amount; + _transfer(from, to, amount); + return true; + } + + function _transfer(address from, address to, uint256 amount) internal { + require(balanceOf[from] >= amount, "balance"); + balanceOf[from] -= amount; + balanceOf[to] += amount; + emit Transfer(from, to, amount); + } +} diff --git a/test/mocks/MockOptimisticGovernor.sol b/test/mocks/MockOptimisticGovernor.sol new file mode 100644 index 00000000..17abe65f --- /dev/null +++ b/test/mocks/MockOptimisticGovernor.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract MockOptimisticGovernor { + address public collateral; + uint256 public bondAmount; + address public optimisticOracleV3; + string public rules; + bytes32 public identifier; + uint64 public liveness; + + constructor( + address _collateral, + uint256 _bondAmount, + address _optimisticOracleV3, + string memory _rules, + bytes32 _identifier, + uint64 _liveness + ) { + collateral = _collateral; + bondAmount = _bondAmount; + optimisticOracleV3 = _optimisticOracleV3; + rules = _rules; + identifier = _identifier; + liveness = _liveness; + } +} diff --git a/test/mocks/MockOptimisticOracleV3.sol b/test/mocks/MockOptimisticOracleV3.sol new file mode 100644 index 00000000..99748cea --- /dev/null +++ b/test/mocks/MockOptimisticOracleV3.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./MockERC20.sol"; + +contract MockOptimisticOracleV3 { + struct EscalationManagerSettings { + bool arbitrateViaEscalationManager; + bool discardOracle; + bool validateDisputers; + address assertingCaller; + address escalationManager; + } + + struct Assertion { + EscalationManagerSettings escalationManagerSettings; + address asserter; + uint64 assertionTime; + bool settled; + MockERC20 currency; + uint64 expirationTime; + bool settlementResolution; + bytes32 domainId; + bytes32 identifier; + uint256 bond; + address callbackRecipient; + address disputer; + } + + mapping(bytes32 => Assertion) private assertions; + + event AssertionDisputed(bytes32 indexed assertionId, address indexed caller, address indexed disputer); + + function setAssertion(bytes32 assertionId, Assertion memory assertion) external { + assertions[assertionId] = assertion; + } + + function setAssertionSimple( + bytes32 assertionId, + address asserter, + uint64 assertionTime, + bool settled, + address currency, + uint64 expirationTime, + bytes32 identifier, + uint256 bond + ) external { + Assertion storage assertion = assertions[assertionId]; + assertion.asserter = asserter; + assertion.assertionTime = assertionTime; + assertion.settled = settled; + assertion.currency = MockERC20(currency); + assertion.expirationTime = expirationTime; + assertion.identifier = identifier; + assertion.bond = bond; + assertion.domainId = bytes32(0); + assertion.callbackRecipient = address(0); + assertion.disputer = address(0); + } + + function getAssertion(bytes32 assertionId) external view returns (Assertion memory) { + return assertions[assertionId]; + } + + function disputeAssertion(bytes32 assertionId, address disputer) external { + Assertion storage assertion = assertions[assertionId]; + require(assertion.asserter != address(0), "missing"); + require(!assertion.settled, "settled"); + require(assertion.disputer == address(0), "already-disputed"); + require(block.timestamp < assertion.expirationTime, "expired"); + + if (assertion.bond > 0) { + bool success = assertion.currency.transferFrom(msg.sender, address(this), assertion.bond); + require(success, "bond-transfer"); + } + + assertion.disputer = disputer; + emit AssertionDisputed(assertionId, msg.sender, disputer); + } +} From 7ae3a70344293b06f9ee784dfc4f54e2e42e5f44 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 09:54:12 -0800 Subject: [PATCH 027/174] add PROPOSE_ENABLED flag Signed-off-by: John Shutt --- agent/.env.example | 1 + agent/README.md | 1 + agent/src/index.js | 61 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/agent/.env.example b/agent/.env.example index 3da80ac3..28c2c9e8 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -33,6 +33,7 @@ PRIVATE_KEY=0x... # Optional tuning POLL_INTERVAL_MS=60000 WATCH_NATIVE_BALANCE=true +# PROPOSE_ENABLED=true # DISPUTE_ENABLED=true # START_BLOCK= # DEFAULT_DEPOSIT_ASSET= diff --git a/agent/README.md b/agent/README.md index dcfae540..89d57466 100644 --- a/agent/README.md +++ b/agent/README.md @@ -26,6 +26,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*` + - Optional proposals: `PROPOSE_ENABLED` (default true) - Optional disputes: `DISPUTE_ENABLED` (default true), `DISPUTE_RETRY_MS` (default 60000) - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` 2. Install deps and start the loop: diff --git a/agent/src/index.js b/agent/src/index.js index 32758031..0cb25663 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -166,6 +166,10 @@ const config = { ? BigInt(process.env.PROPOSE_GAS_LIMIT) : 2_000_000n, executeRetryMs: Number(process.env.EXECUTE_RETRY_MS ?? 60_000), + proposeEnabled: + process.env.PROPOSE_ENABLED === undefined + ? true + : process.env.PROPOSE_ENABLED.toLowerCase() !== 'false', disputeEnabled: process.env.DISPUTE_ENABLED === undefined ? true @@ -357,6 +361,10 @@ async function primeBalances(blockNumber) { } async function postBondAndPropose(transactions) { + if (!config.proposeEnabled) { + throw new Error('Proposals disabled via PROPOSE_ENABLED.'); + } + const normalizedTransactions = normalizeOgTransactions(transactions); const proposerBalance = await publicClient.getBalance({ address: account.address }); const [collateral, bondAmount, optimisticOracle] = await Promise.all([ @@ -916,6 +924,11 @@ async function decideOnSignals(signals) { return; } + if (!config.proposeEnabled && !config.disputeEnabled) { + console.log('[agent] Proposals and disputes are disabled; skipping onchain actions.'); + return; + } + if (!ogContext) { await loadOgContext(); } @@ -1126,7 +1139,7 @@ function extractToolCalls(responseJson) { } function toolDefinitions() { - return [ + const tools = [ { type: 'function', name: 'build_og_transactions', @@ -1223,7 +1236,10 @@ function toolDefinitions() { required: ['asset', 'amountWei'], }, }, - { + ]; + + if (config.proposeEnabled) { + tools.push({ type: 'function', name: 'post_bond_and_propose', description: @@ -1252,8 +1268,11 @@ function toolDefinitions() { }, required: ['transactions'], }, - }, - { + }); + } + + if (config.disputeEnabled) { + tools.push({ type: 'function', name: 'dispute_assertion', description: @@ -1274,8 +1293,10 @@ function toolDefinitions() { }, required: ['assertionId', 'explanation'], }, - }, - ]; + }); + } + + return tools; } async function executeToolCalls(toolCalls) { @@ -1325,6 +1346,17 @@ async function executeToolCalls(toolCalls) { } if (call.name === 'post_bond_and_propose') { + if (!config.proposeEnabled) { + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'skipped', + reason: 'proposals disabled', + }), + }); + continue; + } + const transactions = args.transactions.map((tx) => ({ to: getAddress(tx.to), value: BigInt(tx.value), @@ -1343,6 +1375,17 @@ async function executeToolCalls(toolCalls) { } if (call.name === 'dispute_assertion') { + if (!config.disputeEnabled) { + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'skipped', + reason: 'disputes disabled', + }), + }); + continue; + } + try { const result = await postBondAndDispute({ assertionId: args.assertionId, @@ -1374,7 +1417,11 @@ async function executeToolCalls(toolCalls) { }); } if (builtTransactions && !hasPostProposal) { - const result = await postBondAndPropose(builtTransactions); + if (!config.proposeEnabled) { + console.log('[agent] Built transactions but proposals are disabled; skipping propose.'); + } else { + await postBondAndPropose(builtTransactions); + } } return outputs.filter((item) => item.callId); } From 95efab4d962eb8b6a6ef32c6e4f8e7cf717af17d Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 09:55:45 -0800 Subject: [PATCH 028/174] add readme note about agent modes Signed-off-by: John Shutt --- agent/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/agent/README.md b/agent/README.md index 89d57466..e4146101 100644 --- a/agent/README.md +++ b/agent/README.md @@ -69,6 +69,14 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig All other behavior is intentionally left out. Implement your own `decideOnSignals` in `src/index.js` to add commitment-specific logic and tool use. +### Propose vs Dispute Modes + +Set `PROPOSE_ENABLED` and `DISPUTE_ENABLED` to control behavior: +- Both true: propose and dispute as needed (default). +- Only `PROPOSE_ENABLED=true`: propose only, never dispute. +- Only `DISPUTE_ENABLED=true`: dispute only, never propose. +- Both false: monitor and log opinions only; no on-chain actions. + ## Local Dispute Simulation Use this to validate the dispute path against local mock contracts. From 87c7c4e1bb62b6aefab02a371f3e424c6d2c9d94 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 10:01:29 -0800 Subject: [PATCH 029/174] resolve explanation string Signed-off-by: John Shutt --- agent/src/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 0cb25663..1cd057b5 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -679,10 +679,14 @@ async function pollProposalChanges() { const rules = log.args?.rules; let explanation; if (explanationHex && typeof explanationHex === 'string') { - try { - explanation = hexToString(explanationHex); - } catch (error) { - explanation = undefined; + if (explanationHex.startsWith('0x')) { + try { + explanation = hexToString(explanationHex); + } catch (error) { + explanation = undefined; + } + } else { + explanation = explanationHex; } } From 83b5c8aefc7a451d31a54ce2e16efca4dcb15110 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 10:10:20 -0800 Subject: [PATCH 030/174] rephrase natural language to plain language Signed-off-by: John Shutt --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 87a13122..e787462e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Oya Commitments Monorepo -This repo contains everything needed to set up **Oya commitments**: smart contracts controlled by natural language rules and the agents that serve them. It includes the Solidity contracts, deployment scripts, an optional web UI, and an offchain agent scaffold. +This repo contains everything needed to set up **Oya commitments**: smart contracts controlled by plain language rules and the agents that serve them. It includes the Solidity contracts, deployment scripts, an optional web UI, and an offchain agent scaffold. ## Beta Disclaimer @@ -8,11 +8,11 @@ This is beta software provided “as is.” Use at your own risk. No guarantees ## What Is a Commitment? -A commitment is a Safe controlled by an Optimistic Governor module. The commitment rules are written in natural language (stored onchain or via a URI) and enforced through the Optimistic Governor challenge process. Agents (which can be either AI-driven or deterministic) can interpret onchain and offchain signals and propose valid transactions baed on the commitment's rules. +A commitment is a Safe controlled by an Optimistic Governor module. The commitment rules are written in plain language (stored onchain or via a URI) and enforced through the Optimistic Governor challenge process. Agents (which can be either AI-driven or deterministic) can interpret onchain and offchain signals and propose valid transactions baed on the commitment's rules. ## Concepts (How It Works) -1. **Rules**: You define natural language rules for what the commitment may do. +1. **Rules**: You define plain language rules for what the commitment may do. 2. **Control**: A Safe is deployed and wired to an Optimistic Governor module with those rules. 3. **Proposals**: An agent (or user) proposes transfers via the module and posts the bond. 4. **Challenge Window**: If no one challenges during the period, the proposal can be executed. @@ -45,7 +45,7 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis - `DEPLOYER_PK`: Private key for the deployer. - `OG_COLLATERAL`: Address of the ERC20 collateral token. - `OG_BOND_AMOUNT`: Bond amount for challenges. -- `OG_RULES`: Natural language rules for the commitment. +- `OG_RULES`: Plain language rules for the commitment. ## Alternative Signing Methods From 5027bf5b242c4b83e490c1a4806603e88b51996c Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 10:30:24 -0800 Subject: [PATCH 031/174] edit readme title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e787462e..94bc64ba 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Oya Commitments Monorepo +# Oya Commitments This repo contains everything needed to set up **Oya commitments**: smart contracts controlled by plain language rules and the agents that serve them. It includes the Solidity contracts, deployment scripts, an optional web UI, and an offchain agent scaffold. From 5d8fb322a1b21216f8cab901808800fee27dc7a3 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 10:34:46 -0800 Subject: [PATCH 032/174] refactor agent to prepare for a multiagent library Signed-off-by: John Shutt --- agent-library/README.md | 12 + agent-library/agents/default/agent.js | 26 + agent-library/agents/default/commitment.txt | 1 + agent-library/package.json | 3 + agent/.env.example | 1 + agent/README.md | 17 +- agent/scripts/validate-agent.mjs | 42 + agent/src/index.js | 1596 ++----------------- agent/src/lib/config.js | 44 + agent/src/lib/llm.js | 187 +++ agent/src/lib/og.js | 181 +++ agent/src/lib/polling.js | 266 ++++ agent/src/lib/signer.js | 129 ++ agent/src/lib/tools.js | 324 ++++ agent/src/lib/tx.js | 405 +++++ agent/src/lib/utils.js | 58 + 16 files changed, 1821 insertions(+), 1471 deletions(-) create mode 100644 agent-library/README.md create mode 100644 agent-library/agents/default/agent.js create mode 100644 agent-library/agents/default/commitment.txt create mode 100644 agent-library/package.json create mode 100644 agent/scripts/validate-agent.mjs create mode 100644 agent/src/lib/config.js create mode 100644 agent/src/lib/llm.js create mode 100644 agent/src/lib/og.js create mode 100644 agent/src/lib/polling.js create mode 100644 agent/src/lib/signer.js create mode 100644 agent/src/lib/tools.js create mode 100644 agent/src/lib/tx.js create mode 100644 agent/src/lib/utils.js diff --git a/agent-library/README.md b/agent-library/README.md new file mode 100644 index 00000000..8d75051b --- /dev/null +++ b/agent-library/README.md @@ -0,0 +1,12 @@ +# Agent Library + +Each agent lives under `agent-library/agents//` and must include: +- `agent.js`: decision logic and prompt construction. +- `commitment.txt`: plain language commitment that the agent is designed to serve. + +The runner loads the agent module via `AGENT_MODULE` (relative to repo root) and reads the adjacent `commitment.txt`. + +To add a new agent: +1. Copy `agent-library/agents/default/` to a new folder. +2. Update `agent.js` and `commitment.txt`. +3. Set `AGENT_MODULE=agent-library/agents//agent.js`. diff --git a/agent-library/agents/default/agent.js b/agent-library/agents/default/agent.js new file mode 100644 index 00000000..377b37aa --- /dev/null +++ b/agent-library/agents/default/agent.js @@ -0,0 +1,26 @@ +function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { + const mode = proposeEnabled && disputeEnabled + ? 'You may propose and dispute.' + : proposeEnabled + ? 'You may propose but you may not dispute.' + : disputeEnabled + ? 'You may dispute but you may not propose.' + : 'You may not propose or dispute; provide opinions only.'; + + return [ + 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor).', + 'Your own address is provided in the input as agentAddress; use it when rules refer to “the agent/themselves”.', + 'Given signals and rules, recommend a course of action.', + 'Default to disputing proposals that violate the rules; prefer no-op when unsure.', + mode, + commitmentText ? `Commitment text:\n${commitmentText}` : '', + 'If an onchain action is needed, call a tool.', + 'Use build_og_transactions to construct proposal payloads, then post_bond_and_propose.', + 'Use dispute_assertion with a short human-readable explanation when disputing.', + 'If no action is needed, output strict JSON with keys: action (propose|deposit|dispute|ignore|other) and rationale (string).', + ] + .filter(Boolean) + .join(' '); +} + +export { getSystemPrompt }; diff --git a/agent-library/agents/default/commitment.txt b/agent-library/agents/default/commitment.txt new file mode 100644 index 00000000..e9027f25 --- /dev/null +++ b/agent-library/agents/default/commitment.txt @@ -0,0 +1 @@ +Any assets deposited in this Commitment may be transferred back to the depositor before January 15th, 2026 (12:00AM PST). After the deadline, assets may only be transferred to jdshutt.eth. If a third party is initiating the transfer after the deadline, they may take a 10% cut of the assets being transferred as a fee. diff --git a/agent-library/package.json b/agent-library/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/agent-library/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/agent/.env.example b/agent/.env.example index 28c2c9e8..9c8e5ac1 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -38,6 +38,7 @@ WATCH_NATIVE_BALANCE=true # START_BLOCK= # DEFAULT_DEPOSIT_ASSET= # DEFAULT_DEPOSIT_AMOUNT_WEI= +# AGENT_MODULE=agent-library/agents/default/agent.js # Optional LLM # OPENAI_API_KEY= diff --git a/agent/README.md b/agent/README.md index e4146101..cf580259 100644 --- a/agent/README.md +++ b/agent/README.md @@ -25,7 +25,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` (macOS Keychain or Linux Secret Service) - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*` + - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE` - Optional proposals: `PROPOSE_ENABLED` (default true) - Optional disputes: `DISPUTE_ENABLED` (default true), `DISPUTE_RETRY_MS` (default 60000) - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` @@ -65,9 +65,9 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig - **Monitors proposals**: Watches for Optimistic Governor proposals and routes them to the LLM for rule checks. - **Disputes assertions**: When the LLM flags a proposal as violating the rules, the agent posts the Oracle V3 bond and disputes the associated assertion. A human-readable rationale is logged locally. - **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. -- **Optional LLM decisions**: If `OPENAI_API_KEY` is set, `decideOnSignals` will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions. +- **Optional LLM decisions**: If `OPENAI_API_KEY` is set, the runner will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions in the agent module. -All other behavior is intentionally left out. Implement your own `decideOnSignals` in `src/index.js` to add commitment-specific logic and tool use. +All other behavior is intentionally left out. Implement your own agent in `agent-library/agents//agent.js` to add commitment-specific logic and tool use. ### Propose vs Dispute Modes @@ -77,6 +77,17 @@ Set `PROPOSE_ENABLED` and `DISPUTE_ENABLED` to control behavior: - Only `DISPUTE_ENABLED=true`: dispute only, never propose. - Both false: monitor and log opinions only; no on-chain actions. +### Agent Modules & Commitments + +Use `AGENT_MODULE` to point to an agent implementation under `agent-library/agents//agent.js`. +Each agent directory must include a `commitment.txt` with the plain language commitment the agent is designed to serve. + +You can validate a module quickly: + +```bash +node agent/scripts/validate-agent.mjs --module=agent-library/agents/default/agent.js +``` + ## Local Dispute Simulation Use this to validate the dispute path against local mock contracts. diff --git a/agent/scripts/validate-agent.mjs b/agent/scripts/validate-agent.mjs new file mode 100644 index 00000000..c7eb8b35 --- /dev/null +++ b/agent/scripts/validate-agent.mjs @@ -0,0 +1,42 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +function getArgValue(prefix) { + const arg = process.argv.find((value) => value.startsWith(prefix)); + return arg ? arg.slice(prefix.length) : null; +} + +async function main() { + const moduleArg = getArgValue('--module='); + const modulePath = moduleArg ?? process.env.AGENT_MODULE ?? 'agent-library/agents/default/agent.js'; + const resolvedPath = path.resolve(process.cwd(), modulePath); + + const agentModule = await import(pathToFileURL(resolvedPath).href); + if (typeof agentModule.getSystemPrompt !== 'function') { + throw new Error('Agent module must export getSystemPrompt().'); + } + + const commitmentPath = path.join(path.dirname(resolvedPath), 'commitment.txt'); + const commitmentText = (await readFile(commitmentPath, 'utf8')).trim(); + if (!commitmentText) { + throw new Error('commitment.txt is missing or empty.'); + } + + const prompt = agentModule.getSystemPrompt({ + proposeEnabled: true, + disputeEnabled: true, + commitmentText, + }); + if (!prompt || typeof prompt !== 'string') { + throw new Error('getSystemPrompt() must return a non-empty string.'); + } + + console.log('[agent] Agent module OK:', resolvedPath); + console.log('[agent] commitment.txt length:', commitmentText.length); +} + +main().catch((error) => { + console.error('[agent] validation failed:', error.message ?? error); + process.exit(1); +}); diff --git a/agent/src/index.js b/agent/src/index.js index 1cd057b5..204121c9 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -1,237 +1,31 @@ -import 'dotenv/config'; +import dotenv from 'dotenv'; import { readFile } from 'node:fs/promises'; -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; -import { Wallet } from 'ethers'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { createPublicClient, http } from 'viem'; +import { buildConfig } from './lib/config.js'; +import { createSignerClient } from './lib/signer.js'; import { - createPublicClient, - createWalletClient, - encodeFunctionData, - erc20Abi, - getAddress, - hexToString, - http, - parseAbi, - parseAbiItem, - stringToHex, - zeroAddress, -} from 'viem'; -import { privateKeyToAccount, toAccount } from 'viem/accounts'; - -const optimisticGovernorAbi = parseAbi([ - 'function proposeTransactions((address to,uint8 operation,uint256 value,bytes data)[] transactions, bytes explanation)', - 'function executeProposal((address to,uint8 operation,uint256 value,bytes data)[] transactions)', - 'function collateral() view returns (address)', - 'function bondAmount() view returns (uint256)', - 'function optimisticOracleV3() view returns (address)', - 'function rules() view returns (string)', - 'function identifier() view returns (bytes32)', - 'function liveness() view returns (uint64)', - 'function assertionIds(bytes32) view returns (bytes32)', -]); - -const optimisticOracleAbi = parseAbi([ - 'function disputeAssertion(bytes32 assertionId, address disputer)', - 'function getMinimumBond(address collateral) view returns (uint256)', - 'function getAssertion(bytes32 assertionId) view returns ((bool arbitrateViaEscalationManager,bool discardOracle,bool validateDisputers,address assertingCaller,address escalationManager) escalationManagerSettings,address asserter,uint64 assertionTime,bool settled,address currency,uint64 expirationTime,bool settlementResolution,bytes32 domainId,bytes32 identifier,uint256 bond,address callbackRecipient,address disputer)', -]); - -const transferEvent = parseAbiItem( - 'event Transfer(address indexed from, address indexed to, uint256 value)' -); -const transactionsProposedEvent = parseAbiItem( - 'event TransactionsProposed(address indexed proposer,uint256 indexed proposalTime,bytes32 indexed assertionId,((address to,uint8 operation,uint256 value,bytes data)[] transactions,uint256 requestTime) proposal,bytes32 proposalHash,bytes explanation,string rules,uint256 challengeWindowEnds)' -); -const proposalExecutedEvent = parseAbiItem( - 'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' -); -const proposalDeletedEvent = parseAbiItem( - 'event ProposalDeleted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' -); - -function mustGetEnv(key) { - const value = process.env[key]; - if (!value) { - throw new Error(`Missing required env var ${key}`); - } - - return value; -} - -function normalizePrivateKey(value) { - if (!value) return value; - return value.startsWith('0x') ? value : `0x${value}`; -} - -const execFileAsync = promisify(execFile); - -async function loadPrivateKeyFromKeystore() { - const keystorePath = mustGetEnv('KEYSTORE_PATH'); - const keystorePassword = mustGetEnv('KEYSTORE_PASSWORD'); - const keystoreJson = await readFile(keystorePath, 'utf8'); - const wallet = await Wallet.fromEncryptedJson(keystoreJson, keystorePassword); - return wallet.privateKey; -} - -async function loadPrivateKeyFromKeychain() { - const service = mustGetEnv('KEYCHAIN_SERVICE'); - const account = mustGetEnv('KEYCHAIN_ACCOUNT'); - - if (process.platform === 'darwin') { - const { stdout } = await execFileAsync('security', [ - 'find-generic-password', - '-s', - service, - '-a', - account, - '-w', - ]); - return stdout.trim(); - } - - if (process.platform === 'linux') { - const { stdout } = await execFileAsync('secret-tool', [ - 'lookup', - 'service', - service, - 'account', - account, - ]); - return stdout.trim(); - } - - throw new Error('Keychain lookup not supported on this platform.'); -} - -async function loadPrivateKeyFromVault() { - const vaultAddr = mustGetEnv('VAULT_ADDR').replace(/\/+$/, ''); - const vaultToken = mustGetEnv('VAULT_TOKEN'); - const vaultPath = mustGetEnv('VAULT_SECRET_PATH').replace(/^\/+/, ''); - const vaultNamespace = process.env.VAULT_NAMESPACE; - const vaultKeyField = process.env.VAULT_SECRET_KEY ?? 'private_key'; - - const response = await fetch(`${vaultAddr}/v1/${vaultPath}`, { - headers: { - 'X-Vault-Token': vaultToken, - ...(vaultNamespace ? { 'X-Vault-Namespace': vaultNamespace } : {}), - }, - }); - - if (!response.ok) { - throw new Error(`Vault request failed (${response.status}).`); - } - - const payload = await response.json(); - const data = payload?.data?.data ?? payload?.data ?? {}; - const value = data[vaultKeyField]; - if (!value) { - throw new Error(`Vault secret missing key '${vaultKeyField}'.`); - } - - return value; -} - -function parseAddressList(list) { - if (!list) return []; - return list - .split(',') - .map((value) => value.trim()) - .filter(Boolean) - .map(getAddress); -} - -const config = { - rpcUrl: mustGetEnv('RPC_URL'), - commitmentSafe: getAddress(mustGetEnv('COMMITMENT_SAFE')), - ogModule: getAddress(mustGetEnv('OG_MODULE')), - pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 10_000), - startBlock: process.env.START_BLOCK ? BigInt(process.env.START_BLOCK) : undefined, - watchAssets: parseAddressList(process.env.WATCH_ASSETS), - watchNativeBalance: - process.env.WATCH_NATIVE_BALANCE === undefined - ? true - : process.env.WATCH_NATIVE_BALANCE.toLowerCase() !== 'false', - defaultDepositAsset: process.env.DEFAULT_DEPOSIT_ASSET - ? getAddress(process.env.DEFAULT_DEPOSIT_ASSET) - : undefined, - defaultDepositAmountWei: process.env.DEFAULT_DEPOSIT_AMOUNT_WEI - ? BigInt(process.env.DEFAULT_DEPOSIT_AMOUNT_WEI) - : undefined, - bondSpender: (process.env.BOND_SPENDER ?? 'og').toLowerCase(), - openAiApiKey: process.env.OPENAI_API_KEY, - openAiModel: process.env.OPENAI_MODEL ?? 'gpt-4.1-mini', - openAiBaseUrl: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1', - allowProposeOnSimulationFail: true, - proposeGasLimit: process.env.PROPOSE_GAS_LIMIT - ? BigInt(process.env.PROPOSE_GAS_LIMIT) - : 2_000_000n, - executeRetryMs: Number(process.env.EXECUTE_RETRY_MS ?? 60_000), - proposeEnabled: - process.env.PROPOSE_ENABLED === undefined - ? true - : process.env.PROPOSE_ENABLED.toLowerCase() !== 'false', - disputeEnabled: - process.env.DISPUTE_ENABLED === undefined - ? true - : process.env.DISPUTE_ENABLED.toLowerCase() !== 'false', - disputeRetryMs: Number(process.env.DISPUTE_RETRY_MS ?? 60_000), -}; - + loadOgContext, + loadOptimisticGovernorDefaults, + logOgFundingStatus, +} from './lib/og.js'; +import { + executeReadyProposals, + pollCommitmentChanges, + pollProposalChanges, + primeBalances, +} from './lib/polling.js'; +import { callAgent, explainToolCalls } from './lib/llm.js'; +import { executeToolCalls, toolDefinitions } from './lib/tools.js'; +import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; + +dotenv.config(); +dotenv.config({ path: path.resolve(process.cwd(), 'agent/.env') }); + +const config = buildConfig(); const publicClient = createPublicClient({ transport: http(config.rpcUrl) }); - -async function createSignerClient() { - const signerType = (process.env.SIGNER_TYPE ?? 'env').toLowerCase(); - - if (signerType === 'env') { - const privateKey = normalizePrivateKey(mustGetEnv('PRIVATE_KEY')); - const account = privateKeyToAccount(privateKey); - return { - account, - walletClient: createWalletClient({ account, transport: http(config.rpcUrl) }), - }; - } - - if (signerType === 'keystore') { - const privateKey = normalizePrivateKey(await loadPrivateKeyFromKeystore()); - const account = privateKeyToAccount(privateKey); - return { - account, - walletClient: createWalletClient({ account, transport: http(config.rpcUrl) }), - }; - } - - if (signerType === 'keychain') { - const privateKey = normalizePrivateKey(await loadPrivateKeyFromKeychain()); - const account = privateKeyToAccount(privateKey); - return { - account, - walletClient: createWalletClient({ account, transport: http(config.rpcUrl) }), - }; - } - - if (signerType === 'vault') { - const privateKey = normalizePrivateKey(await loadPrivateKeyFromVault()); - const account = privateKeyToAccount(privateKey); - return { - account, - walletClient: createWalletClient({ account, transport: http(config.rpcUrl) }), - }; - } - - if (['kms', 'vault-signer', 'signer-rpc', 'rpc', 'json-rpc'].includes(signerType)) { - const signerRpcUrl = mustGetEnv('SIGNER_RPC_URL'); - const signerAddress = getAddress(mustGetEnv('SIGNER_ADDRESS')); - const account = toAccount(signerAddress); - return { - account, - walletClient: createWalletClient({ account, transport: http(signerRpcUrl) }), - }; - } - - throw new Error(`Unsupported SIGNER_TYPE '${signerType}'.`); -} - -const { account, walletClient } = await createSignerClient(); +const { account, walletClient } = await createSignerClient({ rpcUrl: config.rpcUrl }); const agentAddress = account.address; const trackedAssets = new Set(config.watchAssets); @@ -240,718 +34,93 @@ let lastProposalCheckedBlock = config.startBlock; let lastNativeBalance; let ogContext; const proposalsByHash = new Map(); -const zeroBytes32 = `0x${'0'.repeat(64)}`; - -async function loadOptimisticGovernorDefaults() { - const collateral = await publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'collateral', - }); - trackedAssets.add(getAddress(collateral)); -} - -async function loadOgContext() { - const [collateral, bondAmount, optimisticOracle, rules, identifier, liveness] = await Promise.all([ - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'collateral', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'bondAmount', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'optimisticOracleV3', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'rules', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'identifier', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'liveness', - }), - ]); - - ogContext = { - collateral, - bondAmount, - optimisticOracle, - rules, - identifier, - liveness, - }; -} +async function loadAgentModule() { + const modulePath = config.agentModule ?? 'agent-library/agents/default/agent.js'; + const resolvedPath = path.resolve(process.cwd(), modulePath); + const moduleUrl = pathToFileURL(resolvedPath).href; + const agentModule = await import(moduleUrl); -async function logOgFundingStatus() { + const commitmentPath = path.join(path.dirname(resolvedPath), 'commitment.txt'); + let commitmentText = ''; try { - const chainId = await publicClient.getChainId(); - const expectedIdentifierStr = - chainId === 11155111 ? 'ASSERT_TRUTH' : 'ASSERT_TRUTH2'; - const expectedIdentifier = stringToHex(expectedIdentifierStr, { size: 32 }); - - const [collateral, bondAmount, optimisticOracle, identifier] = await Promise.all([ - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'collateral', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'bondAmount', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'optimisticOracleV3', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'identifier', - }), - ]); - const minimumBond = await publicClient.readContract({ - address: optimisticOracle, - abi: optimisticOracleAbi, - functionName: 'getMinimumBond', - args: [collateral], - }); - - const requiredBond = bondAmount > minimumBond ? bondAmount : minimumBond; - const collateralBalance = await publicClient.readContract({ - address: collateral, - abi: erc20Abi, - functionName: 'balanceOf', - args: [account.address], - }); - const nativeBalance = await publicClient.getBalance({ address: account.address }); - - if (identifier !== expectedIdentifier) { - console.warn( - `[agent] OG identifier mismatch: expected ${expectedIdentifierStr}, onchain ${identifier}` - ); - } + commitmentText = (await readFile(commitmentPath, 'utf8')).trim(); } catch (error) { - console.warn('[agent] Failed to log OG funding status:', error); + console.warn('[agent] Missing commitment.txt next to agent module:', commitmentPath); } -} -async function primeBalances(blockNumber) { - if (!config.watchNativeBalance) return; - - lastNativeBalance = await publicClient.getBalance({ - address: config.commitmentSafe, - blockNumber, - }); + return { agentModule, commitmentText, resolvedPath }; } -async function postBondAndPropose(transactions) { - if (!config.proposeEnabled) { - throw new Error('Proposals disabled via PROPOSE_ENABLED.'); - } +const { agentModule, commitmentText } = await loadAgentModule(); - const normalizedTransactions = normalizeOgTransactions(transactions); - const proposerBalance = await publicClient.getBalance({ address: account.address }); - const [collateral, bondAmount, optimisticOracle] = await Promise.all([ - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'collateral', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'bondAmount', - }), - publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'optimisticOracleV3', - }), - ]); - let minimumBond = 0n; - try { - minimumBond = await publicClient.readContract({ - address: optimisticOracle, - abi: optimisticOracleAbi, - functionName: 'getMinimumBond', - args: [collateral], - }); - } catch (error) { - console.warn('[agent] Failed to fetch minimum bond from optimistic oracle:', error); +async function decideOnSignals(signals) { + if (!config.openAiApiKey) { + return; } - const requiredBond = bondAmount > minimumBond ? bondAmount : minimumBond; - - if (requiredBond > 0n) { - const collateralBalance = await publicClient.readContract({ - address: collateral, - abi: erc20Abi, - functionName: 'balanceOf', - args: [account.address], + if (!ogContext) { + ogContext = await loadOgContext({ + publicClient, + ogModule: config.ogModule, }); - if (collateralBalance < requiredBond) { - throw new Error( - `Insufficient bond collateral balance: need ${requiredBond.toString()} wei, have ${collateralBalance.toString()}.` - ); - } - const spenders = []; - if (config.bondSpender === 'og' || config.bondSpender === 'both') { - spenders.push(config.ogModule); - } - if (config.bondSpender === 'oo' || config.bondSpender === 'both') { - spenders.push(optimisticOracle); - } - - for (const spender of spenders) { - const approveHash = await walletClient.writeContract({ - address: collateral, - abi: erc20Abi, - functionName: 'approve', - args: [spender, requiredBond], - }); - await publicClient.waitForTransactionReceipt({ hash: approveHash }); - const allowance = await publicClient.readContract({ - address: collateral, - abi: erc20Abi, - functionName: 'allowance', - args: [account.address, spender], - }); - if (allowance < requiredBond) { - throw new Error( - `Insufficient bond allowance: need ${requiredBond.toString()} wei, have ${allowance.toString()} for spender ${spender}.` - ); - } - } - } - - if (proposerBalance === 0n) { - throw new Error( - `Proposer ${account.address} has 0 native balance; cannot pay gas to propose.` - ); } - let proposalHash; - const explanation = 'Agent serving Oya commitment.'; - const explanationBytes = stringToHex(explanation); - const proposalData = encodeFunctionData({ - abi: optimisticGovernorAbi, - functionName: 'proposeTransactions', - args: [normalizedTransactions, explanationBytes], - }); - let simulationError; - let submissionError; - try { - await publicClient.simulateContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'proposeTransactions', - args: [normalizedTransactions, explanationBytes], - account: account.address, - }); - } catch (error) { - simulationError = error; - if (!config.allowProposeOnSimulationFail) { - throw error; - } - console.warn('[agent] Simulation failed; attempting to propose anyway.'); - } + const systemPrompt = + agentModule?.getSystemPrompt?.({ + proposeEnabled: config.proposeEnabled, + disputeEnabled: config.disputeEnabled, + commitmentText, + }) ?? + 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor).'; try { - if (simulationError) { - proposalHash = await walletClient.sendTransaction({ - account, - to: config.ogModule, - data: proposalData, - value: 0n, - gas: config.proposeGasLimit, - }); - } else { - proposalHash = await walletClient.writeContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'proposeTransactions', - args: [normalizedTransactions, explanationBytes], - }); - } - } catch (error) { - submissionError = error; - const message = - error?.shortMessage ?? - error?.message ?? - simulationError?.shortMessage ?? - simulationError?.message ?? - String(error ?? simulationError); - console.warn('[agent] Propose submission failed:', message); - } - - if (proposalHash) { - console.log('[agent] Proposal submitted:', proposalHash); - } - - return { - proposalHash, - bondAmount, - collateral, - optimisticOracle, - submissionError: submissionError ? summarizeViemError(submissionError) : null, - }; -} - -function normalizeOgTransactions(transactions) { - if (!Array.isArray(transactions)) { - throw new Error('transactions must be an array'); - } - - return transactions.map((tx, index) => { - if (!tx || !tx.to) { - throw new Error(`transactions[${index}] missing to`); - } - - return { - to: getAddress(tx.to), - value: BigInt(tx.value ?? 0), - data: tx.data ?? '0x', - operation: Number(tx.operation ?? 0), - }; - }); -} - -function summarizeViemError(error) { - if (!error) return null; - - return { - name: error.name, - shortMessage: error.shortMessage, - message: error.message, - details: error.details, - metaMessages: error.metaMessages, - data: error.data ?? error.cause?.data, - cause: error.cause?.shortMessage ?? error.cause?.message ?? error.cause, - }; -} - -async function makeDeposit({ asset, amountWei }) { - const depositAsset = asset ? getAddress(asset) : config.defaultDepositAsset; - const depositAmount = - amountWei !== undefined ? amountWei : config.defaultDepositAmountWei; - - if (!depositAsset || depositAmount === undefined) { - throw new Error('Deposit requires asset and amount (wei).'); - } - - if (depositAsset === zeroAddress) { - return walletClient.sendTransaction({ - account, - to: config.commitmentSafe, - value: BigInt(depositAmount), + const tools = toolDefinitions({ + proposeEnabled: config.proposeEnabled, + disputeEnabled: config.disputeEnabled, }); - } - - return walletClient.writeContract({ - address: depositAsset, - abi: erc20Abi, - functionName: 'transfer', - args: [config.commitmentSafe, BigInt(depositAmount)], - }); -} - -async function pollCommitmentChanges() { - const latestBlock = await publicClient.getBlockNumber(); - if (lastCheckedBlock === undefined) { - lastCheckedBlock = latestBlock; - await primeBalances(latestBlock); - return []; - } - - if (latestBlock <= lastCheckedBlock) { - return []; - } - - const fromBlock = lastCheckedBlock + 1n; - const toBlock = latestBlock; - const deposits = []; - - for (const asset of trackedAssets) { - const logs = await publicClient.getLogs({ - address: asset, - event: transferEvent, - args: { to: config.commitmentSafe }, - fromBlock, - toBlock, + const allowTools = config.proposeEnabled || config.disputeEnabled; + const decision = await callAgent({ + config, + systemPrompt, + signals, + ogContext, + commitmentText, + agentAddress, + tools, + allowTools, }); - for (const log of logs) { - deposits.push({ - kind: 'erc20Deposit', - asset, - from: log.args.from, - amount: log.args.value, - blockNumber: log.blockNumber, - transactionHash: log.transactionHash, - }); - } - } - - if (config.watchNativeBalance) { - const nativeBalance = await publicClient.getBalance({ - address: config.commitmentSafe, - blockNumber: toBlock, - }); - - if (lastNativeBalance !== undefined && nativeBalance > lastNativeBalance) { - deposits.push({ - kind: 'nativeDeposit', - asset: zeroAddress, - from: 'unknown', - amount: nativeBalance - lastNativeBalance, - blockNumber: toBlock, - transactionHash: undefined, - }); - } - - lastNativeBalance = nativeBalance; - } - - lastCheckedBlock = toBlock; - return deposits; -} - -async function pollProposalChanges() { - const latestBlock = await publicClient.getBlockNumber(); - if (lastProposalCheckedBlock === undefined) { - lastProposalCheckedBlock = latestBlock; - return []; - } - - if (latestBlock <= lastProposalCheckedBlock) { - return []; - } - - const fromBlock = lastProposalCheckedBlock + 1n; - const toBlock = latestBlock; - - const [proposedLogs, executedLogs, deletedLogs] = await Promise.all([ - publicClient.getLogs({ - address: config.ogModule, - event: transactionsProposedEvent, - fromBlock, - toBlock, - }), - publicClient.getLogs({ - address: config.ogModule, - event: proposalExecutedEvent, - fromBlock, - toBlock, - }), - publicClient.getLogs({ - address: config.ogModule, - event: proposalDeletedEvent, - fromBlock, - toBlock, - }), - ]); - - const newProposals = []; - for (const log of proposedLogs) { - const proposalHash = log.args?.proposalHash; - const assertionId = log.args?.assertionId; - const proposal = log.args?.proposal; - const challengeWindowEnds = log.args?.challengeWindowEnds; - if (!proposalHash || !proposal?.transactions) continue; - const proposer = log.args?.proposer; - const explanationHex = log.args?.explanation; - const rules = log.args?.rules; - let explanation; - if (explanationHex && typeof explanationHex === 'string') { - if (explanationHex.startsWith('0x')) { - try { - explanation = hexToString(explanationHex); - } catch (error) { - explanation = undefined; - } - } else { - explanation = explanationHex; - } - } - - const transactions = proposal.transactions.map((tx) => ({ - to: getAddress(tx.to), - operation: Number(tx.operation ?? 0), - value: BigInt(tx.value ?? 0), - data: tx.data ?? '0x', - })); - - const proposalRecord = { - proposalHash, - assertionId, - proposer: proposer ? getAddress(proposer) : undefined, - challengeWindowEnds: BigInt(challengeWindowEnds ?? 0), - transactions, - lastAttemptMs: 0, - disputeAttemptMs: 0, - rules, - explanation, - }; - proposalsByHash.set(proposalHash, proposalRecord); - newProposals.push(proposalRecord); - } - - for (const log of executedLogs) { - const proposalHash = log.args?.proposalHash; - if (proposalHash) { - proposalsByHash.delete(proposalHash); - } - } - - for (const log of deletedLogs) { - const proposalHash = log.args?.proposalHash; - if (proposalHash) { - proposalsByHash.delete(proposalHash); - } - } - - lastProposalCheckedBlock = toBlock; - return newProposals; -} - -async function executeReadyProposals() { - if (proposalsByHash.size === 0) return; - - const latestBlock = await publicClient.getBlockNumber(); - const block = await publicClient.getBlock({ blockNumber: latestBlock }); - const now = BigInt(block.timestamp); - const nowMs = Date.now(); - - for (const proposal of proposalsByHash.values()) { - if (!proposal?.transactions?.length) continue; - if (proposal.challengeWindowEnds === undefined) continue; - if (now < proposal.challengeWindowEnds) continue; - if (proposal.lastAttemptMs && nowMs - proposal.lastAttemptMs < config.executeRetryMs) { - continue; - } - - proposal.lastAttemptMs = nowMs; - - let assertionId; - try { - assertionId = await publicClient.readContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'assertionIds', - args: [proposal.proposalHash], - }); - } catch (error) { - console.warn('[agent] Failed to read assertionId:', error); - continue; - } - - if (!assertionId || assertionId === zeroBytes32) { - proposalsByHash.delete(proposal.proposalHash); - continue; - } - - try { - await publicClient.simulateContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'executeProposal', - args: [proposal.transactions], - account: account.address, - }); - } catch (error) { - console.warn('[agent] Proposal not executable yet:', proposal.proposalHash); - continue; - } - - try { - const txHash = await walletClient.writeContract({ - address: config.ogModule, - abi: optimisticGovernorAbi, - functionName: 'executeProposal', - args: [proposal.transactions], - }); - console.log('[agent] Proposal execution submitted:', txHash); - } catch (error) { - console.warn('[agent] Proposal execution failed:', error?.shortMessage ?? error?.message ?? error); - } - } -} - -async function postBondAndDispute({ assertionId, explanation }) { - if (!config.disputeEnabled) { - throw new Error('Disputes disabled via DISPUTE_ENABLED.'); - } - - if (!ogContext) { - await loadOgContext(); - } - - const proposerBalance = await publicClient.getBalance({ address: account.address }); - if (proposerBalance === 0n) { - throw new Error( - `Disputer ${account.address} has 0 native balance; cannot pay gas to dispute.` - ); - } - - const optimisticOracle = ogContext?.optimisticOracle; - if (!optimisticOracle) { - throw new Error('Missing optimistic oracle address.'); - } - - const assertionRaw = await publicClient.readContract({ - address: optimisticOracle, - abi: optimisticOracleAbi, - functionName: 'getAssertion', - args: [assertionId], - }); - const assertion = normalizeAssertion(assertionRaw); - - const nowBlock = await publicClient.getBlock(); - const now = BigInt(nowBlock.timestamp); - const expirationTime = BigInt(assertion.expirationTime ?? 0); - const disputer = assertion.disputer ? getAddress(assertion.disputer) : zeroAddress; - const settled = Boolean(assertion.settled); - if (settled) { - throw new Error(`Assertion ${assertionId} already settled.`); - } - if (expirationTime !== 0n && now >= expirationTime) { - throw new Error(`Assertion ${assertionId} expired at ${expirationTime}.`); - } - if (disputer !== zeroAddress) { - throw new Error(`Assertion ${assertionId} already disputed by ${disputer}.`); - } - - const bond = BigInt(assertion.bond ?? 0); - const currency = assertion.currency ? getAddress(assertion.currency) : zeroAddress; - if (currency === zeroAddress) { - throw new Error('Assertion currency is zero address; cannot post bond.'); - } - - if (bond > 0n) { - const collateralBalance = await publicClient.readContract({ - address: currency, - abi: erc20Abi, - functionName: 'balanceOf', - args: [account.address], - }); - if (collateralBalance < bond) { - throw new Error( - `Insufficient dispute bond balance: need ${bond.toString()} wei, have ${collateralBalance.toString()}.` - ); + if (!allowTools && decision?.textDecision) { + console.log('[agent] Opinion:', decision.textDecision); + return; } - const approveHash = await walletClient.writeContract({ - address: currency, - abi: erc20Abi, - functionName: 'approve', - args: [optimisticOracle, bond], - }); - await publicClient.waitForTransactionReceipt({ hash: approveHash }); - } - - let disputeHash; - try { - await publicClient.simulateContract({ - address: optimisticOracle, - abi: optimisticOracleAbi, - functionName: 'disputeAssertion', - args: [assertionId, account.address], - account: account.address, - }); - disputeHash = await walletClient.writeContract({ - address: optimisticOracle, - abi: optimisticOracleAbi, - functionName: 'disputeAssertion', - args: [assertionId, account.address], - }); - } catch (error) { - const message = error?.shortMessage ?? error?.message ?? String(error); - throw new Error(`Dispute submission failed: ${message}`); - } - - if (explanation) { - console.log(`[agent] Dispute rationale: ${explanation}`); - } - - console.log('[agent] Dispute submitted:', disputeHash); - - return { - disputeHash, - bondAmount: bond, - collateral: currency, - optimisticOracle, - }; -} - -function normalizeAssertion(assertion) { - if (!assertion) return {}; - if (typeof assertion === 'object' && !Array.isArray(assertion)) { - return assertion; - } - - // viem can return tuple arrays; map indices to named fields. - const tuple = Array.isArray(assertion) ? assertion : []; - return { - escalationManagerSettings: tuple[0], - asserter: tuple[1], - assertionTime: tuple[2], - settled: tuple[3], - currency: tuple[4], - expirationTime: tuple[5], - settlementResolution: tuple[6], - domainId: tuple[7], - identifier: tuple[8], - bond: tuple[9], - callbackRecipient: tuple[10], - disputer: tuple[11], - }; -} - -async function decideOnSignals(signals) { - if (!config.openAiApiKey) { - return; - } - - if (!config.proposeEnabled && !config.disputeEnabled) { - console.log('[agent] Proposals and disputes are disabled; skipping onchain actions.'); - return; - } - - if (!ogContext) { - await loadOgContext(); - } - - try { - const decision = await callAgent(signals, ogContext); if (decision.toolCalls.length > 0) { - const toolOutputs = await executeToolCalls(decision.toolCalls); + const toolOutputs = await executeToolCalls({ + toolCalls: decision.toolCalls, + publicClient, + walletClient, + account, + config, + ogContext, + }); if (decision.responseId && toolOutputs.length > 0) { - const explanation = await explainToolCalls( - decision.responseId, - toolOutputs - ); + const explanation = await explainToolCalls({ + config, + previousResponseId: decision.responseId, + toolOutputs, + }); if (explanation) { console.log('[agent] Agent explanation:', explanation); } } return; } + + if (decision?.textDecision) { + console.log('[agent] Decision:', decision.textDecision); + } } catch (error) { console.error('[agent] Agent call failed', error); } @@ -959,10 +128,29 @@ async function decideOnSignals(signals) { async function agentLoop() { try { - const signals = await pollCommitmentChanges(); - const proposalSignals = await pollProposalChanges(); - const combinedSignals = signals.concat( - proposalSignals.map((proposal) => ({ + const { deposits, lastCheckedBlock: nextCheckedBlock, lastNativeBalance: nextNative } = + await pollCommitmentChanges({ + publicClient, + trackedAssets, + commitmentSafe: config.commitmentSafe, + watchNativeBalance: config.watchNativeBalance, + lastCheckedBlock, + lastNativeBalance, + }); + lastCheckedBlock = nextCheckedBlock; + lastNativeBalance = nextNative; + + const { newProposals, lastProposalCheckedBlock: nextProposalBlock } = + await pollProposalChanges({ + publicClient, + ogModule: config.ogModule, + lastProposalCheckedBlock, + proposalsByHash, + }); + lastProposalCheckedBlock = nextProposalBlock; + + const combinedSignals = deposits.concat( + newProposals.map((proposal) => ({ kind: 'proposal', proposalHash: proposal.proposalHash, assertionId: proposal.assertionId, @@ -978,7 +166,14 @@ async function agentLoop() { await decideOnSignals(combinedSignals); } - await executeReadyProposals(); + await executeReadyProposals({ + publicClient, + walletClient, + account, + ogModule: config.ogModule, + proposalsByHash, + executeRetryMs: config.executeRetryMs, + }); } catch (error) { console.error('[agent] loop error', error); } @@ -987,9 +182,14 @@ async function agentLoop() { } async function startAgent() { - await loadOptimisticGovernorDefaults(); - await loadOgContext(); - await logOgFundingStatus(); + await loadOptimisticGovernorDefaults({ + publicClient, + ogModule: config.ogModule, + trackedAssets, + }); + + ogContext = await loadOgContext({ publicClient, ogModule: config.ogModule }); + await logOgFundingStatus({ publicClient, ogModule: config.ogModule, account }); if (lastCheckedBlock === undefined) { lastCheckedBlock = await publicClient.getBlockNumber(); @@ -998,558 +198,18 @@ async function startAgent() { lastProposalCheckedBlock = lastCheckedBlock; } - await primeBalances(lastCheckedBlock); + lastNativeBalance = await primeBalances({ + publicClient, + commitmentSafe: config.commitmentSafe, + watchNativeBalance: config.watchNativeBalance, + blockNumber: lastCheckedBlock, + }); console.log('[agent] running...'); agentLoop(); } -async function callAgent(signals, context) { - const systemPrompt = - 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor). Your own address is provided in the input as agentAddress; use it when rules refer to “the agent/themselves”. Given signals and rules, recommend a course of action. Default to disputing proposals that violate the rules; prefer no-op when unsure. If an onchain action is needed, call a tool. Use build_og_transactions to construct proposal payloads, then post_bond_and_propose. Use dispute_assertion with a short human-readable explanation when disputing. If no action is needed, output strict JSON with keys: action (propose|deposit|dispute|ignore|other) and rationale (string).'; - - const safeSignals = signals.map((signal) => { - if (signal?.kind === 'proposal') { - return { - ...signal, - challengeWindowEnds: - signal.challengeWindowEnds !== undefined - ? signal.challengeWindowEnds.toString() - : undefined, - transactions: Array.isArray(signal.transactions) - ? signal.transactions.map((tx) => ({ - ...tx, - value: tx.value !== undefined ? tx.value.toString() : undefined, - })) - : undefined, - }; - } - - return { - ...signal, - amount: signal.amount !== undefined ? signal.amount.toString() : undefined, - blockNumber: signal.blockNumber !== undefined ? signal.blockNumber.toString() : undefined, - transactionHash: signal.transactionHash ? String(signal.transactionHash) : undefined, - }; - }); - - const safeContext = { - rules: context?.rules, - identifier: context?.identifier ? String(context.identifier) : undefined, - liveness: context?.liveness !== undefined ? context.liveness.toString() : undefined, - collateral: context?.collateral, - bondAmount: context?.bondAmount !== undefined ? context.bondAmount.toString() : undefined, - optimisticOracle: context?.optimisticOracle, - }; - - const payload = { - model: config.openAiModel, - input: [ - { - role: 'system', - content: systemPrompt, - }, - { - role: 'user', - content: JSON.stringify({ - commitmentSafe: config.commitmentSafe, - ogModule: config.ogModule, - agentAddress, - ogContext: safeContext, - signals: safeSignals, - }), - }, - ], - tools: toolDefinitions(), - tool_choice: 'auto', - parallel_tool_calls: false, - text: { format: { type: 'json_object' } }, - }; - - const res = await fetch(`${config.openAiBaseUrl}/responses`, { - method: 'POST', - headers: { - Authorization: `Bearer ${config.openAiApiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`OpenAI API error: ${res.status} ${text}`); - } - - const json = await res.json(); - const toolCalls = extractToolCalls(json); - const raw = extractFirstText(json); - let textDecision; - if (raw) { - try { - textDecision = JSON.parse(raw); - } catch (error) { - throw new Error(`Failed to parse OpenAI JSON: ${raw}`); - } - } - - return { toolCalls, textDecision, responseId: json?.id }; -} - -function extractFirstText(responseJson) { - // Responses API structure: output -> [{ content: [{ type: 'output_text', text: '...' }, ...] }, ...] - const outputs = responseJson?.output; - if (!Array.isArray(outputs)) return ''; - - for (const item of outputs) { - if (!item?.content) continue; - for (const chunk of item.content) { - if (chunk?.text) return chunk.text; - if (chunk?.output_text) return chunk.output_text?.text ?? ''; - if (chunk?.text?.value) return chunk.text.value; // older shape - } - } - - return ''; -} - -function extractToolCalls(responseJson) { - const outputs = responseJson?.output; - if (!Array.isArray(outputs)) return []; - - const toolCalls = []; - for (const item of outputs) { - if (item?.type === 'tool_call' || item?.type === 'function_call') { - toolCalls.push({ - name: item?.name ?? item?.function?.name, - arguments: item?.arguments ?? item?.function?.arguments, - callId: item?.call_id ?? item?.id, - }); - continue; - } - - if (Array.isArray(item?.tool_calls)) { - for (const call of item.tool_calls) { - toolCalls.push({ - name: call?.name ?? call?.function?.name, - arguments: call?.arguments ?? call?.function?.arguments, - callId: call?.call_id ?? call?.id, - }); - } - } - } - - return toolCalls.filter((call) => call.name); -} - -function toolDefinitions() { - const tools = [ - { - type: 'function', - name: 'build_og_transactions', - description: - 'Build Optimistic Governor transaction payloads from high-level intents. Returns array of {to,value,data,operation} with value as string wei.', - strict: true, - parameters: { - type: 'object', - additionalProperties: false, - properties: { - actions: { - type: 'array', - items: { - type: 'object', - additionalProperties: false, - properties: { - kind: { - type: 'string', - description: - 'Action type: erc20_transfer | native_transfer | contract_call', - }, - token: { - type: ['string', 'null'], - description: - 'ERC20 token address for erc20_transfer.', - }, - to: { - type: ['string', 'null'], - description: 'Recipient or target contract address.', - }, - amountWei: { - type: ['string', 'null'], - description: - 'Amount in wei as a string. For erc20_transfer and native_transfer.', - }, - valueWei: { - type: ['string', 'null'], - description: - 'ETH value to send in contract_call (default 0).', - }, - abi: { - type: ['string', 'null'], - description: - 'Function signature for contract_call, e.g. "setOwner(address)".', - }, - args: { - type: ['array', 'null'], - description: - 'Arguments for contract_call in order, JSON-serializable.', - items: { type: 'string' }, - }, - operation: { - type: ['integer', 'null'], - description: - 'Safe operation (0=CALL,1=DELEGATECALL). Defaults to 0.', - }, - }, - required: [ - 'kind', - 'token', - 'to', - 'amountWei', - 'valueWei', - 'abi', - 'args', - 'operation', - ], - }, - }, - }, - required: ['actions'], - }, - }, - { - type: 'function', - name: 'make_deposit', - description: - 'Deposit funds into the commitment Safe. Use asset=0x000...000 for native ETH. amountWei must be a string of the integer wei amount.', - strict: true, - parameters: { - type: 'object', - additionalProperties: false, - properties: { - asset: { - type: 'string', - description: - 'Asset address (ERC20) or 0x0000000000000000000000000000000000000000 for native.', - }, - amountWei: { - type: 'string', - description: 'Amount in wei as a string.', - }, - }, - required: ['asset', 'amountWei'], - }, - }, - ]; - - if (config.proposeEnabled) { - tools.push({ - type: 'function', - name: 'post_bond_and_propose', - description: - 'Post bond (if required) and propose transactions to the Optimistic Governor.', - strict: true, - parameters: { - type: 'object', - additionalProperties: false, - properties: { - transactions: { - type: 'array', - description: - 'Safe transaction batch to propose. Use value as string wei.', - items: { - type: 'object', - additionalProperties: false, - properties: { - to: { type: 'string' }, - value: { type: 'string' }, - data: { type: 'string' }, - operation: { type: 'integer' }, - }, - required: ['to', 'value', 'data', 'operation'], - }, - }, - }, - required: ['transactions'], - }, - }); - } - - if (config.disputeEnabled) { - tools.push({ - type: 'function', - name: 'dispute_assertion', - description: - 'Post bond (if required) and dispute an assertion on the Optimistic Oracle. Provide a short human-readable explanation.', - strict: true, - parameters: { - type: 'object', - additionalProperties: false, - properties: { - assertionId: { - type: 'string', - description: 'Assertion ID to dispute.', - }, - explanation: { - type: 'string', - description: 'Short human-readable dispute rationale.', - }, - }, - required: ['assertionId', 'explanation'], - }, - }); - } - - return tools; -} - -async function executeToolCalls(toolCalls) { - const outputs = []; - const hasPostProposal = toolCalls.some((call) => call.name === 'post_bond_and_propose'); - let builtTransactions; - for (const call of toolCalls) { - const args = parseToolArguments(call.arguments); - if (!args) { - console.warn('[agent] Skipping tool call with invalid args:', call); - continue; - } - - if (call.name === 'build_og_transactions') { - try { - const transactions = buildOgTransactions(args.actions ?? []); - builtTransactions = transactions; - outputs.push({ - callId: call.callId, - output: JSON.stringify({ status: 'ok', transactions }), - }); - } catch (error) { - outputs.push({ - callId: call.callId, - output: JSON.stringify({ - status: 'error', - message: error?.message ?? String(error), - }), - }); - } - continue; - } - - if (call.name === 'make_deposit') { - const txHash = await makeDeposit({ - asset: args.asset, - amountWei: BigInt(args.amountWei), - }); - outputs.push({ - callId: call.callId, - output: JSON.stringify({ - status: 'submitted', - transactionHash: String(txHash), - }), - }); - continue; - } - - if (call.name === 'post_bond_and_propose') { - if (!config.proposeEnabled) { - outputs.push({ - callId: call.callId, - output: JSON.stringify({ - status: 'skipped', - reason: 'proposals disabled', - }), - }); - continue; - } - - const transactions = args.transactions.map((tx) => ({ - to: getAddress(tx.to), - value: BigInt(tx.value), - data: tx.data, - operation: Number(tx.operation), - })); - const result = await postBondAndPropose(transactions); - outputs.push({ - callId: call.callId, - output: JSON.stringify({ - status: 'submitted', - ...result, - }), - }); - continue; - } - - if (call.name === 'dispute_assertion') { - if (!config.disputeEnabled) { - outputs.push({ - callId: call.callId, - output: JSON.stringify({ - status: 'skipped', - reason: 'disputes disabled', - }), - }); - continue; - } - - try { - const result = await postBondAndDispute({ - assertionId: args.assertionId, - explanation: args.explanation, - }); - outputs.push({ - callId: call.callId, - output: JSON.stringify({ - status: 'submitted', - ...result, - }), - }); - } catch (error) { - outputs.push({ - callId: call.callId, - output: JSON.stringify({ - status: 'error', - message: error?.message ?? String(error), - }), - }); - } - continue; - } - - console.warn('[agent] Unknown tool call:', call.name); - outputs.push({ - callId: call.callId, - output: JSON.stringify({ status: 'skipped', reason: 'unknown tool' }), - }); - } - if (builtTransactions && !hasPostProposal) { - if (!config.proposeEnabled) { - console.log('[agent] Built transactions but proposals are disabled; skipping propose.'); - } else { - await postBondAndPropose(builtTransactions); - } - } - return outputs.filter((item) => item.callId); -} - -function buildOgTransactions(actions) { - if (!Array.isArray(actions) || actions.length === 0) { - throw new Error('actions must be a non-empty array'); - } - - return actions.map((action) => { - const operation = action.operation !== undefined ? Number(action.operation) : 0; - - if (action.kind === 'erc20_transfer') { - if (!action.token || !action.to || action.amountWei === undefined) { - throw new Error('erc20_transfer requires token, to, amountWei'); - } - - const data = encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [getAddress(action.to), BigInt(action.amountWei)], - }); - - return { - to: getAddress(action.token), - value: '0', - data, - operation, - }; - } - - if (action.kind === 'native_transfer') { - if (!action.to || action.amountWei === undefined) { - throw new Error('native_transfer requires to, amountWei'); - } - - return { - to: getAddress(action.to), - value: BigInt(action.amountWei).toString(), - data: '0x', - operation, - }; - } - - if (action.kind === 'contract_call') { - if (!action.to || !action.abi) { - throw new Error('contract_call requires to, abi'); - } - - const abi = parseAbi([`function ${action.abi}`]); - const args = Array.isArray(action.args) ? action.args : []; - const data = encodeFunctionData({ - abi, - functionName: action.abi.split('(')[0], - args, - }); - const value = action.valueWei !== undefined ? BigInt(action.valueWei).toString() : '0'; - - return { - to: getAddress(action.to), - value, - data, - operation, - }; - } - - throw new Error(`Unknown action kind: ${action.kind}`); - }); -} - -function parseToolArguments(raw) { - if (!raw) return null; - if (typeof raw === 'object') return raw; - if (typeof raw === 'string') { - try { - return JSON.parse(raw); - } catch (error) { - return null; - } - } - return null; -} - -async function explainToolCalls(previousResponseId, toolOutputs) { - const input = [ - ...toolOutputs.map((item) => ({ - type: 'function_call_output', - call_id: item.callId, - output: item.output, - })), - { - type: 'message', - role: 'user', - content: [ - { - type: 'input_text', - text: 'Summarize the actions you took and why.', - }, - ], - }, - ]; - - const res = await fetch(`${config.openAiBaseUrl}/responses`, { - method: 'POST', - headers: { - Authorization: `Bearer ${config.openAiApiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model: config.openAiModel, - previous_response_id: previousResponseId, - input, - }), - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`OpenAI API error: ${res.status} ${text}`); - } - - const json = await res.json(); - return extractFirstText(json); -} - if (import.meta.url === `file://${process.argv[1]}`) { startAgent().catch((error) => { console.error('[agent] failed to start', error); diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js new file mode 100644 index 00000000..e0ec9feb --- /dev/null +++ b/agent/src/lib/config.js @@ -0,0 +1,44 @@ +import { getAddress } from 'viem'; +import { mustGetEnv, parseAddressList } from './utils.js'; + +function buildConfig() { + return { + rpcUrl: mustGetEnv('RPC_URL'), + commitmentSafe: getAddress(mustGetEnv('COMMITMENT_SAFE')), + ogModule: getAddress(mustGetEnv('OG_MODULE')), + pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? 10_000), + startBlock: process.env.START_BLOCK ? BigInt(process.env.START_BLOCK) : undefined, + watchAssets: parseAddressList(process.env.WATCH_ASSETS), + watchNativeBalance: + process.env.WATCH_NATIVE_BALANCE === undefined + ? true + : process.env.WATCH_NATIVE_BALANCE.toLowerCase() !== 'false', + defaultDepositAsset: process.env.DEFAULT_DEPOSIT_ASSET + ? getAddress(process.env.DEFAULT_DEPOSIT_ASSET) + : undefined, + defaultDepositAmountWei: process.env.DEFAULT_DEPOSIT_AMOUNT_WEI + ? BigInt(process.env.DEFAULT_DEPOSIT_AMOUNT_WEI) + : undefined, + bondSpender: (process.env.BOND_SPENDER ?? 'og').toLowerCase(), + openAiApiKey: process.env.OPENAI_API_KEY, + openAiModel: process.env.OPENAI_MODEL ?? 'gpt-4.1-mini', + openAiBaseUrl: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1', + allowProposeOnSimulationFail: true, + proposeGasLimit: process.env.PROPOSE_GAS_LIMIT + ? BigInt(process.env.PROPOSE_GAS_LIMIT) + : 2_000_000n, + executeRetryMs: Number(process.env.EXECUTE_RETRY_MS ?? 60_000), + proposeEnabled: + process.env.PROPOSE_ENABLED === undefined + ? true + : process.env.PROPOSE_ENABLED.toLowerCase() !== 'false', + disputeEnabled: + process.env.DISPUTE_ENABLED === undefined + ? true + : process.env.DISPUTE_ENABLED.toLowerCase() !== 'false', + disputeRetryMs: Number(process.env.DISPUTE_RETRY_MS ?? 60_000), + agentModule: process.env.AGENT_MODULE, + }; +} + +export { buildConfig }; diff --git a/agent/src/lib/llm.js b/agent/src/lib/llm.js new file mode 100644 index 00000000..14779947 --- /dev/null +++ b/agent/src/lib/llm.js @@ -0,0 +1,187 @@ +import { parseToolArguments } from './utils.js'; + +function extractFirstText(responseJson) { + const outputs = responseJson?.output; + if (!Array.isArray(outputs)) return ''; + + for (const item of outputs) { + if (!item?.content) continue; + for (const chunk of item.content) { + if (chunk?.text) return chunk.text; + if (chunk?.output_text) return chunk.output_text?.text ?? ''; + if (chunk?.text?.value) return chunk.text.value; + } + } + + return ''; +} + +function extractToolCalls(responseJson) { + const outputs = responseJson?.output; + if (!Array.isArray(outputs)) return []; + + const toolCalls = []; + for (const item of outputs) { + if (item?.type === 'tool_call' || item?.type === 'function_call') { + toolCalls.push({ + name: item?.name ?? item?.function?.name, + arguments: item?.arguments ?? item?.function?.arguments, + callId: item?.call_id ?? item?.id, + }); + continue; + } + + if (Array.isArray(item?.tool_calls)) { + for (const call of item.tool_calls) { + toolCalls.push({ + name: call?.name ?? call?.function?.name, + arguments: call?.arguments ?? call?.function?.arguments, + callId: call?.call_id ?? call?.id, + }); + } + } + } + + return toolCalls.filter((call) => call.name); +} + +async function callAgent({ + config, + systemPrompt, + signals, + ogContext, + commitmentText, + agentAddress, + tools, + allowTools, +}) { + const safeSignals = signals.map((signal) => { + if (signal?.kind === 'proposal') { + return { + ...signal, + challengeWindowEnds: + signal.challengeWindowEnds !== undefined + ? signal.challengeWindowEnds.toString() + : undefined, + transactions: Array.isArray(signal.transactions) + ? signal.transactions.map((tx) => ({ + ...tx, + value: tx.value !== undefined ? tx.value.toString() : undefined, + })) + : undefined, + }; + } + + return { + ...signal, + amount: signal.amount !== undefined ? signal.amount.toString() : undefined, + blockNumber: signal.blockNumber !== undefined ? signal.blockNumber.toString() : undefined, + transactionHash: signal.transactionHash ? String(signal.transactionHash) : undefined, + }; + }); + + const safeContext = { + rules: ogContext?.rules, + identifier: ogContext?.identifier ? String(ogContext.identifier) : undefined, + liveness: ogContext?.liveness !== undefined ? ogContext.liveness.toString() : undefined, + collateral: ogContext?.collateral, + bondAmount: ogContext?.bondAmount !== undefined ? ogContext.bondAmount.toString() : undefined, + optimisticOracle: ogContext?.optimisticOracle, + }; + + const payload = { + model: config.openAiModel, + input: [ + { + role: 'system', + content: systemPrompt, + }, + { + role: 'user', + content: JSON.stringify({ + commitmentSafe: config.commitmentSafe, + ogModule: config.ogModule, + agentAddress, + ogContext: safeContext, + commitment: commitmentText, + signals: safeSignals, + }), + }, + ], + tools: allowTools ? tools : [], + tool_choice: allowTools ? 'auto' : 'none', + parallel_tool_calls: false, + text: { format: { type: 'json_object' } }, + }; + + const res = await fetch(`${config.openAiBaseUrl}/responses`, { + method: 'POST', + headers: { + Authorization: `Bearer ${config.openAiApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`OpenAI API error: ${res.status} ${text}`); + } + + const json = await res.json(); + const toolCalls = allowTools ? extractToolCalls(json) : []; + const raw = extractFirstText(json); + let textDecision; + if (raw) { + try { + textDecision = JSON.parse(raw); + } catch (error) { + throw new Error(`Failed to parse OpenAI JSON: ${raw}`); + } + } + + return { toolCalls, textDecision, responseId: json?.id }; +} + +async function explainToolCalls({ config, previousResponseId, toolOutputs }) { + const input = [ + ...toolOutputs.map((item) => ({ + type: 'function_call_output', + call_id: item.callId, + output: item.output, + })), + { + type: 'message', + role: 'user', + content: [ + { + type: 'input_text', + text: 'Summarize the actions you took and why.', + }, + ], + }, + ]; + + const res = await fetch(`${config.openAiBaseUrl}/responses`, { + method: 'POST', + headers: { + Authorization: `Bearer ${config.openAiApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: config.openAiModel, + previous_response_id: previousResponseId, + input, + }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`OpenAI API error: ${res.status} ${text}`); + } + + const json = await res.json(); + return extractFirstText(json); +} + +export { callAgent, explainToolCalls, extractToolCalls, extractFirstText, parseToolArguments }; diff --git a/agent/src/lib/og.js b/agent/src/lib/og.js new file mode 100644 index 00000000..bcc41eca --- /dev/null +++ b/agent/src/lib/og.js @@ -0,0 +1,181 @@ +import { erc20Abi, getAddress, parseAbi, parseAbiItem, stringToHex } from 'viem'; + +const optimisticGovernorAbi = parseAbi([ + 'function proposeTransactions((address to,uint8 operation,uint256 value,bytes data)[] transactions, bytes explanation)', + 'function executeProposal((address to,uint8 operation,uint256 value,bytes data)[] transactions)', + 'function collateral() view returns (address)', + 'function bondAmount() view returns (uint256)', + 'function optimisticOracleV3() view returns (address)', + 'function rules() view returns (string)', + 'function identifier() view returns (bytes32)', + 'function liveness() view returns (uint64)', + 'function assertionIds(bytes32) view returns (bytes32)', +]); + +const optimisticOracleAbi = parseAbi([ + 'function disputeAssertion(bytes32 assertionId, address disputer)', + 'function getMinimumBond(address collateral) view returns (uint256)', + 'function getAssertion(bytes32 assertionId) view returns ((bool arbitrateViaEscalationManager,bool discardOracle,bool validateDisputers,address assertingCaller,address escalationManager) escalationManagerSettings,address asserter,uint64 assertionTime,bool settled,address currency,uint64 expirationTime,bool settlementResolution,bytes32 domainId,bytes32 identifier,uint256 bond,address callbackRecipient,address disputer)', +]); + +const transferEvent = parseAbiItem( + 'event Transfer(address indexed from, address indexed to, uint256 value)' +); +const transactionsProposedEvent = parseAbiItem( + 'event TransactionsProposed(address indexed proposer,uint256 indexed proposalTime,bytes32 indexed assertionId,((address to,uint8 operation,uint256 value,bytes data)[] transactions,uint256 requestTime) proposal,bytes32 proposalHash,bytes explanation,string rules,uint256 challengeWindowEnds)' +); +const proposalExecutedEvent = parseAbiItem( + 'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' +); +const proposalDeletedEvent = parseAbiItem( + 'event ProposalDeleted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' +); + +async function loadOptimisticGovernorDefaults({ publicClient, ogModule, trackedAssets }) { + const collateral = await publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'collateral', + }); + + trackedAssets.add(getAddress(collateral)); +} + +async function loadOgContext({ publicClient, ogModule }) { + const [collateral, bondAmount, optimisticOracle, rules, identifier, liveness] = + await Promise.all([ + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'collateral', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'bondAmount', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'optimisticOracleV3', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'rules', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'identifier', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'liveness', + }), + ]); + + return { + collateral, + bondAmount, + optimisticOracle, + rules, + identifier, + liveness, + }; +} + +async function logOgFundingStatus({ publicClient, ogModule, account }) { + try { + const chainId = await publicClient.getChainId(); + const expectedIdentifierStr = + chainId === 11155111 ? 'ASSERT_TRUTH' : 'ASSERT_TRUTH2'; + const expectedIdentifier = stringToHex(expectedIdentifierStr, { size: 32 }); + + const [collateral, bondAmount, optimisticOracle, identifier] = await Promise.all([ + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'collateral', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'bondAmount', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'optimisticOracleV3', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'identifier', + }), + ]); + const minimumBond = await publicClient.readContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'getMinimumBond', + args: [collateral], + }); + + const requiredBond = bondAmount > minimumBond ? bondAmount : minimumBond; + const collateralBalance = await publicClient.readContract({ + address: collateral, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }); + const nativeBalance = await publicClient.getBalance({ address: account.address }); + + if (identifier !== expectedIdentifier) { + console.warn( + `[agent] OG identifier mismatch: expected ${expectedIdentifierStr}, onchain ${identifier}` + ); + } + void requiredBond; + void collateralBalance; + void nativeBalance; + } catch (error) { + console.warn('[agent] Failed to log OG funding status:', error); + } +} + +function normalizeAssertion(assertion) { + if (!assertion) return {}; + if (typeof assertion === 'object' && !Array.isArray(assertion)) { + return assertion; + } + + const tuple = Array.isArray(assertion) ? assertion : []; + return { + escalationManagerSettings: tuple[0], + asserter: tuple[1], + assertionTime: tuple[2], + settled: tuple[3], + currency: tuple[4], + expirationTime: tuple[5], + settlementResolution: tuple[6], + domainId: tuple[7], + identifier: tuple[8], + bond: tuple[9], + callbackRecipient: tuple[10], + disputer: tuple[11], + }; +} + +export { + optimisticGovernorAbi, + optimisticOracleAbi, + transferEvent, + transactionsProposedEvent, + proposalExecutedEvent, + proposalDeletedEvent, + loadOptimisticGovernorDefaults, + loadOgContext, + logOgFundingStatus, + normalizeAssertion, +}; diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js new file mode 100644 index 00000000..34e3ee7f --- /dev/null +++ b/agent/src/lib/polling.js @@ -0,0 +1,266 @@ +import { getAddress, hexToString, zeroAddress } from 'viem'; +import { + optimisticGovernorAbi, + proposalDeletedEvent, + proposalExecutedEvent, + transactionsProposedEvent, + transferEvent, +} from './og.js'; + +async function primeBalances({ publicClient, commitmentSafe, watchNativeBalance, blockNumber }) { + if (!watchNativeBalance) return undefined; + + return publicClient.getBalance({ + address: commitmentSafe, + blockNumber, + }); +} + +async function pollCommitmentChanges({ + publicClient, + trackedAssets, + commitmentSafe, + watchNativeBalance, + lastCheckedBlock, + lastNativeBalance, +}) { + const latestBlock = await publicClient.getBlockNumber(); + if (lastCheckedBlock === undefined) { + const nextNativeBalance = await primeBalances({ + publicClient, + commitmentSafe, + watchNativeBalance, + blockNumber: latestBlock, + }); + return { + deposits: [], + lastCheckedBlock: latestBlock, + lastNativeBalance: nextNativeBalance, + }; + } + + if (latestBlock <= lastCheckedBlock) { + return { deposits: [], lastCheckedBlock, lastNativeBalance }; + } + + const fromBlock = lastCheckedBlock + 1n; + const toBlock = latestBlock; + const deposits = []; + + for (const asset of trackedAssets) { + const logs = await publicClient.getLogs({ + address: asset, + event: transferEvent, + args: { to: commitmentSafe }, + fromBlock, + toBlock, + }); + + for (const log of logs) { + deposits.push({ + kind: 'erc20Deposit', + asset, + from: log.args.from, + amount: log.args.value, + blockNumber: log.blockNumber, + transactionHash: log.transactionHash, + }); + } + } + + let nextNativeBalance = lastNativeBalance; + if (watchNativeBalance) { + const nativeBalance = await publicClient.getBalance({ + address: commitmentSafe, + blockNumber: toBlock, + }); + + if (lastNativeBalance !== undefined && nativeBalance > lastNativeBalance) { + deposits.push({ + kind: 'nativeDeposit', + asset: zeroAddress, + from: 'unknown', + amount: nativeBalance - lastNativeBalance, + blockNumber: toBlock, + transactionHash: undefined, + }); + } + + nextNativeBalance = nativeBalance; + } + + return { deposits, lastCheckedBlock: toBlock, lastNativeBalance: nextNativeBalance }; +} + +async function pollProposalChanges({ publicClient, ogModule, lastProposalCheckedBlock, proposalsByHash }) { + const latestBlock = await publicClient.getBlockNumber(); + if (lastProposalCheckedBlock === undefined) { + return { newProposals: [], lastProposalCheckedBlock: latestBlock }; + } + + if (latestBlock <= lastProposalCheckedBlock) { + return { newProposals: [], lastProposalCheckedBlock }; + } + + const fromBlock = lastProposalCheckedBlock + 1n; + const toBlock = latestBlock; + + const [proposedLogs, executedLogs, deletedLogs] = await Promise.all([ + publicClient.getLogs({ + address: ogModule, + event: transactionsProposedEvent, + fromBlock, + toBlock, + }), + publicClient.getLogs({ + address: ogModule, + event: proposalExecutedEvent, + fromBlock, + toBlock, + }), + publicClient.getLogs({ + address: ogModule, + event: proposalDeletedEvent, + fromBlock, + toBlock, + }), + ]); + + const newProposals = []; + for (const log of proposedLogs) { + const proposalHash = log.args?.proposalHash; + const assertionId = log.args?.assertionId; + const proposal = log.args?.proposal; + const challengeWindowEnds = log.args?.challengeWindowEnds; + if (!proposalHash || !proposal?.transactions) continue; + const proposer = log.args?.proposer; + const explanationHex = log.args?.explanation; + const rules = log.args?.rules; + let explanation; + if (explanationHex && typeof explanationHex === 'string') { + if (explanationHex.startsWith('0x')) { + try { + explanation = hexToString(explanationHex); + } catch (error) { + explanation = undefined; + } + } else { + explanation = explanationHex; + } + } + + const transactions = proposal.transactions.map((tx) => ({ + to: getAddress(tx.to), + operation: Number(tx.operation ?? 0), + value: BigInt(tx.value ?? 0), + data: tx.data ?? '0x', + })); + + const proposalRecord = { + proposalHash, + assertionId, + proposer: proposer ? getAddress(proposer) : undefined, + challengeWindowEnds: BigInt(challengeWindowEnds ?? 0), + transactions, + lastAttemptMs: 0, + disputeAttemptMs: 0, + rules, + explanation, + }; + proposalsByHash.set(proposalHash, proposalRecord); + newProposals.push(proposalRecord); + } + + for (const log of executedLogs) { + const proposalHash = log.args?.proposalHash; + if (proposalHash) { + proposalsByHash.delete(proposalHash); + } + } + + for (const log of deletedLogs) { + const proposalHash = log.args?.proposalHash; + if (proposalHash) { + proposalsByHash.delete(proposalHash); + } + } + + return { newProposals, lastProposalCheckedBlock: toBlock }; +} + +async function executeReadyProposals({ + publicClient, + walletClient, + account, + ogModule, + proposalsByHash, + executeRetryMs, +}) { + if (proposalsByHash.size === 0) return; + + const latestBlock = await publicClient.getBlockNumber(); + const block = await publicClient.getBlock({ blockNumber: latestBlock }); + const now = BigInt(block.timestamp); + const nowMs = Date.now(); + + for (const proposal of proposalsByHash.values()) { + if (!proposal?.transactions?.length) continue; + if (proposal.challengeWindowEnds === undefined) continue; + if (now < proposal.challengeWindowEnds) continue; + if (proposal.lastAttemptMs && nowMs - proposal.lastAttemptMs < executeRetryMs) { + continue; + } + + proposal.lastAttemptMs = nowMs; + + let assertionId; + try { + assertionId = await publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'assertionIds', + args: [proposal.proposalHash], + }); + } catch (error) { + console.warn('[agent] Failed to read assertionId:', error); + continue; + } + + if (!assertionId || assertionId === `0x${'0'.repeat(64)}`) { + proposalsByHash.delete(proposal.proposalHash); + continue; + } + + try { + await publicClient.simulateContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'executeProposal', + args: [proposal.transactions], + account: account.address, + }); + } catch (error) { + console.warn('[agent] Proposal not executable yet:', proposal.proposalHash); + continue; + } + + try { + const txHash = await walletClient.writeContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'executeProposal', + args: [proposal.transactions], + }); + console.log('[agent] Proposal execution submitted:', txHash); + } catch (error) { + console.warn('[agent] Proposal execution failed:', error?.shortMessage ?? error?.message ?? error); + } + } +} + +export { + primeBalances, + pollCommitmentChanges, + pollProposalChanges, + executeReadyProposals, +}; diff --git a/agent/src/lib/signer.js b/agent/src/lib/signer.js new file mode 100644 index 00000000..bfe47593 --- /dev/null +++ b/agent/src/lib/signer.js @@ -0,0 +1,129 @@ +import { readFile } from 'node:fs/promises'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { Wallet } from 'ethers'; +import { createWalletClient, getAddress, http } from 'viem'; +import { privateKeyToAccount, toAccount } from 'viem/accounts'; +import { mustGetEnv, normalizePrivateKey } from './utils.js'; + +const execFileAsync = promisify(execFile); + +async function loadPrivateKeyFromKeystore() { + const keystorePath = mustGetEnv('KEYSTORE_PATH'); + const keystorePassword = mustGetEnv('KEYSTORE_PASSWORD'); + const keystoreJson = await readFile(keystorePath, 'utf8'); + const wallet = await Wallet.fromEncryptedJson(keystoreJson, keystorePassword); + return wallet.privateKey; +} + +async function loadPrivateKeyFromKeychain() { + const service = mustGetEnv('KEYCHAIN_SERVICE'); + const account = mustGetEnv('KEYCHAIN_ACCOUNT'); + + if (process.platform === 'darwin') { + const { stdout } = await execFileAsync('security', [ + 'find-generic-password', + '-s', + service, + '-a', + account, + '-w', + ]); + return stdout.trim(); + } + + if (process.platform === 'linux') { + const { stdout } = await execFileAsync('secret-tool', [ + 'lookup', + 'service', + service, + 'account', + account, + ]); + return stdout.trim(); + } + + throw new Error('Keychain lookup not supported on this platform.'); +} + +async function loadPrivateKeyFromVault() { + const vaultAddr = mustGetEnv('VAULT_ADDR').replace(/\/+$/, ''); + const vaultToken = mustGetEnv('VAULT_TOKEN'); + const vaultPath = mustGetEnv('VAULT_SECRET_PATH').replace(/^\/+/, ''); + const vaultNamespace = process.env.VAULT_NAMESPACE; + const vaultKeyField = process.env.VAULT_SECRET_KEY ?? 'private_key'; + + const response = await fetch(`${vaultAddr}/v1/${vaultPath}`, { + headers: { + 'X-Vault-Token': vaultToken, + ...(vaultNamespace ? { 'X-Vault-Namespace': vaultNamespace } : {}), + }, + }); + + if (!response.ok) { + throw new Error(`Vault request failed (${response.status}).`); + } + + const payload = await response.json(); + const data = payload?.data?.data ?? payload?.data ?? {}; + const value = data[vaultKeyField]; + if (!value) { + throw new Error(`Vault secret missing key '${vaultKeyField}'.`); + } + + return value; +} + +async function createSignerClient({ rpcUrl }) { + const signerType = (process.env.SIGNER_TYPE ?? 'env').toLowerCase(); + + if (signerType === 'env') { + const privateKey = normalizePrivateKey(mustGetEnv('PRIVATE_KEY')); + const account = privateKeyToAccount(privateKey); + return { + account, + walletClient: createWalletClient({ account, transport: http(rpcUrl) }), + }; + } + + if (signerType === 'keystore') { + const privateKey = normalizePrivateKey(await loadPrivateKeyFromKeystore()); + const account = privateKeyToAccount(privateKey); + return { + account, + walletClient: createWalletClient({ account, transport: http(rpcUrl) }), + }; + } + + if (signerType === 'keychain') { + const privateKey = normalizePrivateKey(await loadPrivateKeyFromKeychain()); + const account = privateKeyToAccount(privateKey); + return { + account, + walletClient: createWalletClient({ account, transport: http(rpcUrl) }), + }; + } + + if (signerType === 'vault') { + const privateKey = normalizePrivateKey(await loadPrivateKeyFromVault()); + const account = privateKeyToAccount(privateKey); + return { + account, + walletClient: createWalletClient({ account, transport: http(rpcUrl) }), + }; + } + + if (['kms', 'vault-signer', 'signer-rpc', 'rpc', 'json-rpc'].includes(signerType)) { + const signerRpcUrl = mustGetEnv('SIGNER_RPC_URL'); + const signerAddress = getAddress(mustGetEnv('SIGNER_ADDRESS')); + const account = toAccount(signerAddress); + return { + account, + walletClient: createWalletClient({ account, transport: http(signerRpcUrl) }), + }; + } + + throw new Error(`Unsupported SIGNER_TYPE '${signerType}'.`); +} + +export { createSignerClient }; diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js new file mode 100644 index 00000000..d264f81b --- /dev/null +++ b/agent/src/lib/tools.js @@ -0,0 +1,324 @@ +import { getAddress } from 'viem'; +import { buildOgTransactions, makeDeposit, postBondAndDispute, postBondAndPropose } from './tx.js'; +import { parseToolArguments } from './utils.js'; + +function toolDefinitions({ proposeEnabled, disputeEnabled }) { + const tools = [ + { + type: 'function', + name: 'build_og_transactions', + description: + 'Build Optimistic Governor transaction payloads from high-level intents. Returns array of {to,value,data,operation} with value as string wei.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + actions: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + kind: { + type: 'string', + description: + 'Action type: erc20_transfer | native_transfer | contract_call', + }, + token: { + type: ['string', 'null'], + description: + 'ERC20 token address for erc20_transfer.', + }, + to: { + type: ['string', 'null'], + description: 'Recipient or target contract address.', + }, + amountWei: { + type: ['string', 'null'], + description: + 'Amount in wei as a string. For erc20_transfer and native_transfer.', + }, + valueWei: { + type: ['string', 'null'], + description: + 'ETH value to send in contract_call (default 0).', + }, + abi: { + type: ['string', 'null'], + description: + 'Function signature for contract_call, e.g. "setOwner(address)".', + }, + args: { + type: ['array', 'null'], + description: + 'Arguments for contract_call in order, JSON-serializable.', + items: { type: 'string' }, + }, + operation: { + type: ['integer', 'null'], + description: + 'Safe operation (0=CALL,1=DELEGATECALL). Defaults to 0.', + }, + }, + required: [ + 'kind', + 'token', + 'to', + 'amountWei', + 'valueWei', + 'abi', + 'args', + 'operation', + ], + }, + }, + }, + required: ['actions'], + }, + }, + { + type: 'function', + name: 'make_deposit', + description: + 'Deposit funds into the commitment Safe. Use asset=0x000...000 for native ETH. amountWei must be a string of the integer wei amount.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + asset: { + type: 'string', + description: + 'Asset address (ERC20) or 0x0000000000000000000000000000000000000000 for native.', + }, + amountWei: { + type: 'string', + description: 'Amount in wei as a string.', + }, + }, + required: ['asset', 'amountWei'], + }, + }, + ]; + + if (proposeEnabled) { + tools.push({ + type: 'function', + name: 'post_bond_and_propose', + description: + 'Post bond (if required) and propose transactions to the Optimistic Governor.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + transactions: { + type: 'array', + description: 'Safe transaction batch to propose. Use value as string wei.', + items: { + type: 'object', + additionalProperties: false, + properties: { + to: { type: 'string' }, + value: { type: 'string' }, + data: { type: 'string' }, + operation: { type: 'integer' }, + }, + required: ['to', 'value', 'data', 'operation'], + }, + }, + }, + required: ['transactions'], + }, + }); + } + + if (disputeEnabled) { + tools.push({ + type: 'function', + name: 'dispute_assertion', + description: + 'Post bond (if required) and dispute an assertion on the Optimistic Oracle. Provide a short human-readable explanation.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + assertionId: { + type: 'string', + description: 'Assertion ID to dispute.', + }, + explanation: { + type: 'string', + description: 'Short human-readable dispute rationale.', + }, + }, + required: ['assertionId', 'explanation'], + }, + }); + } + + return tools; +} + +async function executeToolCalls({ + toolCalls, + publicClient, + walletClient, + account, + config, + ogContext, +}) { + const outputs = []; + const hasPostProposal = toolCalls.some((call) => call.name === 'post_bond_and_propose'); + let builtTransactions; + + for (const call of toolCalls) { + const args = parseToolArguments(call.arguments); + if (!args) { + console.warn('[agent] Skipping tool call with invalid args:', call); + continue; + } + + if (call.name === 'build_og_transactions') { + try { + const transactions = buildOgTransactions(args.actions ?? []); + builtTransactions = transactions; + outputs.push({ + callId: call.callId, + output: JSON.stringify({ status: 'ok', transactions }), + }); + } catch (error) { + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'error', + message: error?.message ?? String(error), + }), + }); + } + continue; + } + + if (call.name === 'make_deposit') { + const txHash = await makeDeposit({ + walletClient, + account, + config, + asset: args.asset, + amountWei: BigInt(args.amountWei), + }); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'submitted', + transactionHash: String(txHash), + }), + }); + continue; + } + + if (call.name === 'post_bond_and_propose') { + if (!config.proposeEnabled) { + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'skipped', + reason: 'proposals disabled', + }), + }); + continue; + } + + const transactions = args.transactions.map((tx) => ({ + to: getAddress(tx.to), + value: BigInt(tx.value), + data: tx.data, + operation: Number(tx.operation), + })); + const result = await postBondAndPropose({ + publicClient, + walletClient, + account, + config, + ogModule: config.ogModule, + transactions, + }); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'submitted', + ...result, + }), + }); + continue; + } + + if (call.name === 'dispute_assertion') { + if (!config.disputeEnabled) { + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'skipped', + reason: 'disputes disabled', + }), + }); + continue; + } + + try { + const result = await postBondAndDispute({ + publicClient, + walletClient, + account, + config, + ogContext, + assertionId: args.assertionId, + explanation: args.explanation, + }); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'submitted', + ...result, + }), + }); + } catch (error) { + outputs.push({ + callId: call.callId, + output: JSON.stringify({ + status: 'error', + message: error?.message ?? String(error), + }), + }); + } + continue; + } + + console.warn('[agent] Unknown tool call:', call.name); + outputs.push({ + callId: call.callId, + output: JSON.stringify({ status: 'skipped', reason: 'unknown tool' }), + }); + } + + if (builtTransactions && !hasPostProposal) { + if (!config.proposeEnabled) { + console.log('[agent] Built transactions but proposals are disabled; skipping propose.'); + } else { + await postBondAndPropose({ + publicClient, + walletClient, + account, + config, + ogModule: config.ogModule, + transactions: builtTransactions, + }); + } + } + + return outputs.filter((item) => item.callId); +} + +export { executeToolCalls, toolDefinitions }; diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js new file mode 100644 index 00000000..d3e1399e --- /dev/null +++ b/agent/src/lib/tx.js @@ -0,0 +1,405 @@ +import { + encodeFunctionData, + erc20Abi, + getAddress, + parseAbi, + stringToHex, + zeroAddress, +} from 'viem'; +import { optimisticGovernorAbi, optimisticOracleAbi } from './og.js'; +import { normalizeAssertion } from './og.js'; +import { summarizeViemError } from './utils.js'; + +async function postBondAndPropose({ + publicClient, + walletClient, + account, + config, + ogModule, + transactions, +}) { + if (!config.proposeEnabled) { + throw new Error('Proposals disabled via PROPOSE_ENABLED.'); + } + + const normalizedTransactions = normalizeOgTransactions(transactions); + const proposerBalance = await publicClient.getBalance({ address: account.address }); + const [collateral, bondAmount, optimisticOracle] = await Promise.all([ + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'collateral', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'bondAmount', + }), + publicClient.readContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'optimisticOracleV3', + }), + ]); + let minimumBond = 0n; + try { + minimumBond = await publicClient.readContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'getMinimumBond', + args: [collateral], + }); + } catch (error) { + console.warn('[agent] Failed to fetch minimum bond from optimistic oracle:', error); + } + + const requiredBond = bondAmount > minimumBond ? bondAmount : minimumBond; + + if (requiredBond > 0n) { + const collateralBalance = await publicClient.readContract({ + address: collateral, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }); + if (collateralBalance < requiredBond) { + throw new Error( + `Insufficient bond collateral balance: need ${requiredBond.toString()} wei, have ${collateralBalance.toString()}.` + ); + } + const spenders = []; + if (config.bondSpender === 'og' || config.bondSpender === 'both') { + spenders.push(ogModule); + } + if (config.bondSpender === 'oo' || config.bondSpender === 'both') { + spenders.push(optimisticOracle); + } + + for (const spender of spenders) { + const approveHash = await walletClient.writeContract({ + address: collateral, + abi: erc20Abi, + functionName: 'approve', + args: [spender, requiredBond], + }); + await publicClient.waitForTransactionReceipt({ hash: approveHash }); + const allowance = await publicClient.readContract({ + address: collateral, + abi: erc20Abi, + functionName: 'allowance', + args: [account.address, spender], + }); + if (allowance < requiredBond) { + throw new Error( + `Insufficient bond allowance: need ${requiredBond.toString()} wei, have ${allowance.toString()} for spender ${spender}.` + ); + } + } + } + + if (proposerBalance === 0n) { + throw new Error( + `Proposer ${account.address} has 0 native balance; cannot pay gas to propose.` + ); + } + + let proposalHash; + const explanation = 'Agent serving Oya commitment.'; + const explanationBytes = stringToHex(explanation); + const proposalData = encodeFunctionData({ + abi: optimisticGovernorAbi, + functionName: 'proposeTransactions', + args: [normalizedTransactions, explanationBytes], + }); + let simulationError; + let submissionError; + try { + await publicClient.simulateContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'proposeTransactions', + args: [normalizedTransactions, explanationBytes], + account: account.address, + }); + } catch (error) { + simulationError = error; + if (!config.allowProposeOnSimulationFail) { + throw error; + } + console.warn('[agent] Simulation failed; attempting to propose anyway.'); + } + + try { + if (simulationError) { + proposalHash = await walletClient.sendTransaction({ + account, + to: ogModule, + data: proposalData, + value: 0n, + gas: config.proposeGasLimit, + }); + } else { + proposalHash = await walletClient.writeContract({ + address: ogModule, + abi: optimisticGovernorAbi, + functionName: 'proposeTransactions', + args: [normalizedTransactions, explanationBytes], + }); + } + } catch (error) { + submissionError = error; + const message = + error?.shortMessage ?? + error?.message ?? + simulationError?.shortMessage ?? + simulationError?.message ?? + String(error ?? simulationError); + console.warn('[agent] Propose submission failed:', message); + } + + if (proposalHash) { + console.log('[agent] Proposal submitted:', proposalHash); + } + + return { + proposalHash, + bondAmount, + collateral, + optimisticOracle, + submissionError: submissionError ? summarizeViemError(submissionError) : null, + }; +} + +async function postBondAndDispute({ + publicClient, + walletClient, + account, + config, + ogContext, + assertionId, + explanation, +}) { + if (!config.disputeEnabled) { + throw new Error('Disputes disabled via DISPUTE_ENABLED.'); + } + + const proposerBalance = await publicClient.getBalance({ address: account.address }); + if (proposerBalance === 0n) { + throw new Error( + `Disputer ${account.address} has 0 native balance; cannot pay gas to dispute.` + ); + } + + const optimisticOracle = ogContext?.optimisticOracle; + if (!optimisticOracle) { + throw new Error('Missing optimistic oracle address.'); + } + + const assertionRaw = await publicClient.readContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'getAssertion', + args: [assertionId], + }); + const assertion = normalizeAssertion(assertionRaw); + + const nowBlock = await publicClient.getBlock(); + const now = BigInt(nowBlock.timestamp); + const expirationTime = BigInt(assertion.expirationTime ?? 0); + const disputer = assertion.disputer ? getAddress(assertion.disputer) : zeroAddress; + const settled = Boolean(assertion.settled); + if (settled) { + throw new Error(`Assertion ${assertionId} already settled.`); + } + if (expirationTime !== 0n && now >= expirationTime) { + throw new Error(`Assertion ${assertionId} expired at ${expirationTime}.`); + } + if (disputer !== zeroAddress) { + throw new Error(`Assertion ${assertionId} already disputed by ${disputer}.`); + } + + const bond = BigInt(assertion.bond ?? 0); + const currency = assertion.currency ? getAddress(assertion.currency) : zeroAddress; + if (currency === zeroAddress) { + throw new Error('Assertion currency is zero address; cannot post bond.'); + } + + if (bond > 0n) { + const collateralBalance = await publicClient.readContract({ + address: currency, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }); + if (collateralBalance < bond) { + throw new Error( + `Insufficient dispute bond balance: need ${bond.toString()} wei, have ${collateralBalance.toString()}.` + ); + } + + const approveHash = await walletClient.writeContract({ + address: currency, + abi: erc20Abi, + functionName: 'approve', + args: [optimisticOracle, bond], + }); + await publicClient.waitForTransactionReceipt({ hash: approveHash }); + } + + let disputeHash; + try { + await publicClient.simulateContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'disputeAssertion', + args: [assertionId, account.address], + account: account.address, + }); + disputeHash = await walletClient.writeContract({ + address: optimisticOracle, + abi: optimisticOracleAbi, + functionName: 'disputeAssertion', + args: [assertionId, account.address], + }); + } catch (error) { + const message = error?.shortMessage ?? error?.message ?? String(error); + throw new Error(`Dispute submission failed: ${message}`); + } + + if (explanation) { + console.log(`[agent] Dispute rationale: ${explanation}`); + } + + console.log('[agent] Dispute submitted:', disputeHash); + + return { + disputeHash, + bondAmount: bond, + collateral: currency, + optimisticOracle, + }; +} + +function normalizeOgTransactions(transactions) { + if (!Array.isArray(transactions)) { + throw new Error('transactions must be an array'); + } + + return transactions.map((tx, index) => { + if (!tx || !tx.to) { + throw new Error(`transactions[${index}] missing to`); + } + + return { + to: getAddress(tx.to), + value: BigInt(tx.value ?? 0), + data: tx.data ?? '0x', + operation: Number(tx.operation ?? 0), + }; + }); +} + +function buildOgTransactions(actions) { + if (!Array.isArray(actions) || actions.length === 0) { + throw new Error('actions must be a non-empty array'); + } + + return actions.map((action) => { + const operation = action.operation !== undefined ? Number(action.operation) : 0; + + if (action.kind === 'erc20_transfer') { + if (!action.token || !action.to || action.amountWei === undefined) { + throw new Error('erc20_transfer requires token, to, amountWei'); + } + + const data = encodeFunctionData({ + abi: erc20Abi, + functionName: 'transfer', + args: [getAddress(action.to), BigInt(action.amountWei)], + }); + + return { + to: getAddress(action.token), + value: '0', + data, + operation, + }; + } + + if (action.kind === 'native_transfer') { + if (!action.to || action.amountWei === undefined) { + throw new Error('native_transfer requires to, amountWei'); + } + + return { + to: getAddress(action.to), + value: BigInt(action.amountWei).toString(), + data: '0x', + operation, + }; + } + + if (action.kind === 'contract_call') { + if (!action.to || !action.abi) { + throw new Error('contract_call requires to, abi'); + } + + const abi = parseAbi([`function ${action.abi}`]); + const args = Array.isArray(action.args) ? action.args : []; + const data = encodeFunctionData({ + abi, + functionName: action.abi.split('(')[0], + args, + }); + const value = action.valueWei !== undefined ? BigInt(action.valueWei).toString() : '0'; + + return { + to: getAddress(action.to), + value, + data, + operation, + }; + } + + throw new Error(`Unknown action kind: ${action.kind}`); + }); +} + +async function makeDeposit({ + walletClient, + account, + config, + asset, + amountWei, +}) { + const depositAsset = asset ? getAddress(asset) : config.defaultDepositAsset; + const depositAmount = + amountWei !== undefined ? amountWei : config.defaultDepositAmountWei; + + if (!depositAsset || depositAmount === undefined) { + throw new Error('Deposit requires asset and amount (wei).'); + } + + if (depositAsset === zeroAddress) { + return walletClient.sendTransaction({ + account, + to: config.commitmentSafe, + value: BigInt(depositAmount), + }); + } + + return walletClient.writeContract({ + address: depositAsset, + abi: erc20Abi, + functionName: 'transfer', + args: [config.commitmentSafe, BigInt(depositAmount)], + }); +} + +export { + buildOgTransactions, + makeDeposit, + normalizeOgTransactions, + postBondAndDispute, + postBondAndPropose, +}; diff --git a/agent/src/lib/utils.js b/agent/src/lib/utils.js new file mode 100644 index 00000000..1d491809 --- /dev/null +++ b/agent/src/lib/utils.js @@ -0,0 +1,58 @@ +import { getAddress } from 'viem'; + +function mustGetEnv(key) { + const value = process.env[key]; + if (!value) { + throw new Error(`Missing required env var ${key}`); + } + return value; +} + +function normalizePrivateKey(value) { + if (!value) return value; + return value.startsWith('0x') ? value : `0x${value}`; +} + +function parseAddressList(list) { + if (!list) return []; + return list + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + .map(getAddress); +} + +function summarizeViemError(error) { + if (!error) return null; + + return { + name: error.name, + shortMessage: error.shortMessage, + message: error.message, + details: error.details, + metaMessages: error.metaMessages, + data: error.data ?? error.cause?.data, + cause: error.cause?.shortMessage ?? error.cause?.message ?? error.cause, + }; +} + +function parseToolArguments(raw) { + if (!raw) return null; + if (typeof raw === 'object') return raw; + if (typeof raw === 'string') { + try { + return JSON.parse(raw); + } catch (error) { + return null; + } + } + return null; +} + +export { + mustGetEnv, + normalizePrivateKey, + parseAddressList, + parseToolArguments, + summarizeViemError, +}; From bb062980faa40e849e56e6af1cf4b4c96221b8de Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 10:41:13 -0800 Subject: [PATCH 033/174] resolve AGENT_MODULE from repo root Signed-off-by: John Shutt --- agent/src/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 204121c9..5f20a76f 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -1,7 +1,7 @@ import dotenv from 'dotenv'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { createPublicClient, http } from 'viem'; import { buildConfig } from './lib/config.js'; import { createSignerClient } from './lib/signer.js'; @@ -20,8 +20,12 @@ import { callAgent, explainToolCalls } from './lib/llm.js'; import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '../..'); + dotenv.config(); -dotenv.config({ path: path.resolve(process.cwd(), 'agent/.env') }); +dotenv.config({ path: path.resolve(repoRoot, 'agent/.env') }); const config = buildConfig(); const publicClient = createPublicClient({ transport: http(config.rpcUrl) }); @@ -37,7 +41,7 @@ const proposalsByHash = new Map(); async function loadAgentModule() { const modulePath = config.agentModule ?? 'agent-library/agents/default/agent.js'; - const resolvedPath = path.resolve(process.cwd(), modulePath); + const resolvedPath = path.resolve(repoRoot, modulePath); const moduleUrl = pathToFileURL(resolvedPath).href; const agentModule = await import(moduleUrl); From dacfb0d2eaaa6754883c33b10259bf086da221c8 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 10:53:04 -0800 Subject: [PATCH 034/174] update the validator to resolve AGENT_MODULE relative to the repo root Signed-off-by: John Shutt --- agent/scripts/validate-agent.mjs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/agent/scripts/validate-agent.mjs b/agent/scripts/validate-agent.mjs index c7eb8b35..63abfe31 100644 --- a/agent/scripts/validate-agent.mjs +++ b/agent/scripts/validate-agent.mjs @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; function getArgValue(prefix) { const arg = process.argv.find((value) => value.startsWith(prefix)); @@ -9,8 +9,14 @@ function getArgValue(prefix) { async function main() { const moduleArg = getArgValue('--module='); - const modulePath = moduleArg ?? process.env.AGENT_MODULE ?? 'agent-library/agents/default/agent.js'; - const resolvedPath = path.resolve(process.cwd(), modulePath); + const modulePath = + moduleArg ?? process.env.AGENT_MODULE ?? 'agent-library/agents/default/agent.js'; + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const repoRoot = path.resolve(__dirname, '../..'); + const resolvedPath = path.isAbsolute(modulePath) + ? modulePath + : path.resolve(repoRoot, modulePath); const agentModule = await import(pathToFileURL(resolvedPath).href); if (typeof agentModule.getSystemPrompt !== 'function') { From a0cd36179bc92d3c2521457dcc94fea4d39bc8f2 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 10:55:30 -0800 Subject: [PATCH 035/174] update commitment.txt for example agent Signed-off-by: John Shutt --- agent-library/agents/default/commitment.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent-library/agents/default/commitment.txt b/agent-library/agents/default/commitment.txt index e9027f25..064ae23f 100644 --- a/agent-library/agents/default/commitment.txt +++ b/agent-library/agents/default/commitment.txt @@ -1 +1 @@ -Any assets deposited in this Commitment may be transferred back to the depositor before January 15th, 2026 (12:00AM PST). After the deadline, assets may only be transferred to jdshutt.eth. If a third party is initiating the transfer after the deadline, they may take a 10% cut of the assets being transferred as a fee. +The agent watching this commitment should send any funds deposited into this commitment to themselves. \ No newline at end of file From fa08b25e7b965f3523ea11a38fc25b08be280f1c Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 11:06:42 -0800 Subject: [PATCH 036/174] loosen build_og_transactions schema to allow non-string args Signed-off-by: John Shutt --- agent/src/lib/tools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index d264f81b..7151237b 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -53,7 +53,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { type: ['array', 'null'], description: 'Arguments for contract_call in order, JSON-serializable.', - items: { type: 'string' }, + items: {}, }, operation: { type: ['integer', 'null'], From 9a9f9d4ae99620ba414cb9bb7026674ac4cf91bf Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 11:08:25 -0800 Subject: [PATCH 037/174] explicitly outline types to meet OpenAI schema validation Signed-off-by: John Shutt --- agent/src/lib/tools.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 7151237b..ccef2df9 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -53,7 +53,16 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { type: ['array', 'null'], description: 'Arguments for contract_call in order, JSON-serializable.', - items: {}, + items: { + type: [ + 'string', + 'number', + 'boolean', + 'object', + 'array', + 'null', + ], + }, }, operation: { type: ['integer', 'null'], From a6e38576090a98238053a1b0c251ccb087497cea Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 11:17:42 -0800 Subject: [PATCH 038/174] more fixes to the schema Signed-off-by: John Shutt --- agent/src/lib/tools.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index ccef2df9..01798bea 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -54,13 +54,22 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { description: 'Arguments for contract_call in order, JSON-serializable.', items: { - type: [ - 'string', - 'number', - 'boolean', - 'object', - 'array', - 'null', + anyOf: [ + { type: 'string' }, + { type: 'number' }, + { type: 'boolean' }, + { type: 'null' }, + { + type: 'array', + items: { + anyOf: [ + { type: 'string' }, + { type: 'number' }, + { type: 'boolean' }, + { type: 'null' }, + ], + }, + }, ], }, }, From 1e7cf7d96f1c061640b98420b33fec0e651dc058 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 11:26:34 -0800 Subject: [PATCH 039/174] update agent readme --- agent/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/README.md b/agent/README.md index cf580259..bf9f794c 100644 --- a/agent/README.md +++ b/agent/README.md @@ -10,7 +10,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - Node.js 18+ - RPC endpoint the agent can reach -- Private key funded for gas and permissions to propose through the Optimistic Governor +- Private key funded for gas and bond currency to propose through the Optimistic Governor ## Configure From f762f6af91367fbc572de40da93c27373a0ef616 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 12:02:48 -0800 Subject: [PATCH 040/174] add timelock agent Signed-off-by: John Shutt --- agent-library/README.md | 4 + .../agents/default/test-default-agent.mjs | 16 +++ .../agents/timelock-withdraw/agent.js | 27 ++++ .../agents/timelock-withdraw/commitment.txt | 1 + .../timelock-withdraw/simulate-timelock.mjs | 28 ++++ .../timelock-withdraw/test-timelock.mjs | 28 ++++ agent/README.md | 28 ++++ agent/src/index.js | 68 +++++++++ agent/src/lib/timelock.js | 136 ++++++++++++++++++ 9 files changed, 336 insertions(+) create mode 100644 agent-library/agents/default/test-default-agent.mjs create mode 100644 agent-library/agents/timelock-withdraw/agent.js create mode 100644 agent-library/agents/timelock-withdraw/commitment.txt create mode 100644 agent-library/agents/timelock-withdraw/simulate-timelock.mjs create mode 100644 agent-library/agents/timelock-withdraw/test-timelock.mjs create mode 100644 agent/src/lib/timelock.js diff --git a/agent-library/README.md b/agent-library/README.md index 8d75051b..f2c7f9bf 100644 --- a/agent-library/README.md +++ b/agent-library/README.md @@ -10,3 +10,7 @@ To add a new agent: 1. Copy `agent-library/agents/default/` to a new folder. 2. Update `agent.js` and `commitment.txt`. 3. Set `AGENT_MODULE=agent-library/agents//agent.js`. + +Example agents: +- `agent-library/agents/default/`: generic agent using the commitment text. +- `agent-library/agents/timelock-withdraw/`: timelock withdrawal agent that only withdraws to its own address after the timelock. diff --git a/agent-library/agents/default/test-default-agent.mjs b/agent-library/agents/default/test-default-agent.mjs new file mode 100644 index 00000000..e483f2f6 --- /dev/null +++ b/agent-library/agents/default/test-default-agent.mjs @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import { getSystemPrompt } from './agent.js'; + +function run() { + const prompt = getSystemPrompt({ + proposeEnabled: true, + disputeEnabled: true, + commitmentText: 'Test commitment.', + }); + + assert.ok(prompt.includes('monitoring an onchain commitment')); + assert.ok(prompt.includes('Commitment text')); + console.log('[test] default agent prompt OK'); +} + +run(); diff --git a/agent-library/agents/timelock-withdraw/agent.js b/agent-library/agents/timelock-withdraw/agent.js new file mode 100644 index 00000000..4d399ac5 --- /dev/null +++ b/agent-library/agents/timelock-withdraw/agent.js @@ -0,0 +1,27 @@ +function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { + const mode = proposeEnabled && disputeEnabled + ? 'You may propose and dispute.' + : proposeEnabled + ? 'You may propose but you may not dispute.' + : disputeEnabled + ? 'You may dispute but you may not propose.' + : 'You may not propose or dispute; provide opinions only.'; + + return [ + 'You are a timelock withdrawal agent.', + 'You may only withdraw funds to your own agentAddress, and only after the timelock described in the commitment/rules.', + 'If a timelock trigger fires, re-check the rules and propose withdrawals that follow them.', + 'Never propose withdrawals before the timelock or to any address other than agentAddress.', + 'Default to disputing proposals that violate the rules; prefer no-op when unsure.', + mode, + commitmentText ? `Commitment text:\n${commitmentText}` : '', + 'If an onchain action is needed, call a tool.', + 'Use build_og_transactions to construct proposal payloads, then post_bond_and_propose.', + 'Use dispute_assertion with a short human-readable explanation when disputing.', + 'If no action is needed, output strict JSON with keys: action (propose|deposit|dispute|ignore|other) and rationale (string).', + ] + .filter(Boolean) + .join(' '); +} + +export { getSystemPrompt }; diff --git a/agent-library/agents/timelock-withdraw/commitment.txt b/agent-library/agents/timelock-withdraw/commitment.txt new file mode 100644 index 00000000..c98288a6 --- /dev/null +++ b/agent-library/agents/timelock-withdraw/commitment.txt @@ -0,0 +1 @@ +Funds may be withdrawn by the agent to the agent's own address one minute after deposit. Before that time, no withdrawals are permitted. diff --git a/agent-library/agents/timelock-withdraw/simulate-timelock.mjs b/agent-library/agents/timelock-withdraw/simulate-timelock.mjs new file mode 100644 index 00000000..8b443641 --- /dev/null +++ b/agent-library/agents/timelock-withdraw/simulate-timelock.mjs @@ -0,0 +1,28 @@ +import { extractTimelockTriggers } from '../../../agent/src/lib/timelock.js'; + +function simulate({ rulesText, depositTimestampMs, nowMs }) { + const deposits = [ + { + id: 'dep1', + timestampMs: depositTimestampMs, + }, + ]; + + const triggers = extractTimelockTriggers({ rulesText, deposits }); + const due = triggers.filter((trigger) => trigger.timestampMs <= nowMs); + + console.log('[sim] rules:', rulesText); + console.log('[sim] depositTimestampMs:', depositTimestampMs); + console.log('[sim] nowMs:', nowMs); + console.log('[sim] triggers:', triggers); + console.log('[sim] due:', due); +} + +const rulesText = + process.env.TIMELOCK_RULES ?? + 'Funds may be withdrawn five minutes after deposit.'; + +const nowMs = Date.now(); +const depositTimestampMs = nowMs - 10 * 60 * 1000; + +simulate({ rulesText, depositTimestampMs, nowMs }); diff --git a/agent-library/agents/timelock-withdraw/test-timelock.mjs b/agent-library/agents/timelock-withdraw/test-timelock.mjs new file mode 100644 index 00000000..7de29d2c --- /dev/null +++ b/agent-library/agents/timelock-withdraw/test-timelock.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import { extractTimelockTriggers } from '../../../agent/src/lib/timelock.js'; + +function run() { + const rulesAbsolute = + 'Funds may be withdrawn after January 15, 2026 12:00AM PST.'; + const absTriggers = extractTimelockTriggers({ rulesText: rulesAbsolute, deposits: [] }); + assert.equal(absTriggers.length, 1); + assert.equal(absTriggers[0].kind, 'absolute'); + assert.ok(absTriggers[0].timestampMs > 0); + + const rulesRelative = + 'Funds may be withdrawn five minutes after deposit.'; + const deposits = [ + { + id: 'dep1', + timestampMs: Date.UTC(2025, 0, 1, 0, 0, 0), + }, + ]; + const relTriggers = extractTimelockTriggers({ rulesText: rulesRelative, deposits }); + assert.equal(relTriggers.length, 1); + assert.equal(relTriggers[0].kind, 'relative'); + assert.equal(relTriggers[0].timestampMs, deposits[0].timestampMs + 5 * 60 * 1000); + + console.log('[test] timelock parsing OK'); +} + +run(); diff --git a/agent/README.md b/agent/README.md index bf9f794c..efab5e73 100644 --- a/agent/README.md +++ b/agent/README.md @@ -66,6 +66,7 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig - **Disputes assertions**: When the LLM flags a proposal as violating the rules, the agent posts the Oracle V3 bond and disputes the associated assertion. A human-readable rationale is logged locally. - **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. - **Optional LLM decisions**: If `OPENAI_API_KEY` is set, the runner will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions in the agent module. +- **Timelock triggers**: Parses plain language timelocks in rules (absolute dates or “X minutes after deposit”) and emits `timelock` signals when due. All other behavior is intentionally left out. Implement your own agent in `agent-library/agents//agent.js` to add commitment-specific logic and tool use. @@ -88,6 +89,33 @@ You can validate a module quickly: node agent/scripts/validate-agent.mjs --module=agent-library/agents/default/agent.js ``` +Default agent smoke test: + +```bash +node agent-library/agents/default/test-default-agent.mjs +``` + +### Timelock Agent Testing + +Unit test (plain JS): + +```bash +node agent-library/agents/timelock-withdraw/test-timelock.mjs +``` + +Simulation (prints due triggers): + +```bash +node agent-library/agents/timelock-withdraw/simulate-timelock.mjs +``` + +Run the timelock agent: + +```bash +AGENT_MODULE=agent-library/agents/timelock-withdraw/agent.js \ +node agent/src/index.js +``` + ## Local Dispute Simulation Use this to validate the dispute path against local mock contracts. diff --git a/agent/src/index.js b/agent/src/index.js index 5f20a76f..1fcd6141 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -19,6 +19,7 @@ import { import { callAgent, explainToolCalls } from './lib/llm.js'; import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; +import { extractTimelockTriggers } from './lib/timelock.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -38,6 +39,9 @@ let lastProposalCheckedBlock = config.startBlock; let lastNativeBalance; let ogContext; const proposalsByHash = new Map(); +const depositHistory = []; +const blockTimestampCache = new Map(); +const timelockTriggers = new Map(); async function loadAgentModule() { const modulePath = config.agentModule ?? 'agent-library/agents/default/agent.js'; @@ -58,6 +62,43 @@ async function loadAgentModule() { const { agentModule, commitmentText } = await loadAgentModule(); +async function getBlockTimestampMs(blockNumber) { + if (!blockNumber) return undefined; + const key = blockNumber.toString(); + if (blockTimestampCache.has(key)) { + return blockTimestampCache.get(key); + } + const block = await publicClient.getBlock({ blockNumber }); + const timestampMs = Number(block.timestamp) * 1000; + blockTimestampCache.set(key, timestampMs); + return timestampMs; +} + +function updateTimelockSchedule({ rulesText }) { + const triggers = extractTimelockTriggers({ + rulesText, + deposits: depositHistory, + }); + + for (const trigger of triggers) { + if (!timelockTriggers.has(trigger.id)) { + timelockTriggers.set(trigger.id, { ...trigger, fired: false }); + } + } +} + +function collectDueTimelocks(nowMs) { + const due = []; + for (const trigger of timelockTriggers.values()) { + if (trigger.fired) continue; + if (trigger.timestampMs <= nowMs) { + trigger.fired = true; + due.push(trigger); + } + } + return due; +} + async function decideOnSignals(signals) { if (!config.openAiApiKey) { return; @@ -132,6 +173,10 @@ async function decideOnSignals(signals) { async function agentLoop() { try { + const latestBlock = await publicClient.getBlockNumber(); + const latestBlockData = await publicClient.getBlock({ blockNumber: latestBlock }); + const nowMs = Number(latestBlockData.timestamp) * 1000; + const { deposits, lastCheckedBlock: nextCheckedBlock, lastNativeBalance: nextNative } = await pollCommitmentChanges({ publicClient, @@ -144,6 +189,14 @@ async function agentLoop() { lastCheckedBlock = nextCheckedBlock; lastNativeBalance = nextNative; + for (const deposit of deposits) { + const timestampMs = await getBlockTimestampMs(deposit.blockNumber); + depositHistory.push({ + ...deposit, + timestampMs, + }); + } + const { newProposals, lastProposalCheckedBlock: nextProposalBlock } = await pollProposalChanges({ publicClient, @@ -153,6 +206,10 @@ async function agentLoop() { }); lastProposalCheckedBlock = nextProposalBlock; + const rulesText = ogContext?.rules ?? commitmentText ?? ''; + updateTimelockSchedule({ rulesText }); + const dueTimelocks = collectDueTimelocks(nowMs); + const combinedSignals = deposits.concat( newProposals.map((proposal) => ({ kind: 'proposal', @@ -166,6 +223,17 @@ async function agentLoop() { })) ); + for (const trigger of dueTimelocks) { + combinedSignals.push({ + kind: 'timelock', + triggerId: trigger.id, + triggerTimestampMs: trigger.timestampMs, + source: trigger.source, + anchor: trigger.anchor, + deposit: trigger.deposit, + }); + } + if (combinedSignals.length > 0) { await decideOnSignals(combinedSignals); } diff --git a/agent/src/lib/timelock.js b/agent/src/lib/timelock.js new file mode 100644 index 00000000..1b4a5f32 --- /dev/null +++ b/agent/src/lib/timelock.js @@ -0,0 +1,136 @@ +const MONTHS_REGEX = + '(January|February|March|April|May|June|July|August|September|October|November|December)'; + +const NUMBER_WORDS = { + one: 1, + two: 2, + three: 3, + four: 4, + five: 5, + six: 6, + seven: 7, + eight: 8, + nine: 9, + ten: 10, + eleven: 11, + twelve: 12, +}; + +function parseNumber(value) { + if (!value) return null; + const trimmed = value.trim().toLowerCase(); + if (/^\d+$/.test(trimmed)) { + return Number(trimmed); + } + return NUMBER_WORDS[trimmed] ?? null; +} + +function unitToMs(unit) { + switch (unit.toLowerCase()) { + case 'minute': + case 'minutes': + return 60_000; + case 'hour': + case 'hours': + return 3_600_000; + case 'day': + case 'days': + return 86_400_000; + default: + return null; + } +} + +function extractAbsoluteTimelocks(rulesText) { + if (!rulesText) return []; + const regex = new RegExp( + `(after|on or after)\\s+${MONTHS_REGEX}\\s+\\d{1,2},\\s+\\d{4}[^.\\n]*`, + 'gi' + ); + const matches = []; + let match; + while ((match = regex.exec(rulesText)) !== null) { + const phrase = match[0]; + const raw = phrase.replace(/^(after|on or after)\\s+/i, '').trim(); + const trimmed = raw.replace(/[\\s.]+$/g, ''); + let parsed = Date.parse(trimmed); + if (!Number.isFinite(parsed)) { + const cleaned = trimmed.replace(/\s+(PST|PDT|UTC|GMT)\b/i, ''); + parsed = Date.parse(cleaned); + } + if (!Number.isFinite(parsed)) { + const dateOnlyMatch = cleanedDateOnly(trimmed); + if (dateOnlyMatch) { + parsed = Date.parse(dateOnlyMatch); + } + } + if (Number.isFinite(parsed)) { + matches.push({ + kind: 'absolute', + timestampMs: parsed, + source: phrase, + }); + } + } + return matches; +} + +function cleanedDateOnly(text) { + const dateOnly = new RegExp(`${MONTHS_REGEX}\\s+\\d{1,2},\\s+\\d{4}`, 'i'); + const match = text.match(dateOnly); + return match ? match[0] : null; +} + +function extractRelativeTimelocks(rulesText) { + if (!rulesText) return []; + const regex = /(\d+|\w+)\s*(minute|minutes|hour|hours|day|days)\s+after\s+deposit/gi; + const matches = []; + let match; + while ((match = regex.exec(rulesText)) !== null) { + const amount = parseNumber(match[1]); + const unitMs = unitToMs(match[2]); + if (!amount || !unitMs) continue; + matches.push({ + kind: 'relative', + offsetMs: amount * unitMs, + anchor: 'deposit', + source: match[0], + }); + } + return matches; +} + +function extractTimelockTriggers({ rulesText, deposits }) { + const triggers = []; + const absolute = extractAbsoluteTimelocks(rulesText); + for (const lock of absolute) { + triggers.push({ + id: `absolute:${lock.timestampMs}`, + kind: 'absolute', + timestampMs: lock.timestampMs, + source: lock.source, + }); + } + + const relative = extractRelativeTimelocks(rulesText); + if (relative.length > 0 && Array.isArray(deposits)) { + for (const deposit of deposits) { + if (!deposit?.timestampMs) continue; + for (const rule of relative) { + const ts = deposit.timestampMs + rule.offsetMs; + triggers.push({ + id: `relative:${deposit.id ?? deposit.transactionHash ?? deposit.blockNumber}:${rule.offsetMs}`, + kind: 'relative', + timestampMs: ts, + source: rule.source, + anchor: 'deposit', + deposit, + }); + } + } + } + + return triggers; +} + +export { extractTimelockTriggers }; From 00ceab2a07396479ca96c4874faf5039b45875b1 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 3 Feb 2026 12:08:27 -0800 Subject: [PATCH 041/174] handle batch deposits Signed-off-by: John Shutt --- agent/src/lib/polling.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index 34e3ee7f..08604d4a 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -64,6 +64,10 @@ async function pollCommitmentChanges({ amount: log.args.value, blockNumber: log.blockNumber, transactionHash: log.transactionHash, + logIndex: log.logIndex, + id: log.transactionHash + ? `${log.transactionHash}:${log.logIndex ?? '0'}` + : `${log.blockNumber.toString()}:${log.logIndex ?? '0'}`, }); } } @@ -83,6 +87,8 @@ async function pollCommitmentChanges({ amount: nativeBalance - lastNativeBalance, blockNumber: toBlock, transactionHash: undefined, + logIndex: undefined, + id: `native:${toBlock.toString()}:${(nativeBalance - lastNativeBalance).toString()}`, }); } From 528f9e258f6af8c80b3f012214e0b8c9af00517e Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 10:34:44 -0800 Subject: [PATCH 042/174] resolve absolute timelocks and action tracking Signed-off-by: John Shutt --- agent/src/index.js | 24 +++++++++++++++++++----- agent/src/lib/timelock.js | 15 +++++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 1fcd6141..a0a95fb5 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -92,16 +92,24 @@ function collectDueTimelocks(nowMs) { for (const trigger of timelockTriggers.values()) { if (trigger.fired) continue; if (trigger.timestampMs <= nowMs) { - trigger.fired = true; due.push(trigger); } } return due; } +function markTimelocksFired(triggers) { + for (const trigger of triggers) { + const existing = timelockTriggers.get(trigger.id); + if (existing) { + existing.fired = true; + } + } +} + async function decideOnSignals(signals) { if (!config.openAiApiKey) { - return; + return false; } if (!ogContext) { @@ -138,7 +146,7 @@ async function decideOnSignals(signals) { if (!allowTools && decision?.textDecision) { console.log('[agent] Opinion:', decision.textDecision); - return; + return true; } if (decision.toolCalls.length > 0) { @@ -160,15 +168,18 @@ async function decideOnSignals(signals) { console.log('[agent] Agent explanation:', explanation); } } - return; + return true; } if (decision?.textDecision) { console.log('[agent] Decision:', decision.textDecision); + return true; } } catch (error) { console.error('[agent] Agent call failed', error); } + + return false; } async function agentLoop() { @@ -235,7 +246,10 @@ async function agentLoop() { } if (combinedSignals.length > 0) { - await decideOnSignals(combinedSignals); + const decisionOk = await decideOnSignals(combinedSignals); + if (decisionOk && dueTimelocks.length > 0) { + markTimelocksFired(dueTimelocks); + } } await executeReadyProposals({ diff --git a/agent/src/lib/timelock.js b/agent/src/lib/timelock.js index 1b4a5f32..3354ac02 100644 --- a/agent/src/lib/timelock.js +++ b/agent/src/lib/timelock.js @@ -51,8 +51,9 @@ function extractAbsoluteTimelocks(rulesText) { let match; while ((match = regex.exec(rulesText)) !== null) { const phrase = match[0]; - const raw = phrase.replace(/^(after|on or after)\\s+/i, '').trim(); - const trimmed = raw.replace(/[\\s.]+$/g, ''); + const raw = phrase.replace(/^(after|on or after)\s+/i, '').trim(); + const trimmed = raw.replace(/[\s.]+$/g, ''); + const hasTimezone = /\b(PST|PDT|UTC|GMT|Z)\b/i.test(trimmed); let parsed = Date.parse(trimmed); if (!Number.isFinite(parsed)) { const cleaned = trimmed.replace(/\s+(PST|PDT|UTC|GMT)\b/i, ''); @@ -64,6 +65,16 @@ function extractAbsoluteTimelocks(rulesText) { parsed = Date.parse(dateOnlyMatch); } } + if (!hasTimezone && Number.isFinite(parsed)) { + const dateOnlyMatch = cleanedDateOnly(trimmed); + const withUtc = dateOnlyMatch + ? `${dateOnlyMatch} 00:00 UTC` + : `${trimmed} UTC`; + const utcParsed = Date.parse(withUtc); + if (Number.isFinite(utcParsed)) { + parsed = utcParsed; + } + } if (Number.isFinite(parsed)) { matches.push({ kind: 'absolute', From f72dda4d5b99e62d4a9650ef9bd3dca252766366 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 10:44:56 -0800 Subject: [PATCH 043/174] pass timestamp to agent and address bigint serializer Signed-off-by: John Shutt --- agent/src/lib/llm.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/agent/src/lib/llm.js b/agent/src/lib/llm.js index 14779947..ff21f408 100644 --- a/agent/src/lib/llm.js +++ b/agent/src/lib/llm.js @@ -77,6 +77,12 @@ async function callAgent({ amount: signal.amount !== undefined ? signal.amount.toString() : undefined, blockNumber: signal.blockNumber !== undefined ? signal.blockNumber.toString() : undefined, transactionHash: signal.transactionHash ? String(signal.transactionHash) : undefined, + timestampMs: + signal.timestampMs !== undefined ? signal.timestampMs.toString() : undefined, + triggerTimestampMs: + signal.triggerTimestampMs !== undefined + ? signal.triggerTimestampMs.toString() + : undefined, }; }); @@ -98,14 +104,17 @@ async function callAgent({ }, { role: 'user', - content: JSON.stringify({ - commitmentSafe: config.commitmentSafe, - ogModule: config.ogModule, - agentAddress, - ogContext: safeContext, - commitment: commitmentText, - signals: safeSignals, - }), + content: JSON.stringify( + { + commitmentSafe: config.commitmentSafe, + ogModule: config.ogModule, + agentAddress, + ogContext: safeContext, + commitment: commitmentText, + signals: safeSignals, + }, + (_, value) => (typeof value === 'bigint' ? value.toString() : value) + ), }, ], tools: allowTools ? tools : [], From 7c71846967812aa0cbc19682fef45ddf8dd6c073 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 10:45:59 -0800 Subject: [PATCH 044/174] coerce datetime to midnight only if no time of day is present Signed-off-by: John Shutt --- agent/src/lib/timelock.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/src/lib/timelock.js b/agent/src/lib/timelock.js index 3354ac02..18bde130 100644 --- a/agent/src/lib/timelock.js +++ b/agent/src/lib/timelock.js @@ -66,7 +66,8 @@ function extractAbsoluteTimelocks(rulesText) { } } if (!hasTimezone && Number.isFinite(parsed)) { - const dateOnlyMatch = cleanedDateOnly(trimmed); + const hasTime = /\b\d{1,2}:\d{2}(\s*[AP]M)?\b/i.test(trimmed); + const dateOnlyMatch = hasTime ? null : cleanedDateOnly(trimmed); const withUtc = dateOnlyMatch ? `${dateOnlyMatch} 00:00 UTC` : `${trimmed} UTC`; From a43cc9d9d4735aadba4ae88b845ca21463f6fadb Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 10:54:03 -0800 Subject: [PATCH 045/174] improve agent module loader Signed-off-by: John Shutt --- agent-library/README.md | 4 ++-- agent/.env.example | 2 +- agent/README.md | 6 +++--- agent/scripts/validate-agent.mjs | 6 ++++-- agent/src/index.js | 5 ++++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/agent-library/README.md b/agent-library/README.md index f2c7f9bf..66097e74 100644 --- a/agent-library/README.md +++ b/agent-library/README.md @@ -4,12 +4,12 @@ Each agent lives under `agent-library/agents//` and must include: - `agent.js`: decision logic and prompt construction. - `commitment.txt`: plain language commitment that the agent is designed to serve. -The runner loads the agent module via `AGENT_MODULE` (relative to repo root) and reads the adjacent `commitment.txt`. +The runner loads the agent module via `AGENT_MODULE` (agent name) and reads the adjacent `commitment.txt`. To add a new agent: 1. Copy `agent-library/agents/default/` to a new folder. 2. Update `agent.js` and `commitment.txt`. -3. Set `AGENT_MODULE=agent-library/agents//agent.js`. +3. Set `AGENT_MODULE=`. Example agents: - `agent-library/agents/default/`: generic agent using the commitment text. diff --git a/agent/.env.example b/agent/.env.example index 9c8e5ac1..21018dce 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -38,7 +38,7 @@ WATCH_NATIVE_BALANCE=true # START_BLOCK= # DEFAULT_DEPOSIT_ASSET= # DEFAULT_DEPOSIT_AMOUNT_WEI= -# AGENT_MODULE=agent-library/agents/default/agent.js +# AGENT_MODULE=default # Optional LLM # OPENAI_API_KEY= diff --git a/agent/README.md b/agent/README.md index efab5e73..1b0d1890 100644 --- a/agent/README.md +++ b/agent/README.md @@ -80,13 +80,13 @@ Set `PROPOSE_ENABLED` and `DISPUTE_ENABLED` to control behavior: ### Agent Modules & Commitments -Use `AGENT_MODULE` to point to an agent implementation under `agent-library/agents//agent.js`. +Use `AGENT_MODULE` to point to an agent implementation name (e.g., `default`, `timelock-withdraw`). The runner will load `agent-library/agents//agent.js`. Each agent directory must include a `commitment.txt` with the plain language commitment the agent is designed to serve. You can validate a module quickly: ```bash -node agent/scripts/validate-agent.mjs --module=agent-library/agents/default/agent.js +node agent/scripts/validate-agent.mjs --module=default ``` Default agent smoke test: @@ -112,7 +112,7 @@ node agent-library/agents/timelock-withdraw/simulate-timelock.mjs Run the timelock agent: ```bash -AGENT_MODULE=agent-library/agents/timelock-withdraw/agent.js \ +AGENT_MODULE=timelock-withdraw \ node agent/src/index.js ``` diff --git a/agent/scripts/validate-agent.mjs b/agent/scripts/validate-agent.mjs index 63abfe31..2cedfe10 100644 --- a/agent/scripts/validate-agent.mjs +++ b/agent/scripts/validate-agent.mjs @@ -9,8 +9,10 @@ function getArgValue(prefix) { async function main() { const moduleArg = getArgValue('--module='); - const modulePath = - moduleArg ?? process.env.AGENT_MODULE ?? 'agent-library/agents/default/agent.js'; + const agentRef = moduleArg ?? process.env.AGENT_MODULE ?? 'default'; + const modulePath = agentRef.includes('/') + ? agentRef + : `agent-library/agents/${agentRef}/agent.js`; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '../..'); diff --git a/agent/src/index.js b/agent/src/index.js index a0a95fb5..66db9bb1 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -44,7 +44,10 @@ const blockTimestampCache = new Map(); const timelockTriggers = new Map(); async function loadAgentModule() { - const modulePath = config.agentModule ?? 'agent-library/agents/default/agent.js'; + const agentRef = config.agentModule ?? 'default'; + const modulePath = agentRef.includes('/') + ? agentRef + : `agent-library/agents/${agentRef}/agent.js`; const resolvedPath = path.resolve(repoRoot, modulePath); const moduleUrl = pathToFileURL(resolvedPath).href; const agentModule = await import(moduleUrl); From 56d348418a710658e957f8a8881b69b1b2a4e72c Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:07:44 -0800 Subject: [PATCH 046/174] add agent registration files for existing agents Signed-off-by: John Shutt --- agent-library/agents/default/agent.json | 18 ++++++++++++++++++ .../agents/timelock-withdraw/agent.json | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 agent-library/agents/default/agent.json create mode 100644 agent-library/agents/timelock-withdraw/agent.json diff --git a/agent-library/agents/default/agent.json b/agent-library/agents/default/agent.json new file mode 100644 index 00000000..c69a97c4 --- /dev/null +++ b/agent-library/agents/default/agent.json @@ -0,0 +1,18 @@ +{ + "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", + "name": "Oya Default Agent", + "description": "Default Oya commitment agent that monitors deposits and proposals, and can propose or dispute actions based on the commitment rules.", + "image": "https://raw.githubusercontent.com/oya-commitments/oya-commitments/main/agent-library/agents/default/agent.png", + "endpoints": [ + { + "name": "agentWallet", + "endpoint": "eip155:11155111:0x0000000000000000000000000000000000000000" + } + ], + "registrations": [ + { + "agentId": 0, + "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e" + } + ] +} diff --git a/agent-library/agents/timelock-withdraw/agent.json b/agent-library/agents/timelock-withdraw/agent.json new file mode 100644 index 00000000..5686be6a --- /dev/null +++ b/agent-library/agents/timelock-withdraw/agent.json @@ -0,0 +1,18 @@ +{ + "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", + "name": "Oya Timelock Withdraw Agent", + "description": "Timelock agent that only withdraws funds to its own address after the commitment’s timelock conditions are satisfied.", + "image": "https://raw.githubusercontent.com/oya-commitments/oya-commitments/main/agent-library/agents/timelock-withdraw/agent.png", + "endpoints": [ + { + "name": "agentWallet", + "endpoint": "eip155:11155111:0x0000000000000000000000000000000000000000" + } + ], + "registrations": [ + { + "agentId": 0, + "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e" + } + ] +} From 56d827de19bdedc7807a134aef0b3cf814381ad0 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:11:16 -0800 Subject: [PATCH 047/174] =?UTF-8?q?helper=20script=20to=20update=20the=20E?= =?UTF-8?q?RC=E2=80=918004=20metadata=20after=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Shutt --- agent/README.md | 7 +++ agent/scripts/update-agent-metadata.mjs | 74 +++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 agent/scripts/update-agent-metadata.mjs diff --git a/agent/README.md b/agent/README.md index 1b0d1890..5fba369a 100644 --- a/agent/README.md +++ b/agent/README.md @@ -95,6 +95,13 @@ Default agent smoke test: node agent-library/agents/default/test-default-agent.mjs ``` +Update ERC-8004 metadata after registration: + +```bash +AGENT_ID=1 AGENT_WALLET=0x... \ +node agent/scripts/update-agent-metadata.mjs --agent=default +``` + ### Timelock Agent Testing Unit test (plain JS): diff --git a/agent/scripts/update-agent-metadata.mjs b/agent/scripts/update-agent-metadata.mjs new file mode 100644 index 00000000..9ba9f2e6 --- /dev/null +++ b/agent/scripts/update-agent-metadata.mjs @@ -0,0 +1,74 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +function getArgValue(prefix) { + const arg = process.argv.find((value) => value.startsWith(prefix)); + return arg ? arg.slice(prefix.length) : null; +} + +function formatCaip10(chainId, address) { + const cleaned = address.toLowerCase(); + return `eip155:${chainId}:${cleaned}`; +} + +async function main() { + const moduleArg = getArgValue('--agent='); + const agentName = moduleArg ?? process.env.AGENT_MODULE ?? 'default'; + const agentDir = agentName.includes('/') + ? agentName + : `agent-library/agents/${agentName}`; + const agentJsonPath = path.resolve(process.cwd(), agentDir, 'agent.json'); + + const agentIdArg = getArgValue('--agent-id='); + const agentId = agentIdArg ?? process.env.AGENT_ID; + if (!agentId) { + throw new Error('Missing --agent-id or AGENT_ID.'); + } + + const chainId = getArgValue('--chain-id=') ?? process.env.CHAIN_ID ?? '11155111'; + const wallet = getArgValue('--agent-wallet=') ?? process.env.AGENT_WALLET; + if (!wallet) { + throw new Error('Missing --agent-wallet or AGENT_WALLET.'); + } + + const registry = + getArgValue('--agent-registry=') ?? + process.env.AGENT_REGISTRY ?? + '0x8004A818BFB912233c491871b3d84c89A494BD9e'; + + const raw = await readFile(agentJsonPath, 'utf8'); + const json = JSON.parse(raw); + + json.endpoints = Array.isArray(json.endpoints) ? json.endpoints : []; + const walletEndpoint = formatCaip10(chainId, wallet); + const existingEndpoint = json.endpoints.find((item) => item?.name === 'agentWallet'); + if (existingEndpoint) { + existingEndpoint.endpoint = walletEndpoint; + } else { + json.endpoints.push({ name: 'agentWallet', endpoint: walletEndpoint }); + } + + json.registrations = Array.isArray(json.registrations) ? json.registrations : []; + const registryEndpoint = formatCaip10(chainId, registry); + const existingRegistration = json.registrations.find( + (item) => item?.agentRegistry === registryEndpoint + ); + if (existingRegistration) { + existingRegistration.agentId = Number(agentId); + } else { + json.registrations.push({ + agentId: Number(agentId), + agentRegistry: registryEndpoint, + }); + } + + await writeFile(agentJsonPath, `${JSON.stringify(json, null, 2)}\n`, 'utf8'); + + console.log('[agent] Updated metadata:', agentJsonPath); +} + +main().catch((error) => { + console.error('[agent] update failed:', error.message ?? error); + process.exit(1); +}); From 027207151e2f2fc2f1bb3de5f9837728ea175d2f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:18:45 -0800 Subject: [PATCH 048/174] add agent registry script Signed-off-by: John Shutt --- agent-library/agents/default/agent.json | 6 +- .../agents/timelock-withdraw/agent.json | 6 +- agent/README.md | 8 + agent/scripts/register-erc8004.mjs | 160 ++++++++++++++++++ 4 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 agent/scripts/register-erc8004.mjs diff --git a/agent-library/agents/default/agent.json b/agent-library/agents/default/agent.json index c69a97c4..781fab6d 100644 --- a/agent-library/agents/default/agent.json +++ b/agent-library/agents/default/agent.json @@ -6,13 +6,17 @@ "endpoints": [ { "name": "agentWallet", - "endpoint": "eip155:11155111:0x0000000000000000000000000000000000000000" + "endpoint": "eip155:11155111:0x2967c076182f0303037072670e744e26ed4a830f" } ], "registrations": [ { "agentId": 0, "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e" + }, + { + "agentId": 899, + "agentRegistry": "eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e" } ] } diff --git a/agent-library/agents/timelock-withdraw/agent.json b/agent-library/agents/timelock-withdraw/agent.json index 5686be6a..40539191 100644 --- a/agent-library/agents/timelock-withdraw/agent.json +++ b/agent-library/agents/timelock-withdraw/agent.json @@ -6,13 +6,17 @@ "endpoints": [ { "name": "agentWallet", - "endpoint": "eip155:11155111:0x0000000000000000000000000000000000000000" + "endpoint": "eip155:11155111:0x2967c076182f0303037072670e744e26ed4a830f" } ], "registrations": [ { "agentId": 0, "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e" + }, + { + "agentId": 900, + "agentRegistry": "eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e" } ] } diff --git a/agent/README.md b/agent/README.md index 5fba369a..edac03bf 100644 --- a/agent/README.md +++ b/agent/README.md @@ -102,6 +102,14 @@ AGENT_ID=1 AGENT_WALLET=0x... \ node agent/scripts/update-agent-metadata.mjs --agent=default ``` +Register an agent on Sepolia (and update metadata in-place): + +```bash +AGENT_MODULE=default \ +AGENT_URI=https://raw.githubusercontent.com////agent-library/agents/default/agent.json \ +node agent/scripts/register-erc8004.mjs +``` + ### Timelock Agent Testing Unit test (plain JS): diff --git a/agent/scripts/register-erc8004.mjs b/agent/scripts/register-erc8004.mjs new file mode 100644 index 00000000..99aefde7 --- /dev/null +++ b/agent/scripts/register-erc8004.mjs @@ -0,0 +1,160 @@ +import dotenv from 'dotenv'; +import { readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createPublicClient, createWalletClient, decodeEventLog, http } from 'viem'; +import { getAddress } from 'viem'; +import { createSignerClient } from '../src/lib/signer.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '../..'); + +dotenv.config(); +dotenv.config({ path: path.resolve(repoRoot, 'agent/.env') }); + +function getArgValue(prefix) { + const arg = process.argv.find((value) => value.startsWith(prefix)); + return arg ? arg.slice(prefix.length) : null; +} + +function formatCaip10(chainId, address) { + return `eip155:${chainId}:${address.toLowerCase()}`; +} + +const IDENTITY_REGISTRY_BY_CHAIN = { + 11155111: '0x8004A818BFB912233c491871b3d84c89A494BD9e', +}; + +const identityRegistryAbi = [ + { + type: 'function', + name: 'register', + inputs: [{ name: 'agentURI', type: 'string' }], + outputs: [{ name: 'agentId', type: 'uint256' }], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'Registered', + inputs: [ + { indexed: true, name: 'agentId', type: 'uint256' }, + { indexed: false, name: 'agentURI', type: 'string' }, + { indexed: true, name: 'owner', type: 'address' }, + ], + anonymous: false, + }, +]; + +async function main() { + const agentRef = getArgValue('--agent=') ?? process.env.AGENT_MODULE ?? 'default'; + const agentDir = agentRef.includes('/') + ? agentRef + : `agent-library/agents/${agentRef}`; + const agentJsonPath = path.resolve(repoRoot, agentDir, 'agent.json'); + + const agentUriArg = getArgValue('--agent-uri='); + const agentBranch = process.env.AGENT_BRANCH ?? 'erc-8004'; + const agentUriBase = + process.env.AGENT_URI_BASE ?? + `https://raw.githubusercontent.com/oyaprotocol/oya-commitments/${agentBranch}/agent-library/agents`; + const agentUri = + agentUriArg ?? + process.env.AGENT_URI ?? + (agentUriBase ? `${agentUriBase}/${agentRef}/agent.json` : null); + if (!agentUri) { + throw new Error('Missing --agent-uri or AGENT_URI (or AGENT_URI_BASE).'); + } + + const rpcUrl = process.env.RPC_URL; + if (!rpcUrl) { + throw new Error('Missing RPC_URL.'); + } + + const publicClient = createPublicClient({ transport: http(rpcUrl) }); + const { account, walletClient } = await createSignerClient({ rpcUrl }); + + const chainId = + Number(getArgValue('--chain-id=')) || + Number(process.env.CHAIN_ID) || + (await publicClient.getChainId()); + const registryOverride = getArgValue('--agent-registry=') ?? process.env.AGENT_REGISTRY; + const registry = + registryOverride ?? IDENTITY_REGISTRY_BY_CHAIN[chainId]; + if (!registry) { + throw new Error( + `No IdentityRegistry configured for chainId ${chainId}. Provide --agent-registry.` + ); + } + + const txHash = await walletClient.writeContract({ + address: getAddress(registry), + abi: identityRegistryAbi, + functionName: 'register', + args: [agentUri], + }); + + const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + let agentId; + for (const log of receipt.logs) { + try { + const decoded = decodeEventLog({ + abi: identityRegistryAbi, + data: log.data, + topics: log.topics, + }); + if (decoded.eventName === 'Registered') { + agentId = decoded.args.agentId; + break; + } + } catch (error) { + // ignore unrelated logs + } + } + + if (agentId === undefined) { + throw new Error('Failed to parse Registered event for agentId.'); + } + + const wallet = getArgValue('--agent-wallet=') ?? process.env.AGENT_WALLET ?? account.address; + const registryEndpoint = formatCaip10(chainId, registry); + const walletEndpoint = formatCaip10(chainId, wallet); + + const raw = await readFile(agentJsonPath, 'utf8'); + const json = JSON.parse(raw); + json.endpoints = Array.isArray(json.endpoints) ? json.endpoints : []; + const existingEndpoint = json.endpoints.find((item) => item?.name === 'agentWallet'); + if (existingEndpoint) { + existingEndpoint.endpoint = walletEndpoint; + } else { + json.endpoints.push({ name: 'agentWallet', endpoint: walletEndpoint }); + } + + json.registrations = Array.isArray(json.registrations) ? json.registrations : []; + const existingRegistration = json.registrations.find( + (item) => item?.agentRegistry === registryEndpoint + ); + if (existingRegistration) { + existingRegistration.agentId = Number(agentId); + } else { + json.registrations.push({ + agentId: Number(agentId), + agentRegistry: registryEndpoint, + }); + } + + await writeFile(agentJsonPath, `${JSON.stringify(json, null, 2)}\n`, 'utf8'); + + console.log('[agent] Registered:', { + agentId: Number(agentId), + agentRegistry: registryEndpoint, + agentURI: agentUri, + txHash, + }); + console.log('[agent] Updated metadata:', agentJsonPath); +} + +main().catch((error) => { + console.error('[agent] registration failed:', error.message ?? error); + process.exit(1); +}); From f26933b1f7ded2a94ff6364e3f08018016585d7f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:21:27 -0800 Subject: [PATCH 049/174] improve agent registration script Signed-off-by: John Shutt --- agent-library/agents/timelock-withdraw/agent.json | 2 +- agent/.env.example | 5 +++++ agent/README.md | 7 ++++++- agent/scripts/register-erc8004.mjs | 11 +++++++++-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/agent-library/agents/timelock-withdraw/agent.json b/agent-library/agents/timelock-withdraw/agent.json index 40539191..5e0614aa 100644 --- a/agent-library/agents/timelock-withdraw/agent.json +++ b/agent-library/agents/timelock-withdraw/agent.json @@ -15,7 +15,7 @@ "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e" }, { - "agentId": 900, + "agentId": 901, "agentRegistry": "eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e" } ] diff --git a/agent/.env.example b/agent/.env.example index 21018dce..6e766c0a 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -40,6 +40,11 @@ WATCH_NATIVE_BALANCE=true # DEFAULT_DEPOSIT_AMOUNT_WEI= # AGENT_MODULE=default +# ERC-8004 registration helpers +# AGENT_ORG=oyaprotocol +# AGENT_REPO=oya-commitments +# AGENT_BRANCH=erc-8004 + # Optional LLM # OPENAI_API_KEY= # OPENAI_MODEL=gpt-4.1-mini diff --git a/agent/README.md b/agent/README.md index edac03bf..e904815c 100644 --- a/agent/README.md +++ b/agent/README.md @@ -106,10 +106,15 @@ Register an agent on Sepolia (and update metadata in-place): ```bash AGENT_MODULE=default \ -AGENT_URI=https://raw.githubusercontent.com////agent-library/agents/default/agent.json \ +AGENT_BRANCH= \ node agent/scripts/register-erc8004.mjs ``` +The script infers `AGENT_URI` as: +`https://raw.githubusercontent.com////agent-library/agents//agent.json` +Defaults: `AGENT_ORG=oyaprotocol`, `AGENT_REPO=oya-commitments` +Override with `AGENT_URI` or `AGENT_URI_BASE` if needed. + ### Timelock Agent Testing Unit test (plain JS): diff --git a/agent/scripts/register-erc8004.mjs b/agent/scripts/register-erc8004.mjs index 99aefde7..4ffb7c9c 100644 --- a/agent/scripts/register-erc8004.mjs +++ b/agent/scripts/register-erc8004.mjs @@ -54,10 +54,17 @@ async function main() { const agentJsonPath = path.resolve(repoRoot, agentDir, 'agent.json'); const agentUriArg = getArgValue('--agent-uri='); - const agentBranch = process.env.AGENT_BRANCH ?? 'erc-8004'; + const agentOrg = process.env.AGENT_ORG ?? 'oyaprotocol'; + const agentRepo = process.env.AGENT_REPO ?? 'oya-commitments'; + const agentBranch = process.env.AGENT_BRANCH; + if (!agentBranch && !process.env.AGENT_URI_BASE && !process.env.AGENT_URI) { + throw new Error('Missing AGENT_BRANCH (or provide AGENT_URI / AGENT_URI_BASE).'); + } const agentUriBase = process.env.AGENT_URI_BASE ?? - `https://raw.githubusercontent.com/oyaprotocol/oya-commitments/${agentBranch}/agent-library/agents`; + (agentBranch + ? `https://raw.githubusercontent.com/${agentOrg}/${agentRepo}/${agentBranch}/agent-library/agents` + : null); const agentUri = agentUriArg ?? process.env.AGENT_URI ?? From 6e780eadd577e7c369f5a8b44b328215486afc09 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:28:02 -0800 Subject: [PATCH 050/174] address review comments Signed-off-by: John Shutt --- agent-library/agents/default/agent.json | 4 -- .../agents/timelock-withdraw/agent.json | 4 -- agent/scripts/register-erc8004.mjs | 44 ++++++++++++++----- agent/scripts/update-agent-metadata.mjs | 42 +++++++++++++----- 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/agent-library/agents/default/agent.json b/agent-library/agents/default/agent.json index 781fab6d..8011a85d 100644 --- a/agent-library/agents/default/agent.json +++ b/agent-library/agents/default/agent.json @@ -10,10 +10,6 @@ } ], "registrations": [ - { - "agentId": 0, - "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e" - }, { "agentId": 899, "agentRegistry": "eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e" diff --git a/agent-library/agents/timelock-withdraw/agent.json b/agent-library/agents/timelock-withdraw/agent.json index 5e0614aa..b93c6ae2 100644 --- a/agent-library/agents/timelock-withdraw/agent.json +++ b/agent-library/agents/timelock-withdraw/agent.json @@ -10,10 +10,6 @@ } ], "registrations": [ - { - "agentId": 0, - "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e" - }, { "agentId": 901, "agentRegistry": "eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e" diff --git a/agent/scripts/register-erc8004.mjs b/agent/scripts/register-erc8004.mjs index 4ffb7c9c..9e1a3d1f 100644 --- a/agent/scripts/register-erc8004.mjs +++ b/agent/scripts/register-erc8004.mjs @@ -22,6 +22,13 @@ function formatCaip10(chainId, address) { return `eip155:${chainId}:${address.toLowerCase()}`; } +function normalizeAgentName(agentRef) { + if (!agentRef) return 'default'; + if (!agentRef.includes('/')) return agentRef; + const trimmed = agentRef.endsWith('.js') ? path.dirname(agentRef) : agentRef; + return path.basename(trimmed); +} + const IDENTITY_REGISTRY_BY_CHAIN = { 11155111: '0x8004A818BFB912233c491871b3d84c89A494BD9e', }; @@ -48,9 +55,10 @@ const identityRegistryAbi = [ async function main() { const agentRef = getArgValue('--agent=') ?? process.env.AGENT_MODULE ?? 'default'; + const agentName = normalizeAgentName(agentRef); const agentDir = agentRef.includes('/') ? agentRef - : `agent-library/agents/${agentRef}`; + : `agent-library/agents/${agentName}`; const agentJsonPath = path.resolve(repoRoot, agentDir, 'agent.json'); const agentUriArg = getArgValue('--agent-uri='); @@ -68,7 +76,7 @@ async function main() { const agentUri = agentUriArg ?? process.env.AGENT_URI ?? - (agentUriBase ? `${agentUriBase}/${agentRef}/agent.json` : null); + (agentUriBase ? `${agentUriBase}/${agentName}/agent.json` : null); if (!agentUri) { throw new Error('Missing --agent-uri or AGENT_URI (or AGENT_URI_BASE).'); } @@ -124,7 +132,7 @@ async function main() { } const wallet = getArgValue('--agent-wallet=') ?? process.env.AGENT_WALLET ?? account.address; - const registryEndpoint = formatCaip10(chainId, registry); + const registryEndpoint = formatCaip10(chainId, registry).toLowerCase(); const walletEndpoint = formatCaip10(chainId, wallet); const raw = await readFile(agentJsonPath, 'utf8'); @@ -138,17 +146,33 @@ async function main() { } json.registrations = Array.isArray(json.registrations) ? json.registrations : []; - const existingRegistration = json.registrations.find( - (item) => item?.agentRegistry === registryEndpoint - ); - if (existingRegistration) { - existingRegistration.agentId = Number(agentId); - } else { - json.registrations.push({ + const normalizedRegistrations = []; + let updated = false; + for (const entry of json.registrations) { + if (!entry?.agentRegistry) continue; + const normalizedRegistry = String(entry.agentRegistry).toLowerCase(); + if (normalizedRegistry === registryEndpoint) { + if (!updated) { + normalizedRegistrations.push({ + agentId: Number(agentId), + agentRegistry: registryEndpoint, + }); + updated = true; + } + continue; + } + normalizedRegistrations.push({ + ...entry, + agentRegistry: normalizedRegistry, + }); + } + if (!updated) { + normalizedRegistrations.push({ agentId: Number(agentId), agentRegistry: registryEndpoint, }); } + json.registrations = normalizedRegistrations; await writeFile(agentJsonPath, `${JSON.stringify(json, null, 2)}\n`, 'utf8'); diff --git a/agent/scripts/update-agent-metadata.mjs b/agent/scripts/update-agent-metadata.mjs index 9ba9f2e6..50ef5961 100644 --- a/agent/scripts/update-agent-metadata.mjs +++ b/agent/scripts/update-agent-metadata.mjs @@ -13,10 +13,12 @@ function formatCaip10(chainId, address) { } async function main() { - const moduleArg = getArgValue('--agent='); - const agentName = moduleArg ?? process.env.AGENT_MODULE ?? 'default'; - const agentDir = agentName.includes('/') - ? agentName + const agentRef = getArgValue('--agent=') ?? process.env.AGENT_MODULE ?? 'default'; + const agentName = agentRef.includes('/') + ? path.basename(agentRef.endsWith('.js') ? path.dirname(agentRef) : agentRef) + : agentRef; + const agentDir = agentRef.includes('/') + ? agentRef : `agent-library/agents/${agentName}`; const agentJsonPath = path.resolve(process.cwd(), agentDir, 'agent.json'); @@ -50,18 +52,34 @@ async function main() { } json.registrations = Array.isArray(json.registrations) ? json.registrations : []; - const registryEndpoint = formatCaip10(chainId, registry); - const existingRegistration = json.registrations.find( - (item) => item?.agentRegistry === registryEndpoint - ); - if (existingRegistration) { - existingRegistration.agentId = Number(agentId); - } else { - json.registrations.push({ + const registryEndpoint = formatCaip10(chainId, registry).toLowerCase(); + const normalizedRegistrations = []; + let updated = false; + for (const entry of json.registrations) { + if (!entry?.agentRegistry) continue; + const normalizedRegistry = String(entry.agentRegistry).toLowerCase(); + if (normalizedRegistry === registryEndpoint) { + if (!updated) { + normalizedRegistrations.push({ + agentId: Number(agentId), + agentRegistry: registryEndpoint, + }); + updated = true; + } + continue; + } + normalizedRegistrations.push({ + ...entry, + agentRegistry: normalizedRegistry, + }); + } + if (!updated) { + normalizedRegistrations.push({ agentId: Number(agentId), agentRegistry: registryEndpoint, }); } + json.registrations = normalizedRegistrations; await writeFile(agentJsonPath, `${JSON.stringify(json, null, 2)}\n`, 'utf8'); From 6a9d6f7cce190bb835d4312d51f96efc9b62d0f6 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:32:45 -0800 Subject: [PATCH 051/174] normalize agent directory file paths Signed-off-by: John Shutt --- agent/scripts/register-erc8004.mjs | 4 +++- agent/scripts/update-agent-metadata.mjs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/agent/scripts/register-erc8004.mjs b/agent/scripts/register-erc8004.mjs index 9e1a3d1f..f93f4575 100644 --- a/agent/scripts/register-erc8004.mjs +++ b/agent/scripts/register-erc8004.mjs @@ -57,7 +57,9 @@ async function main() { const agentRef = getArgValue('--agent=') ?? process.env.AGENT_MODULE ?? 'default'; const agentName = normalizeAgentName(agentRef); const agentDir = agentRef.includes('/') - ? agentRef + ? agentRef.endsWith('.js') + ? path.dirname(agentRef) + : agentRef : `agent-library/agents/${agentName}`; const agentJsonPath = path.resolve(repoRoot, agentDir, 'agent.json'); diff --git a/agent/scripts/update-agent-metadata.mjs b/agent/scripts/update-agent-metadata.mjs index 50ef5961..2b5b1cc1 100644 --- a/agent/scripts/update-agent-metadata.mjs +++ b/agent/scripts/update-agent-metadata.mjs @@ -18,7 +18,9 @@ async function main() { ? path.basename(agentRef.endsWith('.js') ? path.dirname(agentRef) : agentRef) : agentRef; const agentDir = agentRef.includes('/') - ? agentRef + ? agentRef.endsWith('.js') + ? path.dirname(agentRef) + : agentRef : `agent-library/agents/${agentName}`; const agentJsonPath = path.resolve(process.cwd(), agentDir, 'agent.json'); From 05aad9835dffe3319b78cfbe7b902d699839e4a7 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:40:30 -0800 Subject: [PATCH 052/174] add support for more chains for erc-8004 Signed-off-by: John Shutt --- .../agents/timelock-withdraw/agent.json | 2 +- agent/.env.example | 1 + agent/README.md | 1 + agent/scripts/register-erc8004.mjs | 67 +++++++++++++++++-- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/agent-library/agents/timelock-withdraw/agent.json b/agent-library/agents/timelock-withdraw/agent.json index b93c6ae2..ba64eceb 100644 --- a/agent-library/agents/timelock-withdraw/agent.json +++ b/agent-library/agents/timelock-withdraw/agent.json @@ -11,7 +11,7 @@ ], "registrations": [ { - "agentId": 901, + "agentId": 903, "agentRegistry": "eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e" } ] diff --git a/agent/.env.example b/agent/.env.example index 6e766c0a..288bbb24 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -44,6 +44,7 @@ WATCH_NATIVE_BALANCE=true # AGENT_ORG=oyaprotocol # AGENT_REPO=oya-commitments # AGENT_BRANCH=erc-8004 +# AGENT_NETWORK=ethereum-sepolia # Optional LLM # OPENAI_API_KEY= diff --git a/agent/README.md b/agent/README.md index e904815c..3b5ec7eb 100644 --- a/agent/README.md +++ b/agent/README.md @@ -107,6 +107,7 @@ Register an agent on Sepolia (and update metadata in-place): ```bash AGENT_MODULE=default \ AGENT_BRANCH= \ +AGENT_NETWORK=ethereum-sepolia \ node agent/scripts/register-erc8004.mjs ``` diff --git a/agent/scripts/register-erc8004.mjs b/agent/scripts/register-erc8004.mjs index f93f4575..c6930e17 100644 --- a/agent/scripts/register-erc8004.mjs +++ b/agent/scripts/register-erc8004.mjs @@ -29,8 +29,59 @@ function normalizeAgentName(agentRef) { return path.basename(trimmed); } -const IDENTITY_REGISTRY_BY_CHAIN = { - 11155111: '0x8004A818BFB912233c491871b3d84c89A494BD9e', +const REGISTRY_BY_NETWORK = { + ethereum: { + identityRegistry: '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432', + reputationRegistry: '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63', + }, + 'ethereum-sepolia': { + identityRegistry: '0x8004A818BFB912233c491871b3d84c89A494BD9e', + reputationRegistry: '0x8004B663056A597Dffe9eCcC1965A193B7388713', + }, + base: { + identityRegistry: '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432', + reputationRegistry: '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63', + }, + 'base-sepolia': { + identityRegistry: '0x8004A818BFB912233c491871b3d84c89A494BD9e', + reputationRegistry: '0x8004B663056A597Dffe9eCcC1965A193B7388713', + }, + polygon: { + identityRegistry: '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432', + reputationRegistry: '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63', + }, + 'polygon-amoy': { + identityRegistry: '0x8004A818BFB912233c491871b3d84c89A494BD9e', + reputationRegistry: '0x8004B663056A597Dffe9eCcC1965A193B7388713', + }, + gnosis: { + identityRegistry: '0x8004A818BFB912233c491871b3d84c89A494BD9e', + reputationRegistry: '0x8004B663056A597Dffe9eCcC1965A193B7388713', + }, + scroll: { + identityRegistry: '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432', + reputationRegistry: '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63', + }, + 'scroll-testnet': { + identityRegistry: '0x8004A818BFB912233c491871b3d84c89A494BD9e', + reputationRegistry: '0x8004B663056A597Dffe9eCcC1965A193B7388713', + }, + monad: { + identityRegistry: '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432', + reputationRegistry: '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63', + }, + 'monad-testnet': { + identityRegistry: '0x8004A818BFB912233c491871b3d84c89A494BD9e', + reputationRegistry: '0x8004B663056A597Dffe9eCcC1965A193B7388713', + }, + bsc: { + identityRegistry: '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432', + reputationRegistry: '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63', + }, + 'bsc-testnet': { + identityRegistry: '0x8004A818BFB912233c491871b3d84c89A494BD9e', + reputationRegistry: '0x8004B663056A597Dffe9eCcC1965A193B7388713', + }, }; const identityRegistryAbi = [ @@ -96,11 +147,19 @@ async function main() { Number(process.env.CHAIN_ID) || (await publicClient.getChainId()); const registryOverride = getArgValue('--agent-registry=') ?? process.env.AGENT_REGISTRY; + const network = + getArgValue('--network=') ?? + process.env.AGENT_NETWORK ?? + (chainId === 1 + ? 'ethereum' + : chainId === 11155111 + ? 'ethereum-sepolia' + : undefined); const registry = - registryOverride ?? IDENTITY_REGISTRY_BY_CHAIN[chainId]; + registryOverride ?? (network ? REGISTRY_BY_NETWORK[network]?.identityRegistry : undefined); if (!registry) { throw new Error( - `No IdentityRegistry configured for chainId ${chainId}. Provide --agent-registry.` + `No IdentityRegistry configured for chainId ${chainId}. Provide --agent-registry or set AGENT_NETWORK.` ); } From 6d439589d0607625cea9ac62f58987fdd247a83b Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:41:42 -0800 Subject: [PATCH 053/174] improve handling of env variables and directory resolution Signed-off-by: John Shutt --- agent/scripts/update-agent-metadata.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/agent/scripts/update-agent-metadata.mjs b/agent/scripts/update-agent-metadata.mjs index 2b5b1cc1..d98a38c7 100644 --- a/agent/scripts/update-agent-metadata.mjs +++ b/agent/scripts/update-agent-metadata.mjs @@ -1,7 +1,15 @@ +import dotenv from 'dotenv'; import { readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '../..'); + +dotenv.config(); +dotenv.config({ path: path.resolve(repoRoot, 'agent/.env') }); + function getArgValue(prefix) { const arg = process.argv.find((value) => value.startsWith(prefix)); return arg ? arg.slice(prefix.length) : null; @@ -22,7 +30,7 @@ async function main() { ? path.dirname(agentRef) : agentRef : `agent-library/agents/${agentName}`; - const agentJsonPath = path.resolve(process.cwd(), agentDir, 'agent.json'); + const agentJsonPath = path.resolve(repoRoot, agentDir, 'agent.json'); const agentIdArg = getArgValue('--agent-id='); const agentId = agentIdArg ?? process.env.AGENT_ID; From 910d92ad313c9a13b9664c03c1dda6ba05fdd231 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 11:46:31 -0800 Subject: [PATCH 054/174] store agentId as a string rather than a number in metadata, to avoid overrunning JS safe integer values Signed-off-by: John Shutt --- agent/scripts/register-erc8004.mjs | 7 ++++--- agent/scripts/update-agent-metadata.mjs | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/agent/scripts/register-erc8004.mjs b/agent/scripts/register-erc8004.mjs index c6930e17..3c9443ae 100644 --- a/agent/scripts/register-erc8004.mjs +++ b/agent/scripts/register-erc8004.mjs @@ -206,6 +206,7 @@ async function main() { json.endpoints.push({ name: 'agentWallet', endpoint: walletEndpoint }); } + const agentIdValue = String(agentId); json.registrations = Array.isArray(json.registrations) ? json.registrations : []; const normalizedRegistrations = []; let updated = false; @@ -215,7 +216,7 @@ async function main() { if (normalizedRegistry === registryEndpoint) { if (!updated) { normalizedRegistrations.push({ - agentId: Number(agentId), + agentId: agentIdValue, agentRegistry: registryEndpoint, }); updated = true; @@ -229,7 +230,7 @@ async function main() { } if (!updated) { normalizedRegistrations.push({ - agentId: Number(agentId), + agentId: agentIdValue, agentRegistry: registryEndpoint, }); } @@ -238,7 +239,7 @@ async function main() { await writeFile(agentJsonPath, `${JSON.stringify(json, null, 2)}\n`, 'utf8'); console.log('[agent] Registered:', { - agentId: Number(agentId), + agentId: agentIdValue, agentRegistry: registryEndpoint, agentURI: agentUri, txHash, diff --git a/agent/scripts/update-agent-metadata.mjs b/agent/scripts/update-agent-metadata.mjs index d98a38c7..2981fcc5 100644 --- a/agent/scripts/update-agent-metadata.mjs +++ b/agent/scripts/update-agent-metadata.mjs @@ -63,6 +63,7 @@ async function main() { json.registrations = Array.isArray(json.registrations) ? json.registrations : []; const registryEndpoint = formatCaip10(chainId, registry).toLowerCase(); + const agentIdValue = String(agentId); const normalizedRegistrations = []; let updated = false; for (const entry of json.registrations) { @@ -71,7 +72,7 @@ async function main() { if (normalizedRegistry === registryEndpoint) { if (!updated) { normalizedRegistrations.push({ - agentId: Number(agentId), + agentId: agentIdValue, agentRegistry: registryEndpoint, }); updated = true; @@ -85,7 +86,7 @@ async function main() { } if (!updated) { normalizedRegistrations.push({ - agentId: Number(agentId), + agentId: agentIdValue, agentRegistry: registryEndpoint, }); } From 2d7882be2c3b12a293c9a8f8ebd0a463c63c1593 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 4 Feb 2026 18:31:36 -0800 Subject: [PATCH 055/174] update og identifier check on sepolia to ASSERT_TRUTH2 Signed-off-by: John Shutt --- agent/src/lib/og.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/lib/og.js b/agent/src/lib/og.js index bcc41eca..c24d54de 100644 --- a/agent/src/lib/og.js +++ b/agent/src/lib/og.js @@ -90,7 +90,7 @@ async function logOgFundingStatus({ publicClient, ogModule, account }) { try { const chainId = await publicClient.getChainId(); const expectedIdentifierStr = - chainId === 11155111 ? 'ASSERT_TRUTH' : 'ASSERT_TRUTH2'; + chainId === 11155111 ? 'ASSERT_TRUTH2' : 'ASSERT_TRUTH2'; const expectedIdentifier = stringToHex(expectedIdentifierStr, { size: 32 }); const [collateral, bondAmount, optimisticOracle, identifier] = await Promise.all([ From 2788785458e28cd2a09c0cf41ded9e977ce77464 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Thu, 5 Feb 2026 08:33:50 -0800 Subject: [PATCH 056/174] readme refactor Signed-off-by: John Shutt --- README.md | 277 +++++------------------------------------------------- 1 file changed, 22 insertions(+), 255 deletions(-) diff --git a/README.md b/README.md index 94bc64ba..bc301707 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,23 @@ # Oya Commitments -This repo contains everything needed to set up **Oya commitments**: smart contracts controlled by plain language rules and the agents that serve them. It includes the Solidity contracts, deployment scripts, an optional web UI, and an offchain agent scaffold. +Oya Commitments are Safe-based commitments controlled by plain-language rules and enforced via an Optimistic Governor module. This repo contains the Solidity contracts, deployment scripts, an optional web UI, and an offchain agent scaffold. ## Beta Disclaimer This is beta software provided “as is.” Use at your own risk. No guarantees of safety, correctness, or fitness for any purpose. -## What Is a Commitment? +## How It Works (At a Glance) -A commitment is a Safe controlled by an Optimistic Governor module. The commitment rules are written in plain language (stored onchain or via a URI) and enforced through the Optimistic Governor challenge process. Agents (which can be either AI-driven or deterministic) can interpret onchain and offchain signals and propose valid transactions baed on the commitment's rules. +- Write plain-language rules that define what the commitment may do. +- Deploy a Safe wired to an Optimistic Governor module with those rules. +- An agent or user proposes transactions via the module and posts a bond. +- If no challenge occurs during the window, the proposal is executed by the Safe. -## Concepts (How It Works) - -1. **Rules**: You define plain language rules for what the commitment may do. -2. **Control**: A Safe is deployed and wired to an Optimistic Governor module with those rules. -3. **Proposals**: An agent (or user) proposes transfers via the module and posts the bond. -4. **Challenge Window**: If no one challenges during the period, the proposal can be executed. -5. **Execution**: The Safe executes the approved transfer. - -## Repo Layout - -- `src/` Solidity contracts -- `script/` Foundry deployment and ops scripts -- `test/` Foundry tests -- `agent/` Offchain agent scaffold -- `frontend/` Web UI for configuring and deploying commitments -- `lib/` External dependencies (Foundry) - -## Quick Start (Deploy a Commitment) +## Quick Start 1. Install Foundry: https://book.getfoundry.sh/ -2. Set required environment variables. -3. Run the deployment script. +2. Set required environment variables (see `docs/deployment.md`). +3. Run the deployment script: ```shell forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ @@ -40,238 +26,19 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis --private-key ``` -## Required Environment Variables - -- `DEPLOYER_PK`: Private key for the deployer. -- `OG_COLLATERAL`: Address of the ERC20 collateral token. -- `OG_BOND_AMOUNT`: Bond amount for challenges. -- `OG_RULES`: Plain language rules for the commitment. - -## Alternative Signing Methods - -You can avoid storing raw private keys in `.env` by using the agent’s signer helpers and injecting the key at runtime for Forge scripts. - -Supported signer types: - -- `env`: `PRIVATE_KEY` -- `keystore`: `KEYSTORE_PATH`, `KEYSTORE_PASSWORD` -- `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` -- `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` -- `kms` / `vault-signer` / `rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (RPC signer that accepts `eth_sendTransaction`) - -### Use With Forge Scripts (Deployments + Interactions) - -The `agent/with-signer.mjs` helper resolves a signer and injects it as an env var (e.g., `DEPLOYER_PK`, `PROPOSER_PK`, `EXECUTOR_PK`) for any Forge script. - -```shell -# Private key from env -SIGNER_TYPE=env PRIVATE_KEY=0x... \ - node agent/with-signer.mjs --env DEPLOYER_PK -- \ - forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast - -# Encrypted keystore -SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/deployer.json KEYSTORE_PASSWORD=... \ - node agent/with-signer.mjs --env DEPLOYER_PK -- \ - forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast - -# OS keychain -SIGNER_TYPE=keychain KEYCHAIN_SERVICE=og-deployer KEYCHAIN_ACCOUNT=deployer \ - node agent/with-signer.mjs --env DEPLOYER_PK -- \ - forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast - -# Vault KV (private key stored as a secret) -SIGNER_TYPE=vault VAULT_ADDR=https://vault.example.com VAULT_TOKEN=... VAULT_SECRET_PATH=secret/data/og-deployer \ - node agent/with-signer.mjs --env DEPLOYER_PK -- \ - forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast -``` - -For interactions, swap the env var: +## Documentation -```shell -# Propose a transfer with a non-env signer -SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/proposer.json KEYSTORE_PASSWORD=... \ - node agent/with-signer.mjs --env PROPOSER_PK -- \ - forge script script/ProposeCommitmentTransfer.s.sol:ProposeCommitmentTransfer \ - --rpc-url $MAINNET_RPC_URL \ - --broadcast -``` +- Deployment and configuration: `docs/deployment.md` +- Signer options and `with-signer` helper: `docs/signers.md` +- Offchain agent usage: `docs/agent.md` +- Web frontend: `docs/frontend.md` +- Testing and common commands: `docs/testing.md` -Forge scripts still expect a private key env var, so for KMS/Vault signing without exporting private keys you’ll need an RPC signer proxy that can provide `eth_sendTransaction` (set `SIGNER_RPC_URL` and `SIGNER_ADDRESS`). - -## Optional Overrides - -- `SAFE_SALT_NONCE`, `SAFE_THRESHOLD`, `SAFE_OWNERS` -- `OG_SALT_NONCE`, `OG_CHALLENGE_PERIOD`, `OG_RULES_URI` -- `OG_MASTER_COPY`, `SAFE_SINGLETON`, `SAFE_FALLBACK_HANDLER` -- `MODULE_PROXY_FACTORY` - -## Offchain Agent (Serve a Commitment) - -The agent in `agent/` can propose and execute transactions via the Optimistic Governor module. It ships with generic tools; customize the decision logic, signal monitoring, and overall behavior to match your commitment rules. - -```shell -cd agent -npm install -cp .env.example .env # fill in RPC_URL, PRIVATE_KEY, COMMITMENT_SAFE, OG_MODULE, WATCH_ASSETS -npm start -``` - -Built-in tools include: - -- `postBondAndPropose` -- `makeDeposit` -- `pollCommitmentChanges` - -We will be building a library of agents showcasing different types of commitments, and welcome community contributions! - -## Web Frontend - -`frontend/` provides a lightweight UI for entering Safe + Optimistic Governor parameters and generating the same deployment flow as the script. It uses the connected wallet to submit transactions. - -```shell -cd frontend -npm install -npm run dev -``` - -Environment overrides are minimal today. The UI supports `MODULE_PROXY_FACTORY` (optionally with `VITE_` or `NEXT_PUBLIC_` prefixes). Other defaults are currently hardcoded in `frontend/src/App.jsx` and can be edited there or wired to env vars. - -## Example `.env` - -```ini -# Required -DEPLOYER_PK=0xabc123... -OG_COLLATERAL=0x1111111111111111111111111111111111111111 -OG_BOND_AMOUNT=250000000 -OG_RULES="Any assets deposited in this Commitment may be transferred back to the depositor before January 15th, 2026 (12:00AM PST). After the deadline, assets may only be transferred to jdshutt.eth. If a third party is initiating the transfer after the deadline, they may take a 10% cut of the assets being transferred as a fee." - -# Safe overrides -SAFE_OWNERS=0x2222222222222222222222222222222222222222,0x3333333333333333333333333333333333333333 -SAFE_THRESHOLD=2 -SAFE_SALT_NONCE=12345 - -# Optimistic Governor overrides -OG_CHALLENGE_PERIOD=604800 -OG_RULES_URI=ipfs://bafy... -OG_SALT_NONCE=67890 - -# Optional factory / master copy overrides -MODULE_PROXY_FACTORY=0x4444444444444444444444444444444444444444 -OG_MASTER_COPY=0x5555555555555555555555555555555555555555 -SAFE_SINGLETON=0x6666666666666666666666666666666666666666 -SAFE_FALLBACK_HANDLER=0x7777777777777777777777777777777777777777 -``` - -## Common Commands - -```shell -forge build -forge test -forge fmt -``` - -## Local Testing (Anvil) - -Dry-run (no broadcast): - -```shell -anvil -forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url http://127.0.0.1:8545 \ - --private-key -``` - -Broadcast on Anvil: - -```shell -anvil -forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url http://127.0.0.1:8545 \ - --broadcast \ - --private-key -``` - -## Propose & Execute Transfers - -Propose a transfer (posts the UMA bond via the Optimistic Governor): - -```shell -export PROPOSER_PK= -export OG_MODULE= -export TRANSFER_ASSET= -export TRANSFER_AMOUNT= -export TRANSFER_DESTINATION= - -forge script script/ProposeCommitmentTransfer.s.sol:ProposeCommitmentTransfer \ - --rpc-url \ - --broadcast \ - --private-key $PROPOSER_PK -``` - -Execute a proposal after it passes: - -```shell -export EXECUTOR_PK= -export OG_MODULE= -export PROPOSAL_HASH= -export TRANSFER_ASSET= -export TRANSFER_AMOUNT= -export TRANSFER_DESTINATION= - -forge script script/ExecuteCommitmentTransfer.s.sol:ExecuteCommitmentTransfer \ - --rpc-url \ - --broadcast \ - --private-key $EXECUTOR_PK -``` - -Optional overrides: - -- `TRANSFER_OPERATION` (default `0` for `CALL`) -- `TRANSFER_VALUE` (default `0`) - -## Network Env Files - -You can keep per-network env files and load them with a tool like `dotenvx` or `direnv`. - -Mainnet fork example (`.env.mainnet`): - -```ini -MAINNET_RPC_URL=... -DEPLOYER_PK=0x... -OG_COLLATERAL=0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 -OG_BOND_AMOUNT=250000000 -OG_RULES="Any assets deposited in this Commitment may be transferred back to the depositor before January 15th, 2026 (12:00AM PST). After the deadline, assets may only be transferred to jdshutt.eth. If a third party is initiating the transfer after the deadline, they may take a 10% cut of the assets being transferred as a fee." -OG_IDENTIFIER_STR=ASSERT_TRUTH2 -``` - -A ready-to-edit template is available at `.env.mainnet.example`. - -Sepolia example (`.env.sepolia`): - -```ini -SEPOLIA_RPC_URL=... -DEPLOYER_PK=0x... -SAFE_SINGLETON=0x... -SAFE_PROXY_FACTORY=0x... -SAFE_FALLBACK_HANDLER=0x... -OG_MASTER_COPY=0x... -OG_COLLATERAL=0x... -OG_BOND_AMOUNT=... -OG_RULES="Any assets deposited in this Commitment may be transferred back to the depositor before January 15th, 2026 (12:00AM PST). After the deadline, assets may only be transferred to jdshutt.eth. If a third party is initiating the transfer after the deadline, they may take a 10% cut of the assets being transferred as a fee." -``` - -Load the file before running the script: +## Repo Layout -```shell -dotenvx run -f .env.mainnet -- forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ - --rpc-url $MAINNET_RPC_URL \ - --private-key $DEPLOYER_PK -``` +- `src/` Solidity contracts +- `script/` Foundry deployment and ops scripts +- `test/` Foundry tests +- `agent/` Offchain agent scaffold +- `frontend/` Web UI +- `lib/` External dependencies (Foundry) From ddbb54ec103f0922fd52ea97cab0c762fc349bb6 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Thu, 5 Feb 2026 08:38:11 -0800 Subject: [PATCH 057/174] don't gitignore docs Signed-off-by: John Shutt --- .gitignore | 3 -- docs/agent.md | 26 +++++++++++++++++ docs/deployment.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++ docs/frontend.md | 15 ++++++++++ docs/signers.md | 57 +++++++++++++++++++++++++++++++++++++ docs/testing.md | 10 +++++++ 6 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 docs/agent.md create mode 100644 docs/deployment.md create mode 100644 docs/frontend.md create mode 100644 docs/signers.md create mode 100644 docs/testing.md diff --git a/.gitignore b/.gitignore index b7c69a0c..215ea636 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,6 @@ out/ /broadcast/**/dry-run/ /broadcast/* -# Docs -docs/ - # Dotenv file .env .env.mainnet diff --git a/docs/agent.md b/docs/agent.md new file mode 100644 index 00000000..c3495509 --- /dev/null +++ b/docs/agent.md @@ -0,0 +1,26 @@ +# Offchain Agent + +The agent in `agent/` can propose and execute transactions via the Optimistic Governor module. Customize the decision logic, signal monitoring, and overall behavior to match your commitment rules. + +## Setup + +```shell +cd agent +npm install +cp .env.example .env +npm start +``` + +Fill in at least: + +- `RPC_URL` +- `PRIVATE_KEY` +- `COMMITMENT_SAFE` +- `OG_MODULE` +- `WATCH_ASSETS` + +## Built-In Tools + +- `postBondAndPropose` +- `makeDeposit` +- `pollCommitmentChanges` diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 00000000..cf3a53b9 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,71 @@ +# Deployment and Configuration + +## Deploy a Commitment + +```shell +forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url \ + --broadcast \ + --private-key +``` + +## Required Environment Variables + +- `DEPLOYER_PK`: Private key for the deployer. +- `OG_COLLATERAL`: Address of the ERC20 collateral token. +- `OG_BOND_AMOUNT`: Bond amount for challenges. +- `OG_RULES`: Plain-language rules for the commitment. + +## Optional Overrides + +- `SAFE_SALT_NONCE`, `SAFE_THRESHOLD`, `SAFE_OWNERS` +- `OG_SALT_NONCE`, `OG_CHALLENGE_PERIOD`, `OG_RULES_URI` +- `OG_MASTER_COPY`, `SAFE_SINGLETON`, `SAFE_FALLBACK_HANDLER` +- `MODULE_PROXY_FACTORY` + +## Example `.env` + +```ini +# Required +DEPLOYER_PK=0xabc123... +OG_COLLATERAL=0x1111111111111111111111111111111111111111 +OG_BOND_AMOUNT=250000000 +OG_RULES="Any assets deposited in this Commitment may be transferred back to the depositor before January 15th, 2026 (12:00AM PST). After the deadline, assets may only be transferred to jdshutt.eth. If a third party is initiating the transfer after the deadline, they may take a 10% cut of the assets being transferred as a fee." + +# Safe overrides +SAFE_OWNERS=0x2222222222222222222222222222222222222222,0x3333333333333333333333333333333333333333 +SAFE_THRESHOLD=2 +SAFE_SALT_NONCE=12345 + +# Optimistic Governor overrides +OG_CHALLENGE_PERIOD=604800 +OG_RULES_URI=ipfs://bafy... +OG_SALT_NONCE=67890 + +# Optional factory / master copy overrides +MODULE_PROXY_FACTORY=0x4444444444444444444444444444444444444444 +OG_MASTER_COPY=0x5555555555555555555555555555555555555555 +SAFE_SINGLETON=0x6666666666666666666666666666666666666666 +SAFE_FALLBACK_HANDLER=0x7777777777777777777777777777777777777777 +``` + +## Local Testing (Anvil) + +Dry-run (no broadcast): + +```shell +anvil +forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url http://127.0.0.1:8545 \ + --private-key +``` + +Broadcast on Anvil: + +```shell +anvil +forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url http://127.0.0.1:8545 \ + --broadcast \ + --private-key +``` diff --git a/docs/frontend.md b/docs/frontend.md new file mode 100644 index 00000000..7eb78d03 --- /dev/null +++ b/docs/frontend.md @@ -0,0 +1,15 @@ +# Web Frontend + +`frontend/` provides a lightweight UI for entering Safe + Optimistic Governor parameters and generating the same deployment flow as the script. It uses the connected wallet to submit transactions. + +## Setup + +```shell +cd frontend +npm install +npm run dev +``` + +## Environment Overrides + +The UI supports `MODULE_PROXY_FACTORY` (optionally with `VITE_` or `NEXT_PUBLIC_` prefixes). Other defaults are currently hardcoded in `frontend/src/App.jsx` and can be edited there or wired to env vars. diff --git a/docs/signers.md b/docs/signers.md new file mode 100644 index 00000000..b54791c8 --- /dev/null +++ b/docs/signers.md @@ -0,0 +1,57 @@ +# Signers and Key Management + +You can avoid storing raw private keys in `.env` by using the agent’s signer helper and injecting the key at runtime for Forge scripts. + +## Supported Signer Types + +- `env`: `PRIVATE_KEY` +- `keystore`: `KEYSTORE_PATH`, `KEYSTORE_PASSWORD` +- `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` +- `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` +- `kms` / `vault-signer` / `rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (RPC signer that accepts `eth_sendTransaction`) + +## Use With Forge Scripts + +The `agent/with-signer.mjs` helper resolves a signer and injects it as an env var (for example `DEPLOYER_PK`, `PROPOSER_PK`, `EXECUTOR_PK`) for any Forge script. + +```shell +# Private key from env +SIGNER_TYPE=env PRIVATE_KEY=0x... \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# Encrypted keystore +SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/deployer.json KEYSTORE_PASSWORD=... \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# OS keychain +SIGNER_TYPE=keychain KEYCHAIN_SERVICE=og-deployer KEYCHAIN_ACCOUNT=deployer \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast + +# Vault KV (private key stored as a secret) +SIGNER_TYPE=vault VAULT_ADDR=https://vault.example.com VAULT_TOKEN=... VAULT_SECRET_PATH=secret/data/og-deployer \ + node agent/with-signer.mjs --env DEPLOYER_PK -- \ + forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimisticGovernor \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast +``` + +For interactions, swap the env var: + +```shell +SIGNER_TYPE=keystore KEYSTORE_PATH=./keys/proposer.json KEYSTORE_PASSWORD=... \ + node agent/with-signer.mjs --env PROPOSER_PK -- \ + forge script script/ProposeCommitmentTransfer.s.sol:ProposeCommitmentTransfer \ + --rpc-url $MAINNET_RPC_URL \ + --broadcast +``` + +Forge scripts still expect a private key env var. For KMS/Vault signing without exporting private keys, use an RPC signer proxy that supports `eth_sendTransaction` (set `SIGNER_RPC_URL` and `SIGNER_ADDRESS`). diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..fdd1ca78 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,10 @@ +# Testing and Common Commands + +```shell +forge build +forge test +forge fmt +forge snapshot +``` + +Add `-vv` for logs or `--mt ` to target specific tests. From 4b0bc772c3f1806fa5a408b8b14cb89025b35cc9 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Thu, 5 Feb 2026 14:02:57 -0500 Subject: [PATCH 058/174] re implement DCA agent on top of latest changes --- agent-library/agents/dca-agent/agent.js | 65 +++++++++ agent-library/agents/dca-agent/agent.json | 13 ++ agent-library/agents/dca-agent/commitment.txt | 6 + agent/.env.sepolia.example | 39 ++++++ agent/src/index.js | 123 +++++++++++++++++- agent/src/lib/config.js | 3 + agent/src/lib/polling.js | 105 +++++++++------ agent/src/lib/price.js | 43 ++++++ agent/src/lib/tools.js | 35 +++-- 9 files changed, 378 insertions(+), 54 deletions(-) create mode 100644 agent-library/agents/dca-agent/agent.js create mode 100644 agent-library/agents/dca-agent/agent.json create mode 100644 agent-library/agents/dca-agent/commitment.txt create mode 100644 agent/.env.sepolia.example create mode 100644 agent/src/lib/price.js diff --git a/agent-library/agents/dca-agent/agent.js b/agent-library/agents/dca-agent/agent.js new file mode 100644 index 00000000..15ffb259 --- /dev/null +++ b/agent-library/agents/dca-agent/agent.js @@ -0,0 +1,65 @@ +// DCA Agent - WETH reimbursement loop on Sepolia + +let lastDcaTimestamp = Date.now(); +const DCA_INTERVAL_SECONDS = 45; +const MAX_CYCLES = 2; + +function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { + const mode = proposeEnabled && disputeEnabled + ? 'You may propose and dispute.' + : proposeEnabled + ? 'You may propose but you may not dispute.' + : disputeEnabled + ? 'You may dispute but you may not propose.' + : 'You may not propose or dispute; provide opinions only.'; + + return [ + 'You are a DCA (Dollar Cost Averaging) service agent.', + 'Every 45 seconds, you deliver $0.10 worth of WETH to the Safe and get reimbursed in USDC.', + 'Stop after 2 cycles (MAX_CYCLES = 2). If signals.dcaState.cyclesCompleted >= 2, output action=ignore and do nothing.', + 'Flow: 1) Read balances from signals (Safe USDC and Self WETH), 2) If time >= 45s and balances ok, send WETH, 3) Propose USDC reimbursement to OG.', + 'Check timeSinceLastDca in signals. If >= 45 seconds and balances from signals are sufficient, proceed.', + 'Current ETH/WETH price is provided in signals as ethPriceUSD (from Chainlink oracle).', + 'Calculate: wethToSend = 0.10 / ethPriceUSD, then convert to wei (18 decimals).', + 'Example: if ETH is $2242.51, then 0.10 / 2242.51 = 0.0000446... WETH = 44600000000000 wei.', + 'First, read Safe USDC and Self WETH balances from signals.balances (note: 100000 micro-USDC = 0.10 USDC).', + 'Second, read signals.dcaState to see which steps already completed: depositConfirmed, proposalBuilt, proposalPosted, cyclesCompleted.', + 'Third, if timeSinceLastDca >= 45 seconds and balances are sufficient and depositConfirmed=false, perform a single chained action in ONE response: (a) make_deposit with asset=WETH_ADDRESS and amountWei=calculated amount (waits for confirmation), then (b) build_og_transactions for one erc20_transfer of 100000 micro-USDC to agentAddress, then (c) post_bond_and_propose with those transactions.', + 'Fourth, if depositConfirmed=true and proposalBuilt=false, call build_og_transactions and post_bond_and_propose in the same response.', + 'Fifth, if proposalBuilt=true and proposalPosted=false, call post_bond_and_propose.', + 'Do NOT repeat make_deposit when depositConfirmed=true; use dcaState to avoid duplicate deposits.', + 'If any precondition fails (balances insufficient, time not met, or Safe lacks USDC), output action=ignore.', + 'Use signals.balances.safeUsdcSufficient (boolean) or compare safeUsdcWei against minSafeUsdcWei to decide if Safe has enough USDC.', + 'All non-tool responses must be valid json.', + mode, + commitmentText ? `\nCommitment:\n${commitmentText}` : '', + 'Always verify Safe has at least 100000 micro-USDC (0.10 USDC) before proposing reimbursement.', + 'The agentAddress is provided in the input signals.', + 'Use Sepolia USDC token address 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 for reimbursement transfers (do NOT use mainnet USDC).', + 'WETH address on Sepolia: 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9', + ] + .filter(Boolean) + .join(' '); +} + +function augmentSignals(signals) { + const now = Date.now(); + const timeSinceLastDca = Math.floor((now - lastDcaTimestamp) / 1000); + + return [ + ...signals, + { + kind: 'timer', + timeSinceLastDca, + shouldExecuteDca: timeSinceLastDca >= DCA_INTERVAL_SECONDS, + lastDcaTimestamp, + currentTimestamp: now, + }, + ]; +} + +function markDcaExecuted() { + lastDcaTimestamp = Date.now(); +} + +export { getSystemPrompt, augmentSignals, markDcaExecuted }; diff --git a/agent-library/agents/dca-agent/agent.json b/agent-library/agents/dca-agent/agent.json new file mode 100644 index 00000000..0e164d28 --- /dev/null +++ b/agent-library/agents/dca-agent/agent.json @@ -0,0 +1,13 @@ +{ + "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", + "name": "Oya DCA Agent", + "description": "DCA reimbursement agent for commitments: sends $0.10 WETH to the Safe every 45 seconds and proposes $0.10 USDC reimbursement.", + "image": "https://raw.githubusercontent.com/oya-commitments/oya-commitments/main/agent-library/agents/dca-agent/agent.png", + "endpoints": [ + { + "name": "agentWallet", + "endpoint": "eip155:11155111:0x0000000000000000000000000000000000000000" + } + ], + "registrations": [] +} diff --git a/agent-library/agents/dca-agent/commitment.txt b/agent-library/agents/dca-agent/commitment.txt new file mode 100644 index 00000000..a0db3485 --- /dev/null +++ b/agent-library/agents/dca-agent/commitment.txt @@ -0,0 +1,6 @@ +I would like to DCA (Dollar Cost Average) into Ethereum by purchasing a fixed USD amount of WETH periodically. +Every 45 seconds, an agent may send $0.10 worth of WETH to this Safe to receive reimbursement of exactly $0.10 USDC. +The agent should use the latest Chainlink ETH/USD price at the time of proposal to determine the amount of WETH to transfer. +The Safe will transfer $0.10 USDC to the agent's address if the proposal is valid. +No other transactions are permitted. +If the Safe has insufficient USDC balance, the agent should not propose. diff --git a/agent/.env.sepolia.example b/agent/.env.sepolia.example new file mode 100644 index 00000000..6cdda7f9 --- /dev/null +++ b/agent/.env.sepolia.example @@ -0,0 +1,39 @@ +# Sepolia Testnet Configuration for DCA Agent + +# Network +RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_KEY + +# Commitment addresses (fill after deployment) +COMMITMENT_SAFE=0x_YOUR_SAFE_ADDRESS_HERE +OG_MODULE=0x_YOUR_OG_MODULE_ADDRESS_HERE + +# Assets to watch (Sepolia USDC + WETH) +# USDC: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 +# WETH: 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9 +WATCH_ASSETS=0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238,0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9 + +# Agent module +AGENT_MODULE=dca-agent + +# Signer configuration +SIGNER_TYPE=env +PRIVATE_KEY=0x_YOUR_AGENT_PRIVATE_KEY_HERE + +# Polling interval (ms) +POLL_INTERVAL_MS=5000 + +# Optional tuning +START_BLOCK= +WATCH_NATIVE_BALANCE=true + +# Enable proposals and disputes +PROPOSE_ENABLED=true +DISPUTE_ENABLED=true + +# OpenAI API +OPENAI_API_KEY=sk-YOUR_OPENAI_KEY_HERE +OPENAI_MODEL=gpt-4-turbo-preview +OPENAI_BASE_URL=https://api.openai.com/v1 + +# Optional gas limit for proposals +PROPOSE_GAS_LIMIT=2000000 diff --git a/agent/src/index.js b/agent/src/index.js index 66db9bb1..daabc10b 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { createPublicClient, http } from 'viem'; +import { createPublicClient, erc20Abi, http } from 'viem'; import { buildConfig } from './lib/config.js'; import { createSignerClient } from './lib/signer.js'; import { @@ -20,6 +20,7 @@ import { callAgent, explainToolCalls } from './lib/llm.js'; import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; import { extractTimelockTriggers } from './lib/timelock.js'; +import { getEthPriceUSD, getEthPriceUSDFallback } from './lib/price.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -63,7 +64,19 @@ async function loadAgentModule() { return { agentModule, commitmentText, resolvedPath }; } -const { agentModule, commitmentText } = await loadAgentModule(); +const { agentModule, commitmentText, resolvedPath } = await loadAgentModule(); +const isDcaAgent = resolvedPath.endsWith('agent-library/agents/dca-agent/agent.js'); +const DCA_WETH_ADDRESS = '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9'; +const DCA_USDC_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; +const DCA_USDC_DECIMALS = 6n; +const DCA_USDC_MIN_WEI = 100000n; // 0.10 USDC (6 decimals) +const DCA_MAX_CYCLES = 2; +const dcaState = { + depositConfirmed: false, + proposalBuilt: false, + proposalPosted: false, + cyclesCompleted: 0, +}; async function getBlockTimestampMs(blockNumber) { if (!blockNumber) return undefined; @@ -161,6 +174,36 @@ async function decideOnSignals(signals) { config, ogContext, }); + if (isDcaAgent && toolOutputs.length > 0) { + for (const output of toolOutputs) { + if (!output?.name || !output?.output) continue; + let parsed; + try { + parsed = JSON.parse(output.output); + } catch (error) { + parsed = null; + } + if (!parsed || parsed.status === 'error') continue; + + if (output.name === 'make_deposit' && parsed.status === 'confirmed') { + dcaState.depositConfirmed = true; + dcaState.proposalBuilt = false; + dcaState.proposalPosted = false; + } + if (output.name === 'build_og_transactions' && parsed.status === 'ok') { + dcaState.proposalBuilt = true; + } + if (output.name === 'post_bond_and_propose' && parsed.status === 'submitted') { + dcaState.proposalPosted = true; + dcaState.depositConfirmed = false; + dcaState.proposalBuilt = false; + dcaState.cyclesCompleted = Math.min( + DCA_MAX_CYCLES, + dcaState.cyclesCompleted + 1 + ); + } + } + } if (decision.responseId && toolOutputs.length > 0) { const explanation = await explainToolCalls({ config, @@ -248,8 +291,80 @@ async function agentLoop() { }); } - if (combinedSignals.length > 0) { - const decisionOk = await decideOnSignals(combinedSignals); + // Allow agent module to augment signals (e.g., add timer signals) + let signalsToProcess = combinedSignals; + if (agentModule?.augmentSignals) { + signalsToProcess = agentModule.augmentSignals(combinedSignals, { + nowMs, + latestBlock, + }); + } + + // Fetch ETH price and add to timer signal (if present) + if (signalsToProcess.some((signal) => signal.kind === 'timer')) { + try { + let ethPriceUSD; + try { + ethPriceUSD = await getEthPriceUSD(publicClient, config.chainlinkPriceFeed); + } catch (error) { + console.warn('[agent] Chainlink price fetch failed, using Coingecko fallback'); + ethPriceUSD = await getEthPriceUSDFallback(); + } + + for (const signal of signalsToProcess) { + if (signal.kind === 'timer') { + signal.ethPriceUSD = ethPriceUSD; + } + } + } catch (error) { + console.error('[agent] Failed to fetch ETH price:', error); + } + } + + // Deterministic balance checks for DCA agent (inject into timer signals) + if (isDcaAgent && signalsToProcess.some((signal) => signal.kind === 'timer')) { + try { + const [safeUsdcWei, selfWethWei] = await Promise.all([ + publicClient.readContract({ + address: DCA_USDC_ADDRESS, + abi: erc20Abi, + functionName: 'balanceOf', + args: [config.commitmentSafe], + }), + publicClient.readContract({ + address: DCA_WETH_ADDRESS, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }), + ]); + + const safeUsdcSufficient = safeUsdcWei >= DCA_USDC_MIN_WEI; + const safeUsdcHuman = Number(safeUsdcWei) / 10 ** Number(DCA_USDC_DECIMALS); + const selfWethHuman = Number(selfWethWei) / 1e18; + + for (const signal of signalsToProcess) { + if (signal.kind === 'timer') { + signal.balances = { + safeUsdcWei: safeUsdcWei.toString(), + selfWethWei: selfWethWei.toString(), + safeUsdcHuman, + selfWethHuman, + safeUsdcSufficient, + minSafeUsdcWei: DCA_USDC_MIN_WEI.toString(), + minSafeUsdcHuman: + Number(DCA_USDC_MIN_WEI) / 10 ** Number(DCA_USDC_DECIMALS), + }; + signal.dcaState = { ...dcaState }; + } + } + } catch (error) { + console.error('[agent] Failed to fetch DCA balances:', error); + } + } + + if (signalsToProcess.length > 0) { + const decisionOk = await decideOnSignals(signalsToProcess); if (decisionOk && dueTimelocks.length > 0) { markTimelocksFired(dueTimelocks); } diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index e0ec9feb..2b7e3490 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -38,6 +38,9 @@ function buildConfig() { : process.env.DISPUTE_ENABLED.toLowerCase() !== 'false', disputeRetryMs: Number(process.env.DISPUTE_RETRY_MS ?? 60_000), agentModule: process.env.AGENT_MODULE, + chainlinkPriceFeed: process.env.CHAINLINK_PRICE_FEED + ? getAddress(process.env.CHAINLINK_PRICE_FEED) + : undefined, }; } diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index 08604d4a..a752dd16 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -47,29 +47,38 @@ async function pollCommitmentChanges({ const toBlock = latestBlock; const deposits = []; - for (const asset of trackedAssets) { - const logs = await publicClient.getLogs({ - address: asset, - event: transferEvent, - args: { to: commitmentSafe }, - fromBlock, - toBlock, - }); + const maxRange = 10n; + let currentFrom = fromBlock; + while (currentFrom <= toBlock) { + const currentTo = + currentFrom + maxRange - 1n > toBlock ? toBlock : currentFrom + maxRange - 1n; - for (const log of logs) { - deposits.push({ - kind: 'erc20Deposit', - asset, - from: log.args.from, - amount: log.args.value, - blockNumber: log.blockNumber, - transactionHash: log.transactionHash, - logIndex: log.logIndex, - id: log.transactionHash - ? `${log.transactionHash}:${log.logIndex ?? '0'}` - : `${log.blockNumber.toString()}:${log.logIndex ?? '0'}`, + for (const asset of trackedAssets) { + const logs = await publicClient.getLogs({ + address: asset, + event: transferEvent, + args: { to: commitmentSafe }, + fromBlock: currentFrom, + toBlock: currentTo, }); + + for (const log of logs) { + deposits.push({ + kind: 'erc20Deposit', + asset, + from: log.args.from, + amount: log.args.value, + blockNumber: log.blockNumber, + transactionHash: log.transactionHash, + logIndex: log.logIndex, + id: log.transactionHash + ? `${log.transactionHash}:${log.logIndex ?? '0'}` + : `${log.blockNumber.toString()}:${log.logIndex ?? '0'}`, + }); + } } + + currentFrom = currentTo + 1n; } let nextNativeBalance = lastNativeBalance; @@ -111,26 +120,42 @@ async function pollProposalChanges({ publicClient, ogModule, lastProposalChecked const fromBlock = lastProposalCheckedBlock + 1n; const toBlock = latestBlock; - const [proposedLogs, executedLogs, deletedLogs] = await Promise.all([ - publicClient.getLogs({ - address: ogModule, - event: transactionsProposedEvent, - fromBlock, - toBlock, - }), - publicClient.getLogs({ - address: ogModule, - event: proposalExecutedEvent, - fromBlock, - toBlock, - }), - publicClient.getLogs({ - address: ogModule, - event: proposalDeletedEvent, - fromBlock, - toBlock, - }), - ]); + const maxRange = 10n; + const proposedLogs = []; + const executedLogs = []; + const deletedLogs = []; + let currentFrom = fromBlock; + while (currentFrom <= toBlock) { + const currentTo = + currentFrom + maxRange - 1n > toBlock ? toBlock : currentFrom + maxRange - 1n; + + const [chunkProposed, chunkExecuted, chunkDeleted] = await Promise.all([ + publicClient.getLogs({ + address: ogModule, + event: transactionsProposedEvent, + fromBlock: currentFrom, + toBlock: currentTo, + }), + publicClient.getLogs({ + address: ogModule, + event: proposalExecutedEvent, + fromBlock: currentFrom, + toBlock: currentTo, + }), + publicClient.getLogs({ + address: ogModule, + event: proposalDeletedEvent, + fromBlock: currentFrom, + toBlock: currentTo, + }), + ]); + + proposedLogs.push(...chunkProposed); + executedLogs.push(...chunkExecuted); + deletedLogs.push(...chunkDeleted); + + currentFrom = currentTo + 1n; + } const newProposals = []; for (const log of proposedLogs) { diff --git a/agent/src/lib/price.js b/agent/src/lib/price.js new file mode 100644 index 00000000..6bc7d5e2 --- /dev/null +++ b/agent/src/lib/price.js @@ -0,0 +1,43 @@ +import { parseAbi } from 'viem'; + +const chainlinkAbi = parseAbi([ + 'function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)', +]); + +async function getEthPriceUSD(publicClient, priceFeedAddress = '0x694AA1769357215DE4FAC081bf1f309aDC325306') { + try { + const result = await publicClient.readContract({ + address: priceFeedAddress, + abi: chainlinkAbi, + functionName: 'latestRoundData', + }); + + const answer = result[1]; + const price = Number(answer) / 1e8; + console.log(`[price] ETH/USD from Chainlink: $${price.toFixed(2)}`); + return price; + } catch (error) { + console.error('[price] Failed to fetch ETH price from Chainlink:', error); + throw new Error('Unable to fetch ETH price from Chainlink oracle'); + } +} + +async function getEthPriceUSDFallback() { + try { + const response = await fetch( + 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd' + ); + if (!response.ok) { + throw new Error(`Coingecko API error: ${response.status}`); + } + const data = await response.json(); + const price = data.ethereum.usd; + console.log(`[price] ETH/USD from Coingecko: $${price.toFixed(2)}`); + return price; + } catch (error) { + console.error('[price] Failed to fetch ETH price from Coingecko:', error); + throw error; + } +} + +export { getEthPriceUSD, getEthPriceUSDFallback }; diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 01798bea..95fe6fe7 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -2,6 +2,10 @@ import { getAddress } from 'viem'; import { buildOgTransactions, makeDeposit, postBondAndDispute, postBondAndPropose } from './tx.js'; import { parseToolArguments } from './utils.js'; +function safeStringify(value) { + return JSON.stringify(value, (_, item) => (typeof item === 'bigint' ? item.toString() : item)); +} + function toolDefinitions({ proposeEnabled, disputeEnabled }) { const tools = [ { @@ -205,12 +209,14 @@ async function executeToolCalls({ builtTransactions = transactions; outputs.push({ callId: call.callId, - output: JSON.stringify({ status: 'ok', transactions }), + name: call.name, + output: safeStringify({ status: 'ok', transactions }), }); } catch (error) { outputs.push({ callId: call.callId, - output: JSON.stringify({ + name: call.name, + output: safeStringify({ status: 'error', message: error?.message ?? String(error), }), @@ -227,11 +233,15 @@ async function executeToolCalls({ asset: args.asset, amountWei: BigInt(args.amountWei), }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); outputs.push({ callId: call.callId, - output: JSON.stringify({ - status: 'submitted', + name: call.name, + output: safeStringify({ + status: 'confirmed', transactionHash: String(txHash), + next_step: + 'Do not respond to the user with an update yet. Call build_og_transactions then use the output from build_og_transactions to call post_bond_and_propose.', }), }); continue; @@ -241,7 +251,8 @@ async function executeToolCalls({ if (!config.proposeEnabled) { outputs.push({ callId: call.callId, - output: JSON.stringify({ + name: call.name, + output: safeStringify({ status: 'skipped', reason: 'proposals disabled', }), @@ -265,7 +276,8 @@ async function executeToolCalls({ }); outputs.push({ callId: call.callId, - output: JSON.stringify({ + name: call.name, + output: safeStringify({ status: 'submitted', ...result, }), @@ -277,7 +289,8 @@ async function executeToolCalls({ if (!config.disputeEnabled) { outputs.push({ callId: call.callId, - output: JSON.stringify({ + name: call.name, + output: safeStringify({ status: 'skipped', reason: 'disputes disabled', }), @@ -297,7 +310,8 @@ async function executeToolCalls({ }); outputs.push({ callId: call.callId, - output: JSON.stringify({ + name: call.name, + output: safeStringify({ status: 'submitted', ...result, }), @@ -305,7 +319,8 @@ async function executeToolCalls({ } catch (error) { outputs.push({ callId: call.callId, - output: JSON.stringify({ + name: call.name, + output: safeStringify({ status: 'error', message: error?.message ?? String(error), }), @@ -317,7 +332,7 @@ async function executeToolCalls({ console.warn('[agent] Unknown tool call:', call.name); outputs.push({ callId: call.callId, - output: JSON.stringify({ status: 'skipped', reason: 'unknown tool' }), + output: safeStringify({ status: 'skipped', reason: 'unknown tool' }), }); } From 1b46224470512ec7ab9960a080d3811336e1a870 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Thu, 5 Feb 2026 15:53:50 -0500 Subject: [PATCH 059/174] bug fixes, and reset DCA state when proposal is executed --- agent-library/agents/dca-agent/agent.js | 10 +++---- agent/.env.sepolia.example | 39 ------------------------- agent/src/index.js | 15 ++++++++-- agent/src/lib/polling.js | 20 +++++++++++-- 4 files changed, 35 insertions(+), 49 deletions(-) delete mode 100644 agent/.env.sepolia.example diff --git a/agent-library/agents/dca-agent/agent.js b/agent-library/agents/dca-agent/agent.js index 15ffb259..48478179 100644 --- a/agent-library/agents/dca-agent/agent.js +++ b/agent-library/agents/dca-agent/agent.js @@ -1,7 +1,7 @@ // DCA Agent - WETH reimbursement loop on Sepolia let lastDcaTimestamp = Date.now(); -const DCA_INTERVAL_SECONDS = 45; +const DCA_INTERVAL_SECONDS = 200; const MAX_CYCLES = 2; function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { @@ -15,16 +15,16 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { return [ 'You are a DCA (Dollar Cost Averaging) service agent.', - 'Every 45 seconds, you deliver $0.10 worth of WETH to the Safe and get reimbursed in USDC.', + `Every ${DCA_INTERVAL_SECONDS} seconds, you deliver $0.10 worth of WETH to the Safe and get reimbursed in USDC.`, 'Stop after 2 cycles (MAX_CYCLES = 2). If signals.dcaState.cyclesCompleted >= 2, output action=ignore and do nothing.', - 'Flow: 1) Read balances from signals (Safe USDC and Self WETH), 2) If time >= 45s and balances ok, send WETH, 3) Propose USDC reimbursement to OG.', - 'Check timeSinceLastDca in signals. If >= 45 seconds and balances from signals are sufficient, proceed.', + `Flow: 1) Read balances from signals (Safe USDC and Self WETH), 2) If time >= ${DCA_INTERVAL_SECONDS}s and balances ok, send WETH, 3) Propose USDC reimbursement to OG.`, + `Check timeSinceLastDca in signals. If >= ${DCA_INTERVAL_SECONDS} seconds and balances from signals are sufficient, proceed.`, 'Current ETH/WETH price is provided in signals as ethPriceUSD (from Chainlink oracle).', 'Calculate: wethToSend = 0.10 / ethPriceUSD, then convert to wei (18 decimals).', 'Example: if ETH is $2242.51, then 0.10 / 2242.51 = 0.0000446... WETH = 44600000000000 wei.', 'First, read Safe USDC and Self WETH balances from signals.balances (note: 100000 micro-USDC = 0.10 USDC).', 'Second, read signals.dcaState to see which steps already completed: depositConfirmed, proposalBuilt, proposalPosted, cyclesCompleted.', - 'Third, if timeSinceLastDca >= 45 seconds and balances are sufficient and depositConfirmed=false, perform a single chained action in ONE response: (a) make_deposit with asset=WETH_ADDRESS and amountWei=calculated amount (waits for confirmation), then (b) build_og_transactions for one erc20_transfer of 100000 micro-USDC to agentAddress, then (c) post_bond_and_propose with those transactions.', + `Third, if timeSinceLastDca >= ${DCA_INTERVAL_SECONDS} seconds and balances are sufficient and depositConfirmed=false, perform a single chained action in ONE response: (a) make_deposit with asset=WETH_ADDRESS and amountWei=calculated amount (waits for confirmation), then (b) build_og_transactions for one erc20_transfer of 100000 micro-USDC to agentAddress, then (c) post_bond_and_propose with those transactions.`, 'Fourth, if depositConfirmed=true and proposalBuilt=false, call build_og_transactions and post_bond_and_propose in the same response.', 'Fifth, if proposalBuilt=true and proposalPosted=false, call post_bond_and_propose.', 'Do NOT repeat make_deposit when depositConfirmed=true; use dcaState to avoid duplicate deposits.', diff --git a/agent/.env.sepolia.example b/agent/.env.sepolia.example deleted file mode 100644 index 6cdda7f9..00000000 --- a/agent/.env.sepolia.example +++ /dev/null @@ -1,39 +0,0 @@ -# Sepolia Testnet Configuration for DCA Agent - -# Network -RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_KEY - -# Commitment addresses (fill after deployment) -COMMITMENT_SAFE=0x_YOUR_SAFE_ADDRESS_HERE -OG_MODULE=0x_YOUR_OG_MODULE_ADDRESS_HERE - -# Assets to watch (Sepolia USDC + WETH) -# USDC: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 -# WETH: 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9 -WATCH_ASSETS=0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238,0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9 - -# Agent module -AGENT_MODULE=dca-agent - -# Signer configuration -SIGNER_TYPE=env -PRIVATE_KEY=0x_YOUR_AGENT_PRIVATE_KEY_HERE - -# Polling interval (ms) -POLL_INTERVAL_MS=5000 - -# Optional tuning -START_BLOCK= -WATCH_NATIVE_BALANCE=true - -# Enable proposals and disputes -PROPOSE_ENABLED=true -DISPUTE_ENABLED=true - -# OpenAI API -OPENAI_API_KEY=sk-YOUR_OPENAI_KEY_HERE -OPENAI_MODEL=gpt-4-turbo-preview -OPENAI_BASE_URL=https://api.openai.com/v1 - -# Optional gas limit for proposals -PROPOSE_GAS_LIMIT=2000000 diff --git a/agent/src/index.js b/agent/src/index.js index daabc10b..c9adf013 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -254,14 +254,25 @@ async function agentLoop() { }); } - const { newProposals, lastProposalCheckedBlock: nextProposalBlock } = - await pollProposalChanges({ + const { + newProposals, + executedProposals, + deletedProposals, + lastProposalCheckedBlock: nextProposalBlock, + } = await pollProposalChanges({ publicClient, ogModule: config.ogModule, lastProposalCheckedBlock, proposalsByHash, }); lastProposalCheckedBlock = nextProposalBlock; + const executedProposalCount = executedProposals?.length ?? 0; + const deletedProposalCount = deletedProposals?.length ?? 0; + if (isDcaAgent && (executedProposalCount > 0 || deletedProposalCount > 0)) { + dcaState.proposalPosted = false; + dcaState.proposalBuilt = false; + dcaState.depositConfirmed = false; + } const rulesText = ogContext?.rules ?? commitmentText ?? ''; updateTimelockSchedule({ rulesText }); diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index a752dd16..56c372cb 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -110,11 +110,21 @@ async function pollCommitmentChanges({ async function pollProposalChanges({ publicClient, ogModule, lastProposalCheckedBlock, proposalsByHash }) { const latestBlock = await publicClient.getBlockNumber(); if (lastProposalCheckedBlock === undefined) { - return { newProposals: [], lastProposalCheckedBlock: latestBlock }; + return { + newProposals: [], + executedProposals: [], + deletedProposals: [], + lastProposalCheckedBlock: latestBlock, + }; } if (latestBlock <= lastProposalCheckedBlock) { - return { newProposals: [], lastProposalCheckedBlock }; + return { + newProposals: [], + executedProposals: [], + deletedProposals: [], + lastProposalCheckedBlock, + }; } const fromBlock = lastProposalCheckedBlock + 1n; @@ -202,21 +212,25 @@ async function pollProposalChanges({ publicClient, ogModule, lastProposalChecked newProposals.push(proposalRecord); } + const executedProposals = []; for (const log of executedLogs) { const proposalHash = log.args?.proposalHash; if (proposalHash) { proposalsByHash.delete(proposalHash); + executedProposals.push(proposalHash); } } + const deletedProposals = []; for (const log of deletedLogs) { const proposalHash = log.args?.proposalHash; if (proposalHash) { proposalsByHash.delete(proposalHash); + deletedProposals.push(proposalHash); } } - return { newProposals, lastProposalCheckedBlock: toBlock }; + return { newProposals, executedProposals, deletedProposals, lastProposalCheckedBlock: toBlock }; } async function executeReadyProposals({ From 12be46207ca3318826a6c4aa08de05a2f6d1c72b Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Thu, 5 Feb 2026 16:30:34 -0500 Subject: [PATCH 060/174] working state! Add pendingProposal signal to prevent duplicate DCA proposals. --- agent-library/agents/dca-agent/agent.js | 1 + agent/src/index.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/agent-library/agents/dca-agent/agent.js b/agent-library/agents/dca-agent/agent.js index 48478179..4f9abfaa 100644 --- a/agent-library/agents/dca-agent/agent.js +++ b/agent-library/agents/dca-agent/agent.js @@ -24,6 +24,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'Example: if ETH is $2242.51, then 0.10 / 2242.51 = 0.0000446... WETH = 44600000000000 wei.', 'First, read Safe USDC and Self WETH balances from signals.balances (note: 100000 micro-USDC = 0.10 USDC).', 'Second, read signals.dcaState to see which steps already completed: depositConfirmed, proposalBuilt, proposalPosted, cyclesCompleted.', + 'If signals.pendingProposal is true, output action=ignore and do not call post_bond_and_propose until it becomes false.', `Third, if timeSinceLastDca >= ${DCA_INTERVAL_SECONDS} seconds and balances are sufficient and depositConfirmed=false, perform a single chained action in ONE response: (a) make_deposit with asset=WETH_ADDRESS and amountWei=calculated amount (waits for confirmation), then (b) build_og_transactions for one erc20_transfer of 100000 micro-USDC to agentAddress, then (c) post_bond_and_propose with those transactions.`, 'Fourth, if depositConfirmed=true and proposalBuilt=false, call build_og_transactions and post_bond_and_propose in the same response.', 'Fifth, if proposalBuilt=true and proposalPosted=false, call post_bond_and_propose.', diff --git a/agent/src/index.js b/agent/src/index.js index c9adf013..23432aa0 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -354,6 +354,7 @@ async function agentLoop() { const safeUsdcHuman = Number(safeUsdcWei) / 10 ** Number(DCA_USDC_DECIMALS); const selfWethHuman = Number(selfWethWei) / 1e18; + const pendingProposal = proposalsByHash.size > 0; for (const signal of signalsToProcess) { if (signal.kind === 'timer') { signal.balances = { @@ -367,6 +368,7 @@ async function agentLoop() { Number(DCA_USDC_MIN_WEI) / 10 ** Number(DCA_USDC_DECIMALS), }; signal.dcaState = { ...dcaState }; + signal.pendingProposal = pendingProposal; } } } catch (error) { From bc564d57df0819d338f9c2db970d70b9c67b20e0 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Thu, 5 Feb 2026 16:38:42 -0500 Subject: [PATCH 061/174] on proposal execution, reset time since DCA, increment cycle count --- agent/src/index.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 23432aa0..e2365e9c 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -197,10 +197,6 @@ async function decideOnSignals(signals) { dcaState.proposalPosted = true; dcaState.depositConfirmed = false; dcaState.proposalBuilt = false; - dcaState.cyclesCompleted = Math.min( - DCA_MAX_CYCLES, - dcaState.cyclesCompleted + 1 - ); } } } @@ -268,7 +264,19 @@ async function agentLoop() { lastProposalCheckedBlock = nextProposalBlock; const executedProposalCount = executedProposals?.length ?? 0; const deletedProposalCount = deletedProposals?.length ?? 0; - if (isDcaAgent && (executedProposalCount > 0 || deletedProposalCount > 0)) { + if (isDcaAgent && executedProposalCount > 0) { + dcaState.proposalPosted = false; + dcaState.proposalBuilt = false; + dcaState.depositConfirmed = false; + dcaState.cyclesCompleted = Math.min( + DCA_MAX_CYCLES, + dcaState.cyclesCompleted + executedProposalCount + ); + if (agentModule?.markDcaExecuted) { + agentModule.markDcaExecuted(); + } + } + if (isDcaAgent && deletedProposalCount > 0) { dcaState.proposalPosted = false; dcaState.proposalBuilt = false; dcaState.depositConfirmed = false; From 8e627e5e1555ca7305d561cb285fb5b13928c26c Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Thu, 5 Feb 2026 17:12:55 -0500 Subject: [PATCH 062/174] Track proposal submission status and reset on failure or timeout --- agent/src/index.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/agent/src/index.js b/agent/src/index.js index e2365e9c..c936327b 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -71,11 +71,14 @@ const DCA_USDC_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; const DCA_USDC_DECIMALS = 6n; const DCA_USDC_MIN_WEI = 100000n; // 0.10 USDC (6 decimals) const DCA_MAX_CYCLES = 2; +const DCA_PROPOSAL_CONFIRM_TIMEOUT_MS = 60000; const dcaState = { depositConfirmed: false, proposalBuilt: false, proposalPosted: false, cyclesCompleted: 0, + proposalSubmitHash: null, + proposalSubmitMs: null, }; async function getBlockTimestampMs(blockNumber) { @@ -197,6 +200,8 @@ async function decideOnSignals(signals) { dcaState.proposalPosted = true; dcaState.depositConfirmed = false; dcaState.proposalBuilt = false; + dcaState.proposalSubmitHash = parsed.proposalHash ?? null; + dcaState.proposalSubmitMs = Date.now(); } } } @@ -268,6 +273,8 @@ async function agentLoop() { dcaState.proposalPosted = false; dcaState.proposalBuilt = false; dcaState.depositConfirmed = false; + dcaState.proposalSubmitHash = null; + dcaState.proposalSubmitMs = null; dcaState.cyclesCompleted = Math.min( DCA_MAX_CYCLES, dcaState.cyclesCompleted + executedProposalCount @@ -280,6 +287,36 @@ async function agentLoop() { dcaState.proposalPosted = false; dcaState.proposalBuilt = false; dcaState.depositConfirmed = false; + dcaState.proposalSubmitHash = null; + dcaState.proposalSubmitMs = null; + } + if ( + isDcaAgent && + dcaState.proposalPosted && + dcaState.proposalSubmitHash && + dcaState.proposalSubmitMs + ) { + try { + const receipt = await publicClient.getTransactionReceipt({ + hash: dcaState.proposalSubmitHash, + }); + if (receipt?.status === 0n || receipt?.status === 'reverted') { + dcaState.proposalPosted = false; + dcaState.proposalBuilt = false; + dcaState.proposalSubmitHash = null; + dcaState.proposalSubmitMs = null; + } + } catch (error) { + if ( + Date.now() - dcaState.proposalSubmitMs > + DCA_PROPOSAL_CONFIRM_TIMEOUT_MS + ) { + dcaState.proposalPosted = false; + dcaState.proposalBuilt = false; + dcaState.proposalSubmitHash = null; + dcaState.proposalSubmitMs = null; + } + } } const rulesText = ogContext?.rules ?? commitmentText ?? ''; @@ -362,7 +399,8 @@ async function agentLoop() { const safeUsdcHuman = Number(safeUsdcWei) / 10 ** Number(DCA_USDC_DECIMALS); const selfWethHuman = Number(selfWethWei) / 1e18; - const pendingProposal = proposalsByHash.size > 0; + const pendingProposal = + proposalsByHash.size > 0 || dcaState.proposalPosted === true; for (const signal of signalsToProcess) { if (signal.kind === 'timer') { signal.balances = { From 2c0ae715f11a79b993eabd8aefaabb025ad3b959 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 11:35:52 -0800 Subject: [PATCH 063/174] add contributor guidelines and docs Signed-off-by: John Shutt --- .github/pull_request_template.md | 36 ++++++++++++++++ .github/workflows/test.yml | 56 +++++++++++++++++++++++++ AGENTS.md | 22 ++++++++++ CONTRIBUTING.md | 47 +++++++++++++++++++++ README.md | 2 + agent-library/AGENTS.md | 24 +++++++++++ agent/AGENTS.md | 24 +++++++++++ docs/agent-extension-guidelines.md | 49 ++++++++++++++++++++++ docs/templates/AGENTS.md.template | 25 +++++++++++ docs/templates/AGENT_README.md.template | 20 +++++++++ docs/templates/AREA_README.md.template | 25 +++++++++++ 11 files changed, 330 insertions(+) create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md create mode 100644 agent-library/AGENTS.md create mode 100644 agent/AGENTS.md create mode 100644 docs/agent-extension-guidelines.md create mode 100644 docs/templates/AGENTS.md.template create mode 100644 docs/templates/AGENT_README.md.template create mode 100644 docs/templates/AREA_README.md.template diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..9ef9e192 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,36 @@ +## Summary + +Describe the change and why it is needed. + +## Scope + +- [ ] Solidity (`src/`, `script/`, `test/`) +- [ ] Shared agent runner (`agent/`) +- [ ] Agent module(s) (`agent-library/agents/*`) +- [ ] Frontend (`frontend/`) +- [ ] Docs only + +## Agent Locality Checks + +- [ ] If this PR adds or changes behavior for a specific agent, that behavior is implemented in `agent-library/agents//`. +- [ ] If shared generalized agent files (`agent/src/lib/*`, `agent/src/index.js`) were modified, this PR includes cross-agent justification. + +## Shared Runner Justification + +Required when changing shared generalized agent files. + +Why was an agent-local implementation insufficient? + +Which existing agents are impacted? + +## Testing + +List commands run and key outcomes. + +- [ ] `forge fmt` (if Solidity changed) +- [ ] `forge test` (if Solidity changed) +- [ ] Agent module tests/simulations (if `agent/` or `agent-library/` changed) + +## Configuration Impact + +Document added/changed env vars, addresses, salts, or deployment assumptions. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b79c8d4f..9c056de5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,62 @@ env: FOUNDRY_PROFILE: ci jobs: + shared-agent-justification: + name: Shared agent justification + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - name: Validate shared runner justification + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pull_number = context.payload.pull_request.number; + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number, + per_page: 100 + }); + + const touchedSharedRunner = files.some((f) => + f.filename.startsWith("agent/src/lib/") || f.filename === "agent/src/index.js" + ); + + if (!touchedSharedRunner) { + core.info("Shared runner files not changed; skipping justification check."); + return; + } + + const body = context.payload.pull_request.body || ""; + const marker = "## Shared Runner Justification"; + const start = body.indexOf(marker); + if (start === -1) { + core.setFailed("PR touches shared runner files but is missing the '## Shared Runner Justification' section."); + return; + } + + const sectionText = body.slice(start + marker.length); + const nextHeading = sectionText.search(/\n##\s+/); + const section = nextHeading === -1 ? sectionText : sectionText.slice(0, nextHeading); + + const normalized = section + .replace(/[#>*`_\-\[\]\(\)\r\n]/g, " ") + .replace(/\s+/g, " ") + .trim(); + + if (normalized.length < 40) { + core.setFailed("PR touches shared runner files but Shared Runner Justification is too short. Explain why agent-local implementation was insufficient and which agents are impacted."); + return; + } + + core.info("Shared runner justification present."); + check: name: Foundry project runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 04bd273d..4626d52f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,22 @@ # Repository Guidelines +## Documentation Hierarchy +- Keep contribution guidance in both human docs and machine-readable instructions. +- `AGENTS.md`: normative instructions for coding agents and automation. +- `README.md`: architecture, intent, and operational context for humans. +- `CONTRIBUTING.md`: shared contributor workflow and policy across the repo. +- Precedence when instructions conflict: +1. Closest file to the changed code path wins. +2. `AGENTS.md` is authoritative for agent behavior. +3. Root-level files apply unless overridden by a closer area-level file. + ## Project Structure & Module Organization - **`src/`**: Core Solidity contracts (e.g., `Counter.sol`). Keep new modules grouped by domain and include SPDX + pragma headers. - **`script/`**: Deployment and automation scripts (e.g., `Counter.s.sol`, `DeploySafeWithOptimisticGovernor.s.sol`). Favor reusable helpers and parameterize via environment variables. - **`test/`**: Forge tests using `forge-std`’s `Test` base. Mirror contract names (`.t.sol`) and co-locate fixtures with the subject under test. - **`lib/`**: External dependencies (currently `forge-std`) managed through Foundry. +- **`agent/`**: Shared offchain runner, signer integrations, and reusable tooling. +- **`agent-library/`**: Agent-specific implementations under `agent-library/agents//`. ## Build, Test, and Development Commands - `forge build`: Compile all contracts. @@ -20,12 +32,22 @@ - Keep files focused and small; prefer internal helpers over inline duplication. - Run `forge fmt` to enforce style; include SPDX identifiers and explicit visibility where practical. - Use descriptive variable names (avoid single letters outside of loop counters or hashes). +- For Node.js code in `agent/` and `agent-library/`, keep modules small and isolate side effects at the edges. + +## Agent Locality Rule +- New functionality for a specific agent must be implemented in that agent's own files under `agent-library/agents//`. +- Do not add agent-specific behavior to shared generalized files in `agent/src/lib/` or `agent/src/index.js`. +- Shared generalized files should only change when: +1. The change is required for multiple agents. +2. The change fixes a bug in shared infrastructure. +- If a pull request changes shared generalized agent files, include a brief cross-agent rationale in the PR description and link impacted agents. ## Testing Guidelines - Use `forge-std/Test` utilities for assertions, fuzzing (`testFuzz_*`), and logging. - Name tests with behavior-first patterns (`test_IncrementsCounter`, `testFuzz_SetNumberMaintainsState`). - Cover success, failure, and access-control paths; add revert expectation tests when changing critical flows. - When modifying gas-sensitive code, refresh `forge snapshot` and include notes in PRs. +- For `agent-library` changes, run the relevant agent test/simulation scripts and note commands in the PR. ## Commit & Pull Request Guidelines - Write imperative, concise commit messages (e.g., "Add OG deployment script"); group related changes together. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..80aa2d58 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing + +This repository uses two kinds of contribution guidance: + +- `AGENTS.md` for machine-targeted, normative instructions. +- `README.md` for human-oriented architecture and workflow context. + +## Precedence Rules + +When instructions conflict: + +1. The closest file to the code you are editing wins. +2. `AGENTS.md` is authoritative for agent behavior. +3. Root-level guidance applies when there is no closer override. + +## Directory Guidance + +- `src/`, `script/`, `test/`: Solidity contracts, scripts, and tests. +- `agent/`: shared offchain runner and reusable agent infrastructure. +- `agent-library/agents//`: agent-specific implementations. +- `docs/`: operational and architecture docs. + +## Required Contributor Workflow + +1. Read relevant local docs before editing (`AGENTS.md`, `README.md`). +2. Keep changes scoped to the correct area. +Agent-specific behavior belongs in `agent-library/agents//`. +Shared runner changes in `agent/` require cross-agent justification. +3. Run the minimum required checks. +Solidity changes: `forge fmt`, `forge test`. +Agent changes: relevant module tests/simulations. +4. In PRs, document: +What changed and why. +Tests run. +Any config or environment variable impacts. + +## Agent Extension Policy + +New agent behavior must be added to that agent's own library files instead of shared generalized files. + +- Preferred location: `agent-library/agents//agent.js` and related files in that directory. +- Shared files such as `agent/src/lib/*` and `agent/src/index.js` should only change for multi-agent abstractions or shared bug fixes. +- If shared files are changed, the PR must include: +Why an agent-local implementation was insufficient. +Which existing agents are impacted. + +See `docs/agent-extension-guidelines.md` for the decision framework and examples. diff --git a/README.md b/README.md index bc301707..9c2417f8 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,11 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis ## Documentation +- Contribution workflow and policy: `CONTRIBUTING.md` - Deployment and configuration: `docs/deployment.md` - Signer options and `with-signer` helper: `docs/signers.md` - Offchain agent usage: `docs/agent.md` +- Agent extension decision rules: `docs/agent-extension-guidelines.md` - Web frontend: `docs/frontend.md` - Testing and common commands: `docs/testing.md` diff --git a/agent-library/AGENTS.md b/agent-library/AGENTS.md new file mode 100644 index 00000000..f72e386a --- /dev/null +++ b/agent-library/AGENTS.md @@ -0,0 +1,24 @@ +# Agent Library Guidelines + +## Scope + +This file applies to `agent-library/` and `agent-library/agents/*`. + +## Purpose + +This directory is the home for agent-specific behavior and commitment-specific decision logic. + +## Rules + +- Each agent lives under `agent-library/agents//`. +- Keep commitment logic, prompt strategy, and behavior specialization in that agent's local files. +- Prefer adding new modules over branching shared runner code for one-off behavior. + +## Locality Rule + +When creating a new agent, place functionality in that agent's own files (`agent.js` and adjacent module files). Do not implement single-agent behavior in shared generalized files under `agent/src/lib/` or `agent/src/index.js`. + +## Validation + +- Run module-specific tests/simulations for the changed agent. +- Document test commands in the PR description. diff --git a/agent/AGENTS.md b/agent/AGENTS.md new file mode 100644 index 00000000..adc55d53 --- /dev/null +++ b/agent/AGENTS.md @@ -0,0 +1,24 @@ +# Agent Runner Guidelines + +## Scope + +This file applies to `agent/`. + +## Purpose + +`agent/` contains shared offchain runner infrastructure (config, signer handling, polling, transaction helpers, and runtime wiring). + +## Rules + +- Keep this directory generalized and reusable across multiple agent modules. +- Do not add behavior that is only relevant to one agent module. +- If a shared runner change is required, preserve backward compatibility for existing modules where practical. + +## Locality Rule + +If behavior is specific to one agent, implement it in `agent-library/agents//` instead of `agent/`. + +## Validation + +- Run relevant Node checks and scripts for changed code paths. +- Run at least one affected module test/simulation from `agent-library/agents//`. diff --git a/docs/agent-extension-guidelines.md b/docs/agent-extension-guidelines.md new file mode 100644 index 00000000..4ba8a2cd --- /dev/null +++ b/docs/agent-extension-guidelines.md @@ -0,0 +1,49 @@ +# Agent Extension Guidelines + +This document defines where new behavior should be implemented when adding or updating an agent. + +## Core Rule + +Implement agent-specific behavior in that agent's own files under `agent-library/agents//`. + +Do not place agent-specific logic in shared generalized runner files unless the change is clearly cross-agent. + +## Decision Tree + +1. Does the behavior apply to exactly one agent? +Yes: implement in `agent-library/agents//`. +No: continue. +2. Does the behavior represent reusable infrastructure needed by multiple agents? +Yes: implement in shared files (`agent/src/lib/*` or `agent/src/index.js`) with compatibility checks. +No: keep it agent-local. +3. Is this a bug in shared infrastructure affecting multiple agents? +Yes: patch shared code and note impacted modules in the PR. +No: keep it agent-local. + +## Allowed Agent-Local Changes + +- Prompt logic and tool-choice strategy in a single agent. +- Parsing rules unique to one commitment format. +- Agent-specific scheduling or timelock behavior. +- Agent-specific metadata generation, tests, and fixtures. + +## Allowed Shared-Runner Changes + +- Common transport/signer/config helpers used by multiple agents. +- Shared proposal/dispute plumbing with no commitment-specific assumptions. +- Defect fixes in existing shared logic that impact more than one agent. + +## Anti-Patterns + +- Adding `if (agentName === "...")` branches in shared runner code for new behavior. +- Hardcoding commitment-specific policy in `agent/src/lib/`. +- Reusing shared modules as a shortcut for single-agent feature work. + +## Pull Request Checklist + +- [ ] Agent-specific behavior is implemented in `agent-library/agents//`. +- [ ] Shared runner files were changed only for cross-agent infrastructure or shared bug fixes. +- [ ] If shared files changed, PR includes: +Rationale for why agent-local implementation was insufficient. +List of existing agents affected. +- [ ] Relevant tests/simulations were run and listed. diff --git a/docs/templates/AGENTS.md.template b/docs/templates/AGENTS.md.template new file mode 100644 index 00000000..0ad53398 --- /dev/null +++ b/docs/templates/AGENTS.md.template @@ -0,0 +1,25 @@ +# Area Guidelines + +## Scope + +This file applies to: + +- `` +- `` + +## Purpose + +Describe what belongs in this area and how it fits the repository. + +## Rules + +- Keep changes local to this area unless they are clearly cross-cutting. +- Reference root `AGENTS.md` and `CONTRIBUTING.md` for global expectations. + +## Locality Rule + +If this area includes agent implementations, keep agent-specific functionality in local agent files instead of shared generalized files. + +## Validation + +List the minimum checks contributors should run for changes in this area. diff --git a/docs/templates/AGENT_README.md.template b/docs/templates/AGENT_README.md.template new file mode 100644 index 00000000..20ab7c92 --- /dev/null +++ b/docs/templates/AGENT_README.md.template @@ -0,0 +1,20 @@ +# + +## Purpose + +Describe the commitment or policy this agent is designed to serve. + +## Files + +- `agent.js`: agent-specific logic and decision flow. +- `commitment.txt`: plain-language commitment/rules. +- `agent.json`: metadata (if applicable). + +## Locality Rule + +Agent-specific behavior for this module must stay in this directory. Do not move single-agent logic into shared generalized files in `agent/src/lib/` or `agent/src/index.js`. + +## Validation + +- `` +- `` diff --git a/docs/templates/AREA_README.md.template b/docs/templates/AREA_README.md.template new file mode 100644 index 00000000..000e5353 --- /dev/null +++ b/docs/templates/AREA_README.md.template @@ -0,0 +1,25 @@ +# + +## What This Area Contains + +Short description of responsibility for this folder. + +## When To Edit This Area + +- Use for `` +- Avoid for `` + +## Extension Points + +- `` +- `` + +## Anti-Patterns + +- `` +- `` + +## How To Validate Changes + +- `` +- `` From 374c114b3c82983ba745208fbdb1166d00fa84be Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 11:38:24 -0800 Subject: [PATCH 064/174] remove github workflow job Signed-off-by: John Shutt --- .github/workflows/test.yml | 56 -------------------------------------- 1 file changed, 56 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c056de5..b79c8d4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,62 +11,6 @@ env: FOUNDRY_PROFILE: ci jobs: - shared-agent-justification: - name: Shared agent justification - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - steps: - - name: Validate shared runner justification - uses: actions/github-script@v7 - with: - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const pull_number = context.payload.pull_request.number; - - const files = await github.paginate(github.rest.pulls.listFiles, { - owner, - repo, - pull_number, - per_page: 100 - }); - - const touchedSharedRunner = files.some((f) => - f.filename.startsWith("agent/src/lib/") || f.filename === "agent/src/index.js" - ); - - if (!touchedSharedRunner) { - core.info("Shared runner files not changed; skipping justification check."); - return; - } - - const body = context.payload.pull_request.body || ""; - const marker = "## Shared Runner Justification"; - const start = body.indexOf(marker); - if (start === -1) { - core.setFailed("PR touches shared runner files but is missing the '## Shared Runner Justification' section."); - return; - } - - const sectionText = body.slice(start + marker.length); - const nextHeading = sectionText.search(/\n##\s+/); - const section = nextHeading === -1 ? sectionText : sectionText.slice(0, nextHeading); - - const normalized = section - .replace(/[#>*`_\-\[\]\(\)\r\n]/g, " ") - .replace(/\s+/g, " ") - .trim(); - - if (normalized.length < 40) { - core.setFailed("PR touches shared runner files but Shared Runner Justification is too short. Explain why agent-local implementation was insufficient and which agents are impacted."); - return; - } - - core.info("Shared runner justification present."); - check: name: Foundry project runs-on: ubuntu-latest From 69952a3225cabd5e2907f94b6d0ae2e5ff14d59a Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 11:39:27 -0800 Subject: [PATCH 065/174] remove pull request template Signed-off-by: John Shutt --- .github/pull_request_template.md | 36 -------------------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 9ef9e192..00000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,36 +0,0 @@ -## Summary - -Describe the change and why it is needed. - -## Scope - -- [ ] Solidity (`src/`, `script/`, `test/`) -- [ ] Shared agent runner (`agent/`) -- [ ] Agent module(s) (`agent-library/agents/*`) -- [ ] Frontend (`frontend/`) -- [ ] Docs only - -## Agent Locality Checks - -- [ ] If this PR adds or changes behavior for a specific agent, that behavior is implemented in `agent-library/agents//`. -- [ ] If shared generalized agent files (`agent/src/lib/*`, `agent/src/index.js`) were modified, this PR includes cross-agent justification. - -## Shared Runner Justification - -Required when changing shared generalized agent files. - -Why was an agent-local implementation insufficient? - -Which existing agents are impacted? - -## Testing - -List commands run and key outcomes. - -- [ ] `forge fmt` (if Solidity changed) -- [ ] `forge test` (if Solidity changed) -- [ ] Agent module tests/simulations (if `agent/` or `agent-library/` changed) - -## Configuration Impact - -Document added/changed env vars, addresses, salts, or deployment assumptions. From 063e00856fb85d16b4f54d7bf3ff3d368ac5322e Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 12:11:07 -0800 Subject: [PATCH 066/174] add commitment writing skill Signed-off-by: John Shutt --- AGENTS.md | 1 + CONTRIBUTING.md | 1 + README.md | 1 + skills/add-agent-commitment/SKILL.md | 69 ++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 skills/add-agent-commitment/SKILL.md diff --git a/AGENTS.md b/AGENTS.md index 4626d52f..92c67051 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - `AGENTS.md`: normative instructions for coding agents and automation. - `README.md`: architecture, intent, and operational context for humans. - `CONTRIBUTING.md`: shared contributor workflow and policy across the repo. +- `skills/add-agent-commitment/SKILL.md`: reusable workflow for adding new agent/commitment combos. - Precedence when instructions conflict: 1. Closest file to the changed code path wins. 2. `AGENTS.md` is authoritative for agent behavior. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80aa2d58..2081e222 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,7 @@ This repository uses two kinds of contribution guidance: - `AGENTS.md` for machine-targeted, normative instructions. - `README.md` for human-oriented architecture and workflow context. +- `skills/add-agent-commitment/SKILL.md` for creating new agent/commitment modules. ## Precedence Rules diff --git a/README.md b/README.md index 9c2417f8..012908c6 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ forge script script/DeploySafeWithOptimisticGovernor.s.sol:DeploySafeWithOptimis ## Documentation - Contribution workflow and policy: `CONTRIBUTING.md` +- Skill for new agent/commitment combos: `skills/add-agent-commitment/SKILL.md` - Deployment and configuration: `docs/deployment.md` - Signer options and `with-signer` helper: `docs/signers.md` - Offchain agent usage: `docs/agent.md` diff --git a/skills/add-agent-commitment/SKILL.md b/skills/add-agent-commitment/SKILL.md new file mode 100644 index 00000000..a5874172 --- /dev/null +++ b/skills/add-agent-commitment/SKILL.md @@ -0,0 +1,69 @@ +--- +name: add-agent-commitment +description: Use when creating a new commitment/agent combo in this repo. Scaffolds a new module under agent-library/agents/, keeps commitment-specific logic local to that module, and validates the module without adding one-off behavior to shared runner files. +--- + +# Add Agent/Commitment Combo + +## When To Use + +Use this skill when a user asks to: + +- Add a new agent to `agent-library/agents/` +- Add a new commitment-specific behavior +- Create a new commitment + agent module pair for registration/testing + +## Repository Rules This Skill Enforces + +- Put commitment-specific logic in `agent-library/agents//`. +- Do not add single-agent behavior to shared generalized files like `agent/src/index.js` and `agent/src/lib/*`. +- Only change shared runner files for cross-agent infrastructure or shared bug fixes. + +## Required Inputs + +Collect these before editing: + +1. `agent_name` (kebab-case directory name) +2. Commitment text/rules for `commitment.txt` +3. Behavior constraints (what the agent may and may not do) +4. Metadata inputs needed for `agent.json` (name/description/network pointers) + +## Workflow + +1. Copy `agent-library/agents/default/` to `agent-library/agents//`. +2. Update `agent-library/agents//commitment.txt`. +3. Implement commitment-specific logic in `agent-library/agents//agent.js`. +4. Update `agent-library/agents//agent.json`. +5. Add or update module-local test/simulation scripts in that same module folder. +6. Validate with `node agent/scripts/validate-agent.mjs --module=` and module-specific tests/simulations under `agent-library/agents//`. +7. Summarize changed files and validation commands. + +## Pull Request Expectations + +When opening a PR for a new module: + +- State that agent-specific behavior is isolated to `agent-library/agents//`. +- If shared runner files were changed, explain why an agent-local implementation was insufficient and list impacted agents. +- Include exact validation commands run. + +## Local Setup For Codex And Claude Code + +This repository stores the skill at: + +- `skills/add-agent-commitment/SKILL.md` + +To use it locally, register this folder in your assistant's skills path. + +Codex option (symlink into your local skills directory): + +```bash +mkdir -p "$CODEX_HOME/skills" +ln -sfn "$(pwd)/skills/add-agent-commitment" "$CODEX_HOME/skills/add-agent-commitment" +``` + +Claude Code option: + +- Add or symlink `skills/add-agent-commitment/` into the skills directory configured in your Claude Code setup. +- If your team uses a shared Claude config path, point that path at this repo skill folder. + +After setup, invoke by name (`add-agent-commitment`) or ask to "create a new agent/commitment combo". From 7db01b1012f03aa37c33b7e955f2a3acb8012db3 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 12:14:09 -0800 Subject: [PATCH 067/174] update codex instructions for skill installation Signed-off-by: John Shutt --- skills/add-agent-commitment/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/skills/add-agent-commitment/SKILL.md b/skills/add-agent-commitment/SKILL.md index a5874172..3dbd26c4 100644 --- a/skills/add-agent-commitment/SKILL.md +++ b/skills/add-agent-commitment/SKILL.md @@ -57,6 +57,7 @@ To use it locally, register this folder in your assistant's skills path. Codex option (symlink into your local skills directory): ```bash +export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" mkdir -p "$CODEX_HOME/skills" ln -sfn "$(pwd)/skills/add-agent-commitment" "$CODEX_HOME/skills/add-agent-commitment" ``` From 2713db893a2a525aa2f78e74297d704039b6ab64 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 12:14:30 -0800 Subject: [PATCH 068/174] remove skill reference from CONTRIBUTING Signed-off-by: John Shutt --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2081e222..80aa2d58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,6 @@ This repository uses two kinds of contribution guidance: - `AGENTS.md` for machine-targeted, normative instructions. - `README.md` for human-oriented architecture and workflow context. -- `skills/add-agent-commitment/SKILL.md` for creating new agent/commitment modules. ## Precedence Rules From 9da7d6c38851390b4aea2dd92c90f1a159c4b529 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 12:40:38 -0800 Subject: [PATCH 069/174] add scaffolding for price-race-swap commitment Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 29 ++++++++++ .../agents/price-race-swap/agent.json | 18 ++++++ .../agents/price-race-swap/commitment.txt | 20 +++++++ .../simulate-price-race-swap.mjs | 58 +++++++++++++++++++ .../test-price-race-swap-agent.mjs | 19 ++++++ 5 files changed, 144 insertions(+) create mode 100644 agent-library/agents/price-race-swap/agent.js create mode 100644 agent-library/agents/price-race-swap/agent.json create mode 100644 agent-library/agents/price-race-swap/commitment.txt create mode 100644 agent-library/agents/price-race-swap/simulate-price-race-swap.mjs create mode 100644 agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js new file mode 100644 index 00000000..5682a2c3 --- /dev/null +++ b/agent-library/agents/price-race-swap/agent.js @@ -0,0 +1,29 @@ +function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { + const mode = proposeEnabled && disputeEnabled + ? 'You may propose and dispute.' + : proposeEnabled + ? 'You may propose but you may not dispute.' + : disputeEnabled + ? 'You may dispute but you may not propose.' + : 'You may not propose or dispute; provide opinions only.'; + + return [ + 'You are a price-race swap agent for a commitment Safe controlled by an Optimistic Governor.', + 'Your own address is provided as agentAddress.', + 'Interpret the commitment as a two-branch race and execute at most one winning branch.', + 'First trigger wins. If both triggers appear true in one cycle, ETH branch wins the tie-break.', + 'Use all currently available USDC in the Safe for the winning branch swap.', + 'Preferred flow: build_og_transactions with contract_call actions for approve + Uniswap swap, then post_bond_and_propose.', + 'When pool addresses are specified in the commitment/rules, use those pools. Otherwise prefer a high-liquidity Uniswap route and enforce commitment slippage limits.', + 'Never execute both branches, and never route the purchased asset to addresses other than the commitment Safe unless the commitment explicitly says so.', + 'If there is insufficient evidence that a trigger fired first, or route/liquidity/slippage constraints are not safely satisfiable, return ignore.', + 'Default to disputing proposals that violate these rules; prefer no-op when unsure.', + mode, + commitmentText ? `Commitment text:\n${commitmentText}` : '', + 'If no action is needed, output strict JSON with keys: action (propose|deposit|dispute|ignore|other) and rationale (string).', + ] + .filter(Boolean) + .join(' '); +} + +export { getSystemPrompt }; diff --git a/agent-library/agents/price-race-swap/agent.json b/agent-library/agents/price-race-swap/agent.json new file mode 100644 index 00000000..faf91d52 --- /dev/null +++ b/agent-library/agents/price-race-swap/agent.json @@ -0,0 +1,18 @@ +{ + "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", + "name": "Oya Price Race Swap Agent", + "description": "Agent that monitors ETH/USDC and UMA/USDC threshold triggers and executes one first-trigger-wins USDC swap branch on Uniswap.", + "image": "https://raw.githubusercontent.com/oya-commitments/oya-commitments/main/agent-library/agents/price-race-swap/agent.png", + "endpoints": [ + { + "name": "agentWallet", + "endpoint": "eip155:11155111:0x2967c076182f0303037072670e744e26ed4a830f" + } + ], + "registrations": [ + { + "agentId": 0, + "agentRegistry": "eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e" + } + ] +} diff --git a/agent-library/agents/price-race-swap/commitment.txt b/agent-library/agents/price-race-swap/commitment.txt new file mode 100644 index 00000000..7d588c97 --- /dev/null +++ b/agent-library/agents/price-race-swap/commitment.txt @@ -0,0 +1,20 @@ +Use any available USDC balance currently held by the commitment Safe at the time of execution. + +Monitor two trigger conditions and execute exactly one branch: + +1) ETH branch +- If ETH/USDC >= 3200 first, buy ETH with all available USDC. +- Execute on Uniswap. +- If a specific pool is configured, use that pool; otherwise use a high-liquidity Uniswap pool. + +2) UMA branch +- If UMA/USDC <= 2.10 first, buy UMA with all available USDC. +- Execute on Uniswap. +- If a specific pool is configured, use that pool; otherwise use a high-liquidity Uniswap pool. + +Global rules: +- First trigger wins. +- Execute at most one swap total for this commitment. +- If both triggers become true in the same evaluation cycle, prefer ETH branch. +- Enforce max slippage of 0.50%. +- If no valid route satisfies slippage and liquidity constraints, do not trade. diff --git a/agent-library/agents/price-race-swap/simulate-price-race-swap.mjs b/agent-library/agents/price-race-swap/simulate-price-race-swap.mjs new file mode 100644 index 00000000..fa191567 --- /dev/null +++ b/agent-library/agents/price-race-swap/simulate-price-race-swap.mjs @@ -0,0 +1,58 @@ +function pickWinningBranch({ ethPrice, umaPrice, ethThreshold, umaThreshold }) { + const ethTriggered = ethPrice >= ethThreshold; + const umaTriggered = umaPrice <= umaThreshold; + + if (ethTriggered && umaTriggered) { + return { + winner: 'eth', + reason: 'tie-break: ETH wins when both are true in same evaluation cycle', + }; + } + + if (ethTriggered) { + return { + winner: 'eth', + reason: `ETH/USDC ${ethPrice} >= ${ethThreshold}`, + }; + } + + if (umaTriggered) { + return { + winner: 'uma', + reason: `UMA/USDC ${umaPrice} <= ${umaThreshold}`, + }; + } + + return { + winner: 'none', + reason: 'no trigger hit', + }; +} + +function run() { + const scenarios = [ + { + name: 'ETH wins', + input: { ethPrice: 3250, umaPrice: 2.8, ethThreshold: 3200, umaThreshold: 2.1 }, + }, + { + name: 'UMA wins', + input: { ethPrice: 3000, umaPrice: 2.0, ethThreshold: 3200, umaThreshold: 2.1 }, + }, + { + name: 'Tie -> ETH wins', + input: { ethPrice: 3200, umaPrice: 2.1, ethThreshold: 3200, umaThreshold: 2.1 }, + }, + { + name: 'No trigger', + input: { ethPrice: 3100, umaPrice: 2.4, ethThreshold: 3200, umaThreshold: 2.1 }, + }, + ]; + + for (const scenario of scenarios) { + const result = pickWinningBranch(scenario.input); + console.log(`[sim] ${scenario.name}:`, result); + } +} + +run(); diff --git a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs new file mode 100644 index 00000000..bd1bec43 --- /dev/null +++ b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import { getSystemPrompt } from './agent.js'; + +function run() { + const prompt = getSystemPrompt({ + proposeEnabled: true, + disputeEnabled: true, + commitmentText: 'Price race commitment text.', + }); + + assert.ok(prompt.includes('First trigger wins')); + assert.ok(prompt.includes('Use all currently available USDC in the Safe')); + assert.ok(prompt.includes('execute at most one winning branch')); + assert.ok(prompt.includes('Commitment text')); + + console.log('[test] price-race-swap prompt OK'); +} + +run(); From 1a88a545800eec36e05d2266ad32da2dbff91d97 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 12:57:54 -0800 Subject: [PATCH 070/174] add tooling for price-triggered swaps Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 2 +- agent/.env.example | 14 ++ agent/README.md | 36 +++- agent/scripts/test-build-og-transactions.mjs | 51 +++++ agent/scripts/test-price-signals.mjs | 120 ++++++++++++ agent/src/index.js | 13 ++ agent/src/lib/config.js | 45 ++++- agent/src/lib/llm.js | 4 + agent/src/lib/price.js | 178 ++++++++++++++++++ agent/src/lib/tools.js | 36 +++- agent/src/lib/tx.js | 83 +++++++- agent/src/lib/utils.js | 10 + 12 files changed, 580 insertions(+), 12 deletions(-) create mode 100644 agent/scripts/test-build-og-transactions.mjs create mode 100644 agent/scripts/test-price-signals.mjs create mode 100644 agent/src/lib/price.js diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 5682a2c3..cee68da3 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -13,7 +13,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'Interpret the commitment as a two-branch race and execute at most one winning branch.', 'First trigger wins. If both triggers appear true in one cycle, ETH branch wins the tie-break.', 'Use all currently available USDC in the Safe for the winning branch swap.', - 'Preferred flow: build_og_transactions with contract_call actions for approve + Uniswap swap, then post_bond_and_propose.', + 'Preferred flow: build_og_transactions with uniswap_v3_exact_input_single actions, then post_bond_and_propose.', 'When pool addresses are specified in the commitment/rules, use those pools. Otherwise prefer a high-liquidity Uniswap route and enforce commitment slippage limits.', 'Never execute both branches, and never route the purchased asset to addresses other than the commitment Safe unless the commitment explicitly says so.', 'If there is insufficient evidence that a trigger fired first, or route/liquidity/slippage constraints are not safely satisfiable, return ignore.', diff --git a/agent/.env.example b/agent/.env.example index 288bbb24..b7e732a0 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -33,6 +33,20 @@ PRIVATE_KEY=0x... # Optional tuning POLL_INTERVAL_MS=60000 WATCH_NATIVE_BALANCE=true +# Optional price trigger signals (JSON array) +# PRICE_TRIGGERS_JSON=[ +# { +# "id":"eth-breakout", +# "label":"ETH >= 3200", +# "pool":"0xUniswapV3PoolAddress", +# "baseToken":"0xWETH", +# "quoteToken":"0xUSDC", +# "comparator":"gte", +# "threshold":3200, +# "priority":0, +# "emitOnce":true +# } +# ] # PROPOSE_ENABLED=true # DISPUTE_ENABLED=true # START_BLOCK= diff --git a/agent/README.md b/agent/README.md index 3b5ec7eb..152f34fd 100644 --- a/agent/README.md +++ b/agent/README.md @@ -25,7 +25,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` (macOS Keychain or Linux Secret Service) - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE` + - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE`, `PRICE_TRIGGERS_JSON` - Optional proposals: `PROPOSE_ENABLED` (default true) - Optional disputes: `DISPUTE_ENABLED` (default true), `DISPUTE_RETRY_MS` (default 60000) - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` @@ -67,9 +67,43 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig - **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. - **Optional LLM decisions**: If `OPENAI_API_KEY` is set, the runner will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions in the agent module. - **Timelock triggers**: Parses plain language timelocks in rules (absolute dates or “X minutes after deposit”) and emits `timelock` signals when due. +- **Price triggers**: Optionally evaluates configured Uniswap V3 pools and emits `priceTrigger` signals when `PRICE_TRIGGERS_JSON` thresholds are hit. All other behavior is intentionally left out. Implement your own agent in `agent-library/agents//agent.js` to add commitment-specific logic and tool use. +### Price Trigger Config + +Use `PRICE_TRIGGERS_JSON` to generate deterministic threshold-hit signals: + +```json +[ + { + "id": "eth-breakout", + "label": "ETH >= 3200", + "pool": "0xUniswapV3PoolAddress", + "baseToken": "0xWETH", + "quoteToken": "0xUSDC", + "comparator": "gte", + "threshold": 3200, + "priority": 0, + "emitOnce": true + } +] +``` + +- `comparator`: `gte` or `lte` +- `threshold`: quote-token price per one base token +- `priority`: tie-break ordering when multiple triggers hit in the same cycle (lower first) +- `emitOnce`: if true, emit only the first time the threshold condition turns true + +### Uniswap Swap Action in `build_og_transactions` + +`build_og_transactions` supports action kind `uniswap_v3_exact_input_single`, which expands to: +1. ERC20 `approve(tokenIn -> router, amountInWei)` +2. Router `exactInputSingle(...)` + +This lets agents propose reusable Uniswap swap calldata without embedding raw ABI in prompts. + ### Propose vs Dispute Modes Set `PROPOSE_ENABLED` and `DISPUTE_ENABLED` to control behavior: diff --git a/agent/scripts/test-build-og-transactions.mjs b/agent/scripts/test-build-og-transactions.mjs new file mode 100644 index 00000000..b9de7db8 --- /dev/null +++ b/agent/scripts/test-build-og-transactions.mjs @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict'; +import { decodeFunctionData, erc20Abi, parseAbi } from 'viem'; +import { buildOgTransactions } from '../src/lib/tx.js'; + +function run() { + const router = '0xE592427A0AEce92De3Edee1F18E0157C05861564'; + const usdc = '0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; + const weth = '0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2'; + const recipient = '0x1111111111111111111111111111111111111111'; + + const txs = buildOgTransactions([ + { + kind: 'uniswap_v3_exact_input_single', + token: null, + to: null, + amountWei: null, + valueWei: null, + abi: null, + args: null, + operation: 0, + router, + tokenIn: usdc, + tokenOut: weth, + fee: 3000, + recipient, + amountInWei: '1000000', + amountOutMinWei: '1', + sqrtPriceLimitX96: null, + }, + ]); + + assert.equal(txs.length, 2); + assert.equal(txs[0].to.toLowerCase(), usdc.toLowerCase()); + assert.equal(txs[1].to.toLowerCase(), router.toLowerCase()); + const approveCall = decodeFunctionData({ + abi: erc20Abi, + data: txs[0].data, + }); + assert.equal(approveCall.functionName, 'approve'); + + const swapCall = decodeFunctionData({ + abi: parseAbi([ + 'function exactInputSingle((address tokenIn,address tokenOut,uint24 fee,address recipient,uint256 amountIn,uint256 amountOutMinimum,uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)', + ]), + data: txs[1].data, + }); + assert.equal(swapCall.functionName, 'exactInputSingle'); + console.log('[test] buildOgTransactions uniswap action OK'); +} + +run(); diff --git a/agent/scripts/test-price-signals.mjs b/agent/scripts/test-price-signals.mjs new file mode 100644 index 00000000..b7bcca99 --- /dev/null +++ b/agent/scripts/test-price-signals.mjs @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import { collectPriceTriggerSignals } from '../src/lib/price.js'; + +const WETH = '0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2'; +const USDC = '0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; +const UMA = '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828'; +const POOL_ETH_USDC = '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8'; +const POOL_UMA_USDC = '0x88D97d199b9ED37C29D846d00D443De980832a22'; + +function sqrtPriceX96ForQuotePerBase({ quotePerBase, baseDecimals, quoteDecimals }) { + const raw = quotePerBase * 10 ** (quoteDecimals - baseDecimals); + const sqrtPrice = Math.sqrt(raw); + return BigInt(Math.floor(sqrtPrice * 2 ** 96)); +} + +function buildMockPublicClient() { + const slot0ByPool = new Map([ + [ + POOL_ETH_USDC.toLowerCase(), + [sqrtPriceX96ForQuotePerBase({ quotePerBase: 3200, baseDecimals: 18, quoteDecimals: 6 })], + ], + [ + POOL_UMA_USDC.toLowerCase(), + [sqrtPriceX96ForQuotePerBase({ quotePerBase: 2.0, baseDecimals: 18, quoteDecimals: 6 })], + ], + ]); + + return { + async readContract({ address, functionName }) { + const addr = address.toLowerCase(); + if (functionName === 'token0') { + if (addr === POOL_ETH_USDC.toLowerCase()) return WETH; + if (addr === POOL_UMA_USDC.toLowerCase()) return UMA; + } + if (functionName === 'token1') { + if (addr === POOL_ETH_USDC.toLowerCase()) return USDC; + if (addr === POOL_UMA_USDC.toLowerCase()) return USDC; + } + if (functionName === 'slot0') { + return slot0ByPool.get(addr); + } + if (functionName === 'decimals') { + if (addr === WETH.toLowerCase() || addr === UMA.toLowerCase()) return 18; + if (addr === USDC.toLowerCase()) return 6; + } + throw new Error(`Unexpected mock readContract call: ${functionName} ${address}`); + }, + }; +} + +async function run() { + const publicClient = buildMockPublicClient(); + const triggerState = new Map(); + const tokenMetaCache = new Map(); + const poolMetaCache = new Map(); + + const signals = await collectPriceTriggerSignals({ + publicClient, + triggers: [ + { + id: 'eth-breakout', + label: 'ETH >= 3200', + pool: POOL_ETH_USDC, + baseToken: WETH, + quoteToken: USDC, + comparator: 'gte', + threshold: 3200, + priority: 0, + emitOnce: true, + }, + { + id: 'uma-drop', + label: 'UMA <= 2.1', + pool: POOL_UMA_USDC, + baseToken: UMA, + quoteToken: USDC, + comparator: 'lte', + threshold: 2.1, + priority: 1, + emitOnce: true, + }, + ], + nowMs: Date.now(), + triggerState, + tokenMetaCache, + poolMetaCache, + }); + + assert.equal(signals.length, 2); + assert.equal(signals[0].triggerId, 'eth-breakout'); + assert.equal(signals[1].triggerId, 'uma-drop'); + + const secondPass = await collectPriceTriggerSignals({ + publicClient, + triggers: [ + { + id: 'eth-breakout', + pool: POOL_ETH_USDC, + baseToken: WETH, + quoteToken: USDC, + comparator: 'gte', + threshold: 3200, + priority: 0, + emitOnce: true, + }, + ], + nowMs: Date.now(), + triggerState, + tokenMetaCache, + poolMetaCache, + }); + + assert.equal(secondPass.length, 0); + console.log('[test] price trigger signal collection OK'); +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/agent/src/index.js b/agent/src/index.js index 66db9bb1..5940ab37 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -20,6 +20,7 @@ import { callAgent, explainToolCalls } from './lib/llm.js'; import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; import { extractTimelockTriggers } from './lib/timelock.js'; +import { collectPriceTriggerSignals } from './lib/price.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -42,6 +43,9 @@ const proposalsByHash = new Map(); const depositHistory = []; const blockTimestampCache = new Map(); const timelockTriggers = new Map(); +const priceTriggerState = new Map(); +const tokenMetaCache = new Map(); +const poolMetaCache = new Map(); async function loadAgentModule() { const agentRef = config.agentModule ?? 'default'; @@ -223,6 +227,14 @@ async function agentLoop() { const rulesText = ogContext?.rules ?? commitmentText ?? ''; updateTimelockSchedule({ rulesText }); const dueTimelocks = collectDueTimelocks(nowMs); + const duePriceSignals = await collectPriceTriggerSignals({ + publicClient, + triggers: config.priceTriggers, + nowMs, + triggerState: priceTriggerState, + tokenMetaCache, + poolMetaCache, + }); const combinedSignals = deposits.concat( newProposals.map((proposal) => ({ @@ -247,6 +259,7 @@ async function agentLoop() { deposit: trigger.deposit, }); } + combinedSignals.push(...duePriceSignals); if (combinedSignals.length > 0) { const decisionOk = await decideOnSignals(combinedSignals); diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index e0ec9feb..3dd08931 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -1,5 +1,47 @@ import { getAddress } from 'viem'; -import { mustGetEnv, parseAddressList } from './utils.js'; +import { mustGetEnv, parseAddressList, parseJsonEnv } from './utils.js'; + +function normalizePriceTrigger(trigger, index) { + if (!trigger || typeof trigger !== 'object') { + throw new Error(`PRICE_TRIGGERS_JSON[${index}] must be an object`); + } + if (!trigger.pool || !trigger.baseToken || !trigger.quoteToken) { + throw new Error( + `PRICE_TRIGGERS_JSON[${index}] requires pool, baseToken, and quoteToken` + ); + } + if (!trigger.comparator || !['gte', 'lte'].includes(trigger.comparator)) { + throw new Error(`PRICE_TRIGGERS_JSON[${index}] comparator must be "gte" or "lte"`); + } + if (trigger.threshold === undefined || trigger.threshold === null) { + throw new Error(`PRICE_TRIGGERS_JSON[${index}] requires threshold`); + } + + const threshold = Number(trigger.threshold); + if (!Number.isFinite(threshold)) { + throw new Error(`PRICE_TRIGGERS_JSON[${index}] threshold must be numeric`); + } + + return { + id: trigger.id ?? `price-trigger-${index}`, + label: trigger.label, + pool: getAddress(trigger.pool), + baseToken: getAddress(trigger.baseToken), + quoteToken: getAddress(trigger.quoteToken), + comparator: trigger.comparator, + threshold, + priority: Number(trigger.priority ?? 0), + emitOnce: trigger.emitOnce === undefined ? true : Boolean(trigger.emitOnce), + }; +} + +function parsePriceTriggers(raw) { + const parsed = parseJsonEnv(raw, []); + if (!Array.isArray(parsed)) { + throw new Error('PRICE_TRIGGERS_JSON must be a JSON array'); + } + return parsed.map(normalizePriceTrigger); +} function buildConfig() { return { @@ -38,6 +80,7 @@ function buildConfig() { : process.env.DISPUTE_ENABLED.toLowerCase() !== 'false', disputeRetryMs: Number(process.env.DISPUTE_RETRY_MS ?? 60_000), agentModule: process.env.AGENT_MODULE, + priceTriggers: parsePriceTriggers(process.env.PRICE_TRIGGERS_JSON), }; } diff --git a/agent/src/lib/llm.js b/agent/src/lib/llm.js index ff21f408..09e44eb9 100644 --- a/agent/src/lib/llm.js +++ b/agent/src/lib/llm.js @@ -83,6 +83,10 @@ async function callAgent({ signal.triggerTimestampMs !== undefined ? signal.triggerTimestampMs.toString() : undefined, + threshold: + signal.threshold !== undefined ? String(signal.threshold) : undefined, + observedPrice: + signal.observedPrice !== undefined ? String(signal.observedPrice) : undefined, }; }); diff --git a/agent/src/lib/price.js b/agent/src/lib/price.js new file mode 100644 index 00000000..21311a24 --- /dev/null +++ b/agent/src/lib/price.js @@ -0,0 +1,178 @@ +import { erc20Abi, getAddress, parseAbi } from 'viem'; + +const uniswapV3PoolAbi = parseAbi([ + 'function token0() view returns (address)', + 'function token1() view returns (address)', + 'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)', +]); + +async function loadPoolMeta({ publicClient, pool, tokenMetaCache, poolMetaCache }) { + const cached = poolMetaCache.get(pool); + if (cached) return cached; + + const [token0, token1] = await Promise.all([ + publicClient.readContract({ + address: pool, + abi: uniswapV3PoolAbi, + functionName: 'token0', + }), + publicClient.readContract({ + address: pool, + abi: uniswapV3PoolAbi, + functionName: 'token1', + }), + ]); + + const [normalizedToken0, normalizedToken1] = [getAddress(token0), getAddress(token1)]; + + const ensureDecimals = async (token) => { + if (tokenMetaCache.has(token)) return tokenMetaCache.get(token); + const decimals = Number( + await publicClient.readContract({ + address: token, + abi: erc20Abi, + functionName: 'decimals', + }) + ); + tokenMetaCache.set(token, { decimals }); + return tokenMetaCache.get(token); + }; + + await Promise.all([ensureDecimals(normalizedToken0), ensureDecimals(normalizedToken1)]); + + const meta = { + token0: normalizedToken0, + token1: normalizedToken1, + }; + poolMetaCache.set(pool, meta); + return meta; +} + +function quotePerBaseFromSqrtPriceX96({ sqrtPriceX96, token0Decimals, token1Decimals, baseIsToken0 }) { + const sqrt = Number(sqrtPriceX96); + if (!Number.isFinite(sqrt) || sqrt <= 0) { + throw new Error('Invalid sqrtPriceX96 from pool slot0.'); + } + + const q192 = 2 ** 192; + const rawToken1PerToken0 = (sqrt * sqrt) / q192; + + if (baseIsToken0) { + return rawToken1PerToken0 * 10 ** (token0Decimals - token1Decimals); + } + + if (rawToken1PerToken0 === 0) { + throw new Error('Pool price resolved to zero.'); + } + + return (1 / rawToken1PerToken0) * 10 ** (token1Decimals - token0Decimals); +} + +function evaluateComparator({ comparator, price, threshold }) { + if (comparator === 'gte') return price >= threshold; + if (comparator === 'lte') return price <= threshold; + throw new Error(`Unsupported comparator: ${comparator}`); +} + +async function collectPriceTriggerSignals({ + publicClient, + triggers, + nowMs, + triggerState, + tokenMetaCache, + poolMetaCache, +}) { + if (!Array.isArray(triggers) || triggers.length === 0) { + return []; + } + + const evaluations = []; + + for (const trigger of triggers) { + const pool = getAddress(trigger.pool); + const baseToken = getAddress(trigger.baseToken); + const quoteToken = getAddress(trigger.quoteToken); + + const poolMeta = await loadPoolMeta({ + publicClient, + pool, + tokenMetaCache, + poolMetaCache, + }); + + const baseIsToken0 = + poolMeta.token0 === baseToken && poolMeta.token1 === quoteToken; + const baseIsToken1 = + poolMeta.token1 === baseToken && poolMeta.token0 === quoteToken; + + if (!baseIsToken0 && !baseIsToken1) { + console.warn( + `[agent] Price trigger ${trigger.id} skipped: pool ${pool} does not match base/quote tokens.` + ); + continue; + } + + const slot0 = await publicClient.readContract({ + address: pool, + abi: uniswapV3PoolAbi, + functionName: 'slot0', + }); + + const token0Meta = tokenMetaCache.get(poolMeta.token0); + const token1Meta = tokenMetaCache.get(poolMeta.token1); + + const price = quotePerBaseFromSqrtPriceX96({ + sqrtPriceX96: slot0[0], + token0Decimals: token0Meta.decimals, + token1Decimals: token1Meta.decimals, + baseIsToken0, + }); + + const matches = evaluateComparator({ + comparator: trigger.comparator, + price, + threshold: trigger.threshold, + }); + + const prior = triggerState.get(trigger.id) ?? { + fired: false, + lastMatched: false, + }; + + const shouldEmit = + matches && (!prior.lastMatched || (!trigger.emitOnce && !prior.fired)); + + triggerState.set(trigger.id, { + fired: prior.fired || (matches && trigger.emitOnce), + lastMatched: matches, + }); + + if (!shouldEmit || (trigger.emitOnce && prior.fired)) { + continue; + } + + evaluations.push({ + kind: 'priceTrigger', + triggerId: trigger.id, + triggerLabel: trigger.label, + priority: trigger.priority ?? 0, + pool, + baseToken, + quoteToken, + comparator: trigger.comparator, + threshold: trigger.threshold, + observedPrice: price, + triggerTimestampMs: nowMs, + }); + } + + evaluations.sort((a, b) => { + const priorityOrder = Number(a.priority) - Number(b.priority); + if (priorityOrder !== 0) return priorityOrder; + return String(a.triggerId).localeCompare(String(b.triggerId)); + }); + + return evaluations; +} + +export { collectPriceTriggerSignals }; diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 01798bea..8746c42f 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -23,7 +23,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { kind: { type: 'string', description: - 'Action type: erc20_transfer | native_transfer | contract_call', + 'Action type: erc20_transfer | native_transfer | contract_call | uniswap_v3_exact_input_single', }, token: { type: ['string', 'null'], @@ -78,6 +78,40 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { description: 'Safe operation (0=CALL,1=DELEGATECALL). Defaults to 0.', }, + router: { + type: ['string', 'null'], + description: + 'Uniswap V3 router address for uniswap_v3_exact_input_single.', + }, + tokenIn: { + type: ['string', 'null'], + description: 'Input ERC20 token for Uniswap swap action.', + }, + tokenOut: { + type: ['string', 'null'], + description: 'Output ERC20 token for Uniswap swap action.', + }, + fee: { + type: ['integer', 'null'], + description: 'Uniswap V3 pool fee tier (e.g. 500, 3000, 10000).', + }, + recipient: { + type: ['string', 'null'], + description: 'Recipient of Uniswap swap output tokens.', + }, + amountInWei: { + type: ['string', 'null'], + description: 'Input token amount for Uniswap swap in token wei.', + }, + amountOutMinWei: { + type: ['string', 'null'], + description: 'Minimum output amount for Uniswap swap in token wei.', + }, + sqrtPriceLimitX96: { + type: ['string', 'null'], + description: + 'Optional Uniswap sqrtPriceLimitX96 guard (default 0 for no limit).', + }, }, required: [ 'kind', diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index d3e1399e..f418cd32 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -304,7 +304,9 @@ function buildOgTransactions(actions) { throw new Error('actions must be a non-empty array'); } - return actions.map((action) => { + const transactions = []; + + for (const action of actions) { const operation = action.operation !== undefined ? Number(action.operation) : 0; if (action.kind === 'erc20_transfer') { @@ -318,12 +320,13 @@ function buildOgTransactions(actions) { args: [getAddress(action.to), BigInt(action.amountWei)], }); - return { + transactions.push({ to: getAddress(action.token), value: '0', data, operation, - }; + }); + continue; } if (action.kind === 'native_transfer') { @@ -331,12 +334,13 @@ function buildOgTransactions(actions) { throw new Error('native_transfer requires to, amountWei'); } - return { + transactions.push({ to: getAddress(action.to), value: BigInt(action.amountWei).toString(), data: '0x', operation, - }; + }); + continue; } if (action.kind === 'contract_call') { @@ -353,16 +357,79 @@ function buildOgTransactions(actions) { }); const value = action.valueWei !== undefined ? BigInt(action.valueWei).toString() : '0'; - return { + transactions.push({ to: getAddress(action.to), value, data, operation, - }; + }); + continue; + } + + if (action.kind === 'uniswap_v3_exact_input_single') { + if ( + !action.router || + !action.tokenIn || + !action.tokenOut || + action.fee === undefined || + !action.recipient || + action.amountInWei === undefined || + action.amountOutMinWei === undefined + ) { + throw new Error( + 'uniswap_v3_exact_input_single requires router, tokenIn, tokenOut, fee, recipient, amountInWei, amountOutMinWei' + ); + } + + const router = getAddress(action.router); + const tokenIn = getAddress(action.tokenIn); + const tokenOut = getAddress(action.tokenOut); + const recipient = getAddress(action.recipient); + const fee = Number(action.fee); + + const approveData = encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [router, BigInt(action.amountInWei)], + }); + transactions.push({ + to: tokenIn, + value: '0', + data: approveData, + operation, + }); + + const swapAbi = parseAbi([ + 'function exactInputSingle((address tokenIn,address tokenOut,uint24 fee,address recipient,uint256 amountIn,uint256 amountOutMinimum,uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)', + ]); + const swapData = encodeFunctionData({ + abi: swapAbi, + functionName: 'exactInputSingle', + args: [ + { + tokenIn, + tokenOut, + fee, + recipient, + amountIn: BigInt(action.amountInWei), + amountOutMinimum: BigInt(action.amountOutMinWei), + sqrtPriceLimitX96: BigInt(action.sqrtPriceLimitX96 ?? 0), + }, + ], + }); + transactions.push({ + to: router, + value: '0', + data: swapData, + operation, + }); + continue; } throw new Error(`Unknown action kind: ${action.kind}`); - }); + } + + return transactions; } async function makeDeposit({ diff --git a/agent/src/lib/utils.js b/agent/src/lib/utils.js index 1d491809..b80e62aa 100644 --- a/agent/src/lib/utils.js +++ b/agent/src/lib/utils.js @@ -49,9 +49,19 @@ function parseToolArguments(raw) { return null; } +function parseJsonEnv(raw, fallback) { + if (!raw) return fallback; + try { + return JSON.parse(raw); + } catch (error) { + throw new Error(`Invalid JSON env value: ${error?.message ?? String(error)}`); + } +} + export { mustGetEnv, normalizePrivateKey, + parseJsonEnv, parseAddressList, parseToolArguments, summarizeViemError, From 1637c24085a8b7b6bb9a6eadfdff90cf004379c1 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 13:02:54 -0800 Subject: [PATCH 071/174] don't use JSON with price triggers, parse from commitment Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 111 ++++++++++++++- .../agents/price-race-swap/commitment.txt | 24 ++-- .../test-price-race-swap-agent.mjs | 19 ++- agent/.env.example | 17 +-- agent/README.md | 35 ++--- agent/scripts/test-price-signals.mjs | 11 ++ agent/src/index.js | 29 +++- agent/src/lib/config.js | 56 ++------ agent/src/lib/llm.js | 1 + agent/src/lib/price.js | 130 ++++++++++++++++-- agent/src/lib/utils.js | 10 -- 11 files changed, 324 insertions(+), 119 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index cee68da3..c8bcafa5 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -1,3 +1,104 @@ +function normalizeAddress(raw) { + if (typeof raw !== 'string' || !/^0x[a-fA-F0-9]{40}$/.test(raw)) { + throw new Error(`Invalid address: ${raw}`); + } + return raw; +} + +function normalizeComparator(raw) { + if (raw === 'gte' || raw === '>=') return 'gte'; + if (raw === 'lte' || raw === '<=') return 'lte'; + throw new Error(`Unsupported comparator: ${raw}`); +} + +function parseTokenMap(commitmentText) { + const tokenMap = new Map(); + const lines = commitmentText.split('\n'); + for (const line of lines) { + const match = line.match(/^\s*-\s*([A-Z][A-Z0-9_]*)\s*=\s*(0x[a-fA-F0-9]{40})\s*$/); + if (!match) continue; + tokenMap.set(match[1], normalizeAddress(match[2])); + } + return tokenMap; +} + +function parseTriggerLine(line) { + const raw = line.replace(/^\s*-\s*/, ''); + const fields = raw + .split('|') + .map((segment) => segment.trim()) + .filter(Boolean); + + const out = {}; + for (const field of fields) { + const eq = field.indexOf('='); + if (eq <= 0) continue; + const key = field.slice(0, eq).trim(); + const value = field.slice(eq + 1).trim(); + out[key] = value; + } + return out; +} + +function getPriceTriggers({ commitmentText }) { + if (!commitmentText) return []; + + const tokenMap = parseTokenMap(commitmentText); + const triggers = []; + + for (const line of commitmentText.split('\n')) { + if (!/^\s*-\s*id=/.test(line)) continue; + + const fields = parseTriggerLine(line); + if (!fields.id || !fields.pair || !fields.comparator || !fields.threshold) { + throw new Error(`Malformed trigger line: ${line}`); + } + + const [baseSymbol, quoteSymbol] = fields.pair.split('/').map((value) => value.trim()); + if (!baseSymbol || !quoteSymbol) { + throw new Error(`Invalid pair in trigger line: ${line}`); + } + + const baseToken = tokenMap.get(baseSymbol); + const quoteToken = tokenMap.get(quoteSymbol); + if (!baseToken || !quoteToken) { + throw new Error( + `Token address missing for pair ${fields.pair}. Define both symbols in TOKEN_MAP.` + ); + } + + const threshold = Number(fields.threshold); + if (!Number.isFinite(threshold)) { + throw new Error(`Invalid threshold in trigger line: ${line}`); + } + + const trigger = { + id: fields.id, + label: fields.label ?? `${fields.pair} ${fields.comparator} ${fields.threshold}`, + baseToken, + quoteToken, + comparator: normalizeComparator(fields.comparator), + threshold, + priority: fields.priority !== undefined ? Number(fields.priority) : 0, + emitOnce: fields.emitOnce === undefined ? true : fields.emitOnce !== 'false', + }; + + if (fields.pool) { + if (fields.pool.toLowerCase() === 'high-liquidity') { + trigger.poolSelection = 'high-liquidity'; + } else { + trigger.pool = normalizeAddress(fields.pool); + } + } else { + trigger.poolSelection = 'high-liquidity'; + } + + triggers.push(trigger); + } + + return triggers; +} + function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled ? 'You may propose and dispute.' @@ -10,11 +111,13 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { return [ 'You are a price-race swap agent for a commitment Safe controlled by an Optimistic Governor.', 'Your own address is provided as agentAddress.', - 'Interpret the commitment as a two-branch race and execute at most one winning branch.', - 'First trigger wins. If both triggers appear true in one cycle, ETH branch wins the tie-break.', + 'Interpret the commitment as a multi-choice race and execute at most one winning branch.', + 'Price trigger specs are parsed from the commitment text itself. Do not invent trigger values.', + 'First trigger wins. If both triggers appear true in one cycle, use trigger priority and then lexical triggerId order.', 'Use all currently available USDC in the Safe for the winning branch swap.', 'Preferred flow: build_og_transactions with uniswap_v3_exact_input_single actions, then post_bond_and_propose.', - 'When pool addresses are specified in the commitment/rules, use those pools. Otherwise prefer a high-liquidity Uniswap route and enforce commitment slippage limits.', + 'When pool addresses are specified in the commitment/rules, use those pools. Otherwise use high-liquidity Uniswap routing that satisfies slippage constraints.', + 'Use the poolFee from a priceTrigger signal when preparing uniswap_v3_exact_input_single actions.', 'Never execute both branches, and never route the purchased asset to addresses other than the commitment Safe unless the commitment explicitly says so.', 'If there is insufficient evidence that a trigger fired first, or route/liquidity/slippage constraints are not safely satisfiable, return ignore.', 'Default to disputing proposals that violate these rules; prefer no-op when unsure.', @@ -26,4 +129,4 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { .join(' '); } -export { getSystemPrompt }; +export { getPriceTriggers, getSystemPrompt }; diff --git a/agent-library/agents/price-race-swap/commitment.txt b/agent-library/agents/price-race-swap/commitment.txt index 7d588c97..2cd67e71 100644 --- a/agent-library/agents/price-race-swap/commitment.txt +++ b/agent-library/agents/price-race-swap/commitment.txt @@ -1,20 +1,20 @@ -Use any available USDC balance currently held by the commitment Safe at the time of execution. +Use any available USDC balance currently held by the commitment Safe at execution time. -Monitor two trigger conditions and execute exactly one branch: +TOKEN_MAP: +- USDC = 0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 +- WETH = 0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2 +- UMA = 0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828 -1) ETH branch -- If ETH/USDC >= 3200 first, buy ETH with all available USDC. -- Execute on Uniswap. -- If a specific pool is configured, use that pool; otherwise use a high-liquidity Uniswap pool. +TRIGGERS: +- id=eth-breakout | pair=WETH/USDC | comparator=>= | threshold=3200 | pool=0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 | priority=0 +- id=uma-drop | pair=UMA/USDC | comparator=<= | threshold=2.10 | pool=high-liquidity | priority=1 -2) UMA branch -- If UMA/USDC <= 2.10 first, buy UMA with all available USDC. -- Execute on Uniswap. -- If a specific pool is configured, use that pool; otherwise use a high-liquidity Uniswap pool. +ACTIONS: +- If eth-breakout wins first, buy WETH with all available USDC via Uniswap. +- If uma-drop wins first, buy UMA with all available USDC via Uniswap. -Global rules: +GLOBAL_RULES: - First trigger wins. - Execute at most one swap total for this commitment. -- If both triggers become true in the same evaluation cycle, prefer ETH branch. - Enforce max slippage of 0.50%. - If no valid route satisfies slippage and liquidity constraints, do not trade. diff --git a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs index bd1bec43..166d259b 100644 --- a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs +++ b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs @@ -1,19 +1,30 @@ import assert from 'node:assert/strict'; -import { getSystemPrompt } from './agent.js'; +import { readFileSync } from 'node:fs'; +import { getPriceTriggers, getSystemPrompt } from './agent.js'; function run() { + const commitmentText = readFileSync(new URL('./commitment.txt', import.meta.url), 'utf8'); + const prompt = getSystemPrompt({ proposeEnabled: true, disputeEnabled: true, - commitmentText: 'Price race commitment text.', + commitmentText, }); assert.ok(prompt.includes('First trigger wins')); assert.ok(prompt.includes('Use all currently available USDC in the Safe')); - assert.ok(prompt.includes('execute at most one winning branch')); assert.ok(prompt.includes('Commitment text')); - console.log('[test] price-race-swap prompt OK'); + const triggers = getPriceTriggers({ commitmentText }); + assert.equal(triggers.length, 2); + assert.equal(triggers[0].id, 'eth-breakout'); + assert.equal(triggers[0].comparator, 'gte'); + assert.equal(triggers[0].threshold, 3200); + assert.equal(triggers[1].id, 'uma-drop'); + assert.equal(triggers[1].comparator, 'lte'); + assert.equal(triggers[1].poolSelection, 'high-liquidity'); + + console.log('[test] price-race-swap prompt and trigger parser OK'); } run(); diff --git a/agent/.env.example b/agent/.env.example index b7e732a0..ab59d747 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -33,20 +33,9 @@ PRIVATE_KEY=0x... # Optional tuning POLL_INTERVAL_MS=60000 WATCH_NATIVE_BALANCE=true -# Optional price trigger signals (JSON array) -# PRICE_TRIGGERS_JSON=[ -# { -# "id":"eth-breakout", -# "label":"ETH >= 3200", -# "pool":"0xUniswapV3PoolAddress", -# "baseToken":"0xWETH", -# "quoteToken":"0xUSDC", -# "comparator":"gte", -# "threshold":3200, -# "priority":0, -# "emitOnce":true -# } -# ] +# Optional Uniswap config overrides (otherwise chain defaults are used) +# UNISWAP_V3_FACTORY= +# UNISWAP_V3_FEE_TIERS=500,3000,10000 # PROPOSE_ENABLED=true # DISPUTE_ENABLED=true # START_BLOCK= diff --git a/agent/README.md b/agent/README.md index 152f34fd..4cc68740 100644 --- a/agent/README.md +++ b/agent/README.md @@ -25,7 +25,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` (macOS Keychain or Linux Secret Service) - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE`, `PRICE_TRIGGERS_JSON` + - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE`, `UNISWAP_V3_FACTORY`, `UNISWAP_V3_FEE_TIERS` - Optional proposals: `PROPOSE_ENABLED` (default true) - Optional disputes: `DISPUTE_ENABLED` (default true), `DISPUTE_RETRY_MS` (default 60000) - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` @@ -67,34 +67,21 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig - **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. - **Optional LLM decisions**: If `OPENAI_API_KEY` is set, the runner will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions in the agent module. - **Timelock triggers**: Parses plain language timelocks in rules (absolute dates or “X minutes after deposit”) and emits `timelock` signals when due. -- **Price triggers**: Optionally evaluates configured Uniswap V3 pools and emits `priceTrigger` signals when `PRICE_TRIGGERS_JSON` thresholds are hit. +- **Price triggers**: If your module exports `getPriceTriggers({ commitmentText })`, the runner evaluates those parsed Uniswap V3 thresholds and emits `priceTrigger` signals. All other behavior is intentionally left out. Implement your own agent in `agent-library/agents//agent.js` to add commitment-specific logic and tool use. ### Price Trigger Config -Use `PRICE_TRIGGERS_JSON` to generate deterministic threshold-hit signals: - -```json -[ - { - "id": "eth-breakout", - "label": "ETH >= 3200", - "pool": "0xUniswapV3PoolAddress", - "baseToken": "0xWETH", - "quoteToken": "0xUSDC", - "comparator": "gte", - "threshold": 3200, - "priority": 0, - "emitOnce": true - } -] -``` - -- `comparator`: `gte` or `lte` -- `threshold`: quote-token price per one base token -- `priority`: tie-break ordering when multiple triggers hit in the same cycle (lower first) -- `emitOnce`: if true, emit only the first time the threshold condition turns true +Parse trigger specs from commitment/rules text in your module by exporting `getPriceTriggers({ commitmentText })` from `agent-library/agents//agent.js`. +Each returned trigger should include: +- `id` +- `baseToken` +- `quoteToken` +- `comparator` (`gte` or `lte`) +- `threshold` +- `priority` (optional) +- `pool` or `poolSelection: "high-liquidity"` ### Uniswap Swap Action in `build_og_transactions` diff --git a/agent/scripts/test-price-signals.mjs b/agent/scripts/test-price-signals.mjs index b7bcca99..218b0e32 100644 --- a/agent/scripts/test-price-signals.mjs +++ b/agent/scripts/test-price-signals.mjs @@ -39,6 +39,9 @@ function buildMockPublicClient() { if (functionName === 'slot0') { return slot0ByPool.get(addr); } + if (functionName === 'fee') { + return 3000; + } if (functionName === 'decimals') { if (addr === WETH.toLowerCase() || addr === UMA.toLowerCase()) return 18; if (addr === USDC.toLowerCase()) return 6; @@ -50,12 +53,17 @@ function buildMockPublicClient() { async function run() { const publicClient = buildMockPublicClient(); + const config = { + uniswapV3FeeTiers: [500, 3000, 10000], + }; const triggerState = new Map(); const tokenMetaCache = new Map(); const poolMetaCache = new Map(); + const resolvedPoolCache = new Map(); const signals = await collectPriceTriggerSignals({ publicClient, + config, triggers: [ { id: 'eth-breakout', @@ -84,6 +92,7 @@ async function run() { triggerState, tokenMetaCache, poolMetaCache, + resolvedPoolCache, }); assert.equal(signals.length, 2); @@ -92,6 +101,7 @@ async function run() { const secondPass = await collectPriceTriggerSignals({ publicClient, + config, triggers: [ { id: 'eth-breakout', @@ -108,6 +118,7 @@ async function run() { triggerState, tokenMetaCache, poolMetaCache, + resolvedPoolCache, }); assert.equal(secondPass.length, 0); diff --git a/agent/src/index.js b/agent/src/index.js index 5940ab37..10c506e2 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -46,6 +46,7 @@ const timelockTriggers = new Map(); const priceTriggerState = new Map(); const tokenMetaCache = new Map(); const poolMetaCache = new Map(); +const resolvedPoolCache = new Map(); async function loadAgentModule() { const agentRef = config.agentModule ?? 'default'; @@ -114,6 +115,29 @@ function markTimelocksFired(triggers) { } } +function getActivePriceTriggers({ rulesText }) { + if (typeof agentModule?.getPriceTriggers === 'function') { + try { + const parsed = agentModule.getPriceTriggers({ + commitmentText: rulesText, + }); + if (Array.isArray(parsed)) { + return parsed; + } + console.warn('[agent] getPriceTriggers() returned non-array; ignoring.'); + return []; + } catch (error) { + console.warn( + '[agent] getPriceTriggers() failed; skipping price triggers:', + error?.message ?? error + ); + return []; + } + } + + return []; +} + async function decideOnSignals(signals) { if (!config.openAiApiKey) { return false; @@ -227,13 +251,16 @@ async function agentLoop() { const rulesText = ogContext?.rules ?? commitmentText ?? ''; updateTimelockSchedule({ rulesText }); const dueTimelocks = collectDueTimelocks(nowMs); + const activePriceTriggers = getActivePriceTriggers({ rulesText }); const duePriceSignals = await collectPriceTriggerSignals({ publicClient, - triggers: config.priceTriggers, + config, + triggers: activePriceTriggers, nowMs, triggerState: priceTriggerState, tokenMetaCache, poolMetaCache, + resolvedPoolCache, }); const combinedSignals = deposits.concat( diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index 3dd08931..d970bd72 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -1,46 +1,17 @@ import { getAddress } from 'viem'; -import { mustGetEnv, parseAddressList, parseJsonEnv } from './utils.js'; +import { mustGetEnv, parseAddressList } from './utils.js'; -function normalizePriceTrigger(trigger, index) { - if (!trigger || typeof trigger !== 'object') { - throw new Error(`PRICE_TRIGGERS_JSON[${index}] must be an object`); +function parseFeeTierList(raw) { + if (!raw) return [500, 3000, 10000]; + const values = raw + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + .map((value) => Number(value)); + if (values.some((value) => !Number.isInteger(value) || value <= 0)) { + throw new Error('UNISWAP_V3_FEE_TIERS must be comma-separated positive integers'); } - if (!trigger.pool || !trigger.baseToken || !trigger.quoteToken) { - throw new Error( - `PRICE_TRIGGERS_JSON[${index}] requires pool, baseToken, and quoteToken` - ); - } - if (!trigger.comparator || !['gte', 'lte'].includes(trigger.comparator)) { - throw new Error(`PRICE_TRIGGERS_JSON[${index}] comparator must be "gte" or "lte"`); - } - if (trigger.threshold === undefined || trigger.threshold === null) { - throw new Error(`PRICE_TRIGGERS_JSON[${index}] requires threshold`); - } - - const threshold = Number(trigger.threshold); - if (!Number.isFinite(threshold)) { - throw new Error(`PRICE_TRIGGERS_JSON[${index}] threshold must be numeric`); - } - - return { - id: trigger.id ?? `price-trigger-${index}`, - label: trigger.label, - pool: getAddress(trigger.pool), - baseToken: getAddress(trigger.baseToken), - quoteToken: getAddress(trigger.quoteToken), - comparator: trigger.comparator, - threshold, - priority: Number(trigger.priority ?? 0), - emitOnce: trigger.emitOnce === undefined ? true : Boolean(trigger.emitOnce), - }; -} - -function parsePriceTriggers(raw) { - const parsed = parseJsonEnv(raw, []); - if (!Array.isArray(parsed)) { - throw new Error('PRICE_TRIGGERS_JSON must be a JSON array'); - } - return parsed.map(normalizePriceTrigger); + return values; } function buildConfig() { @@ -80,7 +51,10 @@ function buildConfig() { : process.env.DISPUTE_ENABLED.toLowerCase() !== 'false', disputeRetryMs: Number(process.env.DISPUTE_RETRY_MS ?? 60_000), agentModule: process.env.AGENT_MODULE, - priceTriggers: parsePriceTriggers(process.env.PRICE_TRIGGERS_JSON), + uniswapV3Factory: process.env.UNISWAP_V3_FACTORY + ? getAddress(process.env.UNISWAP_V3_FACTORY) + : undefined, + uniswapV3FeeTiers: parseFeeTierList(process.env.UNISWAP_V3_FEE_TIERS), }; } diff --git a/agent/src/lib/llm.js b/agent/src/lib/llm.js index 09e44eb9..b41f5550 100644 --- a/agent/src/lib/llm.js +++ b/agent/src/lib/llm.js @@ -87,6 +87,7 @@ async function callAgent({ signal.threshold !== undefined ? String(signal.threshold) : undefined, observedPrice: signal.observedPrice !== undefined ? String(signal.observedPrice) : undefined, + poolFee: signal.poolFee !== undefined ? String(signal.poolFee) : undefined, }; }); diff --git a/agent/src/lib/price.js b/agent/src/lib/price.js index 21311a24..ec9ef3da 100644 --- a/agent/src/lib/price.js +++ b/agent/src/lib/price.js @@ -1,16 +1,39 @@ -import { erc20Abi, getAddress, parseAbi } from 'viem'; +import { erc20Abi, getAddress, parseAbi, zeroAddress } from 'viem'; const uniswapV3PoolAbi = parseAbi([ 'function token0() view returns (address)', 'function token1() view returns (address)', + 'function fee() view returns (uint24)', + 'function liquidity() view returns (uint128)', 'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)', ]); +const uniswapV3FactoryAbi = parseAbi([ + 'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)', +]); + +const defaultFactoryByChainId = new Map([ + [1, '0x1F98431c8aD98523631AE4a59f267346ea31F984'], + [11155111, '0x0227628f3f023bb0b980b67d528dd8f8c1b5bf8f'], +]); + +async function getFactoryAddress({ publicClient, configuredFactory }) { + if (configuredFactory) return configuredFactory; + const chainId = await publicClient.getChainId(); + const factory = defaultFactoryByChainId.get(chainId); + if (!factory) { + throw new Error( + `No Uniswap V3 factory configured for chainId ${chainId}. Set UNISWAP_V3_FACTORY.` + ); + } + return getAddress(factory); +} + async function loadPoolMeta({ publicClient, pool, tokenMetaCache, poolMetaCache }) { const cached = poolMetaCache.get(pool); if (cached) return cached; - const [token0, token1] = await Promise.all([ + const [token0, token1, fee] = await Promise.all([ publicClient.readContract({ address: pool, abi: uniswapV3PoolAbi, @@ -21,6 +44,11 @@ async function loadPoolMeta({ publicClient, pool, tokenMetaCache, poolMetaCache abi: uniswapV3PoolAbi, functionName: 'token1', }), + publicClient.readContract({ + address: pool, + abi: uniswapV3PoolAbi, + functionName: 'fee', + }), ]); const [normalizedToken0, normalizedToken1] = [getAddress(token0), getAddress(token1)]; @@ -43,11 +71,81 @@ async function loadPoolMeta({ publicClient, pool, tokenMetaCache, poolMetaCache const meta = { token0: normalizedToken0, token1: normalizedToken1, + fee: Number(fee), }; poolMetaCache.set(pool, meta); return meta; } +async function resolvePoolForTrigger({ + publicClient, + trigger, + config, + resolvedPoolCache, +}) { + const baseToken = getAddress(trigger.baseToken); + const quoteToken = getAddress(trigger.quoteToken); + const cacheKey = `${baseToken}:${quoteToken}:${trigger.pool ?? trigger.poolSelection ?? ''}`; + if (resolvedPoolCache.has(cacheKey)) { + return resolvedPoolCache.get(cacheKey); + } + + if (trigger.pool) { + const resolved = { pool: getAddress(trigger.pool) }; + resolvedPoolCache.set(cacheKey, resolved); + return resolved; + } + + if (trigger.poolSelection !== 'high-liquidity') { + throw new Error( + `Trigger ${trigger.id} must provide pool or poolSelection=high-liquidity` + ); + } + + const factory = await getFactoryAddress({ + publicClient, + configuredFactory: config.uniswapV3Factory, + }); + + let best = null; + for (const feeTier of config.uniswapV3FeeTiers ?? [500, 3000, 10000]) { + const pool = await publicClient.readContract({ + address: factory, + abi: uniswapV3FactoryAbi, + functionName: 'getPool', + args: [baseToken, quoteToken, Number(feeTier)], + }); + + if (!pool || getAddress(pool) === zeroAddress) { + continue; + } + + const normalizedPool = getAddress(pool); + const liquidity = await publicClient.readContract({ + address: normalizedPool, + abi: uniswapV3PoolAbi, + functionName: 'liquidity', + }); + + if (!best || BigInt(liquidity) > best.liquidity) { + best = { + pool: normalizedPool, + liquidity: BigInt(liquidity), + }; + } + } + + if (!best) { + throw new Error( + `No Uniswap V3 pool found for ${baseToken}/${quoteToken} across fee tiers.` + ); + } + + const resolved = { pool: best.pool }; + resolvedPoolCache.set(cacheKey, resolved); + return resolved; +} + function quotePerBaseFromSqrtPriceX96({ sqrtPriceX96, token0Decimals, token1Decimals, baseIsToken0 }) { const sqrt = Number(sqrtPriceX96); if (!Number.isFinite(sqrt) || sqrt <= 0) { @@ -76,11 +174,13 @@ function evaluateComparator({ comparator, price, threshold }) { async function collectPriceTriggerSignals({ publicClient, + config, triggers, nowMs, triggerState, tokenMetaCache, poolMetaCache, + resolvedPoolCache, }) { if (!Array.isArray(triggers) || triggers.length === 0) { return []; @@ -89,10 +189,24 @@ async function collectPriceTriggerSignals({ const evaluations = []; for (const trigger of triggers) { - const pool = getAddress(trigger.pool); const baseToken = getAddress(trigger.baseToken); const quoteToken = getAddress(trigger.quoteToken); + let resolved; + try { + resolved = await resolvePoolForTrigger({ + publicClient, + trigger, + config, + resolvedPoolCache, + }); + } catch (error) { + console.warn(`[agent] Price trigger ${trigger.id} skipped:`, error?.message ?? error); + continue; + } + + const pool = resolved.pool; + const poolMeta = await loadPoolMeta({ publicClient, pool, @@ -100,10 +214,8 @@ async function collectPriceTriggerSignals({ poolMetaCache, }); - const baseIsToken0 = - poolMeta.token0 === baseToken && poolMeta.token1 === quoteToken; - const baseIsToken1 = - poolMeta.token1 === baseToken && poolMeta.token0 === quoteToken; + const baseIsToken0 = poolMeta.token0 === baseToken && poolMeta.token1 === quoteToken; + const baseIsToken1 = poolMeta.token1 === baseToken && poolMeta.token0 === quoteToken; if (!baseIsToken0 && !baseIsToken1) { console.warn( @@ -139,8 +251,7 @@ async function collectPriceTriggerSignals({ lastMatched: false, }; - const shouldEmit = - matches && (!prior.lastMatched || (!trigger.emitOnce && !prior.fired)); + const shouldEmit = matches && (!prior.lastMatched || (!trigger.emitOnce && !prior.fired)); triggerState.set(trigger.id, { fired: prior.fired || (matches && trigger.emitOnce), @@ -157,6 +268,7 @@ async function collectPriceTriggerSignals({ triggerLabel: trigger.label, priority: trigger.priority ?? 0, pool, + poolFee: poolMeta.fee, baseToken, quoteToken, comparator: trigger.comparator, diff --git a/agent/src/lib/utils.js b/agent/src/lib/utils.js index b80e62aa..1d491809 100644 --- a/agent/src/lib/utils.js +++ b/agent/src/lib/utils.js @@ -49,19 +49,9 @@ function parseToolArguments(raw) { return null; } -function parseJsonEnv(raw, fallback) { - if (!raw) return fallback; - try { - return JSON.parse(raw); - } catch (error) { - throw new Error(`Invalid JSON env value: ${error?.message ?? String(error)}`); - } -} - export { mustGetEnv, normalizePrivateKey, - parseJsonEnv, parseAddressList, parseToolArguments, summarizeViemError, From 06c4141590cdaa3f9c6f79784bdfbc5a9d34f485 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 13:09:41 -0800 Subject: [PATCH 072/174] use plain language in commitment Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 132 +++++++++--------- .../agents/price-race-swap/commitment.txt | 21 +-- .../test-price-race-swap-agent.mjs | 3 +- 3 files changed, 71 insertions(+), 85 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index c8bcafa5..ef516cd8 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -6,89 +6,70 @@ function normalizeAddress(raw) { } function normalizeComparator(raw) { - if (raw === 'gte' || raw === '>=') return 'gte'; - if (raw === 'lte' || raw === '<=') return 'lte'; + const normalized = String(raw).trim().toLowerCase(); + if (normalized === 'gte' || normalized === '>=' || normalized === 'greater than or equal to') { + return 'gte'; + } + if (normalized === 'lte' || normalized === '<=' || normalized === 'less than or equal to') { + return 'lte'; + } throw new Error(`Unsupported comparator: ${raw}`); } -function parseTokenMap(commitmentText) { - const tokenMap = new Map(); - const lines = commitmentText.split('\n'); - for (const line of lines) { - const match = line.match(/^\s*-\s*([A-Z][A-Z0-9_]*)\s*=\s*(0x[a-fA-F0-9]{40})\s*$/); - if (!match) continue; - tokenMap.set(match[1], normalizeAddress(match[2])); - } - return tokenMap; -} +function extractTokenAddress({ symbol, commitmentText }) { + const patterns = [ + new RegExp(`\\b${symbol}\\b[^\\n.]{0,80}?(0x[a-fA-F0-9]{40})`, 'i'), + new RegExp(`(0x[a-fA-F0-9]{40})[^\\n.]{0,80}?\\b${symbol}\\b`, 'i'), + ]; -function parseTriggerLine(line) { - const raw = line.replace(/^\s*-\s*/, ''); - const fields = raw - .split('|') - .map((segment) => segment.trim()) - .filter(Boolean); - - const out = {}; - for (const field of fields) { - const eq = field.indexOf('='); - if (eq <= 0) continue; - const key = field.slice(0, eq).trim(); - const value = field.slice(eq + 1).trim(); - out[key] = value; + for (const pattern of patterns) { + const match = commitmentText.match(pattern); + if (match?.[1]) { + return normalizeAddress(match[1]); + } } - return out; + + throw new Error(`Missing token address for symbol ${symbol} in commitment text.`); } -function getPriceTriggers({ commitmentText }) { - if (!commitmentText) return []; +function parseTriggerStatements(commitmentText) { + const lines = commitmentText.split('\n'); + const conditionRegex = + /\bIf\s+([A-Z][A-Z0-9_]*)\s*\/\s*([A-Z][A-Z0-9_]*)\s+(?:price\s+)?(?:is\s+)?(>=|<=|greater than or equal to|less than or equal to)\s*([0-9]+(?:\.[0-9]+)?)/i; - const tokenMap = parseTokenMap(commitmentText); const triggers = []; + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const match = line.match(conditionRegex); + if (!match) continue; - for (const line of commitmentText.split('\n')) { - if (!/^\s*-\s*id=/.test(line)) continue; - - const fields = parseTriggerLine(line); - if (!fields.id || !fields.pair || !fields.comparator || !fields.threshold) { - throw new Error(`Malformed trigger line: ${line}`); - } - - const [baseSymbol, quoteSymbol] = fields.pair.split('/').map((value) => value.trim()); - if (!baseSymbol || !quoteSymbol) { - throw new Error(`Invalid pair in trigger line: ${line}`); - } - - const baseToken = tokenMap.get(baseSymbol); - const quoteToken = tokenMap.get(quoteSymbol); - if (!baseToken || !quoteToken) { - throw new Error( - `Token address missing for pair ${fields.pair}. Define both symbols in TOKEN_MAP.` - ); - } - - const threshold = Number(fields.threshold); + const baseSymbol = match[1].trim(); + const quoteSymbol = match[2].trim(); + const comparator = normalizeComparator(match[3]); + const threshold = Number(match[4]); if (!Number.isFinite(threshold)) { - throw new Error(`Invalid threshold in trigger line: ${line}`); + throw new Error(`Invalid threshold in line: ${line}`); } + const context = [line, lines[index + 1] ?? '', lines[index + 2] ?? ''].join(' '); + const poolMatch = context.match(/pool(?:\s+address)?\s*(0x[a-fA-F0-9]{40})/i); + const hasHighLiquidity = /high[-\s]liquidity/i.test(context); + const trigger = { - id: fields.id, - label: fields.label ?? `${fields.pair} ${fields.comparator} ${fields.threshold}`, - baseToken, - quoteToken, - comparator: normalizeComparator(fields.comparator), + id: `trigger-${triggers.length + 1}-${baseSymbol.toLowerCase()}-${quoteSymbol.toLowerCase()}`, + label: `${baseSymbol}/${quoteSymbol} ${comparator === 'gte' ? '>=' : '<='} ${threshold}`, + baseSymbol, + quoteSymbol, + comparator, threshold, - priority: fields.priority !== undefined ? Number(fields.priority) : 0, - emitOnce: fields.emitOnce === undefined ? true : fields.emitOnce !== 'false', + priority: triggers.length, + emitOnce: true, }; - if (fields.pool) { - if (fields.pool.toLowerCase() === 'high-liquidity') { - trigger.poolSelection = 'high-liquidity'; - } else { - trigger.pool = normalizeAddress(fields.pool); - } + if (poolMatch?.[1]) { + trigger.pool = normalizeAddress(poolMatch[1]); + } else if (hasHighLiquidity) { + trigger.poolSelection = 'high-liquidity'; } else { trigger.poolSelection = 'high-liquidity'; } @@ -99,6 +80,23 @@ function getPriceTriggers({ commitmentText }) { return triggers; } +function getPriceTriggers({ commitmentText }) { + if (!commitmentText) return []; + + const parsed = parseTriggerStatements(commitmentText); + return parsed.map((trigger) => ({ + id: trigger.id, + label: trigger.label, + baseToken: extractTokenAddress({ symbol: trigger.baseSymbol, commitmentText }), + quoteToken: extractTokenAddress({ symbol: trigger.quoteSymbol, commitmentText }), + comparator: trigger.comparator, + threshold: trigger.threshold, + priority: trigger.priority, + emitOnce: trigger.emitOnce, + ...(trigger.pool ? { pool: trigger.pool } : { poolSelection: trigger.poolSelection }), + })); +} + function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled ? 'You may propose and dispute.' @@ -112,7 +110,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'You are a price-race swap agent for a commitment Safe controlled by an Optimistic Governor.', 'Your own address is provided as agentAddress.', 'Interpret the commitment as a multi-choice race and execute at most one winning branch.', - 'Price trigger specs are parsed from the commitment text itself. Do not invent trigger values.', + 'Price trigger specs are parsed from plain-language commitment text itself. Do not invent trigger values.', 'First trigger wins. If both triggers appear true in one cycle, use trigger priority and then lexical triggerId order.', 'Use all currently available USDC in the Safe for the winning branch swap.', 'Preferred flow: build_og_transactions with uniswap_v3_exact_input_single actions, then post_bond_and_propose.', diff --git a/agent-library/agents/price-race-swap/commitment.txt b/agent-library/agents/price-race-swap/commitment.txt index 2cd67e71..94d6cdd5 100644 --- a/agent-library/agents/price-race-swap/commitment.txt +++ b/agent-library/agents/price-race-swap/commitment.txt @@ -1,20 +1,9 @@ -Use any available USDC balance currently held by the commitment Safe at execution time. +This commitment should use whatever USDC is available in the Safe at the moment of execution. -TOKEN_MAP: -- USDC = 0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 -- WETH = 0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2 -- UMA = 0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828 +Token addresses for this commitment are as follows: USDC is 0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, WETH is 0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2, and UMA is 0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828. -TRIGGERS: -- id=eth-breakout | pair=WETH/USDC | comparator=>= | threshold=3200 | pool=0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 | priority=0 -- id=uma-drop | pair=UMA/USDC | comparator=<= | threshold=2.10 | pool=high-liquidity | priority=1 +If WETH/USDC is greater than or equal to 3200 and this is the first trigger that becomes true, buy WETH using all available USDC. For this WETH branch, use Uniswap V3 pool 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8. -ACTIONS: -- If eth-breakout wins first, buy WETH with all available USDC via Uniswap. -- If uma-drop wins first, buy UMA with all available USDC via Uniswap. +If UMA/USDC is less than or equal to 2.10 and this is the first trigger that becomes true, buy UMA using all available USDC. For this UMA branch, use a high-liquidity Uniswap pool. -GLOBAL_RULES: -- First trigger wins. -- Execute at most one swap total for this commitment. -- Enforce max slippage of 0.50%. -- If no valid route satisfies slippage and liquidity constraints, do not trade. +Only one branch may execute in total. First trigger wins. Enforce max slippage of 0.50%. If no valid route meets slippage and liquidity constraints, do not trade. diff --git a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs index 166d259b..6bdde0bb 100644 --- a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs +++ b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs @@ -17,10 +17,9 @@ function run() { const triggers = getPriceTriggers({ commitmentText }); assert.equal(triggers.length, 2); - assert.equal(triggers[0].id, 'eth-breakout'); assert.equal(triggers[0].comparator, 'gte'); assert.equal(triggers[0].threshold, 3200); - assert.equal(triggers[1].id, 'uma-drop'); + assert.equal(triggers[0].pool.toLowerCase(), '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8'); assert.equal(triggers[1].comparator, 'lte'); assert.equal(triggers[1].poolSelection, 'high-liquidity'); From 821c12c875ea9c99856b4bd06b719c3a4f3c1a7f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 13:22:37 -0800 Subject: [PATCH 073/174] target UNI instead of UMA as second asset Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/commitment.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent-library/agents/price-race-swap/commitment.txt b/agent-library/agents/price-race-swap/commitment.txt index 94d6cdd5..8497995e 100644 --- a/agent-library/agents/price-race-swap/commitment.txt +++ b/agent-library/agents/price-race-swap/commitment.txt @@ -1,9 +1,9 @@ This commitment should use whatever USDC is available in the Safe at the moment of execution. -Token addresses for this commitment are as follows: USDC is 0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, WETH is 0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2, and UMA is 0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828. +Token addresses for this commitment are as follows: USDC is 0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, WETH is 0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2, and UNI is 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984. If WETH/USDC is greater than or equal to 3200 and this is the first trigger that becomes true, buy WETH using all available USDC. For this WETH branch, use Uniswap V3 pool 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8. -If UMA/USDC is less than or equal to 2.10 and this is the first trigger that becomes true, buy UMA using all available USDC. For this UMA branch, use a high-liquidity Uniswap pool. +If UNI/USDC is less than or equal to 6.00 and this is the first trigger that becomes true, buy UNI using all available USDC. For this UNI branch, use a high-liquidity Uniswap pool. Only one branch may execute in total. First trigger wins. Enforce max slippage of 0.50%. If no valid route meets slippage and liquidity constraints, do not trade. From a57ddde05d83c533d8e99b17af03b2e531bd8203 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 13:50:48 -0800 Subject: [PATCH 074/174] use uni and weth uniswap pools on sepolia Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/commitment.txt | 8 ++++---- .../agents/price-race-swap/test-price-race-swap-agent.mjs | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/agent-library/agents/price-race-swap/commitment.txt b/agent-library/agents/price-race-swap/commitment.txt index 8497995e..a3c0df13 100644 --- a/agent-library/agents/price-race-swap/commitment.txt +++ b/agent-library/agents/price-race-swap/commitment.txt @@ -1,9 +1,9 @@ -This commitment should use whatever USDC is available in the Safe at the moment of execution. +This commitment should use whatever WETH is available in the Safe at the moment of execution. -Token addresses for this commitment are as follows: USDC is 0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, WETH is 0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2, and UNI is 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984. +Token addresses for this commitment are as follows: WETH is 0xfff9976782d46cc05630d1f6ebab18b2324d6b14, USDC is 0x1c7d4b196cb0c7b01d743fbc6116a902379c7238, and UNI is 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984. -If WETH/USDC is greater than or equal to 3200 and this is the first trigger that becomes true, buy WETH using all available USDC. For this WETH branch, use Uniswap V3 pool 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8. +If WETH/USDC is greater than or equal to 1800 and this is the first trigger that becomes true, swap all available WETH for USDC. For this WETH-to-USDC branch, use Uniswap V3 pool 0x6418eec70f50913ff0d756b48d32ce7c02b47c47. -If UNI/USDC is less than or equal to 6.00 and this is the first trigger that becomes true, buy UNI using all available USDC. For this UNI branch, use a high-liquidity Uniswap pool. +If UNI/WETH is less than or equal to 0.03 and this is the first trigger that becomes true, swap all available WETH for UNI. For this WETH-to-UNI branch, use Uniswap V3 pool 0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7. Only one branch may execute in total. First trigger wins. Enforce max slippage of 0.50%. If no valid route meets slippage and liquidity constraints, do not trade. diff --git a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs index 6bdde0bb..abfdb21a 100644 --- a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs +++ b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs @@ -18,10 +18,11 @@ function run() { const triggers = getPriceTriggers({ commitmentText }); assert.equal(triggers.length, 2); assert.equal(triggers[0].comparator, 'gte'); - assert.equal(triggers[0].threshold, 3200); - assert.equal(triggers[0].pool.toLowerCase(), '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8'); + assert.equal(triggers[0].threshold, 1800); + assert.equal(triggers[0].pool.toLowerCase(), '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'); assert.equal(triggers[1].comparator, 'lte'); - assert.equal(triggers[1].poolSelection, 'high-liquidity'); + assert.equal(triggers[1].threshold, 0.03); + assert.equal(triggers[1].pool.toLowerCase(), '0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7'); console.log('[test] price-race-swap prompt and trigger parser OK'); } From 8b9853f4b80f231c7c746fe1f7dc0fc169473cf8 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 6 Feb 2026 13:52:49 -0800 Subject: [PATCH 075/174] rename price.js to uniswapV3Price.js to be specific Signed-off-by: John Shutt --- ...test-price-signals.mjs => test-uniswap-v3-price-signals.mjs} | 2 +- agent/src/index.js | 2 +- agent/src/lib/{price.js => uniswapV3Price.js} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename agent/scripts/{test-price-signals.mjs => test-uniswap-v3-price-signals.mjs} (98%) rename agent/src/lib/{price.js => uniswapV3Price.js} (100%) diff --git a/agent/scripts/test-price-signals.mjs b/agent/scripts/test-uniswap-v3-price-signals.mjs similarity index 98% rename from agent/scripts/test-price-signals.mjs rename to agent/scripts/test-uniswap-v3-price-signals.mjs index 218b0e32..ca315ae5 100644 --- a/agent/scripts/test-price-signals.mjs +++ b/agent/scripts/test-uniswap-v3-price-signals.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { collectPriceTriggerSignals } from '../src/lib/price.js'; +import { collectPriceTriggerSignals } from '../src/lib/uniswapV3Price.js'; const WETH = '0xC02aaA39b223FE8D0A0E5C4F27eAD9083C756Cc2'; const USDC = '0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; diff --git a/agent/src/index.js b/agent/src/index.js index 10c506e2..deae686b 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -20,7 +20,7 @@ import { callAgent, explainToolCalls } from './lib/llm.js'; import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; import { extractTimelockTriggers } from './lib/timelock.js'; -import { collectPriceTriggerSignals } from './lib/price.js'; +import { collectPriceTriggerSignals } from './lib/uniswapV3Price.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/agent/src/lib/price.js b/agent/src/lib/uniswapV3Price.js similarity index 100% rename from agent/src/lib/price.js rename to agent/src/lib/uniswapV3Price.js From 70bf586203070bfc7bc8f97ef66b378b1ccc60ff Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Mon, 9 Feb 2026 15:08:10 -0500 Subject: [PATCH 076/174] Move DCA policy and runtime state from shared runner into dca-agent module hooks --- agent-library/agents/dca-agent/agent.js | 112 ++++++++++++++++++++++- agent/src/index.js | 115 ++++++------------------ 2 files changed, 137 insertions(+), 90 deletions(-) diff --git a/agent-library/agents/dca-agent/agent.js b/agent-library/agents/dca-agent/agent.js index 4f9abfaa..4f226e5e 100644 --- a/agent-library/agents/dca-agent/agent.js +++ b/agent-library/agents/dca-agent/agent.js @@ -3,6 +3,22 @@ let lastDcaTimestamp = Date.now(); const DCA_INTERVAL_SECONDS = 200; const MAX_CYCLES = 2; +const DCA_POLICY = Object.freeze({ + wethAddress: '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9', + usdcAddress: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', + usdcDecimals: 6n, + minSafeUsdcWei: 100000n, // 0.10 USDC (6 decimals) + maxCycles: MAX_CYCLES, + proposalConfirmTimeoutMs: 60000, +}); +let dcaState = { + depositConfirmed: false, + proposalBuilt: false, + proposalPosted: false, + cyclesCompleted: 0, + proposalSubmitHash: null, + proposalSubmitMs: null, +}; function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled @@ -63,4 +79,98 @@ function markDcaExecuted() { lastDcaTimestamp = Date.now(); } -export { getSystemPrompt, augmentSignals, markDcaExecuted }; +function getDcaPolicy() { + return DCA_POLICY; +} + +function getDcaState() { + return { ...dcaState }; +} + +function getPendingProposal(onchainPending) { + return Boolean(onchainPending || dcaState.proposalPosted); +} + +function onToolOutput({ name, parsedOutput }) { + if (!name || !parsedOutput || parsedOutput.status === 'error') return; + + if (name === 'make_deposit' && parsedOutput.status === 'confirmed') { + dcaState.depositConfirmed = true; + dcaState.proposalBuilt = false; + dcaState.proposalPosted = false; + return; + } + + if (name === 'build_og_transactions' && parsedOutput.status === 'ok') { + dcaState.proposalBuilt = true; + return; + } + + if (name === 'post_bond_and_propose' && parsedOutput.status === 'submitted') { + dcaState.proposalPosted = true; + dcaState.depositConfirmed = false; + dcaState.proposalBuilt = false; + dcaState.proposalSubmitHash = parsedOutput.proposalHash ?? null; + dcaState.proposalSubmitMs = Date.now(); + } +} + +function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 }) { + if (executedProposalCount > 0) { + dcaState.proposalPosted = false; + dcaState.proposalBuilt = false; + dcaState.depositConfirmed = false; + dcaState.proposalSubmitHash = null; + dcaState.proposalSubmitMs = null; + dcaState.cyclesCompleted = Math.min( + DCA_POLICY.maxCycles, + dcaState.cyclesCompleted + executedProposalCount + ); + markDcaExecuted(); + } + + if (deletedProposalCount > 0) { + dcaState.proposalPosted = false; + dcaState.proposalBuilt = false; + dcaState.depositConfirmed = false; + dcaState.proposalSubmitHash = null; + dcaState.proposalSubmitMs = null; + } +} + +async function reconcileProposalSubmission({ publicClient }) { + if (!dcaState.proposalPosted || !dcaState.proposalSubmitHash || !dcaState.proposalSubmitMs) { + return; + } + + try { + const receipt = await publicClient.getTransactionReceipt({ + hash: dcaState.proposalSubmitHash, + }); + if (receipt?.status === 0n || receipt?.status === 'reverted') { + dcaState.proposalPosted = false; + dcaState.proposalBuilt = false; + dcaState.proposalSubmitHash = null; + dcaState.proposalSubmitMs = null; + } + } catch (error) { + if (Date.now() - dcaState.proposalSubmitMs > DCA_POLICY.proposalConfirmTimeoutMs) { + dcaState.proposalPosted = false; + dcaState.proposalBuilt = false; + dcaState.proposalSubmitHash = null; + dcaState.proposalSubmitMs = null; + } + } +} + +export { + getSystemPrompt, + augmentSignals, + markDcaExecuted, + getDcaPolicy, + getDcaState, + getPendingProposal, + onToolOutput, + onProposalEvents, + reconcileProposalSubmission, +}; diff --git a/agent/src/index.js b/agent/src/index.js index c936327b..ff263066 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -66,20 +66,7 @@ async function loadAgentModule() { const { agentModule, commitmentText, resolvedPath } = await loadAgentModule(); const isDcaAgent = resolvedPath.endsWith('agent-library/agents/dca-agent/agent.js'); -const DCA_WETH_ADDRESS = '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9'; -const DCA_USDC_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; -const DCA_USDC_DECIMALS = 6n; -const DCA_USDC_MIN_WEI = 100000n; // 0.10 USDC (6 decimals) -const DCA_MAX_CYCLES = 2; -const DCA_PROPOSAL_CONFIRM_TIMEOUT_MS = 60000; -const dcaState = { - depositConfirmed: false, - proposalBuilt: false, - proposalPosted: false, - cyclesCompleted: 0, - proposalSubmitHash: null, - proposalSubmitMs: null, -}; +const dcaPolicy = isDcaAgent ? agentModule?.getDcaPolicy?.() : undefined; async function getBlockTimestampMs(blockNumber) { if (!blockNumber) return undefined; @@ -177,7 +164,7 @@ async function decideOnSignals(signals) { config, ogContext, }); - if (isDcaAgent && toolOutputs.length > 0) { + if (isDcaAgent && toolOutputs.length > 0 && agentModule?.onToolOutput) { for (const output of toolOutputs) { if (!output?.name || !output?.output) continue; let parsed; @@ -186,23 +173,10 @@ async function decideOnSignals(signals) { } catch (error) { parsed = null; } - if (!parsed || parsed.status === 'error') continue; - - if (output.name === 'make_deposit' && parsed.status === 'confirmed') { - dcaState.depositConfirmed = true; - dcaState.proposalBuilt = false; - dcaState.proposalPosted = false; - } - if (output.name === 'build_og_transactions' && parsed.status === 'ok') { - dcaState.proposalBuilt = true; - } - if (output.name === 'post_bond_and_propose' && parsed.status === 'submitted') { - dcaState.proposalPosted = true; - dcaState.depositConfirmed = false; - dcaState.proposalBuilt = false; - dcaState.proposalSubmitHash = parsed.proposalHash ?? null; - dcaState.proposalSubmitMs = Date.now(); - } + agentModule.onToolOutput({ + name: output.name, + parsedOutput: parsed, + }); } } if (decision.responseId && toolOutputs.length > 0) { @@ -269,54 +243,14 @@ async function agentLoop() { lastProposalCheckedBlock = nextProposalBlock; const executedProposalCount = executedProposals?.length ?? 0; const deletedProposalCount = deletedProposals?.length ?? 0; - if (isDcaAgent && executedProposalCount > 0) { - dcaState.proposalPosted = false; - dcaState.proposalBuilt = false; - dcaState.depositConfirmed = false; - dcaState.proposalSubmitHash = null; - dcaState.proposalSubmitMs = null; - dcaState.cyclesCompleted = Math.min( - DCA_MAX_CYCLES, - dcaState.cyclesCompleted + executedProposalCount - ); - if (agentModule?.markDcaExecuted) { - agentModule.markDcaExecuted(); - } - } - if (isDcaAgent && deletedProposalCount > 0) { - dcaState.proposalPosted = false; - dcaState.proposalBuilt = false; - dcaState.depositConfirmed = false; - dcaState.proposalSubmitHash = null; - dcaState.proposalSubmitMs = null; + if (isDcaAgent && agentModule?.onProposalEvents) { + agentModule.onProposalEvents({ + executedProposalCount, + deletedProposalCount, + }); } - if ( - isDcaAgent && - dcaState.proposalPosted && - dcaState.proposalSubmitHash && - dcaState.proposalSubmitMs - ) { - try { - const receipt = await publicClient.getTransactionReceipt({ - hash: dcaState.proposalSubmitHash, - }); - if (receipt?.status === 0n || receipt?.status === 'reverted') { - dcaState.proposalPosted = false; - dcaState.proposalBuilt = false; - dcaState.proposalSubmitHash = null; - dcaState.proposalSubmitMs = null; - } - } catch (error) { - if ( - Date.now() - dcaState.proposalSubmitMs > - DCA_PROPOSAL_CONFIRM_TIMEOUT_MS - ) { - dcaState.proposalPosted = false; - dcaState.proposalBuilt = false; - dcaState.proposalSubmitHash = null; - dcaState.proposalSubmitMs = null; - } - } + if (isDcaAgent && agentModule?.reconcileProposalSubmission) { + await agentModule.reconcileProposalSubmission({ publicClient }); } const rulesText = ogContext?.rules ?? commitmentText ?? ''; @@ -378,29 +312,31 @@ async function agentLoop() { } // Deterministic balance checks for DCA agent (inject into timer signals) - if (isDcaAgent && signalsToProcess.some((signal) => signal.kind === 'timer')) { + if (isDcaAgent && dcaPolicy && signalsToProcess.some((signal) => signal.kind === 'timer')) { try { const [safeUsdcWei, selfWethWei] = await Promise.all([ publicClient.readContract({ - address: DCA_USDC_ADDRESS, + address: dcaPolicy.usdcAddress, abi: erc20Abi, functionName: 'balanceOf', args: [config.commitmentSafe], }), publicClient.readContract({ - address: DCA_WETH_ADDRESS, + address: dcaPolicy.wethAddress, abi: erc20Abi, functionName: 'balanceOf', args: [account.address], }), ]); - const safeUsdcSufficient = safeUsdcWei >= DCA_USDC_MIN_WEI; - const safeUsdcHuman = Number(safeUsdcWei) / 10 ** Number(DCA_USDC_DECIMALS); + const safeUsdcSufficient = safeUsdcWei >= dcaPolicy.minSafeUsdcWei; + const safeUsdcHuman = Number(safeUsdcWei) / 10 ** Number(dcaPolicy.usdcDecimals); const selfWethHuman = Number(selfWethWei) / 1e18; - const pendingProposal = - proposalsByHash.size > 0 || dcaState.proposalPosted === true; + const pendingProposal = agentModule?.getPendingProposal + ? agentModule.getPendingProposal(proposalsByHash.size > 0) + : proposalsByHash.size > 0; + const currentDcaState = agentModule?.getDcaState ? agentModule.getDcaState() : {}; for (const signal of signalsToProcess) { if (signal.kind === 'timer') { signal.balances = { @@ -409,11 +345,12 @@ async function agentLoop() { safeUsdcHuman, selfWethHuman, safeUsdcSufficient, - minSafeUsdcWei: DCA_USDC_MIN_WEI.toString(), + minSafeUsdcWei: dcaPolicy.minSafeUsdcWei.toString(), minSafeUsdcHuman: - Number(DCA_USDC_MIN_WEI) / 10 ** Number(DCA_USDC_DECIMALS), + Number(dcaPolicy.minSafeUsdcWei) / + 10 ** Number(dcaPolicy.usdcDecimals), }; - signal.dcaState = { ...dcaState }; + signal.dcaState = currentDcaState; signal.pendingProposal = pendingProposal; } } From 21c1b4663b09f3008820b91b1e4cb96c9569b901 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Mon, 9 Feb 2026 15:09:46 -0500 Subject: [PATCH 077/174] Move DCA signals out of shared runner into dca-agent module hook --- agent-library/agents/dca-agent/agent.js | 90 +++++++++++++++++++++++++ agent/src/index.js | 79 +++------------------- 2 files changed, 101 insertions(+), 68 deletions(-) diff --git a/agent-library/agents/dca-agent/agent.js b/agent-library/agents/dca-agent/agent.js index 4f226e5e..d3740eef 100644 --- a/agent-library/agents/dca-agent/agent.js +++ b/agent-library/agents/dca-agent/agent.js @@ -1,8 +1,14 @@ // DCA Agent - WETH reimbursement loop on Sepolia +import { erc20Abi, parseAbi } from 'viem'; + let lastDcaTimestamp = Date.now(); const DCA_INTERVAL_SECONDS = 200; const MAX_CYCLES = 2; +const CHAINLINK_ETH_USD_FEED_SEPOLIA = '0x694AA1769357215DE4FAC081bf1f309aDC325306'; +const chainlinkAbi = parseAbi([ + 'function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)', +]); const DCA_POLICY = Object.freeze({ wethAddress: '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9', usdcAddress: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', @@ -79,6 +85,89 @@ function markDcaExecuted() { lastDcaTimestamp = Date.now(); } +async function getEthPriceUSD(publicClient, chainlinkFeedAddress) { + const result = await publicClient.readContract({ + address: chainlinkFeedAddress, + abi: chainlinkAbi, + functionName: 'latestRoundData', + }); + const answer = result[1]; + if (answer <= 0n) { + throw new Error('Invalid Chainlink ETH/USD price'); + } + return Number(answer) / 1e8; +} + +async function getEthPriceUSDFallback() { + const response = await fetch( + 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd' + ); + if (!response.ok) { + throw new Error(`Coingecko API error: ${response.status}`); + } + const data = await response.json(); + if (!data?.ethereum?.usd) { + throw new Error('Invalid Coingecko response'); + } + return data.ethereum.usd; +} + +async function enrichSignals(signals, { publicClient, config, account, onchainPendingProposal }) { + if (!signals.some((signal) => signal.kind === 'timer')) { + return signals; + } + + let ethPriceUSD; + try { + const chainlinkFeedAddress = + config?.chainlinkPriceFeed ?? CHAINLINK_ETH_USD_FEED_SEPOLIA; + ethPriceUSD = await getEthPriceUSD(publicClient, chainlinkFeedAddress); + } catch (error) { + ethPriceUSD = await getEthPriceUSDFallback(); + } + + const [safeUsdcWei, selfWethWei] = await Promise.all([ + publicClient.readContract({ + address: DCA_POLICY.usdcAddress, + abi: erc20Abi, + functionName: 'balanceOf', + args: [config.commitmentSafe], + }), + publicClient.readContract({ + address: DCA_POLICY.wethAddress, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }), + ]); + + const safeUsdcSufficient = safeUsdcWei >= DCA_POLICY.minSafeUsdcWei; + const safeUsdcHuman = Number(safeUsdcWei) / 10 ** Number(DCA_POLICY.usdcDecimals); + const selfWethHuman = Number(selfWethWei) / 1e18; + const pendingProposal = getPendingProposal(onchainPendingProposal); + const currentDcaState = getDcaState(); + + return signals.map((signal) => { + if (signal.kind !== 'timer') return signal; + return { + ...signal, + ethPriceUSD, + balances: { + safeUsdcWei: safeUsdcWei.toString(), + selfWethWei: selfWethWei.toString(), + safeUsdcHuman, + selfWethHuman, + safeUsdcSufficient, + minSafeUsdcWei: DCA_POLICY.minSafeUsdcWei.toString(), + minSafeUsdcHuman: + Number(DCA_POLICY.minSafeUsdcWei) / 10 ** Number(DCA_POLICY.usdcDecimals), + }, + dcaState: currentDcaState, + pendingProposal, + }; + }); +} + function getDcaPolicy() { return DCA_POLICY; } @@ -173,4 +262,5 @@ export { onToolOutput, onProposalEvents, reconcileProposalSubmission, + enrichSignals, }; diff --git a/agent/src/index.js b/agent/src/index.js index ff263066..266cef18 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { createPublicClient, erc20Abi, http } from 'viem'; +import { createPublicClient, http } from 'viem'; import { buildConfig } from './lib/config.js'; import { createSignerClient } from './lib/signer.js'; import { @@ -20,7 +20,6 @@ import { callAgent, explainToolCalls } from './lib/llm.js'; import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; import { extractTimelockTriggers } from './lib/timelock.js'; -import { getEthPriceUSD, getEthPriceUSDFallback } from './lib/price.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -66,7 +65,6 @@ async function loadAgentModule() { const { agentModule, commitmentText, resolvedPath } = await loadAgentModule(); const isDcaAgent = resolvedPath.endsWith('agent-library/agents/dca-agent/agent.js'); -const dcaPolicy = isDcaAgent ? agentModule?.getDcaPolicy?.() : undefined; async function getBlockTimestampMs(blockNumber) { if (!blockNumber) return undefined; @@ -289,73 +287,18 @@ async function agentLoop() { latestBlock, }); } - - // Fetch ETH price and add to timer signal (if present) - if (signalsToProcess.some((signal) => signal.kind === 'timer')) { - try { - let ethPriceUSD; - try { - ethPriceUSD = await getEthPriceUSD(publicClient, config.chainlinkPriceFeed); - } catch (error) { - console.warn('[agent] Chainlink price fetch failed, using Coingecko fallback'); - ethPriceUSD = await getEthPriceUSDFallback(); - } - - for (const signal of signalsToProcess) { - if (signal.kind === 'timer') { - signal.ethPriceUSD = ethPriceUSD; - } - } - } catch (error) { - console.error('[agent] Failed to fetch ETH price:', error); - } - } - - // Deterministic balance checks for DCA agent (inject into timer signals) - if (isDcaAgent && dcaPolicy && signalsToProcess.some((signal) => signal.kind === 'timer')) { + if (agentModule?.enrichSignals) { try { - const [safeUsdcWei, selfWethWei] = await Promise.all([ - publicClient.readContract({ - address: dcaPolicy.usdcAddress, - abi: erc20Abi, - functionName: 'balanceOf', - args: [config.commitmentSafe], - }), - publicClient.readContract({ - address: dcaPolicy.wethAddress, - abi: erc20Abi, - functionName: 'balanceOf', - args: [account.address], - }), - ]); - - const safeUsdcSufficient = safeUsdcWei >= dcaPolicy.minSafeUsdcWei; - const safeUsdcHuman = Number(safeUsdcWei) / 10 ** Number(dcaPolicy.usdcDecimals); - const selfWethHuman = Number(selfWethWei) / 1e18; - - const pendingProposal = agentModule?.getPendingProposal - ? agentModule.getPendingProposal(proposalsByHash.size > 0) - : proposalsByHash.size > 0; - const currentDcaState = agentModule?.getDcaState ? agentModule.getDcaState() : {}; - for (const signal of signalsToProcess) { - if (signal.kind === 'timer') { - signal.balances = { - safeUsdcWei: safeUsdcWei.toString(), - selfWethWei: selfWethWei.toString(), - safeUsdcHuman, - selfWethHuman, - safeUsdcSufficient, - minSafeUsdcWei: dcaPolicy.minSafeUsdcWei.toString(), - minSafeUsdcHuman: - Number(dcaPolicy.minSafeUsdcWei) / - 10 ** Number(dcaPolicy.usdcDecimals), - }; - signal.dcaState = currentDcaState; - signal.pendingProposal = pendingProposal; - } - } + signalsToProcess = await agentModule.enrichSignals(signalsToProcess, { + publicClient, + config, + account, + onchainPendingProposal: proposalsByHash.size > 0, + nowMs, + latestBlock, + }); } catch (error) { - console.error('[agent] Failed to fetch DCA balances:', error); + console.error('[agent] Failed to enrich signals:', error); } } From 9dc1edd52b571e7ca183f8b5f8fe8d2f2947ea3c Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Mon, 9 Feb 2026 15:13:42 -0500 Subject: [PATCH 078/174] Remove DCA-specific branching from shared file by using generic module hooks --- agent/src/index.js | 11 +++++------ agent/src/lib/tools.js | 2 -- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 266cef18..d3a3c229 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -60,11 +60,10 @@ async function loadAgentModule() { console.warn('[agent] Missing commitment.txt next to agent module:', commitmentPath); } - return { agentModule, commitmentText, resolvedPath }; + return { agentModule, commitmentText }; } -const { agentModule, commitmentText, resolvedPath } = await loadAgentModule(); -const isDcaAgent = resolvedPath.endsWith('agent-library/agents/dca-agent/agent.js'); +const { agentModule, commitmentText } = await loadAgentModule(); async function getBlockTimestampMs(blockNumber) { if (!blockNumber) return undefined; @@ -162,7 +161,7 @@ async function decideOnSignals(signals) { config, ogContext, }); - if (isDcaAgent && toolOutputs.length > 0 && agentModule?.onToolOutput) { + if (toolOutputs.length > 0 && agentModule?.onToolOutput) { for (const output of toolOutputs) { if (!output?.name || !output?.output) continue; let parsed; @@ -241,13 +240,13 @@ async function agentLoop() { lastProposalCheckedBlock = nextProposalBlock; const executedProposalCount = executedProposals?.length ?? 0; const deletedProposalCount = deletedProposals?.length ?? 0; - if (isDcaAgent && agentModule?.onProposalEvents) { + if (agentModule?.onProposalEvents) { agentModule.onProposalEvents({ executedProposalCount, deletedProposalCount, }); } - if (isDcaAgent && agentModule?.reconcileProposalSubmission) { + if (agentModule?.reconcileProposalSubmission) { await agentModule.reconcileProposalSubmission({ publicClient }); } diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 95fe6fe7..26c7f594 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -240,8 +240,6 @@ async function executeToolCalls({ output: safeStringify({ status: 'confirmed', transactionHash: String(txHash), - next_step: - 'Do not respond to the user with an update yet. Call build_og_transactions then use the output from build_og_transactions to call post_bond_and_propose.', }), }); continue; From b3d0bf71fb9d2241665f3bbe844934a83736218a Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Mon, 9 Feb 2026 15:22:33 -0500 Subject: [PATCH 079/174] add viem to agent library dependencies --- agent-library/package-lock.json | 216 ++++++++++++++++++++++++++++++++ agent-library/package.json | 4 +- 2 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 agent-library/package-lock.json diff --git a/agent-library/package-lock.json b/agent-library/package-lock.json new file mode 100644 index 00000000..6e8a00cc --- /dev/null +++ b/agent-library/package-lock.json @@ -0,0 +1,216 @@ +{ + "name": "agent-library", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "viem": "^2.20.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/ox": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.11.3.tgz", + "integrity": "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem": { + "version": "2.45.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.45.2.tgz", + "integrity": "sha512-GXPMmj0ukqFNL87sgpsZBy4CjGvsFQk42/EUdsn8dv3ZWtL4ukDXNCM0nME2hU0IcuS29CuUbrwbZN6iWxAipw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.11.3", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/agent-library/package.json b/agent-library/package.json index 3dbc1ca5..ea631776 100644 --- a/agent-library/package.json +++ b/agent-library/package.json @@ -1,3 +1 @@ -{ - "type": "module" -} +{"type":"module","dependencies":{"viem":"^2.20.0"}} From 70a58c3b30c9fba762a11cfa11faed5e839ddf3d Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 14:14:24 -0800 Subject: [PATCH 080/174] remove regex detection of triggers and tokens and add some lightweight validation Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 109 +------------- .../test-price-race-swap-agent.mjs | 16 +-- agent/README.md | 14 +- .../scripts/test-price-trigger-inference.mjs | 80 +++++++++++ agent/src/index.js | 39 ++++- agent/src/lib/priceTriggerInference.js | 134 ++++++++++++++++++ 6 files changed, 263 insertions(+), 129 deletions(-) create mode 100644 agent/scripts/test-price-trigger-inference.mjs create mode 100644 agent/src/lib/priceTriggerInference.js diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index ef516cd8..8f4a9728 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -1,102 +1,3 @@ -function normalizeAddress(raw) { - if (typeof raw !== 'string' || !/^0x[a-fA-F0-9]{40}$/.test(raw)) { - throw new Error(`Invalid address: ${raw}`); - } - return raw; -} - -function normalizeComparator(raw) { - const normalized = String(raw).trim().toLowerCase(); - if (normalized === 'gte' || normalized === '>=' || normalized === 'greater than or equal to') { - return 'gte'; - } - if (normalized === 'lte' || normalized === '<=' || normalized === 'less than or equal to') { - return 'lte'; - } - throw new Error(`Unsupported comparator: ${raw}`); -} - -function extractTokenAddress({ symbol, commitmentText }) { - const patterns = [ - new RegExp(`\\b${symbol}\\b[^\\n.]{0,80}?(0x[a-fA-F0-9]{40})`, 'i'), - new RegExp(`(0x[a-fA-F0-9]{40})[^\\n.]{0,80}?\\b${symbol}\\b`, 'i'), - ]; - - for (const pattern of patterns) { - const match = commitmentText.match(pattern); - if (match?.[1]) { - return normalizeAddress(match[1]); - } - } - - throw new Error(`Missing token address for symbol ${symbol} in commitment text.`); -} - -function parseTriggerStatements(commitmentText) { - const lines = commitmentText.split('\n'); - const conditionRegex = - /\bIf\s+([A-Z][A-Z0-9_]*)\s*\/\s*([A-Z][A-Z0-9_]*)\s+(?:price\s+)?(?:is\s+)?(>=|<=|greater than or equal to|less than or equal to)\s*([0-9]+(?:\.[0-9]+)?)/i; - - const triggers = []; - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; - const match = line.match(conditionRegex); - if (!match) continue; - - const baseSymbol = match[1].trim(); - const quoteSymbol = match[2].trim(); - const comparator = normalizeComparator(match[3]); - const threshold = Number(match[4]); - if (!Number.isFinite(threshold)) { - throw new Error(`Invalid threshold in line: ${line}`); - } - - const context = [line, lines[index + 1] ?? '', lines[index + 2] ?? ''].join(' '); - const poolMatch = context.match(/pool(?:\s+address)?\s*(0x[a-fA-F0-9]{40})/i); - const hasHighLiquidity = /high[-\s]liquidity/i.test(context); - - const trigger = { - id: `trigger-${triggers.length + 1}-${baseSymbol.toLowerCase()}-${quoteSymbol.toLowerCase()}`, - label: `${baseSymbol}/${quoteSymbol} ${comparator === 'gte' ? '>=' : '<='} ${threshold}`, - baseSymbol, - quoteSymbol, - comparator, - threshold, - priority: triggers.length, - emitOnce: true, - }; - - if (poolMatch?.[1]) { - trigger.pool = normalizeAddress(poolMatch[1]); - } else if (hasHighLiquidity) { - trigger.poolSelection = 'high-liquidity'; - } else { - trigger.poolSelection = 'high-liquidity'; - } - - triggers.push(trigger); - } - - return triggers; -} - -function getPriceTriggers({ commitmentText }) { - if (!commitmentText) return []; - - const parsed = parseTriggerStatements(commitmentText); - return parsed.map((trigger) => ({ - id: trigger.id, - label: trigger.label, - baseToken: extractTokenAddress({ symbol: trigger.baseSymbol, commitmentText }), - quoteToken: extractTokenAddress({ symbol: trigger.quoteSymbol, commitmentText }), - comparator: trigger.comparator, - threshold: trigger.threshold, - priority: trigger.priority, - emitOnce: trigger.emitOnce, - ...(trigger.pool ? { pool: trigger.pool } : { poolSelection: trigger.poolSelection }), - })); -} - function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled ? 'You may propose and dispute.' @@ -110,13 +11,13 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'You are a price-race swap agent for a commitment Safe controlled by an Optimistic Governor.', 'Your own address is provided as agentAddress.', 'Interpret the commitment as a multi-choice race and execute at most one winning branch.', - 'Price trigger specs are parsed from plain-language commitment text itself. Do not invent trigger values.', - 'First trigger wins. If both triggers appear true in one cycle, use trigger priority and then lexical triggerId order.', - 'Use all currently available USDC in the Safe for the winning branch swap.', + 'Use your reasoning over the plain-language commitment and incoming signals. Do not depend on rigid text pattern matching.', + 'First trigger wins. If multiple triggers appear true in one cycle, use signal priority and then lexical triggerId order.', + 'Use all currently available WETH in the Safe for the winning branch swap.', 'Preferred flow: build_og_transactions with uniswap_v3_exact_input_single actions, then post_bond_and_propose.', 'When pool addresses are specified in the commitment/rules, use those pools. Otherwise use high-liquidity Uniswap routing that satisfies slippage constraints.', 'Use the poolFee from a priceTrigger signal when preparing uniswap_v3_exact_input_single actions.', - 'Never execute both branches, and never route the purchased asset to addresses other than the commitment Safe unless the commitment explicitly says so.', + 'Never execute both branches, and never route purchased assets to addresses other than the commitment Safe unless explicitly required by the commitment.', 'If there is insufficient evidence that a trigger fired first, or route/liquidity/slippage constraints are not safely satisfiable, return ignore.', 'Default to disputing proposals that violate these rules; prefer no-op when unsure.', mode, @@ -127,4 +28,4 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { .join(' '); } -export { getPriceTriggers, getSystemPrompt }; +export { getSystemPrompt }; diff --git a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs index abfdb21a..57e9bbd2 100644 --- a/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs +++ b/agent-library/agents/price-race-swap/test-price-race-swap-agent.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; -import { getPriceTriggers, getSystemPrompt } from './agent.js'; +import { getSystemPrompt } from './agent.js'; function run() { const commitmentText = readFileSync(new URL('./commitment.txt', import.meta.url), 'utf8'); @@ -12,19 +12,11 @@ function run() { }); assert.ok(prompt.includes('First trigger wins')); - assert.ok(prompt.includes('Use all currently available USDC in the Safe')); + assert.ok(prompt.includes('Use all currently available WETH in the Safe')); + assert.ok(prompt.includes('Do not depend on rigid text pattern matching')); assert.ok(prompt.includes('Commitment text')); - const triggers = getPriceTriggers({ commitmentText }); - assert.equal(triggers.length, 2); - assert.equal(triggers[0].comparator, 'gte'); - assert.equal(triggers[0].threshold, 1800); - assert.equal(triggers[0].pool.toLowerCase(), '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'); - assert.equal(triggers[1].comparator, 'lte'); - assert.equal(triggers[1].threshold, 0.03); - assert.equal(triggers[1].pool.toLowerCase(), '0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7'); - - console.log('[test] price-race-swap prompt and trigger parser OK'); + console.log('[test] price-race-swap prompt OK'); } run(); diff --git a/agent/README.md b/agent/README.md index 4cc68740..36c11891 100644 --- a/agent/README.md +++ b/agent/README.md @@ -67,21 +67,15 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig - **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. - **Optional LLM decisions**: If `OPENAI_API_KEY` is set, the runner will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions in the agent module. - **Timelock triggers**: Parses plain language timelocks in rules (absolute dates or “X minutes after deposit”) and emits `timelock` signals when due. -- **Price triggers**: If your module exports `getPriceTriggers({ commitmentText })`, the runner evaluates those parsed Uniswap V3 thresholds and emits `priceTrigger` signals. +- **Price triggers**: The runner can infer Uniswap V3 price triggers directly from plain-language commitment text using the LLM. If a module exports `getPriceTriggers({ commitmentText })`, that explicit parser takes precedence. All other behavior is intentionally left out. Implement your own agent in `agent-library/agents//agent.js` to add commitment-specific logic and tool use. ### Price Trigger Config -Parse trigger specs from commitment/rules text in your module by exporting `getPriceTriggers({ commitmentText })` from `agent-library/agents//agent.js`. -Each returned trigger should include: -- `id` -- `baseToken` -- `quoteToken` -- `comparator` (`gte` or `lte`) -- `threshold` -- `priority` (optional) -- `pool` or `poolSelection: "high-liquidity"` +Default behavior: infer trigger specs from commitment/rules text via LLM reasoning (requires `OPENAI_API_KEY`). + +Optional behavior: export `getPriceTriggers({ commitmentText })` from `agent-library/agents//agent.js` if you want deterministic module-owned parsing. ### Uniswap Swap Action in `build_og_transactions` diff --git a/agent/scripts/test-price-trigger-inference.mjs b/agent/scripts/test-price-trigger-inference.mjs new file mode 100644 index 00000000..c743212f --- /dev/null +++ b/agent/scripts/test-price-trigger-inference.mjs @@ -0,0 +1,80 @@ +import assert from 'node:assert/strict'; +import { sanitizeInferredTriggers } from '../src/lib/priceTriggerInference.js'; + +const WETH = '0xfff9976782d46cc05630d1f6ebab18b2324d6b14'; +const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; +const UNI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'; +const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; + +function run() { + const normalized = sanitizeInferredTriggers([ + { + id: 'b', + baseToken: UNI, + quoteToken: WETH, + comparator: 'lte', + threshold: 0.03, + priority: 1, + }, + { + id: 'a', + baseToken: WETH, + quoteToken: USDC, + comparator: 'gte', + threshold: 1800, + priority: 0, + pool: POOL, + }, + ]); + + assert.equal(normalized.length, 2); + assert.equal(normalized[0].id, 'a'); + assert.equal(normalized[1].id, 'b'); + + assert.throws(() => + sanitizeInferredTriggers([ + { + id: 'dup', + baseToken: WETH, + quoteToken: USDC, + comparator: 'gte', + threshold: 1, + }, + { + id: 'dup', + baseToken: UNI, + quoteToken: WETH, + comparator: 'lte', + threshold: 1, + }, + ]) + ); + + assert.throws(() => + sanitizeInferredTriggers([ + { + id: 'bad-threshold', + baseToken: WETH, + quoteToken: USDC, + comparator: 'gte', + threshold: 0, + }, + ]) + ); + + assert.throws(() => + sanitizeInferredTriggers([ + { + id: 'same-pair', + baseToken: WETH, + quoteToken: WETH, + comparator: 'gte', + threshold: 1, + }, + ]) + ); + + console.log('[test] inferred trigger sanitizer OK'); +} + +run(); diff --git a/agent/src/index.js b/agent/src/index.js index b18df18e..1fcb1732 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -21,6 +21,7 @@ import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; import { extractTimelockTriggers } from './lib/timelock.js'; import { collectPriceTriggerSignals } from './lib/uniswapV3Price.js'; +import { inferPriceTriggersFromCommitment } from './lib/priceTriggerInference.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -47,6 +48,8 @@ const priceTriggerState = new Map(); const tokenMetaCache = new Map(); const poolMetaCache = new Map(); const resolvedPoolCache = new Map(); +const inferredPriceTriggersCache = new Map(); +let warnedPriceInferenceDisabled = false; async function loadAgentModule() { const agentRef = config.agentModule ?? 'default'; @@ -115,10 +118,10 @@ function markTimelocksFired(triggers) { } } -function getActivePriceTriggers({ rulesText }) { +async function getActivePriceTriggers({ rulesText }) { if (typeof agentModule?.getPriceTriggers === 'function') { try { - const parsed = agentModule.getPriceTriggers({ + const parsed = await agentModule.getPriceTriggers({ commitmentText: rulesText, }); if (Array.isArray(parsed)) { @@ -135,6 +138,36 @@ function getActivePriceTriggers({ rulesText }) { } } + if (!rulesText) return []; + + if (inferredPriceTriggersCache.has(rulesText)) { + return inferredPriceTriggersCache.get(rulesText); + } + + if (!config.openAiApiKey) { + if (!warnedPriceInferenceDisabled) { + console.warn( + '[agent] OPENAI_API_KEY is not set; cannot infer price triggers from commitment text.' + ); + warnedPriceInferenceDisabled = true; + } + return []; + } + + try { + const inferred = await inferPriceTriggersFromCommitment({ + config, + commitmentText: rulesText, + }); + inferredPriceTriggersCache.set(rulesText, inferred); + return inferred; + } catch (error) { + console.warn( + '[agent] Failed to infer price triggers from commitment text:', + error?.message ?? error + ); + } + return []; } @@ -281,7 +314,7 @@ async function agentLoop() { const rulesText = ogContext?.rules ?? commitmentText ?? ''; updateTimelockSchedule({ rulesText }); const dueTimelocks = collectDueTimelocks(nowMs); - const activePriceTriggers = getActivePriceTriggers({ rulesText }); + const activePriceTriggers = await getActivePriceTriggers({ rulesText }); const duePriceSignals = await collectPriceTriggerSignals({ publicClient, config, diff --git a/agent/src/lib/priceTriggerInference.js b/agent/src/lib/priceTriggerInference.js new file mode 100644 index 00000000..dee4428c --- /dev/null +++ b/agent/src/lib/priceTriggerInference.js @@ -0,0 +1,134 @@ +import { getAddress, zeroAddress } from 'viem'; + +function extractFirstText(responseJson) { + const outputs = responseJson?.output; + if (!Array.isArray(outputs)) return ''; + + for (const item of outputs) { + if (!item?.content) continue; + for (const chunk of item.content) { + if (chunk?.text) return chunk.text; + if (chunk?.output_text) return chunk.output_text?.text ?? ''; + if (chunk?.text?.value) return chunk.text.value; + } + } + + return ''; +} + +function normalizeComparator(raw) { + const value = String(raw ?? '').trim().toLowerCase(); + if (value === 'gte' || value === '>=') return 'gte'; + if (value === 'lte' || value === '<=') return 'lte'; + throw new Error(`Unsupported comparator in inferred trigger: ${raw}`); +} + +function normalizeInferredTrigger(trigger, index) { + if (!trigger || typeof trigger !== 'object') { + throw new Error(`Inferred trigger at index ${index} is not an object.`); + } + + const threshold = Number(trigger.threshold); + if (!Number.isFinite(threshold) || threshold <= 0) { + throw new Error(`Inferred trigger ${index} has invalid threshold.`); + } + + const normalized = { + id: trigger.id ? String(trigger.id) : `inferred-trigger-${index + 1}`, + label: trigger.label ? String(trigger.label) : undefined, + baseToken: getAddress(String(trigger.baseToken)), + quoteToken: getAddress(String(trigger.quoteToken)), + comparator: normalizeComparator(trigger.comparator), + threshold, + priority: Number.isFinite(Number(trigger.priority)) ? Number(trigger.priority) : index, + emitOnce: trigger.emitOnce === undefined ? true : Boolean(trigger.emitOnce), + }; + if (!Number.isInteger(normalized.priority) || normalized.priority < 0) { + throw new Error(`Inferred trigger ${index} has invalid priority.`); + } + if (normalized.baseToken === normalized.quoteToken) { + throw new Error(`Inferred trigger ${index} has identical base and quote token.`); + } + + if (trigger.pool) { + normalized.pool = getAddress(String(trigger.pool)); + if (normalized.pool === zeroAddress) { + throw new Error(`Inferred trigger ${index} has zero-address pool.`); + } + } else { + normalized.poolSelection = 'high-liquidity'; + } + + return normalized; +} + +function sanitizeInferredTriggers(triggers) { + const normalized = triggers.map(normalizeInferredTrigger); + const seenIds = new Set(); + for (const trigger of normalized) { + if (seenIds.has(trigger.id)) { + throw new Error(`Duplicate inferred trigger id: ${trigger.id}`); + } + seenIds.add(trigger.id); + } + + normalized.sort((a, b) => { + const p = a.priority - b.priority; + if (p !== 0) return p; + return a.id.localeCompare(b.id); + }); + + return normalized; +} + +async function inferPriceTriggersFromCommitment({ config, commitmentText }) { + if (!config?.openAiApiKey || !commitmentText) { + return []; + } + + const payload = { + model: config.openAiModel, + input: [ + { + role: 'system', + content: + 'Extract price-trigger specifications from a plain-language commitment. Return strict JSON with shape {"triggers":[...]}. Each trigger must include: id, label, baseToken, quoteToken, comparator (gte|lte), threshold (number), priority (number), and optional pool (address). If pool is omitted, high-liquidity routing will be used. Use only information present in the commitment text and do not invent token addresses.', + }, + { + role: 'user', + content: commitmentText, + }, + ], + text: { format: { type: 'json_object' } }, + }; + + const res = await fetch(`${config.openAiBaseUrl}/responses`, { + method: 'POST', + headers: { + Authorization: `Bearer ${config.openAiApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`OpenAI API error while inferring triggers: ${res.status} ${text}`); + } + + const json = await res.json(); + const raw = extractFirstText(json); + if (!raw) return []; + + let parsed; + try { + parsed = JSON.parse(raw); + } catch (error) { + throw new Error(`Failed to parse inferred trigger JSON: ${raw}`); + } + + const triggers = Array.isArray(parsed?.triggers) ? parsed.triggers : []; + return sanitizeInferredTriggers(triggers); +} + +export { inferPriceTriggersFromCommitment, sanitizeInferredTriggers }; From 740c757e675ccc26e0f8b7de6a88526705e3d52c Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 14:38:15 -0800 Subject: [PATCH 081/174] remove commitment-specific functionality from shared files Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 173 +++++++++++++++++- .../test-trigger-inference.mjs | 26 +-- agent/README.md | 6 +- agent/src/index.js | 34 +--- agent/src/lib/priceTriggerInference.js | 134 -------------- 5 files changed, 182 insertions(+), 191 deletions(-) rename agent/scripts/test-price-trigger-inference.mjs => agent-library/agents/price-race-swap/test-trigger-inference.mjs (71%) delete mode 100644 agent/src/lib/priceTriggerInference.js diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 8f4a9728..d67587d7 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -1,3 +1,174 @@ +const inferredTriggersCache = new Map(); + +function isHexChar(char) { + const code = char.charCodeAt(0); + return ( + (code >= 48 && code <= 57) || + (code >= 65 && code <= 70) || + (code >= 97 && code <= 102) + ); +} + +function normalizeAddress(value) { + if (typeof value !== 'string') { + throw new Error(`Invalid address: ${value}`); + } + if (value.length !== 42 || !value.startsWith('0x')) { + throw new Error(`Invalid address: ${value}`); + } + for (let i = 2; i < value.length; i += 1) { + if (!isHexChar(value[i])) { + throw new Error(`Invalid address: ${value}`); + } + } + return value; +} + +function normalizeComparator(value) { + const normalized = String(value ?? '').trim().toLowerCase(); + if (normalized === 'gte' || normalized === '>=') return 'gte'; + if (normalized === 'lte' || normalized === '<=') return 'lte'; + throw new Error(`Unsupported comparator: ${value}`); +} + +function extractFirstText(responseJson) { + const outputs = responseJson?.output; + if (!Array.isArray(outputs)) return ''; + + for (const item of outputs) { + if (!item?.content) continue; + for (const chunk of item.content) { + if (chunk?.text) return chunk.text; + if (chunk?.output_text) return chunk.output_text?.text ?? ''; + if (chunk?.text?.value) return chunk.text.value; + } + } + + return ''; +} + +function sanitizeInferredTriggers(rawTriggers) { + if (!Array.isArray(rawTriggers)) { + return []; + } + + const normalized = rawTriggers.map((trigger, index) => { + if (!trigger || typeof trigger !== 'object') { + throw new Error(`Inferred trigger at index ${index} is not an object.`); + } + + const baseToken = normalizeAddress(String(trigger.baseToken)); + const quoteToken = normalizeAddress(String(trigger.quoteToken)); + if (baseToken.toLowerCase() === quoteToken.toLowerCase()) { + throw new Error(`Inferred trigger ${index} uses the same base and quote token.`); + } + + const threshold = Number(trigger.threshold); + if (!Number.isFinite(threshold) || threshold <= 0) { + throw new Error(`Inferred trigger ${index} has invalid threshold.`); + } + + const priorityRaw = trigger.priority ?? index; + const priority = Number(priorityRaw); + if (!Number.isInteger(priority) || priority < 0) { + throw new Error(`Inferred trigger ${index} has invalid priority.`); + } + + const out = { + id: trigger.id ? String(trigger.id) : `inferred-trigger-${index + 1}`, + label: trigger.label ? String(trigger.label) : undefined, + baseToken, + quoteToken, + comparator: normalizeComparator(trigger.comparator), + threshold, + priority, + emitOnce: trigger.emitOnce === undefined ? true : Boolean(trigger.emitOnce), + }; + + if (trigger.pool) { + out.pool = normalizeAddress(String(trigger.pool)); + } else { + out.poolSelection = 'high-liquidity'; + } + + return out; + }); + + const seenIds = new Set(); + for (const trigger of normalized) { + if (seenIds.has(trigger.id)) { + throw new Error(`Duplicate inferred trigger id: ${trigger.id}`); + } + seenIds.add(trigger.id); + } + + normalized.sort((a, b) => { + const priorityCmp = a.priority - b.priority; + if (priorityCmp !== 0) return priorityCmp; + return a.id.localeCompare(b.id); + }); + + return normalized; +} + +async function getPriceTriggers({ commitmentText, config }) { + if (!commitmentText || !config?.openAiApiKey) { + return []; + } + + if (inferredTriggersCache.has(commitmentText)) { + return inferredTriggersCache.get(commitmentText); + } + + const payload = { + model: config.openAiModel, + input: [ + { + role: 'system', + content: + 'Extract Uniswap V3 price race triggers from this plain-language commitment. Return strict JSON with shape {"triggers":[...]}. Each trigger must include: id, label, baseToken, quoteToken, comparator (gte|lte), threshold (number), priority (number), and optional pool (address). If no pool is explicit, omit pool and it will use high-liquidity selection. Use only addresses and conditions present in the commitment text.', + }, + { + role: 'user', + content: commitmentText, + }, + ], + text: { format: { type: 'json_object' } }, + }; + + const res = await fetch(`${config.openAiBaseUrl}/responses`, { + method: 'POST', + headers: { + Authorization: `Bearer ${config.openAiApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`OpenAI API error while inferring triggers: ${res.status} ${text}`); + } + + const json = await res.json(); + const raw = extractFirstText(json); + if (!raw) { + inferredTriggersCache.set(commitmentText, []); + return []; + } + + let parsed; + try { + parsed = JSON.parse(raw); + } catch (error) { + throw new Error(`Failed to parse inferred trigger JSON: ${raw}`); + } + + const triggers = sanitizeInferredTriggers(parsed?.triggers ?? []); + inferredTriggersCache.set(commitmentText, triggers); + return triggers; +} + function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled ? 'You may propose and dispute.' @@ -28,4 +199,4 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { .join(' '); } -export { getSystemPrompt }; +export { getPriceTriggers, getSystemPrompt, sanitizeInferredTriggers }; diff --git a/agent/scripts/test-price-trigger-inference.mjs b/agent-library/agents/price-race-swap/test-trigger-inference.mjs similarity index 71% rename from agent/scripts/test-price-trigger-inference.mjs rename to agent-library/agents/price-race-swap/test-trigger-inference.mjs index c743212f..94e7e71e 100644 --- a/agent/scripts/test-price-trigger-inference.mjs +++ b/agent-library/agents/price-race-swap/test-trigger-inference.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { sanitizeInferredTriggers } from '../src/lib/priceTriggerInference.js'; +import { sanitizeInferredTriggers } from './agent.js'; const WETH = '0xfff9976782d46cc05630d1f6ebab18b2324d6b14'; const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; @@ -9,7 +9,7 @@ const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; function run() { const normalized = sanitizeInferredTriggers([ { - id: 'b', + id: 'second', baseToken: UNI, quoteToken: WETH, comparator: 'lte', @@ -17,7 +17,7 @@ function run() { priority: 1, }, { - id: 'a', + id: 'first', baseToken: WETH, quoteToken: USDC, comparator: 'gte', @@ -28,8 +28,8 @@ function run() { ]); assert.equal(normalized.length, 2); - assert.equal(normalized[0].id, 'a'); - assert.equal(normalized[1].id, 'b'); + assert.equal(normalized[0].id, 'first'); + assert.equal(normalized[1].id, 'second'); assert.throws(() => sanitizeInferredTriggers([ @@ -53,19 +53,7 @@ function run() { assert.throws(() => sanitizeInferredTriggers([ { - id: 'bad-threshold', - baseToken: WETH, - quoteToken: USDC, - comparator: 'gte', - threshold: 0, - }, - ]) - ); - - assert.throws(() => - sanitizeInferredTriggers([ - { - id: 'same-pair', + id: 'bad', baseToken: WETH, quoteToken: WETH, comparator: 'gte', @@ -74,7 +62,7 @@ function run() { ]) ); - console.log('[test] inferred trigger sanitizer OK'); + console.log('[test] local inferred trigger sanitizer OK'); } run(); diff --git a/agent/README.md b/agent/README.md index 36c11891..af4b26f3 100644 --- a/agent/README.md +++ b/agent/README.md @@ -67,15 +67,13 @@ For interactions, swap the env var (e.g., `PROPOSER_PK`, `EXECUTOR_PK`). For sig - **Deposits**: `makeDeposit` can send ERC20 or native assets into the commitment. - **Optional LLM decisions**: If `OPENAI_API_KEY` is set, the runner will call the OpenAI Responses API with signals and OG context and expect strict-JSON actions (propose/deposit/ignore). Wire your own validation/broadcast of any suggested actions in the agent module. - **Timelock triggers**: Parses plain language timelocks in rules (absolute dates or “X minutes after deposit”) and emits `timelock` signals when due. -- **Price triggers**: The runner can infer Uniswap V3 price triggers directly from plain-language commitment text using the LLM. If a module exports `getPriceTriggers({ commitmentText })`, that explicit parser takes precedence. +- **Price triggers**: If a module exports `getPriceTriggers({ commitmentText, config })`, the runner evaluates those parsed/inferred Uniswap V3 thresholds and emits `priceTrigger` signals. All other behavior is intentionally left out. Implement your own agent in `agent-library/agents//agent.js` to add commitment-specific logic and tool use. ### Price Trigger Config -Default behavior: infer trigger specs from commitment/rules text via LLM reasoning (requires `OPENAI_API_KEY`). - -Optional behavior: export `getPriceTriggers({ commitmentText })` from `agent-library/agents//agent.js` if you want deterministic module-owned parsing. +Export `getPriceTriggers({ commitmentText, config })` from `agent-library/agents//agent.js` when your agent needs price-trigger behavior. This keeps commitment interpretation local to the module. ### Uniswap Swap Action in `build_og_transactions` diff --git a/agent/src/index.js b/agent/src/index.js index 1fcb1732..bf7381a3 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -21,7 +21,6 @@ import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; import { extractTimelockTriggers } from './lib/timelock.js'; import { collectPriceTriggerSignals } from './lib/uniswapV3Price.js'; -import { inferPriceTriggersFromCommitment } from './lib/priceTriggerInference.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -48,8 +47,6 @@ const priceTriggerState = new Map(); const tokenMetaCache = new Map(); const poolMetaCache = new Map(); const resolvedPoolCache = new Map(); -const inferredPriceTriggersCache = new Map(); -let warnedPriceInferenceDisabled = false; async function loadAgentModule() { const agentRef = config.agentModule ?? 'default'; @@ -123,6 +120,7 @@ async function getActivePriceTriggers({ rulesText }) { try { const parsed = await agentModule.getPriceTriggers({ commitmentText: rulesText, + config, }); if (Array.isArray(parsed)) { return parsed; @@ -138,36 +136,6 @@ async function getActivePriceTriggers({ rulesText }) { } } - if (!rulesText) return []; - - if (inferredPriceTriggersCache.has(rulesText)) { - return inferredPriceTriggersCache.get(rulesText); - } - - if (!config.openAiApiKey) { - if (!warnedPriceInferenceDisabled) { - console.warn( - '[agent] OPENAI_API_KEY is not set; cannot infer price triggers from commitment text.' - ); - warnedPriceInferenceDisabled = true; - } - return []; - } - - try { - const inferred = await inferPriceTriggersFromCommitment({ - config, - commitmentText: rulesText, - }); - inferredPriceTriggersCache.set(rulesText, inferred); - return inferred; - } catch (error) { - console.warn( - '[agent] Failed to infer price triggers from commitment text:', - error?.message ?? error - ); - } - return []; } diff --git a/agent/src/lib/priceTriggerInference.js b/agent/src/lib/priceTriggerInference.js deleted file mode 100644 index dee4428c..00000000 --- a/agent/src/lib/priceTriggerInference.js +++ /dev/null @@ -1,134 +0,0 @@ -import { getAddress, zeroAddress } from 'viem'; - -function extractFirstText(responseJson) { - const outputs = responseJson?.output; - if (!Array.isArray(outputs)) return ''; - - for (const item of outputs) { - if (!item?.content) continue; - for (const chunk of item.content) { - if (chunk?.text) return chunk.text; - if (chunk?.output_text) return chunk.output_text?.text ?? ''; - if (chunk?.text?.value) return chunk.text.value; - } - } - - return ''; -} - -function normalizeComparator(raw) { - const value = String(raw ?? '').trim().toLowerCase(); - if (value === 'gte' || value === '>=') return 'gte'; - if (value === 'lte' || value === '<=') return 'lte'; - throw new Error(`Unsupported comparator in inferred trigger: ${raw}`); -} - -function normalizeInferredTrigger(trigger, index) { - if (!trigger || typeof trigger !== 'object') { - throw new Error(`Inferred trigger at index ${index} is not an object.`); - } - - const threshold = Number(trigger.threshold); - if (!Number.isFinite(threshold) || threshold <= 0) { - throw new Error(`Inferred trigger ${index} has invalid threshold.`); - } - - const normalized = { - id: trigger.id ? String(trigger.id) : `inferred-trigger-${index + 1}`, - label: trigger.label ? String(trigger.label) : undefined, - baseToken: getAddress(String(trigger.baseToken)), - quoteToken: getAddress(String(trigger.quoteToken)), - comparator: normalizeComparator(trigger.comparator), - threshold, - priority: Number.isFinite(Number(trigger.priority)) ? Number(trigger.priority) : index, - emitOnce: trigger.emitOnce === undefined ? true : Boolean(trigger.emitOnce), - }; - if (!Number.isInteger(normalized.priority) || normalized.priority < 0) { - throw new Error(`Inferred trigger ${index} has invalid priority.`); - } - if (normalized.baseToken === normalized.quoteToken) { - throw new Error(`Inferred trigger ${index} has identical base and quote token.`); - } - - if (trigger.pool) { - normalized.pool = getAddress(String(trigger.pool)); - if (normalized.pool === zeroAddress) { - throw new Error(`Inferred trigger ${index} has zero-address pool.`); - } - } else { - normalized.poolSelection = 'high-liquidity'; - } - - return normalized; -} - -function sanitizeInferredTriggers(triggers) { - const normalized = triggers.map(normalizeInferredTrigger); - const seenIds = new Set(); - for (const trigger of normalized) { - if (seenIds.has(trigger.id)) { - throw new Error(`Duplicate inferred trigger id: ${trigger.id}`); - } - seenIds.add(trigger.id); - } - - normalized.sort((a, b) => { - const p = a.priority - b.priority; - if (p !== 0) return p; - return a.id.localeCompare(b.id); - }); - - return normalized; -} - -async function inferPriceTriggersFromCommitment({ config, commitmentText }) { - if (!config?.openAiApiKey || !commitmentText) { - return []; - } - - const payload = { - model: config.openAiModel, - input: [ - { - role: 'system', - content: - 'Extract price-trigger specifications from a plain-language commitment. Return strict JSON with shape {"triggers":[...]}. Each trigger must include: id, label, baseToken, quoteToken, comparator (gte|lte), threshold (number), priority (number), and optional pool (address). If pool is omitted, high-liquidity routing will be used. Use only information present in the commitment text and do not invent token addresses.', - }, - { - role: 'user', - content: commitmentText, - }, - ], - text: { format: { type: 'json_object' } }, - }; - - const res = await fetch(`${config.openAiBaseUrl}/responses`, { - method: 'POST', - headers: { - Authorization: `Bearer ${config.openAiApiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`OpenAI API error while inferring triggers: ${res.status} ${text}`); - } - - const json = await res.json(); - const raw = extractFirstText(json); - if (!raw) return []; - - let parsed; - try { - parsed = JSON.parse(raw); - } catch (error) { - throw new Error(`Failed to parse inferred trigger JSON: ${raw}`); - } - - const triggers = Array.isArray(parsed?.triggers) ? parsed.triggers : []; - return sanitizeInferredTriggers(triggers); -} - -export { inferPriceTriggersFromCommitment, sanitizeInferredTriggers }; From 37796d79c5128d571269a7f86d3c046e1f8bdb32 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 14:45:08 -0800 Subject: [PATCH 082/174] improve tooling, single-fire persistence, local agent code isolation, shared runners, and simulation Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 228 +++++++++++++++++- .../simulate-price-race-swap.mjs | 34 +-- .../agents/price-race-swap/test-allowlist.mjs | 76 ++++++ .../test-single-fire-state.mjs | 35 +++ .../test-trigger-inference.mjs | 1 + agent/src/index.js | 47 +++- agent/src/lib/tools.js | 11 +- 7 files changed, 402 insertions(+), 30 deletions(-) create mode 100644 agent-library/agents/price-race-swap/test-allowlist.mjs create mode 100644 agent-library/agents/price-race-swap/test-single-fire-state.mjs diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index d67587d7..2316c618 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -1,3 +1,27 @@ +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const TOKENS = Object.freeze({ + WETH: '0xfff9976782d46cc05630d1f6ebab18b2324d6b14', + USDC: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238', + UNI: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', +}); + +const ALLOWED_POOLS = new Set([ + '0x6418eec70f50913ff0d756b48d32ce7c02b47c47', + '0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7', +]); + +const ALLOWED_ROUTERS = new Set([ + '0xe592427a0aece92de3edee1f18e0157c05861564', + '0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45', +]); + const inferredTriggersCache = new Map(); function isHexChar(char) { @@ -21,7 +45,7 @@ function normalizeAddress(value) { throw new Error(`Invalid address: ${value}`); } } - return value; + return value.toLowerCase(); } function normalizeComparator(value) { @@ -47,6 +71,61 @@ function extractFirstText(responseJson) { return ''; } +function statePath() { + return process.env.PRICE_RACE_STATE_PATH + ? path.resolve(process.env.PRICE_RACE_STATE_PATH) + : path.join(__dirname, '.price-race-state.json'); +} + +function commitmentKey(commitmentText) { + return createHash('sha256').update(commitmentText ?? '').digest('hex'); +} + +function readState() { + const file = statePath(); + if (!existsSync(file)) { + return { commitments: {} }; + } + + try { + const parsed = JSON.parse(readFileSync(file, 'utf8')); + if (!parsed || typeof parsed !== 'object') { + return { commitments: {} }; + } + return { + commitments: + parsed.commitments && typeof parsed.commitments === 'object' + ? parsed.commitments + : {}, + }; + } catch (error) { + return { commitments: {} }; + } +} + +function writeState(state) { + writeFileSync(statePath(), JSON.stringify(state, null, 2)); +} + +function isCommitmentExecuted(commitmentText) { + if (!commitmentText) return false; + const key = commitmentKey(commitmentText); + const state = readState(); + return Boolean(state.commitments?.[key]?.executed); +} + +function markCommitmentExecuted(commitmentText, metadata = {}) { + if (!commitmentText) return; + const key = commitmentKey(commitmentText); + const state = readState(); + state.commitments[key] = { + executed: true, + executedAt: new Date().toISOString(), + ...metadata, + }; + writeState(state); +} + function sanitizeInferredTriggers(rawTriggers) { if (!Array.isArray(rawTriggers)) { return []; @@ -59,7 +138,7 @@ function sanitizeInferredTriggers(rawTriggers) { const baseToken = normalizeAddress(String(trigger.baseToken)); const quoteToken = normalizeAddress(String(trigger.quoteToken)); - if (baseToken.toLowerCase() === quoteToken.toLowerCase()) { + if (baseToken === quoteToken) { throw new Error(`Inferred trigger ${index} uses the same base and quote token.`); } @@ -86,9 +165,13 @@ function sanitizeInferredTriggers(rawTriggers) { }; if (trigger.pool) { - out.pool = normalizeAddress(String(trigger.pool)); + const pool = normalizeAddress(String(trigger.pool)); + if (!ALLOWED_POOLS.has(pool)) { + throw new Error(`Inferred trigger ${out.id} references non-allowlisted pool ${pool}`); + } + out.pool = pool; } else { - out.poolSelection = 'high-liquidity'; + throw new Error(`Inferred trigger ${out.id} must include an explicit pool address.`); } return out; @@ -116,6 +199,10 @@ async function getPriceTriggers({ commitmentText, config }) { return []; } + if (isCommitmentExecuted(commitmentText)) { + return []; + } + if (inferredTriggersCache.has(commitmentText)) { return inferredTriggersCache.get(commitmentText); } @@ -126,7 +213,7 @@ async function getPriceTriggers({ commitmentText, config }) { { role: 'system', content: - 'Extract Uniswap V3 price race triggers from this plain-language commitment. Return strict JSON with shape {"triggers":[...]}. Each trigger must include: id, label, baseToken, quoteToken, comparator (gte|lte), threshold (number), priority (number), and optional pool (address). If no pool is explicit, omit pool and it will use high-liquidity selection. Use only addresses and conditions present in the commitment text.', + 'Extract exactly two Uniswap V3 price race triggers from this plain-language commitment. Return strict JSON: {"triggers":[...]}. Each trigger must include: id, label, baseToken, quoteToken, comparator (gte|lte), threshold (number), priority (number), and pool (address). Use only addresses and conditions present in the commitment text. Do not invent pools, tokens, or thresholds.', }, { role: 'user', @@ -169,6 +256,121 @@ async function getPriceTriggers({ commitmentText, config }) { return triggers; } +function parseCallArgs(call) { + if (call?.parsedArguments && typeof call.parsedArguments === 'object') { + return call.parsedArguments; + } + if (typeof call?.arguments === 'string') { + try { + return JSON.parse(call.arguments); + } catch (error) { + return null; + } + } + return null; +} + +function isMatchingPriceSignal(signal, actionFee, tokenIn, tokenOut) { + if (!signal || signal.kind !== 'priceTrigger') return false; + if (!signal.pool || !ALLOWED_POOLS.has(String(signal.pool).toLowerCase())) return false; + + const sBase = String(signal.baseToken ?? '').toLowerCase(); + const sQuote = String(signal.quoteToken ?? '').toLowerCase(); + const pairMatches = + (sBase === tokenIn && sQuote === tokenOut) || + (sBase === tokenOut && sQuote === tokenIn); + if (!pairMatches) return false; + + if (signal.poolFee === undefined || signal.poolFee === null) return false; + return Number(signal.poolFee) === actionFee; +} + +async function validateToolCalls({ toolCalls, signals, commitmentText, commitmentSafe }) { + if (isCommitmentExecuted(commitmentText)) { + throw new Error('Commitment already executed; refusing additional swap proposals.'); + } + + const validated = []; + const safeAddress = commitmentSafe ? String(commitmentSafe).toLowerCase() : null; + + for (const call of toolCalls) { + if (call.name === 'dispute_assertion') { + validated.push(call); + continue; + } + + if (call.name === 'post_bond_and_propose') { + continue; + } + + if (call.name !== 'build_og_transactions') { + continue; + } + + const args = parseCallArgs(call); + if (!args || !Array.isArray(args.actions) || args.actions.length !== 1) { + throw new Error('build_og_transactions must include exactly one swap action.'); + } + + const action = args.actions[0]; + if (action.kind !== 'uniswap_v3_exact_input_single') { + throw new Error('Only uniswap_v3_exact_input_single is allowed for this agent.'); + } + + const tokenIn = normalizeAddress(String(action.tokenIn)); + const tokenOut = normalizeAddress(String(action.tokenOut)); + const router = normalizeAddress(String(action.router)); + const recipient = normalizeAddress(String(action.recipient)); + const fee = Number(action.fee); + const amountIn = BigInt(action.amountInWei ?? '0'); + const amountOutMin = BigInt(action.amountOutMinWei ?? '0'); + + if (tokenIn !== TOKENS.WETH) { + throw new Error('Swap tokenIn must be Sepolia WETH.'); + } + if (tokenOut !== TOKENS.USDC && tokenOut !== TOKENS.UNI) { + throw new Error('Swap tokenOut must be Sepolia USDC or UNI.'); + } + if (!ALLOWED_ROUTERS.has(router)) { + throw new Error(`Router ${router} is not allowlisted.`); + } + if (safeAddress && recipient !== safeAddress) { + throw new Error('Swap recipient must be the commitment Safe.'); + } + if (!Number.isInteger(fee) || fee <= 0) { + throw new Error('Swap fee must be a positive integer.'); + } + if (amountIn <= 0n) { + throw new Error('Swap amountInWei must be > 0.'); + } + if (amountOutMin < 0n) { + throw new Error('Swap amountOutMinWei must be >= 0.'); + } + + const hasSignalMatch = Array.isArray(signals) + ? signals.some((signal) => isMatchingPriceSignal(signal, fee, tokenIn, tokenOut)) + : false; + if (!hasSignalMatch) { + throw new Error( + 'Swap action does not match an allowlisted priceTrigger signal in the current cycle.' + ); + } + + validated.push(call); + } + + return validated; +} + +async function onToolOutput({ name, parsedOutput, commitmentText }) { + if (name !== 'post_bond_and_propose') return; + if (parsedOutput?.status !== 'submitted') return; + + markCommitmentExecuted(commitmentText, { + proposalHash: parsedOutput?.proposalHash ? String(parsedOutput.proposalHash) : null, + }); +} + function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled ? 'You may propose and dispute.' @@ -185,10 +387,10 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'Use your reasoning over the plain-language commitment and incoming signals. Do not depend on rigid text pattern matching.', 'First trigger wins. If multiple triggers appear true in one cycle, use signal priority and then lexical triggerId order.', 'Use all currently available WETH in the Safe for the winning branch swap.', - 'Preferred flow: build_og_transactions with uniswap_v3_exact_input_single actions, then post_bond_and_propose.', - 'When pool addresses are specified in the commitment/rules, use those pools. Otherwise use high-liquidity Uniswap routing that satisfies slippage constraints.', + 'Preferred flow: build_og_transactions with one uniswap_v3_exact_input_single action, then rely on runner propose submission.', + 'Only use allowlisted Sepolia addresses from the commitment context. Never execute both branches.', 'Use the poolFee from a priceTrigger signal when preparing uniswap_v3_exact_input_single actions.', - 'Never execute both branches, and never route purchased assets to addresses other than the commitment Safe unless explicitly required by the commitment.', + 'Never route purchased assets to addresses other than the commitment Safe unless explicitly required by the commitment.', 'If there is insufficient evidence that a trigger fired first, or route/liquidity/slippage constraints are not safely satisfiable, return ignore.', 'Default to disputing proposals that violate these rules; prefer no-op when unsure.', mode, @@ -199,4 +401,12 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { .join(' '); } -export { getPriceTriggers, getSystemPrompt, sanitizeInferredTriggers }; +export { + getPriceTriggers, + getSystemPrompt, + isCommitmentExecuted, + markCommitmentExecuted, + onToolOutput, + sanitizeInferredTriggers, + validateToolCalls, +}; diff --git a/agent-library/agents/price-race-swap/simulate-price-race-swap.mjs b/agent-library/agents/price-race-swap/simulate-price-race-swap.mjs index fa191567..14aafb19 100644 --- a/agent-library/agents/price-race-swap/simulate-price-race-swap.mjs +++ b/agent-library/agents/price-race-swap/simulate-price-race-swap.mjs @@ -1,25 +1,25 @@ -function pickWinningBranch({ ethPrice, umaPrice, ethThreshold, umaThreshold }) { +function pickWinningBranch({ ethPrice, uniWethPrice, ethThreshold, uniWethThreshold }) { const ethTriggered = ethPrice >= ethThreshold; - const umaTriggered = umaPrice <= umaThreshold; + const uniTriggered = uniWethPrice <= uniWethThreshold; - if (ethTriggered && umaTriggered) { + if (ethTriggered && uniTriggered) { return { - winner: 'eth', - reason: 'tie-break: ETH wins when both are true in same evaluation cycle', + winner: 'weth-to-usdc', + reason: 'tie-break: WETH/USDC branch wins when both are true in same evaluation cycle', }; } if (ethTriggered) { return { - winner: 'eth', - reason: `ETH/USDC ${ethPrice} >= ${ethThreshold}`, + winner: 'weth-to-usdc', + reason: `WETH/USDC ${ethPrice} >= ${ethThreshold}`, }; } - if (umaTriggered) { + if (uniTriggered) { return { - winner: 'uma', - reason: `UMA/USDC ${umaPrice} <= ${umaThreshold}`, + winner: 'weth-to-uni', + reason: `UNI/WETH ${uniWethPrice} <= ${uniWethThreshold}`, }; } @@ -32,20 +32,20 @@ function pickWinningBranch({ ethPrice, umaPrice, ethThreshold, umaThreshold }) { function run() { const scenarios = [ { - name: 'ETH wins', - input: { ethPrice: 3250, umaPrice: 2.8, ethThreshold: 3200, umaThreshold: 2.1 }, + name: 'WETH->USDC wins', + input: { ethPrice: 1850, uniWethPrice: 0.05, ethThreshold: 1800, uniWethThreshold: 0.03 }, }, { - name: 'UMA wins', - input: { ethPrice: 3000, umaPrice: 2.0, ethThreshold: 3200, umaThreshold: 2.1 }, + name: 'WETH->UNI wins', + input: { ethPrice: 1700, uniWethPrice: 0.02, ethThreshold: 1800, uniWethThreshold: 0.03 }, }, { - name: 'Tie -> ETH wins', - input: { ethPrice: 3200, umaPrice: 2.1, ethThreshold: 3200, umaThreshold: 2.1 }, + name: 'Tie -> WETH->USDC wins', + input: { ethPrice: 1800, uniWethPrice: 0.03, ethThreshold: 1800, uniWethThreshold: 0.03 }, }, { name: 'No trigger', - input: { ethPrice: 3100, umaPrice: 2.4, ethThreshold: 3200, umaThreshold: 2.1 }, + input: { ethPrice: 1700, uniWethPrice: 0.04, ethThreshold: 1800, uniWethThreshold: 0.03 }, }, ]; diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs new file mode 100644 index 00000000..b4d3757f --- /dev/null +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict'; +import { validateToolCalls } from './agent.js'; + +const WETH = '0xfff9976782d46cc05630d1f6ebab18b2324d6b14'; +const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; +const ROUTER = '0xe592427a0aece92de3edee1f18e0157c05861564'; +const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; + +async function run() { + const toolCalls = [ + { + name: 'build_og_transactions', + callId: '1', + parsedArguments: { + actions: [ + { + kind: 'uniswap_v3_exact_input_single', + router: ROUTER, + tokenIn: WETH, + tokenOut: USDC, + fee: 3000, + recipient: '0x1234000000000000000000000000000000000000', + amountInWei: '1', + amountOutMinWei: '0', + }, + ], + }, + }, + ]; + + const signals = [ + { + kind: 'priceTrigger', + pool: POOL, + poolFee: 3000, + baseToken: WETH, + quoteToken: USDC, + }, + ]; + + const ok = await validateToolCalls({ + toolCalls, + signals, + commitmentText: 'x', + commitmentSafe: '0x1234000000000000000000000000000000000000', + }); + assert.equal(ok.length, 1); + + await assert.rejects(() => + validateToolCalls({ + toolCalls: [ + { + ...toolCalls[0], + parsedArguments: { + actions: [ + { + ...toolCalls[0].parsedArguments.actions[0], + router: '0x0000000000000000000000000000000000000001', + }, + ], + }, + }, + ], + signals, + commitmentText: 'y', + commitmentSafe: '0x1234000000000000000000000000000000000000', + }) + ); + + console.log('[test] allowlist validation OK'); +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/agent-library/agents/price-race-swap/test-single-fire-state.mjs b/agent-library/agents/price-race-swap/test-single-fire-state.mjs new file mode 100644 index 00000000..e5d20bd0 --- /dev/null +++ b/agent-library/agents/price-race-swap/test-single-fire-state.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +async function run() { + const tempDir = mkdtempSync(path.join(os.tmpdir(), 'price-race-state-')); + try { + process.env.PRICE_RACE_STATE_PATH = path.join(tempDir, 'state.json'); + + const mod = await import(`./agent.js?test=${Date.now()}`); + const commitment = 'Test commitment text for persistence'; + + assert.equal(mod.isCommitmentExecuted(commitment), false); + mod.markCommitmentExecuted(commitment, { proposalHash: '0xabc' }); + assert.equal(mod.isCommitmentExecuted(commitment), true); + + await mod.onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { status: 'submitted', proposalHash: '0xdef' }, + commitmentText: 'Another commitment', + }); + assert.equal(mod.isCommitmentExecuted('Another commitment'), true); + + console.log('[test] single-fire state persistence OK'); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + delete process.env.PRICE_RACE_STATE_PATH; + } +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/agent-library/agents/price-race-swap/test-trigger-inference.mjs b/agent-library/agents/price-race-swap/test-trigger-inference.mjs index 94e7e71e..1d6de81e 100644 --- a/agent-library/agents/price-race-swap/test-trigger-inference.mjs +++ b/agent-library/agents/price-race-swap/test-trigger-inference.mjs @@ -15,6 +15,7 @@ function run() { comparator: 'lte', threshold: 0.03, priority: 1, + pool: '0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7', }, { id: 'first', diff --git a/agent/src/index.js b/agent/src/index.js index bf7381a3..eb9ebfc2 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -16,7 +16,7 @@ import { pollProposalChanges, primeBalances, } from './lib/polling.js'; -import { callAgent, explainToolCalls } from './lib/llm.js'; +import { callAgent, explainToolCalls, parseToolArguments } from './lib/llm.js'; import { executeToolCalls, toolDefinitions } from './lib/tools.js'; import { makeDeposit, postBondAndDispute, postBondAndPropose } from './lib/tx.js'; import { extractTimelockTriggers } from './lib/timelock.js'; @@ -182,8 +182,46 @@ async function decideOnSignals(signals) { } if (decision.toolCalls.length > 0) { + let approvedToolCalls = decision.toolCalls; + if (typeof agentModule?.validateToolCalls === 'function') { + try { + const validated = await agentModule.validateToolCalls({ + toolCalls: decision.toolCalls.map((call) => ({ + ...call, + parsedArguments: parseToolArguments(call.arguments), + })), + signals, + commitmentText, + commitmentSafe: config.commitmentSafe, + agentAddress, + }); + if (Array.isArray(validated)) { + approvedToolCalls = validated.map((call) => ({ + name: call.name, + callId: call.callId, + arguments: + call.arguments !== undefined + ? call.arguments + : JSON.stringify(call.parsedArguments ?? {}), + })); + } else { + approvedToolCalls = []; + } + } catch (error) { + console.warn( + '[agent] validateToolCalls rejected tool calls:', + error?.message ?? error + ); + approvedToolCalls = []; + } + } + + if (approvedToolCalls.length === 0) { + return true; + } + const toolOutputs = await executeToolCalls({ - toolCalls: decision.toolCalls, + toolCalls: approvedToolCalls, publicClient, walletClient, account, @@ -199,9 +237,12 @@ async function decideOnSignals(signals) { } catch (error) { parsed = null; } - agentModule.onToolOutput({ + await agentModule.onToolOutput({ name: output.name, parsedOutput: parsed, + commitmentText, + commitmentSafe: config.commitmentSafe, + agentAddress, }); } } diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 5c27a40c..585fd1c6 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -364,6 +364,7 @@ async function executeToolCalls({ console.warn('[agent] Unknown tool call:', call.name); outputs.push({ callId: call.callId, + name: call.name, output: safeStringify({ status: 'skipped', reason: 'unknown tool' }), }); } @@ -372,7 +373,7 @@ async function executeToolCalls({ if (!config.proposeEnabled) { console.log('[agent] Built transactions but proposals are disabled; skipping propose.'); } else { - await postBondAndPropose({ + const result = await postBondAndPropose({ publicClient, walletClient, account, @@ -380,6 +381,14 @@ async function executeToolCalls({ ogModule: config.ogModule, transactions: builtTransactions, }); + outputs.push({ + callId: 'auto_post_bond_and_propose', + name: 'post_bond_and_propose', + output: safeStringify({ + status: 'submitted', + ...result, + }), + }); } } From 11f82ab1e7bf9964348d07636037e13647a99f03 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 17:15:22 -0800 Subject: [PATCH 083/174] fix weth address for sepolia in commitment Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/commitment.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent-library/agents/price-race-swap/commitment.txt b/agent-library/agents/price-race-swap/commitment.txt index a3c0df13..3feb1f17 100644 --- a/agent-library/agents/price-race-swap/commitment.txt +++ b/agent-library/agents/price-race-swap/commitment.txt @@ -1,6 +1,6 @@ This commitment should use whatever WETH is available in the Safe at the moment of execution. -Token addresses for this commitment are as follows: WETH is 0xfff9976782d46cc05630d1f6ebab18b2324d6b14, USDC is 0x1c7d4b196cb0c7b01d743fbc6116a902379c7238, and UNI is 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984. +Token addresses for this commitment are as follows: WETH is 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9, USDC is 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238, and UNI is 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984. If WETH/USDC is greater than or equal to 1800 and this is the first trigger that becomes true, swap all available WETH for USDC. For this WETH-to-USDC branch, use Uniswap V3 pool 0x6418eec70f50913ff0d756b48d32ce7c02b47c47. From 19fd1ac5a6373c33a7c1e7e08a5cfb9003643e02 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 17:27:33 -0800 Subject: [PATCH 084/174] update uniswap script to specify high-liquidity pool instead of hardcoding pool addresses Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 22 +++++++------------ .../agents/price-race-swap/commitment.txt | 4 ++-- .../test-trigger-inference.mjs | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 2316c618..4c3e9508 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -12,15 +12,11 @@ const TOKENS = Object.freeze({ UNI: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', }); -const ALLOWED_POOLS = new Set([ - '0x6418eec70f50913ff0d756b48d32ce7c02b47c47', - '0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7', -]); - const ALLOWED_ROUTERS = new Set([ '0xe592427a0aece92de3edee1f18e0157c05861564', '0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45', ]); +const ALLOWED_FEE_TIERS = new Set([500, 3000, 10000]); const inferredTriggersCache = new Map(); @@ -165,13 +161,9 @@ function sanitizeInferredTriggers(rawTriggers) { }; if (trigger.pool) { - const pool = normalizeAddress(String(trigger.pool)); - if (!ALLOWED_POOLS.has(pool)) { - throw new Error(`Inferred trigger ${out.id} references non-allowlisted pool ${pool}`); - } - out.pool = pool; + out.pool = normalizeAddress(String(trigger.pool)); } else { - throw new Error(`Inferred trigger ${out.id} must include an explicit pool address.`); + out.poolSelection = 'high-liquidity'; } return out; @@ -213,7 +205,7 @@ async function getPriceTriggers({ commitmentText, config }) { { role: 'system', content: - 'Extract exactly two Uniswap V3 price race triggers from this plain-language commitment. Return strict JSON: {"triggers":[...]}. Each trigger must include: id, label, baseToken, quoteToken, comparator (gte|lte), threshold (number), priority (number), and pool (address). Use only addresses and conditions present in the commitment text. Do not invent pools, tokens, or thresholds.', + 'Extract exactly two Uniswap V3 price race triggers from this plain-language commitment. Return strict JSON: {"triggers":[...]}. Each trigger must include: id, label, baseToken, quoteToken, comparator (gte|lte), threshold (number), priority (number), and optional pool (address). If pool is not explicit in the commitment, omit it and high-liquidity pool selection will be used. Use only addresses and conditions present in the commitment text. Do not invent pools, tokens, or thresholds.', }, { role: 'user', @@ -272,7 +264,6 @@ function parseCallArgs(call) { function isMatchingPriceSignal(signal, actionFee, tokenIn, tokenOut) { if (!signal || signal.kind !== 'priceTrigger') return false; - if (!signal.pool || !ALLOWED_POOLS.has(String(signal.pool).toLowerCase())) return false; const sBase = String(signal.baseToken ?? '').toLowerCase(); const sQuote = String(signal.quoteToken ?? '').toLowerCase(); @@ -340,6 +331,9 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen if (!Number.isInteger(fee) || fee <= 0) { throw new Error('Swap fee must be a positive integer.'); } + if (!ALLOWED_FEE_TIERS.has(fee)) { + throw new Error('Swap fee tier is not allowlisted.'); + } if (amountIn <= 0n) { throw new Error('Swap amountInWei must be > 0.'); } @@ -352,7 +346,7 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen : false; if (!hasSignalMatch) { throw new Error( - 'Swap action does not match an allowlisted priceTrigger signal in the current cycle.' + 'Swap action does not match a current priceTrigger signal in the current cycle.' ); } diff --git a/agent-library/agents/price-race-swap/commitment.txt b/agent-library/agents/price-race-swap/commitment.txt index 3feb1f17..9dfce469 100644 --- a/agent-library/agents/price-race-swap/commitment.txt +++ b/agent-library/agents/price-race-swap/commitment.txt @@ -2,8 +2,8 @@ This commitment should use whatever WETH is available in the Safe at the moment Token addresses for this commitment are as follows: WETH is 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9, USDC is 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238, and UNI is 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984. -If WETH/USDC is greater than or equal to 1800 and this is the first trigger that becomes true, swap all available WETH for USDC. For this WETH-to-USDC branch, use Uniswap V3 pool 0x6418eec70f50913ff0d756b48d32ce7c02b47c47. +If WETH/USDC is greater than or equal to 1800 and this is the first trigger that becomes true, swap all available WETH for USDC using a high-liquidity Uniswap V3 pool. -If UNI/WETH is less than or equal to 0.03 and this is the first trigger that becomes true, swap all available WETH for UNI. For this WETH-to-UNI branch, use Uniswap V3 pool 0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7. +If UNI/WETH is less than or equal to 0.03 and this is the first trigger that becomes true, swap all available WETH for UNI using a high-liquidity Uniswap V3 pool. Only one branch may execute in total. First trigger wins. Enforce max slippage of 0.50%. If no valid route meets slippage and liquidity constraints, do not trade. diff --git a/agent-library/agents/price-race-swap/test-trigger-inference.mjs b/agent-library/agents/price-race-swap/test-trigger-inference.mjs index 1d6de81e..5a60b3ff 100644 --- a/agent-library/agents/price-race-swap/test-trigger-inference.mjs +++ b/agent-library/agents/price-race-swap/test-trigger-inference.mjs @@ -15,7 +15,6 @@ function run() { comparator: 'lte', threshold: 0.03, priority: 1, - pool: '0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7', }, { id: 'first', @@ -31,6 +30,7 @@ function run() { assert.equal(normalized.length, 2); assert.equal(normalized[0].id, 'first'); assert.equal(normalized[1].id, 'second'); + assert.equal(normalized[1].poolSelection, 'high-liquidity'); assert.throws(() => sanitizeInferredTriggers([ From 0d0e7d8efc020b35c08f155b32fcdca2c4c8e222 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 17:32:48 -0800 Subject: [PATCH 085/174] fix some sepolia addresses in uniswap commitment Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 2 +- agent-library/agents/price-race-swap/test-allowlist.mjs | 2 +- agent-library/agents/price-race-swap/test-trigger-inference.mjs | 2 +- agent/src/lib/uniswapV3Price.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 4c3e9508..ec623f2a 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const TOKENS = Object.freeze({ - WETH: '0xfff9976782d46cc05630d1f6ebab18b2324d6b14', + WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', USDC: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238', UNI: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', }); diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index b4d3757f..918e9c1b 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { validateToolCalls } from './agent.js'; -const WETH = '0xfff9976782d46cc05630d1f6ebab18b2324d6b14'; +const WETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'; const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; const ROUTER = '0xe592427a0aece92de3edee1f18e0157c05861564'; const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; diff --git a/agent-library/agents/price-race-swap/test-trigger-inference.mjs b/agent-library/agents/price-race-swap/test-trigger-inference.mjs index 5a60b3ff..4ed623bc 100644 --- a/agent-library/agents/price-race-swap/test-trigger-inference.mjs +++ b/agent-library/agents/price-race-swap/test-trigger-inference.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { sanitizeInferredTriggers } from './agent.js'; -const WETH = '0xfff9976782d46cc05630d1f6ebab18b2324d6b14'; +const WETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'; const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; const UNI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'; const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; diff --git a/agent/src/lib/uniswapV3Price.js b/agent/src/lib/uniswapV3Price.js index ec9ef3da..47a17539 100644 --- a/agent/src/lib/uniswapV3Price.js +++ b/agent/src/lib/uniswapV3Price.js @@ -14,7 +14,7 @@ const uniswapV3FactoryAbi = parseAbi([ const defaultFactoryByChainId = new Map([ [1, '0x1F98431c8aD98523631AE4a59f267346ea31F984'], - [11155111, '0x0227628f3f023bb0b980b67d528dd8f8c1b5bf8f'], + [11155111, '0x0227628f3F023bb0B980b67D528571c95c6DaC1c'], ]); async function getFactoryAddress({ publicClient, configuredFactory }) { From ada18ebfc144cd4c0332f3212c9e366ced9aa8b7 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 17:38:31 -0800 Subject: [PATCH 086/174] allow multiple price signal triggers for uniswap commitment Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index ec623f2a..da221fa2 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -157,7 +157,8 @@ function sanitizeInferredTriggers(rawTriggers) { comparator: normalizeComparator(trigger.comparator), threshold, priority, - emitOnce: trigger.emitOnce === undefined ? true : Boolean(trigger.emitOnce), + // Keep price conditions level-triggered so a later deposit can still act. + emitOnce: trigger.emitOnce === undefined ? false : Boolean(trigger.emitOnce), }; if (trigger.pool) { From 0651b93166e4f9d7f19e58e20a11e6094f6b208b Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 17:44:04 -0800 Subject: [PATCH 087/174] infer active price triggers in uniswap commitment Signed-off-by: John Shutt --- agent/src/index.js | 7 +++++++ agent/src/lib/polling.js | 45 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index eb9ebfc2..a2a13aec 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -272,6 +272,13 @@ async function decideOnSignals(signals) { async function agentLoop() { try { + const triggerSeedRulesText = ogContext?.rules ?? commitmentText ?? ''; + const triggerSeed = await getActivePriceTriggers({ rulesText: triggerSeedRulesText }); + for (const trigger of triggerSeed) { + if (trigger?.baseToken) trackedAssets.add(trigger.baseToken); + if (trigger?.quoteToken) trackedAssets.add(trigger.quoteToken); + } + const latestBlock = await publicClient.getBlockNumber(); const latestBlockData = await publicClient.getBlock({ blockNumber: latestBlock }); const nowMs = Number(latestBlockData.timestamp) * 1000; diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index 56c372cb..2c76c9f9 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -1,4 +1,4 @@ -import { getAddress, hexToString, zeroAddress } from 'viem'; +import { erc20Abi, getAddress, hexToString, zeroAddress } from 'viem'; import { optimisticGovernorAbi, proposalDeletedEvent, @@ -16,6 +16,34 @@ async function primeBalances({ publicClient, commitmentSafe, watchNativeBalance, }); } +async function primeAssetBalanceSignals({ publicClient, trackedAssets, commitmentSafe, blockNumber }) { + const balances = await Promise.all( + Array.from(trackedAssets).map(async (asset) => { + const balance = await publicClient.readContract({ + address: asset, + abi: erc20Abi, + functionName: 'balanceOf', + args: [commitmentSafe], + blockNumber, + }); + return { asset, balance }; + }) + ); + + return balances + .filter((item) => item.balance > 0n) + .map((item) => ({ + kind: 'erc20BalanceSnapshot', + asset: item.asset, + from: 'snapshot', + amount: item.balance, + blockNumber, + transactionHash: undefined, + logIndex: undefined, + id: `snapshot:${item.asset}:${blockNumber.toString()}`, + })); +} + async function pollCommitmentChanges({ publicClient, trackedAssets, @@ -32,8 +60,21 @@ async function pollCommitmentChanges({ watchNativeBalance, blockNumber: latestBlock, }); + const initialAssetSignals = await primeAssetBalanceSignals({ + publicClient, + trackedAssets, + commitmentSafe, + blockNumber: latestBlock, + }); + if (initialAssetSignals.length > 0) { + console.log( + `[agent] Startup balance snapshot signals: ${initialAssetSignals + .map((s) => `${s.asset}:${s.amount.toString()}`) + .join(', ')}` + ); + } return { - deposits: [], + deposits: initialAssetSignals, lastCheckedBlock: latestBlock, lastNativeBalance: nextNativeBalance, }; From 8574fcd1778c658dd7f33fb0424c6311bcb4c837 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 17:52:09 -0800 Subject: [PATCH 088/174] check balances every cycle Signed-off-by: John Shutt --- agent/src/index.js | 10 +++++- agent/src/lib/polling.js | 70 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index a2a13aec..23fee5a9 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -38,6 +38,7 @@ const trackedAssets = new Set(config.watchAssets); let lastCheckedBlock = config.startBlock; let lastProposalCheckedBlock = config.startBlock; let lastNativeBalance; +let lastAssetBalances = new Map(); let ogContext; const proposalsByHash = new Map(); const depositHistory = []; @@ -283,7 +284,12 @@ async function agentLoop() { const latestBlockData = await publicClient.getBlock({ blockNumber: latestBlock }); const nowMs = Number(latestBlockData.timestamp) * 1000; - const { deposits, lastCheckedBlock: nextCheckedBlock, lastNativeBalance: nextNative } = + const { + deposits, + lastCheckedBlock: nextCheckedBlock, + lastNativeBalance: nextNative, + lastAssetBalances: nextAssetBalances, + } = await pollCommitmentChanges({ publicClient, trackedAssets, @@ -291,9 +297,11 @@ async function agentLoop() { watchNativeBalance: config.watchNativeBalance, lastCheckedBlock, lastNativeBalance, + lastAssetBalances, }); lastCheckedBlock = nextCheckedBlock; lastNativeBalance = nextNative; + lastAssetBalances = nextAssetBalances ?? lastAssetBalances; for (const deposit of deposits) { const timestampMs = await getBlockTimestampMs(deposit.blockNumber); diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index 2c76c9f9..5585fd31 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -1,4 +1,4 @@ -import { erc20Abi, getAddress, hexToString, zeroAddress } from 'viem'; +import { erc20Abi, getAddress, hexToString, isAddressEqual, zeroAddress } from 'viem'; import { optimisticGovernorAbi, proposalDeletedEvent, @@ -19,6 +19,9 @@ async function primeBalances({ publicClient, commitmentSafe, watchNativeBalance, async function primeAssetBalanceSignals({ publicClient, trackedAssets, commitmentSafe, blockNumber }) { const balances = await Promise.all( Array.from(trackedAssets).map(async (asset) => { + if (isAddressEqual(asset, zeroAddress)) { + return { asset, balance: 0n }; + } const balance = await publicClient.readContract({ address: asset, abi: erc20Abi, @@ -44,6 +47,47 @@ async function primeAssetBalanceSignals({ publicClient, trackedAssets, commitmen })); } +async function collectAssetBalanceChangeSignals({ + publicClient, + trackedAssets, + commitmentSafe, + blockNumber, + lastAssetBalances, +}) { + const nextAssetBalances = new Map(lastAssetBalances ?? []); + const signals = []; + + for (const asset of trackedAssets) { + if (isAddressEqual(asset, zeroAddress)) { + continue; + } + const current = await publicClient.readContract({ + address: asset, + abi: erc20Abi, + functionName: 'balanceOf', + args: [commitmentSafe], + blockNumber, + }); + const previous = nextAssetBalances.get(asset); + nextAssetBalances.set(asset, current); + + if (current > 0n && (previous === undefined || current !== previous)) { + signals.push({ + kind: 'erc20BalanceSnapshot', + asset, + from: 'snapshot', + amount: current, + blockNumber, + transactionHash: undefined, + logIndex: undefined, + id: `snapshot:${asset}:${blockNumber.toString()}`, + }); + } + } + + return { signals, nextAssetBalances }; +} + async function pollCommitmentChanges({ publicClient, trackedAssets, @@ -51,6 +95,7 @@ async function pollCommitmentChanges({ watchNativeBalance, lastCheckedBlock, lastNativeBalance, + lastAssetBalances, }) { const latestBlock = await publicClient.getBlockNumber(); if (lastCheckedBlock === undefined) { @@ -77,11 +122,16 @@ async function pollCommitmentChanges({ deposits: initialAssetSignals, lastCheckedBlock: latestBlock, lastNativeBalance: nextNativeBalance, + lastAssetBalances: + lastAssetBalances ?? + new Map( + initialAssetSignals.map((signal) => [signal.asset, BigInt(signal.amount)]) + ), }; } if (latestBlock <= lastCheckedBlock) { - return { deposits: [], lastCheckedBlock, lastNativeBalance }; + return { deposits: [], lastCheckedBlock, lastNativeBalance, lastAssetBalances }; } const fromBlock = lastCheckedBlock + 1n; @@ -145,7 +195,21 @@ async function pollCommitmentChanges({ nextNativeBalance = nativeBalance; } - return { deposits, lastCheckedBlock: toBlock, lastNativeBalance: nextNativeBalance }; + const { signals: balanceSignals, nextAssetBalances } = await collectAssetBalanceChangeSignals({ + publicClient, + trackedAssets, + commitmentSafe, + blockNumber: toBlock, + lastAssetBalances, + }); + deposits.push(...balanceSignals); + + return { + deposits, + lastCheckedBlock: toBlock, + lastNativeBalance: nextNativeBalance, + lastAssetBalances: nextAssetBalances, + }; } async function pollProposalChanges({ publicClient, ogModule, lastProposalCheckedBlock, proposalsByHash }) { From 16f40eb4606660036362e2c6975a2956fb86611a Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 17:56:22 -0800 Subject: [PATCH 089/174] attempt some fixes for address polling Signed-off-by: John Shutt --- agent/src/index.js | 6 +++--- agent/src/lib/polling.js | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 23fee5a9..7103fbe6 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -34,7 +34,7 @@ const publicClient = createPublicClient({ transport: http(config.rpcUrl) }); const { account, walletClient } = await createSignerClient({ rpcUrl: config.rpcUrl }); const agentAddress = account.address; -const trackedAssets = new Set(config.watchAssets); +const trackedAssets = new Set(config.watchAssets.map((asset) => String(asset).toLowerCase())); let lastCheckedBlock = config.startBlock; let lastProposalCheckedBlock = config.startBlock; let lastNativeBalance; @@ -276,8 +276,8 @@ async function agentLoop() { const triggerSeedRulesText = ogContext?.rules ?? commitmentText ?? ''; const triggerSeed = await getActivePriceTriggers({ rulesText: triggerSeedRulesText }); for (const trigger of triggerSeed) { - if (trigger?.baseToken) trackedAssets.add(trigger.baseToken); - if (trigger?.quoteToken) trackedAssets.add(trigger.quoteToken); + if (trigger?.baseToken) trackedAssets.add(String(trigger.baseToken).toLowerCase()); + if (trigger?.quoteToken) trackedAssets.add(String(trigger.quoteToken).toLowerCase()); } const latestBlock = await publicClient.getBlockNumber(); diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index 5585fd31..4c9026e6 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -144,10 +144,13 @@ async function pollCommitmentChanges({ const currentTo = currentFrom + maxRange - 1n > toBlock ? toBlock : currentFrom + maxRange - 1n; - for (const asset of trackedAssets) { - const logs = await publicClient.getLogs({ - address: asset, - event: transferEvent, + for (const asset of trackedAssets) { + if (isAddressEqual(asset, zeroAddress)) { + continue; + } + const logs = await publicClient.getLogs({ + address: asset, + event: transferEvent, args: { to: commitmentSafe }, fromBlock: currentFrom, toBlock: currentTo, From 361ff30dec003753fa10cccaf2327c55dc0ed8fc Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 17:58:59 -0800 Subject: [PATCH 090/174] allow model to act based only on price signals, and pass balance snapshot on every run for non-zero balances Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 7 +++++++ agent/src/lib/polling.js | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index da221fa2..dd85ad62 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -380,8 +380,15 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'Your own address is provided as agentAddress.', 'Interpret the commitment as a multi-choice race and execute at most one winning branch.', 'Use your reasoning over the plain-language commitment and incoming signals. Do not depend on rigid text pattern matching.', + 'Treat erc20BalanceSnapshot signals as authoritative current Safe balances for this cycle.', + 'If exactly one priceTrigger signal is present in this cycle, treat it as the winning branch for this cycle.', + 'When both a winning priceTrigger and a WETH erc20BalanceSnapshot are present, you have enough information to act.', 'First trigger wins. If multiple triggers appear true in one cycle, use signal priority and then lexical triggerId order.', 'Use all currently available WETH in the Safe for the winning branch swap.', + 'Build one uniswap_v3_exact_input_single action where amountInWei equals the WETH snapshot amount.', + 'Compute amountOutMinWei using observedPrice and max slippage 0.50% (multiply expected output by 0.995).', + 'If tokenIn is the base token of observedPrice, expectedOut ~= amountIn * observedPrice adjusted for token decimals.', + 'If tokenIn is the quote token and tokenOut is base, expectedOut ~= amountIn / observedPrice adjusted for token decimals.', 'Preferred flow: build_og_transactions with one uniswap_v3_exact_input_single action, then rely on runner propose submission.', 'Only use allowlisted Sepolia addresses from the commitment context. Never execute both branches.', 'Use the poolFee from a priceTrigger signal when preparing uniswap_v3_exact_input_single actions.', diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index 4c9026e6..3f75f7b1 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -71,7 +71,7 @@ async function collectAssetBalanceChangeSignals({ const previous = nextAssetBalances.get(asset); nextAssetBalances.set(asset, current); - if (current > 0n && (previous === undefined || current !== previous)) { + if (current > 0n) { signals.push({ kind: 'erc20BalanceSnapshot', asset, From e2163ef38e3989ba09dbcc88070df7abc020b2a5 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 18:03:31 -0800 Subject: [PATCH 091/174] set build_og_transactions strictness to false, since different og transaction payloads will use different inputs Signed-off-by: John Shutt --- agent/src/lib/tools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 585fd1c6..32f72198 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -13,7 +13,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { name: 'build_og_transactions', description: 'Build Optimistic Governor transaction payloads from high-level intents. Returns array of {to,value,data,operation} with value as string wei.', - strict: true, + strict: false, parameters: { type: 'object', additionalProperties: false, From e364a35202d15ff10c508e04375e84d4bb2da4fd Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 18:06:38 -0800 Subject: [PATCH 092/174] patch explainToolCalls to exclude synthetic outputs Signed-off-by: John Shutt --- .../agents/price-race-swap/.price-race-state.json | 9 +++++++++ agent/src/index.js | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 agent-library/agents/price-race-swap/.price-race-state.json diff --git a/agent-library/agents/price-race-swap/.price-race-state.json b/agent-library/agents/price-race-swap/.price-race-state.json new file mode 100644 index 00000000..0c214092 --- /dev/null +++ b/agent-library/agents/price-race-swap/.price-race-state.json @@ -0,0 +1,9 @@ +{ + "commitments": { + "6d8f4c776d6c70afd1c482fdfb1f1bc311fda8eee58097197f8fd65e64f83bbc": { + "executed": true, + "executedAt": "2026-02-10T02:04:27.539Z", + "proposalHash": "0xc532391db50bf9c6ce99ffb491316a0d9df5760837362ab54d4e328d011efc1b" + } + } +} \ No newline at end of file diff --git a/agent/src/index.js b/agent/src/index.js index 7103fbe6..51636032 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -247,11 +247,20 @@ async function decideOnSignals(signals) { }); } } - if (decision.responseId && toolOutputs.length > 0) { + const modelCallIds = new Set( + approvedToolCalls + .map((call) => call?.callId) + .filter((callId) => typeof callId === 'string' && callId.length > 0) + ); + const explainableOutputs = toolOutputs.filter( + (output) => output?.callId && modelCallIds.has(output.callId) + ); + + if (decision.responseId && explainableOutputs.length > 0) { const explanation = await explainToolCalls({ config, previousResponseId: decision.responseId, - toolOutputs, + toolOutputs: explainableOutputs, }); if (explanation) { console.log('[agent] Agent explanation:', explanation); From e067c3c5ba93bab0486af5d8cf6ce5c0f1958c76 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 18:11:25 -0800 Subject: [PATCH 093/174] remove single-fire state logic Signed-off-by: John Shutt --- .../price-race-swap/.price-race-state.json | 9 -- agent-library/agents/price-race-swap/agent.js | 83 ------------------- .../test-single-fire-state.mjs | 35 -------- 3 files changed, 127 deletions(-) delete mode 100644 agent-library/agents/price-race-swap/.price-race-state.json delete mode 100644 agent-library/agents/price-race-swap/test-single-fire-state.mjs diff --git a/agent-library/agents/price-race-swap/.price-race-state.json b/agent-library/agents/price-race-swap/.price-race-state.json deleted file mode 100644 index 0c214092..00000000 --- a/agent-library/agents/price-race-swap/.price-race-state.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "commitments": { - "6d8f4c776d6c70afd1c482fdfb1f1bc311fda8eee58097197f8fd65e64f83bbc": { - "executed": true, - "executedAt": "2026-02-10T02:04:27.539Z", - "proposalHash": "0xc532391db50bf9c6ce99ffb491316a0d9df5760837362ab54d4e328d011efc1b" - } - } -} \ No newline at end of file diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index dd85ad62..08a14a31 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -1,11 +1,3 @@ -import { createHash } from 'node:crypto'; -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - const TOKENS = Object.freeze({ WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', USDC: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238', @@ -67,61 +59,6 @@ function extractFirstText(responseJson) { return ''; } -function statePath() { - return process.env.PRICE_RACE_STATE_PATH - ? path.resolve(process.env.PRICE_RACE_STATE_PATH) - : path.join(__dirname, '.price-race-state.json'); -} - -function commitmentKey(commitmentText) { - return createHash('sha256').update(commitmentText ?? '').digest('hex'); -} - -function readState() { - const file = statePath(); - if (!existsSync(file)) { - return { commitments: {} }; - } - - try { - const parsed = JSON.parse(readFileSync(file, 'utf8')); - if (!parsed || typeof parsed !== 'object') { - return { commitments: {} }; - } - return { - commitments: - parsed.commitments && typeof parsed.commitments === 'object' - ? parsed.commitments - : {}, - }; - } catch (error) { - return { commitments: {} }; - } -} - -function writeState(state) { - writeFileSync(statePath(), JSON.stringify(state, null, 2)); -} - -function isCommitmentExecuted(commitmentText) { - if (!commitmentText) return false; - const key = commitmentKey(commitmentText); - const state = readState(); - return Boolean(state.commitments?.[key]?.executed); -} - -function markCommitmentExecuted(commitmentText, metadata = {}) { - if (!commitmentText) return; - const key = commitmentKey(commitmentText); - const state = readState(); - state.commitments[key] = { - executed: true, - executedAt: new Date().toISOString(), - ...metadata, - }; - writeState(state); -} - function sanitizeInferredTriggers(rawTriggers) { if (!Array.isArray(rawTriggers)) { return []; @@ -192,10 +129,6 @@ async function getPriceTriggers({ commitmentText, config }) { return []; } - if (isCommitmentExecuted(commitmentText)) { - return []; - } - if (inferredTriggersCache.has(commitmentText)) { return inferredTriggersCache.get(commitmentText); } @@ -278,10 +211,6 @@ function isMatchingPriceSignal(signal, actionFee, tokenIn, tokenOut) { } async function validateToolCalls({ toolCalls, signals, commitmentText, commitmentSafe }) { - if (isCommitmentExecuted(commitmentText)) { - throw new Error('Commitment already executed; refusing additional swap proposals.'); - } - const validated = []; const safeAddress = commitmentSafe ? String(commitmentSafe).toLowerCase() : null; @@ -357,15 +286,6 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen return validated; } -async function onToolOutput({ name, parsedOutput, commitmentText }) { - if (name !== 'post_bond_and_propose') return; - if (parsedOutput?.status !== 'submitted') return; - - markCommitmentExecuted(commitmentText, { - proposalHash: parsedOutput?.proposalHash ? String(parsedOutput.proposalHash) : null, - }); -} - function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled ? 'You may propose and dispute.' @@ -406,9 +326,6 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { export { getPriceTriggers, getSystemPrompt, - isCommitmentExecuted, - markCommitmentExecuted, - onToolOutput, sanitizeInferredTriggers, validateToolCalls, }; diff --git a/agent-library/agents/price-race-swap/test-single-fire-state.mjs b/agent-library/agents/price-race-swap/test-single-fire-state.mjs deleted file mode 100644 index e5d20bd0..00000000 --- a/agent-library/agents/price-race-swap/test-single-fire-state.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import assert from 'node:assert/strict'; -import { mkdtempSync, rmSync } from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; - -async function run() { - const tempDir = mkdtempSync(path.join(os.tmpdir(), 'price-race-state-')); - try { - process.env.PRICE_RACE_STATE_PATH = path.join(tempDir, 'state.json'); - - const mod = await import(`./agent.js?test=${Date.now()}`); - const commitment = 'Test commitment text for persistence'; - - assert.equal(mod.isCommitmentExecuted(commitment), false); - mod.markCommitmentExecuted(commitment, { proposalHash: '0xabc' }); - assert.equal(mod.isCommitmentExecuted(commitment), true); - - await mod.onToolOutput({ - name: 'post_bond_and_propose', - parsedOutput: { status: 'submitted', proposalHash: '0xdef' }, - commitmentText: 'Another commitment', - }); - assert.equal(mod.isCommitmentExecuted('Another commitment'), true); - - console.log('[test] single-fire state persistence OK'); - } finally { - rmSync(tempDir, { recursive: true, force: true }); - delete process.env.PRICE_RACE_STATE_PATH; - } -} - -run().catch((error) => { - console.error(error); - process.exit(1); -}); From 022d39222663329e4b8e177cea3f73adb28e3012 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 18:21:53 -0800 Subject: [PATCH 094/174] update sepolia swap router Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 68 ++++++++++++++++--- .../agents/price-race-swap/test-allowlist.mjs | 7 +- agent/.env.example | 1 + agent/README.md | 2 +- agent/src/lib/config.js | 5 +- agent/src/lib/tx.js | 3 + 6 files changed, 74 insertions(+), 12 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 08a14a31..99dff544 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -5,9 +5,9 @@ const TOKENS = Object.freeze({ }); const ALLOWED_ROUTERS = new Set([ - '0xe592427a0aece92de3edee1f18e0157c05861564', - '0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45', + '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e', ]); +const DEFAULT_ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; const ALLOWED_FEE_TIERS = new Set([500, 3000, 10000]); const inferredTriggersCache = new Map(); @@ -210,9 +210,35 @@ function isMatchingPriceSignal(signal, actionFee, tokenIn, tokenOut) { return Number(signal.poolFee) === actionFee; } +function pickWinningPriceTrigger(signals) { + const triggers = Array.isArray(signals) + ? signals.filter((signal) => signal?.kind === 'priceTrigger') + : []; + if (triggers.length === 0) return null; + const sorted = [...triggers].sort((a, b) => { + const pa = Number(a?.priority ?? Number.MAX_SAFE_INTEGER); + const pb = Number(b?.priority ?? Number.MAX_SAFE_INTEGER); + if (pa !== pb) return pa - pb; + return String(a?.triggerId ?? '').localeCompare(String(b?.triggerId ?? '')); + }); + return sorted[0]; +} + +function pickWethSnapshot(signals) { + if (!Array.isArray(signals)) return null; + for (const signal of signals) { + if (signal?.kind !== 'erc20BalanceSnapshot') continue; + if (String(signal.asset ?? '').toLowerCase() !== TOKENS.WETH) continue; + return signal; + } + return null; +} + async function validateToolCalls({ toolCalls, signals, commitmentText, commitmentSafe }) { const validated = []; const safeAddress = commitmentSafe ? String(commitmentSafe).toLowerCase() : null; + const winningTrigger = pickWinningPriceTrigger(signals); + const wethSnapshot = pickWethSnapshot(signals); for (const call of toolCalls) { if (call.name === 'dispute_assertion') { @@ -238,14 +264,35 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen throw new Error('Only uniswap_v3_exact_input_single is allowed for this agent.'); } - const tokenIn = normalizeAddress(String(action.tokenIn)); - const tokenOut = normalizeAddress(String(action.tokenOut)); - const router = normalizeAddress(String(action.router)); - const recipient = normalizeAddress(String(action.recipient)); - const fee = Number(action.fee); - const amountIn = BigInt(action.amountInWei ?? '0'); + if (!winningTrigger) { + throw new Error('No priceTrigger signal available for this cycle.'); + } + if (!wethSnapshot?.amount) { + throw new Error('No WETH erc20BalanceSnapshot available for this cycle.'); + } + + const inferredTokenOut = + String(winningTrigger.baseToken ?? '').toLowerCase() === TOKENS.WETH + ? String(winningTrigger.quoteToken ?? '').toLowerCase() + : String(winningTrigger.baseToken ?? '').toLowerCase(); + + const tokenIn = normalizeAddress(String(action.tokenIn ?? TOKENS.WETH)); + const tokenOut = normalizeAddress(String(action.tokenOut ?? inferredTokenOut)); + const router = normalizeAddress(String(action.router ?? DEFAULT_ROUTER)); + const recipient = normalizeAddress(String(action.recipient ?? safeAddress)); + const fee = Number(action.fee ?? winningTrigger.poolFee); + const amountIn = BigInt(action.amountInWei ?? String(wethSnapshot.amount)); const amountOutMin = BigInt(action.amountOutMinWei ?? '0'); + action.tokenIn = tokenIn; + action.tokenOut = tokenOut; + action.router = router; + action.recipient = recipient; + action.fee = fee; + action.amountInWei = amountIn.toString(); + action.amountOutMinWei = amountOutMin.toString(); + args.actions[0] = action; + if (tokenIn !== TOKENS.WETH) { throw new Error('Swap tokenIn must be Sepolia WETH.'); } @@ -280,7 +327,10 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen ); } - validated.push(call); + validated.push({ + ...call, + parsedArguments: args, + }); } return validated; diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index 918e9c1b..cb9976e5 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -3,7 +3,7 @@ import { validateToolCalls } from './agent.js'; const WETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'; const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; -const ROUTER = '0xe592427a0aece92de3edee1f18e0157c05861564'; +const ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; async function run() { @@ -36,6 +36,11 @@ async function run() { baseToken: WETH, quoteToken: USDC, }, + { + kind: 'erc20BalanceSnapshot', + asset: WETH, + amount: '30000', + }, ]; const ok = await validateToolCalls({ diff --git a/agent/.env.example b/agent/.env.example index ab59d747..812907da 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -37,6 +37,7 @@ WATCH_NATIVE_BALANCE=true # UNISWAP_V3_FACTORY= # UNISWAP_V3_FEE_TIERS=500,3000,10000 # PROPOSE_ENABLED=true +# ALLOW_PROPOSE_ON_SIMULATION_FAIL=false # DISPUTE_ENABLED=true # START_BLOCK= # DEFAULT_DEPOSIT_ASSET= diff --git a/agent/README.md b/agent/README.md index af4b26f3..59288243 100644 --- a/agent/README.md +++ b/agent/README.md @@ -26,7 +26,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE`, `UNISWAP_V3_FACTORY`, `UNISWAP_V3_FEE_TIERS` - - Optional proposals: `PROPOSE_ENABLED` (default true) + - Optional proposals: `PROPOSE_ENABLED` (default true), `ALLOW_PROPOSE_ON_SIMULATION_FAIL` (default false) - Optional disputes: `DISPUTE_ENABLED` (default true), `DISPUTE_RETRY_MS` (default 60000) - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` 2. Install deps and start the loop: diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index c379667e..1d38365a 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -36,7 +36,10 @@ function buildConfig() { openAiApiKey: process.env.OPENAI_API_KEY, openAiModel: process.env.OPENAI_MODEL ?? 'gpt-4.1-mini', openAiBaseUrl: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1', - allowProposeOnSimulationFail: true, + allowProposeOnSimulationFail: + process.env.ALLOW_PROPOSE_ON_SIMULATION_FAIL === undefined + ? false + : process.env.ALLOW_PROPOSE_ON_SIMULATION_FAIL.toLowerCase() === 'true', proposeGasLimit: process.env.PROPOSE_GAS_LIMIT ? BigInt(process.env.PROPOSE_GAS_LIMIT) : 2_000_000n, diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index f418cd32..18178acc 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -123,6 +123,9 @@ async function postBondAndPropose({ }); } catch (error) { simulationError = error; + const simulationMessage = + error?.shortMessage ?? error?.message ?? summarizeViemError(error)?.message ?? String(error); + console.warn('[agent] Proposal simulation failed:', simulationMessage); if (!config.allowProposeOnSimulationFail) { throw error; } From 8b91e459267156691d877fcc11b32003b5ff912f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 18:23:56 -0800 Subject: [PATCH 095/174] pin validation test to Sepolia swap router address Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 3 +- .../agents/price-race-swap/test-allowlist.mjs | 38 +++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 99dff544..4a92ae2f 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -278,7 +278,7 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen const tokenIn = normalizeAddress(String(action.tokenIn ?? TOKENS.WETH)); const tokenOut = normalizeAddress(String(action.tokenOut ?? inferredTokenOut)); - const router = normalizeAddress(String(action.router ?? DEFAULT_ROUTER)); + const router = DEFAULT_ROUTER; const recipient = normalizeAddress(String(action.recipient ?? safeAddress)); const fee = Number(action.fee ?? winningTrigger.poolFee); const amountIn = BigInt(action.amountInWei ?? String(wethSnapshot.amount)); @@ -356,6 +356,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'First trigger wins. If multiple triggers appear true in one cycle, use signal priority and then lexical triggerId order.', 'Use all currently available WETH in the Safe for the winning branch swap.', 'Build one uniswap_v3_exact_input_single action where amountInWei equals the WETH snapshot amount.', + `Set router to Sepolia Uniswap V3 SwapRouter02 at ${DEFAULT_ROUTER}.`, 'Compute amountOutMinWei using observedPrice and max slippage 0.50% (multiply expected output by 0.995).', 'If tokenIn is the base token of observedPrice, expectedOut ~= amountIn * observedPrice adjusted for token decimals.', 'If tokenIn is the quote token and tokenOut is base, expectedOut ~= amountIn / observedPrice adjusted for token decimals.', diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index cb9976e5..4ceb7fb1 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -51,26 +51,26 @@ async function run() { }); assert.equal(ok.length, 1); - await assert.rejects(() => - validateToolCalls({ - toolCalls: [ - { - ...toolCalls[0], - parsedArguments: { - actions: [ - { - ...toolCalls[0].parsedArguments.actions[0], - router: '0x0000000000000000000000000000000000000001', - }, - ], - }, + const rewritten = await validateToolCalls({ + toolCalls: [ + { + ...toolCalls[0], + parsedArguments: { + actions: [ + { + ...toolCalls[0].parsedArguments.actions[0], + router: '0x0000000000000000000000000000000000000001', + }, + ], }, - ], - signals, - commitmentText: 'y', - commitmentSafe: '0x1234000000000000000000000000000000000000', - }) - ); + }, + ], + signals, + commitmentText: 'y', + commitmentSafe: '0x1234000000000000000000000000000000000000', + }); + assert.equal(rewritten.length, 1); + assert.equal(rewritten[0].parsedArguments.actions[0].router, ROUTER); console.log('[test] allowlist validation OK'); } From f43db74bfca614138728d09c7da7451d60e27ca6 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 18:27:20 -0800 Subject: [PATCH 096/174] add single-fire lock implementation back in Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 27 +++++++++++++++++++ .../agents/price-race-swap/test-allowlist.mjs | 23 +++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 4a92ae2f..cb5b7d9b 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -11,6 +11,10 @@ const DEFAULT_ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; const ALLOWED_FEE_TIERS = new Set([500, 3000, 10000]); const inferredTriggersCache = new Map(); +const singleFireState = { + proposalSubmitted: false, + proposalHash: null, +}; function isHexChar(char) { const code = char.charCodeAt(0); @@ -253,6 +257,9 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen if (call.name !== 'build_og_transactions') { continue; } + if (singleFireState.proposalSubmitted) { + throw new Error('Single-fire lock engaged: a proposal was already submitted.'); + } const args = parseCallArgs(call); if (!args || !Array.isArray(args.actions) || args.actions.length !== 1) { @@ -349,6 +356,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'You are a price-race swap agent for a commitment Safe controlled by an Optimistic Governor.', 'Your own address is provided as agentAddress.', 'Interpret the commitment as a multi-choice race and execute at most one winning branch.', + 'Single-fire mode is enabled: after one successful proposal submission, do not propose again.', 'Use your reasoning over the plain-language commitment and incoming signals. Do not depend on rigid text pattern matching.', 'Treat erc20BalanceSnapshot signals as authoritative current Safe balances for this cycle.', 'If exactly one priceTrigger signal is present in this cycle, treat it as the winning branch for this cycle.', @@ -374,9 +382,28 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { .join(' '); } +function onToolOutput({ name, parsedOutput }) { + if (!name || !parsedOutput || parsedOutput.status !== 'submitted') return; + if (name !== 'post_bond_and_propose' && name !== 'auto_post_bond_and_propose') return; + singleFireState.proposalSubmitted = true; + singleFireState.proposalHash = parsedOutput.proposalHash ?? null; +} + +function getSingleFireState() { + return { ...singleFireState }; +} + +function resetSingleFireState() { + singleFireState.proposalSubmitted = false; + singleFireState.proposalHash = null; +} + export { getPriceTriggers, getSystemPrompt, + getSingleFireState, + onToolOutput, + resetSingleFireState, sanitizeInferredTriggers, validateToolCalls, }; diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index 4ceb7fb1..96034fa7 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { validateToolCalls } from './agent.js'; +import { onToolOutput, resetSingleFireState, validateToolCalls } from './agent.js'; const WETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'; const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; @@ -7,6 +7,8 @@ const ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; async function run() { + resetSingleFireState(); + const toolCalls = [ { name: 'build_og_transactions', @@ -72,6 +74,25 @@ async function run() { assert.equal(rewritten.length, 1); assert.equal(rewritten[0].parsedArguments.actions[0].router, ROUTER); + onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { + status: 'submitted', + proposalHash: '0x1234', + }, + }); + + await assert.rejects( + () => + validateToolCalls({ + toolCalls, + signals, + commitmentText: 'z', + commitmentSafe: '0x1234000000000000000000000000000000000000', + }), + /Single-fire lock engaged/ + ); + console.log('[test] allowlist validation OK'); } From 7e91373f9cfd6e1b1b6dd7581e4a783b8c0d770b Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 18:41:40 -0800 Subject: [PATCH 097/174] codex ignore agent/.env Signed-off-by: John Shutt --- .codexignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codexignore b/.codexignore index ca05dd84..d549b3a2 100644 --- a/.codexignore +++ b/.codexignore @@ -1,3 +1,4 @@ .env .env.* -**/*.env**/*.env.* \ No newline at end of file +**/*.env**/*.env.* +agent/.env \ No newline at end of file From f7242a61050a05af504995f2a462bc122adbdeee Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 19:03:55 -0800 Subject: [PATCH 098/174] try using multiple uniswap quoter options Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 189 +++++++++++++++++- .../agents/price-race-swap/test-allowlist.mjs | 24 +++ agent/.env.example | 1 + agent/README.md | 2 +- agent/src/index.js | 2 + agent/src/lib/config.js | 3 + 6 files changed, 215 insertions(+), 6 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index cb5b7d9b..85f55aef 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -9,6 +9,51 @@ const ALLOWED_ROUTERS = new Set([ ]); const DEFAULT_ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; const ALLOWED_FEE_TIERS = new Set([500, 3000, 10000]); +const QUOTER_CANDIDATES_BY_CHAIN = new Map([ + [1, ['0x61fFE014bA17989E743c5F6cB21bF9697530B21e', '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6']], + [11155111, ['0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3', '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6']], +]); +const quoterV2Abi = [ + { + type: 'function', + name: 'quoteExactInputSingle', + stateMutability: 'nonpayable', + inputs: [ + { + name: 'params', + type: 'tuple', + components: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'fee', type: 'uint24' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' }, + ], + }, + ], + outputs: [ + { name: 'amountOut', type: 'uint256' }, + { name: 'sqrtPriceX96After', type: 'uint160' }, + { name: 'initializedTicksCrossed', type: 'uint32' }, + { name: 'gasEstimate', type: 'uint256' }, + ], + }, +]; +const quoterV1Abi = [ + { + type: 'function', + name: 'quoteExactInputSingle', + stateMutability: 'view', + inputs: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'fee', type: 'uint24' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' }, + ], + outputs: [{ name: 'amountOut', type: 'uint256' }], + }, +]; const inferredTriggersCache = new Map(); const singleFireState = { @@ -238,7 +283,133 @@ function pickWethSnapshot(signals) { return null; } -async function validateToolCalls({ toolCalls, signals, commitmentText, commitmentSafe }) { +async function resolveQuoterCandidates({ publicClient, config }) { + if (!publicClient) { + throw new Error('publicClient is required for Uniswap quoter reads.'); + } + if (config?.uniswapV3Quoter) { + return [normalizeAddress(String(config.uniswapV3Quoter))]; + } + const chainId = await publicClient.getChainId(); + const byChain = QUOTER_CANDIDATES_BY_CHAIN.get(Number(chainId)); + if (!Array.isArray(byChain) || byChain.length === 0) { + throw new Error( + `No Uniswap V3 quoter configured for chainId ${chainId}. Set UNISWAP_V3_QUOTER.` + ); + } + return byChain.map((value) => normalizeAddress(value)); +} + +async function tryQuoteExactInputSingleV2({ + publicClient, + quoter, + tokenIn, + tokenOut, + fee, + amountIn, +}) { + const quoteCall = await publicClient.simulateContract({ + address: quoter, + abi: quoterV2Abi, + functionName: 'quoteExactInputSingle', + args: [ + { + tokenIn, + tokenOut, + fee, + amountIn, + sqrtPriceLimitX96: 0n, + }, + ], + }); + const result = quoteCall?.result; + return Array.isArray(result) && result.length > 0 ? BigInt(result[0]) : BigInt(result ?? 0n); +} + +async function tryQuoteExactInputSingleV1({ + publicClient, + quoter, + tokenIn, + tokenOut, + fee, + amountIn, +}) { + const quoteCall = await publicClient.simulateContract({ + address: quoter, + abi: quoterV1Abi, + functionName: 'quoteExactInputSingle', + args: [tokenIn, tokenOut, fee, amountIn, 0n], + }); + return BigInt(quoteCall?.result ?? 0n); +} + +async function quoteMinOutWithSlippage({ + publicClient, + config, + tokenIn, + tokenOut, + fee, + amountIn, + slippageBps = 50, +}) { + const quoters = await resolveQuoterCandidates({ publicClient, config }); + let quotedAmountOut = 0n; + let selectedQuoter = null; + const failures = []; + + for (const quoter of quoters) { + try { + quotedAmountOut = await tryQuoteExactInputSingleV2({ + publicClient, + quoter, + tokenIn, + tokenOut, + fee, + amountIn, + }); + selectedQuoter = quoter; + break; + } catch (v2Error) { + try { + quotedAmountOut = await tryQuoteExactInputSingleV1({ + publicClient, + quoter, + tokenIn, + tokenOut, + fee, + amountIn, + }); + selectedQuoter = quoter; + break; + } catch (v1Error) { + failures.push( + `${quoter}: ${ + v1Error?.shortMessage ?? + v1Error?.message ?? + v2Error?.shortMessage ?? + v2Error?.message ?? + 'quote failed' + }` + ); + } + } + } + + if (!selectedQuoter) { + throw new Error(`No compatible Uniswap quoter found. Tried: ${failures.join(' | ')}`); + } + + if (quotedAmountOut <= 0n) { + throw new Error('Uniswap quoter returned zero output for this swap.'); + } + const minAmountOut = (quotedAmountOut * BigInt(10_000 - slippageBps)) / 10_000n; + if (minAmountOut <= 0n) { + throw new Error('Swap output is too small after slippage guard; refusing proposal.'); + } + return { quoter: selectedQuoter, quotedAmountOut, minAmountOut }; +} + +async function validateToolCalls({ toolCalls, signals, commitmentText, commitmentSafe, publicClient, config }) { const validated = []; const safeAddress = commitmentSafe ? String(commitmentSafe).toLowerCase() : null; const winningTrigger = pickWinningPriceTrigger(signals); @@ -289,7 +460,16 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen const recipient = normalizeAddress(String(action.recipient ?? safeAddress)); const fee = Number(action.fee ?? winningTrigger.poolFee); const amountIn = BigInt(action.amountInWei ?? String(wethSnapshot.amount)); - const amountOutMin = BigInt(action.amountOutMinWei ?? '0'); + const quoted = await quoteMinOutWithSlippage({ + publicClient, + config, + tokenIn, + tokenOut, + fee, + amountIn, + slippageBps: 50, + }); + const amountOutMin = quoted.minAmountOut; action.tokenIn = tokenIn; action.tokenOut = tokenOut; @@ -365,9 +545,8 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'Use all currently available WETH in the Safe for the winning branch swap.', 'Build one uniswap_v3_exact_input_single action where amountInWei equals the WETH snapshot amount.', `Set router to Sepolia Uniswap V3 SwapRouter02 at ${DEFAULT_ROUTER}.`, - 'Compute amountOutMinWei using observedPrice and max slippage 0.50% (multiply expected output by 0.995).', - 'If tokenIn is the base token of observedPrice, expectedOut ~= amountIn * observedPrice adjusted for token decimals.', - 'If tokenIn is the quote token and tokenOut is base, expectedOut ~= amountIn / observedPrice adjusted for token decimals.', + 'Do not estimate amountOutMinWei from observedPrice.', + 'The runner validates and overwrites amountOutMinWei using Uniswap V3 Quoter with 0.50% slippage protection.', 'Preferred flow: build_og_transactions with one uniswap_v3_exact_input_single action, then rely on runner propose submission.', 'Only use allowlisted Sepolia addresses from the commitment context. Never execute both branches.', 'Use the poolFee from a priceTrigger signal when preparing uniswap_v3_exact_input_single actions.', diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index 96034fa7..d7cc9e8c 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -4,6 +4,7 @@ import { onToolOutput, resetSingleFireState, validateToolCalls } from './agent.j const WETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'; const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; const ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; +const QUOTER = '0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3'; const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; async function run() { @@ -50,8 +51,17 @@ async function run() { signals, commitmentText: 'x', commitmentSafe: '0x1234000000000000000000000000000000000000', + publicClient: { + getChainId: async () => 11155111, + simulateContract: async ({ address }) => { + assert.equal(address.toLowerCase(), QUOTER.toLowerCase()); + return { result: [1000000n, 0n, 0, 0n] }; + }, + }, + config: {}, }); assert.equal(ok.length, 1); + assert.equal(ok[0].parsedArguments.actions[0].amountOutMinWei, '995000'); const rewritten = await validateToolCalls({ toolCalls: [ @@ -70,9 +80,18 @@ async function run() { signals, commitmentText: 'y', commitmentSafe: '0x1234000000000000000000000000000000000000', + publicClient: { + getChainId: async () => 11155111, + simulateContract: async ({ address }) => { + assert.equal(address.toLowerCase(), QUOTER.toLowerCase()); + return { result: [500000n, 0n, 0, 0n] }; + }, + }, + config: {}, }); assert.equal(rewritten.length, 1); assert.equal(rewritten[0].parsedArguments.actions[0].router, ROUTER); + assert.equal(rewritten[0].parsedArguments.actions[0].amountOutMinWei, '497500'); onToolOutput({ name: 'post_bond_and_propose', @@ -89,6 +108,11 @@ async function run() { signals, commitmentText: 'z', commitmentSafe: '0x1234000000000000000000000000000000000000', + publicClient: { + getChainId: async () => 11155111, + simulateContract: async () => ({ result: [1000000n, 0n, 0, 0n] }), + }, + config: {}, }), /Single-fire lock engaged/ ); diff --git a/agent/.env.example b/agent/.env.example index 812907da..7f20b76b 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -35,6 +35,7 @@ POLL_INTERVAL_MS=60000 WATCH_NATIVE_BALANCE=true # Optional Uniswap config overrides (otherwise chain defaults are used) # UNISWAP_V3_FACTORY= +# UNISWAP_V3_QUOTER= # UNISWAP_V3_FEE_TIERS=500,3000,10000 # PROPOSE_ENABLED=true # ALLOW_PROPOSE_ON_SIMULATION_FAIL=false diff --git a/agent/README.md b/agent/README.md index 59288243..041beca4 100644 --- a/agent/README.md +++ b/agent/README.md @@ -25,7 +25,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` (macOS Keychain or Linux Secret Service) - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE`, `UNISWAP_V3_FACTORY`, `UNISWAP_V3_FEE_TIERS` + - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE`, `UNISWAP_V3_FACTORY`, `UNISWAP_V3_QUOTER`, `UNISWAP_V3_FEE_TIERS` - Optional proposals: `PROPOSE_ENABLED` (default true), `ALLOW_PROPOSE_ON_SIMULATION_FAIL` (default false) - Optional disputes: `DISPUTE_ENABLED` (default true), `DISPUTE_RETRY_MS` (default 60000) - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` diff --git a/agent/src/index.js b/agent/src/index.js index 51636032..8c0960e5 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -195,6 +195,8 @@ async function decideOnSignals(signals) { commitmentText, commitmentSafe: config.commitmentSafe, agentAddress, + publicClient, + config, }); if (Array.isArray(validated)) { approvedToolCalls = validated.map((call) => ({ diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index 1d38365a..a1249803 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -60,6 +60,9 @@ function buildConfig() { uniswapV3Factory: process.env.UNISWAP_V3_FACTORY ? getAddress(process.env.UNISWAP_V3_FACTORY) : undefined, + uniswapV3Quoter: process.env.UNISWAP_V3_QUOTER + ? getAddress(process.env.UNISWAP_V3_QUOTER) + : undefined, uniswapV3FeeTiers: parseFeeTierList(process.env.UNISWAP_V3_FEE_TIERS), }; } From ed623bd770bf0d6af1fddc5addfad0c73c36d784 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Mon, 9 Feb 2026 19:07:24 -0800 Subject: [PATCH 099/174] execute pending proposals before making new proposals Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 13 +++++++++- agent/src/index.js | 25 +++++++++++-------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 85f55aef..71319cc0 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -409,7 +409,15 @@ async function quoteMinOutWithSlippage({ return { quoter: selectedQuoter, quotedAmountOut, minAmountOut }; } -async function validateToolCalls({ toolCalls, signals, commitmentText, commitmentSafe, publicClient, config }) { +async function validateToolCalls({ + toolCalls, + signals, + commitmentText, + commitmentSafe, + publicClient, + config, + onchainPendingProposal, +}) { const validated = []; const safeAddress = commitmentSafe ? String(commitmentSafe).toLowerCase() : null; const winningTrigger = pickWinningPriceTrigger(signals); @@ -428,6 +436,9 @@ async function validateToolCalls({ toolCalls, signals, commitmentText, commitmen if (call.name !== 'build_og_transactions') { continue; } + if (onchainPendingProposal) { + throw new Error('Pending proposal exists onchain; execute it before creating a new proposal.'); + } if (singleFireState.proposalSubmitted) { throw new Error('Single-fire lock engaged: a proposal was already submitted.'); } diff --git a/agent/src/index.js b/agent/src/index.js index 8c0960e5..ffeca971 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -140,7 +140,7 @@ async function getActivePriceTriggers({ rulesText }) { return []; } -async function decideOnSignals(signals) { +async function decideOnSignals(signals, { onchainPendingProposal = false } = {}) { if (!config.openAiApiKey) { return false; } @@ -197,6 +197,7 @@ async function decideOnSignals(signals) { agentAddress, publicClient, config, + onchainPendingProposal, }); if (Array.isArray(validated)) { approvedToolCalls = validated.map((call) => ({ @@ -346,6 +347,15 @@ async function agentLoop() { await agentModule.reconcileProposalSubmission({ publicClient }); } + await executeReadyProposals({ + publicClient, + walletClient, + account, + ogModule: config.ogModule, + proposalsByHash, + executeRetryMs: config.executeRetryMs, + }); + const rulesText = ogContext?.rules ?? commitmentText ?? ''; updateTimelockSchedule({ rulesText }); const dueTimelocks = collectDueTimelocks(nowMs); @@ -410,20 +420,13 @@ async function agentLoop() { } if (signalsToProcess.length > 0) { - const decisionOk = await decideOnSignals(signalsToProcess); + const decisionOk = await decideOnSignals(signalsToProcess, { + onchainPendingProposal: proposalsByHash.size > 0, + }); if (decisionOk && dueTimelocks.length > 0) { markTimelocksFired(dueTimelocks); } } - - await executeReadyProposals({ - publicClient, - walletClient, - account, - ogModule: config.ogModule, - proposalsByHash, - executeRetryMs: config.executeRetryMs, - }); } catch (error) { console.error('[agent] loop error', error); } From 95fab4ec5a80a2a98f530dcd3e8912d333ec5973 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:10:18 -0800 Subject: [PATCH 100/174] enforce swapping full balance Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 2 +- agent-library/agents/price-race-swap/test-allowlist.mjs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 71319cc0..0dd85329 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -470,7 +470,7 @@ async function validateToolCalls({ const router = DEFAULT_ROUTER; const recipient = normalizeAddress(String(action.recipient ?? safeAddress)); const fee = Number(action.fee ?? winningTrigger.poolFee); - const amountIn = BigInt(action.amountInWei ?? String(wethSnapshot.amount)); + const amountIn = BigInt(String(wethSnapshot.amount)); const quoted = await quoteMinOutWithSlippage({ publicClient, config, diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index d7cc9e8c..02508e1f 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -61,6 +61,7 @@ async function run() { config: {}, }); assert.equal(ok.length, 1); + assert.equal(ok[0].parsedArguments.actions[0].amountInWei, '30000'); assert.equal(ok[0].parsedArguments.actions[0].amountOutMinWei, '995000'); const rewritten = await validateToolCalls({ From fe46c27dc058efab70d748adc93048ce6bb15a93 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:18:27 -0800 Subject: [PATCH 101/174] track assets with lowercase addresses Signed-off-by: John Shutt --- .../test-tracked-assets-normalization.mjs | 29 +++++++++++++++++++ agent/src/lib/og.js | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 agent/scripts/test-tracked-assets-normalization.mjs diff --git a/agent/scripts/test-tracked-assets-normalization.mjs b/agent/scripts/test-tracked-assets-normalization.mjs new file mode 100644 index 00000000..3a73f307 --- /dev/null +++ b/agent/scripts/test-tracked-assets-normalization.mjs @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import { loadOptimisticGovernorDefaults } from '../src/lib/og.js'; + +const COLLATERAL_CHECKSUM = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; +const COLLATERAL_LOWER = COLLATERAL_CHECKSUM.toLowerCase(); +const OG_MODULE = '0x0000000000000000000000000000000000000001'; + +async function run() { + const trackedAssets = new Set([COLLATERAL_LOWER]); + const publicClient = { + readContract: async () => COLLATERAL_CHECKSUM, + }; + + await loadOptimisticGovernorDefaults({ + publicClient, + ogModule: OG_MODULE, + trackedAssets, + }); + + assert.equal(trackedAssets.size, 1); + assert.equal(trackedAssets.has(COLLATERAL_LOWER), true); + + console.log('[test] tracked asset normalization OK'); +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/agent/src/lib/og.js b/agent/src/lib/og.js index c24d54de..89af0ca3 100644 --- a/agent/src/lib/og.js +++ b/agent/src/lib/og.js @@ -38,7 +38,7 @@ async function loadOptimisticGovernorDefaults({ publicClient, ogModule, trackedA functionName: 'collateral', }); - trackedAssets.add(getAddress(collateral)); + trackedAssets.add(getAddress(collateral).toLowerCase()); } async function loadOgContext({ publicClient, ogModule }) { From c566d776529e5004126c8851f2b71b678d77bd71 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:20:41 -0800 Subject: [PATCH 102/174] prevent delegatecall in uniswap agent Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 6 ++++ .../agents/price-race-swap/test-allowlist.mjs | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 0dd85329..438f10ef 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -469,6 +469,7 @@ async function validateToolCalls({ const tokenOut = normalizeAddress(String(action.tokenOut ?? inferredTokenOut)); const router = DEFAULT_ROUTER; const recipient = normalizeAddress(String(action.recipient ?? safeAddress)); + const operation = action.operation === undefined ? 0 : Number(action.operation); const fee = Number(action.fee ?? winningTrigger.poolFee); const amountIn = BigInt(String(wethSnapshot.amount)); const quoted = await quoteMinOutWithSlippage({ @@ -486,11 +487,16 @@ async function validateToolCalls({ action.tokenOut = tokenOut; action.router = router; action.recipient = recipient; + action.operation = 0; action.fee = fee; action.amountInWei = amountIn.toString(); action.amountOutMinWei = amountOutMin.toString(); args.actions[0] = action; + if (!Number.isInteger(operation) || operation !== 0) { + throw new Error('Swap action operation must be 0 (CALL).'); + } + if (tokenIn !== TOKENS.WETH) { throw new Error('Swap tokenIn must be Sepolia WETH.'); } diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index 02508e1f..4bdfb11f 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -93,6 +93,35 @@ async function run() { assert.equal(rewritten.length, 1); assert.equal(rewritten[0].parsedArguments.actions[0].router, ROUTER); assert.equal(rewritten[0].parsedArguments.actions[0].amountOutMinWei, '497500'); + assert.equal(rewritten[0].parsedArguments.actions[0].operation, 0); + + await assert.rejects( + () => + validateToolCalls({ + toolCalls: [ + { + ...toolCalls[0], + parsedArguments: { + actions: [ + { + ...toolCalls[0].parsedArguments.actions[0], + operation: 1, + }, + ], + }, + }, + ], + signals, + commitmentText: 'op-check', + commitmentSafe: '0x1234000000000000000000000000000000000000', + publicClient: { + getChainId: async () => 11155111, + simulateContract: async () => ({ result: [1000000n, 0n, 0, 0n] }), + }, + config: {}, + }), + /operation must be 0/ + ); onToolOutput({ name: 'post_bond_and_propose', From 27c5530a44ad17dbb7fc9249f1575f0bfb9b2d93 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:31:37 -0800 Subject: [PATCH 103/174] single-fire lock only when proposal hash is not empty (proposal submitted) Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 6 +++++- .../agents/price-race-swap/test-allowlist.mjs | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 438f10ef..d20a5dce 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -581,8 +581,12 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { function onToolOutput({ name, parsedOutput }) { if (!name || !parsedOutput || parsedOutput.status !== 'submitted') return; if (name !== 'post_bond_and_propose' && name !== 'auto_post_bond_and_propose') return; + const proposalHash = typeof parsedOutput.proposalHash === 'string' + ? parsedOutput.proposalHash.trim() + : ''; + if (!proposalHash) return; singleFireState.proposalSubmitted = true; - singleFireState.proposalHash = parsedOutput.proposalHash ?? null; + singleFireState.proposalHash = proposalHash; } function getSingleFireState() { diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index 4bdfb11f..8ab61a90 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -1,5 +1,10 @@ import assert from 'node:assert/strict'; -import { onToolOutput, resetSingleFireState, validateToolCalls } from './agent.js'; +import { + getSingleFireState, + onToolOutput, + resetSingleFireState, + validateToolCalls, +} from './agent.js'; const WETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'; const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; @@ -123,6 +128,15 @@ async function run() { /operation must be 0/ ); + onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { + status: 'submitted', + submissionError: { message: 'failed' }, + }, + }); + assert.equal(getSingleFireState().proposalSubmitted, false); + onToolOutput({ name: 'post_bond_and_propose', parsedOutput: { From 37e67ca3cfeac6dbeb7a6772ad4f67ffc8494c08 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:33:30 -0800 Subject: [PATCH 104/174] only run decision logic when asset balance has changed Signed-off-by: John Shutt --- .../test-polling-balance-transitions.mjs | 73 +++++++++++++++++++ agent/src/index.js | 12 +++ agent/src/lib/polling.js | 7 +- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 agent/scripts/test-polling-balance-transitions.mjs diff --git a/agent/scripts/test-polling-balance-transitions.mjs b/agent/scripts/test-polling-balance-transitions.mjs new file mode 100644 index 00000000..d23fb238 --- /dev/null +++ b/agent/scripts/test-polling-balance-transitions.mjs @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict'; +import { pollCommitmentChanges } from '../src/lib/polling.js'; + +const TOKEN = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; +const SAFE = '0x1234000000000000000000000000000000000000'; + +function createClient({ balances }) { + let block = 100n; + return { + getBlockNumber: async () => { + block += 1n; + return block; + }, + readContract: async () => balances.get(block) ?? 0n, + getLogs: async () => [], + getBalance: async () => 0n, + }; +} + +async function run() { + const balances = new Map([ + [101n, 30000n], + [102n, 30000n], + [103n, 25000n], + ]); + const publicClient = createClient({ balances }); + const trackedAssets = new Set([TOKEN]); + + const first = await pollCommitmentChanges({ + publicClient, + trackedAssets, + commitmentSafe: SAFE, + watchNativeBalance: false, + lastCheckedBlock: undefined, + lastNativeBalance: undefined, + lastAssetBalances: undefined, + }); + const firstSnapshots = first.deposits.filter((s) => s.kind === 'erc20BalanceSnapshot'); + assert.equal(firstSnapshots.length, 1); + assert.equal(BigInt(firstSnapshots[0].amount), 30000n); + + const second = await pollCommitmentChanges({ + publicClient, + trackedAssets, + commitmentSafe: SAFE, + watchNativeBalance: false, + lastCheckedBlock: first.lastCheckedBlock, + lastNativeBalance: first.lastNativeBalance, + lastAssetBalances: first.lastAssetBalances, + }); + const secondSnapshots = second.deposits.filter((s) => s.kind === 'erc20BalanceSnapshot'); + assert.equal(secondSnapshots.length, 0); + + const third = await pollCommitmentChanges({ + publicClient, + trackedAssets, + commitmentSafe: SAFE, + watchNativeBalance: false, + lastCheckedBlock: second.lastCheckedBlock, + lastNativeBalance: second.lastNativeBalance, + lastAssetBalances: second.lastAssetBalances, + }); + const thirdSnapshots = third.deposits.filter((s) => s.kind === 'erc20BalanceSnapshot'); + assert.equal(thirdSnapshots.length, 1); + assert.equal(BigInt(thirdSnapshots[0].amount), 25000n); + + console.log('[test] polling balance transition gating OK'); +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/agent/src/index.js b/agent/src/index.js index ffeca971..cfc682bd 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -70,6 +70,17 @@ async function loadAgentModule() { } const { agentModule, commitmentText } = await loadAgentModule(); +const pollingOptions = (() => { + if (typeof agentModule?.getPollingOptions !== 'function') { + return {}; + } + try { + return agentModule.getPollingOptions({ commitmentText }) ?? {}; + } catch (error) { + console.warn('[agent] getPollingOptions() failed; using defaults.'); + return {}; + } +})(); async function getBlockTimestampMs(blockNumber) { if (!blockNumber) return undefined; @@ -310,6 +321,7 @@ async function agentLoop() { lastCheckedBlock, lastNativeBalance, lastAssetBalances, + emitBalanceSnapshotsEveryPoll: Boolean(pollingOptions.emitBalanceSnapshotsEveryPoll), }); lastCheckedBlock = nextCheckedBlock; lastNativeBalance = nextNative; diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index 3f75f7b1..f358821b 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -53,6 +53,7 @@ async function collectAssetBalanceChangeSignals({ commitmentSafe, blockNumber, lastAssetBalances, + emitBalanceSnapshotsEveryPoll = false, }) { const nextAssetBalances = new Map(lastAssetBalances ?? []); const signals = []; @@ -71,7 +72,9 @@ async function collectAssetBalanceChangeSignals({ const previous = nextAssetBalances.get(asset); nextAssetBalances.set(asset, current); - if (current > 0n) { + const hasChanged = previous === undefined || current !== previous; + const shouldEmit = emitBalanceSnapshotsEveryPoll ? current > 0n : hasChanged; + if (shouldEmit) { signals.push({ kind: 'erc20BalanceSnapshot', asset, @@ -96,6 +99,7 @@ async function pollCommitmentChanges({ lastCheckedBlock, lastNativeBalance, lastAssetBalances, + emitBalanceSnapshotsEveryPoll = false, }) { const latestBlock = await publicClient.getBlockNumber(); if (lastCheckedBlock === undefined) { @@ -204,6 +208,7 @@ async function pollCommitmentChanges({ commitmentSafe, blockNumber: toBlock, lastAssetBalances, + emitBalanceSnapshotsEveryPoll, }); deposits.push(...balanceSignals); From 6b1050d7b215cf78ba01c883ceb5b89251357adb Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:41:11 -0800 Subject: [PATCH 105/174] only apply the winning price trigger Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 13 +++--- .../agents/price-race-swap/test-allowlist.mjs | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index d20a5dce..fc5863bc 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -522,12 +522,15 @@ async function validateToolCalls({ throw new Error('Swap amountOutMinWei must be >= 0.'); } - const hasSignalMatch = Array.isArray(signals) - ? signals.some((signal) => isMatchingPriceSignal(signal, fee, tokenIn, tokenOut)) - : false; - if (!hasSignalMatch) { + const matchesWinningTrigger = isMatchingPriceSignal( + winningTrigger, + fee, + tokenIn, + tokenOut + ); + if (!matchesWinningTrigger) { throw new Error( - 'Swap action does not match a current priceTrigger signal in the current cycle.' + 'Swap action must match the winning priceTrigger signal for this cycle.' ); } diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index 8ab61a90..7f615b4a 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -8,6 +8,7 @@ import { const WETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'; const USDC = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; +const UNI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'; const ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; const QUOTER = '0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3'; const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; @@ -39,6 +40,8 @@ async function run() { const signals = [ { kind: 'priceTrigger', + triggerId: 't1', + priority: 1, pool: POOL, poolFee: 3000, baseToken: WETH, @@ -128,6 +131,46 @@ async function run() { /operation must be 0/ ); + await assert.rejects( + () => + validateToolCalls({ + toolCalls: [ + { + ...toolCalls[0], + parsedArguments: { + actions: [ + { + ...toolCalls[0].parsedArguments.actions[0], + tokenOut: UNI, + fee: 500, + }, + ], + }, + }, + ], + signals: [ + ...signals, + { + kind: 'priceTrigger', + triggerId: 't2', + priority: 2, + pool: '0x287b0e934ed0439e2a7b1d5f0fc25ea2c24b64f7', + poolFee: 500, + baseToken: UNI, + quoteToken: WETH, + }, + ], + commitmentText: 'winner-check', + commitmentSafe: '0x1234000000000000000000000000000000000000', + publicClient: { + getChainId: async () => 11155111, + simulateContract: async () => ({ result: [1000000n, 0n, 0, 0n] }), + }, + config: {}, + }), + /must match the winning priceTrigger/ + ); + onToolOutput({ name: 'post_bond_and_propose', parsedOutput: { From 40bcc303c4a370e7b032f4bc5c06eb87f4cb8187 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:42:12 -0800 Subject: [PATCH 106/174] price-race-swap no longer requires a same-cycle erc20BalanceSnapshot to act Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 44 ++++++++++++++++--- .../agents/price-race-swap/test-allowlist.mjs | 22 ++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index fc5863bc..6c002b55 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -54,6 +54,15 @@ const quoterV1Abi = [ outputs: [{ name: 'amountOut', type: 'uint256' }], }, ]; +const erc20BalanceOfAbi = [ + { + type: 'function', + name: 'balanceOf', + stateMutability: 'view', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ name: '', type: 'uint256' }], + }, +]; const inferredTriggersCache = new Map(); const singleFireState = { @@ -283,6 +292,29 @@ function pickWethSnapshot(signals) { return null; } +async function resolveWethAmount({ + wethSnapshot, + publicClient, + commitmentSafe, +}) { + if (wethSnapshot?.amount !== undefined && wethSnapshot?.amount !== null) { + return BigInt(String(wethSnapshot.amount)); + } + if (!publicClient) { + throw new Error('No WETH balance snapshot and no public client available for fallback read.'); + } + if (!commitmentSafe) { + throw new Error('No WETH balance snapshot and no commitment Safe address for fallback read.'); + } + const current = await publicClient.readContract({ + address: TOKENS.WETH, + abi: erc20BalanceOfAbi, + functionName: 'balanceOf', + args: [commitmentSafe], + }); + return BigInt(current); +} + async function resolveQuoterCandidates({ publicClient, config }) { if (!publicClient) { throw new Error('publicClient is required for Uniswap quoter reads.'); @@ -456,10 +488,6 @@ async function validateToolCalls({ if (!winningTrigger) { throw new Error('No priceTrigger signal available for this cycle.'); } - if (!wethSnapshot?.amount) { - throw new Error('No WETH erc20BalanceSnapshot available for this cycle.'); - } - const inferredTokenOut = String(winningTrigger.baseToken ?? '').toLowerCase() === TOKENS.WETH ? String(winningTrigger.quoteToken ?? '').toLowerCase() @@ -471,7 +499,11 @@ async function validateToolCalls({ const recipient = normalizeAddress(String(action.recipient ?? safeAddress)); const operation = action.operation === undefined ? 0 : Number(action.operation); const fee = Number(action.fee ?? winningTrigger.poolFee); - const amountIn = BigInt(String(wethSnapshot.amount)); + const amountIn = await resolveWethAmount({ + wethSnapshot, + publicClient, + commitmentSafe: safeAddress, + }); const quoted = await quoteMinOutWithSlippage({ publicClient, config, @@ -560,7 +592,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'Use your reasoning over the plain-language commitment and incoming signals. Do not depend on rigid text pattern matching.', 'Treat erc20BalanceSnapshot signals as authoritative current Safe balances for this cycle.', 'If exactly one priceTrigger signal is present in this cycle, treat it as the winning branch for this cycle.', - 'When both a winning priceTrigger and a WETH erc20BalanceSnapshot are present, you have enough information to act.', + 'When a winning priceTrigger is present, use the latest known Safe WETH balance (snapshot if present, otherwise current onchain balance).', 'First trigger wins. If multiple triggers appear true in one cycle, use signal priority and then lexical triggerId order.', 'Use all currently available WETH in the Safe for the winning branch swap.', 'Build one uniswap_v3_exact_input_single action where amountInWei equals the WETH snapshot amount.', diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index 7f615b4a..a8393ee0 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -72,6 +72,28 @@ async function run() { assert.equal(ok[0].parsedArguments.actions[0].amountInWei, '30000'); assert.equal(ok[0].parsedArguments.actions[0].amountOutMinWei, '995000'); + const noSnapshot = await validateToolCalls({ + toolCalls, + signals: [signals[0]], + commitmentText: 'x-fallback', + commitmentSafe: '0x1234000000000000000000000000000000000000', + publicClient: { + getChainId: async () => 11155111, + readContract: async ({ functionName, address }) => { + assert.equal(functionName, 'balanceOf'); + assert.equal(address.toLowerCase(), WETH); + return 30000n; + }, + simulateContract: async ({ address }) => { + assert.equal(address.toLowerCase(), QUOTER.toLowerCase()); + return { result: [1000000n, 0n, 0, 0n] }; + }, + }, + config: {}, + }); + assert.equal(noSnapshot.length, 1); + assert.equal(noSnapshot[0].parsedArguments.actions[0].amountInWei, '30000'); + const rewritten = await validateToolCalls({ toolCalls: [ { From 53c4b9ecbaefe676ef998d83e871610a329630f2 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:53:19 -0800 Subject: [PATCH 107/174] Treat rejected tool calls as no-op, not successful decision Signed-off-by: John Shutt --- agent/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/index.js b/agent/src/index.js index cfc682bd..ec4c683c 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -232,7 +232,7 @@ async function decideOnSignals(signals, { onchainPendingProposal = false } = {}) } if (approvedToolCalls.length === 0) { - return true; + return false; } const toolOutputs = await executeToolCalls({ From 1525909cda00b56a0d7d56b1e2b2486cd9569406 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 08:54:12 -0800 Subject: [PATCH 108/174] add error handling per trigger Signed-off-by: John Shutt --- agent/src/lib/uniswapV3Price.js | 134 ++++++++++++++++---------------- 1 file changed, 66 insertions(+), 68 deletions(-) diff --git a/agent/src/lib/uniswapV3Price.js b/agent/src/lib/uniswapV3Price.js index 47a17539..ebaafb42 100644 --- a/agent/src/lib/uniswapV3Price.js +++ b/agent/src/lib/uniswapV3Price.js @@ -189,93 +189,91 @@ async function collectPriceTriggerSignals({ const evaluations = []; for (const trigger of triggers) { - const baseToken = getAddress(trigger.baseToken); - const quoteToken = getAddress(trigger.quoteToken); - - let resolved; try { - resolved = await resolvePoolForTrigger({ + const baseToken = getAddress(trigger.baseToken); + const quoteToken = getAddress(trigger.quoteToken); + + const resolved = await resolvePoolForTrigger({ publicClient, trigger, config, resolvedPoolCache, }); - } catch (error) { - console.warn(`[agent] Price trigger ${trigger.id} skipped:`, error?.message ?? error); - continue; - } - - const pool = resolved.pool; + const pool = resolved.pool; - const poolMeta = await loadPoolMeta({ - publicClient, - pool, - tokenMetaCache, - poolMetaCache, - }); + const poolMeta = await loadPoolMeta({ + publicClient, + pool, + tokenMetaCache, + poolMetaCache, + }); - const baseIsToken0 = poolMeta.token0 === baseToken && poolMeta.token1 === quoteToken; - const baseIsToken1 = poolMeta.token1 === baseToken && poolMeta.token0 === quoteToken; + const baseIsToken0 = poolMeta.token0 === baseToken && poolMeta.token1 === quoteToken; + const baseIsToken1 = poolMeta.token1 === baseToken && poolMeta.token0 === quoteToken; - if (!baseIsToken0 && !baseIsToken1) { - console.warn( - `[agent] Price trigger ${trigger.id} skipped: pool ${pool} does not match base/quote tokens.` - ); - continue; - } + if (!baseIsToken0 && !baseIsToken1) { + console.warn( + `[agent] Price trigger ${trigger.id} skipped: pool ${pool} does not match base/quote tokens.` + ); + continue; + } - const slot0 = await publicClient.readContract({ - address: pool, - abi: uniswapV3PoolAbi, - functionName: 'slot0', - }); + const slot0 = await publicClient.readContract({ + address: pool, + abi: uniswapV3PoolAbi, + functionName: 'slot0', + }); - const token0Meta = tokenMetaCache.get(poolMeta.token0); - const token1Meta = tokenMetaCache.get(poolMeta.token1); + const token0Meta = tokenMetaCache.get(poolMeta.token0); + const token1Meta = tokenMetaCache.get(poolMeta.token1); - const price = quotePerBaseFromSqrtPriceX96({ - sqrtPriceX96: slot0[0], - token0Decimals: token0Meta.decimals, - token1Decimals: token1Meta.decimals, - baseIsToken0, - }); + const price = quotePerBaseFromSqrtPriceX96({ + sqrtPriceX96: slot0[0], + token0Decimals: token0Meta.decimals, + token1Decimals: token1Meta.decimals, + baseIsToken0, + }); - const matches = evaluateComparator({ - comparator: trigger.comparator, - price, - threshold: trigger.threshold, - }); + const matches = evaluateComparator({ + comparator: trigger.comparator, + price, + threshold: trigger.threshold, + }); - const prior = triggerState.get(trigger.id) ?? { - fired: false, - lastMatched: false, - }; + const prior = triggerState.get(trigger.id) ?? { + fired: false, + lastMatched: false, + }; - const shouldEmit = matches && (!prior.lastMatched || (!trigger.emitOnce && !prior.fired)); + const shouldEmit = matches && (!prior.lastMatched || (!trigger.emitOnce && !prior.fired)); - triggerState.set(trigger.id, { - fired: prior.fired || (matches && trigger.emitOnce), - lastMatched: matches, - }); + triggerState.set(trigger.id, { + fired: prior.fired || (matches && trigger.emitOnce), + lastMatched: matches, + }); - if (!shouldEmit || (trigger.emitOnce && prior.fired)) { + if (!shouldEmit || (trigger.emitOnce && prior.fired)) { + continue; + } + + evaluations.push({ + kind: 'priceTrigger', + triggerId: trigger.id, + triggerLabel: trigger.label, + priority: trigger.priority ?? 0, + pool, + poolFee: poolMeta.fee, + baseToken, + quoteToken, + comparator: trigger.comparator, + threshold: trigger.threshold, + observedPrice: price, + triggerTimestampMs: nowMs, + }); + } catch (error) { + console.warn(`[agent] Price trigger ${trigger.id} skipped:`, error?.message ?? error); continue; } - - evaluations.push({ - kind: 'priceTrigger', - triggerId: trigger.id, - triggerLabel: trigger.label, - priority: trigger.priority ?? 0, - pool, - poolFee: poolMeta.fee, - baseToken, - quoteToken, - comparator: trigger.comparator, - threshold: trigger.threshold, - observedPrice: price, - triggerTimestampMs: nowMs, - }); } evaluations.sort((a, b) => { From c4c2ed6f53d62daf289f66b18228a3a9101efe9a Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 09:25:44 -0800 Subject: [PATCH 109/174] separated balance snapshots from the deposit stream so timelock anchors are no longer polluted by snapshot signals Signed-off-by: John Shutt --- .../scripts/test-polling-balance-transitions.mjs | 9 ++++++--- agent/src/index.js | 2 ++ agent/src/lib/polling.js | 15 +++++++++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/agent/scripts/test-polling-balance-transitions.mjs b/agent/scripts/test-polling-balance-transitions.mjs index d23fb238..b89bf971 100644 --- a/agent/scripts/test-polling-balance-transitions.mjs +++ b/agent/scripts/test-polling-balance-transitions.mjs @@ -35,7 +35,8 @@ async function run() { lastNativeBalance: undefined, lastAssetBalances: undefined, }); - const firstSnapshots = first.deposits.filter((s) => s.kind === 'erc20BalanceSnapshot'); + const firstSnapshots = first.balanceSnapshots.filter((s) => s.kind === 'erc20BalanceSnapshot'); + assert.equal(first.deposits.length, 0); assert.equal(firstSnapshots.length, 1); assert.equal(BigInt(firstSnapshots[0].amount), 30000n); @@ -48,7 +49,8 @@ async function run() { lastNativeBalance: first.lastNativeBalance, lastAssetBalances: first.lastAssetBalances, }); - const secondSnapshots = second.deposits.filter((s) => s.kind === 'erc20BalanceSnapshot'); + const secondSnapshots = second.balanceSnapshots.filter((s) => s.kind === 'erc20BalanceSnapshot'); + assert.equal(second.deposits.length, 0); assert.equal(secondSnapshots.length, 0); const third = await pollCommitmentChanges({ @@ -60,7 +62,8 @@ async function run() { lastNativeBalance: second.lastNativeBalance, lastAssetBalances: second.lastAssetBalances, }); - const thirdSnapshots = third.deposits.filter((s) => s.kind === 'erc20BalanceSnapshot'); + const thirdSnapshots = third.balanceSnapshots.filter((s) => s.kind === 'erc20BalanceSnapshot'); + assert.equal(third.deposits.length, 0); assert.equal(thirdSnapshots.length, 1); assert.equal(BigInt(thirdSnapshots[0].amount), 25000n); diff --git a/agent/src/index.js b/agent/src/index.js index ec4c683c..06c3ec80 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -309,6 +309,7 @@ async function agentLoop() { const { deposits, + balanceSnapshots, lastCheckedBlock: nextCheckedBlock, lastNativeBalance: nextNative, lastAssetBalances: nextAssetBalances, @@ -384,6 +385,7 @@ async function agentLoop() { }); const combinedSignals = deposits.concat( + balanceSnapshots, newProposals.map((proposal) => ({ kind: 'proposal', proposalHash: proposal.proposalHash, diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index f358821b..3d5d84ba 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -123,7 +123,8 @@ async function pollCommitmentChanges({ ); } return { - deposits: initialAssetSignals, + deposits: [], + balanceSnapshots: initialAssetSignals, lastCheckedBlock: latestBlock, lastNativeBalance: nextNativeBalance, lastAssetBalances: @@ -135,7 +136,13 @@ async function pollCommitmentChanges({ } if (latestBlock <= lastCheckedBlock) { - return { deposits: [], lastCheckedBlock, lastNativeBalance, lastAssetBalances }; + return { + deposits: [], + balanceSnapshots: [], + lastCheckedBlock, + lastNativeBalance, + lastAssetBalances, + }; } const fromBlock = lastCheckedBlock + 1n; @@ -202,7 +209,7 @@ async function pollCommitmentChanges({ nextNativeBalance = nativeBalance; } - const { signals: balanceSignals, nextAssetBalances } = await collectAssetBalanceChangeSignals({ + const { signals: balanceSnapshots, nextAssetBalances } = await collectAssetBalanceChangeSignals({ publicClient, trackedAssets, commitmentSafe, @@ -210,10 +217,10 @@ async function pollCommitmentChanges({ lastAssetBalances, emitBalanceSnapshotsEveryPoll, }); - deposits.push(...balanceSignals); return { deposits, + balanceSnapshots, lastCheckedBlock: toBlock, lastNativeBalance: nextNativeBalance, lastAssetBalances: nextAssetBalances, From 93d3fca745be70fde16dcd5f72019848cdb7c99f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 09:29:27 -0800 Subject: [PATCH 110/174] fix false zero-balance snapshots after startup and misleading execution log classification Signed-off-by: John Shutt --- .../test-polling-balance-transitions.mjs | 23 +++++++++++-------- agent/src/lib/polling.js | 20 +++++++++------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/agent/scripts/test-polling-balance-transitions.mjs b/agent/scripts/test-polling-balance-transitions.mjs index b89bf971..966f93fd 100644 --- a/agent/scripts/test-polling-balance-transitions.mjs +++ b/agent/scripts/test-polling-balance-transitions.mjs @@ -2,29 +2,34 @@ import assert from 'node:assert/strict'; import { pollCommitmentChanges } from '../src/lib/polling.js'; const TOKEN = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; +const TOKEN_ZERO = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'; const SAFE = '0x1234000000000000000000000000000000000000'; -function createClient({ balances }) { +function createClient({ getTokenBalance }) { let block = 100n; return { getBlockNumber: async () => { block += 1n; return block; }, - readContract: async () => balances.get(block) ?? 0n, + readContract: async ({ address }) => getTokenBalance({ block, address }), getLogs: async () => [], getBalance: async () => 0n, }; } async function run() { - const balances = new Map([ - [101n, 30000n], - [102n, 30000n], - [103n, 25000n], - ]); - const publicClient = createClient({ balances }); - const trackedAssets = new Set([TOKEN]); + const publicClient = createClient({ + getTokenBalance: ({ block, address }) => { + const token = String(address).toLowerCase(); + if (token === TOKEN_ZERO) return 0n; + if (block === 101n) return 30000n; + if (block === 102n) return 30000n; + if (block === 103n) return 25000n; + return 0n; + }, + }); + const trackedAssets = new Set([TOKEN, TOKEN_ZERO]); const first = await pollCommitmentChanges({ publicClient, diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index 3d5d84ba..e2d0d9ef 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -33,7 +33,7 @@ async function primeAssetBalanceSignals({ publicClient, trackedAssets, commitmen }) ); - return balances + const signals = balances .filter((item) => item.balance > 0n) .map((item) => ({ kind: 'erc20BalanceSnapshot', @@ -45,6 +45,9 @@ async function primeAssetBalanceSignals({ publicClient, trackedAssets, commitmen logIndex: undefined, id: `snapshot:${item.asset}:${blockNumber.toString()}`, })); + + const balanceMap = new Map(balances.map((item) => [item.asset, item.balance])); + return { signals, balanceMap }; } async function collectAssetBalanceChangeSignals({ @@ -109,12 +112,13 @@ async function pollCommitmentChanges({ watchNativeBalance, blockNumber: latestBlock, }); - const initialAssetSignals = await primeAssetBalanceSignals({ + const { signals: initialAssetSignals, balanceMap: initialAssetBalanceMap } = + await primeAssetBalanceSignals({ publicClient, trackedAssets, commitmentSafe, blockNumber: latestBlock, - }); + }); if (initialAssetSignals.length > 0) { console.log( `[agent] Startup balance snapshot signals: ${initialAssetSignals @@ -128,10 +132,7 @@ async function pollCommitmentChanges({ lastCheckedBlock: latestBlock, lastNativeBalance: nextNativeBalance, lastAssetBalances: - lastAssetBalances ?? - new Map( - initialAssetSignals.map((signal) => [signal.asset, BigInt(signal.amount)]) - ), + lastAssetBalances ?? initialAssetBalanceMap, }; } @@ -405,7 +406,10 @@ async function executeReadyProposals({ account: account.address, }); } catch (error) { - console.warn('[agent] Proposal not executable yet:', proposal.proposalHash); + const reason = error?.shortMessage ?? error?.message ?? String(error); + console.warn( + `[agent] Proposal execution simulation failed for ${proposal.proposalHash}: ${reason}` + ); continue; } From 55cf65f8f367b6b5bdbd3e0759b594402dd85564 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 09:33:37 -0800 Subject: [PATCH 111/174] additional fixes for price race swap agent Signed-off-by: John Shutt --- agent-library/agents/price-race-swap/agent.js | 131 +++++++++++++++++- .../agents/price-race-swap/test-allowlist.mjs | 31 ++++- 2 files changed, 153 insertions(+), 9 deletions(-) diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 6c002b55..69671b09 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -1,3 +1,10 @@ +import { readFile, unlink, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + const TOKENS = Object.freeze({ WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', USDC: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238', @@ -63,12 +70,108 @@ const erc20BalanceOfAbi = [ outputs: [{ name: '', type: 'uint256' }], }, ]; +const proposalExecutedEvent = { + type: 'event', + name: 'ProposalExecuted', + inputs: [ + { indexed: true, name: 'proposalHash', type: 'bytes32' }, + { indexed: true, name: 'assertionId', type: 'bytes32' }, + ], + anonymous: false, +}; const inferredTriggersCache = new Map(); const singleFireState = { proposalSubmitted: false, proposalHash: null, }; +let singleFireStateHydrated = false; +let singleFireReconciledOnchain = false; + +function getSingleFireStatePath() { + const fromEnv = process.env.PRICE_RACE_SWAP_STATE_FILE; + if (fromEnv && String(fromEnv).trim().length > 0) { + return path.resolve(String(fromEnv).trim()); + } + return path.join(__dirname, '.single-fire-state.json'); +} + +function normalizeHash(value) { + if (typeof value !== 'string') return null; + const v = value.trim(); + if (!/^0x[0-9a-fA-F]{64}$/.test(v)) return null; + return v.toLowerCase(); +} + +async function persistSingleFireState() { + const payload = JSON.stringify( + { + proposalSubmitted: singleFireState.proposalSubmitted, + proposalHash: singleFireState.proposalHash, + }, + null, + 2 + ); + await writeFile(getSingleFireStatePath(), payload, 'utf8'); +} + +async function hydrateSingleFireState() { + if (singleFireStateHydrated) return; + singleFireStateHydrated = true; + try { + const raw = await readFile(getSingleFireStatePath(), 'utf8'); + const parsed = JSON.parse(raw); + singleFireState.proposalSubmitted = Boolean(parsed?.proposalSubmitted); + singleFireState.proposalHash = normalizeHash(parsed?.proposalHash) ?? null; + } catch (error) { + // Missing/corrupt state file means unlocked unless chain reconciliation proves otherwise. + } +} + +async function lockSingleFire({ proposalHash = null } = {}) { + singleFireState.proposalSubmitted = true; + singleFireState.proposalHash = normalizeHash(proposalHash) ?? singleFireState.proposalHash; + await persistSingleFireState(); +} + +async function reconcileSingleFireFromChain({ publicClient }) { + if (singleFireReconciledOnchain) return; + singleFireReconciledOnchain = true; + if (!publicClient || singleFireState.proposalSubmitted) return; + + const rawOgModule = process.env.OG_MODULE; + if (!rawOgModule) return; + + let ogModule; + try { + ogModule = normalizeAddress(String(rawOgModule)); + } catch (error) { + return; + } + + const latestBlock = await publicClient.getBlockNumber(); + const configuredStart = process.env.START_BLOCK ? BigInt(process.env.START_BLOCK) : 0n; + const chunkSize = 50_000n; + let currentTo = latestBlock; + + while (currentTo >= configuredStart) { + const minFrom = currentTo >= chunkSize - 1n ? currentTo - chunkSize + 1n : 0n; + const currentFrom = minFrom < configuredStart ? configuredStart : minFrom; + const logs = await publicClient.getLogs({ + address: ogModule, + event: proposalExecutedEvent, + fromBlock: currentFrom, + toBlock: currentTo, + }); + if (logs.length > 0) { + const hash = normalizeHash(logs[logs.length - 1]?.args?.proposalHash); + await lockSingleFire({ proposalHash: hash }); + return; + } + if (currentFrom === configuredStart) break; + currentTo = currentFrom - 1n; + } +} function isHexChar(char) { const code = char.charCodeAt(0); @@ -450,6 +553,9 @@ async function validateToolCalls({ config, onchainPendingProposal, }) { + await hydrateSingleFireState(); + await reconcileSingleFireFromChain({ publicClient }); + const validated = []; const safeAddress = commitmentSafe ? String(commitmentSafe).toLowerCase() : null; const winningTrigger = pickWinningPriceTrigger(signals); @@ -613,15 +719,18 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { .join(' '); } -function onToolOutput({ name, parsedOutput }) { +async function onToolOutput({ name, parsedOutput }) { if (!name || !parsedOutput || parsedOutput.status !== 'submitted') return; if (name !== 'post_bond_and_propose' && name !== 'auto_post_bond_and_propose') return; - const proposalHash = typeof parsedOutput.proposalHash === 'string' - ? parsedOutput.proposalHash.trim() - : ''; + const proposalHash = normalizeHash(parsedOutput.proposalHash); if (!proposalHash) return; - singleFireState.proposalSubmitted = true; - singleFireState.proposalHash = proposalHash; + await lockSingleFire({ proposalHash }); +} + +function onProposalEvents({ executedProposalCount = 0 }) { + if (executedProposalCount > 0) { + void lockSingleFire(); + } } function getSingleFireState() { @@ -631,6 +740,14 @@ function getSingleFireState() { function resetSingleFireState() { singleFireState.proposalSubmitted = false; singleFireState.proposalHash = null; + singleFireStateHydrated = true; + singleFireReconciledOnchain = false; + void unlink(getSingleFireStatePath()).catch(() => {}); +} + +async function reconcileProposalSubmission({ publicClient }) { + await hydrateSingleFireState(); + await reconcileSingleFireFromChain({ publicClient }); } export { @@ -638,6 +755,8 @@ export { getSystemPrompt, getSingleFireState, onToolOutput, + onProposalEvents, + reconcileProposalSubmission, resetSingleFireState, sanitizeInferredTriggers, validateToolCalls, diff --git a/agent-library/agents/price-race-swap/test-allowlist.mjs b/agent-library/agents/price-race-swap/test-allowlist.mjs index a8393ee0..c7ffffa6 100644 --- a/agent-library/agents/price-race-swap/test-allowlist.mjs +++ b/agent-library/agents/price-race-swap/test-allowlist.mjs @@ -1,7 +1,9 @@ import assert from 'node:assert/strict'; import { getSingleFireState, + onProposalEvents, onToolOutput, + reconcileProposalSubmission, resetSingleFireState, validateToolCalls, } from './agent.js'; @@ -14,6 +16,7 @@ const QUOTER = '0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3'; const POOL = '0x6418eec70f50913ff0d756b48d32ce7c02b47c47'; async function run() { + process.env.PRICE_RACE_SWAP_STATE_FILE = '/tmp/price-race-swap-state-test.json'; resetSingleFireState(); const toolCalls = [ @@ -193,7 +196,7 @@ async function run() { /must match the winning priceTrigger/ ); - onToolOutput({ + await onToolOutput({ name: 'post_bond_and_propose', parsedOutput: { status: 'submitted', @@ -202,11 +205,11 @@ async function run() { }); assert.equal(getSingleFireState().proposalSubmitted, false); - onToolOutput({ + await onToolOutput({ name: 'post_bond_and_propose', parsedOutput: { status: 'submitted', - proposalHash: '0x1234', + proposalHash: '0x1234000000000000000000000000000000000000000000000000000000000000', }, }); @@ -226,6 +229,28 @@ async function run() { /Single-fire lock engaged/ ); + resetSingleFireState(); + process.env.OG_MODULE = '0x1234000000000000000000000000000000000000'; + await reconcileProposalSubmission({ + publicClient: { + getBlockNumber: async () => 100n, + getLogs: async () => [ + { + args: { + proposalHash: + '0xabcd000000000000000000000000000000000000000000000000000000000000', + }, + }, + ], + }, + }); + assert.equal(getSingleFireState().proposalSubmitted, true); + + resetSingleFireState(); + onProposalEvents({ executedProposalCount: 1 }); + await new Promise((resolve) => setTimeout(resolve, 0)); + assert.equal(getSingleFireState().proposalSubmitted, true); + console.log('[test] allowlist validation OK'); } From 0f0a57943e5a5c169d9442216ad3e7737a0cbb18 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 09:45:49 -0800 Subject: [PATCH 112/174] collectAssetBalanceChangeSignals now skips zero-balance snapshots on first observation Signed-off-by: John Shutt --- agent/scripts/test-polling-balance-transitions.mjs | 3 +++ agent/src/lib/polling.js | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/agent/scripts/test-polling-balance-transitions.mjs b/agent/scripts/test-polling-balance-transitions.mjs index 966f93fd..48bba257 100644 --- a/agent/scripts/test-polling-balance-transitions.mjs +++ b/agent/scripts/test-polling-balance-transitions.mjs @@ -3,6 +3,7 @@ import { pollCommitmentChanges } from '../src/lib/polling.js'; const TOKEN = '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238'; const TOKEN_ZERO = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'; +const TOKEN_NEW_ZERO = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'; const SAFE = '0x1234000000000000000000000000000000000000'; function createClient({ getTokenBalance }) { @@ -23,6 +24,7 @@ async function run() { getTokenBalance: ({ block, address }) => { const token = String(address).toLowerCase(); if (token === TOKEN_ZERO) return 0n; + if (token === TOKEN_NEW_ZERO) return 0n; if (block === 101n) return 30000n; if (block === 102n) return 30000n; if (block === 103n) return 25000n; @@ -45,6 +47,7 @@ async function run() { assert.equal(firstSnapshots.length, 1); assert.equal(BigInt(firstSnapshots[0].amount), 30000n); + trackedAssets.add(TOKEN_NEW_ZERO); const second = await pollCommitmentChanges({ publicClient, trackedAssets, diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index e2d0d9ef..e3837ffc 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -75,8 +75,11 @@ async function collectAssetBalanceChangeSignals({ const previous = nextAssetBalances.get(asset); nextAssetBalances.set(asset, current); - const hasChanged = previous === undefined || current !== previous; - const shouldEmit = emitBalanceSnapshotsEveryPoll ? current > 0n : hasChanged; + const hasChanged = previous !== undefined && current !== previous; + const isFirstObservationNonZero = previous === undefined && current > 0n; + const shouldEmit = emitBalanceSnapshotsEveryPoll + ? current > 0n + : hasChanged || isFirstObservationNonZero; if (shouldEmit) { signals.push({ kind: 'erc20BalanceSnapshot', From 51e007fc8130c8b13fc90bb40915c6ccc862c5e8 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 10:02:16 -0800 Subject: [PATCH 113/174] malformed-trigger handling in agent/src/lib/uniswapV3Price.js Signed-off-by: John Shutt --- agent/src/lib/uniswapV3Price.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/agent/src/lib/uniswapV3Price.js b/agent/src/lib/uniswapV3Price.js index ebaafb42..a902cead 100644 --- a/agent/src/lib/uniswapV3Price.js +++ b/agent/src/lib/uniswapV3Price.js @@ -189,6 +189,13 @@ async function collectPriceTriggerSignals({ const evaluations = []; for (const trigger of triggers) { + const triggerId = trigger && typeof trigger === 'object' && trigger.id !== undefined + ? String(trigger.id) + : 'unknown-trigger'; + if (!trigger || typeof trigger !== 'object') { + console.warn(`[agent] Price trigger ${triggerId} skipped: malformed trigger entry.`); + continue; + } try { const baseToken = getAddress(trigger.baseToken); const quoteToken = getAddress(trigger.quoteToken); @@ -271,7 +278,7 @@ async function collectPriceTriggerSignals({ triggerTimestampMs: nowMs, }); } catch (error) { - console.warn(`[agent] Price trigger ${trigger.id} skipped:`, error?.message ?? error); + console.warn(`[agent] Price trigger ${triggerId} skipped:`, error?.message ?? error); continue; } } From 8c88dc41e6d729e51a923ef765dbec0c42fba9e1 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 10:39:41 -0800 Subject: [PATCH 114/174] add polymarket shared library and support in the shared agent files, including offchain clob and onchain ctf actions Signed-off-by: John Shutt --- agent/src/index.js | 4 +- agent/src/lib/config.js | 12 ++ agent/src/lib/polymarket.js | 129 +++++++++++++++++++++ agent/src/lib/tools.js | 217 +++++++++++++++++++++++++++++++++--- agent/src/lib/tx.js | 98 +++++++++++++++- 5 files changed, 445 insertions(+), 15 deletions(-) create mode 100644 agent/src/lib/polymarket.js diff --git a/agent/src/index.js b/agent/src/index.js index d3a3c229..e6136165 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -134,8 +134,10 @@ async function decideOnSignals(signals) { const tools = toolDefinitions({ proposeEnabled: config.proposeEnabled, disputeEnabled: config.disputeEnabled, + clobEnabled: config.polymarketClobEnabled, }); - const allowTools = config.proposeEnabled || config.disputeEnabled; + const allowTools = + config.proposeEnabled || config.disputeEnabled || config.polymarketClobEnabled; const decision = await callAgent({ config, systemPrompt, diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index 2b7e3490..32da5593 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -41,6 +41,18 @@ function buildConfig() { chainlinkPriceFeed: process.env.CHAINLINK_PRICE_FEED ? getAddress(process.env.CHAINLINK_PRICE_FEED) : undefined, + polymarketChainId: Number(process.env.POLYMARKET_CHAIN_ID ?? 137), + polymarketConditionalTokens: process.env.POLYMARKET_CONDITIONAL_TOKENS + ? getAddress(process.env.POLYMARKET_CONDITIONAL_TOKENS) + : getAddress('0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'), + polymarketClobEnabled: + process.env.POLYMARKET_CLOB_ENABLED === undefined + ? true + : process.env.POLYMARKET_CLOB_ENABLED.toLowerCase() !== 'false', + polymarketClobHost: process.env.POLYMARKET_CLOB_HOST ?? 'https://clob.polymarket.com', + polymarketClobApiKey: process.env.POLYMARKET_CLOB_API_KEY, + polymarketClobApiSecret: process.env.POLYMARKET_CLOB_API_SECRET, + polymarketClobApiPassphrase: process.env.POLYMARKET_CLOB_API_PASSPHRASE, }; } diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js new file mode 100644 index 00000000..ec96a528 --- /dev/null +++ b/agent/src/lib/polymarket.js @@ -0,0 +1,129 @@ +const DEFAULT_CLOB_HOST = 'https://clob.polymarket.com'; + +function normalizeClobHost(host) { + return (host ?? DEFAULT_CLOB_HOST).replace(/\/+$/, ''); +} + +function buildClobAuthHeaders(config) { + const apiKey = config.polymarketClobApiKey; + const apiSecret = config.polymarketClobApiSecret; + const apiPassphrase = config.polymarketClobApiPassphrase; + if (!apiKey || !apiSecret || !apiPassphrase) { + throw new Error( + 'Missing CLOB credentials. Set POLYMARKET_CLOB_API_KEY, POLYMARKET_CLOB_API_SECRET, and POLYMARKET_CLOB_API_PASSPHRASE.' + ); + } + + return { + 'POLY_API_KEY': apiKey, + 'POLY_SIGNATURE': apiSecret, + 'POLY_PASSPHRASE': apiPassphrase, + }; +} + +async function clobRequest({ + config, + path, + method, + body, +}) { + const host = normalizeClobHost(config.polymarketClobHost); + const headers = { + 'Content-Type': 'application/json', + ...buildClobAuthHeaders(config), + }; + const response = await fetch(`${host}${path}`, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + }); + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : null; + } catch (error) { + parsed = { raw: text }; + } + + if (!response.ok) { + throw new Error( + `CLOB request failed (${method} ${path}): ${response.status} ${response.statusText} ${text}` + ); + } + + return parsed; +} + +async function placeClobOrder({ + config, + signedOrder, + owner, + orderType, +}) { + if (!signedOrder || typeof signedOrder !== 'object') { + throw new Error('signedOrder is required and must be an object.'); + } + if (!owner) { + throw new Error('owner is required.'); + } + if (!orderType) { + throw new Error('orderType is required.'); + } + + return clobRequest({ + config, + method: 'POST', + path: '/order', + body: { + order: signedOrder, + owner, + orderType, + }, + }); +} + +async function cancelClobOrders({ + config, + mode, + orderIds, + market, + assetId, +}) { + if (mode === 'all') { + return clobRequest({ + config, + method: 'DELETE', + path: '/cancel-all', + }); + } + + if (mode === 'market') { + if (!market && !assetId) { + throw new Error('cancel mode=market requires market or assetId.'); + } + return clobRequest({ + config, + method: 'DELETE', + path: '/cancel-market-orders', + body: { + market, + asset_id: assetId, + }, + }); + } + + if (!Array.isArray(orderIds) || orderIds.length === 0) { + throw new Error('cancel mode=ids requires non-empty orderIds.'); + } + + return clobRequest({ + config, + method: 'DELETE', + path: '/orders', + body: { + orderIDs: orderIds, + }, + }); +} + +export { cancelClobOrders, placeClobOrder }; diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 26c7f594..a3482302 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -1,12 +1,13 @@ import { getAddress } from 'viem'; import { buildOgTransactions, makeDeposit, postBondAndDispute, postBondAndPropose } from './tx.js'; +import { cancelClobOrders, placeClobOrder } from './polymarket.js'; import { parseToolArguments } from './utils.js'; function safeStringify(value) { return JSON.stringify(value, (_, item) => (typeof item === 'bigint' ? item.toString() : item)); } -function toolDefinitions({ proposeEnabled, disputeEnabled }) { +function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { const tools = [ { type: 'function', @@ -27,7 +28,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { kind: { type: 'string', description: - 'Action type: erc20_transfer | native_transfer | contract_call', + 'Action type: erc20_transfer | native_transfer | contract_call | ctf_split | ctf_merge | ctf_redeem', }, token: { type: ['string', 'null'], @@ -82,17 +83,47 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { description: 'Safe operation (0=CALL,1=DELEGATECALL). Defaults to 0.', }, + chainId: { + type: ['integer', 'null'], + description: 'Chain id for CTF actions (defaults to POLYMARKET_CHAIN_ID).', + }, + ctfContract: { + type: ['string', 'null'], + description: + 'ConditionalTokens contract address override for CTF actions.', + }, + collateralToken: { + type: ['string', 'null'], + description: 'Collateral token address for CTF actions.', + }, + conditionId: { + type: ['string', 'null'], + description: 'Condition id bytes32 for CTF actions.', + }, + parentCollectionId: { + type: ['string', 'null'], + description: + 'Parent collection id bytes32 for CTF actions (default zero bytes32).', + }, + partition: { + type: ['array', 'null'], + description: + 'Index partition for ctf_split/ctf_merge. Defaults to [1,2].', + items: { type: 'integer' }, + }, + amount: { + type: ['string', 'null'], + description: + 'Collateral/full-set amount in base units for ctf_split/ctf_merge.', + }, + indexSets: { + type: ['array', 'null'], + description: + 'Index sets for ctf_redeem. Defaults to [1,2].', + items: { type: 'integer' }, + }, }, - required: [ - 'kind', - 'token', - 'to', - 'amountWei', - 'valueWei', - 'abi', - 'args', - 'operation', - ], + required: ['kind'], }, }, }, @@ -181,6 +212,77 @@ function toolDefinitions({ proposeEnabled, disputeEnabled }) { }); } + if (clobEnabled) { + tools.push( + { + type: 'function', + name: 'polymarket_clob_place_order', + description: + 'Submit a signed Polymarket CLOB order (BUY or SELL) to the CLOB API.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + owner: { + type: 'string', + description: 'EOA owner address associated with the signed order.', + }, + side: { + type: 'string', + description: 'BUY or SELL.', + }, + tokenId: { + type: 'string', + description: 'Polymarket token id for the order.', + }, + orderType: { + type: 'string', + description: 'Order type, e.g. GTC, GTD, FOK, or FAK.', + }, + signedOrder: { + type: 'object', + description: + 'Signed order payload expected by the CLOB API /order endpoint.', + }, + }, + required: ['owner', 'side', 'tokenId', 'orderType', 'signedOrder'], + }, + }, + { + type: 'function', + name: 'polymarket_clob_cancel_orders', + description: + 'Cancel Polymarket CLOB orders by ids, by market, or cancel all open orders.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + mode: { + type: 'string', + description: 'ids | market | all', + }, + orderIds: { + type: ['array', 'null'], + items: { type: 'string' }, + description: 'Order ids required when mode=ids.', + }, + market: { + type: ['string', 'null'], + description: 'Market id used when mode=market.', + }, + assetId: { + type: ['string', 'null'], + description: 'Optional asset id used when mode=market.', + }, + }, + required: ['mode', 'orderIds', 'market', 'assetId'], + }, + } + ); + } + return tools; } @@ -205,7 +307,7 @@ async function executeToolCalls({ if (call.name === 'build_og_transactions') { try { - const transactions = buildOgTransactions(args.actions ?? []); + const transactions = buildOgTransactions(args.actions ?? [], { config }); builtTransactions = transactions; outputs.push({ callId: call.callId, @@ -225,6 +327,95 @@ async function executeToolCalls({ continue; } + if (call.name === 'polymarket_clob_place_order') { + if (!config.polymarketClobEnabled) { + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'skipped', + reason: 'polymarket CLOB disabled', + }), + }); + continue; + } + + try { + if (args.side !== 'BUY' && args.side !== 'SELL') { + throw new Error('side must be BUY or SELL'); + } + if (!args.tokenId) { + throw new Error('tokenId is required'); + } + const result = await placeClobOrder({ + config, + signedOrder: args.signedOrder, + owner: args.owner, + orderType: args.orderType, + }); + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'submitted', + result, + }), + }); + } catch (error) { + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'error', + message: error?.message ?? String(error), + }), + }); + } + continue; + } + + if (call.name === 'polymarket_clob_cancel_orders') { + if (!config.polymarketClobEnabled) { + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'skipped', + reason: 'polymarket CLOB disabled', + }), + }); + continue; + } + + try { + const result = await cancelClobOrders({ + config, + mode: args.mode, + orderIds: args.orderIds, + market: args.market, + assetId: args.assetId, + }); + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'submitted', + result, + }), + }); + } catch (error) { + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'error', + message: error?.message ?? String(error), + }), + }); + } + continue; + } + if (call.name === 'make_deposit') { const txHash = await makeDeposit({ walletClient, diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index d3e1399e..57ca5c7b 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -10,6 +10,14 @@ import { optimisticGovernorAbi, optimisticOracleAbi } from './og.js'; import { normalizeAssertion } from './og.js'; import { summarizeViemError } from './utils.js'; +const conditionalTokensAbi = parseAbi([ + 'function splitPosition(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount)', + 'function mergePositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount)', + 'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)', +]); + +const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; + async function postBondAndPropose({ publicClient, walletClient, @@ -299,11 +307,13 @@ function normalizeOgTransactions(transactions) { }); } -function buildOgTransactions(actions) { +function buildOgTransactions(actions, options = {}) { if (!Array.isArray(actions) || actions.length === 0) { throw new Error('actions must be a non-empty array'); } + const config = options.config ?? {}; + return actions.map((action) => { const operation = action.operation !== undefined ? Number(action.operation) : 0; @@ -361,6 +371,92 @@ function buildOgTransactions(actions) { }; } + if (action.kind === 'ctf_split' || action.kind === 'ctf_merge') { + const chainId = Number(action.chainId ?? config.polymarketChainId ?? 137); + if (chainId !== Number(config.polymarketChainId ?? 137)) { + throw new Error( + `Unsupported chainId ${chainId} for CTF action. Expected ${Number(config.polymarketChainId ?? 137)}.` + ); + } + if (!action.collateralToken || !action.conditionId || action.amount === undefined) { + throw new Error(`${action.kind} requires collateralToken, conditionId, amount`); + } + const ctfContract = action.ctfContract + ? getAddress(action.ctfContract) + : config.polymarketConditionalTokens; + if (!ctfContract) { + throw new Error(`${action.kind} requires ctfContract or POLYMARKET_CONDITIONAL_TOKENS`); + } + const parentCollectionId = action.parentCollectionId ?? ZERO_BYTES32; + const partition = Array.isArray(action.partition) && action.partition.length > 0 + ? action.partition.map((value) => BigInt(value)) + : [1n, 2n]; + const amount = BigInt(action.amount); + if (amount <= 0n) { + throw new Error(`${action.kind} amount must be > 0`); + } + + const functionName = action.kind === 'ctf_split' ? 'splitPosition' : 'mergePositions'; + const data = encodeFunctionData({ + abi: conditionalTokensAbi, + functionName, + args: [ + getAddress(action.collateralToken), + parentCollectionId, + action.conditionId, + partition, + amount, + ], + }); + + return { + to: ctfContract, + value: '0', + data, + operation, + }; + } + + if (action.kind === 'ctf_redeem') { + const chainId = Number(action.chainId ?? config.polymarketChainId ?? 137); + if (chainId !== Number(config.polymarketChainId ?? 137)) { + throw new Error( + `Unsupported chainId ${chainId} for CTF action. Expected ${Number(config.polymarketChainId ?? 137)}.` + ); + } + if (!action.collateralToken || !action.conditionId) { + throw new Error('ctf_redeem requires collateralToken and conditionId'); + } + const ctfContract = action.ctfContract + ? getAddress(action.ctfContract) + : config.polymarketConditionalTokens; + if (!ctfContract) { + throw new Error('ctf_redeem requires ctfContract or POLYMARKET_CONDITIONAL_TOKENS'); + } + const parentCollectionId = action.parentCollectionId ?? ZERO_BYTES32; + const indexSets = Array.isArray(action.indexSets) && action.indexSets.length > 0 + ? action.indexSets.map((value) => BigInt(value)) + : [1n, 2n]; + + const data = encodeFunctionData({ + abi: conditionalTokensAbi, + functionName: 'redeemPositions', + args: [ + getAddress(action.collateralToken), + parentCollectionId, + action.conditionId, + indexSets, + ], + }); + + return { + to: ctfContract, + value: '0', + data, + operation, + }; + } + throw new Error(`Unknown action kind: ${action.kind}`); }); } From 50d39114a36dc670d4c368c97b6dd8331aa4d761 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 10:44:59 -0800 Subject: [PATCH 115/174] no need to check chainId when building OG transactions Signed-off-by: John Shutt --- agent/src/lib/config.js | 1 - agent/src/lib/tools.js | 8 ++------ agent/src/lib/tx.js | 22 ++++++---------------- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index 3f794d64..834b7b67 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -57,7 +57,6 @@ function buildConfig() { chainlinkPriceFeed: process.env.CHAINLINK_PRICE_FEED ? getAddress(process.env.CHAINLINK_PRICE_FEED) : undefined, - polymarketChainId: Number(process.env.POLYMARKET_CHAIN_ID ?? 137), polymarketConditionalTokens: process.env.POLYMARKET_CONDITIONAL_TOKENS ? getAddress(process.env.POLYMARKET_CONDITIONAL_TOKENS) : getAddress('0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'), diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index a14e1e4e..b1edc09a 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -28,8 +28,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { kind: { type: 'string', description: - 'Action type: erc20_transfer | native_transfer | contract_call | ctf_split | ctf_merge | ctf_redeem', - 'Action type: erc20_transfer | native_transfer | contract_call | uniswap_v3_exact_input_single', + 'Action type: erc20_transfer | native_transfer | contract_call | uniswap_v3_exact_input_single | ctf_split | ctf_merge | ctf_redeem', }, token: { type: ['string', 'null'], @@ -84,10 +83,6 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { description: 'Safe operation (0=CALL,1=DELEGATECALL). Defaults to 0.', }, - chainId: { - type: ['integer', 'null'], - description: 'Chain id for CTF actions (defaults to POLYMARKET_CHAIN_ID).', - }, ctfContract: { type: ['string', 'null'], description: @@ -122,6 +117,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { description: 'Index sets for ctf_redeem. Defaults to [1,2].', items: { type: 'integer' }, + }, router: { type: ['string', 'null'], description: diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index 3a71ab7b..e1b160fd 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -440,12 +440,6 @@ function buildOgTransactions(actions, options = {}) { } if (action.kind === 'ctf_split' || action.kind === 'ctf_merge') { - const chainId = Number(action.chainId ?? config.polymarketChainId ?? 137); - if (chainId !== Number(config.polymarketChainId ?? 137)) { - throw new Error( - `Unsupported chainId ${chainId} for CTF action. Expected ${Number(config.polymarketChainId ?? 137)}.` - ); - } if (!action.collateralToken || !action.conditionId || action.amount === undefined) { throw new Error(`${action.kind} requires collateralToken, conditionId, amount`); } @@ -477,21 +471,16 @@ function buildOgTransactions(actions, options = {}) { ], }); - return { + transactions.push({ to: ctfContract, value: '0', data, operation, - }; + }); + continue; } if (action.kind === 'ctf_redeem') { - const chainId = Number(action.chainId ?? config.polymarketChainId ?? 137); - if (chainId !== Number(config.polymarketChainId ?? 137)) { - throw new Error( - `Unsupported chainId ${chainId} for CTF action. Expected ${Number(config.polymarketChainId ?? 137)}.` - ); - } if (!action.collateralToken || !action.conditionId) { throw new Error('ctf_redeem requires collateralToken and conditionId'); } @@ -517,12 +506,13 @@ function buildOgTransactions(actions, options = {}) { ], }); - return { + transactions.push({ to: ctfContract, value: '0', data, operation, - }; + }); + continue; } throw new Error(`Unknown action kind: ${action.kind}`); From 6b2d2ee11eafe41f1de2f17bc071a301200693da Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 10:56:05 -0800 Subject: [PATCH 116/174] add back support for monitoring mode Signed-off-by: John Shutt --- agent/src/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index 5d575c24..eb50e1ba 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -172,13 +172,13 @@ async function decideOnSignals(signals, { onchainPendingProposal = false } = {}) 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor).'; try { + const writeActionsEnabled = config.proposeEnabled || config.disputeEnabled; const tools = toolDefinitions({ proposeEnabled: config.proposeEnabled, disputeEnabled: config.disputeEnabled, - clobEnabled: config.polymarketClobEnabled, + clobEnabled: writeActionsEnabled && config.polymarketClobEnabled, }); - const allowTools = - config.proposeEnabled || config.disputeEnabled || config.polymarketClobEnabled; + const allowTools = writeActionsEnabled; const decision = await callAgent({ config, systemPrompt, From a4f1af1f7ce9b5acfde28cc79c71ca2c8a416869 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 10:58:34 -0800 Subject: [PATCH 117/174] replace raw secret forwarding with per-request HMAC signature generation Signed-off-by: John Shutt --- agent/src/lib/polymarket.js | 47 +++++++++++++++++++++++++++++++++---- agent/src/lib/tools.js | 2 ++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index ec96a528..a594dd83 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -1,41 +1,74 @@ +import crypto from 'node:crypto'; + const DEFAULT_CLOB_HOST = 'https://clob.polymarket.com'; function normalizeClobHost(host) { return (host ?? DEFAULT_CLOB_HOST).replace(/\/+$/, ''); } -function buildClobAuthHeaders(config) { +function buildClobAuthHeaders({ + config, + signingAddress, + timestamp, + method, + path, + bodyText, +}) { const apiKey = config.polymarketClobApiKey; const apiSecret = config.polymarketClobApiSecret; const apiPassphrase = config.polymarketClobApiPassphrase; + if (!signingAddress) { + throw new Error('Missing signingAddress for CLOB auth headers.'); + } if (!apiKey || !apiSecret || !apiPassphrase) { throw new Error( 'Missing CLOB credentials. Set POLYMARKET_CLOB_API_KEY, POLYMARKET_CLOB_API_SECRET, and POLYMARKET_CLOB_API_PASSPHRASE.' ); } + const payload = `${timestamp}${method.toUpperCase()}${path}${bodyText ?? ''}`; + const secretBytes = Buffer.from(apiSecret, 'base64'); + const signature = crypto + .createHmac('sha256', secretBytes) + .update(payload) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + return { + 'POLY_ADDRESS': signingAddress, 'POLY_API_KEY': apiKey, - 'POLY_SIGNATURE': apiSecret, + 'POLY_SIGNATURE': signature, + 'POLY_TIMESTAMP': String(timestamp), 'POLY_PASSPHRASE': apiPassphrase, }; } async function clobRequest({ config, + signingAddress, path, method, body, }) { const host = normalizeClobHost(config.polymarketClobHost); + const bodyText = body === undefined ? '' : JSON.stringify(body); + const timestamp = Date.now(); const headers = { 'Content-Type': 'application/json', - ...buildClobAuthHeaders(config), + ...buildClobAuthHeaders({ + config, + signingAddress, + timestamp, + method, + path, + bodyText, + }), }; const response = await fetch(`${host}${path}`, { method, headers, - body: body === undefined ? undefined : JSON.stringify(body), + body: body === undefined ? undefined : bodyText, }); const text = await response.text(); let parsed; @@ -56,6 +89,7 @@ async function clobRequest({ async function placeClobOrder({ config, + signingAddress, signedOrder, owner, orderType, @@ -72,6 +106,7 @@ async function placeClobOrder({ return clobRequest({ config, + signingAddress, method: 'POST', path: '/order', body: { @@ -84,6 +119,7 @@ async function placeClobOrder({ async function cancelClobOrders({ config, + signingAddress, mode, orderIds, market, @@ -92,6 +128,7 @@ async function cancelClobOrders({ if (mode === 'all') { return clobRequest({ config, + signingAddress, method: 'DELETE', path: '/cancel-all', }); @@ -103,6 +140,7 @@ async function cancelClobOrders({ } return clobRequest({ config, + signingAddress, method: 'DELETE', path: '/cancel-market-orders', body: { @@ -118,6 +156,7 @@ async function cancelClobOrders({ return clobRequest({ config, + signingAddress, method: 'DELETE', path: '/orders', body: { diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index b1edc09a..ee83546c 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -379,6 +379,7 @@ async function executeToolCalls({ } const result = await placeClobOrder({ config, + signingAddress: account.address, signedOrder: args.signedOrder, owner: args.owner, orderType: args.orderType, @@ -420,6 +421,7 @@ async function executeToolCalls({ try { const result = await cancelClobOrders({ config, + signingAddress: account.address, mode: args.mode, orderIds: args.orderIds, market: args.market, From 7ae2425f94170d8345d499fa50e9e499179ba37f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 11:07:14 -0800 Subject: [PATCH 118/174] add strict preflight validation for clob orders Signed-off-by: John Shutt --- agent/src/lib/tools.js | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index ee83546c..7a753c7b 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -7,6 +7,50 @@ function safeStringify(value) { return JSON.stringify(value, (_, item) => (typeof item === 'bigint' ? item.toString() : item)); } +function normalizeOrderSide(value) { + if (typeof value !== 'string') return undefined; + const normalized = value.trim().toUpperCase(); + return normalized === 'BUY' || normalized === 'SELL' ? normalized : undefined; +} + +function getFirstString(values) { + for (const value of values) { + if (typeof value === 'string' && value.trim()) { + return value.trim(); + } + } + return undefined; +} + +function extractSignedOrderSideAndTokenId(signedOrder) { + if (!signedOrder || typeof signedOrder !== 'object') { + return { side: undefined, tokenId: undefined }; + } + + const container = + signedOrder.order && typeof signedOrder.order === 'object' + ? signedOrder.order + : signedOrder; + + const side = normalizeOrderSide(container.side ?? signedOrder.side); + const tokenId = getFirstString([ + container.tokenId, + container.tokenID, + container.token_id, + container.assetId, + container.assetID, + container.asset_id, + signedOrder.tokenId, + signedOrder.tokenID, + signedOrder.token_id, + signedOrder.assetId, + signedOrder.assetID, + signedOrder.asset_id, + ]); + + return { side, tokenId }; +} + function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { const tools = [ { @@ -377,6 +421,24 @@ async function executeToolCalls({ if (!args.tokenId) { throw new Error('tokenId is required'); } + const declaredTokenId = String(args.tokenId).trim(); + const { side: signedOrderSide, tokenId: signedOrderTokenId } = + extractSignedOrderSideAndTokenId(args.signedOrder); + if (!signedOrderSide || !signedOrderTokenId) { + throw new Error( + 'signedOrder must include embedded side and token id (side + tokenId/asset_id).' + ); + } + if (signedOrderSide !== args.side) { + throw new Error( + `signedOrder side mismatch: declared ${args.side}, signed order has ${signedOrderSide}.` + ); + } + if (signedOrderTokenId !== declaredTokenId) { + throw new Error( + `signedOrder token mismatch: declared ${declaredTokenId}, signed order has ${signedOrderTokenId}.` + ); + } const result = await placeClobOrder({ config, signingAddress: account.address, From 8a506d28fcb8d2b9d70cc084184f39019227093e Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 11:17:27 -0800 Subject: [PATCH 119/174] auto-approve tokens before splitPosition function call Signed-off-by: John Shutt --- agent/src/lib/tx.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index e1b160fd..f480857f 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -443,6 +443,7 @@ function buildOgTransactions(actions, options = {}) { if (!action.collateralToken || !action.conditionId || action.amount === undefined) { throw new Error(`${action.kind} requires collateralToken, conditionId, amount`); } + const collateralToken = getAddress(action.collateralToken); const ctfContract = action.ctfContract ? getAddress(action.ctfContract) : config.polymarketConditionalTokens; @@ -458,12 +459,26 @@ function buildOgTransactions(actions, options = {}) { throw new Error(`${action.kind} amount must be > 0`); } + if (action.kind === 'ctf_split') { + const approveData = encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [ctfContract, amount], + }); + transactions.push({ + to: collateralToken, + value: '0', + data: approveData, + operation, + }); + } + const functionName = action.kind === 'ctf_split' ? 'splitPosition' : 'mergePositions'; const data = encodeFunctionData({ abi: conditionalTokensAbi, functionName, args: [ - getAddress(action.collateralToken), + collateralToken, parentCollectionId, action.conditionId, partition, From 9710ef19ae29b10e1014b485e5941cd35c551ff9 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 11:28:27 -0800 Subject: [PATCH 120/174] update polymarket_clob_place_order in agent/src/lib/tools.js to normalize side before validation Signed-off-by: John Shutt --- agent/src/lib/tools.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 7a753c7b..093fcd73 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -415,7 +415,8 @@ async function executeToolCalls({ } try { - if (args.side !== 'BUY' && args.side !== 'SELL') { + const declaredSide = normalizeOrderSide(args.side); + if (!declaredSide) { throw new Error('side must be BUY or SELL'); } if (!args.tokenId) { @@ -429,9 +430,9 @@ async function executeToolCalls({ 'signedOrder must include embedded side and token id (side + tokenId/asset_id).' ); } - if (signedOrderSide !== args.side) { + if (signedOrderSide !== declaredSide) { throw new Error( - `signedOrder side mismatch: declared ${args.side}, signed order has ${signedOrderSide}.` + `signedOrder side mismatch: declared ${declaredSide}, signed order has ${signedOrderSide}.` ); } if (signedOrderTokenId !== declaredTokenId) { From bf2b1f7e092016adc7ffb74eb1541b79aa65d8dc Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 11:31:46 -0800 Subject: [PATCH 121/174] unwrap signed order inputs before submitting Signed-off-by: John Shutt --- agent/src/lib/polymarket.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index a594dd83..4bfd3909 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -104,13 +104,18 @@ async function placeClobOrder({ throw new Error('orderType is required.'); } + const normalizedOrder = + signedOrder.order && typeof signedOrder.order === 'object' + ? signedOrder.order + : signedOrder; + return clobRequest({ config, signingAddress, method: 'POST', path: '/order', body: { - order: signedOrder, + order: normalizedOrder, owner, orderType, }, From 00620b8046def65136ceeb2b1f311eeb2746132c Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 11:41:00 -0800 Subject: [PATCH 122/174] patch to pass api key owner as owner, not an address, for clob orders, and send a raw array body rather than an object for deleting clob orders Signed-off-by: John Shutt --- agent/src/lib/polymarket.js | 12 +++++------- agent/src/lib/tools.js | 22 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index 4bfd3909..fec9b0aa 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -91,14 +91,14 @@ async function placeClobOrder({ config, signingAddress, signedOrder, - owner, + ownerApiKey, orderType, }) { if (!signedOrder || typeof signedOrder !== 'object') { throw new Error('signedOrder is required and must be an object.'); } - if (!owner) { - throw new Error('owner is required.'); + if (!ownerApiKey) { + throw new Error('ownerApiKey is required.'); } if (!orderType) { throw new Error('orderType is required.'); @@ -116,7 +116,7 @@ async function placeClobOrder({ path: '/order', body: { order: normalizedOrder, - owner, + owner: ownerApiKey, orderType, }, }); @@ -164,9 +164,7 @@ async function cancelClobOrders({ signingAddress, method: 'DELETE', path: '/orders', - body: { - orderIDs: orderIds, - }, + body: orderIds, }); } diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 093fcd73..6c91573b 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -299,8 +299,9 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { additionalProperties: false, properties: { owner: { - type: 'string', - description: 'EOA owner address associated with the signed order.', + type: ['string', 'null'], + description: + 'Optional CLOB API key owner override; defaults to POLYMARKET_CLOB_API_KEY.', }, side: { type: 'string', @@ -320,7 +321,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { 'Signed order payload expected by the CLOB API /order endpoint.', }, }, - required: ['owner', 'side', 'tokenId', 'orderType', 'signedOrder'], + required: ['side', 'tokenId', 'orderType', 'signedOrder'], }, }, { @@ -440,11 +441,24 @@ async function executeToolCalls({ `signedOrder token mismatch: declared ${declaredTokenId}, signed order has ${signedOrderTokenId}.` ); } + const configuredOwnerApiKey = config.polymarketClobApiKey; + if (!configuredOwnerApiKey) { + throw new Error('Missing POLYMARKET_CLOB_API_KEY in runtime config.'); + } + const requestedOwner = + typeof args.owner === 'string' && args.owner.trim() + ? args.owner.trim() + : undefined; + if (requestedOwner && requestedOwner !== configuredOwnerApiKey) { + throw new Error( + 'owner mismatch: provided owner does not match configured POLYMARKET_CLOB_API_KEY.' + ); + } const result = await placeClobOrder({ config, signingAddress: account.address, signedOrder: args.signedOrder, - owner: args.owner, + ownerApiKey: configuredOwnerApiKey, orderType: args.orderType, }); outputs.push({ From cc927a04a297962d6a887c1351c42e9af8d2499c Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 11:44:57 -0800 Subject: [PATCH 123/174] fix timestamp handling per polymarket docs Signed-off-by: John Shutt --- agent/src/lib/polymarket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index fec9b0aa..5c28ffb6 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -53,7 +53,7 @@ async function clobRequest({ }) { const host = normalizeClobHost(config.polymarketClobHost); const bodyText = body === undefined ? '' : JSON.stringify(body); - const timestamp = Date.now(); + const timestamp = Math.floor(Date.now() / 1000); const headers = { 'Content-Type': 'application/json', ...buildClobAuthHeaders({ From d3d8433a87e8ce62cfdfd17250a147cfeacebf51 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 11:49:52 -0800 Subject: [PATCH 124/174] enable clob-only agent mode and fix schema for order types Signed-off-by: John Shutt --- agent/src/index.js | 7 ++++--- agent/src/lib/config.js | 2 +- agent/src/lib/tools.js | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index eb50e1ba..d457f4da 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -172,13 +172,14 @@ async function decideOnSignals(signals, { onchainPendingProposal = false } = {}) 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor).'; try { - const writeActionsEnabled = config.proposeEnabled || config.disputeEnabled; + const executableToolsEnabled = + config.proposeEnabled || config.disputeEnabled || config.polymarketClobEnabled; const tools = toolDefinitions({ proposeEnabled: config.proposeEnabled, disputeEnabled: config.disputeEnabled, - clobEnabled: writeActionsEnabled && config.polymarketClobEnabled, + clobEnabled: config.polymarketClobEnabled, }); - const allowTools = writeActionsEnabled; + const allowTools = executableToolsEnabled; const decision = await callAgent({ config, systemPrompt, diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index 834b7b67..c0888122 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -62,7 +62,7 @@ function buildConfig() { : getAddress('0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'), polymarketClobEnabled: process.env.POLYMARKET_CLOB_ENABLED === undefined - ? true + ? false : process.env.POLYMARKET_CLOB_ENABLED.toLowerCase() !== 'false', polymarketClobHost: process.env.POLYMARKET_CLOB_HOST ?? 'https://clob.polymarket.com', polymarketClobApiKey: process.env.POLYMARKET_CLOB_API_KEY, diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 6c91573b..52f8f8d4 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -13,6 +13,25 @@ function normalizeOrderSide(value) { return normalized === 'BUY' || normalized === 'SELL' ? normalized : undefined; } +function normalizeOrderType(value) { + if (typeof value !== 'string') return undefined; + const normalized = value.trim().toUpperCase(); + return normalized === 'GTC' || + normalized === 'GTD' || + normalized === 'FOK' || + normalized === 'FAK' + ? normalized + : undefined; +} + +function normalizeCancelMode(value) { + if (typeof value !== 'string') return undefined; + const normalized = value.trim().toLowerCase(); + return normalized === 'ids' || normalized === 'market' || normalized === 'all' + ? normalized + : undefined; +} + function getFirstString(values) { for (const value of values) { if (typeof value === 'string' && value.trim()) { @@ -313,6 +332,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { }, orderType: { type: 'string', + enum: ['GTC', 'GTD', 'FOK', 'FAK'], description: 'Order type, e.g. GTC, GTD, FOK, or FAK.', }, signedOrder: { @@ -336,6 +356,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { properties: { mode: { type: 'string', + enum: ['ids', 'market', 'all'], description: 'ids | market | all', }, orderIds: { @@ -424,6 +445,10 @@ async function executeToolCalls({ throw new Error('tokenId is required'); } const declaredTokenId = String(args.tokenId).trim(); + const orderType = normalizeOrderType(args.orderType); + if (!orderType) { + throw new Error('orderType must be one of GTC, GTD, FOK, FAK'); + } const { side: signedOrderSide, tokenId: signedOrderTokenId } = extractSignedOrderSideAndTokenId(args.signedOrder); if (!signedOrderSide || !signedOrderTokenId) { @@ -459,7 +484,7 @@ async function executeToolCalls({ signingAddress: account.address, signedOrder: args.signedOrder, ownerApiKey: configuredOwnerApiKey, - orderType: args.orderType, + orderType, }); outputs.push({ callId: call.callId, @@ -496,10 +521,14 @@ async function executeToolCalls({ } try { + const mode = normalizeCancelMode(args.mode); + if (!mode) { + throw new Error('mode must be one of ids, market, all'); + } const result = await cancelClobOrders({ config, signingAddress: account.address, - mode: args.mode, + mode, orderIds: args.orderIds, market: args.market, assetId: args.assetId, From cefb0f24218e0924a5a1750418102fe3fd515689 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 11:51:43 -0800 Subject: [PATCH 125/174] add unit test for normalizing order types and cancel mode for polymarket Signed-off-by: John Shutt --- .../test-polymarket-tool-normalization.mjs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 agent/scripts/test-polymarket-tool-normalization.mjs diff --git a/agent/scripts/test-polymarket-tool-normalization.mjs b/agent/scripts/test-polymarket-tool-normalization.mjs new file mode 100644 index 00000000..c8012463 --- /dev/null +++ b/agent/scripts/test-polymarket-tool-normalization.mjs @@ -0,0 +1,128 @@ +import assert from 'node:assert/strict'; +import { executeToolCalls, toolDefinitions } from '../src/lib/tools.js'; + +const TEST_ACCOUNT = { address: '0x1111111111111111111111111111111111111111' }; + +function parseToolOutput(output) { + return JSON.parse(output.output); +} + +async function run() { + const defs = toolDefinitions({ + proposeEnabled: false, + disputeEnabled: false, + clobEnabled: true, + }); + const placeOrderDef = defs.find((tool) => tool.name === 'polymarket_clob_place_order'); + const cancelOrdersDef = defs.find((tool) => tool.name === 'polymarket_clob_cancel_orders'); + + assert.ok(placeOrderDef); + assert.ok(cancelOrdersDef); + assert.deepEqual(placeOrderDef.parameters.properties.orderType.enum, ['GTC', 'GTD', 'FOK', 'FAK']); + assert.deepEqual(cancelOrdersDef.parameters.properties.mode.enum, ['ids', 'market', 'all']); + + const config = { + polymarketClobEnabled: true, + polymarketClobHost: 'https://clob.polymarket.com', + polymarketClobApiKey: 'dummy-api-key', + // Keep secret/passphrase absent so calls fail before any network request. + polymarketClobApiSecret: undefined, + polymarketClobApiPassphrase: undefined, + }; + + const invalidOrderType = await executeToolCalls({ + toolCalls: [ + { + callId: 'invalid-order-type', + name: 'polymarket_clob_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'LIMIT', + signedOrder: { side: 'BUY', tokenId: '123' }, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const invalidOrderTypeOut = parseToolOutput(invalidOrderType[0]); + assert.equal(invalidOrderTypeOut.status, 'error'); + assert.match(invalidOrderTypeOut.message, /orderType must be one of/); + + const normalizedOrderType = await executeToolCalls({ + toolCalls: [ + { + callId: 'normalized-order-type', + name: 'polymarket_clob_place_order', + arguments: { + side: ' buy ', + tokenId: '123', + orderType: ' gtc ', + signedOrder: { side: 'BUY', tokenId: '123' }, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const normalizedOrderTypeOut = parseToolOutput(normalizedOrderType[0]); + assert.equal(normalizedOrderTypeOut.status, 'error'); + assert.match(normalizedOrderTypeOut.message, /Missing CLOB credentials/); + + const invalidCancelMode = await executeToolCalls({ + toolCalls: [ + { + callId: 'invalid-cancel-mode', + name: 'polymarket_clob_cancel_orders', + arguments: { + mode: 'nope', + orderIds: ['order-1'], + market: null, + assetId: null, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const invalidCancelModeOut = parseToolOutput(invalidCancelMode[0]); + assert.equal(invalidCancelModeOut.status, 'error'); + assert.match(invalidCancelModeOut.message, /mode must be one of ids, market, all/); + + const normalizedCancelMode = await executeToolCalls({ + toolCalls: [ + { + callId: 'normalized-cancel-mode', + name: 'polymarket_clob_cancel_orders', + arguments: { + mode: ' IDS ', + orderIds: ['order-1'], + market: null, + assetId: null, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const normalizedCancelModeOut = parseToolOutput(normalizedCancelMode[0]); + assert.equal(normalizedCancelModeOut.status, 'error'); + assert.match(normalizedCancelModeOut.message, /Missing CLOB credentials/); + + console.log('[test] polymarket tool normalization OK'); +} + +run(); From 885bd21c49cc7bee2e7040541a774aa52983ec40 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 12:32:35 -0800 Subject: [PATCH 126/174] add erc-1155 support Signed-off-by: John Shutt --- agent/scripts/test-erc1155-deposit.mjs | 53 +++++++++++++++++++++++ agent/src/lib/tools.js | 60 +++++++++++++++++++++++++- agent/src/lib/tx.js | 41 ++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 agent/scripts/test-erc1155-deposit.mjs diff --git a/agent/scripts/test-erc1155-deposit.mjs b/agent/scripts/test-erc1155-deposit.mjs new file mode 100644 index 00000000..4abf9eec --- /dev/null +++ b/agent/scripts/test-erc1155-deposit.mjs @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import { makeErc1155Deposit } from '../src/lib/tx.js'; + +async function run() { + const token = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; + const account = { address: '0x1111111111111111111111111111111111111111' }; + const config = { commitmentSafe: '0x2222222222222222222222222222222222222222' }; + + let writeContractArgs; + const walletClient = { + async writeContract(args) { + writeContractArgs = args; + return '0xabc123'; + }, + }; + + const txHash = await makeErc1155Deposit({ + walletClient, + account, + config, + token, + tokenId: '7', + amount: '3', + data: null, + }); + + assert.equal(txHash, '0xabc123'); + assert.equal(writeContractArgs.address.toLowerCase(), token.toLowerCase()); + assert.equal(writeContractArgs.functionName, 'safeTransferFrom'); + assert.equal(writeContractArgs.args[0].toLowerCase(), account.address.toLowerCase()); + assert.equal(writeContractArgs.args[1].toLowerCase(), config.commitmentSafe.toLowerCase()); + assert.equal(writeContractArgs.args[2], 7n); + assert.equal(writeContractArgs.args[3], 3n); + assert.equal(writeContractArgs.args[4], '0x'); + + await assert.rejects( + () => + makeErc1155Deposit({ + walletClient, + account, + config, + token, + tokenId: '7', + amount: '0', + data: '0x', + }), + /amount must be > 0/ + ); + + console.log('[test] makeErc1155Deposit OK'); +} + +run(); diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 52f8f8d4..457e75f8 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -1,5 +1,11 @@ import { getAddress } from 'viem'; -import { buildOgTransactions, makeDeposit, postBondAndDispute, postBondAndPropose } from './tx.js'; +import { + buildOgTransactions, + makeDeposit, + makeErc1155Deposit, + postBondAndDispute, + postBondAndPropose, +} from './tx.js'; import { cancelClobOrders, placeClobOrder } from './polymarket.js'; import { parseToolArguments } from './utils.js'; @@ -246,6 +252,36 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { required: ['asset', 'amountWei'], }, }, + { + type: 'function', + name: 'make_erc1155_deposit', + description: + 'Deposit ERC1155 tokens into the commitment Safe using safeTransferFrom from the agent wallet.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + token: { + type: 'string', + description: 'ERC1155 token contract address.', + }, + tokenId: { + type: 'string', + description: 'ERC1155 token id as a base-10 string.', + }, + amount: { + type: 'string', + description: 'ERC1155 amount as a base-10 string.', + }, + data: { + type: ['string', 'null'], + description: 'Optional calldata bytes for safeTransferFrom, defaults to 0x.', + }, + }, + required: ['token', 'tokenId', 'amount'], + }, + }, ]; if (proposeEnabled) { @@ -574,6 +610,28 @@ async function executeToolCalls({ continue; } + if (call.name === 'make_erc1155_deposit') { + const txHash = await makeErc1155Deposit({ + walletClient, + account, + config, + token: args.token, + tokenId: args.tokenId, + amount: args.amount, + data: args.data, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'confirmed', + transactionHash: String(txHash), + }), + }); + continue; + } + if (call.name === 'post_bond_and_propose') { if (!config.proposeEnabled) { outputs.push({ diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index f480857f..030a82e8 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -16,6 +16,10 @@ const conditionalTokensAbi = parseAbi([ 'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)', ]); +const erc1155TransferAbi = parseAbi([ + 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)', +]); + const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; async function postBondAndPropose({ @@ -567,8 +571,45 @@ async function makeDeposit({ }); } +async function makeErc1155Deposit({ + walletClient, + account, + config, + token, + tokenId, + amount, + data, +}) { + if (!token || tokenId === undefined || amount === undefined) { + throw new Error('ERC1155 deposit requires token, tokenId, and amount.'); + } + + const normalizedToken = getAddress(token); + const normalizedTokenId = BigInt(tokenId); + const normalizedAmount = BigInt(amount); + if (normalizedAmount <= 0n) { + throw new Error('ERC1155 deposit amount must be > 0.'); + } + + const transferData = data ?? '0x'; + + return walletClient.writeContract({ + address: normalizedToken, + abi: erc1155TransferAbi, + functionName: 'safeTransferFrom', + args: [ + account.address, + config.commitmentSafe, + normalizedTokenId, + normalizedAmount, + transferData, + ], + }); +} + export { buildOgTransactions, + makeErc1155Deposit, makeDeposit, normalizeOgTransactions, postBondAndDispute, From 103611a9e421ccc85b12caf3085395c96782f7e5 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 12:34:38 -0800 Subject: [PATCH 127/174] polymarket transaction patches Signed-off-by: John Shutt --- agent/scripts/test-build-og-transactions.mjs | 57 ++++++++++++++++++- .../test-polymarket-tool-normalization.mjs | 27 +++++++++ agent/src/lib/tools.js | 53 +++++++++++++++++ agent/src/lib/tx.js | 14 +++++ 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/agent/scripts/test-build-og-transactions.mjs b/agent/scripts/test-build-og-transactions.mjs index b9de7db8..1cc6ff28 100644 --- a/agent/scripts/test-build-og-transactions.mjs +++ b/agent/scripts/test-build-og-transactions.mjs @@ -45,7 +45,62 @@ function run() { data: txs[1].data, }); assert.equal(swapCall.functionName, 'exactInputSingle'); - console.log('[test] buildOgTransactions uniswap action OK'); + + const ctf = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; + const conditionId = `0x${'11'.repeat(32)}`; + const ctfTxs = buildOgTransactions( + [ + { + kind: 'ctf_split', + ctfContract: null, + collateralToken: usdc, + conditionId, + parentCollectionId: null, + partition: [1, 2], + amount: '250000', + operation: 0, + }, + ], + { + config: { + polymarketConditionalTokens: ctf, + }, + } + ); + assert.equal(ctfTxs.length, 3); + assert.equal(ctfTxs[0].to.toLowerCase(), usdc.toLowerCase()); + assert.equal(ctfTxs[1].to.toLowerCase(), usdc.toLowerCase()); + assert.equal(ctfTxs[2].to.toLowerCase(), ctf.toLowerCase()); + + const resetApproveCall = decodeFunctionData({ + abi: erc20Abi, + data: ctfTxs[0].data, + }); + assert.equal(resetApproveCall.functionName, 'approve'); + assert.equal(resetApproveCall.args[0].toLowerCase(), ctf.toLowerCase()); + assert.equal(resetApproveCall.args[1], 0n); + + const amountApproveCall = decodeFunctionData({ + abi: erc20Abi, + data: ctfTxs[1].data, + }); + assert.equal(amountApproveCall.functionName, 'approve'); + assert.equal(amountApproveCall.args[0].toLowerCase(), ctf.toLowerCase()); + assert.equal(amountApproveCall.args[1], 250000n); + + const splitCall = decodeFunctionData({ + abi: parseAbi([ + 'function splitPosition(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount)', + ]), + data: ctfTxs[2].data, + }); + assert.equal(splitCall.functionName, 'splitPosition'); + assert.equal(splitCall.args[0].toLowerCase(), usdc.toLowerCase()); + assert.equal(splitCall.args[2], conditionId); + assert.deepEqual(splitCall.args[3], [1n, 2n]); + assert.equal(splitCall.args[4], 250000n); + + console.log('[test] buildOgTransactions uniswap + ctf_split actions OK'); } run(); diff --git a/agent/scripts/test-polymarket-tool-normalization.mjs b/agent/scripts/test-polymarket-tool-normalization.mjs index c8012463..e7f4ab1b 100644 --- a/agent/scripts/test-polymarket-tool-normalization.mjs +++ b/agent/scripts/test-polymarket-tool-normalization.mjs @@ -76,6 +76,33 @@ async function run() { assert.equal(normalizedOrderTypeOut.status, 'error'); assert.match(normalizedOrderTypeOut.message, /Missing CLOB credentials/); + const mismatchedIdentity = await executeToolCalls({ + toolCalls: [ + { + callId: 'mismatched-identity', + name: 'polymarket_clob_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + signedOrder: { + side: 'BUY', + tokenId: '123', + maker: '0x3333333333333333333333333333333333333333', + }, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const mismatchedIdentityOut = parseToolOutput(mismatchedIdentity[0]); + assert.equal(mismatchedIdentityOut.status, 'error'); + assert.match(mismatchedIdentityOut.message, /signedOrder identity mismatch/); + const invalidCancelMode = await executeToolCalls({ toolCalls: [ { diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 457e75f8..6e7bec00 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -47,6 +47,17 @@ function getFirstString(values) { return undefined; } +function maybeAddress(value) { + if (typeof value !== 'string') return undefined; + const trimmed = value.trim(); + if (!/^0x[0-9a-fA-F]{40}$/.test(trimmed)) return undefined; + try { + return getAddress(trimmed); + } catch (error) { + return undefined; + } +} + function extractSignedOrderSideAndTokenId(signedOrder) { if (!signedOrder || typeof signedOrder !== 'object') { return { side: undefined, tokenId: undefined }; @@ -76,6 +87,38 @@ function extractSignedOrderSideAndTokenId(signedOrder) { return { side, tokenId }; } +function extractSignedOrderIdentityAddresses(signedOrder) { + if (!signedOrder || typeof signedOrder !== 'object') { + return []; + } + + const container = + signedOrder.order && typeof signedOrder.order === 'object' + ? signedOrder.order + : signedOrder; + const candidates = [ + container.signer, + container.signerAddress, + container.maker, + container.makerAddress, + container.funder, + container.funderAddress, + container.user, + container.userAddress, + signedOrder.signer, + signedOrder.signerAddress, + signedOrder.maker, + signedOrder.makerAddress, + signedOrder.funder, + signedOrder.funderAddress, + signedOrder.user, + signedOrder.userAddress, + ]; + + const normalized = candidates.map(maybeAddress).filter(Boolean); + return Array.from(new Set(normalized)); +} + function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { const tools = [ { @@ -502,6 +545,16 @@ async function executeToolCalls({ `signedOrder token mismatch: declared ${declaredTokenId}, signed order has ${signedOrderTokenId}.` ); } + const signerAddress = getAddress(account.address); + const identityAddresses = extractSignedOrderIdentityAddresses(args.signedOrder); + if ( + identityAddresses.length > 0 && + !identityAddresses.some((address) => address === signerAddress) + ) { + throw new Error( + `signedOrder identity mismatch: expected ${signerAddress}, signed order contains ${identityAddresses.join(', ')}.` + ); + } const configuredOwnerApiKey = config.polymarketClobApiKey; if (!configuredOwnerApiKey) { throw new Error('Missing POLYMARKET_CLOB_API_KEY in runtime config.'); diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index 030a82e8..87a265c7 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -464,6 +464,20 @@ function buildOgTransactions(actions, options = {}) { } if (action.kind === 'ctf_split') { + // Use zero-first approval for compatibility with ERC20 tokens that + // require allowance reset before setting a new non-zero allowance. + const resetApproveData = encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [ctfContract, 0n], + }); + transactions.push({ + to: collateralToken, + value: '0', + data: resetApproveData, + operation, + }); + const approveData = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', From 2794e1589f7594af8ae891ee685c967d7c060fa5 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 12:37:18 -0800 Subject: [PATCH 128/174] gate onchain tools behind explicit flags Signed-off-by: John Shutt --- .../test-polymarket-tool-normalization.mjs | 37 ++++++++++++++++++ agent/src/index.js | 1 + agent/src/lib/tools.js | 38 +++++++++++++++++-- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/agent/scripts/test-polymarket-tool-normalization.mjs b/agent/scripts/test-polymarket-tool-normalization.mjs index e7f4ab1b..69b67579 100644 --- a/agent/scripts/test-polymarket-tool-normalization.mjs +++ b/agent/scripts/test-polymarket-tool-normalization.mjs @@ -15,9 +15,13 @@ async function run() { }); const placeOrderDef = defs.find((tool) => tool.name === 'polymarket_clob_place_order'); const cancelOrdersDef = defs.find((tool) => tool.name === 'polymarket_clob_cancel_orders'); + const makeDepositDef = defs.find((tool) => tool.name === 'make_deposit'); + const makeErc1155DepositDef = defs.find((tool) => tool.name === 'make_erc1155_deposit'); assert.ok(placeOrderDef); assert.ok(cancelOrdersDef); + assert.equal(makeDepositDef, undefined); + assert.equal(makeErc1155DepositDef, undefined); assert.deepEqual(placeOrderDef.parameters.properties.orderType.enum, ['GTC', 'GTD', 'FOK', 'FAK']); assert.deepEqual(cancelOrdersDef.parameters.properties.mode.enum, ['ids', 'market', 'all']); @@ -149,6 +153,39 @@ async function run() { assert.equal(normalizedCancelModeOut.status, 'error'); assert.match(normalizedCancelModeOut.message, /Missing CLOB credentials/); + const blockedOnchainDeposit = await executeToolCalls({ + toolCalls: [ + { + callId: 'blocked-onchain-deposit', + name: 'make_deposit', + arguments: { + asset: '0x0000000000000000000000000000000000000000', + amountWei: '1', + }, + }, + ], + publicClient: { + async waitForTransactionReceipt() { + throw new Error('should not be called'); + }, + }, + walletClient: { + async sendTransaction() { + throw new Error('should not be called'); + }, + }, + account: TEST_ACCOUNT, + config: { + ...config, + proposeEnabled: false, + disputeEnabled: false, + }, + ogContext: null, + }); + const blockedOnchainDepositOut = parseToolOutput(blockedOnchainDeposit[0]); + assert.equal(blockedOnchainDepositOut.status, 'skipped'); + assert.equal(blockedOnchainDepositOut.reason, 'onchain tools disabled'); + console.log('[test] polymarket tool normalization OK'); } diff --git a/agent/src/index.js b/agent/src/index.js index d457f4da..98b3c5ba 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -178,6 +178,7 @@ async function decideOnSignals(signals, { onchainPendingProposal = false } = {}) proposeEnabled: config.proposeEnabled, disputeEnabled: config.disputeEnabled, clobEnabled: config.polymarketClobEnabled, + onchainToolsEnabled: config.proposeEnabled || config.disputeEnabled, }); const allowTools = executableToolsEnabled; const decision = await callAgent({ diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 6e7bec00..05fc4f7c 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -119,7 +119,12 @@ function extractSignedOrderIdentityAddresses(signedOrder) { return Array.from(new Set(normalized)); } -function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { +function toolDefinitions({ + proposeEnabled, + disputeEnabled, + clobEnabled, + onchainToolsEnabled = proposeEnabled || disputeEnabled, +}) { const tools = [ { type: 'function', @@ -327,7 +332,11 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { }, ]; - if (proposeEnabled) { + if (!onchainToolsEnabled) { + tools.length = 0; + } + + if (onchainToolsEnabled && proposeEnabled) { tools.push({ type: 'function', name: 'post_bond_and_propose', @@ -359,7 +368,7 @@ function toolDefinitions({ proposeEnabled, disputeEnabled, clobEnabled }) { }); } - if (disputeEnabled) { + if (onchainToolsEnabled && disputeEnabled) { tools.push({ type: 'function', name: 'dispute_assertion', @@ -470,6 +479,7 @@ async function executeToolCalls({ ogContext, }) { const outputs = []; + const onchainToolsEnabled = config.proposeEnabled || config.disputeEnabled; const hasPostProposal = toolCalls.some((call) => call.name === 'post_bond_and_propose'); let builtTransactions; @@ -644,6 +654,17 @@ async function executeToolCalls({ } if (call.name === 'make_deposit') { + if (!onchainToolsEnabled) { + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'skipped', + reason: 'onchain tools disabled', + }), + }); + continue; + } const txHash = await makeDeposit({ walletClient, account, @@ -664,6 +685,17 @@ async function executeToolCalls({ } if (call.name === 'make_erc1155_deposit') { + if (!onchainToolsEnabled) { + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'skipped', + reason: 'onchain tools disabled', + }), + }); + continue; + } const txHash = await makeErc1155Deposit({ walletClient, account, From 448eb4a1465d9d07b43e8811fd9bcf2879a888ce Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 12:49:05 -0800 Subject: [PATCH 129/174] add timeout/retry guard for clob requests and relax cancel schema requirements to match the docs Signed-off-by: John Shutt --- .../test-polymarket-tool-normalization.mjs | 1 + agent/src/lib/config.js | 5 + agent/src/lib/polymarket.js | 116 +++++++++++++----- agent/src/lib/tools.js | 2 +- 4 files changed, 93 insertions(+), 31 deletions(-) diff --git a/agent/scripts/test-polymarket-tool-normalization.mjs b/agent/scripts/test-polymarket-tool-normalization.mjs index 69b67579..0c2ad7f9 100644 --- a/agent/scripts/test-polymarket-tool-normalization.mjs +++ b/agent/scripts/test-polymarket-tool-normalization.mjs @@ -24,6 +24,7 @@ async function run() { assert.equal(makeErc1155DepositDef, undefined); assert.deepEqual(placeOrderDef.parameters.properties.orderType.enum, ['GTC', 'GTD', 'FOK', 'FAK']); assert.deepEqual(cancelOrdersDef.parameters.properties.mode.enum, ['ids', 'market', 'all']); + assert.deepEqual(cancelOrdersDef.parameters.required, ['mode']); const config = { polymarketClobEnabled: true, diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index c0888122..49191318 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -68,6 +68,11 @@ function buildConfig() { polymarketClobApiKey: process.env.POLYMARKET_CLOB_API_KEY, polymarketClobApiSecret: process.env.POLYMARKET_CLOB_API_SECRET, polymarketClobApiPassphrase: process.env.POLYMARKET_CLOB_API_PASSPHRASE, + polymarketClobRequestTimeoutMs: Number( + process.env.POLYMARKET_CLOB_REQUEST_TIMEOUT_MS ?? 15_000 + ), + polymarketClobMaxRetries: Number(process.env.POLYMARKET_CLOB_MAX_RETRIES ?? 1), + polymarketClobRetryDelayMs: Number(process.env.POLYMARKET_CLOB_RETRY_DELAY_MS ?? 250), uniswapV3Factory: process.env.UNISWAP_V3_FACTORY ? getAddress(process.env.UNISWAP_V3_FACTORY) : undefined, diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index 5c28ffb6..51a37cc6 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -1,11 +1,36 @@ import crypto from 'node:crypto'; const DEFAULT_CLOB_HOST = 'https://clob.polymarket.com'; +const DEFAULT_CLOB_REQUEST_TIMEOUT_MS = 15_000; +const DEFAULT_CLOB_MAX_RETRIES = 1; +const DEFAULT_CLOB_RETRY_DELAY_MS = 250; + +function normalizeNonNegativeInteger(value, fallback) { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0) return fallback; + return Math.floor(parsed); +} function normalizeClobHost(host) { return (host ?? DEFAULT_CLOB_HOST).replace(/\/+$/, ''); } +function shouldRetryResponseStatus(status) { + return status === 429 || status >= 500; +} + +function shouldRetryError(error) { + if (!error) return false; + if (error.name === 'AbortError' || error.name === 'TimeoutError') return true; + if (error.name === 'TypeError') return true; + return false; +} + +async function sleep(ms) { + if (ms <= 0) return; + await new Promise((resolve) => setTimeout(resolve, ms)); +} + function buildClobAuthHeaders({ config, signingAddress, @@ -53,38 +78,69 @@ async function clobRequest({ }) { const host = normalizeClobHost(config.polymarketClobHost); const bodyText = body === undefined ? '' : JSON.stringify(body); - const timestamp = Math.floor(Date.now() / 1000); - const headers = { - 'Content-Type': 'application/json', - ...buildClobAuthHeaders({ - config, - signingAddress, - timestamp, - method, - path, - bodyText, - }), - }; - const response = await fetch(`${host}${path}`, { - method, - headers, - body: body === undefined ? undefined : bodyText, - }); - const text = await response.text(); - let parsed; - try { - parsed = text ? JSON.parse(text) : null; - } catch (error) { - parsed = { raw: text }; - } - - if (!response.ok) { - throw new Error( - `CLOB request failed (${method} ${path}): ${response.status} ${response.statusText} ${text}` - ); + const timeoutMs = normalizeNonNegativeInteger( + config.polymarketClobRequestTimeoutMs, + DEFAULT_CLOB_REQUEST_TIMEOUT_MS + ); + const maxRetries = normalizeNonNegativeInteger( + config.polymarketClobMaxRetries, + DEFAULT_CLOB_MAX_RETRIES + ); + const retryDelayMs = normalizeNonNegativeInteger( + config.polymarketClobRetryDelayMs, + DEFAULT_CLOB_RETRY_DELAY_MS + ); + + for (let attempt = 0; attempt <= maxRetries; attempt += 1) { + const timestamp = Math.floor(Date.now() / 1000); + const headers = { + 'Content-Type': 'application/json', + ...buildClobAuthHeaders({ + config, + signingAddress, + timestamp, + method, + path, + bodyText, + }), + }; + + try { + const response = await fetch(`${host}${path}`, { + method, + headers, + body: body === undefined ? undefined : bodyText, + signal: AbortSignal.timeout(timeoutMs), + }); + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : null; + } catch (error) { + parsed = { raw: text }; + } + + if (!response.ok) { + if (attempt < maxRetries && shouldRetryResponseStatus(response.status)) { + await sleep(retryDelayMs); + continue; + } + throw new Error( + `CLOB request failed (${method} ${path}): ${response.status} ${response.statusText} ${text}` + ); + } + + return parsed; + } catch (error) { + if (attempt < maxRetries && shouldRetryError(error)) { + await sleep(retryDelayMs); + continue; + } + throw error; + } } - return parsed; + throw new Error(`CLOB request failed (${method} ${path}) after retries.`); } async function placeClobOrder({ diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 05fc4f7c..e4d56043 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -461,7 +461,7 @@ function toolDefinitions({ description: 'Optional asset id used when mode=market.', }, }, - required: ['mode', 'orderIds', 'market', 'assetId'], + required: ['mode'], }, } ); From 542d141ee1f602570d5f06fe736168ccbc286843 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 12:53:16 -0800 Subject: [PATCH 130/174] do not allow delegatecall operations for ctf transactions Signed-off-by: John Shutt --- agent/scripts/test-build-og-transactions.mjs | 19 +++++++++++++++++++ agent/src/lib/tx.js | 8 ++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/agent/scripts/test-build-og-transactions.mjs b/agent/scripts/test-build-og-transactions.mjs index 1cc6ff28..de34ca03 100644 --- a/agent/scripts/test-build-og-transactions.mjs +++ b/agent/scripts/test-build-og-transactions.mjs @@ -71,6 +71,9 @@ function run() { assert.equal(ctfTxs[0].to.toLowerCase(), usdc.toLowerCase()); assert.equal(ctfTxs[1].to.toLowerCase(), usdc.toLowerCase()); assert.equal(ctfTxs[2].to.toLowerCase(), ctf.toLowerCase()); + assert.equal(ctfTxs[0].operation, 0); + assert.equal(ctfTxs[1].operation, 0); + assert.equal(ctfTxs[2].operation, 0); const resetApproveCall = decodeFunctionData({ abi: erc20Abi, @@ -100,6 +103,22 @@ function run() { assert.deepEqual(splitCall.args[3], [1n, 2n]); assert.equal(splitCall.args[4], 250000n); + const ctfRedeemTxs = buildOgTransactions( + [ + { + kind: 'ctf_redeem', + ctfContract: ctf, + collateralToken: usdc, + conditionId, + indexSets: [1, 2], + operation: 1, + }, + ], + { config: {} } + ); + assert.equal(ctfRedeemTxs.length, 1); + assert.equal(ctfRedeemTxs[0].operation, 0); + console.log('[test] buildOgTransactions uniswap + ctf_split actions OK'); } diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index 87a265c7..53d6d452 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -475,7 +475,7 @@ function buildOgTransactions(actions, options = {}) { to: collateralToken, value: '0', data: resetApproveData, - operation, + operation: 0, }); const approveData = encodeFunctionData({ @@ -487,7 +487,7 @@ function buildOgTransactions(actions, options = {}) { to: collateralToken, value: '0', data: approveData, - operation, + operation: 0, }); } @@ -508,7 +508,7 @@ function buildOgTransactions(actions, options = {}) { to: ctfContract, value: '0', data, - operation, + operation: 0, }); continue; } @@ -543,7 +543,7 @@ function buildOgTransactions(actions, options = {}) { to: ctfContract, value: '0', data, - operation, + operation: 0, }); continue; } From 443c3ba20166e8f705ea4b56031e34bf56999e66 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 12:55:19 -0800 Subject: [PATCH 131/174] decouple clob identity from runtime signer Signed-off-by: John Shutt --- .../test-polymarket-tool-normalization.mjs | 30 +++++++++++++++++++ agent/src/lib/config.js | 3 ++ agent/src/lib/tools.js | 21 +++++++++---- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/agent/scripts/test-polymarket-tool-normalization.mjs b/agent/scripts/test-polymarket-tool-normalization.mjs index 0c2ad7f9..a7c36dfc 100644 --- a/agent/scripts/test-polymarket-tool-normalization.mjs +++ b/agent/scripts/test-polymarket-tool-normalization.mjs @@ -108,6 +108,36 @@ async function run() { assert.equal(mismatchedIdentityOut.status, 'error'); assert.match(mismatchedIdentityOut.message, /signedOrder identity mismatch/); + const configuredIdentityMatch = await executeToolCalls({ + toolCalls: [ + { + callId: 'configured-identity-match', + name: 'polymarket_clob_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + signedOrder: { + side: 'BUY', + tokenId: '123', + maker: '0x3333333333333333333333333333333333333333', + }, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config: { + ...config, + polymarketClobAddress: '0x3333333333333333333333333333333333333333', + }, + ogContext: null, + }); + const configuredIdentityMatchOut = parseToolOutput(configuredIdentityMatch[0]); + assert.equal(configuredIdentityMatchOut.status, 'error'); + assert.match(configuredIdentityMatchOut.message, /Missing CLOB credentials/); + const invalidCancelMode = await executeToolCalls({ toolCalls: [ { diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index 49191318..e942de07 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -65,6 +65,9 @@ function buildConfig() { ? false : process.env.POLYMARKET_CLOB_ENABLED.toLowerCase() !== 'false', polymarketClobHost: process.env.POLYMARKET_CLOB_HOST ?? 'https://clob.polymarket.com', + polymarketClobAddress: process.env.POLYMARKET_CLOB_ADDRESS + ? getAddress(process.env.POLYMARKET_CLOB_ADDRESS) + : undefined, polymarketClobApiKey: process.env.POLYMARKET_CLOB_API_KEY, polymarketClobApiSecret: process.env.POLYMARKET_CLOB_API_SECRET, polymarketClobApiPassphrase: process.env.POLYMARKET_CLOB_API_PASSPHRASE, diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index e4d56043..12c83e8c 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -526,6 +526,10 @@ async function executeToolCalls({ } try { + const runtimeSignerAddress = getAddress(account.address); + const clobAuthAddress = config.polymarketClobAddress + ? getAddress(config.polymarketClobAddress) + : runtimeSignerAddress; const declaredSide = normalizeOrderSide(args.side); if (!declaredSide) { throw new Error('side must be BUY or SELL'); @@ -555,14 +559,17 @@ async function executeToolCalls({ `signedOrder token mismatch: declared ${declaredTokenId}, signed order has ${signedOrderTokenId}.` ); } - const signerAddress = getAddress(account.address); const identityAddresses = extractSignedOrderIdentityAddresses(args.signedOrder); + const allowedIdentityAddresses = new Set([ + clobAuthAddress, + runtimeSignerAddress, + ]); if ( identityAddresses.length > 0 && - !identityAddresses.some((address) => address === signerAddress) + !identityAddresses.some((address) => allowedIdentityAddresses.has(address)) ) { throw new Error( - `signedOrder identity mismatch: expected ${signerAddress}, signed order contains ${identityAddresses.join(', ')}.` + `signedOrder identity mismatch: expected one of ${Array.from(allowedIdentityAddresses).join(', ')}, signed order contains ${identityAddresses.join(', ')}.` ); } const configuredOwnerApiKey = config.polymarketClobApiKey; @@ -580,7 +587,7 @@ async function executeToolCalls({ } const result = await placeClobOrder({ config, - signingAddress: account.address, + signingAddress: clobAuthAddress, signedOrder: args.signedOrder, ownerApiKey: configuredOwnerApiKey, orderType, @@ -620,13 +627,17 @@ async function executeToolCalls({ } try { + const runtimeSignerAddress = getAddress(account.address); + const clobAuthAddress = config.polymarketClobAddress + ? getAddress(config.polymarketClobAddress) + : runtimeSignerAddress; const mode = normalizeCancelMode(args.mode); if (!mode) { throw new Error('mode must be one of ids, market, all'); } const result = await cancelClobOrders({ config, - signingAddress: account.address, + signingAddress: clobAuthAddress, mode, orderIds: args.orderIds, market: args.market, From 78d3c570fb7fb4fcae58c2d4e8d7adf007894a0f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Tue, 10 Feb 2026 13:12:35 -0800 Subject: [PATCH 132/174] do not retry orders Signed-off-by: John Shutt --- .../test-polymarket-request-retries.mjs | 75 +++++++++++++++++++ agent/src/lib/polymarket.js | 15 +++- 2 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 agent/scripts/test-polymarket-request-retries.mjs diff --git a/agent/scripts/test-polymarket-request-retries.mjs b/agent/scripts/test-polymarket-request-retries.mjs new file mode 100644 index 00000000..ed816c19 --- /dev/null +++ b/agent/scripts/test-polymarket-request-retries.mjs @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import { cancelClobOrders, placeClobOrder } from '../src/lib/polymarket.js'; + +const TEST_CONFIG = { + polymarketClobHost: 'https://clob.polymarket.com', + polymarketClobApiKey: 'test-api-key', + polymarketClobApiSecret: Buffer.from('test-secret').toString('base64'), + polymarketClobApiPassphrase: 'test-passphrase', + polymarketClobRequestTimeoutMs: 1_000, + polymarketClobMaxRetries: 2, + polymarketClobRetryDelayMs: 0, +}; + +function jsonResponse(status, body, statusText = '') { + const text = JSON.stringify(body); + return { + ok: status >= 200 && status < 300, + status, + statusText, + async text() { + return text; + }, + }; +} + +async function run() { + const originalFetch = globalThis.fetch; + try { + let orderCalls = 0; + globalThis.fetch = async () => { + orderCalls += 1; + return jsonResponse(500, { error: 'temporary failure' }, 'Internal Server Error'); + }; + + await assert.rejects( + placeClobOrder({ + config: TEST_CONFIG, + signingAddress: '0x1111111111111111111111111111111111111111', + signedOrder: { + maker: '0x1111111111111111111111111111111111111111', + tokenId: '1', + side: 'BUY', + }, + ownerApiKey: 'owner-key', + orderType: 'GTC', + }), + /CLOB request failed \(POST \/order\): 500/ + ); + assert.equal(orderCalls, 1); + + let cancelCalls = 0; + globalThis.fetch = async () => { + cancelCalls += 1; + if (cancelCalls === 1) { + return jsonResponse(500, { error: 'temporary failure' }, 'Internal Server Error'); + } + return jsonResponse(200, { canceled: ['order-1'] }); + }; + + const cancelResult = await cancelClobOrders({ + config: TEST_CONFIG, + signingAddress: '0x1111111111111111111111111111111111111111', + mode: 'ids', + orderIds: ['order-1'], + }); + assert.deepEqual(cancelResult, { canceled: ['order-1'] }); + assert.equal(cancelCalls, 2); + + console.log('[test] polymarket request retries OK'); + } finally { + globalThis.fetch = originalFetch; + } +} + +run(); diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index 51a37cc6..dc97194a 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -26,6 +26,14 @@ function shouldRetryError(error) { return false; } +function canRetryRequest({ method, path }) { + const normalizedMethod = method.toUpperCase(); + if (normalizedMethod === 'POST' && path === '/order') { + return false; + } + return true; +} + async function sleep(ms) { if (ms <= 0) return; await new Promise((resolve) => setTimeout(resolve, ms)); @@ -86,12 +94,13 @@ async function clobRequest({ config.polymarketClobMaxRetries, DEFAULT_CLOB_MAX_RETRIES ); + const retriesAllowed = canRetryRequest({ method, path }) ? maxRetries : 0; const retryDelayMs = normalizeNonNegativeInteger( config.polymarketClobRetryDelayMs, DEFAULT_CLOB_RETRY_DELAY_MS ); - for (let attempt = 0; attempt <= maxRetries; attempt += 1) { + for (let attempt = 0; attempt <= retriesAllowed; attempt += 1) { const timestamp = Math.floor(Date.now() / 1000); const headers = { 'Content-Type': 'application/json', @@ -121,7 +130,7 @@ async function clobRequest({ } if (!response.ok) { - if (attempt < maxRetries && shouldRetryResponseStatus(response.status)) { + if (attempt < retriesAllowed && shouldRetryResponseStatus(response.status)) { await sleep(retryDelayMs); continue; } @@ -132,7 +141,7 @@ async function clobRequest({ return parsed; } catch (error) { - if (attempt < maxRetries && shouldRetryError(error)) { + if (attempt < retriesAllowed && shouldRetryError(error)) { await sleep(retryDelayMs); continue; } From 1ac4e19bc734325460b58cdae8e7240f5314214e Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 11 Feb 2026 10:47:16 -0800 Subject: [PATCH 133/174] check that all addresses are allowed in a multi-address order Signed-off-by: John Shutt --- .../test-polymarket-tool-normalization.mjs | 94 ++++++++++++++++++- agent/src/lib/tools.js | 94 +++++++++---------- 2 files changed, 138 insertions(+), 50 deletions(-) diff --git a/agent/scripts/test-polymarket-tool-normalization.mjs b/agent/scripts/test-polymarket-tool-normalization.mjs index a7c36dfc..e74124f8 100644 --- a/agent/scripts/test-polymarket-tool-normalization.mjs +++ b/agent/scripts/test-polymarket-tool-normalization.mjs @@ -67,7 +67,11 @@ async function run() { side: ' buy ', tokenId: '123', orderType: ' gtc ', - signedOrder: { side: 'BUY', tokenId: '123' }, + signedOrder: { + side: 'BUY', + tokenId: '123', + maker: TEST_ACCOUNT.address, + }, }, }, ], @@ -81,6 +85,32 @@ async function run() { assert.equal(normalizedOrderTypeOut.status, 'error'); assert.match(normalizedOrderTypeOut.message, /Missing CLOB credentials/); + const missingIdentity = await executeToolCalls({ + toolCalls: [ + { + callId: 'missing-identity', + name: 'polymarket_clob_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + signedOrder: { + side: 'BUY', + tokenId: '123', + }, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const missingIdentityOut = parseToolOutput(missingIdentity[0]); + assert.equal(missingIdentityOut.status, 'error'); + assert.match(missingIdentityOut.message, /must include an identity field/); + const mismatchedIdentity = await executeToolCalls({ toolCalls: [ { @@ -108,6 +138,68 @@ async function run() { assert.equal(mismatchedIdentityOut.status, 'error'); assert.match(mismatchedIdentityOut.message, /signedOrder identity mismatch/); + const mixedIdentityWrappedOrder = await executeToolCalls({ + toolCalls: [ + { + callId: 'mixed-identity-wrapped-order', + name: 'polymarket_clob_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + signedOrder: { + // Wrapper identity should not override nested submitted order identity. + maker: TEST_ACCOUNT.address, + order: { + side: 'BUY', + tokenId: '123', + maker: '0x3333333333333333333333333333333333333333', + }, + }, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const mixedIdentityWrappedOrderOut = parseToolOutput(mixedIdentityWrappedOrder[0]); + assert.equal(mixedIdentityWrappedOrderOut.status, 'error'); + assert.match(mixedIdentityWrappedOrderOut.message, /signedOrder identity mismatch/); + + const missingNestedSideInWrappedOrder = await executeToolCalls({ + toolCalls: [ + { + callId: 'missing-nested-side', + name: 'polymarket_clob_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + signedOrder: { + // Nested payload is what will be submitted and must carry side/token. + side: 'BUY', + maker: TEST_ACCOUNT.address, + order: { + tokenId: '123', + maker: TEST_ACCOUNT.address, + }, + }, + }, + }, + ], + publicClient: {}, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const missingNestedSideInWrappedOrderOut = parseToolOutput(missingNestedSideInWrappedOrder[0]); + assert.equal(missingNestedSideInWrappedOrderOut.status, 'error'); + assert.match(missingNestedSideInWrappedOrderOut.message, /must include embedded side and token id/); + const configuredIdentityMatch = await executeToolCalls({ toolCalls: [ { diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 12c83e8c..86752b2b 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -58,61 +58,47 @@ function maybeAddress(value) { } } -function extractSignedOrderSideAndTokenId(signedOrder) { +function normalizeSignedOrderPayload(signedOrder) { if (!signedOrder || typeof signedOrder !== 'object') { - return { side: undefined, tokenId: undefined }; + return undefined; } + return signedOrder.order && typeof signedOrder.order === 'object' + ? signedOrder.order + : signedOrder; +} - const container = - signedOrder.order && typeof signedOrder.order === 'object' - ? signedOrder.order - : signedOrder; +function extractSignedOrderSideAndTokenId(orderPayload) { + if (!orderPayload || typeof orderPayload !== 'object') { + return { side: undefined, tokenId: undefined }; + } - const side = normalizeOrderSide(container.side ?? signedOrder.side); + const side = normalizeOrderSide(orderPayload.side); const tokenId = getFirstString([ - container.tokenId, - container.tokenID, - container.token_id, - container.assetId, - container.assetID, - container.asset_id, - signedOrder.tokenId, - signedOrder.tokenID, - signedOrder.token_id, - signedOrder.assetId, - signedOrder.assetID, - signedOrder.asset_id, + orderPayload.tokenId, + orderPayload.tokenID, + orderPayload.token_id, + orderPayload.assetId, + orderPayload.assetID, + orderPayload.asset_id, ]); return { side, tokenId }; } -function extractSignedOrderIdentityAddresses(signedOrder) { - if (!signedOrder || typeof signedOrder !== 'object') { +function extractSignedOrderIdentityAddresses(orderPayload) { + if (!orderPayload || typeof orderPayload !== 'object') { return []; } - const container = - signedOrder.order && typeof signedOrder.order === 'object' - ? signedOrder.order - : signedOrder; const candidates = [ - container.signer, - container.signerAddress, - container.maker, - container.makerAddress, - container.funder, - container.funderAddress, - container.user, - container.userAddress, - signedOrder.signer, - signedOrder.signerAddress, - signedOrder.maker, - signedOrder.makerAddress, - signedOrder.funder, - signedOrder.funderAddress, - signedOrder.user, - signedOrder.userAddress, + orderPayload.signer, + orderPayload.signerAddress, + orderPayload.maker, + orderPayload.makerAddress, + orderPayload.funder, + orderPayload.funderAddress, + orderPayload.user, + orderPayload.userAddress, ]; const normalized = candidates.map(maybeAddress).filter(Boolean); @@ -530,6 +516,10 @@ async function executeToolCalls({ const clobAuthAddress = config.polymarketClobAddress ? getAddress(config.polymarketClobAddress) : runtimeSignerAddress; + const normalizedSignedOrder = normalizeSignedOrderPayload(args.signedOrder); + if (!normalizedSignedOrder) { + throw new Error('signedOrder is required and must be an object.'); + } const declaredSide = normalizeOrderSide(args.side); if (!declaredSide) { throw new Error('side must be BUY or SELL'); @@ -543,7 +533,7 @@ async function executeToolCalls({ throw new Error('orderType must be one of GTC, GTD, FOK, FAK'); } const { side: signedOrderSide, tokenId: signedOrderTokenId } = - extractSignedOrderSideAndTokenId(args.signedOrder); + extractSignedOrderSideAndTokenId(normalizedSignedOrder); if (!signedOrderSide || !signedOrderTokenId) { throw new Error( 'signedOrder must include embedded side and token id (side + tokenId/asset_id).' @@ -559,17 +549,23 @@ async function executeToolCalls({ `signedOrder token mismatch: declared ${declaredTokenId}, signed order has ${signedOrderTokenId}.` ); } - const identityAddresses = extractSignedOrderIdentityAddresses(args.signedOrder); + const identityAddresses = + extractSignedOrderIdentityAddresses(normalizedSignedOrder); + if (identityAddresses.length === 0) { + throw new Error( + 'signedOrder must include an identity field (maker/signer/funder/user).' + ); + } const allowedIdentityAddresses = new Set([ clobAuthAddress, runtimeSignerAddress, ]); - if ( - identityAddresses.length > 0 && - !identityAddresses.some((address) => allowedIdentityAddresses.has(address)) - ) { + const unauthorizedIdentities = identityAddresses.filter( + (address) => !allowedIdentityAddresses.has(address) + ); + if (unauthorizedIdentities.length > 0) { throw new Error( - `signedOrder identity mismatch: expected one of ${Array.from(allowedIdentityAddresses).join(', ')}, signed order contains ${identityAddresses.join(', ')}.` + `signedOrder identity mismatch: expected only ${Array.from(allowedIdentityAddresses).join(', ')}, signed order contains unauthorized ${unauthorizedIdentities.join(', ')}.` ); } const configuredOwnerApiKey = config.polymarketClobApiKey; @@ -588,7 +584,7 @@ async function executeToolCalls({ const result = await placeClobOrder({ config, signingAddress: clobAuthAddress, - signedOrder: args.signedOrder, + signedOrder: normalizedSignedOrder, ownerApiKey: configuredOwnerApiKey, orderType, }); From a9636aa759123b5974ce9e6f1e664b9d0750bc6e Mon Sep 17 00:00:00 2001 From: John Shutt Date: Wed, 11 Feb 2026 11:04:00 -0800 Subject: [PATCH 134/174] add polymarket tooling documentation Signed-off-by: John Shutt --- agent/.env.example | 11 +++++ agent/README.md | 118 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/agent/.env.example b/agent/.env.example index 7f20b76b..881b9cba 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -33,6 +33,17 @@ PRIVATE_KEY=0x... # Optional tuning POLL_INTERVAL_MS=60000 WATCH_NATIVE_BALANCE=true +# Optional Polymarket config +# POLYMARKET_CONDITIONAL_TOKENS=0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 +# POLYMARKET_CLOB_ENABLED=false +# POLYMARKET_CLOB_HOST=https://clob.polymarket.com +# POLYMARKET_CLOB_ADDRESS= +# POLYMARKET_CLOB_API_KEY= +# POLYMARKET_CLOB_API_SECRET= +# POLYMARKET_CLOB_API_PASSPHRASE= +# POLYMARKET_CLOB_REQUEST_TIMEOUT_MS=15000 +# POLYMARKET_CLOB_MAX_RETRIES=1 +# POLYMARKET_CLOB_RETRY_DELAY_MS=250 # Optional Uniswap config overrides (otherwise chain defaults are used) # UNISWAP_V3_FACTORY= # UNISWAP_V3_QUOTER= diff --git a/agent/README.md b/agent/README.md index 041beca4..ae263426 100644 --- a/agent/README.md +++ b/agent/README.md @@ -25,7 +25,7 @@ This is beta software provided “as is.” Use at your own risk. No guarantees - `keychain`: `KEYCHAIN_SERVICE`, `KEYCHAIN_ACCOUNT` (macOS Keychain or Linux Secret Service) - `vault`: `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_SECRET_PATH`, optional `VAULT_SECRET_KEY` (default `private_key`) - `kms`/`vault-signer`/`rpc`: `SIGNER_RPC_URL`, `SIGNER_ADDRESS` (JSON-RPC signer that accepts `eth_sendTransaction`) - - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE`, `UNISWAP_V3_FACTORY`, `UNISWAP_V3_QUOTER`, `UNISWAP_V3_FEE_TIERS` + - Optional tuning: `POLL_INTERVAL_MS`, `START_BLOCK`, `WATCH_NATIVE_BALANCE`, `DEFAULT_DEPOSIT_*`, `AGENT_MODULE`, `UNISWAP_V3_FACTORY`, `UNISWAP_V3_QUOTER`, `UNISWAP_V3_FEE_TIERS`, `POLYMARKET_*` - Optional proposals: `PROPOSE_ENABLED` (default true), `ALLOW_PROPOSE_ON_SIMULATION_FAIL` (default false) - Optional disputes: `DISPUTE_ENABLED` (default true), `DISPUTE_RETRY_MS` (default 60000) - Optional LLM: `OPENAI_API_KEY`, `OPENAI_MODEL` (default `gpt-4.1-mini`), `OPENAI_BASE_URL` @@ -83,6 +83,122 @@ Export `getPriceTriggers({ commitmentText, config })` from `agent-library/agents This lets agents propose reusable Uniswap swap calldata without embedding raw ABI in prompts. +### Polymarket Support (CLOB + CTF) + +The shared tooling supports: +- Onchain Conditional Tokens Framework (CTF) actions through `build_og_transactions`. +- Offchain CLOB order placement/cancel through signed API requests. +- Direct ERC1155 deposits to the commitment Safe. + +#### Polymarket Environment Variables + +Set these when using Polymarket functionality: +- `POLYMARKET_CONDITIONAL_TOKENS`: Optional CTF contract address override used by CTF actions (default is Polymarket mainnet ConditionalTokens). +- `POLYMARKET_CLOB_ENABLED`: Enable CLOB tools (`true`/`false`, default `false`). +- `POLYMARKET_CLOB_HOST`: CLOB API host (default `https://clob.polymarket.com`). +- `POLYMARKET_CLOB_ADDRESS`: Optional address used as `POLY_ADDRESS` for CLOB auth (for proxy/funder setups). Defaults to runtime signer address. +- `POLYMARKET_CLOB_API_KEY`, `POLYMARKET_CLOB_API_SECRET`, `POLYMARKET_CLOB_API_PASSPHRASE`: Required for authenticated CLOB calls. +- `POLYMARKET_CLOB_REQUEST_TIMEOUT_MS`, `POLYMARKET_CLOB_MAX_RETRIES`, `POLYMARKET_CLOB_RETRY_DELAY_MS`: Optional request tuning. + +#### Execution Modes + +- `PROPOSE_ENABLED=true` and/or `DISPUTE_ENABLED=true`: onchain tools are enabled (`build_og_transactions`, `make_deposit`, `make_erc1155_deposit`, propose/dispute tools). +- `PROPOSE_ENABLED=false` and `DISPUTE_ENABLED=false`: onchain tools are disabled. +- `POLYMARKET_CLOB_ENABLED=true`: CLOB tools can still run in this mode (`polymarket_clob_place_order`, `polymarket_clob_cancel_orders`). +- All three disabled (`PROPOSE_ENABLED=false`, `DISPUTE_ENABLED=false`, `POLYMARKET_CLOB_ENABLED=false`): monitor/opinion only. + +#### CTF Actions (`build_og_transactions`) + +Supported kinds: +- `ctf_split` +- `ctf_merge` +- `ctf_redeem` + +Example `ctf_split` action: + +```json +{ + "name": "build_og_transactions", + "arguments": { + "actions": [ + { + "kind": "ctf_split", + "collateralToken": "0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "conditionId": "0x1111111111111111111111111111111111111111111111111111111111111111", + "partition": [1, 2], + "amount": "1000000" + } + ] + } +} +``` + +`ctf_split` auto-inserts ERC20 approvals to the CTF contract (`approve(0)`, then `approve(amount)`) before `splitPosition(...)`. + +#### ERC1155 Deposit to Safe + +Use `make_erc1155_deposit` after receiving YES/NO position tokens: + +```json +{ + "name": "make_erc1155_deposit", + "arguments": { + "token": "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045", + "tokenId": "123456789", + "amount": "1", + "data": "0x" + } +} +``` + +#### CLOB Place/Cancel Tools + +`polymarket_clob_place_order` submits a pre-signed order: + +```json +{ + "name": "polymarket_clob_place_order", + "arguments": { + "side": "BUY", + "tokenId": "123456789", + "orderType": "GTC", + "signedOrder": { + "maker": "0xYourSignerOrClobAddress", + "tokenId": "123456789", + "side": "BUY" + } + } +} +``` + +`polymarket_clob_cancel_orders` supports `ids`, `market`, or `all`: + +```json +{ + "name": "polymarket_clob_cancel_orders", + "arguments": { + "mode": "ids", + "orderIds": ["order-id-1"] + } +} +``` + +#### CLOB Identity Validation Rules + +For `polymarket_clob_place_order`, the runner validates the same order payload that will be sent to `/order`: +- The submitted order must include `side` and `tokenId`/`asset_id` that match declared tool args. +- The submitted order must include at least one identity field: `maker`/`signer`/`funder`/`user` (or corresponding `*Address` variants). +- Every extracted identity address must be allowlisted: + - runtime signer address, and + - `POLYMARKET_CLOB_ADDRESS` when set. + +If any identity is outside that allowlist, the tool call is rejected before submission. + +#### CLOB Retry Behavior + +- `POST /order` is not automatically retried. +- Cancel endpoints (and other retry-eligible requests) can use configured retry settings. + ### Propose vs Dispute Modes Set `PROPOSE_ENABLED` and `DISPUTE_ENABLED` to control behavior: From fa5236308c666af9fec19b50f12639e61b6735c5 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Wed, 11 Feb 2026 17:25:14 -0500 Subject: [PATCH 135/174] register dca agent --- agent-library/agents/dca-agent/agent.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/agent-library/agents/dca-agent/agent.json b/agent-library/agents/dca-agent/agent.json index 0e164d28..666fdc33 100644 --- a/agent-library/agents/dca-agent/agent.json +++ b/agent-library/agents/dca-agent/agent.json @@ -6,8 +6,13 @@ "endpoints": [ { "name": "agentWallet", - "endpoint": "eip155:11155111:0x0000000000000000000000000000000000000000" + "endpoint": "eip155:11155111:0xff0a50c38b6dd8b1d3181bfec33b64a6d9467319" } ], - "registrations": [] + "registrations": [ + { + "agentId": "1139", + "agentRegistry": "eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e" + } + ] } From 440bdb8094b654d86888abd52e9d2ac522617a37 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Wed, 11 Feb 2026 18:01:50 -0500 Subject: [PATCH 136/174] create limit order agent and commitment --- agent-library/agents/limit-order/agent.js | 26 +++++++++++++++++++ agent-library/agents/limit-order/agent.json | 1 + .../agents/limit-order/commitment.txt | 4 +++ 3 files changed, 31 insertions(+) create mode 100644 agent-library/agents/limit-order/agent.js create mode 100644 agent-library/agents/limit-order/agent.json create mode 100644 agent-library/agents/limit-order/commitment.txt diff --git a/agent-library/agents/limit-order/agent.js b/agent-library/agents/limit-order/agent.js new file mode 100644 index 00000000..660daaf3 --- /dev/null +++ b/agent-library/agents/limit-order/agent.js @@ -0,0 +1,26 @@ +function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { + const mode = proposeEnabled && disputeEnabled + ? 'You may propose and dispute.' + : proposeEnabled + ? 'You may propose but you may not dispute.' + : disputeEnabled + ? 'You may dispute but you may not propose.' + : 'You may not propose or dispute; provide opinions only.'; + + return [ + 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor).', + 'Your own address is provided in the input as agentAddress; use it when rules refer to "the agent/themselves".', + 'Given signals and rules, recommend a course of action.', + 'Default to disputing proposals that violate the rules; prefer no-op when unsure.', + mode, + commitmentText ? `Commitment text:\n${commitmentText}` : '', + 'If an onchain action is needed, call a tool.', + 'Use build_og_transactions to construct proposal payloads, then post_bond_and_propose.', + 'Use dispute_assertion with a short human-readable explanation when disputing.', + 'If no action is needed, output strict JSON with keys: action (propose|deposit|dispute|ignore|other) and rationale (string).', + ] + .filter(Boolean) + .join(' '); +} + +export { getSystemPrompt }; diff --git a/agent-library/agents/limit-order/agent.json b/agent-library/agents/limit-order/agent.json new file mode 100644 index 00000000..71d440aa --- /dev/null +++ b/agent-library/agents/limit-order/agent.json @@ -0,0 +1 @@ +{"type":"https://eips.ethereum.org/EIPS/eip-8004#registration-v1","name":"Oya Limit Order Agent","description":"Single limit order: when Chainlink price target is hit, proposes uniswap_v3_exact_input_single so the Safe swaps its pre-funded WETH or USDC. Recipient = Safe.","image":"https://raw.githubusercontent.com/oya-commitments/oya-commitments/main/agent-library/agents/default/agent.png","endpoints":[{"name":"agentWallet","endpoint":"eip155:11155111:0x0000000000000000000000000000000000000000"}],"registrations":[]} diff --git a/agent-library/agents/limit-order/commitment.txt b/agent-library/agents/limit-order/commitment.txt new file mode 100644 index 00000000..9e209c40 --- /dev/null +++ b/agent-library/agents/limit-order/commitment.txt @@ -0,0 +1,4 @@ +When the ETH/USD price is less than or equal to 2000, the agent may propose a swap of 1 USDC (1000000 micro-USDC) for WETH using this Safe's funds. +The Safe must be pre-funded with sufficient USDC. Swap output (WETH) goes to this Safe. +Token addresses: WETH 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9, USDC 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238. +Only execute one swap. Max slippage 0.50%. From 3493fb6bd6b0fb5081bbe7d35d03b3d1e15b106e Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Wed, 11 Feb 2026 18:09:21 -0500 Subject: [PATCH 137/174] Implement limit-order agent with Chainlink price, enrichSignals, and validateToolCalls --- agent-library/agents/limit-order/agent.js | 445 +++++++++++++++++++++- 1 file changed, 435 insertions(+), 10 deletions(-) diff --git a/agent-library/agents/limit-order/agent.js b/agent-library/agents/limit-order/agent.js index 660daaf3..74715f61 100644 --- a/agent-library/agents/limit-order/agent.js +++ b/agent-library/agents/limit-order/agent.js @@ -1,3 +1,179 @@ +// Limit Order Agent - Single limit order on Sepolia (WETH/USDC) + +import { erc20Abi, parseAbi } from 'viem'; + +const TOKENS = Object.freeze({ + WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', + USDC: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238', +}); +const CHAINLINK_ETH_USD_FEED_SEPOLIA = '0x694AA1769357215DE4FAC081bf1f309aDC325306'; +const DEFAULT_ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; +const ALLOWED_ROUTERS = new Set([DEFAULT_ROUTER]); +const ALLOWED_FEE_TIERS = new Set([500, 3000, 10000]); +const QUOTER_CANDIDATES_BY_CHAIN = new Map([ + [1, ['0x61fFE014bA17989E743c5F6cB21bF9697530B21e', '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6']], + [11155111, ['0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3', '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6']], +]); +const SLIPPAGE_BPS = 50; + +const chainlinkAbi = parseAbi([ + 'function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)', +]); +const quoterV2Abi = [ + { + type: 'function', + name: 'quoteExactInputSingle', + stateMutability: 'nonpayable', + inputs: [ + { + name: 'params', + type: 'tuple', + components: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'fee', type: 'uint24' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' }, + ], + }, + ], + outputs: [ + { name: 'amountOut', type: 'uint256' }, + { name: 'sqrtPriceX96After', type: 'uint160' }, + { name: 'initializedTicksCrossed', type: 'uint32' }, + { name: 'gasEstimate', type: 'uint256' }, + ], + }, +]; +const quoterV1Abi = [ + { + type: 'function', + name: 'quoteExactInputSingle', + stateMutability: 'view', + inputs: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'fee', type: 'uint24' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' }, + ], + outputs: [{ name: 'amountOut', type: 'uint256' }], + }, +]; + +let lastPollTimestamp = Date.now(); +let limitOrderState = { + proposalBuilt: false, + proposalPosted: false, + orderFilled: false, + proposalSubmitHash: null, + proposalSubmitMs: null, +}; + +function normalizeAddress(value) { + if (typeof value !== 'string' || value.length !== 42 || !value.startsWith('0x')) { + throw new Error(`Invalid address: ${value}`); + } + return value.toLowerCase(); +} + +async function getEthPriceUSD(publicClient, chainlinkFeedAddress) { + const result = await publicClient.readContract({ + address: chainlinkFeedAddress, + abi: chainlinkAbi, + functionName: 'latestRoundData', + }); + const answer = result[1]; + if (answer <= 0n) { + throw new Error('Invalid Chainlink ETH/USD price'); + } + return Number(answer) / 1e8; +} + +async function getEthPriceUSDFallback() { + const response = await fetch( + 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd' + ); + if (!response.ok) { + throw new Error(`Coingecko API error: ${response.status}`); + } + const data = await response.json(); + if (!data?.ethereum?.usd) { + throw new Error('Invalid Coingecko response'); + } + return data.ethereum.usd; +} + +async function resolveQuoterCandidates({ publicClient, config }) { + if (config?.uniswapV3Quoter) { + return [normalizeAddress(String(config.uniswapV3Quoter))]; + } + const chainId = await publicClient.getChainId(); + const byChain = QUOTER_CANDIDATES_BY_CHAIN.get(Number(chainId)); + if (!Array.isArray(byChain) || byChain.length === 0) { + throw new Error(`No Uniswap V3 quoter configured for chainId ${chainId}. Set UNISWAP_V3_QUOTER.`); + } + return byChain.map((v) => normalizeAddress(v)); +} + +async function tryQuoteV2({ publicClient, quoter, tokenIn, tokenOut, fee, amountIn }) { + const quoteCall = await publicClient.simulateContract({ + address: quoter, + abi: quoterV2Abi, + functionName: 'quoteExactInputSingle', + args: [{ tokenIn, tokenOut, fee, amountIn, sqrtPriceLimitX96: 0n }], + }); + const result = quoteCall?.result; + return Array.isArray(result) && result.length > 0 ? BigInt(result[0]) : BigInt(result ?? 0n); +} + +async function tryQuoteV1({ publicClient, quoter, tokenIn, tokenOut, fee, amountIn }) { + const quoteCall = await publicClient.simulateContract({ + address: quoter, + abi: quoterV1Abi, + functionName: 'quoteExactInputSingle', + args: [tokenIn, tokenOut, fee, amountIn, 0n], + }); + return BigInt(quoteCall?.result ?? 0n); +} + +async function quoteMinOutWithSlippage({ publicClient, config, tokenIn, tokenOut, fee, amountIn }) { + const quoters = await resolveQuoterCandidates({ publicClient, config }); + let quotedAmountOut = 0n; + let selectedQuoter = null; + const failures = []; + + for (const quoter of quoters) { + try { + quotedAmountOut = await tryQuoteV2({ publicClient, quoter, tokenIn, tokenOut, fee, amountIn }); + selectedQuoter = quoter; + break; + } catch (v2Error) { + try { + quotedAmountOut = await tryQuoteV1({ publicClient, quoter, tokenIn, tokenOut, fee, amountIn }); + selectedQuoter = quoter; + break; + } catch (v1Error) { + failures.push( + `${quoter}: ${v1Error?.message ?? v2Error?.message ?? 'quote failed'}` + ); + } + } + } + + if (!selectedQuoter) { + throw new Error(`No compatible Uniswap quoter found. Tried: ${failures.join(' | ')}`); + } + if (quotedAmountOut <= 0n) { + throw new Error('Uniswap quoter returned zero output for this swap.'); + } + const minAmountOut = (quotedAmountOut * BigInt(10_000 - SLIPPAGE_BPS)) / 10_000n; + if (minAmountOut <= 0n) { + throw new Error('Swap output too small after slippage; refusing proposal.'); + } + return { minAmountOut }; +} + function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled ? 'You may propose and dispute.' @@ -8,19 +184,268 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { : 'You may not propose or dispute; provide opinions only.'; return [ - 'You are an agent monitoring an onchain commitment (Safe + Optimistic Governor).', - 'Your own address is provided in the input as agentAddress; use it when rules refer to "the agent/themselves".', - 'Given signals and rules, recommend a course of action.', - 'Default to disputing proposals that violate the rules; prefer no-op when unsure.', + 'You are a limit order agent. Read the commitment to extract the limit price, comparator (e.g. less than or equal to, greater than or equal to), and swap amount.', + 'Compare ethPriceUSD (from signals, Chainlink ETH/USD) to the limit in the commitment. When the condition is satisfied, propose a single swap of the Safe\'s funds.', + 'No deposits. The Safe must be pre-funded. Recipient of the swap is always the Safe (commitmentSafe).', + 'Read signals: ethPriceUSD (Chainlink), safeWethHuman, safeUsdcHuman (human-readable balances), limitOrderState, pendingProposal.', + 'If the price condition is met and orderFilled is false and Safe has sufficient balance and no pendingProposal, call build_og_transactions with one uniswap_v3_exact_input_single action, then post_bond_and_propose.', + 'Extract tokenIn, tokenOut, amountInWei from the commitment. Set recipient to commitmentSafe. Use router at 0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e and an allowlisted fee tier (500, 3000, 10000).', + 'If the price condition is not met, or orderFilled is true, or pendingProposal, or insufficient balance, output action=ignore.', + 'Single execution only. Never propose make_deposit; reject any such tool call.', mode, - commitmentText ? `Commitment text:\n${commitmentText}` : '', - 'If an onchain action is needed, call a tool.', - 'Use build_og_transactions to construct proposal payloads, then post_bond_and_propose.', - 'Use dispute_assertion with a short human-readable explanation when disputing.', - 'If no action is needed, output strict JSON with keys: action (propose|deposit|dispute|ignore|other) and rationale (string).', + commitmentText ? `\nCommitment:\n${commitmentText}` : '', + 'If no action is needed, output strict JSON with keys: action (propose|dispute|ignore|other) and rationale (string).', ] .filter(Boolean) .join(' '); } -export { getSystemPrompt }; +function augmentSignals(signals) { + const now = Date.now(); + lastPollTimestamp = now; + return [ + ...signals, + { + kind: 'priceSignal', + currentTimestamp: now, + lastPollTimestamp: now, + }, + ]; +} + +async function enrichSignals(signals, { publicClient, config, account, onchainPendingProposal }) { + if (!signals.some((s) => s.kind === 'priceSignal')) { + return signals; + } + + let ethPriceUSD; + try { + const feed = config?.chainlinkPriceFeed ?? CHAINLINK_ETH_USD_FEED_SEPOLIA; + ethPriceUSD = await getEthPriceUSD(publicClient, feed); + } catch { + ethPriceUSD = await getEthPriceUSDFallback(); + } + + const [safeWethWei, safeUsdcWei] = await Promise.all([ + publicClient.readContract({ + address: TOKENS.WETH, + abi: erc20Abi, + functionName: 'balanceOf', + args: [config.commitmentSafe], + }), + publicClient.readContract({ + address: TOKENS.USDC, + abi: erc20Abi, + functionName: 'balanceOf', + args: [config.commitmentSafe], + }), + ]); + + const safeWethHuman = Number(safeWethWei) / 1e18; + const safeUsdcHuman = Number(safeUsdcWei) / 1e6; + const pendingProposal = Boolean(onchainPendingProposal || limitOrderState.proposalPosted); + + return signals.map((signal) => { + if (signal.kind !== 'priceSignal') return signal; + return { + ...signal, + ethPriceUSD, + safeWethHuman, + safeUsdcHuman, + limitOrderState: { ...limitOrderState }, + pendingProposal, + }; + }); +} + +function parseCallArgs(call) { + if (call?.parsedArguments && typeof call.parsedArguments === 'object') { + return call.parsedArguments; + } + if (typeof call?.arguments === 'string') { + try { + return JSON.parse(call.arguments); + } catch { + return null; + } + } + return null; +} + +async function validateToolCalls({ + toolCalls, + signals, + commitmentText, + commitmentSafe, + publicClient, + config, + onchainPendingProposal, +}) { + const validated = []; + const safeAddress = commitmentSafe ? String(commitmentSafe).toLowerCase() : null; + + for (const call of toolCalls) { + if (call.name === 'dispute_assertion') { + validated.push(call); + continue; + } + if (call.name === 'make_deposit') { + throw new Error('Limit order agent does not use make_deposit; reject.'); + } + if (call.name === 'post_bond_and_propose') { + continue; + } + if (call.name !== 'build_og_transactions') { + continue; + } + + if (onchainPendingProposal) { + throw new Error('Pending proposal exists onchain; execute it before creating a new proposal.'); + } + if (limitOrderState.orderFilled) { + throw new Error('Limit order already filled; single-fire lock.'); + } + if (limitOrderState.proposalPosted) { + throw new Error('Proposal already submitted; wait for execution.'); + } + + const args = parseCallArgs(call); + if (!args || !Array.isArray(args.actions) || args.actions.length !== 1) { + throw new Error('build_og_transactions must include exactly one swap action.'); + } + + const action = args.actions[0]; + if (action.kind !== 'uniswap_v3_exact_input_single') { + throw new Error('Only uniswap_v3_exact_input_single is allowed.'); + } + + const tokenIn = normalizeAddress(String(action.tokenIn)); + const tokenOut = normalizeAddress(String(action.tokenOut)); + const recipient = normalizeAddress(String(action.recipient ?? safeAddress)); + const router = normalizeAddress(String(action.router ?? DEFAULT_ROUTER)); + const fee = Number(action.fee ?? 3000); + const amountInWei = BigInt(String(action.amountInWei)); + + if (!action.tokenIn || !action.tokenOut || !action.amountInWei) { + throw new Error('action must include tokenIn, tokenOut, and amountInWei.'); + } + if (tokenIn !== TOKENS.WETH && tokenIn !== TOKENS.USDC) { + throw new Error('tokenIn must be Sepolia WETH or USDC.'); + } + if (tokenOut !== TOKENS.WETH && tokenOut !== TOKENS.USDC) { + throw new Error('tokenOut must be Sepolia WETH or USDC.'); + } + if (tokenIn === tokenOut) { + throw new Error('tokenIn and tokenOut must differ.'); + } + if (recipient !== safeAddress) { + throw new Error('Recipient must be the commitment Safe.'); + } + if (!ALLOWED_ROUTERS.has(router)) { + throw new Error(`Router ${router} is not allowlisted.`); + } + if (!ALLOWED_FEE_TIERS.has(fee)) { + throw new Error(`Fee tier ${fee} is not allowlisted.`); + } + if (amountInWei <= 0n) { + throw new Error('amountInWei must be positive.'); + } + + const inputBalance = await publicClient.readContract({ + address: tokenIn, + abi: erc20Abi, + functionName: 'balanceOf', + args: [commitmentSafe], + }); + if (BigInt(inputBalance) < amountInWei) { + throw new Error('Safe has insufficient balance for this swap.'); + } + + const { minAmountOut } = await quoteMinOutWithSlippage({ + publicClient, + config, + tokenIn, + tokenOut, + fee, + amountIn: amountInWei, + }); + + action.tokenIn = tokenIn; + action.tokenOut = tokenOut; + action.router = router; + action.recipient = safeAddress; + action.operation = 0; + action.fee = fee; + action.amountInWei = amountInWei.toString(); + action.amountOutMinWei = minAmountOut.toString(); + args.actions[0] = action; + + validated.push({ ...call, parsedArguments: args }); + } + + return validated; +} + +function onToolOutput({ name, parsedOutput }) { + if (!name || !parsedOutput || parsedOutput.status === 'error') return; + + if (name === 'build_og_transactions' && parsedOutput.status === 'ok') { + limitOrderState.proposalBuilt = true; + return; + } + + if (name === 'post_bond_and_propose' && parsedOutput.status === 'submitted') { + limitOrderState.proposalPosted = true; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = parsedOutput.proposalHash ?? null; + limitOrderState.proposalSubmitMs = Date.now(); + } +} + +function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 }) { + if (executedProposalCount > 0) { + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + limitOrderState.orderFilled = true; + } + if (deletedProposalCount > 0) { + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + } +} + +async function reconcileProposalSubmission({ publicClient }) { + if (!limitOrderState.proposalPosted || !limitOrderState.proposalSubmitHash) return; + try { + const receipt = await publicClient.getTransactionReceipt({ + hash: limitOrderState.proposalSubmitHash, + }); + if (receipt?.status === 0n || receipt?.status === 'reverted') { + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + } + } catch { + if (Date.now() - (limitOrderState.proposalSubmitMs ?? 0) > 60_000) { + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + } + } +} + +export { + getSystemPrompt, + augmentSignals, + enrichSignals, + validateToolCalls, + onToolOutput, + onProposalEvents, + reconcileProposalSubmission, +}; From e33754e5c82e396c75b5ad547d40ad5b09f8be77 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Wed, 11 Feb 2026 18:10:52 -0500 Subject: [PATCH 138/174] Add test-limit-order-agent.mjs and simulate-limit-order.mjs --- .../limit-order/simulate-limit-order.mjs | 46 +++++++++++++++++++ .../limit-order/test-limit-order-agent.mjs | 30 ++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 agent-library/agents/limit-order/simulate-limit-order.mjs create mode 100644 agent-library/agents/limit-order/test-limit-order-agent.mjs diff --git a/agent-library/agents/limit-order/simulate-limit-order.mjs b/agent-library/agents/limit-order/simulate-limit-order.mjs new file mode 100644 index 00000000..e227390e --- /dev/null +++ b/agent-library/agents/limit-order/simulate-limit-order.mjs @@ -0,0 +1,46 @@ +/** + * Simulates when a limit order would trigger based on ethPrice vs limit. + * lte: trigger when ethPrice <= limitPrice (buy when cheap) + * gte: trigger when ethPrice >= limitPrice (sell when expensive) + */ +function wouldTrigger({ ethPrice, limitPrice, comparator }) { + if (comparator === 'lte') return ethPrice <= limitPrice; + if (comparator === 'gte') return ethPrice >= limitPrice; + throw new Error(`Unknown comparator: ${comparator}`); +} + +function run() { + const scenarios = [ + { + name: 'lte: price below limit -> trigger', + input: { ethPrice: 1999, limitPrice: 2000, comparator: 'lte' }, + }, + { + name: 'lte: price at limit -> trigger', + input: { ethPrice: 2000, limitPrice: 2000, comparator: 'lte' }, + }, + { + name: 'lte: price above limit -> no trigger', + input: { ethPrice: 2001, limitPrice: 2000, comparator: 'lte' }, + }, + { + name: 'gte: price above limit -> trigger', + input: { ethPrice: 2500, limitPrice: 2000, comparator: 'gte' }, + }, + { + name: 'gte: price at limit -> trigger', + input: { ethPrice: 2000, limitPrice: 2000, comparator: 'gte' }, + }, + { + name: 'gte: price below limit -> no trigger', + input: { ethPrice: 1500, limitPrice: 2000, comparator: 'gte' }, + }, + ]; + + for (const scenario of scenarios) { + const result = wouldTrigger(scenario.input); + console.log(`[sim] ${scenario.name}:`, result); + } +} + +run(); diff --git a/agent-library/agents/limit-order/test-limit-order-agent.mjs b/agent-library/agents/limit-order/test-limit-order-agent.mjs new file mode 100644 index 00000000..465fdb5d --- /dev/null +++ b/agent-library/agents/limit-order/test-limit-order-agent.mjs @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import { getSystemPrompt, augmentSignals } from './agent.js'; + +function run() { + const prompt = getSystemPrompt({ + proposeEnabled: true, + disputeEnabled: true, + commitmentText: 'When ETH/USD is less than or equal to 2000, swap 1 USDC for WETH.', + }); + + assert.ok(prompt.includes('limit order agent')); + assert.ok(prompt.includes('ethPriceUSD')); + assert.ok(prompt.includes('safeWethHuman')); + assert.ok(prompt.includes('safeUsdcHuman')); + assert.ok(prompt.includes('commitment')); + assert.ok(prompt.includes('uniswap_v3_exact_input_single')); + assert.ok(prompt.includes('make_deposit')); + + const signals = [{ kind: 'deposit' }]; + const augmented = augmentSignals(signals); + assert.equal(augmented.length, 2); + const priceSignal = augmented.find((s) => s.kind === 'priceSignal'); + assert.ok(priceSignal); + assert.ok(typeof priceSignal.currentTimestamp === 'number'); + assert.ok(typeof priceSignal.lastPollTimestamp === 'number'); + + console.log('[test] limit-order agent OK'); +} + +run(); From 9a568174c6cdc71e11f08fff8d7fe2c7647f4c1d Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Wed, 11 Feb 2026 18:48:38 -0500 Subject: [PATCH 139/174] modify commitment text --- agent-library/agents/limit-order/commitment.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agent-library/agents/limit-order/commitment.txt b/agent-library/agents/limit-order/commitment.txt index 9e209c40..c828afa4 100644 --- a/agent-library/agents/limit-order/commitment.txt +++ b/agent-library/agents/limit-order/commitment.txt @@ -1,4 +1,6 @@ -When the ETH/USD price is less than or equal to 2000, the agent may propose a swap of 1 USDC (1000000 micro-USDC) for WETH using this Safe's funds. +Limit Order Rules: Purchase 1 USDC of WETH if WETH price is below $2000. + +When the ETH/USD price is less than or equal to 2000, the agent should propose a swap of 1 USDC for WETH using this Safe's funds. The Safe must be pre-funded with sufficient USDC. Swap output (WETH) goes to this Safe. Token addresses: WETH 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9, USDC 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238. Only execute one swap. Max slippage 0.50%. From fbd54df72d2d1b595b99d3443ca9aaee8dd6de65 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Wed, 11 Feb 2026 22:13:31 -0500 Subject: [PATCH 140/174] add an additional limit order agent based on a tecnical indicator, 200D SMA --- agent-library/agents/limit-order-sma/agent.js | 484 ++++++++++++++++++ .../agents/limit-order-sma/agent.json | 1 + .../agents/limit-order-sma/commitment.txt | 6 + .../simulate-sma-limit-order.mjs | 46 ++ .../test-limit-order-sma-agent.mjs | 51 ++ 5 files changed, 588 insertions(+) create mode 100644 agent-library/agents/limit-order-sma/agent.js create mode 100644 agent-library/agents/limit-order-sma/agent.json create mode 100644 agent-library/agents/limit-order-sma/commitment.txt create mode 100644 agent-library/agents/limit-order-sma/simulate-sma-limit-order.mjs create mode 100644 agent-library/agents/limit-order-sma/test-limit-order-sma-agent.mjs diff --git a/agent-library/agents/limit-order-sma/agent.js b/agent-library/agents/limit-order-sma/agent.js new file mode 100644 index 00000000..42878674 --- /dev/null +++ b/agent-library/agents/limit-order-sma/agent.js @@ -0,0 +1,484 @@ +// SMA Limit Order Agent - Single limit order with 200-day SMA as dynamic limit on Sepolia (WETH/USDC) + +import { erc20Abi } from 'viem'; + +const TOKENS = Object.freeze({ + WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', + USDC: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238', +}); +const DEFAULT_ROUTER = '0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e'; +const ALLOWED_ROUTERS = new Set([DEFAULT_ROUTER]); +const ALLOWED_FEE_TIERS = new Set([500, 3000, 10000]); +const QUOTER_CANDIDATES_BY_CHAIN = new Map([ + [1, ['0x61fFE014bA17989E743c5F6cB21bF9697530B21e', '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6']], + [11155111, ['0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3', '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6']], +]); +const SLIPPAGE_BPS = 50; +const SMA_CACHE_TTL_MS = 12 * 60 * 60 * 1000; // 12 hours +const SMA_MIN_POINTS = 100; +const COINGECKO_MARKET_CHART_URL = + 'https://api.coingecko.com/api/v3/coins/ethereum/market_chart?vs_currency=usd&days=200'; + +const quoterV2Abi = [ + { + type: 'function', + name: 'quoteExactInputSingle', + stateMutability: 'nonpayable', + inputs: [ + { + name: 'params', + type: 'tuple', + components: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'fee', type: 'uint24' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' }, + ], + }, + ], + outputs: [ + { name: 'amountOut', type: 'uint256' }, + { name: 'sqrtPriceX96After', type: 'uint160' }, + { name: 'initializedTicksCrossed', type: 'uint32' }, + { name: 'gasEstimate', type: 'uint256' }, + ], + }, +]; +const quoterV1Abi = [ + { + type: 'function', + name: 'quoteExactInputSingle', + stateMutability: 'view', + inputs: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'fee', type: 'uint24' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' }, + ], + outputs: [{ name: 'amountOut', type: 'uint256' }], + }, +]; + +let lastPollTimestamp = Date.now(); +let limitOrderState = { + proposalBuilt: false, + proposalPosted: false, + orderFilled: false, + proposalSubmitHash: null, + proposalSubmitMs: null, +}; + +let priceDataCache = { ethPriceUSD: null, smaEth200USD: null, fetchedAt: 0 }; + +function normalizeAddress(value) { + if (typeof value !== 'string' || value.length !== 42 || !value.startsWith('0x')) { + throw new Error(`Invalid address: ${value}`); + } + return value.toLowerCase(); +} + +async function fetchEthPriceDataFromCoinGecko() { + const now = Date.now(); + if ( + priceDataCache.ethPriceUSD !== null && + priceDataCache.smaEth200USD !== null && + now - priceDataCache.fetchedAt < SMA_CACHE_TTL_MS + ) { + return { + ethPriceUSD: priceDataCache.ethPriceUSD, + smaEth200USD: priceDataCache.smaEth200USD, + fetchedAt: priceDataCache.fetchedAt, + }; + } + + const apiKey = process.env.COINGECKO_API_KEY; + const baseUrl = apiKey + ? 'https://pro-api.coingecko.com/api/v3/coins/ethereum/market_chart?vs_currency=usd&days=200' + : COINGECKO_MARKET_CHART_URL; + const headers = apiKey ? { 'x-cg-pro-api-key': apiKey } : {}; + + const response = await fetch(baseUrl, { headers }); + if (!response.ok) { + throw new Error(`CoinGecko market_chart API error: ${response.status}`); + } + const data = await response.json(); + const prices = data?.prices; + if (!Array.isArray(prices) || prices.length < SMA_MIN_POINTS) { + throw new Error( + `Insufficient price data: got ${prices?.length ?? 0} points, need at least ${SMA_MIN_POINTS}` + ); + } + + const priceValues = prices.map((p) => (Array.isArray(p) ? p[1] : 0)).filter((v) => typeof v === 'number' && v > 0); + if (priceValues.length < SMA_MIN_POINTS) { + throw new Error( + `Insufficient valid price points: got ${priceValues.length}, need at least ${SMA_MIN_POINTS}` + ); + } + + const sum = priceValues.reduce((a, b) => a + b, 0); + const smaEth200USD = sum / priceValues.length; + const ethPriceUSD = priceValues[priceValues.length - 1]; + + priceDataCache = { ethPriceUSD, smaEth200USD, fetchedAt: now }; + return { ethPriceUSD, smaEth200USD, fetchedAt: now }; +} + +async function resolveQuoterCandidates({ publicClient, config }) { + if (config?.uniswapV3Quoter) { + return [normalizeAddress(String(config.uniswapV3Quoter))]; + } + const chainId = await publicClient.getChainId(); + const byChain = QUOTER_CANDIDATES_BY_CHAIN.get(Number(chainId)); + if (!Array.isArray(byChain) || byChain.length === 0) { + throw new Error(`No Uniswap V3 quoter configured for chainId ${chainId}. Set UNISWAP_V3_QUOTER.`); + } + return byChain.map((v) => normalizeAddress(v)); +} + +async function tryQuoteV2({ publicClient, quoter, tokenIn, tokenOut, fee, amountIn }) { + const quoteCall = await publicClient.simulateContract({ + address: quoter, + abi: quoterV2Abi, + functionName: 'quoteExactInputSingle', + args: [{ tokenIn, tokenOut, fee, amountIn, sqrtPriceLimitX96: 0n }], + }); + const result = quoteCall?.result; + return Array.isArray(result) && result.length > 0 ? BigInt(result[0]) : BigInt(result ?? 0n); +} + +async function tryQuoteV1({ publicClient, quoter, tokenIn, tokenOut, fee, amountIn }) { + const quoteCall = await publicClient.simulateContract({ + address: quoter, + abi: quoterV1Abi, + functionName: 'quoteExactInputSingle', + args: [tokenIn, tokenOut, fee, amountIn, 0n], + }); + return BigInt(quoteCall?.result ?? 0n); +} + +async function quoteMinOutWithSlippage({ publicClient, config, tokenIn, tokenOut, fee, amountIn }) { + const quoters = await resolveQuoterCandidates({ publicClient, config }); + let quotedAmountOut = 0n; + let selectedQuoter = null; + const failures = []; + + for (const quoter of quoters) { + try { + quotedAmountOut = await tryQuoteV2({ publicClient, quoter, tokenIn, tokenOut, fee, amountIn }); + selectedQuoter = quoter; + break; + } catch (v2Error) { + try { + quotedAmountOut = await tryQuoteV1({ publicClient, quoter, tokenIn, tokenOut, fee, amountIn }); + selectedQuoter = quoter; + break; + } catch (v1Error) { + failures.push( + `${quoter}: ${v1Error?.message ?? v2Error?.message ?? 'quote failed'}` + ); + } + } + } + + if (!selectedQuoter) { + throw new Error(`No compatible Uniswap quoter found. Tried: ${failures.join(' | ')}`); + } + if (quotedAmountOut <= 0n) { + throw new Error('Uniswap quoter returned zero output for this swap.'); + } + const minAmountOut = (quotedAmountOut * BigInt(10_000 - SLIPPAGE_BPS)) / 10_000n; + if (minAmountOut <= 0n) { + throw new Error('Swap output too small after slippage; refusing proposal.'); + } + return { minAmountOut }; +} + +function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { + const mode = proposeEnabled && disputeEnabled + ? 'You may propose and dispute.' + : proposeEnabled + ? 'You may propose but you may not dispute.' + : disputeEnabled + ? 'You may dispute but you may not propose.' + : 'You may not propose or dispute; provide opinions only.'; + + return [ + 'You are a limit order agent with a dynamic limit price from the 200-day Simple Moving Average (SMA).', + 'The limit price is smaEth200USD (from signals), not a static value. Compare ethPriceUSD to smaEth200USD. Both come from CoinGecko 200-day market data.', + 'When ethPriceUSD <= smaEth200USD and smaEth200USD is truthy, propose a single swap of the Safe\'s funds.', + 'If smaEth200USD is null or missing, output action=ignore.', + 'No deposits. The Safe must be pre-funded. Recipient of the swap is always the Safe (commitmentSafe).', + 'Read signals: ethPriceUSD (current price), smaEth200USD (200-day SMA), safeWethHuman, safeUsdcHuman (human-readable balances), limitOrderState, pendingProposal.', + 'If the price condition is met and orderFilled is false and Safe has sufficient balance and no pendingProposal, call build_og_transactions with one uniswap_v3_exact_input_single action, then post_bond_and_propose.', + 'Extract tokenIn, tokenOut, amountInWei from the commitment. Set recipient to commitmentSafe. Use router at 0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e and an allowlisted fee tier (500, 3000, 10000).', + 'If the price condition is not met, or ethPriceUSD or smaEth200USD is null, or orderFilled is true, or pendingProposal, or insufficient balance, output action=ignore.', + 'Single execution only. Never propose make_deposit; reject any such tool call.', + mode, + commitmentText ? `\nCommitment:\n${commitmentText}` : '', + 'If no action is needed, output strict JSON with keys: action (propose|dispute|ignore|other) and rationale (string).', + ] + .filter(Boolean) + .join(' '); +} + +function augmentSignals(signals) { + const now = Date.now(); + lastPollTimestamp = now; + return [ + ...signals, + { + kind: 'priceSignal', + currentTimestamp: now, + lastPollTimestamp: now, + }, + ]; +} + +async function enrichSignals(signals, { publicClient, config, account, onchainPendingProposal }) { + if (!signals.some((s) => s.kind === 'priceSignal')) { + return signals; + } + + let ethPriceUSD = null; + let smaEth200USD = null; + let smaEth200USDAt = null; + try { + const result = await fetchEthPriceDataFromCoinGecko(); + ethPriceUSD = result.ethPriceUSD; + smaEth200USD = result.smaEth200USD; + smaEth200USDAt = result.fetchedAt; + } catch { + ethPriceUSD = null; + smaEth200USD = null; + smaEth200USDAt = null; + } + + const [safeWethWei, safeUsdcWei] = await Promise.all([ + publicClient.readContract({ + address: TOKENS.WETH, + abi: erc20Abi, + functionName: 'balanceOf', + args: [config.commitmentSafe], + }), + publicClient.readContract({ + address: TOKENS.USDC, + abi: erc20Abi, + functionName: 'balanceOf', + args: [config.commitmentSafe], + }), + ]); + + const safeWethHuman = Number(safeWethWei) / 1e18; + const safeUsdcHuman = Number(safeUsdcWei) / 1e6; + const pendingProposal = Boolean(onchainPendingProposal || limitOrderState.proposalPosted); + + return signals.map((signal) => { + if (signal.kind !== 'priceSignal') return signal; + return { + ...signal, + ethPriceUSD, + smaEth200USD, + smaEth200USDAt, + safeWethHuman, + safeUsdcHuman, + limitOrderState: { ...limitOrderState }, + pendingProposal, + }; + }); +} + +function parseCallArgs(call) { + if (call?.parsedArguments && typeof call.parsedArguments === 'object') { + return call.parsedArguments; + } + if (typeof call?.arguments === 'string') { + try { + return JSON.parse(call.arguments); + } catch { + return null; + } + } + return null; +} + +async function validateToolCalls({ + toolCalls, + signals, + commitmentText, + commitmentSafe, + publicClient, + config, + onchainPendingProposal, +}) { + const validated = []; + const safeAddress = commitmentSafe ? String(commitmentSafe).toLowerCase() : null; + + for (const call of toolCalls) { + if (call.name === 'dispute_assertion') { + validated.push(call); + continue; + } + if (call.name === 'make_deposit') { + throw new Error('Limit order agent does not use make_deposit; reject.'); + } + if (call.name === 'post_bond_and_propose') { + continue; + } + if (call.name !== 'build_og_transactions') { + continue; + } + + if (onchainPendingProposal) { + throw new Error('Pending proposal exists onchain; execute it before creating a new proposal.'); + } + if (limitOrderState.orderFilled) { + throw new Error('Limit order already filled; single-fire lock.'); + } + if (limitOrderState.proposalPosted) { + throw new Error('Proposal already submitted; wait for execution.'); + } + + const args = parseCallArgs(call); + if (!args || !Array.isArray(args.actions) || args.actions.length !== 1) { + throw new Error('build_og_transactions must include exactly one swap action.'); + } + + const action = args.actions[0]; + if (action.kind !== 'uniswap_v3_exact_input_single') { + throw new Error('Only uniswap_v3_exact_input_single is allowed.'); + } + + const tokenIn = normalizeAddress(String(action.tokenIn)); + const tokenOut = normalizeAddress(String(action.tokenOut)); + const recipient = normalizeAddress(String(action.recipient ?? safeAddress)); + const router = normalizeAddress(String(action.router ?? DEFAULT_ROUTER)); + const fee = Number(action.fee ?? 3000); + const amountInWei = BigInt(String(action.amountInWei)); + + if (!action.tokenIn || !action.tokenOut || !action.amountInWei) { + throw new Error('action must include tokenIn, tokenOut, and amountInWei.'); + } + if (tokenIn !== TOKENS.WETH && tokenIn !== TOKENS.USDC) { + throw new Error('tokenIn must be Sepolia WETH or USDC.'); + } + if (tokenOut !== TOKENS.WETH && tokenOut !== TOKENS.USDC) { + throw new Error('tokenOut must be Sepolia WETH or USDC.'); + } + if (tokenIn === tokenOut) { + throw new Error('tokenIn and tokenOut must differ.'); + } + if (recipient !== safeAddress) { + throw new Error('Recipient must be the commitment Safe.'); + } + if (!ALLOWED_ROUTERS.has(router)) { + throw new Error(`Router ${router} is not allowlisted.`); + } + if (!ALLOWED_FEE_TIERS.has(fee)) { + throw new Error(`Fee tier ${fee} is not allowlisted.`); + } + if (amountInWei <= 0n) { + throw new Error('amountInWei must be positive.'); + } + + const inputBalance = await publicClient.readContract({ + address: tokenIn, + abi: erc20Abi, + functionName: 'balanceOf', + args: [commitmentSafe], + }); + if (BigInt(inputBalance) < amountInWei) { + throw new Error('Safe has insufficient balance for this swap.'); + } + + const { minAmountOut } = await quoteMinOutWithSlippage({ + publicClient, + config, + tokenIn, + tokenOut, + fee, + amountIn: amountInWei, + }); + + action.tokenIn = tokenIn; + action.tokenOut = tokenOut; + action.router = router; + action.recipient = safeAddress; + action.operation = 0; + action.fee = fee; + action.amountInWei = amountInWei.toString(); + action.amountOutMinWei = minAmountOut.toString(); + args.actions[0] = action; + + validated.push({ ...call, parsedArguments: args }); + } + + return validated; +} + +function onToolOutput({ name, parsedOutput }) { + if (!name || !parsedOutput || parsedOutput.status === 'error') return; + + if (name === 'build_og_transactions' && parsedOutput.status === 'ok') { + limitOrderState.proposalBuilt = true; + return; + } + + if (name === 'post_bond_and_propose' && parsedOutput.status === 'submitted') { + limitOrderState.proposalPosted = true; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = parsedOutput.proposalHash ?? null; + limitOrderState.proposalSubmitMs = Date.now(); + } +} + +function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 }) { + if (executedProposalCount > 0) { + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + limitOrderState.orderFilled = true; + } + if (deletedProposalCount > 0) { + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + } +} + +async function reconcileProposalSubmission({ publicClient }) { + if (!limitOrderState.proposalPosted || !limitOrderState.proposalSubmitHash) return; + try { + const receipt = await publicClient.getTransactionReceipt({ + hash: limitOrderState.proposalSubmitHash, + }); + if (receipt?.status === 0n || receipt?.status === 'reverted') { + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + } + } catch { + if (Date.now() - (limitOrderState.proposalSubmitMs ?? 0) > 60_000) { + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + } + } +} + +export { + getSystemPrompt, + augmentSignals, + enrichSignals, + validateToolCalls, + onToolOutput, + onProposalEvents, + reconcileProposalSubmission, + fetchEthPriceDataFromCoinGecko, +}; diff --git a/agent-library/agents/limit-order-sma/agent.json b/agent-library/agents/limit-order-sma/agent.json new file mode 100644 index 00000000..96af9717 --- /dev/null +++ b/agent-library/agents/limit-order-sma/agent.json @@ -0,0 +1 @@ +{"type":"https://eips.ethereum.org/EIPS/eip-8004#registration-v1","name":"Oya SMA Limit Order Agent","description":"Single limit order with dynamic limit from 200-day SMA: when ethPriceUSD <= smaEth200USD, proposes uniswap_v3_exact_input_single so the Safe swaps its pre-funded WETH or USDC. Recipient = Safe.","image":"https://raw.githubusercontent.com/oya-commitments/oya-commitments/main/agent-library/agents/default/agent.png","endpoints":[{"name":"agentWallet","endpoint":"eip155:11155111:0x0000000000000000000000000000000000000000"}],"registrations":[]} diff --git a/agent-library/agents/limit-order-sma/commitment.txt b/agent-library/agents/limit-order-sma/commitment.txt new file mode 100644 index 00000000..0e34818e --- /dev/null +++ b/agent-library/agents/limit-order-sma/commitment.txt @@ -0,0 +1,6 @@ +Limit Order Rules (SMA-based): Purchase WETH when ETH price is at or below the 200-day Simple Moving Average. + +When ethPriceUSD (CoinGecko) is less than or equal to smaEth200USD (200-day SMA from CoinGecko), propose a swap of 1 USDC ($1 = 1000000 micro-USDC) for WETH using this Safe's funds. +The Safe must be pre-funded with sufficient USDC. Swap output (WETH) goes to this Safe. +Token addresses: WETH 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9, USDC 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238. +Only execute one swap. Max slippage 0.50%. diff --git a/agent-library/agents/limit-order-sma/simulate-sma-limit-order.mjs b/agent-library/agents/limit-order-sma/simulate-sma-limit-order.mjs new file mode 100644 index 00000000..f44afdbf --- /dev/null +++ b/agent-library/agents/limit-order-sma/simulate-sma-limit-order.mjs @@ -0,0 +1,46 @@ +/** + * Simulates when an SMA-based limit order would trigger. + * lte: trigger when ethPrice <= sma (buy when price dips below average) + * gte: trigger when ethPrice >= sma (sell when price rises above average) + */ +function wouldTrigger({ ethPrice, sma, comparator }) { + if (comparator === 'lte') return ethPrice <= sma; + if (comparator === 'gte') return ethPrice >= sma; + throw new Error(`Unknown comparator: ${comparator}`); +} + +function run() { + const scenarios = [ + { + name: 'lte: price below SMA -> trigger', + input: { ethPrice: 1999, sma: 2000, comparator: 'lte' }, + }, + { + name: 'lte: price at SMA -> trigger', + input: { ethPrice: 2000, sma: 2000, comparator: 'lte' }, + }, + { + name: 'lte: price above SMA -> no trigger', + input: { ethPrice: 2001, sma: 2000, comparator: 'lte' }, + }, + { + name: 'gte: price above SMA -> trigger', + input: { ethPrice: 2500, sma: 2000, comparator: 'gte' }, + }, + { + name: 'gte: price at SMA -> trigger', + input: { ethPrice: 2000, sma: 2000, comparator: 'gte' }, + }, + { + name: 'gte: price below SMA -> no trigger', + input: { ethPrice: 1500, sma: 2000, comparator: 'gte' }, + }, + ]; + + for (const scenario of scenarios) { + const result = wouldTrigger(scenario.input); + console.log(`[sim] ${scenario.name}:`, result); + } +} + +run(); diff --git a/agent-library/agents/limit-order-sma/test-limit-order-sma-agent.mjs b/agent-library/agents/limit-order-sma/test-limit-order-sma-agent.mjs new file mode 100644 index 00000000..b3e1630a --- /dev/null +++ b/agent-library/agents/limit-order-sma/test-limit-order-sma-agent.mjs @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict'; +import { getSystemPrompt, augmentSignals, fetchEthPriceDataFromCoinGecko } from './agent.js'; + +async function run() { + const prompt = getSystemPrompt({ + proposeEnabled: true, + disputeEnabled: true, + commitmentText: 'When ethPriceUSD <= smaEth200USD, swap 1 USDC for WETH.', + }); + + assert.ok(prompt.includes('smaEth200USD')); + assert.ok(prompt.includes('ethPriceUSD')); + assert.ok(prompt.includes('safeWethHuman')); + assert.ok(prompt.includes('safeUsdcHuman')); + assert.ok(prompt.includes('commitment')); + assert.ok(prompt.includes('uniswap_v3_exact_input_single')); + assert.ok(prompt.includes('make_deposit')); + assert.ok(prompt.includes('200-day')); + assert.ok(prompt.includes('ethPriceUSD <= smaEth200USD')); + + const signals = [{ kind: 'deposit' }]; + const augmented = augmentSignals(signals); + assert.equal(augmented.length, 2); + const priceSignal = augmented.find((s) => s.kind === 'priceSignal'); + assert.ok(priceSignal); + assert.ok(typeof priceSignal.currentTimestamp === 'number'); + assert.ok(typeof priceSignal.lastPollTimestamp === 'number'); + + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => ({ + ok: true, + json: async () => ({ + prices: Array.from({ length: 150 }, (_, i) => [ + Date.now() - (150 - i) * 86400 * 1000, + 2000 + i * 0.1, + ]), + }), + }); + try { + const { ethPriceUSD, smaEth200USD, fetchedAt } = await fetchEthPriceDataFromCoinGecko(); + assert.ok(typeof ethPriceUSD === 'number' && ethPriceUSD > 0); + assert.ok(typeof smaEth200USD === 'number' && smaEth200USD > 0); + assert.ok(typeof fetchedAt === 'number'); + } finally { + globalThis.fetch = originalFetch; + } + + console.log('[test] limit-order-sma agent OK'); +} + +run(); From 62aa0d60d3037b9cb1837c67a0e03823cc780842 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Thu, 12 Feb 2026 15:11:37 -0500 Subject: [PATCH 141/174] Guard proposal state updates behind real submission hash in limit-order agents --- agent-library/agents/limit-order-sma/agent.js | 10 ++++++---- agent-library/agents/limit-order/agent.js | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/agent-library/agents/limit-order-sma/agent.js b/agent-library/agents/limit-order-sma/agent.js index 42878674..b0e710e9 100644 --- a/agent-library/agents/limit-order-sma/agent.js +++ b/agent-library/agents/limit-order-sma/agent.js @@ -427,10 +427,12 @@ function onToolOutput({ name, parsedOutput }) { } if (name === 'post_bond_and_propose' && parsedOutput.status === 'submitted') { - limitOrderState.proposalPosted = true; - limitOrderState.proposalBuilt = false; - limitOrderState.proposalSubmitHash = parsedOutput.proposalHash ?? null; - limitOrderState.proposalSubmitMs = Date.now(); + if (parsedOutput.proposalHash) { + limitOrderState.proposalPosted = true; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = parsedOutput.proposalHash ?? null; + limitOrderState.proposalSubmitMs = Date.now(); + } } } diff --git a/agent-library/agents/limit-order/agent.js b/agent-library/agents/limit-order/agent.js index 74715f61..034cc950 100644 --- a/agent-library/agents/limit-order/agent.js +++ b/agent-library/agents/limit-order/agent.js @@ -395,10 +395,12 @@ function onToolOutput({ name, parsedOutput }) { } if (name === 'post_bond_and_propose' && parsedOutput.status === 'submitted') { - limitOrderState.proposalPosted = true; - limitOrderState.proposalBuilt = false; - limitOrderState.proposalSubmitHash = parsedOutput.proposalHash ?? null; - limitOrderState.proposalSubmitMs = Date.now(); + if (parsedOutput.proposalHash) { + limitOrderState.proposalPosted = true; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = parsedOutput.proposalHash ?? null; + limitOrderState.proposalSubmitMs = Date.now(); + } } } From 6cf4b0b790aa094175fd99751f991f1b623e75a7 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Thu, 12 Feb 2026 15:15:52 -0500 Subject: [PATCH 142/174] Persist single-fire order state across restarts by fetching from optimistic governor contracts ProposalExecuted logs --- agent-library/agents/limit-order-sma/agent.js | 32 +++++++++++++++++-- agent-library/agents/limit-order/agent.js | 31 ++++++++++++++++-- agent/src/index.js | 6 +++- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/agent-library/agents/limit-order-sma/agent.js b/agent-library/agents/limit-order-sma/agent.js index b0e710e9..157daf4c 100644 --- a/agent-library/agents/limit-order-sma/agent.js +++ b/agent-library/agents/limit-order-sma/agent.js @@ -1,6 +1,6 @@ // SMA Limit Order Agent - Single limit order with 200-day SMA as dynamic limit on Sepolia (WETH/USDC) -import { erc20Abi } from 'viem'; +import { erc20Abi, parseAbiItem } from 'viem'; const TOKENS = Object.freeze({ WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', @@ -69,9 +69,13 @@ let limitOrderState = { proposalSubmitHash: null, proposalSubmitMs: null, }; - +let hydratedFromChain = false; let priceDataCache = { ethPriceUSD: null, smaEth200USD: null, fetchedAt: 0 }; +const proposalExecutedEvent = parseAbiItem( + 'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' +); + function normalizeAddress(value) { if (typeof value !== 'string' || value.length !== 42 || !value.startsWith('0x')) { throw new Error(`Invalid address: ${value}`); @@ -452,7 +456,29 @@ function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 } } -async function reconcileProposalSubmission({ publicClient }) { +async function reconcileProposalSubmission({ publicClient, ogModule, startBlock }) { + if (!hydratedFromChain && ogModule) { + hydratedFromChain = true; + try { + const toBlock = await publicClient.getBlockNumber(); + const fromBlock = startBlock ?? 0n; + const logs = await publicClient.getLogs({ + address: ogModule, + event: proposalExecutedEvent, + fromBlock, + toBlock, + }); + if (logs.length > 0) { + limitOrderState.orderFilled = true; + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + } + } catch (err) { + console.warn('[limit-order-sma] Failed to hydrate from chain:', err?.message ?? err); + } + } if (!limitOrderState.proposalPosted || !limitOrderState.proposalSubmitHash) return; try { const receipt = await publicClient.getTransactionReceipt({ diff --git a/agent-library/agents/limit-order/agent.js b/agent-library/agents/limit-order/agent.js index 034cc950..7f3cb603 100644 --- a/agent-library/agents/limit-order/agent.js +++ b/agent-library/agents/limit-order/agent.js @@ -1,6 +1,6 @@ // Limit Order Agent - Single limit order on Sepolia (WETH/USDC) -import { erc20Abi, parseAbi } from 'viem'; +import { erc20Abi, parseAbi, parseAbiItem } from 'viem'; const TOKENS = Object.freeze({ WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', @@ -69,6 +69,11 @@ let limitOrderState = { proposalSubmitHash: null, proposalSubmitMs: null, }; +let hydratedFromChain = false; + +const proposalExecutedEvent = parseAbiItem( + 'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' +); function normalizeAddress(value) { if (typeof value !== 'string' || value.length !== 42 || !value.startsWith('0x')) { @@ -420,7 +425,29 @@ function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 } } -async function reconcileProposalSubmission({ publicClient }) { +async function reconcileProposalSubmission({ publicClient, ogModule, startBlock }) { + if (!hydratedFromChain && ogModule) { + hydratedFromChain = true; + try { + const toBlock = await publicClient.getBlockNumber(); + const fromBlock = startBlock ?? 0n; + const logs = await publicClient.getLogs({ + address: ogModule, + event: proposalExecutedEvent, + fromBlock, + toBlock, + }); + if (logs.length > 0) { + limitOrderState.orderFilled = true; + limitOrderState.proposalPosted = false; + limitOrderState.proposalBuilt = false; + limitOrderState.proposalSubmitHash = null; + limitOrderState.proposalSubmitMs = null; + } + } catch (err) { + console.warn('[limit-order] Failed to hydrate from chain:', err?.message ?? err); + } + } if (!limitOrderState.proposalPosted || !limitOrderState.proposalSubmitHash) return; try { const receipt = await publicClient.getTransactionReceipt({ diff --git a/agent/src/index.js b/agent/src/index.js index 06c3ec80..fa322aee 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -357,7 +357,11 @@ async function agentLoop() { }); } if (agentModule?.reconcileProposalSubmission) { - await agentModule.reconcileProposalSubmission({ publicClient }); + await agentModule.reconcileProposalSubmission({ + publicClient, + ogModule: config.ogModule, + startBlock: config.startBlock, + }); } await executeReadyProposals({ From 952462ee030312c3903001fd517708fc98ab9ae8 Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Thu, 12 Feb 2026 15:17:21 -0500 Subject: [PATCH 143/174] Prefer validated parsedArguments over original call.arguments when building approved tool calls --- agent/src/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/agent/src/index.js b/agent/src/index.js index fa322aee..40b1f999 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -215,9 +215,11 @@ async function decideOnSignals(signals, { onchainPendingProposal = false } = {}) name: call.name, callId: call.callId, arguments: - call.arguments !== undefined - ? call.arguments - : JSON.stringify(call.parsedArguments ?? {}), + call.parsedArguments !== undefined + ? JSON.stringify(call.parsedArguments) + : call.arguments !== undefined + ? call.arguments + : JSON.stringify({}), })); } else { approvedToolCalls = []; From 1753e904bdb4763567d11e3fcbbebb98d602ce47 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Thu, 12 Feb 2026 14:38:39 -0800 Subject: [PATCH 144/174] add copy trading agent and support for shared CLOB runtime signing support with EIP712 Signed-off-by: John Shutt --- agent-library/README.md | 1 + agent-library/agents/copy-trading/agent.js | 579 ++++++++++++++++++ agent-library/agents/copy-trading/agent.json | 13 + .../agents/copy-trading/commitment.txt | 13 + .../copy-trading/test-copy-trading-agent.mjs | 167 +++++ agent/.env.example | 1 + agent/README.md | 24 +- .../test-polymarket-tool-normalization.mjs | 144 +++++ agent/src/lib/config.js | 3 + agent/src/lib/polymarket.js | 219 ++++++- agent/src/lib/tools.js | 264 +++++++- 11 files changed, 1425 insertions(+), 3 deletions(-) create mode 100644 agent-library/agents/copy-trading/agent.js create mode 100644 agent-library/agents/copy-trading/agent.json create mode 100644 agent-library/agents/copy-trading/commitment.txt create mode 100644 agent-library/agents/copy-trading/test-copy-trading-agent.mjs diff --git a/agent-library/README.md b/agent-library/README.md index 66097e74..a07a4b1c 100644 --- a/agent-library/README.md +++ b/agent-library/README.md @@ -14,3 +14,4 @@ To add a new agent: Example agents: - `agent-library/agents/default/`: generic agent using the commitment text. - `agent-library/agents/timelock-withdraw/`: timelock withdrawal agent that only withdraws to its own address after the timelock. +- `agent-library/agents/copy-trading/`: copy-trading agent that mirrors configured Polymarket BUY trades at 99% Safe sizing, deposits YES/NO tokens, and proposes reimbursement. diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js new file mode 100644 index 00000000..d4b08b52 --- /dev/null +++ b/agent-library/agents/copy-trading/agent.js @@ -0,0 +1,579 @@ +const erc20BalanceOfAbi = [ + { + type: 'function', + name: 'balanceOf', + stateMutability: 'view', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ name: '', type: 'uint256' }], + }, +]; +const erc1155BalanceOfAbi = [ + { + type: 'function', + name: 'balanceOf', + stateMutability: 'view', + inputs: [ + { name: 'account', type: 'address' }, + { name: 'id', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'uint256' }], + }, +]; + +const DATA_API_HOST = 'https://data-api.polymarket.com'; +const COPY_BPS = 9900n; +const FEE_BPS = 100n; +const BPS_DENOMINATOR = 10_000n; +const PRICE_SCALE = 1_000_000n; +const DEFAULT_COLLATERAL_TOKEN = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; + +let copyTradingState = { + seenSourceTradeId: null, + activeSourceTradeId: null, + activeOutcome: null, + activeTokenId: null, + reimbursementAmountWei: null, + orderSubmitted: false, + tokenDeposited: false, + reimbursementProposed: false, +}; + +function normalizeAddress(value) { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + if (!/^0x[0-9a-fA-F]{40}$/.test(trimmed)) return null; + return trimmed.toLowerCase(); +} + +function normalizeTokenId(value) { + if (value === null || value === undefined || value === '') return null; + try { + const normalized = BigInt(value); + if (normalized < 0n) return null; + return normalized.toString(); + } catch (error) { + return null; + } +} + +function normalizeOutcome(value) { + if (typeof value !== 'string') return null; + const normalized = value.trim().toLowerCase(); + if (normalized === 'yes') return 'YES'; + if (normalized === 'no') return 'NO'; + return null; +} + +function normalizeTradeSide(value) { + if (typeof value !== 'string') return null; + const normalized = value.trim().toUpperCase(); + return normalized === 'BUY' || normalized === 'SELL' ? normalized : null; +} + +function normalizeTradePrice(value) { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= 1) { + return null; + } + return parsed; +} + +function parseActivityEntry(entry) { + if (!entry || typeof entry !== 'object') return null; + + const tradeId = + entry.id ?? + entry.tradeId ?? + entry.transactionHash ?? + entry.txHash ?? + entry.orderID ?? + entry.orderId; + const side = normalizeTradeSide(entry.side); + const outcome = normalizeOutcome(entry.outcome); + const price = normalizeTradePrice(entry.price); + + if (!tradeId || !side || !outcome || !price) return null; + + return { + id: String(tradeId), + side, + outcome, + price, + market: entry.conditionId ? String(entry.conditionId) : undefined, + timestamp: entry.timestamp ? String(entry.timestamp) : undefined, + txHash: entry.transactionHash ? String(entry.transactionHash) : undefined, + }; +} + +function getPolicy(config) { + const sourceUserRaw = process.env.COPY_TRADING_SOURCE_USER; + const market = process.env.COPY_TRADING_MARKET?.trim() || null; + const yesTokenId = normalizeTokenId(process.env.COPY_TRADING_YES_TOKEN_ID); + const noTokenId = normalizeTokenId(process.env.COPY_TRADING_NO_TOKEN_ID); + const collateralToken = + normalizeAddress(process.env.COPY_TRADING_COLLATERAL_TOKEN) ?? + normalizeAddress(DEFAULT_COLLATERAL_TOKEN); + const ctfContract = + normalizeAddress(process.env.COPY_TRADING_CTF_CONTRACT) ?? + normalizeAddress(config?.polymarketConditionalTokens); + + const errors = []; + const sourceUser = normalizeAddress(sourceUserRaw); + if (!sourceUser) errors.push('COPY_TRADING_SOURCE_USER missing or invalid address.'); + if (!market) errors.push('COPY_TRADING_MARKET is required.'); + if (!yesTokenId) errors.push('COPY_TRADING_YES_TOKEN_ID is required.'); + if (!noTokenId) errors.push('COPY_TRADING_NO_TOKEN_ID is required.'); + if (!collateralToken) { + errors.push('COPY_TRADING_COLLATERAL_TOKEN invalid and no default available.'); + } + if (!ctfContract) { + errors.push( + 'COPY_TRADING_CTF_CONTRACT invalid and POLYMARKET_CONDITIONAL_TOKENS unavailable.' + ); + } + + return { + sourceUser, + market, + yesTokenId, + noTokenId, + collateralToken, + ctfContract, + ready: errors.length === 0, + errors, + }; +} + +function calculateCopyAmounts(safeBalanceWei) { + const normalized = BigInt(safeBalanceWei ?? 0); + if (normalized <= 0n) { + return { + safeBalanceWei: '0', + copyAmountWei: '0', + feeAmountWei: '0', + }; + } + + const copyAmountWei = (normalized * COPY_BPS) / BPS_DENOMINATOR; + const feeAmountWei = normalized - copyAmountWei; + + return { + safeBalanceWei: normalized.toString(), + copyAmountWei: copyAmountWei.toString(), + feeAmountWei: feeAmountWei.toString(), + }; +} + +function computeBuyOrderAmounts({ collateralAmountWei, price }) { + const normalizedCollateralAmountWei = BigInt(collateralAmountWei); + if (normalizedCollateralAmountWei <= 0n) { + throw new Error('collateralAmountWei must be > 0 for buy-order sizing.'); + } + + const normalizedPrice = normalizeTradePrice(price); + if (!normalizedPrice) { + throw new Error('price must be a number between 0 and 1 for buy-order sizing.'); + } + + const priceScaled = BigInt(Math.round(normalizedPrice * Number(PRICE_SCALE))); + if (priceScaled <= 0n) { + throw new Error('price is too small for buy-order sizing.'); + } + + const makerAmount = (normalizedCollateralAmountWei * PRICE_SCALE) / priceScaled; + if (makerAmount <= 0n) { + throw new Error('makerAmount computed to zero; refusing order.'); + } + + return { + makerAmount: makerAmount.toString(), + takerAmount: normalizedCollateralAmountWei.toString(), + priceScaled: priceScaled.toString(), + }; +} + +async function fetchLatestSourceTrade({ policy }) { + const params = new URLSearchParams({ + user: policy.sourceUser, + limit: '10', + offset: '0', + }); + params.set('type', 'TRADE'); + params.set('market', policy.market); + + const response = await fetch(`${DATA_API_HOST}/activity?${params.toString()}`, { + signal: AbortSignal.timeout(10_000), + }); + if (!response.ok) { + throw new Error(`Data API request failed (${response.status}).`); + } + + const data = await response.json(); + if (!Array.isArray(data)) { + return null; + } + + for (const item of data) { + const parsed = parseActivityEntry(item); + if (!parsed) continue; + if (parsed.outcome !== 'YES' && parsed.outcome !== 'NO') continue; + return parsed; + } + + return null; +} + +function activateTradeCandidate({ trade, tokenId, reimbursementAmountWei }) { + copyTradingState.activeSourceTradeId = trade.id; + copyTradingState.activeOutcome = trade.outcome; + copyTradingState.activeTokenId = tokenId; + copyTradingState.reimbursementAmountWei = reimbursementAmountWei; + copyTradingState.orderSubmitted = false; + copyTradingState.tokenDeposited = false; + copyTradingState.reimbursementProposed = false; +} + +function clearActiveTrade({ markSeen = false } = {}) { + if (markSeen && copyTradingState.activeSourceTradeId) { + copyTradingState.seenSourceTradeId = copyTradingState.activeSourceTradeId; + } + + copyTradingState.activeSourceTradeId = null; + copyTradingState.activeOutcome = null; + copyTradingState.activeTokenId = null; + copyTradingState.reimbursementAmountWei = null; + copyTradingState.orderSubmitted = false; + copyTradingState.tokenDeposited = false; + copyTradingState.reimbursementProposed = false; +} + +function getPollingOptions() { + return { + emitBalanceSnapshotsEveryPoll: true, + }; +} + +function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { + const mode = proposeEnabled && disputeEnabled + ? 'You may propose and dispute.' + : proposeEnabled + ? 'You may propose but you may not dispute.' + : disputeEnabled + ? 'You may dispute but you may not propose.' + : 'You may not propose or dispute; provide opinions only.'; + + return [ + 'You are a copy-trading commitment agent.', + 'Copy only BUY trades from the configured source user and configured market.', + 'Trade size must be exactly 99% of Safe collateral at detection time. Keep 1% in the Safe as fee.', + 'Flow must stay simple: place CLOB order from your own wallet, wait for YES/NO tokens, deposit tokens to Safe, then propose reimbursement transfer to agentAddress.', + 'Never trade more than 99% of Safe collateral and never reimburse more than the stored copy amount.', + 'Use polymarket_clob_build_sign_and_place_order for order placement, make_erc1155_deposit for YES/NO deposit, and build_og_transactions for reimbursement transfer.', + 'If preconditions are not met, return ignore.', + 'Default to disputing proposals that violate these rules; prefer no-op when unsure.', + mode, + commitmentText ? `Commitment text:\n${commitmentText}` : '', + 'If no action is needed, output strict JSON with keys: action (propose|deposit|dispute|ignore|other) and rationale (string).', + ] + .filter(Boolean) + .join(' '); +} + +async function enrichSignals(signals, { publicClient, config, account, onchainPendingProposal }) { + const policy = getPolicy(config); + const stateSnapshot = { ...copyTradingState }; + + const outSignals = [...signals]; + if (!policy.ready) { + outSignals.push({ + kind: 'copyTradingState', + policy, + state: stateSnapshot, + error: 'copy-trading policy config incomplete', + }); + return outSignals; + } + + let latestTrade = null; + let tradeFetchError; + try { + latestTrade = await fetchLatestSourceTrade({ policy }); + } catch (error) { + tradeFetchError = error?.message ?? String(error); + } + + const [safeCollateralWei, yesBalance, noBalance] = await Promise.all([ + publicClient.readContract({ + address: policy.collateralToken, + abi: erc20BalanceOfAbi, + functionName: 'balanceOf', + args: [config.commitmentSafe], + }), + publicClient.readContract({ + address: policy.ctfContract, + abi: erc1155BalanceOfAbi, + functionName: 'balanceOf', + args: [account.address, BigInt(policy.yesTokenId)], + }), + publicClient.readContract({ + address: policy.ctfContract, + abi: erc1155BalanceOfAbi, + functionName: 'balanceOf', + args: [account.address, BigInt(policy.noTokenId)], + }), + ]); + + const amounts = calculateCopyAmounts(safeCollateralWei); + if ( + latestTrade && + latestTrade.side === 'BUY' && + latestTrade.id !== copyTradingState.seenSourceTradeId && + !copyTradingState.activeSourceTradeId && + BigInt(amounts.copyAmountWei) > 0n + ) { + const targetTokenId = latestTrade.outcome === 'YES' ? policy.yesTokenId : policy.noTokenId; + activateTradeCandidate({ + trade: latestTrade, + tokenId: targetTokenId, + reimbursementAmountWei: amounts.copyAmountWei, + }); + } + + const activeTokenBalance = + copyTradingState.activeTokenId === policy.yesTokenId + ? yesBalance + : copyTradingState.activeTokenId === policy.noTokenId + ? noBalance + : 0n; + + outSignals.push({ + kind: 'copyTradingState', + policy, + state: { ...copyTradingState }, + activeTrade: latestTrade, + balances: { + safeCollateralWei: safeCollateralWei.toString(), + yesBalance: yesBalance.toString(), + noBalance: noBalance.toString(), + activeTokenBalance: activeTokenBalance.toString(), + }, + metrics: { + ...amounts, + copyBps: COPY_BPS.toString(), + feeBps: FEE_BPS.toString(), + }, + pendingProposal: Boolean(onchainPendingProposal || copyTradingState.reimbursementProposed), + tradeFetchError, + }); + + return outSignals; +} + +function parseCallArgs(call) { + if (call?.parsedArguments && typeof call.parsedArguments === 'object') { + return call.parsedArguments; + } + if (typeof call?.arguments === 'string') { + try { + return JSON.parse(call.arguments); + } catch (error) { + return null; + } + } + return null; +} + +function findCopySignal(signals) { + return signals.find((signal) => signal?.kind === 'copyTradingState'); +} + +async function validateToolCalls({ + toolCalls, + signals, + config, + agentAddress, + onchainPendingProposal, +}) { + const copySignal = findCopySignal(signals ?? []); + if (!copySignal || !copySignal.policy?.ready) { + return []; + } + + const validated = []; + const policy = copySignal.policy; + const state = copySignal.state ?? {}; + const activeTrade = copySignal.activeTrade; + const activeTokenBalance = BigInt(copySignal.balances?.activeTokenBalance ?? 0); + const pendingProposal = Boolean(onchainPendingProposal || copySignal.pendingProposal); + + for (const call of toolCalls) { + if (call.name === 'dispute_assertion') { + validated.push(call); + continue; + } + + if (call.name === 'post_bond_and_propose') { + continue; + } + + if (call.name === 'polymarket_clob_build_sign_and_place_order') { + if (!state.activeSourceTradeId) { + throw new Error('No active source trade to copy.'); + } + if (state.orderSubmitted) { + throw new Error('Copy order already submitted for active trade.'); + } + if (activeTrade?.side !== 'BUY') { + throw new Error('Only BUY source trades are eligible for copy trading.'); + } + if (!state.activeTokenId) { + throw new Error('No active YES/NO token id configured for copy trade.'); + } + const reimbursementAmountWei = BigInt(state.reimbursementAmountWei ?? 0); + if (reimbursementAmountWei <= 0n) { + throw new Error('Reimbursement amount is zero; refusing copy-trade order.'); + } + + const { makerAmount, takerAmount } = computeBuyOrderAmounts({ + collateralAmountWei: reimbursementAmountWei, + price: activeTrade.price, + }); + + validated.push({ + ...call, + parsedArguments: { + side: 'BUY', + tokenId: String(state.activeTokenId), + orderType: 'FOK', + makerAmount, + takerAmount, + }, + }); + continue; + } + + if (call.name === 'make_erc1155_deposit') { + if (!state.orderSubmitted) { + throw new Error('Cannot deposit YES/NO tokens before copy order submission.'); + } + if (state.tokenDeposited) { + throw new Error('YES/NO tokens already deposited for active trade.'); + } + if (!state.activeTokenId) { + throw new Error('No active YES/NO token id for deposit.'); + } + if (activeTokenBalance <= 0n) { + throw new Error('No YES/NO token balance available to deposit yet.'); + } + + validated.push({ + ...call, + parsedArguments: { + token: policy.ctfContract, + tokenId: String(state.activeTokenId), + amount: activeTokenBalance.toString(), + data: '0x', + }, + }); + continue; + } + + if (call.name === 'build_og_transactions') { + if (!state.tokenDeposited) { + throw new Error('Cannot build reimbursement proposal before token deposit confirmation.'); + } + if (state.reimbursementProposed) { + throw new Error('Reimbursement proposal already submitted for active trade.'); + } + if (pendingProposal) { + throw new Error('Pending proposal exists; wait before proposing reimbursement.'); + } + const reimbursementAmountWei = BigInt(state.reimbursementAmountWei ?? 0); + if (reimbursementAmountWei <= 0n) { + throw new Error('Reimbursement amount is zero; refusing proposal build.'); + } + + validated.push({ + ...call, + parsedArguments: { + actions: [ + { + kind: 'erc20_transfer', + token: policy.collateralToken, + to: agentAddress, + amountWei: reimbursementAmountWei.toString(), + }, + ], + }, + }); + continue; + } + + // Ignore all other tool calls for this specialized module. + } + + return validated; +} + +function onToolOutput({ name, parsedOutput }) { + if (!name || !parsedOutput || parsedOutput.status === 'error') { + return; + } + + if (name === 'polymarket_clob_build_sign_and_place_order' && parsedOutput.status === 'submitted') { + copyTradingState.orderSubmitted = true; + return; + } + + if (name === 'make_erc1155_deposit' && parsedOutput.status === 'confirmed') { + copyTradingState.tokenDeposited = true; + return; + } + + if ( + (name === 'post_bond_and_propose' || name === 'auto_post_bond_and_propose') && + parsedOutput.status === 'submitted' + ) { + copyTradingState.reimbursementProposed = true; + } +} + +function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 }) { + if (executedProposalCount > 0) { + clearActiveTrade({ markSeen: true }); + } + + if (deletedProposalCount > 0) { + copyTradingState.reimbursementProposed = false; + } +} + +function getCopyTradingState() { + return { ...copyTradingState }; +} + +function resetCopyTradingState() { + copyTradingState = { + seenSourceTradeId: null, + activeSourceTradeId: null, + activeOutcome: null, + activeTokenId: null, + reimbursementAmountWei: null, + orderSubmitted: false, + tokenDeposited: false, + reimbursementProposed: false, + }; +} + +export { + calculateCopyAmounts, + computeBuyOrderAmounts, + enrichSignals, + getCopyTradingState, + getPollingOptions, + getSystemPrompt, + onProposalEvents, + onToolOutput, + resetCopyTradingState, + validateToolCalls, +}; diff --git a/agent-library/agents/copy-trading/agent.json b/agent-library/agents/copy-trading/agent.json new file mode 100644 index 00000000..ddb004a0 --- /dev/null +++ b/agent-library/agents/copy-trading/agent.json @@ -0,0 +1,13 @@ +{ + "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", + "name": "Oya Copy Trading Agent", + "description": "Copy-trading commitment agent that mirrors configured Polymarket BUY trades at 99% Safe sizing, deposits YES/NO tokens, and proposes reimbursement.", + "image": "https://raw.githubusercontent.com/oya-commitments/oya-commitments/main/agent-library/agents/copy-trading/agent.png", + "endpoints": [ + { + "name": "agentWallet", + "endpoint": "eip155:137:0x0000000000000000000000000000000000000000" + } + ], + "registrations": [] +} diff --git a/agent-library/agents/copy-trading/commitment.txt b/agent-library/agents/copy-trading/commitment.txt new file mode 100644 index 00000000..94cf7e6c --- /dev/null +++ b/agent-library/agents/copy-trading/commitment.txt @@ -0,0 +1,13 @@ +This commitment accepts deposits from multiple users. + +The agent monitors a single configured source trader and a single configured Polymarket market. +When the source trader executes a BUY trade in that market, the agent may copy that trade direction. + +The agent must size the copied trade to exactly 99% of the Safe's collateral balance at detection time. +The remaining 1% stays in the Safe as the agent fee. + +The agent executes the copied trade from the agent wallet through the Polymarket CLOB API. +After receiving YES or NO tokens, the agent deposits those ERC1155 tokens into this Safe. + +After token deposit confirmation, the agent proposes one reimbursement transfer from this Safe to the agent wallet equal to the copied trade collateral amount. +No other transfers are allowed. diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs new file mode 100644 index 00000000..9b9a7c07 --- /dev/null +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -0,0 +1,167 @@ +import assert from 'node:assert/strict'; +import { + calculateCopyAmounts, + computeBuyOrderAmounts, + getSystemPrompt, + validateToolCalls, +} from './agent.js'; + +function runPromptTest() { + const prompt = getSystemPrompt({ + proposeEnabled: true, + disputeEnabled: true, + commitmentText: 'Copy-trade commitment.', + }); + + assert.ok(prompt.includes('copy-trading commitment agent')); + assert.ok(prompt.includes('99%')); + assert.ok(prompt.includes('1%')); +} + +function runMathTests() { + const amounts = calculateCopyAmounts(1_000_000n); + assert.equal(amounts.copyAmountWei, '990000'); + assert.equal(amounts.feeAmountWei, '10000'); + + const sized = computeBuyOrderAmounts({ + collateralAmountWei: 990000n, + price: 0.55, + }); + assert.equal(sized.takerAmount, '990000'); + assert.ok(BigInt(sized.makerAmount) > 0n); +} + +async function runValidateToolCallTests() { + const policy = { + ready: true, + ctfContract: '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045', + collateralToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + }; + + const orderValidated = await validateToolCalls({ + toolCalls: [ + { + callId: 'order', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: {}, + }, + ], + signals: [ + { + kind: 'copyTradingState', + policy, + state: { + activeSourceTradeId: 'trade-1', + activeTokenId: '123', + reimbursementAmountWei: '990000', + orderSubmitted: false, + tokenDeposited: false, + reimbursementProposed: false, + }, + activeTrade: { + side: 'BUY', + price: 0.55, + }, + balances: { + activeTokenBalance: '0', + }, + pendingProposal: false, + }, + ], + config: {}, + agentAddress: '0x1111111111111111111111111111111111111111', + onchainPendingProposal: false, + }); + assert.equal(orderValidated.length, 1); + assert.equal(orderValidated[0].parsedArguments.side, 'BUY'); + assert.equal(orderValidated[0].parsedArguments.tokenId, '123'); + assert.equal(orderValidated[0].parsedArguments.orderType, 'FOK'); + assert.equal(orderValidated[0].parsedArguments.takerAmount, '990000'); + + const depositValidated = await validateToolCalls({ + toolCalls: [ + { + callId: 'deposit', + name: 'make_erc1155_deposit', + arguments: {}, + }, + ], + signals: [ + { + kind: 'copyTradingState', + policy, + state: { + activeSourceTradeId: 'trade-1', + activeTokenId: '123', + reimbursementAmountWei: '990000', + orderSubmitted: true, + tokenDeposited: false, + reimbursementProposed: false, + }, + activeTrade: { + side: 'BUY', + price: 0.55, + }, + balances: { + activeTokenBalance: '5', + }, + pendingProposal: false, + }, + ], + config: {}, + agentAddress: '0x1111111111111111111111111111111111111111', + onchainPendingProposal: false, + }); + assert.equal(depositValidated.length, 1); + assert.equal(depositValidated[0].parsedArguments.token, policy.ctfContract); + assert.equal(depositValidated[0].parsedArguments.tokenId, '123'); + assert.equal(depositValidated[0].parsedArguments.amount, '5'); + + const reimbursementValidated = await validateToolCalls({ + toolCalls: [ + { + callId: 'reimbursement', + name: 'build_og_transactions', + arguments: {}, + }, + ], + signals: [ + { + kind: 'copyTradingState', + policy, + state: { + activeSourceTradeId: 'trade-1', + activeTokenId: '123', + reimbursementAmountWei: '990000', + orderSubmitted: true, + tokenDeposited: true, + reimbursementProposed: false, + }, + activeTrade: { + side: 'BUY', + price: 0.55, + }, + balances: { + activeTokenBalance: '0', + }, + pendingProposal: false, + }, + ], + config: {}, + agentAddress: '0x1111111111111111111111111111111111111111', + onchainPendingProposal: false, + }); + assert.equal(reimbursementValidated.length, 1); + assert.equal(reimbursementValidated[0].parsedArguments.actions.length, 1); + assert.equal(reimbursementValidated[0].parsedArguments.actions[0].kind, 'erc20_transfer'); + assert.equal(reimbursementValidated[0].parsedArguments.actions[0].amountWei, '990000'); +} + +async function run() { + runPromptTest(); + runMathTests(); + await runValidateToolCallTests(); + console.log('[test] copy-trading agent OK'); +} + +run(); diff --git a/agent/.env.example b/agent/.env.example index 881b9cba..800d0f0b 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -35,6 +35,7 @@ POLL_INTERVAL_MS=60000 WATCH_NATIVE_BALANCE=true # Optional Polymarket config # POLYMARKET_CONDITIONAL_TOKENS=0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 +# POLYMARKET_EXCHANGE= # POLYMARKET_CLOB_ENABLED=false # POLYMARKET_CLOB_HOST=https://clob.polymarket.com # POLYMARKET_CLOB_ADDRESS= diff --git a/agent/README.md b/agent/README.md index ae263426..10848aa4 100644 --- a/agent/README.md +++ b/agent/README.md @@ -94,6 +94,7 @@ The shared tooling supports: Set these when using Polymarket functionality: - `POLYMARKET_CONDITIONAL_TOKENS`: Optional CTF contract address override used by CTF actions (default is Polymarket mainnet ConditionalTokens). +- `POLYMARKET_EXCHANGE`: Optional CTF exchange override for EIP-712 order signing domain. - `POLYMARKET_CLOB_ENABLED`: Enable CLOB tools (`true`/`false`, default `false`). - `POLYMARKET_CLOB_HOST`: CLOB API host (default `https://clob.polymarket.com`). - `POLYMARKET_CLOB_ADDRESS`: Optional address used as `POLY_ADDRESS` for CLOB auth (for proxy/funder setups). Defaults to runtime signer address. @@ -104,7 +105,7 @@ Set these when using Polymarket functionality: - `PROPOSE_ENABLED=true` and/or `DISPUTE_ENABLED=true`: onchain tools are enabled (`build_og_transactions`, `make_deposit`, `make_erc1155_deposit`, propose/dispute tools). - `PROPOSE_ENABLED=false` and `DISPUTE_ENABLED=false`: onchain tools are disabled. -- `POLYMARKET_CLOB_ENABLED=true`: CLOB tools can still run in this mode (`polymarket_clob_place_order`, `polymarket_clob_cancel_orders`). +- `POLYMARKET_CLOB_ENABLED=true`: CLOB tools can still run in this mode (`polymarket_clob_place_order`, `polymarket_clob_build_sign_and_place_order`, `polymarket_clob_cancel_orders`). - All three disabled (`PROPOSE_ENABLED=false`, `DISPUTE_ENABLED=false`, `POLYMARKET_CLOB_ENABLED=false`): monitor/opinion only. #### CTF Actions (`build_og_transactions`) @@ -171,6 +172,21 @@ Use `make_erc1155_deposit` after receiving YES/NO position tokens: } ``` +`polymarket_clob_build_sign_and_place_order` builds and signs the order with the runtime signer before submission: + +```json +{ + "name": "polymarket_clob_build_sign_and_place_order", + "arguments": { + "side": "BUY", + "tokenId": "123456789", + "orderType": "FOK", + "makerAmount": "1000000", + "takerAmount": "450000" + } +} +``` + `polymarket_clob_cancel_orders` supports `ids`, `market`, or `all`: ```json @@ -194,6 +210,12 @@ For `polymarket_clob_place_order`, the runner validates the same order payload t If any identity is outside that allowlist, the tool call is rejected before submission. +For `polymarket_clob_build_sign_and_place_order`, `maker` and `signer` must also be one of: +- runtime signer address, or +- `POLYMARKET_CLOB_ADDRESS` when set. + +This tool requires a signer backend that supports `signTypedData`. + #### CLOB Retry Behavior - `POST /order` is not automatically retried. diff --git a/agent/scripts/test-polymarket-tool-normalization.mjs b/agent/scripts/test-polymarket-tool-normalization.mjs index e74124f8..ae841598 100644 --- a/agent/scripts/test-polymarket-tool-normalization.mjs +++ b/agent/scripts/test-polymarket-tool-normalization.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { executeToolCalls, toolDefinitions } from '../src/lib/tools.js'; const TEST_ACCOUNT = { address: '0x1111111111111111111111111111111111111111' }; +const TEST_SIGNATURE = `0x${'1'.repeat(130)}`; function parseToolOutput(output) { return JSON.parse(output.output); @@ -14,15 +15,25 @@ async function run() { clobEnabled: true, }); const placeOrderDef = defs.find((tool) => tool.name === 'polymarket_clob_place_order'); + const buildSignAndPlaceOrderDef = defs.find( + (tool) => tool.name === 'polymarket_clob_build_sign_and_place_order' + ); const cancelOrdersDef = defs.find((tool) => tool.name === 'polymarket_clob_cancel_orders'); const makeDepositDef = defs.find((tool) => tool.name === 'make_deposit'); const makeErc1155DepositDef = defs.find((tool) => tool.name === 'make_erc1155_deposit'); assert.ok(placeOrderDef); + assert.ok(buildSignAndPlaceOrderDef); assert.ok(cancelOrdersDef); assert.equal(makeDepositDef, undefined); assert.equal(makeErc1155DepositDef, undefined); assert.deepEqual(placeOrderDef.parameters.properties.orderType.enum, ['GTC', 'GTD', 'FOK', 'FAK']); + assert.deepEqual(buildSignAndPlaceOrderDef.parameters.properties.orderType.enum, [ + 'GTC', + 'GTD', + 'FOK', + 'FAK', + ]); assert.deepEqual(cancelOrdersDef.parameters.properties.mode.enum, ['ids', 'market', 'all']); assert.deepEqual(cancelOrdersDef.parameters.required, ['mode']); @@ -230,6 +241,139 @@ async function run() { assert.equal(configuredIdentityMatchOut.status, 'error'); assert.match(configuredIdentityMatchOut.message, /Missing CLOB credentials/); + const missingTypedDataSupport = await executeToolCalls({ + toolCalls: [ + { + callId: 'missing-typed-data-support', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + makerAmount: '1000000', + takerAmount: '400000', + }, + }, + ], + publicClient: { + async getChainId() { + return 137; + }, + }, + walletClient: {}, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const missingTypedDataSupportOut = parseToolOutput(missingTypedDataSupport[0]); + assert.equal(missingTypedDataSupportOut.status, 'error'); + assert.match(missingTypedDataSupportOut.message, /signTypedData/); + + const recordedSignInputs = []; + const buildSignAndPlace = await executeToolCalls({ + toolCalls: [ + { + callId: 'build-sign-place', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: { + side: ' buy ', + tokenId: '123', + orderType: ' gtc ', + makerAmount: '1000000', + takerAmount: '450000', + signatureType: 'EOA', + }, + }, + ], + publicClient: { + async getChainId() { + return 137; + }, + }, + walletClient: { + async signTypedData(args) { + recordedSignInputs.push(args); + return TEST_SIGNATURE; + }, + }, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const buildSignAndPlaceOut = parseToolOutput(buildSignAndPlace[0]); + assert.equal(buildSignAndPlaceOut.status, 'error'); + assert.match(buildSignAndPlaceOut.message, /Missing CLOB credentials/); + assert.equal(recordedSignInputs.length, 1); + assert.equal(recordedSignInputs[0].domain.chainId, 137); + assert.equal( + recordedSignInputs[0].domain.verifyingContract.toLowerCase(), + '0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e' + ); + assert.equal(recordedSignInputs[0].message.side, 0); + assert.equal(recordedSignInputs[0].message.signatureType, 0); + assert.equal(recordedSignInputs[0].message.tokenId, 123n); + + const invalidBuildSignIdentity = await executeToolCalls({ + toolCalls: [ + { + callId: 'invalid-build-sign-identity', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + makerAmount: '1000000', + takerAmount: '450000', + maker: '0x3333333333333333333333333333333333333333', + }, + }, + ], + publicClient: { + async getChainId() { + return 137; + }, + }, + walletClient: { + async signTypedData() { + return TEST_SIGNATURE; + }, + }, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const invalidBuildSignIdentityOut = parseToolOutput(invalidBuildSignIdentity[0]); + assert.equal(invalidBuildSignIdentityOut.status, 'error'); + assert.match(invalidBuildSignIdentityOut.message, /maker identity mismatch/); + + const missingChainIdForBuildSign = await executeToolCalls({ + toolCalls: [ + { + callId: 'missing-chain-id-build-sign', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + makerAmount: '1000000', + takerAmount: '450000', + }, + }, + ], + publicClient: {}, + walletClient: { + async signTypedData() { + return TEST_SIGNATURE; + }, + }, + account: TEST_ACCOUNT, + config, + ogContext: null, + }); + const missingChainIdForBuildSignOut = parseToolOutput(missingChainIdForBuildSign[0]); + assert.equal(missingChainIdForBuildSignOut.status, 'error'); + assert.match(missingChainIdForBuildSignOut.message, /chainId is required/); + const invalidCancelMode = await executeToolCalls({ toolCalls: [ { diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index e942de07..d2ce1744 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -60,6 +60,9 @@ function buildConfig() { polymarketConditionalTokens: process.env.POLYMARKET_CONDITIONAL_TOKENS ? getAddress(process.env.POLYMARKET_CONDITIONAL_TOKENS) : getAddress('0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'), + polymarketExchange: process.env.POLYMARKET_EXCHANGE + ? getAddress(process.env.POLYMARKET_EXCHANGE) + : undefined, polymarketClobEnabled: process.env.POLYMARKET_CLOB_ENABLED === undefined ? false diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index dc97194a..8f521345 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -1,9 +1,41 @@ import crypto from 'node:crypto'; +import { getAddress, zeroAddress } from 'viem'; const DEFAULT_CLOB_HOST = 'https://clob.polymarket.com'; const DEFAULT_CLOB_REQUEST_TIMEOUT_MS = 15_000; const DEFAULT_CLOB_MAX_RETRIES = 1; const DEFAULT_CLOB_RETRY_DELAY_MS = 250; +const CLOB_EIP712_DOMAIN_NAME = 'Polymarket CTF Exchange'; +const CLOB_EIP712_DOMAIN_VERSION = '1'; +const DEFAULT_EIP712_ORDER_SIDE = 0; +const DEFAULT_EIP712_SIGNATURE_TYPE = 0; +const ORDER_EIP712_TYPES = Object.freeze([ + { name: 'salt', type: 'uint256' }, + { name: 'maker', type: 'address' }, + { name: 'signer', type: 'address' }, + { name: 'taker', type: 'address' }, + { name: 'tokenId', type: 'uint256' }, + { name: 'makerAmount', type: 'uint256' }, + { name: 'takerAmount', type: 'uint256' }, + { name: 'expiration', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'feeRateBps', type: 'uint256' }, + { name: 'side', type: 'uint8' }, + { name: 'signatureType', type: 'uint8' }, +]); +const SIDE_INDEX = Object.freeze({ + BUY: 0, + SELL: 1, +}); +const SIGNATURE_TYPE_INDEX = Object.freeze({ + EOA: 0, + POLY_GNOSIS_SAFE: 1, + POLY_PROXY: 2, +}); +const DEFAULT_CTF_EXCHANGE_BY_CHAIN_ID = Object.freeze({ + 137: '0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e', + 80002: '0xdfe02eb6733538f8ea35d585af8de5958ad99e40', +}); function normalizeNonNegativeInteger(value, fallback) { const parsed = Number(value); @@ -15,6 +47,185 @@ function normalizeClobHost(host) { return (host ?? DEFAULT_CLOB_HOST).replace(/\/+$/, ''); } +function normalizeUint(value, fieldName, { allowZero = true } = {}) { + if (value === null || value === undefined || value === '') { + throw new Error(`${fieldName} is required.`); + } + + let normalized; + try { + normalized = BigInt(value); + } catch (error) { + throw new Error(`${fieldName} must be an integer value.`); + } + + if (normalized < 0n || (!allowZero && normalized === 0n)) { + throw new Error(`${fieldName} must be ${allowZero ? '>= 0' : '> 0'}.`); + } + + return normalized; +} + +function normalizeSideIndex(value) { + if (value === null || value === undefined || value === '') { + return DEFAULT_EIP712_ORDER_SIDE; + } + if (typeof value === 'number') { + if (value === 0 || value === 1) return value; + throw new Error('side must be BUY/SELL or enum index 0/1.'); + } + + if (typeof value === 'string') { + const normalized = value.trim().toUpperCase(); + if (normalized in SIDE_INDEX) { + return SIDE_INDEX[normalized]; + } + if (normalized === '0' || normalized === '1') { + return Number(normalized); + } + } + + throw new Error('side must be BUY/SELL or enum index 0/1.'); +} + +function normalizeSignatureTypeIndex(value) { + if (value === null || value === undefined || value === '') { + return DEFAULT_EIP712_SIGNATURE_TYPE; + } + if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 2) { + return value; + } + + if (typeof value === 'string') { + const normalized = value.trim().toUpperCase(); + if (normalized in SIGNATURE_TYPE_INDEX) { + return SIGNATURE_TYPE_INDEX[normalized]; + } + if (normalized === '0' || normalized === '1' || normalized === '2') { + return Number(normalized); + } + } + + throw new Error('signatureType must be EOA/POLY_GNOSIS_SAFE/POLY_PROXY or enum index 0/1/2.'); +} + +function randomSalt() { + return BigInt(`0x${crypto.randomBytes(32).toString('hex')}`).toString(); +} + +function resolveClobExchangeAddress({ chainId, exchangeOverride }) { + if (exchangeOverride) { + return getAddress(exchangeOverride); + } + + const exchange = DEFAULT_CTF_EXCHANGE_BY_CHAIN_ID[Number(chainId)]; + if (!exchange) { + throw new Error( + `No default Polymarket exchange for chainId=${chainId}. Set POLYMARKET_EXCHANGE or provide exchange in tool args.` + ); + } + return getAddress(exchange); +} + +function buildClobOrderFromRaw({ + maker, + signer, + taker, + tokenId, + makerAmount, + takerAmount, + expiration, + nonce, + feeRateBps, + side, + signatureType, + salt, +}) { + const normalizedMaker = getAddress(maker); + const normalizedSigner = getAddress(signer); + const normalizedTaker = taker ? getAddress(taker) : zeroAddress; + const normalizedTokenId = normalizeUint(tokenId, 'tokenId'); + const normalizedMakerAmount = normalizeUint(makerAmount, 'makerAmount', { allowZero: false }); + const normalizedTakerAmount = normalizeUint(takerAmount, 'takerAmount', { allowZero: false }); + const normalizedExpiration = normalizeUint(expiration ?? 0, 'expiration'); + const normalizedNonce = normalizeUint(nonce ?? 0, 'nonce'); + const normalizedFeeRateBps = normalizeUint(feeRateBps ?? 0, 'feeRateBps'); + const normalizedSide = normalizeSideIndex(side); + const normalizedSignatureType = normalizeSignatureTypeIndex(signatureType); + const normalizedSalt = normalizeUint(salt ?? randomSalt(), 'salt', { allowZero: false }); + + return { + salt: normalizedSalt.toString(), + maker: normalizedMaker, + signer: normalizedSigner, + taker: normalizedTaker, + tokenId: normalizedTokenId.toString(), + makerAmount: normalizedMakerAmount.toString(), + takerAmount: normalizedTakerAmount.toString(), + expiration: normalizedExpiration.toString(), + nonce: normalizedNonce.toString(), + feeRateBps: normalizedFeeRateBps.toString(), + side: normalizedSide, + signatureType: normalizedSignatureType, + }; +} + +async function signClobOrder({ + walletClient, + account, + chainId, + exchange, + order, + domainName = CLOB_EIP712_DOMAIN_NAME, + domainVersion = CLOB_EIP712_DOMAIN_VERSION, +}) { + if (!walletClient || typeof walletClient.signTypedData !== 'function') { + throw new Error( + 'Runtime signer does not support signTypedData; cannot build and sign CLOB orders.' + ); + } + + const normalizedChainId = Number(chainId); + if (!Number.isInteger(normalizedChainId) || normalizedChainId <= 0) { + throw new Error(`Invalid chainId for CLOB signing: ${chainId}`); + } + + const normalizedExchange = getAddress(exchange); + const normalizedOrder = buildClobOrderFromRaw(order); + const message = { + ...normalizedOrder, + salt: BigInt(normalizedOrder.salt), + tokenId: BigInt(normalizedOrder.tokenId), + makerAmount: BigInt(normalizedOrder.makerAmount), + takerAmount: BigInt(normalizedOrder.takerAmount), + expiration: BigInt(normalizedOrder.expiration), + nonce: BigInt(normalizedOrder.nonce), + feeRateBps: BigInt(normalizedOrder.feeRateBps), + side: Number(normalizedOrder.side), + signatureType: Number(normalizedOrder.signatureType), + }; + + const signature = await walletClient.signTypedData({ + account, + domain: { + name: domainName, + version: domainVersion, + chainId: normalizedChainId, + verifyingContract: normalizedExchange, + }, + types: { + Order: ORDER_EIP712_TYPES, + }, + primaryType: 'Order', + message, + }); + + return { + ...normalizedOrder, + signature, + }; +} + function shouldRetryResponseStatus(status) { return status === 429 || status >= 500; } @@ -233,4 +444,10 @@ async function cancelClobOrders({ }); } -export { cancelClobOrders, placeClobOrder }; +export { + buildClobOrderFromRaw, + cancelClobOrders, + placeClobOrder, + resolveClobExchangeAddress, + signClobOrder, +}; diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 86752b2b..6d485d52 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -6,7 +6,13 @@ import { postBondAndDispute, postBondAndPropose, } from './tx.js'; -import { cancelClobOrders, placeClobOrder } from './polymarket.js'; +import { + buildClobOrderFromRaw, + cancelClobOrders, + placeClobOrder, + resolveClobExchangeAddress, + signClobOrder, +} from './polymarket.js'; import { parseToolArguments } from './utils.js'; function safeStringify(value) { @@ -19,6 +25,12 @@ function normalizeOrderSide(value) { return normalized === 'BUY' || normalized === 'SELL' ? normalized : undefined; } +function normalizeOrderSideEnumIndex(value) { + const normalized = normalizeOrderSide(value); + if (!normalized) return undefined; + return normalized === 'BUY' ? 0 : 1; +} + function normalizeOrderType(value) { if (typeof value !== 'string') return undefined; const normalized = value.trim().toUpperCase(); @@ -58,6 +70,21 @@ function maybeAddress(value) { } } +function normalizeOptionalUintString(value, fieldName) { + if (value === undefined || value === null || value === '') { + return undefined; + } + try { + const normalized = BigInt(value); + if (normalized < 0n) { + throw new Error(`${fieldName} must be >= 0.`); + } + return normalized.toString(); + } catch (error) { + throw new Error(`${fieldName} must be an integer value.`); + } +} + function normalizeSignedOrderPayload(signedOrder) { if (!signedOrder || typeof signedOrder !== 'object') { return undefined; @@ -418,6 +445,91 @@ function toolDefinitions({ required: ['side', 'tokenId', 'orderType', 'signedOrder'], }, }, + { + type: 'function', + name: 'polymarket_clob_build_sign_and_place_order', + description: + 'Build an unsigned CLOB order, sign it with the runtime signer (EIP-712), and submit it.', + strict: true, + parameters: { + type: 'object', + additionalProperties: false, + properties: { + owner: { + type: ['string', 'null'], + description: + 'Optional CLOB API key owner override; defaults to POLYMARKET_CLOB_API_KEY.', + }, + side: { + type: 'string', + description: 'BUY or SELL.', + }, + tokenId: { + type: 'string', + description: 'Polymarket token id for the order.', + }, + orderType: { + type: 'string', + enum: ['GTC', 'GTD', 'FOK', 'FAK'], + description: 'Order type, e.g. GTC, GTD, FOK, or FAK.', + }, + makerAmount: { + type: 'string', + description: 'Order makerAmount in base units as an integer string.', + }, + takerAmount: { + type: 'string', + description: 'Order takerAmount in base units as an integer string.', + }, + maker: { + type: ['string', 'null'], + description: + 'Optional maker override. Must match runtime signer or POLYMARKET_CLOB_ADDRESS.', + }, + signer: { + type: ['string', 'null'], + description: + 'Optional signer override. Must match runtime signer or POLYMARKET_CLOB_ADDRESS.', + }, + taker: { + type: ['string', 'null'], + description: 'Optional taker address override (defaults to zero address).', + }, + expiration: { + type: ['string', 'null'], + description: 'Optional expiration timestamp as integer string. Default 0.', + }, + nonce: { + type: ['string', 'null'], + description: 'Optional nonce as integer string. Default 0.', + }, + feeRateBps: { + type: ['string', 'null'], + description: 'Optional fee rate in bps as integer string. Default 0.', + }, + signatureType: { + type: ['string', 'integer', 'null'], + description: + 'Optional signature type (EOA|POLY_GNOSIS_SAFE|POLY_PROXY or enum 0/1/2). Default EOA.', + }, + salt: { + type: ['string', 'null'], + description: 'Optional uint256 salt as integer string. Random if omitted.', + }, + exchange: { + type: ['string', 'null'], + description: + 'Optional CTF exchange address override for EIP-712 domain verifyingContract.', + }, + chainId: { + type: ['integer', 'null'], + description: + 'Optional chainId override for EIP-712 domain. Defaults to current RPC chain.', + }, + }, + required: ['side', 'tokenId', 'orderType', 'makerAmount', 'takerAmount'], + }, + }, { type: 'function', name: 'polymarket_clob_cancel_orders', @@ -660,6 +772,156 @@ async function executeToolCalls({ continue; } + if (call.name === 'polymarket_clob_build_sign_and_place_order') { + if (!config.polymarketClobEnabled) { + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'skipped', + reason: 'polymarket CLOB disabled', + }), + }); + continue; + } + + try { + const runtimeSignerAddress = getAddress(account.address); + const clobAuthAddress = config.polymarketClobAddress + ? getAddress(config.polymarketClobAddress) + : runtimeSignerAddress; + const declaredSide = normalizeOrderSide(args.side); + if (!declaredSide) { + throw new Error('side must be BUY or SELL'); + } + const declaredSideEnum = normalizeOrderSideEnumIndex(declaredSide); + if (declaredSideEnum === undefined) { + throw new Error('side must be BUY or SELL'); + } + if (!args.tokenId) { + throw new Error('tokenId is required'); + } + const declaredTokenId = String(args.tokenId).trim(); + const orderType = normalizeOrderType(args.orderType); + if (!orderType) { + throw new Error('orderType must be one of GTC, GTD, FOK, FAK'); + } + if (!args.makerAmount) { + throw new Error('makerAmount is required'); + } + if (!args.takerAmount) { + throw new Error('takerAmount is required'); + } + + const allowedIdentityAddresses = new Set([ + runtimeSignerAddress, + clobAuthAddress, + ]); + const maker = args.maker ? getAddress(String(args.maker)) : clobAuthAddress; + const signer = args.signer ? getAddress(String(args.signer)) : runtimeSignerAddress; + if (!allowedIdentityAddresses.has(maker)) { + throw new Error( + `maker identity mismatch: maker must be one of ${Array.from( + allowedIdentityAddresses + ).join(', ')}.` + ); + } + if (!allowedIdentityAddresses.has(signer)) { + throw new Error( + `signer identity mismatch: signer must be one of ${Array.from( + allowedIdentityAddresses + ).join(', ')}.` + ); + } + + const runtimeChainId = + typeof publicClient?.getChainId === 'function' + ? await publicClient.getChainId() + : undefined; + const chainId = Number(args.chainId ?? runtimeChainId); + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error( + 'chainId is required to sign CLOB orders (provide chainId or use a client with getChainId).' + ); + } + const exchange = resolveClobExchangeAddress({ + chainId, + exchangeOverride: args.exchange ?? config.polymarketExchange, + }); + const normalizedSalt = normalizeOptionalUintString(args.salt, 'salt'); + const normalizedExpiration = normalizeOptionalUintString( + args.expiration, + 'expiration' + ); + const normalizedNonce = normalizeOptionalUintString(args.nonce, 'nonce'); + const normalizedFeeRateBps = normalizeOptionalUintString( + args.feeRateBps, + 'feeRateBps' + ); + const unsignedOrder = buildClobOrderFromRaw({ + maker, + signer, + taker: args.taker, + tokenId: declaredTokenId, + makerAmount: args.makerAmount, + takerAmount: args.takerAmount, + side: declaredSideEnum, + signatureType: args.signatureType, + salt: normalizedSalt, + expiration: normalizedExpiration, + nonce: normalizedNonce, + feeRateBps: normalizedFeeRateBps, + }); + const signedOrder = await signClobOrder({ + walletClient, + account, + chainId, + exchange, + order: unsignedOrder, + }); + + const configuredOwnerApiKey = config.polymarketClobApiKey; + if (!configuredOwnerApiKey) { + throw new Error('Missing POLYMARKET_CLOB_API_KEY in runtime config.'); + } + const requestedOwner = + typeof args.owner === 'string' && args.owner.trim() + ? args.owner.trim() + : undefined; + if (requestedOwner && requestedOwner !== configuredOwnerApiKey) { + throw new Error( + 'owner mismatch: provided owner does not match configured POLYMARKET_CLOB_API_KEY.' + ); + } + const result = await placeClobOrder({ + config, + signingAddress: clobAuthAddress, + signedOrder, + ownerApiKey: configuredOwnerApiKey, + orderType, + }); + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'submitted', + signedOrder, + result, + }), + }); + } catch (error) { + outputs.push({ + callId: call.callId, + name: call.name, + output: safeStringify({ + status: 'error', + message: error?.message ?? String(error), + }), + }); + } + continue; + } + if (call.name === 'make_deposit') { if (!onchainToolsEnabled) { outputs.push({ From 455caf9612a1743322bfedc17f8c6b6f40359350 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 10:46:22 -0800 Subject: [PATCH 145/174] preserve triggering trade snapshot for order sizing, and clear active trade only for the matching reimbursement proposal Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 69 +++++++++- .../copy-trading/test-copy-trading-agent.mjs | 124 ++++++++++++++++-- agent/src/index.js | 2 + 3 files changed, 177 insertions(+), 18 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index d4b08b52..05660a17 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -30,12 +30,15 @@ const DEFAULT_COLLATERAL_TOKEN = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; let copyTradingState = { seenSourceTradeId: null, activeSourceTradeId: null, + activeTradeSide: null, + activeTradePrice: null, activeOutcome: null, activeTokenId: null, reimbursementAmountWei: null, orderSubmitted: false, tokenDeposited: false, reimbursementProposed: false, + reimbursementProposalHash: null, }; function normalizeAddress(value) { @@ -78,6 +81,13 @@ function normalizeTradePrice(value) { return parsed; } +function normalizeHash(value) { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + if (!/^0x[0-9a-fA-F]{64}$/.test(trimmed)) return null; + return trimmed.toLowerCase(); +} + function parseActivityEntry(entry) { if (!entry || typeof entry !== 'object') return null; @@ -225,12 +235,15 @@ async function fetchLatestSourceTrade({ policy }) { function activateTradeCandidate({ trade, tokenId, reimbursementAmountWei }) { copyTradingState.activeSourceTradeId = trade.id; + copyTradingState.activeTradeSide = trade.side; + copyTradingState.activeTradePrice = trade.price; copyTradingState.activeOutcome = trade.outcome; copyTradingState.activeTokenId = tokenId; copyTradingState.reimbursementAmountWei = reimbursementAmountWei; copyTradingState.orderSubmitted = false; copyTradingState.tokenDeposited = false; copyTradingState.reimbursementProposed = false; + copyTradingState.reimbursementProposalHash = null; } function clearActiveTrade({ markSeen = false } = {}) { @@ -239,12 +252,15 @@ function clearActiveTrade({ markSeen = false } = {}) { } copyTradingState.activeSourceTradeId = null; + copyTradingState.activeTradeSide = null; + copyTradingState.activeTradePrice = null; copyTradingState.activeOutcome = null; copyTradingState.activeTokenId = null; copyTradingState.reimbursementAmountWei = null; copyTradingState.orderSubmitted = false; copyTradingState.tokenDeposited = false; copyTradingState.reimbursementProposed = false; + copyTradingState.reimbursementProposalHash = null; } function getPollingOptions() { @@ -350,7 +366,7 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe kind: 'copyTradingState', policy, state: { ...copyTradingState }, - activeTrade: latestTrade, + latestObservedTrade: latestTrade, balances: { safeCollateralWei: safeCollateralWei.toString(), yesBalance: yesBalance.toString(), @@ -402,7 +418,6 @@ async function validateToolCalls({ const validated = []; const policy = copySignal.policy; const state = copySignal.state ?? {}; - const activeTrade = copySignal.activeTrade; const activeTokenBalance = BigInt(copySignal.balances?.activeTokenBalance ?? 0); const pendingProposal = Boolean(onchainPendingProposal || copySignal.pendingProposal); @@ -423,9 +438,12 @@ async function validateToolCalls({ if (state.orderSubmitted) { throw new Error('Copy order already submitted for active trade.'); } - if (activeTrade?.side !== 'BUY') { + if (state.activeTradeSide !== 'BUY') { throw new Error('Only BUY source trades are eligible for copy trading.'); } + if (state.activeTradePrice === null || state.activeTradePrice === undefined) { + throw new Error('Missing triggering trade price snapshot for active trade.'); + } if (!state.activeTokenId) { throw new Error('No active YES/NO token id configured for copy trade.'); } @@ -436,7 +454,7 @@ async function validateToolCalls({ const { makerAmount, takerAmount } = computeBuyOrderAmounts({ collateralAmountWei: reimbursementAmountWei, - price: activeTrade.price, + price: state.activeTradePrice, }); validated.push({ @@ -535,15 +553,49 @@ function onToolOutput({ name, parsedOutput }) { parsedOutput.status === 'submitted' ) { copyTradingState.reimbursementProposed = true; + copyTradingState.reimbursementProposalHash = + normalizeHash(parsedOutput.proposalHash) ?? copyTradingState.reimbursementProposalHash; } } -function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 }) { - if (executedProposalCount > 0) { +function onProposalEvents({ + executedProposals = [], + deletedProposals = [], + executedProposalCount = 0, + deletedProposalCount = 0, +}) { + const trackedHash = normalizeHash(copyTradingState.reimbursementProposalHash); + const executedHashes = Array.isArray(executedProposals) + ? executedProposals.map((hash) => normalizeHash(hash)).filter(Boolean) + : []; + const deletedHashes = Array.isArray(deletedProposals) + ? deletedProposals.map((hash) => normalizeHash(hash)).filter(Boolean) + : []; + + if (trackedHash && executedHashes.includes(trackedHash)) { clearActiveTrade({ markSeen: true }); } - if (deletedProposalCount > 0) { + if (trackedHash && deletedHashes.includes(trackedHash)) { + copyTradingState.reimbursementProposed = false; + copyTradingState.reimbursementProposalHash = null; + } + + // Backward-compatible fallback for environments that only pass counts and no hashes. + if ( + !trackedHash && + copyTradingState.reimbursementProposed && + executedProposalCount > 0 && + (!Array.isArray(executedProposals) || executedProposals.length === 0) + ) { + clearActiveTrade({ markSeen: true }); + } + if ( + !trackedHash && + copyTradingState.reimbursementProposed && + deletedProposalCount > 0 && + (!Array.isArray(deletedProposals) || deletedProposals.length === 0) + ) { copyTradingState.reimbursementProposed = false; } } @@ -556,12 +608,15 @@ function resetCopyTradingState() { copyTradingState = { seenSourceTradeId: null, activeSourceTradeId: null, + activeTradeSide: null, + activeTradePrice: null, activeOutcome: null, activeTokenId: null, reimbursementAmountWei: null, orderSubmitted: false, tokenDeposited: false, reimbursementProposed: false, + reimbursementProposalHash: null, }; } diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index 9b9a7c07..61cfeb7e 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -2,10 +2,23 @@ import assert from 'node:assert/strict'; import { calculateCopyAmounts, computeBuyOrderAmounts, + enrichSignals, + getCopyTradingState, getSystemPrompt, + onProposalEvents, + onToolOutput, + resetCopyTradingState, validateToolCalls, } from './agent.js'; +const YES_TOKEN_ID = '123'; +const NO_TOKEN_ID = '456'; +const TEST_ACCOUNT = '0x1111111111111111111111111111111111111111'; +const TEST_SAFE = '0x2222222222222222222222222222222222222222'; +const TEST_SOURCE_USER = '0x3333333333333333333333333333333333333333'; +const TEST_PROPOSAL_HASH = `0x${'a'.repeat(64)}`; +const OTHER_PROPOSAL_HASH = `0x${'b'.repeat(64)}`; + function runPromptTest() { const prompt = getSystemPrompt({ proposeEnabled: true, @@ -52,15 +65,17 @@ async function runValidateToolCallTests() { policy, state: { activeSourceTradeId: 'trade-1', + activeTradeSide: 'BUY', + activeTradePrice: 0.55, activeTokenId: '123', reimbursementAmountWei: '990000', orderSubmitted: false, tokenDeposited: false, reimbursementProposed: false, }, - activeTrade: { - side: 'BUY', - price: 0.55, + latestObservedTrade: { + side: 'SELL', + price: 0.99, }, balances: { activeTokenBalance: '0', @@ -76,6 +91,7 @@ async function runValidateToolCallTests() { assert.equal(orderValidated[0].parsedArguments.side, 'BUY'); assert.equal(orderValidated[0].parsedArguments.tokenId, '123'); assert.equal(orderValidated[0].parsedArguments.orderType, 'FOK'); + assert.equal(orderValidated[0].parsedArguments.makerAmount, '1800000'); assert.equal(orderValidated[0].parsedArguments.takerAmount, '990000'); const depositValidated = await validateToolCalls({ @@ -92,16 +108,14 @@ async function runValidateToolCallTests() { policy, state: { activeSourceTradeId: 'trade-1', + activeTradeSide: 'BUY', + activeTradePrice: 0.55, activeTokenId: '123', reimbursementAmountWei: '990000', orderSubmitted: true, tokenDeposited: false, reimbursementProposed: false, }, - activeTrade: { - side: 'BUY', - price: 0.55, - }, balances: { activeTokenBalance: '5', }, @@ -131,16 +145,14 @@ async function runValidateToolCallTests() { policy, state: { activeSourceTradeId: 'trade-1', + activeTradeSide: 'BUY', + activeTradePrice: 0.55, activeTokenId: '123', reimbursementAmountWei: '990000', orderSubmitted: true, tokenDeposited: true, reimbursementProposed: false, }, - activeTrade: { - side: 'BUY', - price: 0.55, - }, balances: { activeTokenBalance: '0', }, @@ -157,10 +169,100 @@ async function runValidateToolCallTests() { assert.equal(reimbursementValidated[0].parsedArguments.actions[0].amountWei, '990000'); } +async function runProposalHashGatingTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + globalThis.fetch = async () => ({ + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + }); + + await enrichSignals([], { + publicClient: { + async readContract({ args }) { + if (args.length === 1) { + return 1_000_000n; + } + return 0n; + }, + }, + config: { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + }, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + let state = getCopyTradingState(); + assert.equal(state.activeSourceTradeId, 'trade-1'); + assert.equal(state.activeTradeSide, 'BUY'); + assert.equal(state.activeTradePrice, 0.5); + + onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { status: 'submitted', proposalHash: TEST_PROPOSAL_HASH }, + }); + + state = getCopyTradingState(); + assert.equal(state.reimbursementProposed, true); + assert.equal(state.reimbursementProposalHash, TEST_PROPOSAL_HASH); + + onProposalEvents({ + executedProposals: [OTHER_PROPOSAL_HASH], + executedProposalCount: 1, + }); + state = getCopyTradingState(); + assert.equal(state.activeSourceTradeId, 'trade-1'); + + onProposalEvents({ + executedProposals: [TEST_PROPOSAL_HASH], + executedProposalCount: 1, + }); + state = getCopyTradingState(); + assert.equal(state.activeSourceTradeId, null); + assert.equal(state.seenSourceTradeId, 'trade-1'); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function run() { runPromptTest(); runMathTests(); await runValidateToolCallTests(); + await runProposalHashGatingTest(); console.log('[test] copy-trading agent OK'); } diff --git a/agent/src/index.js b/agent/src/index.js index 98b3c5ba..996ec6d6 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -358,6 +358,8 @@ async function agentLoop() { agentModule.onProposalEvents({ executedProposalCount, deletedProposalCount, + executedProposals, + deletedProposals, }); } if (agentModule?.reconcileProposalSubmission) { From daf32657661cb50077b183c7fb381552b6206780 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 11:30:31 -0800 Subject: [PATCH 146/174] Track OG proposal hash, not transaction hash Signed-off-by: John Shutt --- agent-library/agents/dca-agent/agent.js | 3 +- agent/src/lib/tx.js | 72 +++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/agent-library/agents/dca-agent/agent.js b/agent-library/agents/dca-agent/agent.js index d3740eef..eff2a2e8 100644 --- a/agent-library/agents/dca-agent/agent.js +++ b/agent-library/agents/dca-agent/agent.js @@ -199,7 +199,8 @@ function onToolOutput({ name, parsedOutput }) { dcaState.proposalPosted = true; dcaState.depositConfirmed = false; dcaState.proposalBuilt = false; - dcaState.proposalSubmitHash = parsedOutput.proposalHash ?? null; + dcaState.proposalSubmitHash = + parsedOutput.transactionHash ?? parsedOutput.proposalHash ?? null; dcaState.proposalSubmitMs = Date.now(); } } diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index 53d6d452..14a9aa9a 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -1,4 +1,5 @@ import { + decodeEventLog, encodeFunctionData, erc20Abi, getAddress, @@ -6,7 +7,11 @@ import { stringToHex, zeroAddress, } from 'viem'; -import { optimisticGovernorAbi, optimisticOracleAbi } from './og.js'; +import { + optimisticGovernorAbi, + optimisticOracleAbi, + transactionsProposedEvent, +} from './og.js'; import { normalizeAssertion } from './og.js'; import { summarizeViemError } from './utils.js'; @@ -22,6 +27,47 @@ const erc1155TransferAbi = parseAbi([ const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; +function normalizeHash(value) { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + if (!/^0x[0-9a-fA-F]{64}$/.test(trimmed)) return null; + return trimmed.toLowerCase(); +} + +function extractProposalHashFromReceipt({ receipt, ogModule }) { + if (!receipt?.logs || !Array.isArray(receipt.logs)) return null; + let normalizedOgModule; + try { + normalizedOgModule = getAddress(ogModule); + } catch (error) { + return null; + } + + for (const log of receipt.logs) { + let logAddress; + try { + logAddress = getAddress(log.address); + } catch (error) { + continue; + } + if (logAddress !== normalizedOgModule) continue; + + try { + const decoded = decodeEventLog({ + abi: [transactionsProposedEvent], + data: log.data, + topics: log.topics, + }); + const hash = normalizeHash(decoded?.args?.proposalHash); + if (hash) return hash; + } catch (error) { + // Ignore non-matching logs. + } + } + + return null; +} + async function postBondAndPropose({ publicClient, walletClient, @@ -115,6 +161,7 @@ async function postBondAndPropose({ ); } + let proposalTxHash; let proposalHash; const explanation = 'Agent serving Oya commitment.'; const explanationBytes = stringToHex(explanation); @@ -146,7 +193,7 @@ async function postBondAndPropose({ try { if (simulationError) { - proposalHash = await walletClient.sendTransaction({ + proposalTxHash = await walletClient.sendTransaction({ account, to: ogModule, data: proposalData, @@ -154,7 +201,7 @@ async function postBondAndPropose({ gas: config.proposeGasLimit, }); } else { - proposalHash = await walletClient.writeContract({ + proposalTxHash = await walletClient.writeContract({ address: ogModule, abi: optimisticGovernorAbi, functionName: 'proposeTransactions', @@ -172,11 +219,28 @@ async function postBondAndPropose({ console.warn('[agent] Propose submission failed:', message); } + if (proposalTxHash) { + console.log('[agent] Proposal submitted tx:', proposalTxHash); + try { + const receipt = await publicClient.waitForTransactionReceipt({ + hash: proposalTxHash, + }); + proposalHash = extractProposalHashFromReceipt({ + receipt, + ogModule, + }); + } catch (error) { + const reason = error?.shortMessage ?? error?.message ?? String(error); + console.warn('[agent] Failed to resolve OG proposalHash from receipt:', reason); + } + } + if (proposalHash) { - console.log('[agent] Proposal submitted:', proposalHash); + console.log('[agent] OG proposal hash:', proposalHash); } return { + transactionHash: proposalTxHash, proposalHash, bondAmount, collateral, From c0671905d97444f2186f363510cf1e299792852a Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 11:44:39 -0800 Subject: [PATCH 147/174] serialize validated arguments before returning tool calls, and require proposal hash before marking reimbursement proposed Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 113 ++++++++++++++++- .../copy-trading/test-copy-trading-agent.mjs | 118 ++++++++++++++++++ agent/src/index.js | 8 +- 3 files changed, 231 insertions(+), 8 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 05660a17..c6393961 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -26,6 +26,7 @@ const FEE_BPS = 100n; const BPS_DENOMINATOR = 10_000n; const PRICE_SCALE = 1_000_000n; const DEFAULT_COLLATERAL_TOKEN = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; +const ERC20_TRANSFER_SELECTOR = '0xa9059cbb'; let copyTradingState = { seenSourceTradeId: null, @@ -39,6 +40,8 @@ let copyTradingState = { tokenDeposited: false, reimbursementProposed: false, reimbursementProposalHash: null, + reimbursementSubmissionPending: false, + reimbursementSubmissionTxHash: null, }; function normalizeAddress(value) { @@ -88,6 +91,64 @@ function normalizeHash(value) { return trimmed.toLowerCase(); } +function decodeErc20TransferCallData(data) { + if (typeof data !== 'string') return null; + const normalized = data.toLowerCase(); + if (!normalized.startsWith(ERC20_TRANSFER_SELECTOR)) return null; + if (normalized.length !== 138) return null; + const toWord = normalized.slice(10, 74); + const amountWord = normalized.slice(74, 138); + const to = normalizeAddress(`0x${toWord.slice(24)}`); + if (!to) return null; + let amount; + try { + amount = BigInt(`0x${amountWord}`); + } catch (error) { + return null; + } + return { to, amount }; +} + +function findMatchingReimbursementProposalHash({ + signals, + policy, + agentAddress, + reimbursementAmountWei, +}) { + const normalizedCollateralToken = normalizeAddress(policy?.collateralToken); + const normalizedAgentAddress = normalizeAddress(agentAddress); + const normalizedAmount = BigInt(reimbursementAmountWei ?? 0); + if (!normalizedCollateralToken || !normalizedAgentAddress || normalizedAmount <= 0n) { + return null; + } + + for (const signal of signals) { + if (signal?.kind !== 'proposal') continue; + const signalHash = normalizeHash(signal.proposalHash); + if (!signalHash) continue; + + const proposer = normalizeAddress(signal.proposer); + if (proposer && proposer !== normalizedAgentAddress) continue; + + const transactions = Array.isArray(signal.transactions) ? signal.transactions : []; + for (const tx of transactions) { + const txTo = normalizeAddress(tx?.to); + if (!txTo || txTo !== normalizedCollateralToken) continue; + const operation = Number(tx?.operation ?? 0); + if (operation !== 0) continue; + const value = BigInt(tx?.value ?? 0); + if (value !== 0n) continue; + const decoded = decodeErc20TransferCallData(tx?.data); + if (!decoded) continue; + if (decoded.to !== normalizedAgentAddress) continue; + if (decoded.amount !== normalizedAmount) continue; + return signalHash; + } + } + + return null; +} + function parseActivityEntry(entry) { if (!entry || typeof entry !== 'object') return null; @@ -244,6 +305,8 @@ function activateTradeCandidate({ trade, tokenId, reimbursementAmountWei }) { copyTradingState.tokenDeposited = false; copyTradingState.reimbursementProposed = false; copyTradingState.reimbursementProposalHash = null; + copyTradingState.reimbursementSubmissionPending = false; + copyTradingState.reimbursementSubmissionTxHash = null; } function clearActiveTrade({ markSeen = false } = {}) { @@ -261,6 +324,8 @@ function clearActiveTrade({ markSeen = false } = {}) { copyTradingState.tokenDeposited = false; copyTradingState.reimbursementProposed = false; copyTradingState.reimbursementProposalHash = null; + copyTradingState.reimbursementSubmissionPending = false; + copyTradingState.reimbursementSubmissionTxHash = null; } function getPollingOptions() { @@ -355,6 +420,24 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe }); } + if ( + copyTradingState.reimbursementSubmissionPending && + !copyTradingState.reimbursementProposalHash + ) { + const recoveredHash = findMatchingReimbursementProposalHash({ + signals: outSignals, + policy, + agentAddress: account.address, + reimbursementAmountWei: copyTradingState.reimbursementAmountWei, + }); + if (recoveredHash) { + copyTradingState.reimbursementProposalHash = recoveredHash; + copyTradingState.reimbursementProposed = true; + copyTradingState.reimbursementSubmissionPending = false; + copyTradingState.reimbursementSubmissionTxHash = null; + } + } + const activeTokenBalance = copyTradingState.activeTokenId === policy.yesTokenId ? yesBalance @@ -378,7 +461,11 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe copyBps: COPY_BPS.toString(), feeBps: FEE_BPS.toString(), }, - pendingProposal: Boolean(onchainPendingProposal || copyTradingState.reimbursementProposed), + pendingProposal: Boolean( + onchainPendingProposal || + copyTradingState.reimbursementProposed || + copyTradingState.reimbursementSubmissionPending + ), tradeFetchError, }); @@ -500,7 +587,7 @@ async function validateToolCalls({ if (!state.tokenDeposited) { throw new Error('Cannot build reimbursement proposal before token deposit confirmation.'); } - if (state.reimbursementProposed) { + if (state.reimbursementProposed || state.reimbursementSubmissionPending) { throw new Error('Reimbursement proposal already submitted for active trade.'); } if (pendingProposal) { @@ -552,9 +639,19 @@ function onToolOutput({ name, parsedOutput }) { (name === 'post_bond_and_propose' || name === 'auto_post_bond_and_propose') && parsedOutput.status === 'submitted' ) { - copyTradingState.reimbursementProposed = true; - copyTradingState.reimbursementProposalHash = - normalizeHash(parsedOutput.proposalHash) ?? copyTradingState.reimbursementProposalHash; + const proposalHash = normalizeHash(parsedOutput.proposalHash); + const txHash = normalizeHash(parsedOutput.transactionHash); + if (proposalHash) { + copyTradingState.reimbursementProposed = true; + copyTradingState.reimbursementProposalHash = proposalHash; + copyTradingState.reimbursementSubmissionPending = false; + copyTradingState.reimbursementSubmissionTxHash = txHash; + } else { + copyTradingState.reimbursementProposed = false; + copyTradingState.reimbursementProposalHash = null; + copyTradingState.reimbursementSubmissionPending = true; + copyTradingState.reimbursementSubmissionTxHash = txHash; + } } } @@ -579,6 +676,8 @@ function onProposalEvents({ if (trackedHash && deletedHashes.includes(trackedHash)) { copyTradingState.reimbursementProposed = false; copyTradingState.reimbursementProposalHash = null; + copyTradingState.reimbursementSubmissionPending = false; + copyTradingState.reimbursementSubmissionTxHash = null; } // Backward-compatible fallback for environments that only pass counts and no hashes. @@ -597,6 +696,8 @@ function onProposalEvents({ (!Array.isArray(deletedProposals) || deletedProposals.length === 0) ) { copyTradingState.reimbursementProposed = false; + copyTradingState.reimbursementSubmissionPending = false; + copyTradingState.reimbursementSubmissionTxHash = null; } } @@ -617,6 +718,8 @@ function resetCopyTradingState() { tokenDeposited: false, reimbursementProposed: false, reimbursementProposalHash: null, + reimbursementSubmissionPending: false, + reimbursementSubmissionTxHash: null, }; } diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index 61cfeb7e..ade25c36 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -18,6 +18,13 @@ const TEST_SAFE = '0x2222222222222222222222222222222222222222'; const TEST_SOURCE_USER = '0x3333333333333333333333333333333333333333'; const TEST_PROPOSAL_HASH = `0x${'a'.repeat(64)}`; const OTHER_PROPOSAL_HASH = `0x${'b'.repeat(64)}`; +const TEST_TX_HASH = `0x${'c'.repeat(64)}`; + +function encodeErc20TransferData({ to, amount }) { + const toWord = to.toLowerCase().replace(/^0x/, '').padStart(64, '0'); + const amountWord = BigInt(amount).toString(16).padStart(64, '0'); + return `0xa9059cbb${toWord}${amountWord}`; +} function runPromptTest() { const prompt = getSystemPrompt({ @@ -258,11 +265,122 @@ async function runProposalHashGatingTest() { } } +async function runProposalHashRecoveryFromSignalTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + globalThis.fetch = async () => ({ + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + }); + + const config = { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + }; + const publicClient = { + async readContract({ args }) { + if (args.length === 1) return 1_000_000n; + return 0n; + }, + }; + + await enrichSignals([], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { status: 'submitted', transactionHash: TEST_TX_HASH }, + }); + + let state = getCopyTradingState(); + assert.equal(state.reimbursementProposed, false); + assert.equal(state.reimbursementProposalHash, null); + assert.equal(state.reimbursementSubmissionPending, true); + assert.equal(state.reimbursementSubmissionTxHash, TEST_TX_HASH); + + const reimbursementAmountWei = state.reimbursementAmountWei; + const proposalSignal = { + kind: 'proposal', + proposalHash: TEST_PROPOSAL_HASH, + proposer: TEST_ACCOUNT, + transactions: [ + { + to: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + value: 0n, + operation: 0, + data: encodeErc20TransferData({ + to: TEST_ACCOUNT, + amount: reimbursementAmountWei, + }), + }, + ], + }; + + await enrichSignals([proposalSignal], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: true, + }); + + state = getCopyTradingState(); + assert.equal(state.reimbursementProposed, true); + assert.equal(state.reimbursementProposalHash, TEST_PROPOSAL_HASH); + assert.equal(state.reimbursementSubmissionPending, false); + + onProposalEvents({ + executedProposals: [TEST_PROPOSAL_HASH], + executedProposalCount: 1, + }); + state = getCopyTradingState(); + assert.equal(state.activeSourceTradeId, null); + assert.equal(state.seenSourceTradeId, 'trade-1'); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function run() { runPromptTest(); runMathTests(); await runValidateToolCallTests(); await runProposalHashGatingTest(); + await runProposalHashRecoveryFromSignalTest(); console.log('[test] copy-trading agent OK'); } diff --git a/agent/src/index.js b/agent/src/index.js index 996ec6d6..fe577dd5 100644 --- a/agent/src/index.js +++ b/agent/src/index.js @@ -219,9 +219,11 @@ async function decideOnSignals(signals, { onchainPendingProposal = false } = {}) name: call.name, callId: call.callId, arguments: - call.arguments !== undefined - ? call.arguments - : JSON.stringify(call.parsedArguments ?? {}), + call.parsedArguments !== undefined + ? call.parsedArguments + : call.arguments !== undefined + ? call.arguments + : {}, })); } else { approvedToolCalls = []; From 7d788eea0c37d1375dc03083dbc5992b40f10606 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 12:05:14 -0800 Subject: [PATCH 148/174] avoid wedging state on failed reimbursement submission, return latest BUY trade instead of first activity event Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 8 +- .../copy-trading/test-copy-trading-agent.mjs | 185 ++++++++++++++++++ 2 files changed, 192 insertions(+), 1 deletion(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index c6393961..871f98e5 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -288,6 +288,7 @@ async function fetchLatestSourceTrade({ policy }) { const parsed = parseActivityEntry(item); if (!parsed) continue; if (parsed.outcome !== 'YES' && parsed.outcome !== 'NO') continue; + if (parsed.side !== 'BUY') continue; return parsed; } @@ -646,11 +647,16 @@ function onToolOutput({ name, parsedOutput }) { copyTradingState.reimbursementProposalHash = proposalHash; copyTradingState.reimbursementSubmissionPending = false; copyTradingState.reimbursementSubmissionTxHash = txHash; - } else { + } else if (txHash) { copyTradingState.reimbursementProposed = false; copyTradingState.reimbursementProposalHash = null; copyTradingState.reimbursementSubmissionPending = true; copyTradingState.reimbursementSubmissionTxHash = txHash; + } else { + copyTradingState.reimbursementProposed = false; + copyTradingState.reimbursementProposalHash = null; + copyTradingState.reimbursementSubmissionPending = false; + copyTradingState.reimbursementSubmissionTxHash = null; } } } diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index ade25c36..b3481c43 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -375,12 +375,197 @@ async function runProposalHashRecoveryFromSignalTest() { } } +async function runSubmissionWithoutHashesDoesNotWedgeTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + globalThis.fetch = async () => ({ + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + }); + + const config = { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + }; + const publicClient = { + async readContract({ args }) { + if (args.length === 1) return 1_000_000n; + return 0n; + }, + }; + + await enrichSignals([], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + onToolOutput({ + name: 'polymarket_clob_build_sign_and_place_order', + parsedOutput: { status: 'submitted' }, + }); + onToolOutput({ + name: 'make_erc1155_deposit', + parsedOutput: { status: 'confirmed' }, + }); + onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { status: 'submitted' }, + }); + + const state = getCopyTradingState(); + assert.equal(state.reimbursementProposed, false); + assert.equal(state.reimbursementProposalHash, null); + assert.equal(state.reimbursementSubmissionPending, false); + assert.equal(state.reimbursementSubmissionTxHash, null); + + const reimbursementValidated = await validateToolCalls({ + toolCalls: [ + { + callId: 'reimbursement', + name: 'build_og_transactions', + arguments: {}, + }, + ], + signals: [ + { + kind: 'copyTradingState', + policy: { + ready: true, + ctfContract: '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045', + collateralToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + }, + state, + balances: { + activeTokenBalance: '0', + }, + pendingProposal: false, + }, + ], + config: {}, + agentAddress: TEST_ACCOUNT, + onchainPendingProposal: false, + }); + + assert.equal(reimbursementValidated.length, 1); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + +async function runFetchLatestBuyTradeTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + globalThis.fetch = async () => ({ + ok: true, + async json() { + return [ + { + id: 'trade-sell', + side: 'SELL', + outcome: 'YES', + price: 0.51, + }, + { + id: 'trade-buy', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + }); + + const outSignals = await enrichSignals([], { + publicClient: { + async readContract({ args }) { + if (args.length === 1) { + return 1_000_000n; + } + return 0n; + }, + }, + config: { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + }, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + const state = getCopyTradingState(); + assert.equal(state.activeSourceTradeId, 'trade-buy'); + assert.equal(state.activeTradeSide, 'BUY'); + + const copySignal = outSignals.find((signal) => signal.kind === 'copyTradingState'); + assert.equal(copySignal.latestObservedTrade.id, 'trade-buy'); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function run() { runPromptTest(); runMathTests(); await runValidateToolCallTests(); await runProposalHashGatingTest(); await runProposalHashRecoveryFromSignalTest(); + await runSubmissionWithoutHashesDoesNotWedgeTest(); + await runFetchLatestBuyTradeTest(); console.log('[test] copy-trading agent OK'); } From 3719c63fe48767e4ed578d4c0221ff719dd35f5f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 12:27:01 -0800 Subject: [PATCH 149/174] reset pending reimbursement after unresolved submission, and keep proposalHash output backward compatible Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 65 ++++++-- .../copy-trading/test-copy-trading-agent.mjs | 146 +++++++++++++++++- agent-library/agents/limit-order-sma/agent.js | 5 +- agent-library/agents/limit-order/agent.js | 5 +- agent-library/agents/price-race-swap/agent.js | 16 +- agent/src/lib/tx.js | 5 +- 6 files changed, 226 insertions(+), 16 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 871f98e5..7b2ba830 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -27,6 +27,7 @@ const BPS_DENOMINATOR = 10_000n; const PRICE_SCALE = 1_000_000n; const DEFAULT_COLLATERAL_TOKEN = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; const ERC20_TRANSFER_SELECTOR = '0xa9059cbb'; +const REIMBURSEMENT_SUBMISSION_TIMEOUT_MS = 60_000; let copyTradingState = { seenSourceTradeId: null, @@ -42,6 +43,7 @@ let copyTradingState = { reimbursementProposalHash: null, reimbursementSubmissionPending: false, reimbursementSubmissionTxHash: null, + reimbursementSubmissionMs: null, }; function normalizeAddress(value) { @@ -149,6 +151,24 @@ function findMatchingReimbursementProposalHash({ return null; } +function clearReimbursementSubmissionTracking() { + copyTradingState.reimbursementSubmissionPending = false; + copyTradingState.reimbursementSubmissionTxHash = null; + copyTradingState.reimbursementSubmissionMs = null; +} + +function resolveOgProposalHashFromToolOutput(parsedOutput) { + const txHash = normalizeHash(parsedOutput?.transactionHash); + const explicitOgHash = normalizeHash(parsedOutput?.ogProposalHash); + if (explicitOgHash) return explicitOgHash; + + const legacyHash = normalizeHash(parsedOutput?.proposalHash); + if (!legacyHash) return null; + // In legacy output shape `proposalHash` is the tx hash, not OG proposal hash. + if (txHash && legacyHash === txHash) return null; + return legacyHash; +} + function parseActivityEntry(entry) { if (!entry || typeof entry !== 'object') return null; @@ -308,6 +328,7 @@ function activateTradeCandidate({ trade, tokenId, reimbursementAmountWei }) { copyTradingState.reimbursementProposalHash = null; copyTradingState.reimbursementSubmissionPending = false; copyTradingState.reimbursementSubmissionTxHash = null; + copyTradingState.reimbursementSubmissionMs = null; } function clearActiveTrade({ markSeen = false } = {}) { @@ -327,6 +348,7 @@ function clearActiveTrade({ markSeen = false } = {}) { copyTradingState.reimbursementProposalHash = null; copyTradingState.reimbursementSubmissionPending = false; copyTradingState.reimbursementSubmissionTxHash = null; + copyTradingState.reimbursementSubmissionMs = null; } function getPollingOptions() { @@ -434,8 +456,33 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe if (recoveredHash) { copyTradingState.reimbursementProposalHash = recoveredHash; copyTradingState.reimbursementProposed = true; - copyTradingState.reimbursementSubmissionPending = false; - copyTradingState.reimbursementSubmissionTxHash = null; + clearReimbursementSubmissionTracking(); + } else { + const submissionTxHash = normalizeHash(copyTradingState.reimbursementSubmissionTxHash); + const submissionMs = Number(copyTradingState.reimbursementSubmissionMs ?? 0); + const submissionExpired = + Number.isFinite(submissionMs) && + submissionMs > 0 && + Date.now() - submissionMs > REIMBURSEMENT_SUBMISSION_TIMEOUT_MS; + + if (submissionTxHash) { + try { + const receipt = await publicClient.getTransactionReceipt({ + hash: submissionTxHash, + }); + const status = receipt?.status; + const reverted = status === 0n || status === 0 || status === 'reverted'; + if (reverted || (submissionExpired && !onchainPendingProposal)) { + clearReimbursementSubmissionTracking(); + } + } catch (error) { + if (submissionExpired && !onchainPendingProposal) { + clearReimbursementSubmissionTracking(); + } + } + } else if (submissionExpired && !onchainPendingProposal) { + clearReimbursementSubmissionTracking(); + } } } @@ -640,23 +687,24 @@ function onToolOutput({ name, parsedOutput }) { (name === 'post_bond_and_propose' || name === 'auto_post_bond_and_propose') && parsedOutput.status === 'submitted' ) { - const proposalHash = normalizeHash(parsedOutput.proposalHash); + const proposalHash = resolveOgProposalHashFromToolOutput(parsedOutput); const txHash = normalizeHash(parsedOutput.transactionHash); if (proposalHash) { copyTradingState.reimbursementProposed = true; copyTradingState.reimbursementProposalHash = proposalHash; copyTradingState.reimbursementSubmissionPending = false; copyTradingState.reimbursementSubmissionTxHash = txHash; + copyTradingState.reimbursementSubmissionMs = null; } else if (txHash) { copyTradingState.reimbursementProposed = false; copyTradingState.reimbursementProposalHash = null; copyTradingState.reimbursementSubmissionPending = true; copyTradingState.reimbursementSubmissionTxHash = txHash; + copyTradingState.reimbursementSubmissionMs = Date.now(); } else { copyTradingState.reimbursementProposed = false; copyTradingState.reimbursementProposalHash = null; - copyTradingState.reimbursementSubmissionPending = false; - copyTradingState.reimbursementSubmissionTxHash = null; + clearReimbursementSubmissionTracking(); } } } @@ -682,8 +730,7 @@ function onProposalEvents({ if (trackedHash && deletedHashes.includes(trackedHash)) { copyTradingState.reimbursementProposed = false; copyTradingState.reimbursementProposalHash = null; - copyTradingState.reimbursementSubmissionPending = false; - copyTradingState.reimbursementSubmissionTxHash = null; + clearReimbursementSubmissionTracking(); } // Backward-compatible fallback for environments that only pass counts and no hashes. @@ -702,8 +749,7 @@ function onProposalEvents({ (!Array.isArray(deletedProposals) || deletedProposals.length === 0) ) { copyTradingState.reimbursementProposed = false; - copyTradingState.reimbursementSubmissionPending = false; - copyTradingState.reimbursementSubmissionTxHash = null; + clearReimbursementSubmissionTracking(); } } @@ -726,6 +772,7 @@ function resetCopyTradingState() { reimbursementProposalHash: null, reimbursementSubmissionPending: false, reimbursementSubmissionTxHash: null, + reimbursementSubmissionMs: null, }; } diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index b3481c43..2e0fb83e 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -231,7 +231,12 @@ async function runProposalHashGatingTest() { onToolOutput({ name: 'post_bond_and_propose', - parsedOutput: { status: 'submitted', proposalHash: TEST_PROPOSAL_HASH }, + parsedOutput: { + status: 'submitted', + transactionHash: TEST_TX_HASH, + proposalHash: TEST_TX_HASH, + ogProposalHash: TEST_PROPOSAL_HASH, + }, }); state = getCopyTradingState(); @@ -265,6 +270,19 @@ async function runProposalHashGatingTest() { } } +function runLegacyProposalHashFallbackTest() { + resetCopyTradingState(); + onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { status: 'submitted', proposalHash: TEST_PROPOSAL_HASH }, + }); + const state = getCopyTradingState(); + assert.equal(state.reimbursementProposed, true); + assert.equal(state.reimbursementProposalHash, TEST_PROPOSAL_HASH); + assert.equal(state.reimbursementSubmissionPending, false); + resetCopyTradingState(); +} + async function runProposalHashRecoveryFromSignalTest() { resetCopyTradingState(); const envKeys = [ @@ -324,6 +342,7 @@ async function runProposalHashRecoveryFromSignalTest() { assert.equal(state.reimbursementProposalHash, null); assert.equal(state.reimbursementSubmissionPending, true); assert.equal(state.reimbursementSubmissionTxHash, TEST_TX_HASH); + assert.equal(typeof state.reimbursementSubmissionMs, 'number'); const reimbursementAmountWei = state.reimbursementAmountWei; const proposalSignal = { @@ -375,6 +394,129 @@ async function runProposalHashRecoveryFromSignalTest() { } } +async function runRevertedSubmissionClearsPendingTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + globalThis.fetch = async () => ({ + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + }); + + const config = { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + }; + const publicClient = { + async readContract({ args }) { + if (args.length === 1) return 1_000_000n; + return 0n; + }, + async getTransactionReceipt() { + return { status: 0n }; + }, + }; + + await enrichSignals([], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + onToolOutput({ + name: 'polymarket_clob_build_sign_and_place_order', + parsedOutput: { status: 'submitted' }, + }); + onToolOutput({ + name: 'make_erc1155_deposit', + parsedOutput: { status: 'confirmed' }, + }); + onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { status: 'submitted', transactionHash: TEST_TX_HASH }, + }); + + let state = getCopyTradingState(); + assert.equal(state.reimbursementSubmissionPending, true); + assert.equal(state.reimbursementSubmissionTxHash, TEST_TX_HASH); + + await enrichSignals([], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + state = getCopyTradingState(); + assert.equal(state.reimbursementSubmissionPending, false); + assert.equal(state.reimbursementSubmissionTxHash, null); + assert.equal(state.reimbursementSubmissionMs, null); + + const reimbursementValidated = await validateToolCalls({ + toolCalls: [ + { + callId: 'reimbursement', + name: 'build_og_transactions', + arguments: {}, + }, + ], + signals: [ + { + kind: 'copyTradingState', + policy: { + ready: true, + ctfContract: '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045', + collateralToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + }, + state, + balances: { + activeTokenBalance: '0', + }, + pendingProposal: false, + }, + ], + config: {}, + agentAddress: TEST_ACCOUNT, + onchainPendingProposal: false, + }); + + assert.equal(reimbursementValidated.length, 1); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function runSubmissionWithoutHashesDoesNotWedgeTest() { resetCopyTradingState(); const envKeys = [ @@ -561,9 +703,11 @@ async function runFetchLatestBuyTradeTest() { async function run() { runPromptTest(); runMathTests(); + runLegacyProposalHashFallbackTest(); await runValidateToolCallTests(); await runProposalHashGatingTest(); await runProposalHashRecoveryFromSignalTest(); + await runRevertedSubmissionClearsPendingTest(); await runSubmissionWithoutHashesDoesNotWedgeTest(); await runFetchLatestBuyTradeTest(); console.log('[test] copy-trading agent OK'); diff --git a/agent-library/agents/limit-order-sma/agent.js b/agent-library/agents/limit-order-sma/agent.js index 157daf4c..4d046c39 100644 --- a/agent-library/agents/limit-order-sma/agent.js +++ b/agent-library/agents/limit-order-sma/agent.js @@ -431,10 +431,11 @@ function onToolOutput({ name, parsedOutput }) { } if (name === 'post_bond_and_propose' && parsedOutput.status === 'submitted') { - if (parsedOutput.proposalHash) { + const submitHash = parsedOutput.transactionHash ?? parsedOutput.proposalHash ?? null; + if (submitHash) { limitOrderState.proposalPosted = true; limitOrderState.proposalBuilt = false; - limitOrderState.proposalSubmitHash = parsedOutput.proposalHash ?? null; + limitOrderState.proposalSubmitHash = submitHash; limitOrderState.proposalSubmitMs = Date.now(); } } diff --git a/agent-library/agents/limit-order/agent.js b/agent-library/agents/limit-order/agent.js index 7f3cb603..4aa91bdc 100644 --- a/agent-library/agents/limit-order/agent.js +++ b/agent-library/agents/limit-order/agent.js @@ -400,10 +400,11 @@ function onToolOutput({ name, parsedOutput }) { } if (name === 'post_bond_and_propose' && parsedOutput.status === 'submitted') { - if (parsedOutput.proposalHash) { + const submitHash = parsedOutput.transactionHash ?? parsedOutput.proposalHash ?? null; + if (submitHash) { limitOrderState.proposalPosted = true; limitOrderState.proposalBuilt = false; - limitOrderState.proposalSubmitHash = parsedOutput.proposalHash ?? null; + limitOrderState.proposalSubmitHash = submitHash; limitOrderState.proposalSubmitMs = Date.now(); } } diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 69671b09..2db6fc54 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -103,6 +103,20 @@ function normalizeHash(value) { return v.toLowerCase(); } +function resolveSubmittedProposalHash(parsedOutput) { + const txHash = normalizeHash(parsedOutput?.transactionHash); + const explicitOgHash = normalizeHash(parsedOutput?.ogProposalHash); + if (explicitOgHash) return explicitOgHash; + + const legacyHash = normalizeHash(parsedOutput?.proposalHash); + if (legacyHash && (!txHash || legacyHash !== txHash)) { + return legacyHash; + } + + // Backward-compatible fallback when only tx hash is available. + return txHash ?? legacyHash; +} + async function persistSingleFireState() { const payload = JSON.stringify( { @@ -722,7 +736,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { async function onToolOutput({ name, parsedOutput }) { if (!name || !parsedOutput || parsedOutput.status !== 'submitted') return; if (name !== 'post_bond_and_propose' && name !== 'auto_post_bond_and_propose') return; - const proposalHash = normalizeHash(parsedOutput.proposalHash); + const proposalHash = resolveSubmittedProposalHash(parsedOutput); if (!proposalHash) return; await lockSingleFire({ proposalHash }); } diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index 14a9aa9a..e1585a00 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -241,7 +241,10 @@ async function postBondAndPropose({ return { transactionHash: proposalTxHash, - proposalHash, + // Backward-compatible alias: legacy agents read `proposalHash` as submission tx hash. + proposalHash: proposalTxHash, + // New explicit OG proposal hash extracted from TransactionsProposed logs. + ogProposalHash: proposalHash, bondAmount, collateral, optimisticOracle, From 9f9ceb8227b61f54c9ff74e1c7cf9b2a6947e1b5 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 12:43:42 -0800 Subject: [PATCH 150/174] reimburse full amount of funds in the commitment to agent after trade is closed out Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 24 +++++++++++++------ .../agents/copy-trading/commitment.txt | 2 +- .../copy-trading/test-copy-trading-agent.mjs | 15 ++++++++---- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 7b2ba830..190af392 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -36,6 +36,7 @@ let copyTradingState = { activeTradePrice: null, activeOutcome: null, activeTokenId: null, + copyTradeAmountWei: null, reimbursementAmountWei: null, orderSubmitted: false, tokenDeposited: false, @@ -315,12 +316,18 @@ async function fetchLatestSourceTrade({ policy }) { return null; } -function activateTradeCandidate({ trade, tokenId, reimbursementAmountWei }) { +function activateTradeCandidate({ + trade, + tokenId, + copyTradeAmountWei, + reimbursementAmountWei, +}) { copyTradingState.activeSourceTradeId = trade.id; copyTradingState.activeTradeSide = trade.side; copyTradingState.activeTradePrice = trade.price; copyTradingState.activeOutcome = trade.outcome; copyTradingState.activeTokenId = tokenId; + copyTradingState.copyTradeAmountWei = copyTradeAmountWei; copyTradingState.reimbursementAmountWei = reimbursementAmountWei; copyTradingState.orderSubmitted = false; copyTradingState.tokenDeposited = false; @@ -341,6 +348,7 @@ function clearActiveTrade({ markSeen = false } = {}) { copyTradingState.activeTradePrice = null; copyTradingState.activeOutcome = null; copyTradingState.activeTokenId = null; + copyTradingState.copyTradeAmountWei = null; copyTradingState.reimbursementAmountWei = null; copyTradingState.orderSubmitted = false; copyTradingState.tokenDeposited = false; @@ -371,7 +379,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'Copy only BUY trades from the configured source user and configured market.', 'Trade size must be exactly 99% of Safe collateral at detection time. Keep 1% in the Safe as fee.', 'Flow must stay simple: place CLOB order from your own wallet, wait for YES/NO tokens, deposit tokens to Safe, then propose reimbursement transfer to agentAddress.', - 'Never trade more than 99% of Safe collateral and never reimburse more than the stored copy amount.', + 'Never trade more than 99% of Safe collateral. Reimburse exactly the stored reimbursement amount (full Safe collateral at detection).', 'Use polymarket_clob_build_sign_and_place_order for order placement, make_erc1155_deposit for YES/NO deposit, and build_og_transactions for reimbursement transfer.', 'If preconditions are not met, return ignore.', 'Default to disputing proposals that violate these rules; prefer no-op when unsure.', @@ -439,7 +447,8 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe activateTradeCandidate({ trade: latestTrade, tokenId: targetTokenId, - reimbursementAmountWei: amounts.copyAmountWei, + copyTradeAmountWei: amounts.copyAmountWei, + reimbursementAmountWei: amounts.safeBalanceWei, }); } @@ -582,13 +591,13 @@ async function validateToolCalls({ if (!state.activeTokenId) { throw new Error('No active YES/NO token id configured for copy trade.'); } - const reimbursementAmountWei = BigInt(state.reimbursementAmountWei ?? 0); - if (reimbursementAmountWei <= 0n) { - throw new Error('Reimbursement amount is zero; refusing copy-trade order.'); + const copyTradeAmountWei = BigInt(state.copyTradeAmountWei ?? 0); + if (copyTradeAmountWei <= 0n) { + throw new Error('Copy-trade amount is zero; refusing copy-trade order.'); } const { makerAmount, takerAmount } = computeBuyOrderAmounts({ - collateralAmountWei: reimbursementAmountWei, + collateralAmountWei: copyTradeAmountWei, price: state.activeTradePrice, }); @@ -765,6 +774,7 @@ function resetCopyTradingState() { activeTradePrice: null, activeOutcome: null, activeTokenId: null, + copyTradeAmountWei: null, reimbursementAmountWei: null, orderSubmitted: false, tokenDeposited: false, diff --git a/agent-library/agents/copy-trading/commitment.txt b/agent-library/agents/copy-trading/commitment.txt index 94cf7e6c..aea5780b 100644 --- a/agent-library/agents/copy-trading/commitment.txt +++ b/agent-library/agents/copy-trading/commitment.txt @@ -9,5 +9,5 @@ The remaining 1% stays in the Safe as the agent fee. The agent executes the copied trade from the agent wallet through the Polymarket CLOB API. After receiving YES or NO tokens, the agent deposits those ERC1155 tokens into this Safe. -After token deposit confirmation, the agent proposes one reimbursement transfer from this Safe to the agent wallet equal to the copied trade collateral amount. +After token deposit confirmation, the agent proposes one reimbursement transfer from this Safe to the agent wallet equal to the full Safe collateral balance captured when the copy trade was detected. No other transfers are allowed. diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index 2e0fb83e..a9470642 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -75,7 +75,8 @@ async function runValidateToolCallTests() { activeTradeSide: 'BUY', activeTradePrice: 0.55, activeTokenId: '123', - reimbursementAmountWei: '990000', + copyTradeAmountWei: '990000', + reimbursementAmountWei: '1000000', orderSubmitted: false, tokenDeposited: false, reimbursementProposed: false, @@ -118,7 +119,8 @@ async function runValidateToolCallTests() { activeTradeSide: 'BUY', activeTradePrice: 0.55, activeTokenId: '123', - reimbursementAmountWei: '990000', + copyTradeAmountWei: '990000', + reimbursementAmountWei: '1000000', orderSubmitted: true, tokenDeposited: false, reimbursementProposed: false, @@ -155,7 +157,8 @@ async function runValidateToolCallTests() { activeTradeSide: 'BUY', activeTradePrice: 0.55, activeTokenId: '123', - reimbursementAmountWei: '990000', + copyTradeAmountWei: '990000', + reimbursementAmountWei: '1000000', orderSubmitted: true, tokenDeposited: true, reimbursementProposed: false, @@ -173,7 +176,7 @@ async function runValidateToolCallTests() { assert.equal(reimbursementValidated.length, 1); assert.equal(reimbursementValidated[0].parsedArguments.actions.length, 1); assert.equal(reimbursementValidated[0].parsedArguments.actions[0].kind, 'erc20_transfer'); - assert.equal(reimbursementValidated[0].parsedArguments.actions[0].amountWei, '990000'); + assert.equal(reimbursementValidated[0].parsedArguments.actions[0].amountWei, '1000000'); } async function runProposalHashGatingTest() { @@ -228,6 +231,8 @@ async function runProposalHashGatingTest() { assert.equal(state.activeSourceTradeId, 'trade-1'); assert.equal(state.activeTradeSide, 'BUY'); assert.equal(state.activeTradePrice, 0.5); + assert.equal(state.copyTradeAmountWei, '990000'); + assert.equal(state.reimbursementAmountWei, '1000000'); onToolOutput({ name: 'post_bond_and_propose', @@ -343,6 +348,8 @@ async function runProposalHashRecoveryFromSignalTest() { assert.equal(state.reimbursementSubmissionPending, true); assert.equal(state.reimbursementSubmissionTxHash, TEST_TX_HASH); assert.equal(typeof state.reimbursementSubmissionMs, 'number'); + assert.equal(state.copyTradeAmountWei, '990000'); + assert.equal(state.reimbursementAmountWei, '1000000'); const reimbursementAmountWei = state.reimbursementAmountWei; const proposalSignal = { From 039eb1dcfd1742d75bdec3a93246f695b56a9a12 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 13:08:34 -0800 Subject: [PATCH 151/174] implement orderId tracking and fill confirmation Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 306 +++++++++++++++++- .../copy-trading/test-copy-trading-agent.mjs | 181 +++++++++++ 2 files changed, 486 insertions(+), 1 deletion(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 190af392..6a5c59ee 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -1,3 +1,5 @@ +import crypto from 'node:crypto'; + const erc20BalanceOfAbi = [ { type: 'function', @@ -28,6 +30,18 @@ const PRICE_SCALE = 1_000_000n; const DEFAULT_COLLATERAL_TOKEN = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; const ERC20_TRANSFER_SELECTOR = '0xa9059cbb'; const REIMBURSEMENT_SUBMISSION_TIMEOUT_MS = 60_000; +const DEFAULT_CLOB_HOST = 'https://clob.polymarket.com'; +const DEFAULT_CLOB_REQUEST_TIMEOUT_MS = 15_000; +const CLOB_SUCCESS_TERMINAL_STATUS = 'CONFIRMED'; +const CLOB_FAILURE_TERMINAL_STATUS = 'FAILED'; +const CLOB_ORDER_FAILURE_STATUSES = new Set([ + 'FAILED', + 'REJECTED', + 'CANCELED', + 'CANCELLED', + 'EXPIRED', +]); +const CLOB_ORDER_FILLED_STATUSES = new Set(['FILLED', 'MATCHED', 'CONFIRMED']); let copyTradingState = { seenSourceTradeId: null, @@ -38,6 +52,10 @@ let copyTradingState = { activeTokenId: null, copyTradeAmountWei: null, reimbursementAmountWei: null, + copyOrderId: null, + copyOrderStatus: null, + copyOrderFilled: false, + copyOrderSubmittedMs: null, orderSubmitted: false, tokenDeposited: false, reimbursementProposed: false, @@ -94,6 +112,210 @@ function normalizeHash(value) { return trimmed.toLowerCase(); } +function normalizeOrderId(value) { + if (typeof value !== 'string') return null; + const normalized = value.trim(); + return normalized.length > 0 ? normalized : null; +} + +function normalizeClobStatus(value) { + if (typeof value !== 'string') return null; + const normalized = value.trim().toUpperCase(); + return normalized.length > 0 ? normalized : null; +} + +function parseFiniteNumber(value) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return null; + return parsed; +} + +function normalizeClobHost(host) { + return String(host ?? DEFAULT_CLOB_HOST).replace(/\/+$/, ''); +} + +function hasClobCredentials(config) { + return Boolean( + config?.polymarketClobApiKey && + config?.polymarketClobApiSecret && + config?.polymarketClobApiPassphrase + ); +} + +function getClobAuthAddress({ config, accountAddress }) { + return ( + normalizeAddress(config?.polymarketClobAddress) ?? + normalizeAddress(accountAddress) + ); +} + +function buildClobAuthHeaders({ + config, + signingAddress, + timestamp, + method, + path, + bodyText = '', +}) { + const secretBytes = Buffer.from(String(config.polymarketClobApiSecret), 'base64'); + const payload = `${timestamp}${method.toUpperCase()}${path}${bodyText}`; + const signature = crypto + .createHmac('sha256', secretBytes) + .update(payload) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + return { + 'POLY_ADDRESS': signingAddress, + 'POLY_API_KEY': String(config.polymarketClobApiKey), + 'POLY_SIGNATURE': signature, + 'POLY_TIMESTAMP': String(timestamp), + 'POLY_PASSPHRASE': String(config.polymarketClobApiPassphrase), + }; +} + +async function clobGet({ config, signingAddress, path }) { + const host = normalizeClobHost(config?.polymarketClobHost); + const timeoutMs = Number(config?.polymarketClobRequestTimeoutMs ?? DEFAULT_CLOB_REQUEST_TIMEOUT_MS); + const timestamp = Math.floor(Date.now() / 1000); + const headers = buildClobAuthHeaders({ + config, + signingAddress, + timestamp, + method: 'GET', + path, + bodyText: '', + }); + + const response = await fetch(`${host}${path}`, { + method: 'GET', + headers, + signal: AbortSignal.timeout(timeoutMs), + }); + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : null; + } catch (error) { + parsed = { raw: text }; + } + + if (!response.ok) { + throw new Error( + `CLOB GET failed (${path}): ${response.status} ${response.statusText} ${text}` + ); + } + + return parsed; +} + +function extractOrderSummary(payload) { + const order = + payload?.order && typeof payload.order === 'object' + ? payload.order + : payload && typeof payload === 'object' + ? payload + : null; + if (!order) return null; + + return { + id: normalizeOrderId(order.id ?? order.orderId ?? order.order_id), + status: normalizeClobStatus(order.status), + originalSize: parseFiniteNumber(order.original_size ?? order.originalSize), + sizeMatched: parseFiniteNumber(order.size_matched ?? order.sizeMatched), + }; +} + +function isOrderFullyMatched(order) { + if (!order) return false; + if (order.originalSize === null || order.sizeMatched === null) return false; + if (order.originalSize <= 0) return false; + return order.sizeMatched + 1e-12 >= order.originalSize; +} + +function tradeIncludesOrderId(trade, orderId) { + const normalizedOrderId = String(orderId).trim().toLowerCase(); + if (!normalizedOrderId) return false; + + const takerOrderId = normalizeOrderId(trade?.taker_order_id ?? trade?.takerOrderId); + if (takerOrderId && takerOrderId.toLowerCase() === normalizedOrderId) { + return true; + } + + const makerOrders = Array.isArray(trade?.maker_orders) + ? trade.maker_orders + : Array.isArray(trade?.makerOrders) + ? trade.makerOrders + : []; + for (const makerOrder of makerOrders) { + const makerOrderId = normalizeOrderId(makerOrder?.order_id ?? makerOrder?.orderId); + if (makerOrderId && makerOrderId.toLowerCase() === normalizedOrderId) { + return true; + } + } + + return false; +} + +function dedupeTrades(trades) { + const seen = new Set(); + const unique = []; + for (const trade of trades) { + const id = normalizeOrderId(trade?.id); + const key = id ?? JSON.stringify(trade); + if (seen.has(key)) continue; + seen.add(key); + unique.push(trade); + } + return unique; +} + +function extractOrderIdFromSubmission(parsedOutput) { + return normalizeOrderId( + parsedOutput?.result?.order?.id ?? + parsedOutput?.result?.id ?? + parsedOutput?.result?.orderID ?? + parsedOutput?.result?.orderId ?? + parsedOutput?.order?.id ?? + parsedOutput?.id ?? + parsedOutput?.orderID ?? + parsedOutput?.orderId + ); +} + +function extractOrderStatusFromSubmission(parsedOutput) { + return normalizeClobStatus( + parsedOutput?.result?.order?.status ?? + parsedOutput?.result?.status ?? + parsedOutput?.order?.status + ); +} + +async function fetchRelatedClobTrades({ + config, + signingAddress, + orderId, + market, + clobAuthAddress, + submittedMs, +}) { + const afterSeconds = Math.max(0, Math.floor((Number(submittedMs ?? Date.now()) - 60_000) / 1000)); + const all = []; + for (const role of ['maker', 'taker']) { + const params = new URLSearchParams(); + params.set(role, clobAuthAddress); + params.set('market', market); + params.set('after', String(afterSeconds)); + const path = `/data/trades?${params.toString()}`; + const payload = await clobGet({ config, signingAddress, path }); + if (Array.isArray(payload)) { + all.push(...payload); + } + } + return dedupeTrades(all).filter((trade) => tradeIncludesOrderId(trade, orderId)); +} + function decodeErc20TransferCallData(data) { if (typeof data !== 'string') return null; const normalized = data.toLowerCase(); @@ -329,6 +551,10 @@ function activateTradeCandidate({ copyTradingState.activeTokenId = tokenId; copyTradingState.copyTradeAmountWei = copyTradeAmountWei; copyTradingState.reimbursementAmountWei = reimbursementAmountWei; + copyTradingState.copyOrderId = null; + copyTradingState.copyOrderStatus = null; + copyTradingState.copyOrderFilled = false; + copyTradingState.copyOrderSubmittedMs = null; copyTradingState.orderSubmitted = false; copyTradingState.tokenDeposited = false; copyTradingState.reimbursementProposed = false; @@ -350,6 +576,10 @@ function clearActiveTrade({ markSeen = false } = {}) { copyTradingState.activeTokenId = null; copyTradingState.copyTradeAmountWei = null; copyTradingState.reimbursementAmountWei = null; + copyTradingState.copyOrderId = null; + copyTradingState.copyOrderStatus = null; + copyTradingState.copyOrderFilled = false; + copyTradingState.copyOrderSubmittedMs = null; copyTradingState.orderSubmitted = false; copyTradingState.tokenDeposited = false; copyTradingState.reimbursementProposed = false; @@ -378,7 +608,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'You are a copy-trading commitment agent.', 'Copy only BUY trades from the configured source user and configured market.', 'Trade size must be exactly 99% of Safe collateral at detection time. Keep 1% in the Safe as fee.', - 'Flow must stay simple: place CLOB order from your own wallet, wait for YES/NO tokens, deposit tokens to Safe, then propose reimbursement transfer to agentAddress.', + 'Flow must stay simple: place CLOB order from your own wallet, wait for CLOB fill confirmation and YES/NO token receipt, deposit tokens to Safe, then propose reimbursement transfer to agentAddress.', 'Never trade more than 99% of Safe collateral. Reimburse exactly the stored reimbursement amount (full Safe collateral at detection).', 'Use polymarket_clob_build_sign_and_place_order for order placement, make_erc1155_deposit for YES/NO deposit, and build_og_transactions for reimbursement transfer.', 'If preconditions are not met, return ignore.', @@ -452,6 +682,68 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe }); } + let orderFillCheckError; + if ( + copyTradingState.activeSourceTradeId && + copyTradingState.orderSubmitted && + !copyTradingState.tokenDeposited && + !copyTradingState.copyOrderFilled && + copyTradingState.copyOrderId + ) { + const clobAuthAddress = getClobAuthAddress({ + config, + accountAddress: account.address, + }); + if (hasClobCredentials(config) && clobAuthAddress) { + try { + const signingAddress = clobAuthAddress; + const orderPath = `/data/order/${encodeURIComponent(copyTradingState.copyOrderId)}`; + const orderPayload = await clobGet({ + config, + signingAddress, + path: orderPath, + }); + const orderSummary = extractOrderSummary(orderPayload); + if (orderSummary?.status) { + copyTradingState.copyOrderStatus = orderSummary.status; + } + + const relatedTrades = await fetchRelatedClobTrades({ + config, + signingAddress, + orderId: copyTradingState.copyOrderId, + market: policy.market, + clobAuthAddress, + submittedMs: copyTradingState.copyOrderSubmittedMs, + }); + const relatedStatuses = relatedTrades + .map((trade) => normalizeClobStatus(trade?.status)) + .filter(Boolean); + const anyFailedTrade = relatedStatuses.some( + (status) => status === CLOB_FAILURE_TERMINAL_STATUS + ); + const allConfirmedTrades = + relatedStatuses.length > 0 && + relatedStatuses.every((status) => status === CLOB_SUCCESS_TERMINAL_STATUS); + const orderFilled = + isOrderFullyMatched(orderSummary) || + CLOB_ORDER_FILLED_STATUSES.has(orderSummary?.status ?? ''); + const orderFailed = CLOB_ORDER_FAILURE_STATUSES.has(orderSummary?.status ?? ''); + + if (orderFailed || anyFailedTrade) { + copyTradingState.orderSubmitted = false; + copyTradingState.copyOrderFilled = false; + copyTradingState.copyOrderId = null; + copyTradingState.copyOrderSubmittedMs = null; + } else if (allConfirmedTrades && orderFilled) { + copyTradingState.copyOrderFilled = true; + } + } catch (error) { + orderFillCheckError = error?.message ?? String(error); + } + } + } + if ( copyTradingState.reimbursementSubmissionPending && !copyTradingState.reimbursementProposalHash @@ -524,6 +816,7 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe copyTradingState.reimbursementSubmissionPending ), tradeFetchError, + orderFillCheckError, }); return outSignals; @@ -618,6 +911,9 @@ async function validateToolCalls({ if (!state.orderSubmitted) { throw new Error('Cannot deposit YES/NO tokens before copy order submission.'); } + if (state.copyOrderId && !state.copyOrderFilled) { + throw new Error('Copy order has not been filled yet; wait before depositing tokens.'); + } if (state.tokenDeposited) { throw new Error('YES/NO tokens already deposited for active trade.'); } @@ -684,6 +980,10 @@ function onToolOutput({ name, parsedOutput }) { if (name === 'polymarket_clob_build_sign_and_place_order' && parsedOutput.status === 'submitted') { copyTradingState.orderSubmitted = true; + copyTradingState.copyOrderId = extractOrderIdFromSubmission(parsedOutput); + copyTradingState.copyOrderStatus = extractOrderStatusFromSubmission(parsedOutput); + copyTradingState.copyOrderFilled = false; + copyTradingState.copyOrderSubmittedMs = Date.now(); return; } @@ -776,6 +1076,10 @@ function resetCopyTradingState() { activeTokenId: null, copyTradeAmountWei: null, reimbursementAmountWei: null, + copyOrderId: null, + copyOrderStatus: null, + copyOrderFilled: false, + copyOrderSubmittedMs: null, orderSubmitted: false, tokenDeposited: false, reimbursementProposed: false, diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index a9470642..0f13d76e 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -121,6 +121,8 @@ async function runValidateToolCallTests() { activeTokenId: '123', copyTradeAmountWei: '990000', reimbursementAmountWei: '1000000', + copyOrderId: 'order-1', + copyOrderFilled: true, orderSubmitted: true, tokenDeposited: false, reimbursementProposed: false, @@ -140,6 +142,46 @@ async function runValidateToolCallTests() { assert.equal(depositValidated[0].parsedArguments.tokenId, '123'); assert.equal(depositValidated[0].parsedArguments.amount, '5'); + await assert.rejects( + () => + validateToolCalls({ + toolCalls: [ + { + callId: 'deposit-not-filled', + name: 'make_erc1155_deposit', + arguments: {}, + }, + ], + signals: [ + { + kind: 'copyTradingState', + policy, + state: { + activeSourceTradeId: 'trade-1', + activeTradeSide: 'BUY', + activeTradePrice: 0.55, + activeTokenId: '123', + copyTradeAmountWei: '990000', + reimbursementAmountWei: '1000000', + copyOrderId: 'order-1', + copyOrderFilled: false, + orderSubmitted: true, + tokenDeposited: false, + reimbursementProposed: false, + }, + balances: { + activeTokenBalance: '5', + }, + pendingProposal: false, + }, + ], + config: {}, + agentAddress: '0x1111111111111111111111111111111111111111', + onchainPendingProposal: false, + }), + /not been filled yet/ + ); + const reimbursementValidated = await validateToolCalls({ toolCalls: [ { @@ -524,6 +566,144 @@ async function runRevertedSubmissionClearsPendingTest() { } } +async function runOrderFillConfirmationGatesDepositTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + const apiSecret = Buffer.from('test-secret').toString('base64'); + globalThis.fetch = async (url) => { + const asText = String(url); + if (asText.includes('data-api.polymarket.com/activity')) { + return { + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + async text() { + return JSON.stringify([ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]); + }, + }; + } + if (asText.includes('/data/order/order-1')) { + return { + ok: true, + async text() { + return JSON.stringify({ + order: { + id: 'order-1', + status: 'filled', + original_size: '100', + size_matched: '100', + }, + }); + }, + }; + } + if (asText.includes('/data/trades?')) { + return { + ok: true, + async text() { + return JSON.stringify([ + { + id: 'trade-confirmed-1', + status: 'CONFIRMED', + taker_order_id: 'order-1', + }, + ]); + }, + }; + } + throw new Error(`Unexpected fetch URL in test: ${asText}`); + }; + + const config = { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + polymarketClobHost: 'https://clob.polymarket.com', + polymarketClobApiKey: 'api-key', + polymarketClobApiSecret: apiSecret, + polymarketClobApiPassphrase: 'pass', + }; + const publicClient = { + async readContract({ args }) { + if (args.length === 1) return 1_000_000n; + return 5n; + }, + }; + + await enrichSignals([], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + onToolOutput({ + name: 'polymarket_clob_build_sign_and_place_order', + parsedOutput: { + status: 'submitted', + result: { + id: 'order-1', + status: 'live', + }, + }, + }); + + let state = getCopyTradingState(); + assert.equal(state.copyOrderId, 'order-1'); + assert.equal(state.copyOrderFilled, false); + + await enrichSignals([], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + state = getCopyTradingState(); + assert.equal(state.copyOrderId, 'order-1'); + assert.equal(state.copyOrderFilled, true); + assert.equal(state.copyOrderStatus, 'FILLED'); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function runSubmissionWithoutHashesDoesNotWedgeTest() { resetCopyTradingState(); const envKeys = [ @@ -715,6 +895,7 @@ async function run() { await runProposalHashGatingTest(); await runProposalHashRecoveryFromSignalTest(); await runRevertedSubmissionClearsPendingTest(); + await runOrderFillConfirmationGatesDepositTest(); await runSubmissionWithoutHashesDoesNotWedgeTest(); await runFetchLatestBuyTradeTest(); console.log('[test] copy-trading agent OK'); From a094b7d8be672b02b18e776108b08ebdde812e94 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 13:15:28 -0800 Subject: [PATCH 152/174] remove unused function in copy trading agent and use shared helper functions instead of reimplementing Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 119 +++++---------------- agent/src/lib/polymarket.js | 41 +++++++ 2 files changed, 65 insertions(+), 95 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 6a5c59ee..2df5f7cd 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -1,4 +1,4 @@ -import crypto from 'node:crypto'; +import { getClobOrder, getClobTrades } from '../../../agent/src/lib/polymarket.js'; const erc20BalanceOfAbi = [ { @@ -30,8 +30,6 @@ const PRICE_SCALE = 1_000_000n; const DEFAULT_COLLATERAL_TOKEN = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; const ERC20_TRANSFER_SELECTOR = '0xa9059cbb'; const REIMBURSEMENT_SUBMISSION_TIMEOUT_MS = 60_000; -const DEFAULT_CLOB_HOST = 'https://clob.polymarket.com'; -const DEFAULT_CLOB_REQUEST_TIMEOUT_MS = 15_000; const CLOB_SUCCESS_TERMINAL_STATUS = 'CONFIRMED'; const CLOB_FAILURE_TERMINAL_STATUS = 'FAILED'; const CLOB_ORDER_FAILURE_STATUSES = new Set([ @@ -130,10 +128,6 @@ function parseFiniteNumber(value) { return parsed; } -function normalizeClobHost(host) { - return String(host ?? DEFAULT_CLOB_HOST).replace(/\/+$/, ''); -} - function hasClobCredentials(config) { return Boolean( config?.polymarketClobApiKey && @@ -149,67 +143,6 @@ function getClobAuthAddress({ config, accountAddress }) { ); } -function buildClobAuthHeaders({ - config, - signingAddress, - timestamp, - method, - path, - bodyText = '', -}) { - const secretBytes = Buffer.from(String(config.polymarketClobApiSecret), 'base64'); - const payload = `${timestamp}${method.toUpperCase()}${path}${bodyText}`; - const signature = crypto - .createHmac('sha256', secretBytes) - .update(payload) - .digest('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_'); - - return { - 'POLY_ADDRESS': signingAddress, - 'POLY_API_KEY': String(config.polymarketClobApiKey), - 'POLY_SIGNATURE': signature, - 'POLY_TIMESTAMP': String(timestamp), - 'POLY_PASSPHRASE': String(config.polymarketClobApiPassphrase), - }; -} - -async function clobGet({ config, signingAddress, path }) { - const host = normalizeClobHost(config?.polymarketClobHost); - const timeoutMs = Number(config?.polymarketClobRequestTimeoutMs ?? DEFAULT_CLOB_REQUEST_TIMEOUT_MS); - const timestamp = Math.floor(Date.now() / 1000); - const headers = buildClobAuthHeaders({ - config, - signingAddress, - timestamp, - method: 'GET', - path, - bodyText: '', - }); - - const response = await fetch(`${host}${path}`, { - method: 'GET', - headers, - signal: AbortSignal.timeout(timeoutMs), - }); - const text = await response.text(); - let parsed; - try { - parsed = text ? JSON.parse(text) : null; - } catch (error) { - parsed = { raw: text }; - } - - if (!response.ok) { - throw new Error( - `CLOB GET failed (${path}): ${response.status} ${response.statusText} ${text}` - ); - } - - return parsed; -} - function extractOrderSummary(payload) { const order = payload?.order && typeof payload.order === 'object' @@ -302,17 +235,28 @@ async function fetchRelatedClobTrades({ }) { const afterSeconds = Math.max(0, Math.floor((Number(submittedMs ?? Date.now()) - 60_000) / 1000)); const all = []; - for (const role of ['maker', 'taker']) { - const params = new URLSearchParams(); - params.set(role, clobAuthAddress); - params.set('market', market); - params.set('after', String(afterSeconds)); - const path = `/data/trades?${params.toString()}`; - const payload = await clobGet({ config, signingAddress, path }); - if (Array.isArray(payload)) { - all.push(...payload); - } + const makerTrades = await getClobTrades({ + config, + signingAddress, + maker: clobAuthAddress, + market, + after: afterSeconds, + }); + if (Array.isArray(makerTrades)) { + all.push(...makerTrades); + } + + const takerTrades = await getClobTrades({ + config, + signingAddress, + taker: clobAuthAddress, + market, + after: afterSeconds, + }); + if (Array.isArray(takerTrades)) { + all.push(...takerTrades); } + return dedupeTrades(all).filter((trade) => tradeIncludesOrderId(trade, orderId)); } @@ -697,11 +641,10 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe if (hasClobCredentials(config) && clobAuthAddress) { try { const signingAddress = clobAuthAddress; - const orderPath = `/data/order/${encodeURIComponent(copyTradingState.copyOrderId)}`; - const orderPayload = await clobGet({ + const orderPayload = await getClobOrder({ config, signingAddress, - path: orderPath, + orderId: copyTradingState.copyOrderId, }); const orderSummary = extractOrderSummary(orderPayload); if (orderSummary?.status) { @@ -822,20 +765,6 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe return outSignals; } -function parseCallArgs(call) { - if (call?.parsedArguments && typeof call.parsedArguments === 'object') { - return call.parsedArguments; - } - if (typeof call?.arguments === 'string') { - try { - return JSON.parse(call.arguments); - } catch (error) { - return null; - } - } - return null; -} - function findCopySignal(signals) { return signals.find((signal) => signal?.kind === 'copyTradingState'); } diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index 8f521345..bca9ccd2 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -398,6 +398,45 @@ async function placeClobOrder({ }); } +async function getClobOrder({ config, signingAddress, orderId }) { + if (!orderId || typeof orderId !== 'string' || orderId.trim().length === 0) { + throw new Error('orderId is required.'); + } + + return clobRequest({ + config, + signingAddress, + method: 'GET', + path: `/data/order/${encodeURIComponent(orderId.trim())}`, + }); +} + +async function getClobTrades({ + config, + signingAddress, + maker, + taker, + market, + after, +}) { + if (!maker && !taker) { + throw new Error('getClobTrades requires maker or taker.'); + } + + const params = new URLSearchParams(); + if (maker) params.set('maker', String(maker)); + if (taker) params.set('taker', String(taker)); + if (market) params.set('market', String(market)); + if (after !== undefined && after !== null) params.set('after', String(after)); + + return clobRequest({ + config, + signingAddress, + method: 'GET', + path: `/data/trades?${params.toString()}`, + }); +} + async function cancelClobOrders({ config, signingAddress, @@ -447,6 +486,8 @@ async function cancelClobOrders({ export { buildClobOrderFromRaw, cancelClobOrders, + getClobOrder, + getClobTrades, placeClobOrder, resolveClobExchangeAddress, signClobOrder, From 6c6f95d4f960a46aa1bf59eceefa5e64bc0be0a3 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 13:20:16 -0800 Subject: [PATCH 153/174] move some polymarket consts into the shared library Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 23 ++++++++++------------ agent/src/lib/polymarket.js | 18 +++++++++++++++++ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 2df5f7cd..95dba298 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -1,4 +1,13 @@ -import { getClobOrder, getClobTrades } from '../../../agent/src/lib/polymarket.js'; +import { + CLOB_FAILURE_TERMINAL_STATUS, + CLOB_ORDER_FAILURE_STATUSES, + CLOB_ORDER_FILLED_STATUSES, + CLOB_SUCCESS_TERMINAL_STATUS, + DATA_API_HOST, + DEFAULT_COLLATERAL_TOKEN, + getClobOrder, + getClobTrades, +} from '../../../agent/src/lib/polymarket.js'; const erc20BalanceOfAbi = [ { @@ -22,24 +31,12 @@ const erc1155BalanceOfAbi = [ }, ]; -const DATA_API_HOST = 'https://data-api.polymarket.com'; const COPY_BPS = 9900n; const FEE_BPS = 100n; const BPS_DENOMINATOR = 10_000n; const PRICE_SCALE = 1_000_000n; -const DEFAULT_COLLATERAL_TOKEN = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; const ERC20_TRANSFER_SELECTOR = '0xa9059cbb'; const REIMBURSEMENT_SUBMISSION_TIMEOUT_MS = 60_000; -const CLOB_SUCCESS_TERMINAL_STATUS = 'CONFIRMED'; -const CLOB_FAILURE_TERMINAL_STATUS = 'FAILED'; -const CLOB_ORDER_FAILURE_STATUSES = new Set([ - 'FAILED', - 'REJECTED', - 'CANCELED', - 'CANCELLED', - 'EXPIRED', -]); -const CLOB_ORDER_FILLED_STATUSES = new Set(['FILLED', 'MATCHED', 'CONFIRMED']); let copyTradingState = { seenSourceTradeId: null, diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index bca9ccd2..16e57ccd 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -5,6 +5,18 @@ const DEFAULT_CLOB_HOST = 'https://clob.polymarket.com'; const DEFAULT_CLOB_REQUEST_TIMEOUT_MS = 15_000; const DEFAULT_CLOB_MAX_RETRIES = 1; const DEFAULT_CLOB_RETRY_DELAY_MS = 250; +const DATA_API_HOST = 'https://data-api.polymarket.com'; +const DEFAULT_COLLATERAL_TOKEN = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; +const CLOB_SUCCESS_TERMINAL_STATUS = 'CONFIRMED'; +const CLOB_FAILURE_TERMINAL_STATUS = 'FAILED'; +const CLOB_ORDER_FAILURE_STATUSES = new Set([ + 'FAILED', + 'REJECTED', + 'CANCELED', + 'CANCELLED', + 'EXPIRED', +]); +const CLOB_ORDER_FILLED_STATUSES = new Set(['FILLED', 'MATCHED', 'CONFIRMED']); const CLOB_EIP712_DOMAIN_NAME = 'Polymarket CTF Exchange'; const CLOB_EIP712_DOMAIN_VERSION = '1'; const DEFAULT_EIP712_ORDER_SIDE = 0; @@ -484,6 +496,12 @@ async function cancelClobOrders({ } export { + CLOB_FAILURE_TERMINAL_STATUS, + CLOB_ORDER_FAILURE_STATUSES, + CLOB_ORDER_FILLED_STATUSES, + CLOB_SUCCESS_TERMINAL_STATUS, + DATA_API_HOST, + DEFAULT_COLLATERAL_TOKEN, buildClobOrderFromRaw, cancelClobOrders, getClobOrder, From baafdd2234f7eb002521c0096a77624363fc5ecb Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 13:29:07 -0800 Subject: [PATCH 154/174] refactor to use viem abis Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 51 +++++++--------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 95dba298..a79b0869 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -8,34 +8,12 @@ import { getClobOrder, getClobTrades, } from '../../../agent/src/lib/polymarket.js'; - -const erc20BalanceOfAbi = [ - { - type: 'function', - name: 'balanceOf', - stateMutability: 'view', - inputs: [{ name: 'account', type: 'address' }], - outputs: [{ name: '', type: 'uint256' }], - }, -]; -const erc1155BalanceOfAbi = [ - { - type: 'function', - name: 'balanceOf', - stateMutability: 'view', - inputs: [ - { name: 'account', type: 'address' }, - { name: 'id', type: 'uint256' }, - ], - outputs: [{ name: '', type: 'uint256' }], - }, -]; +import { decodeFunctionData, erc20Abi, erc1155Abi } from 'viem'; const COPY_BPS = 9900n; const FEE_BPS = 100n; const BPS_DENOMINATOR = 10_000n; const PRICE_SCALE = 1_000_000n; -const ERC20_TRANSFER_SELECTOR = '0xa9059cbb'; const REIMBURSEMENT_SUBMISSION_TIMEOUT_MS = 60_000; let copyTradingState = { @@ -259,20 +237,21 @@ async function fetchRelatedClobTrades({ function decodeErc20TransferCallData(data) { if (typeof data !== 'string') return null; - const normalized = data.toLowerCase(); - if (!normalized.startsWith(ERC20_TRANSFER_SELECTOR)) return null; - if (normalized.length !== 138) return null; - const toWord = normalized.slice(10, 74); - const amountWord = normalized.slice(74, 138); - const to = normalizeAddress(`0x${toWord.slice(24)}`); - if (!to) return null; - let amount; + try { - amount = BigInt(`0x${amountWord}`); + const decoded = decodeFunctionData({ + abi: erc20Abi, + data, + }); + if (decoded.functionName !== 'transfer') return null; + const to = normalizeAddress(decoded.args?.[0]); + if (!to) return null; + const amount = BigInt(decoded.args?.[1] ?? 0n); + if (amount < 0n) return null; + return { to, amount }; } catch (error) { return null; } - return { to, amount }; } function findMatchingReimbursementProposalHash({ @@ -588,19 +567,19 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe const [safeCollateralWei, yesBalance, noBalance] = await Promise.all([ publicClient.readContract({ address: policy.collateralToken, - abi: erc20BalanceOfAbi, + abi: erc20Abi, functionName: 'balanceOf', args: [config.commitmentSafe], }), publicClient.readContract({ address: policy.ctfContract, - abi: erc1155BalanceOfAbi, + abi: erc1155Abi, functionName: 'balanceOf', args: [account.address, BigInt(policy.yesTokenId)], }), publicClient.readContract({ address: policy.ctfContract, - abi: erc1155BalanceOfAbi, + abi: erc1155Abi, functionName: 'balanceOf', args: [account.address, BigInt(policy.noTokenId)], }), From 54cea820a46c6701ab9cd81b7d7a1e0a7d1126a3 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 13:35:35 -0800 Subject: [PATCH 155/174] dedupe normalizer functions Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 19 +++------- agent-library/agents/limit-order-sma/agent.js | 9 +---- agent-library/agents/limit-order/agent.js | 9 +---- agent-library/agents/price-race-swap/agent.js | 37 +++---------------- agent/src/lib/tx.js | 11 +----- agent/src/lib/utils.js | 26 +++++++++++++ 6 files changed, 44 insertions(+), 67 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index a79b0869..26be1579 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -9,6 +9,10 @@ import { getClobTrades, } from '../../../agent/src/lib/polymarket.js'; import { decodeFunctionData, erc20Abi, erc1155Abi } from 'viem'; +import { + normalizeAddressOrNull, + normalizeHashOrNull, +} from '../../../agent/src/lib/utils.js'; const COPY_BPS = 9900n; const FEE_BPS = 100n; @@ -37,13 +41,7 @@ let copyTradingState = { reimbursementSubmissionTxHash: null, reimbursementSubmissionMs: null, }; - -function normalizeAddress(value) { - if (typeof value !== 'string') return null; - const trimmed = value.trim(); - if (!/^0x[0-9a-fA-F]{40}$/.test(trimmed)) return null; - return trimmed.toLowerCase(); -} +const normalizeAddress = normalizeAddressOrNull; function normalizeTokenId(value) { if (value === null || value === undefined || value === '') return null; @@ -78,12 +76,7 @@ function normalizeTradePrice(value) { return parsed; } -function normalizeHash(value) { - if (typeof value !== 'string') return null; - const trimmed = value.trim(); - if (!/^0x[0-9a-fA-F]{64}$/.test(trimmed)) return null; - return trimmed.toLowerCase(); -} +const normalizeHash = normalizeHashOrNull; function normalizeOrderId(value) { if (typeof value !== 'string') return null; diff --git a/agent-library/agents/limit-order-sma/agent.js b/agent-library/agents/limit-order-sma/agent.js index 4d046c39..558b525d 100644 --- a/agent-library/agents/limit-order-sma/agent.js +++ b/agent-library/agents/limit-order-sma/agent.js @@ -1,6 +1,7 @@ // SMA Limit Order Agent - Single limit order with 200-day SMA as dynamic limit on Sepolia (WETH/USDC) import { erc20Abi, parseAbiItem } from 'viem'; +import { normalizeAddressOrThrow } from '../../../agent/src/lib/utils.js'; const TOKENS = Object.freeze({ WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', @@ -71,18 +72,12 @@ let limitOrderState = { }; let hydratedFromChain = false; let priceDataCache = { ethPriceUSD: null, smaEth200USD: null, fetchedAt: 0 }; +const normalizeAddress = (value) => normalizeAddressOrThrow(value, { requireHex: false }); const proposalExecutedEvent = parseAbiItem( 'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' ); -function normalizeAddress(value) { - if (typeof value !== 'string' || value.length !== 42 || !value.startsWith('0x')) { - throw new Error(`Invalid address: ${value}`); - } - return value.toLowerCase(); -} - async function fetchEthPriceDataFromCoinGecko() { const now = Date.now(); if ( diff --git a/agent-library/agents/limit-order/agent.js b/agent-library/agents/limit-order/agent.js index 4aa91bdc..5e11b2b2 100644 --- a/agent-library/agents/limit-order/agent.js +++ b/agent-library/agents/limit-order/agent.js @@ -1,6 +1,7 @@ // Limit Order Agent - Single limit order on Sepolia (WETH/USDC) import { erc20Abi, parseAbi, parseAbiItem } from 'viem'; +import { normalizeAddressOrThrow } from '../../../agent/src/lib/utils.js'; const TOKENS = Object.freeze({ WETH: '0x7b79995e5f793a07bc00c21412e50ecae098e7f9', @@ -70,18 +71,12 @@ let limitOrderState = { proposalSubmitMs: null, }; let hydratedFromChain = false; +const normalizeAddress = (value) => normalizeAddressOrThrow(value, { requireHex: false }); const proposalExecutedEvent = parseAbiItem( 'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)' ); -function normalizeAddress(value) { - if (typeof value !== 'string' || value.length !== 42 || !value.startsWith('0x')) { - throw new Error(`Invalid address: ${value}`); - } - return value.toLowerCase(); -} - async function getEthPriceUSD(publicClient, chainlinkFeedAddress) { const result = await publicClient.readContract({ address: chainlinkFeedAddress, diff --git a/agent-library/agents/price-race-swap/agent.js b/agent-library/agents/price-race-swap/agent.js index 2db6fc54..d21cbc46 100644 --- a/agent-library/agents/price-race-swap/agent.js +++ b/agent-library/agents/price-race-swap/agent.js @@ -1,6 +1,10 @@ import { readFile, unlink, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { + normalizeAddressOrThrow, + normalizeHashOrNull, +} from '../../../agent/src/lib/utils.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -87,6 +91,8 @@ const singleFireState = { }; let singleFireStateHydrated = false; let singleFireReconciledOnchain = false; +const normalizeAddress = normalizeAddressOrThrow; +const normalizeHash = normalizeHashOrNull; function getSingleFireStatePath() { const fromEnv = process.env.PRICE_RACE_SWAP_STATE_FILE; @@ -96,13 +102,6 @@ function getSingleFireStatePath() { return path.join(__dirname, '.single-fire-state.json'); } -function normalizeHash(value) { - if (typeof value !== 'string') return null; - const v = value.trim(); - if (!/^0x[0-9a-fA-F]{64}$/.test(v)) return null; - return v.toLowerCase(); -} - function resolveSubmittedProposalHash(parsedOutput) { const txHash = normalizeHash(parsedOutput?.transactionHash); const explicitOgHash = normalizeHash(parsedOutput?.ogProposalHash); @@ -187,30 +186,6 @@ async function reconcileSingleFireFromChain({ publicClient }) { } } -function isHexChar(char) { - const code = char.charCodeAt(0); - return ( - (code >= 48 && code <= 57) || - (code >= 65 && code <= 70) || - (code >= 97 && code <= 102) - ); -} - -function normalizeAddress(value) { - if (typeof value !== 'string') { - throw new Error(`Invalid address: ${value}`); - } - if (value.length !== 42 || !value.startsWith('0x')) { - throw new Error(`Invalid address: ${value}`); - } - for (let i = 2; i < value.length; i += 1) { - if (!isHexChar(value[i])) { - throw new Error(`Invalid address: ${value}`); - } - } - return value.toLowerCase(); -} - function normalizeComparator(value) { const normalized = String(value ?? '').trim().toLowerCase(); if (normalized === 'gte' || normalized === '>=') return 'gte'; diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index e1585a00..5a4e5b8e 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -13,7 +13,7 @@ import { transactionsProposedEvent, } from './og.js'; import { normalizeAssertion } from './og.js'; -import { summarizeViemError } from './utils.js'; +import { normalizeHashOrNull, summarizeViemError } from './utils.js'; const conditionalTokensAbi = parseAbi([ 'function splitPosition(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount)', @@ -27,13 +27,6 @@ const erc1155TransferAbi = parseAbi([ const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; -function normalizeHash(value) { - if (typeof value !== 'string') return null; - const trimmed = value.trim(); - if (!/^0x[0-9a-fA-F]{64}$/.test(trimmed)) return null; - return trimmed.toLowerCase(); -} - function extractProposalHashFromReceipt({ receipt, ogModule }) { if (!receipt?.logs || !Array.isArray(receipt.logs)) return null; let normalizedOgModule; @@ -58,7 +51,7 @@ function extractProposalHashFromReceipt({ receipt, ogModule }) { data: log.data, topics: log.topics, }); - const hash = normalizeHash(decoded?.args?.proposalHash); + const hash = normalizeHashOrNull(decoded?.args?.proposalHash); if (hash) return hash; } catch (error) { // Ignore non-matching logs. diff --git a/agent/src/lib/utils.js b/agent/src/lib/utils.js index 1d491809..21d0f0db 100644 --- a/agent/src/lib/utils.js +++ b/agent/src/lib/utils.js @@ -36,6 +36,29 @@ function summarizeViemError(error) { }; } +function normalizeAddressOrNull(value, { trim = true, requireHex = true } = {}) { + if (typeof value !== 'string') return null; + const candidate = trim ? value.trim() : value; + if (candidate.length !== 42 || !candidate.startsWith('0x')) return null; + if (requireHex && !/^0x[0-9a-fA-F]{40}$/.test(candidate)) return null; + return candidate.toLowerCase(); +} + +function normalizeAddressOrThrow(value, options = {}) { + const normalized = normalizeAddressOrNull(value, { trim: false, ...options }); + if (!normalized) { + throw new Error(`Invalid address: ${value}`); + } + return normalized; +} + +function normalizeHashOrNull(value) { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + if (!/^0x[0-9a-fA-F]{64}$/.test(trimmed)) return null; + return trimmed.toLowerCase(); +} + function parseToolArguments(raw) { if (!raw) return null; if (typeof raw === 'object') return raw; @@ -51,6 +74,9 @@ function parseToolArguments(raw) { export { mustGetEnv, + normalizeAddressOrNull, + normalizeAddressOrThrow, + normalizeHashOrNull, normalizePrivateKey, parseAddressList, parseToolArguments, From 99bca424db8491b5fad8b16a1073f6f8c7778586 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 13:41:49 -0800 Subject: [PATCH 156/174] move a few functions into the shared library from the copy trading agent Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 48 +++------------------- agent/src/lib/polling.js | 7 ++++ agent/src/lib/utils.js | 42 ++++++++++++++++++- 3 files changed, 54 insertions(+), 43 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 26be1579..fbe923d4 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -8,11 +8,15 @@ import { getClobOrder, getClobTrades, } from '../../../agent/src/lib/polymarket.js'; -import { decodeFunctionData, erc20Abi, erc1155Abi } from 'viem'; +import { erc20Abi, erc1155Abi } from 'viem'; import { + decodeErc20TransferCallData, normalizeAddressOrNull, normalizeHashOrNull, + normalizeTokenId, + parseFiniteNumber, } from '../../../agent/src/lib/utils.js'; +import { getAlwaysEmitBalanceSnapshotPollingOptions } from '../../../agent/src/lib/polling.js'; const COPY_BPS = 9900n; const FEE_BPS = 100n; @@ -43,17 +47,6 @@ let copyTradingState = { }; const normalizeAddress = normalizeAddressOrNull; -function normalizeTokenId(value) { - if (value === null || value === undefined || value === '') return null; - try { - const normalized = BigInt(value); - if (normalized < 0n) return null; - return normalized.toString(); - } catch (error) { - return null; - } -} - function normalizeOutcome(value) { if (typeof value !== 'string') return null; const normalized = value.trim().toLowerCase(); @@ -90,12 +83,6 @@ function normalizeClobStatus(value) { return normalized.length > 0 ? normalized : null; } -function parseFiniteNumber(value) { - const parsed = Number(value); - if (!Number.isFinite(parsed)) return null; - return parsed; -} - function hasClobCredentials(config) { return Boolean( config?.polymarketClobApiKey && @@ -228,25 +215,6 @@ async function fetchRelatedClobTrades({ return dedupeTrades(all).filter((trade) => tradeIncludesOrderId(trade, orderId)); } -function decodeErc20TransferCallData(data) { - if (typeof data !== 'string') return null; - - try { - const decoded = decodeFunctionData({ - abi: erc20Abi, - data, - }); - if (decoded.functionName !== 'transfer') return null; - const to = normalizeAddress(decoded.args?.[0]); - if (!to) return null; - const amount = BigInt(decoded.args?.[1] ?? 0n); - if (amount < 0n) return null; - return { to, amount }; - } catch (error) { - return null; - } -} - function findMatchingReimbursementProposalHash({ signals, policy, @@ -502,11 +470,7 @@ function clearActiveTrade({ markSeen = false } = {}) { copyTradingState.reimbursementSubmissionMs = null; } -function getPollingOptions() { - return { - emitBalanceSnapshotsEveryPoll: true, - }; -} +const getPollingOptions = getAlwaysEmitBalanceSnapshotPollingOptions; function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { const mode = proposeEnabled && disputeEnabled diff --git a/agent/src/lib/polling.js b/agent/src/lib/polling.js index e3837ffc..39825f0f 100644 --- a/agent/src/lib/polling.js +++ b/agent/src/lib/polling.js @@ -7,6 +7,12 @@ import { transferEvent, } from './og.js'; +function getAlwaysEmitBalanceSnapshotPollingOptions() { + return { + emitBalanceSnapshotsEveryPoll: true, + }; +} + async function primeBalances({ publicClient, commitmentSafe, watchNativeBalance, blockNumber }) { if (!watchNativeBalance) return undefined; @@ -432,6 +438,7 @@ async function executeReadyProposals({ export { primeBalances, + getAlwaysEmitBalanceSnapshotPollingOptions, pollCommitmentChanges, pollProposalChanges, executeReadyProposals, diff --git a/agent/src/lib/utils.js b/agent/src/lib/utils.js index 21d0f0db..c74c0c94 100644 --- a/agent/src/lib/utils.js +++ b/agent/src/lib/utils.js @@ -1,4 +1,4 @@ -import { getAddress } from 'viem'; +import { decodeFunctionData, erc20Abi, getAddress } from 'viem'; function mustGetEnv(key) { const value = process.env[key]; @@ -59,6 +59,43 @@ function normalizeHashOrNull(value) { return trimmed.toLowerCase(); } +function normalizeTokenId(value) { + if (value === null || value === undefined || value === '') return null; + try { + const normalized = BigInt(value); + if (normalized < 0n) return null; + return normalized.toString(); + } catch (error) { + return null; + } +} + +function parseFiniteNumber(value) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return null; + return parsed; +} + +function decodeErc20TransferCallData(data) { + if (typeof data !== 'string') return null; + + try { + const decoded = decodeFunctionData({ + abi: erc20Abi, + data, + }); + if (decoded.functionName !== 'transfer') return null; + + const to = normalizeAddressOrNull(decoded.args?.[0], { trim: false }); + if (!to) return null; + const amount = BigInt(decoded.args?.[1] ?? 0n); + if (amount < 0n) return null; + return { to, amount }; + } catch (error) { + return null; + } +} + function parseToolArguments(raw) { if (!raw) return null; if (typeof raw === 'object') return raw; @@ -73,11 +110,14 @@ function parseToolArguments(raw) { } export { + decodeErc20TransferCallData, mustGetEnv, normalizeAddressOrNull, normalizeAddressOrThrow, normalizeHashOrNull, + normalizeTokenId, normalizePrivateKey, + parseFiniteNumber, parseAddressList, parseToolArguments, summarizeViemError, From 21950586c67801575ca1b1c69f74ca7b8c1d62f9 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 13:43:50 -0800 Subject: [PATCH 157/174] update copy trading agent entry in the agent library readme Signed-off-by: John Shutt --- agent-library/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent-library/README.md b/agent-library/README.md index a07a4b1c..52b7c694 100644 --- a/agent-library/README.md +++ b/agent-library/README.md @@ -14,4 +14,4 @@ To add a new agent: Example agents: - `agent-library/agents/default/`: generic agent using the commitment text. - `agent-library/agents/timelock-withdraw/`: timelock withdrawal agent that only withdraws to its own address after the timelock. -- `agent-library/agents/copy-trading/`: copy-trading agent that mirrors configured Polymarket BUY trades at 99% Safe sizing, deposits YES/NO tokens, and proposes reimbursement. +- `agent-library/agents/copy-trading/`: copy-trading agent for one configured source trader + market; it reacts to BUY trades only, submits a 99%-of-Safe collateral CLOB order from the agent wallet, waits for fill/token receipt, deposits YES/NO tokens to the Safe, then proposes reimbursement to the agent wallet for the full Safe collateral snapshot captured at trigger time (1% implied agent fee via reduced copy size). From a36ee89646cd61aa410d0e0c68383df5582d50d9 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 13:48:03 -0800 Subject: [PATCH 158/174] do not set orderSubmitted to true without an order ID Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 9 +- .../copy-trading/test-copy-trading-agent.mjs | 134 ++++++++++++++++++ 2 files changed, 139 insertions(+), 4 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index fbe923d4..98613d88 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -773,7 +773,7 @@ async function validateToolCalls({ if (!state.orderSubmitted) { throw new Error('Cannot deposit YES/NO tokens before copy order submission.'); } - if (state.copyOrderId && !state.copyOrderFilled) { + if (!state.copyOrderFilled) { throw new Error('Copy order has not been filled yet; wait before depositing tokens.'); } if (state.tokenDeposited) { @@ -841,11 +841,12 @@ function onToolOutput({ name, parsedOutput }) { } if (name === 'polymarket_clob_build_sign_and_place_order' && parsedOutput.status === 'submitted') { - copyTradingState.orderSubmitted = true; - copyTradingState.copyOrderId = extractOrderIdFromSubmission(parsedOutput); + const submittedOrderId = extractOrderIdFromSubmission(parsedOutput); + copyTradingState.orderSubmitted = Boolean(submittedOrderId); + copyTradingState.copyOrderId = submittedOrderId; copyTradingState.copyOrderStatus = extractOrderStatusFromSubmission(parsedOutput); copyTradingState.copyOrderFilled = false; - copyTradingState.copyOrderSubmittedMs = Date.now(); + copyTradingState.copyOrderSubmittedMs = submittedOrderId ? Date.now() : null; return; } diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index 0f13d76e..b9568686 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -704,6 +704,139 @@ async function runOrderFillConfirmationGatesDepositTest() { } } +async function runMissingOrderIdDoesNotAdvanceOrderStateTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + globalThis.fetch = async () => ({ + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + }); + + const config = { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + }; + const publicClient = { + async readContract({ args }) { + if (args.length === 1) return 1_000_000n; + return 5n; + }, + }; + + await enrichSignals([], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + onToolOutput({ + name: 'polymarket_clob_build_sign_and_place_order', + parsedOutput: { status: 'submitted' }, + }); + + const state = getCopyTradingState(); + assert.equal(state.orderSubmitted, false); + assert.equal(state.copyOrderId, null); + assert.equal(state.copyOrderSubmittedMs, null); + assert.equal(state.copyOrderFilled, false); + + await assert.rejects( + () => + validateToolCalls({ + toolCalls: [ + { + callId: 'deposit-no-order-id', + name: 'make_erc1155_deposit', + arguments: {}, + }, + ], + signals: [ + { + kind: 'copyTradingState', + policy: { + ready: true, + ctfContract: config.polymarketConditionalTokens, + collateralToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + }, + state, + balances: { + activeTokenBalance: '5', + }, + pendingProposal: false, + }, + ], + config: {}, + agentAddress: TEST_ACCOUNT, + onchainPendingProposal: false, + }), + /before copy order submission/ + ); + + const orderValidated = await validateToolCalls({ + toolCalls: [ + { + callId: 'order-retry', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: {}, + }, + ], + signals: [ + { + kind: 'copyTradingState', + policy: { + ready: true, + ctfContract: config.polymarketConditionalTokens, + collateralToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + }, + state, + balances: { + activeTokenBalance: '5', + }, + pendingProposal: false, + }, + ], + config: {}, + agentAddress: TEST_ACCOUNT, + onchainPendingProposal: false, + }); + assert.equal(orderValidated.length, 1); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function runSubmissionWithoutHashesDoesNotWedgeTest() { resetCopyTradingState(); const envKeys = [ @@ -896,6 +1029,7 @@ async function run() { await runProposalHashRecoveryFromSignalTest(); await runRevertedSubmissionClearsPendingTest(); await runOrderFillConfirmationGatesDepositTest(); + await runMissingOrderIdDoesNotAdvanceOrderStateTest(); await runSubmissionWithoutHashesDoesNotWedgeTest(); await runFetchLatestBuyTradeTest(); console.log('[test] copy-trading agent OK'); From 4a3187850d4c251f8a31972f62348a8c577cafa8 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 14:02:43 -0800 Subject: [PATCH 159/174] use polymarket relayer infrastructure and safe wallets for gasless transactions Signed-off-by: John Shutt --- agent/.env.example | 1 + agent/README.md | 3 + .../test-polymarket-tool-normalization.mjs | 75 +++++++++++++++++++ agent/src/lib/config.js | 1 + agent/src/lib/polymarket.js | 4 +- agent/src/lib/tools.js | 27 ++++++- 6 files changed, 108 insertions(+), 3 deletions(-) diff --git a/agent/.env.example b/agent/.env.example index 800d0f0b..61e645ae 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -39,6 +39,7 @@ WATCH_NATIVE_BALANCE=true # POLYMARKET_CLOB_ENABLED=false # POLYMARKET_CLOB_HOST=https://clob.polymarket.com # POLYMARKET_CLOB_ADDRESS= +# POLYMARKET_CLOB_SIGNATURE_TYPE=POLY_GNOSIS_SAFE # POLYMARKET_CLOB_API_KEY= # POLYMARKET_CLOB_API_SECRET= # POLYMARKET_CLOB_API_PASSPHRASE= diff --git a/agent/README.md b/agent/README.md index 10848aa4..11b58fdf 100644 --- a/agent/README.md +++ b/agent/README.md @@ -98,6 +98,9 @@ Set these when using Polymarket functionality: - `POLYMARKET_CLOB_ENABLED`: Enable CLOB tools (`true`/`false`, default `false`). - `POLYMARKET_CLOB_HOST`: CLOB API host (default `https://clob.polymarket.com`). - `POLYMARKET_CLOB_ADDRESS`: Optional address used as `POLY_ADDRESS` for CLOB auth (for proxy/funder setups). Defaults to runtime signer address. +- `POLYMARKET_CLOB_SIGNATURE_TYPE`: Optional default order signature type for build/sign flow (`EOA`/`POLY_PROXY`/`POLY_GNOSIS_SAFE` or `0`/`1`/`2`). + - Per Polymarket docs: `0=EOA`, `1=POLY_PROXY`, `2=POLY_GNOSIS_SAFE`. + - When using `POLY_PROXY` or `POLY_GNOSIS_SAFE`, set `POLYMARKET_CLOB_ADDRESS` to the proxy/funder wallet address. - `POLYMARKET_CLOB_API_KEY`, `POLYMARKET_CLOB_API_SECRET`, `POLYMARKET_CLOB_API_PASSPHRASE`: Required for authenticated CLOB calls. - `POLYMARKET_CLOB_REQUEST_TIMEOUT_MS`, `POLYMARKET_CLOB_MAX_RETRIES`, `POLYMARKET_CLOB_RETRY_DELAY_MS`: Optional request tuning. diff --git a/agent/scripts/test-polymarket-tool-normalization.mjs b/agent/scripts/test-polymarket-tool-normalization.mjs index ae841598..071e0984 100644 --- a/agent/scripts/test-polymarket-tool-normalization.mjs +++ b/agent/scripts/test-polymarket-tool-normalization.mjs @@ -313,6 +313,81 @@ async function run() { assert.equal(recordedSignInputs[0].message.signatureType, 0); assert.equal(recordedSignInputs[0].message.tokenId, 123n); + const proxySigWithoutClobAddress = await executeToolCalls({ + toolCalls: [ + { + callId: 'proxy-sig-without-clob-address', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + makerAmount: '1000000', + takerAmount: '450000', + }, + }, + ], + publicClient: { + async getChainId() { + return 137; + }, + }, + walletClient: { + async signTypedData() { + return TEST_SIGNATURE; + }, + }, + account: TEST_ACCOUNT, + config: { + ...config, + polymarketClobSignatureType: 'POLY_GNOSIS_SAFE', + }, + ogContext: null, + }); + const proxySigWithoutClobAddressOut = parseToolOutput(proxySigWithoutClobAddress[0]); + assert.equal(proxySigWithoutClobAddressOut.status, 'error'); + assert.match(proxySigWithoutClobAddressOut.message, /POLYMARKET_CLOB_ADDRESS is required/); + + const recordedSafeSignInputs = []; + const defaultSafeSignatureType = await executeToolCalls({ + toolCalls: [ + { + callId: 'default-safe-signature-type', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: { + side: 'BUY', + tokenId: '123', + orderType: 'GTC', + makerAmount: '1000000', + takerAmount: '450000', + }, + }, + ], + publicClient: { + async getChainId() { + return 137; + }, + }, + walletClient: { + async signTypedData(args) { + recordedSafeSignInputs.push(args); + return TEST_SIGNATURE; + }, + }, + account: TEST_ACCOUNT, + config: { + ...config, + polymarketClobAddress: '0x3333333333333333333333333333333333333333', + polymarketClobSignatureType: 'POLY_GNOSIS_SAFE', + }, + ogContext: null, + }); + const defaultSafeSignatureTypeOut = parseToolOutput(defaultSafeSignatureType[0]); + assert.equal(defaultSafeSignatureTypeOut.status, 'error'); + assert.match(defaultSafeSignatureTypeOut.message, /Missing CLOB credentials/); + assert.equal(recordedSafeSignInputs.length, 1); + assert.equal(recordedSafeSignInputs[0].message.signatureType, 2); + const invalidBuildSignIdentity = await executeToolCalls({ toolCalls: [ { diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index d2ce1744..4b8a0b04 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -71,6 +71,7 @@ function buildConfig() { polymarketClobAddress: process.env.POLYMARKET_CLOB_ADDRESS ? getAddress(process.env.POLYMARKET_CLOB_ADDRESS) : undefined, + polymarketClobSignatureType: process.env.POLYMARKET_CLOB_SIGNATURE_TYPE, polymarketClobApiKey: process.env.POLYMARKET_CLOB_API_KEY, polymarketClobApiSecret: process.env.POLYMARKET_CLOB_API_SECRET, polymarketClobApiPassphrase: process.env.POLYMARKET_CLOB_API_PASSPHRASE, diff --git a/agent/src/lib/polymarket.js b/agent/src/lib/polymarket.js index 16e57ccd..5566e7a7 100644 --- a/agent/src/lib/polymarket.js +++ b/agent/src/lib/polymarket.js @@ -41,8 +41,8 @@ const SIDE_INDEX = Object.freeze({ }); const SIGNATURE_TYPE_INDEX = Object.freeze({ EOA: 0, - POLY_GNOSIS_SAFE: 1, - POLY_PROXY: 2, + POLY_PROXY: 1, + POLY_GNOSIS_SAFE: 2, }); const DEFAULT_CTF_EXCHANGE_BY_CHAIN_ID = Object.freeze({ 137: '0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e', diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 6d485d52..214b77a0 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -858,6 +858,18 @@ async function executeToolCalls({ args.feeRateBps, 'feeRateBps' ); + const configuredSignatureType = + config.polymarketClobSignatureType !== undefined && + config.polymarketClobSignatureType !== null && + String(config.polymarketClobSignatureType).trim() !== '' + ? config.polymarketClobSignatureType + : undefined; + const requestedSignatureType = + args.signatureType !== undefined && + args.signatureType !== null && + String(args.signatureType).trim() !== '' + ? args.signatureType + : configuredSignatureType; const unsignedOrder = buildClobOrderFromRaw({ maker, signer, @@ -866,12 +878,25 @@ async function executeToolCalls({ makerAmount: args.makerAmount, takerAmount: args.takerAmount, side: declaredSideEnum, - signatureType: args.signatureType, + signatureType: requestedSignatureType, salt: normalizedSalt, expiration: normalizedExpiration, nonce: normalizedNonce, feeRateBps: normalizedFeeRateBps, }); + const signatureTypeIndex = Number(unsignedOrder.signatureType); + if (signatureTypeIndex !== 0) { + if (!config.polymarketClobAddress) { + throw new Error( + 'POLYMARKET_CLOB_ADDRESS is required for POLY_PROXY/POLY_GNOSIS_SAFE signature types.' + ); + } + if (maker !== clobAuthAddress) { + throw new Error( + 'maker must match POLYMARKET_CLOB_ADDRESS for POLY_PROXY/POLY_GNOSIS_SAFE signature types.' + ); + } + } const signedOrder = await signClobOrder({ walletClient, account, From b21e923cfc1885be61c2fe8a3081a1c8378ec1f4 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 14:23:48 -0800 Subject: [PATCH 160/174] implement a full relayer-client integration for polymarket Signed-off-by: John Shutt --- agent-library/README.md | 2 +- agent-library/agents/copy-trading/agent.js | 12 +- .../copy-trading/test-copy-trading-agent.mjs | 69 ++ agent/.env.example | 17 + agent/README.md | 12 + agent/scripts/test-erc1155-deposit.mjs | 118 ++++ agent/src/lib/config.js | 40 ++ agent/src/lib/polymarket-relayer.js | 657 ++++++++++++++++++ agent/src/lib/tools.js | 1 + agent/src/lib/tx.js | 42 ++ 10 files changed, 966 insertions(+), 4 deletions(-) create mode 100644 agent/src/lib/polymarket-relayer.js diff --git a/agent-library/README.md b/agent-library/README.md index 52b7c694..96df8e32 100644 --- a/agent-library/README.md +++ b/agent-library/README.md @@ -14,4 +14,4 @@ To add a new agent: Example agents: - `agent-library/agents/default/`: generic agent using the commitment text. - `agent-library/agents/timelock-withdraw/`: timelock withdrawal agent that only withdraws to its own address after the timelock. -- `agent-library/agents/copy-trading/`: copy-trading agent for one configured source trader + market; it reacts to BUY trades only, submits a 99%-of-Safe collateral CLOB order from the agent wallet, waits for fill/token receipt, deposits YES/NO tokens to the Safe, then proposes reimbursement to the agent wallet for the full Safe collateral snapshot captured at trigger time (1% implied agent fee via reduced copy size). +- `agent-library/agents/copy-trading/`: copy-trading agent for one configured source trader + market; it reacts to BUY trades only, submits a 99%-of-Safe collateral CLOB order from the configured trading wallet, waits for fill/token receipt, deposits YES/NO tokens to the Safe (direct onchain or via Polymarket relayer), then proposes reimbursement to the agent wallet for the full Safe collateral snapshot captured at trigger time (1% implied agent fee via reduced copy size). diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 98613d88..3838b06c 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -485,7 +485,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'You are a copy-trading commitment agent.', 'Copy only BUY trades from the configured source user and configured market.', 'Trade size must be exactly 99% of Safe collateral at detection time. Keep 1% in the Safe as fee.', - 'Flow must stay simple: place CLOB order from your own wallet, wait for CLOB fill confirmation and YES/NO token receipt, deposit tokens to Safe, then propose reimbursement transfer to agentAddress.', + 'Flow must stay simple: place CLOB order from your configured trading wallet, wait for CLOB fill confirmation and YES/NO token receipt, deposit tokens to Safe, then propose reimbursement transfer to agentAddress.', 'Never trade more than 99% of Safe collateral. Reimburse exactly the stored reimbursement amount (full Safe collateral at detection).', 'Use polymarket_clob_build_sign_and_place_order for order placement, make_erc1155_deposit for YES/NO deposit, and build_og_transactions for reimbursement transfer.', 'If preconditions are not met, return ignore.', @@ -520,6 +520,11 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe } catch (error) { tradeFetchError = error?.message ?? String(error); } + const tokenHolderAddress = + getClobAuthAddress({ + config, + accountAddress: account.address, + }) ?? normalizeAddress(account.address); const [safeCollateralWei, yesBalance, noBalance] = await Promise.all([ publicClient.readContract({ @@ -532,13 +537,13 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe address: policy.ctfContract, abi: erc1155Abi, functionName: 'balanceOf', - args: [account.address, BigInt(policy.yesTokenId)], + args: [tokenHolderAddress, BigInt(policy.yesTokenId)], }), publicClient.readContract({ address: policy.ctfContract, abi: erc1155Abi, functionName: 'balanceOf', - args: [account.address, BigInt(policy.noTokenId)], + args: [tokenHolderAddress, BigInt(policy.noTokenId)], }), ]); @@ -680,6 +685,7 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe yesBalance: yesBalance.toString(), noBalance: noBalance.toString(), activeTokenBalance: activeTokenBalance.toString(), + tokenHolderAddress, }, metrics: { ...amounts, diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index b9568686..3c17e806 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -16,6 +16,7 @@ const NO_TOKEN_ID = '456'; const TEST_ACCOUNT = '0x1111111111111111111111111111111111111111'; const TEST_SAFE = '0x2222222222222222222222222222222222222222'; const TEST_SOURCE_USER = '0x3333333333333333333333333333333333333333'; +const TEST_CLOB_PROXY = '0x4444444444444444444444444444444444444444'; const TEST_PROPOSAL_HASH = `0x${'a'.repeat(64)}`; const OTHER_PROPOSAL_HASH = `0x${'b'.repeat(64)}`; const TEST_TX_HASH = `0x${'c'.repeat(64)}`; @@ -1020,6 +1021,73 @@ async function runFetchLatestBuyTradeTest() { } } +async function runTokenBalancesUseClobAddressTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + globalThis.fetch = async () => ({ + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + }); + + const erc1155BalanceCallAddresses = []; + await enrichSignals([], { + publicClient: { + async readContract({ args }) { + if (args.length === 1) { + return 1_000_000n; + } + erc1155BalanceCallAddresses.push(String(args[0]).toLowerCase()); + return 1n; + }, + }, + config: { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + polymarketClobAddress: TEST_CLOB_PROXY, + }, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + assert.equal(erc1155BalanceCallAddresses.length, 2); + assert.equal(erc1155BalanceCallAddresses[0], TEST_CLOB_PROXY.toLowerCase()); + assert.equal(erc1155BalanceCallAddresses[1], TEST_CLOB_PROXY.toLowerCase()); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function run() { runPromptTest(); runMathTests(); @@ -1032,6 +1100,7 @@ async function run() { await runMissingOrderIdDoesNotAdvanceOrderStateTest(); await runSubmissionWithoutHashesDoesNotWedgeTest(); await runFetchLatestBuyTradeTest(); + await runTokenBalancesUseClobAddressTest(); console.log('[test] copy-trading agent OK'); } diff --git a/agent/.env.example b/agent/.env.example index 61e645ae..dcdff6f4 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -46,6 +46,23 @@ WATCH_NATIVE_BALANCE=true # POLYMARKET_CLOB_REQUEST_TIMEOUT_MS=15000 # POLYMARKET_CLOB_MAX_RETRIES=1 # POLYMARKET_CLOB_RETRY_DELAY_MS=250 +# Optional Polymarket relayer (gasless SAFE/PROXY transactions) +# POLYMARKET_RELAYER_ENABLED=false +# POLYMARKET_RELAYER_HOST=https://relayer-v2.polymarket.com +# POLYMARKET_RELAYER_TX_TYPE=SAFE +# POLYMARKET_RELAYER_FROM_ADDRESS= +# POLYMARKET_RELAYER_RESOLVE_PROXY_ADDRESS=true +# POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY=false +# POLYMARKET_RELAYER_CHAIN_ID= +# POLYMARKET_RELAYER_REQUEST_TIMEOUT_MS=15000 +# POLYMARKET_RELAYER_POLL_INTERVAL_MS=2000 +# POLYMARKET_RELAYER_POLL_TIMEOUT_MS=120000 +# POLYMARKET_API_KEY= +# POLYMARKET_API_SECRET= +# POLYMARKET_API_PASSPHRASE= +# POLYMARKET_BUILDER_API_KEY= +# POLYMARKET_BUILDER_SECRET= +# POLYMARKET_BUILDER_PASSPHRASE= # Optional Uniswap config overrides (otherwise chain defaults are used) # UNISWAP_V3_FACTORY= # UNISWAP_V3_QUOTER= diff --git a/agent/README.md b/agent/README.md index 11b58fdf..c5b7c895 100644 --- a/agent/README.md +++ b/agent/README.md @@ -103,6 +103,16 @@ Set these when using Polymarket functionality: - When using `POLY_PROXY` or `POLY_GNOSIS_SAFE`, set `POLYMARKET_CLOB_ADDRESS` to the proxy/funder wallet address. - `POLYMARKET_CLOB_API_KEY`, `POLYMARKET_CLOB_API_SECRET`, `POLYMARKET_CLOB_API_PASSPHRASE`: Required for authenticated CLOB calls. - `POLYMARKET_CLOB_REQUEST_TIMEOUT_MS`, `POLYMARKET_CLOB_MAX_RETRIES`, `POLYMARKET_CLOB_RETRY_DELAY_MS`: Optional request tuning. +- `POLYMARKET_RELAYER_ENABLED`: Enable Polymarket relayer submission for ERC1155 deposits (`true`/`false`, default `false`). +- `POLYMARKET_RELAYER_HOST`: Relayer API host (default `https://relayer-v2.polymarket.com`). +- `POLYMARKET_RELAYER_TX_TYPE`: Relayer wallet type (`SAFE` default, or `PROXY`). +- `POLYMARKET_RELAYER_FROM_ADDRESS`: Optional explicit relayer wallet address to send from. +- `POLYMARKET_RELAYER_RESOLVE_PROXY_ADDRESS`: Resolve proxy address via relayer API when from-address is not set (default `true`). +- `POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY`: Optionally create proxy wallet when absent (default `false`). +- `POLYMARKET_RELAYER_CHAIN_ID`, `POLYMARKET_RELAYER_REQUEST_TIMEOUT_MS`, `POLYMARKET_RELAYER_POLL_INTERVAL_MS`, `POLYMARKET_RELAYER_POLL_TIMEOUT_MS`: Optional relayer runtime tuning. +- Builder credentials for relayer auth headers: + - Preferred: `POLYMARKET_BUILDER_API_KEY`, `POLYMARKET_BUILDER_SECRET`, `POLYMARKET_BUILDER_PASSPHRASE`. + - Fallbacks supported: `POLYMARKET_API_*` then `POLYMARKET_CLOB_API_*`. #### Execution Modes @@ -155,6 +165,8 @@ Use `make_erc1155_deposit` after receiving YES/NO position tokens: } ``` +When `POLYMARKET_RELAYER_ENABLED=true`, this tool submits via Polymarket relayer (SAFE/PROXY) instead of direct onchain `writeContract`. For SAFE proxy-wallet mode, set `POLYMARKET_RELAYER_FROM_ADDRESS` (or `POLYMARKET_CLOB_ADDRESS`) to the proxy Safe that holds the YES/NO tokens. + #### CLOB Place/Cancel Tools `polymarket_clob_place_order` submits a pre-signed order: diff --git a/agent/scripts/test-erc1155-deposit.mjs b/agent/scripts/test-erc1155-deposit.mjs index 4abf9eec..7d830bf2 100644 --- a/agent/scripts/test-erc1155-deposit.mjs +++ b/agent/scripts/test-erc1155-deposit.mjs @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { decodeFunctionData, parseAbi } from 'viem'; import { makeErc1155Deposit } from '../src/lib/tx.js'; async function run() { @@ -47,6 +48,123 @@ async function run() { /amount must be > 0/ ); + const relayerFromAddress = '0x3333333333333333333333333333333333333333'; + const relayedTxHash = `0x${'1'.repeat(64)}`; + const onchainTxHash = `0x${'2'.repeat(64)}`; + let relayerSubmitBody; + let relayerSubmitHeaders; + let statusPollCount = 0; + const oldFetch = globalThis.fetch; + try { + globalThis.fetch = async (url, options = {}) => { + const asText = String(url); + if (asText.endsWith('/relayer/transaction')) { + relayerSubmitBody = JSON.parse(options.body); + relayerSubmitHeaders = options.headers; + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ txHash: relayedTxHash }); + }, + }; + } + + if (asText.endsWith(`/relayer/transaction-status/${relayedTxHash}`)) { + statusPollCount += 1; + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + if (statusPollCount === 1) { + return JSON.stringify({ status: 'PENDING', txHash: relayedTxHash }); + } + return JSON.stringify({ + status: 'MINED', + txHash: relayedTxHash, + transactionHash: onchainTxHash, + }); + }, + }; + } + + throw new Error(`Unexpected relayer fetch URL: ${asText}`); + }; + + let relayerSignedMessage; + const relayerWalletClient = { + async signMessage({ message }) { + relayerSignedMessage = message; + return `0x${'a'.repeat(130)}`; + }, + }; + + const relayerPublicClient = { + async getChainId() { + return 137; + }, + async readContract() { + return 12n; + }, + }; + + const relayerConfig = { + commitmentSafe: config.commitmentSafe, + polymarketRelayerEnabled: true, + polymarketRelayerHost: 'https://relayer-v2.polymarket.com', + polymarketRelayerFromAddress: relayerFromAddress, + polymarketRelayerTxType: 'SAFE', + polymarketBuilderApiKey: 'builder-key', + polymarketBuilderSecret: Buffer.from('builder-secret').toString('base64'), + polymarketBuilderPassphrase: 'builder-passphrase', + polymarketRelayerPollIntervalMs: 0, + polymarketRelayerPollTimeoutMs: 1_000, + }; + + const relayerDepositHash = await makeErc1155Deposit({ + publicClient: relayerPublicClient, + walletClient: relayerWalletClient, + account, + config: relayerConfig, + token, + tokenId: '7', + amount: '3', + data: null, + }); + + assert.equal(relayerDepositHash, onchainTxHash); + assert.equal(relayerSubmitBody.type, 'SAFE'); + assert.equal(relayerSubmitBody.from.toLowerCase(), relayerFromAddress.toLowerCase()); + assert.equal(relayerSubmitBody.to.toLowerCase(), token.toLowerCase()); + assert.equal(relayerSubmitBody.value, '0'); + assert.equal(relayerSubmitBody.operation, 0); + assert.equal(relayerSubmitBody.nonce, '12'); + assert.equal(typeof relayerSubmitBody.signature, 'string'); + assert.equal(relayerSubmitBody.metadata.tool, 'make_erc1155_deposit'); + assert.equal(relayerSubmitHeaders.POLY_BUILDER_API_KEY, 'builder-key'); + assert.equal(relayerSubmitHeaders.POLY_BUILDER_PASSPHRASE, 'builder-passphrase'); + assert.equal(typeof relayerSubmitHeaders.POLY_BUILDER_SIGNATURE, 'string'); + assert.equal(typeof relayerSubmitHeaders.POLY_BUILDER_TIMESTAMP, 'string'); + assert.ok(relayerSignedMessage?.raw); + + const decoded = decodeFunctionData({ + abi: parseAbi([ + 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)', + ]), + data: relayerSubmitBody.data, + }); + assert.equal(decoded.functionName, 'safeTransferFrom'); + assert.equal(decoded.args[0].toLowerCase(), relayerFromAddress.toLowerCase()); + assert.equal(decoded.args[1].toLowerCase(), config.commitmentSafe.toLowerCase()); + assert.equal(decoded.args[2], 7n); + assert.equal(decoded.args[3], 3n); + assert.equal(decoded.args[4], '0x'); + } finally { + globalThis.fetch = oldFetch; + } + console.log('[test] makeErc1155Deposit OK'); } diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index 4b8a0b04..3b97e9c3 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -80,6 +80,46 @@ function buildConfig() { ), polymarketClobMaxRetries: Number(process.env.POLYMARKET_CLOB_MAX_RETRIES ?? 1), polymarketClobRetryDelayMs: Number(process.env.POLYMARKET_CLOB_RETRY_DELAY_MS ?? 250), + polymarketRelayerEnabled: + process.env.POLYMARKET_RELAYER_ENABLED === undefined + ? false + : process.env.POLYMARKET_RELAYER_ENABLED.toLowerCase() !== 'false', + polymarketRelayerHost: + process.env.POLYMARKET_RELAYER_HOST ?? 'https://relayer-v2.polymarket.com', + polymarketRelayerTxType: process.env.POLYMARKET_RELAYER_TX_TYPE ?? 'SAFE', + polymarketRelayerFromAddress: process.env.POLYMARKET_RELAYER_FROM_ADDRESS + ? getAddress(process.env.POLYMARKET_RELAYER_FROM_ADDRESS) + : undefined, + polymarketRelayerResolveProxyAddress: + process.env.POLYMARKET_RELAYER_RESOLVE_PROXY_ADDRESS === undefined + ? true + : process.env.POLYMARKET_RELAYER_RESOLVE_PROXY_ADDRESS.toLowerCase() !== + 'false', + polymarketRelayerAutoDeployProxy: + process.env.POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY === undefined + ? false + : process.env.POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY.toLowerCase() === 'true', + polymarketRelayerChainId: process.env.POLYMARKET_RELAYER_CHAIN_ID + ? Number(process.env.POLYMARKET_RELAYER_CHAIN_ID) + : undefined, + polymarketRelayerRequestTimeoutMs: Number( + process.env.POLYMARKET_RELAYER_REQUEST_TIMEOUT_MS ?? 15_000 + ), + polymarketRelayerPollIntervalMs: Number( + process.env.POLYMARKET_RELAYER_POLL_INTERVAL_MS ?? 2_000 + ), + polymarketRelayerPollTimeoutMs: Number( + process.env.POLYMARKET_RELAYER_POLL_TIMEOUT_MS ?? 120_000 + ), + polymarketApiKey: process.env.POLYMARKET_API_KEY, + polymarketApiSecret: process.env.POLYMARKET_API_SECRET, + polymarketApiPassphrase: process.env.POLYMARKET_API_PASSPHRASE, + polymarketBuilderApiKey: + process.env.POLYMARKET_BUILDER_API_KEY ?? process.env.POLYMARKET_API_KEY, + polymarketBuilderSecret: + process.env.POLYMARKET_BUILDER_SECRET ?? process.env.POLYMARKET_API_SECRET, + polymarketBuilderPassphrase: + process.env.POLYMARKET_BUILDER_PASSPHRASE ?? process.env.POLYMARKET_API_PASSPHRASE, uniswapV3Factory: process.env.UNISWAP_V3_FACTORY ? getAddress(process.env.UNISWAP_V3_FACTORY) : undefined, diff --git a/agent/src/lib/polymarket-relayer.js b/agent/src/lib/polymarket-relayer.js new file mode 100644 index 00000000..92cc6663 --- /dev/null +++ b/agent/src/lib/polymarket-relayer.js @@ -0,0 +1,657 @@ +import crypto from 'node:crypto'; +import { encodePacked, getAddress, hashTypedData, isHex, keccak256, parseAbi } from 'viem'; +import { normalizeAddressOrNull, normalizeHashOrNull } from './utils.js'; + +const DEFAULT_RELAYER_HOST = 'https://relayer-v2.polymarket.com'; +const DEFAULT_RELAYER_REQUEST_TIMEOUT_MS = 15_000; +const DEFAULT_RELAYER_POLL_INTERVAL_MS = 2_000; +const DEFAULT_RELAYER_POLL_TIMEOUT_MS = 120_000; +const SAFE_TX_NONCE_ABI = parseAbi(['function nonce() view returns (uint256)']); +const SAFE_EIP712_TYPES = Object.freeze({ + EIP712Domain: [ + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + SafeTx: [ + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + { name: 'operation', type: 'uint8' }, + { name: 'nonce', type: 'uint256' }, + ], +}); +const RELAYER_TX_TYPE = Object.freeze({ + SAFE: 'SAFE', + PROXY: 'PROXY', +}); +const RELAYER_SUCCESS_STATUSES = new Set(['MINED', 'CONFIRMED']); +const RELAYER_FAILURE_STATUSES = new Set(['FAILED', 'REVERTED']); + +function normalizeNonNegativeInteger(value, fallback) { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0) return fallback; + return Math.floor(parsed); +} + +function normalizeRelayerHost(host) { + return (host ?? DEFAULT_RELAYER_HOST).replace(/\/+$/, ''); +} + +function normalizeRelayerTxType(value) { + if (typeof value !== 'string') { + return RELAYER_TX_TYPE.SAFE; + } + const normalized = value.trim().toUpperCase(); + if (normalized === RELAYER_TX_TYPE.PROXY) return RELAYER_TX_TYPE.PROXY; + return RELAYER_TX_TYPE.SAFE; +} + +function normalizeHexData(data) { + if (!data) return '0x'; + if (typeof data !== 'string' || !isHex(data)) { + throw new Error('Relayer transaction data must be a hex string.'); + } + return data; +} + +function getBuilderCredentials(config) { + const apiKey = + config?.polymarketBuilderApiKey ?? + config?.polymarketApiKey ?? + config?.polymarketClobApiKey; + const secret = + config?.polymarketBuilderSecret ?? + config?.polymarketApiSecret ?? + config?.polymarketClobApiSecret; + const passphrase = + config?.polymarketBuilderPassphrase ?? + config?.polymarketApiPassphrase ?? + config?.polymarketClobApiPassphrase; + return { apiKey, secret, passphrase }; +} + +function buildRelayerAuthHeaders({ + config, + method, + path, + bodyText, +}) { + const { apiKey, secret, passphrase } = getBuilderCredentials(config); + if (!apiKey || !secret || !passphrase) { + throw new Error( + 'Missing Polymarket builder credentials. Set POLYMARKET_BUILDER_API_KEY/POLYMARKET_BUILDER_SECRET/POLYMARKET_BUILDER_PASSPHRASE (or POLYMARKET_API_* / POLYMARKET_CLOB_* fallbacks).' + ); + } + + const timestamp = Date.now().toString(); + const payload = `${timestamp}${method.toUpperCase()}${path}${bodyText ?? ''}`; + const secretBytes = Buffer.from(secret, 'base64'); + const signature = crypto + .createHmac('sha256', secretBytes) + .update(payload) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + return { + POLY_BUILDER_API_KEY: apiKey, + POLY_BUILDER_SIGNATURE: signature, + POLY_BUILDER_TIMESTAMP: timestamp, + POLY_BUILDER_PASSPHRASE: passphrase, + }; +} + +async function sleep(ms) { + if (ms <= 0) return; + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function relayerRequest({ + config, + method, + path, + body, +}) { + const host = normalizeRelayerHost(config?.polymarketRelayerHost); + const bodyText = body === undefined ? '' : JSON.stringify(body); + const timeoutMs = normalizeNonNegativeInteger( + config?.polymarketRelayerRequestTimeoutMs, + DEFAULT_RELAYER_REQUEST_TIMEOUT_MS + ); + const headers = { + 'Content-Type': 'application/json', + ...buildRelayerAuthHeaders({ + config, + method, + path, + bodyText, + }), + }; + + const response = await fetch(`${host}${path}`, { + method, + headers, + body: body === undefined ? undefined : bodyText, + signal: AbortSignal.timeout(timeoutMs), + }); + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : null; + } catch (error) { + parsed = { raw: text }; + } + + if (!response.ok) { + throw new Error( + `Relayer request failed (${method} ${path}): ${response.status} ${response.statusText} ${text}` + ); + } + + return parsed; +} + +function collectPayloadObjects(payload) { + const out = []; + const seen = new Set(); + const queue = [payload]; + + while (queue.length > 0 && out.length < 24) { + const current = queue.shift(); + if (!current || typeof current !== 'object') continue; + if (seen.has(current)) continue; + seen.add(current); + out.push(current); + + for (const value of Object.values(current)) { + if (value && typeof value === 'object') { + queue.push(value); + } + } + } + + return out; +} + +function extractStringField(payload, fieldNames) { + const candidates = collectPayloadObjects(payload); + for (const candidate of candidates) { + for (const fieldName of fieldNames) { + const value = candidate?.[fieldName]; + if (typeof value === 'string' && value.trim().length > 0) { + return value.trim(); + } + if (typeof value === 'number' && Number.isFinite(value)) { + return String(value); + } + if (typeof value === 'bigint') { + return value.toString(); + } + } + } + return null; +} + +function extractRelayerTxHash(payload) { + const hashCandidate = extractStringField(payload, [ + 'txHash', + 'relayTxHash', + 'relay_hash', + 'hash', + ]); + const normalized = normalizeHashOrNull(hashCandidate); + return normalized; +} + +function extractTransactionHash(payload) { + const hashCandidate = extractStringField(payload, [ + 'transactionHash', + 'transaction_hash', + 'chainTxHash', + ]); + return normalizeHashOrNull(hashCandidate); +} + +function extractRelayerStatus(payload) { + const statusCandidate = extractStringField(payload, ['status', 'txStatus', 'state']); + if (!statusCandidate) return null; + return statusCandidate.toUpperCase(); +} + +function isPolymarketRelayerEnabled(config) { + return Boolean(config?.polymarketRelayerEnabled); +} + +async function getRelayerProxyAddress({ + config, + signerAddress, +}) { + const response = await relayerRequest({ + config, + method: 'GET', + path: `/relayer/proxy-address/${encodeURIComponent(getAddress(signerAddress))}`, + }); + const candidate = extractStringField(response, [ + 'proxyWallet', + 'proxyAddress', + 'walletAddress', + 'address', + ]); + const normalized = normalizeAddressOrNull(candidate); + return normalized ? getAddress(normalized) : null; +} + +async function getSafeNonce({ + publicClient, + safeAddress, +}) { + return publicClient.readContract({ + address: getAddress(safeAddress), + abi: SAFE_TX_NONCE_ABI, + functionName: 'nonce', + }); +} + +async function getProxyNonce({ + config, + proxyAddress, +}) { + const response = await relayerRequest({ + config, + method: 'GET', + path: `/relayer/proxy-nonce/${encodeURIComponent(getAddress(proxyAddress))}`, + }); + const nonceCandidate = extractStringField(response, ['nonce']); + if (nonceCandidate === null) { + throw new Error('Relayer proxy nonce response did not include nonce.'); + } + return BigInt(nonceCandidate); +} + +async function createProxyWallet({ + config, + signerAddress, + chainId, + txType, +}) { + return relayerRequest({ + config, + method: 'POST', + path: '/relayer/create-proxy-wallet', + body: { + from: getAddress(signerAddress), + chainId: Number(chainId), + relayerTxType: txType, + }, + }); +} + +async function waitForRelayerTransaction({ + config, + txHash, +}) { + const normalizedTxHash = normalizeHashOrNull(txHash); + if (!normalizedTxHash) { + throw new Error(`Invalid relayer txHash: ${txHash}`); + } + const pollIntervalMs = normalizeNonNegativeInteger( + config?.polymarketRelayerPollIntervalMs, + DEFAULT_RELAYER_POLL_INTERVAL_MS + ); + const timeoutMs = normalizeNonNegativeInteger( + config?.polymarketRelayerPollTimeoutMs, + DEFAULT_RELAYER_POLL_TIMEOUT_MS + ); + const deadline = Date.now() + timeoutMs; + let lastPayload = null; + + while (Date.now() <= deadline) { + lastPayload = await relayerRequest({ + config, + method: 'GET', + path: `/relayer/transaction-status/${encodeURIComponent(normalizedTxHash)}`, + }); + const status = extractRelayerStatus(lastPayload); + if (status && RELAYER_SUCCESS_STATUSES.has(status)) { + return lastPayload; + } + if (status && RELAYER_FAILURE_STATUSES.has(status)) { + throw new Error( + `Relayer transaction failed with status=${status} for txHash=${normalizedTxHash}.` + ); + } + await sleep(pollIntervalMs); + } + + throw new Error( + `Timed out waiting for relayer transaction ${normalizedTxHash}. Last payload: ${JSON.stringify( + lastPayload + )}` + ); +} + +async function signSafeTransaction({ + walletClient, + account, + chainId, + fromAddress, + toAddress, + value, + data, + operation, + nonce, +}) { + if (!walletClient || typeof walletClient.signMessage !== 'function') { + throw new Error( + 'Runtime signer does not support signMessage; cannot sign SAFE relayer transaction.' + ); + } + if (!Number.isInteger(operation) || operation < 0 || operation > 1) { + throw new Error('SAFE relayer transaction operation must be 0 or 1.'); + } + + const txHash = hashTypedData({ + domain: { + chainId: Number(chainId), + verifyingContract: getAddress(fromAddress), + }, + primaryType: 'SafeTx', + types: SAFE_EIP712_TYPES, + message: { + to: getAddress(toAddress), + value: BigInt(value), + data: normalizeHexData(data), + operation, + nonce: BigInt(nonce), + }, + }); + const signature = await walletClient.signMessage({ + account, + message: { raw: txHash }, + }); + + return { + txHash, + signature, + signatureParams: { + to: getAddress(toAddress), + value: BigInt(value).toString(), + data: normalizeHexData(data), + operation, + nonce: BigInt(nonce).toString(), + }, + }; +} + +async function signProxyTransaction({ + walletClient, + account, + chainId, + fromAddress, + toAddress, + data, + nonce, +}) { + if (!walletClient || typeof walletClient.signMessage !== 'function') { + throw new Error( + 'Runtime signer does not support signMessage; cannot sign PROXY relayer transaction.' + ); + } + + const encoded = encodePacked( + ['uint256', 'address', 'address', 'bytes', 'uint256'], + [ + BigInt(chainId), + getAddress(fromAddress), + getAddress(toAddress), + normalizeHexData(data), + BigInt(nonce), + ] + ); + const txHash = keccak256(encoded); + const signature = await walletClient.signMessage({ + account, + message: { raw: txHash }, + }); + + return { + txHash, + signature, + signatureParams: { + from: getAddress(fromAddress), + to: getAddress(toAddress), + data: normalizeHexData(data), + nonce: BigInt(nonce).toString(), + chainId: Number(chainId), + }, + }; +} + +async function resolveFromAddress({ + config, + explicitFrom, + accountAddress, + chainId, + txType, +}) { + if (explicitFrom) { + return getAddress(explicitFrom); + } + if (config?.polymarketRelayerFromAddress) { + return getAddress(config.polymarketRelayerFromAddress); + } + if (config?.polymarketClobAddress) { + return getAddress(config.polymarketClobAddress); + } + + const resolveProxyAddress = config?.polymarketRelayerResolveProxyAddress !== false; + if (resolveProxyAddress) { + try { + const existingProxy = await getRelayerProxyAddress({ + config, + signerAddress: accountAddress, + }); + if (existingProxy) { + return existingProxy; + } + } catch (error) { + // Continue to optional deployment/fallback. + } + } + + if (config?.polymarketRelayerAutoDeployProxy) { + const deployResponse = await createProxyWallet({ + config, + signerAddress: accountAddress, + chainId, + txType, + }); + const deployTxHash = extractRelayerTxHash(deployResponse); + if (!deployTxHash) { + throw new Error('Relayer proxy deployment did not return txHash.'); + } + await waitForRelayerTransaction({ + config, + txHash: deployTxHash, + }); + const deployedProxy = await getRelayerProxyAddress({ + config, + signerAddress: accountAddress, + }); + if (deployedProxy) { + return deployedProxy; + } + } + + return null; +} + +async function relayPolymarketTransaction({ + publicClient, + walletClient, + account, + config, + from, + to, + data, + value = 0n, + operation = 0, + nonce, + metadata, +}) { + if (!isPolymarketRelayerEnabled(config)) { + throw new Error('Polymarket relayer is disabled (POLYMARKET_RELAYER_ENABLED=false).'); + } + if (!publicClient) { + throw new Error('publicClient is required for relayer transaction submission.'); + } + if (!walletClient) { + throw new Error('walletClient is required for relayer transaction submission.'); + } + const runtimeAddress = getAddress(account?.address); + const chainId = Number( + config?.polymarketRelayerChainId ?? + (typeof publicClient.getChainId === 'function' + ? await publicClient.getChainId() + : undefined) + ); + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error( + 'Unable to resolve chainId for relayer transaction. Set POLYMARKET_RELAYER_CHAIN_ID.' + ); + } + + const txType = normalizeRelayerTxType(config?.polymarketRelayerTxType); + const fromAddress = await resolveFromAddress({ + config, + explicitFrom: from, + accountAddress: runtimeAddress, + chainId, + txType, + }); + if (!fromAddress) { + throw new Error( + 'Unable to resolve relayer wallet address. Set POLYMARKET_RELAYER_FROM_ADDRESS or POLYMARKET_CLOB_ADDRESS, or enable POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY.' + ); + } + + const normalizedTo = getAddress(to); + const normalizedData = normalizeHexData(data); + const normalizedValue = BigInt(value ?? 0n); + const normalizedOperation = Number(operation ?? 0); + let normalizedNonce; + if (nonce === undefined || nonce === null) { + if (txType === RELAYER_TX_TYPE.SAFE) { + try { + normalizedNonce = await getSafeNonce({ + publicClient, + safeAddress: fromAddress, + }); + } catch (error) { + const reason = error?.shortMessage ?? error?.message ?? String(error); + throw new Error( + `Failed to read SAFE nonce from ${fromAddress}. Ensure POLYMARKET_RELAYER_FROM_ADDRESS points to a deployed Safe proxy on chainId=${chainId}. ${reason}` + ); + } + } else { + normalizedNonce = await getProxyNonce({ + config, + proxyAddress: fromAddress, + }); + } + } else { + normalizedNonce = BigInt(nonce); + } + + const signed = + txType === RELAYER_TX_TYPE.SAFE + ? await signSafeTransaction({ + walletClient, + account, + chainId, + fromAddress, + toAddress: normalizedTo, + value: normalizedValue, + data: normalizedData, + operation: normalizedOperation, + nonce: normalizedNonce, + }) + : await signProxyTransaction({ + walletClient, + account, + chainId, + fromAddress, + toAddress: normalizedTo, + data: normalizedData, + nonce: normalizedNonce, + }); + + const txRequest = { + type: txType, + from: fromAddress, + to: normalizedTo, + data: normalizedData, + value: normalizedValue.toString(), + nonce: normalizedNonce.toString(), + }; + if (txType === RELAYER_TX_TYPE.SAFE) { + txRequest.operation = normalizedOperation; + } + + const submitResponse = await relayerRequest({ + config, + method: 'POST', + path: '/relayer/transaction', + body: { + ...txRequest, + txHash: signed.txHash, + signature: signed.signature, + signatureParams: signed.signatureParams, + metadata, + }, + }); + + const relayTxHash = extractRelayerTxHash(submitResponse) ?? normalizeHashOrNull(signed.txHash); + if (!relayTxHash) { + throw new Error('Relayer submission did not return txHash.'); + } + + const statusResponse = await waitForRelayerTransaction({ + config, + txHash: relayTxHash, + }); + const status = extractRelayerStatus(statusResponse); + let transactionHash = extractTransactionHash(statusResponse); + if (!transactionHash) { + try { + await publicClient.getTransactionReceipt({ hash: relayTxHash }); + transactionHash = relayTxHash; + } catch (error) { + // Relay tx hash is not necessarily the chain tx hash. + } + } + + if (!transactionHash) { + throw new Error( + `Relayer transaction ${relayTxHash} reached status=${status ?? 'unknown'} without transactionHash.` + ); + } + + return { + relayTxHash, + transactionHash, + status, + from: fromAddress, + txType, + nonce: normalizedNonce.toString(), + submitResponse, + statusResponse, + }; +} + +export { + RELAYER_TX_TYPE, + getRelayerProxyAddress, + isPolymarketRelayerEnabled, + relayPolymarketTransaction, +}; diff --git a/agent/src/lib/tools.js b/agent/src/lib/tools.js index 214b77a0..83365df0 100644 --- a/agent/src/lib/tools.js +++ b/agent/src/lib/tools.js @@ -991,6 +991,7 @@ async function executeToolCalls({ continue; } const txHash = await makeErc1155Deposit({ + publicClient, walletClient, account, config, diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index 5a4e5b8e..6b89defa 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -13,6 +13,10 @@ import { transactionsProposedEvent, } from './og.js'; import { normalizeAssertion } from './og.js'; +import { + isPolymarketRelayerEnabled, + relayPolymarketTransaction, +} from './polymarket-relayer.js'; import { normalizeHashOrNull, summarizeViemError } from './utils.js'; const conditionalTokensAbi = parseAbi([ @@ -646,6 +650,7 @@ async function makeDeposit({ } async function makeErc1155Deposit({ + publicClient, walletClient, account, config, @@ -666,6 +671,43 @@ async function makeErc1155Deposit({ } const transferData = data ?? '0x'; + if (isPolymarketRelayerEnabled(config)) { + const relayerFromAddress = config?.polymarketRelayerFromAddress ?? config?.polymarketClobAddress; + if (!relayerFromAddress) { + throw new Error( + 'Polymarket relayer ERC1155 deposit requires POLYMARKET_RELAYER_FROM_ADDRESS or POLYMARKET_CLOB_ADDRESS.' + ); + } + const transferCallData = encodeFunctionData({ + abi: erc1155TransferAbi, + functionName: 'safeTransferFrom', + args: [ + getAddress(relayerFromAddress), + config.commitmentSafe, + normalizedTokenId, + normalizedAmount, + transferData, + ], + }); + const relayed = await relayPolymarketTransaction({ + publicClient, + walletClient, + account, + config, + from: relayerFromAddress, + to: normalizedToken, + data: transferCallData, + value: 0n, + operation: 0, + metadata: { + tool: 'make_erc1155_deposit', + token: normalizedToken, + tokenId: normalizedTokenId.toString(), + amount: normalizedAmount.toString(), + }, + }); + return relayed.transactionHash; + } return walletClient.writeContract({ address: normalizedToken, From c6d79d391c47186d9c2ef94d40a3b7a8b9a3bf94 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 14:42:29 -0800 Subject: [PATCH 161/174] address some issues in the polymarket safe proxy relayer infrastructure implementation Signed-off-by: John Shutt --- agent/scripts/test-erc1155-deposit.mjs | 104 ++- agent/src/lib/polymarket-relayer.js | 951 ++++++++++++++++++++----- agent/src/lib/tx.js | 2 +- 3 files changed, 845 insertions(+), 212 deletions(-) diff --git a/agent/scripts/test-erc1155-deposit.mjs b/agent/scripts/test-erc1155-deposit.mjs index 7d830bf2..2f965702 100644 --- a/agent/scripts/test-erc1155-deposit.mjs +++ b/agent/scripts/test-erc1155-deposit.mjs @@ -1,5 +1,11 @@ import assert from 'node:assert/strict'; -import { decodeFunctionData, parseAbi } from 'viem'; +import { + decodeFunctionData, + encodeAbiParameters, + getCreate2Address, + keccak256, + parseAbi, +} from 'viem'; import { makeErc1155Deposit } from '../src/lib/tx.js'; async function run() { @@ -48,17 +54,60 @@ async function run() { /amount must be > 0/ ); - const relayerFromAddress = '0x3333333333333333333333333333333333333333'; + const relayerFromAddress = getCreate2Address({ + from: '0xaacfeea03eb1561c4e67d661e40682bd20e3541b', + salt: keccak256( + encodeAbiParameters( + [ + { + type: 'address', + }, + ], + [account.address] + ) + ), + bytecodeHash: + '0xb61d27f6f0f1579b6af9d23fafd567586f35f7d2f43d6bd5f85c0b690952d469', + }); const relayedTxHash = `0x${'1'.repeat(64)}`; const onchainTxHash = `0x${'2'.repeat(64)}`; + const relayerTransactionId = 'relayer-tx-1'; let relayerSubmitBody; let relayerSubmitHeaders; let statusPollCount = 0; + let sawSubmitEndpoint = false; const oldFetch = globalThis.fetch; try { globalThis.fetch = async (url, options = {}) => { const asText = String(url); - if (asText.endsWith('/relayer/transaction')) { + const asLower = asText.toLowerCase(); + if (asLower.includes('/deployed?') && asLower.includes(relayerFromAddress.toLowerCase())) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ deployed: true }); + }, + }; + } + + if (asText.includes('/relay-payload?')) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + address: relayerFromAddress, + nonce: '12', + }); + }, + }; + } + + if (asText.endsWith('/submit')) { + sawSubmitEndpoint = true; relayerSubmitBody = JSON.parse(options.body); relayerSubmitHeaders = options.headers; return { @@ -66,12 +115,20 @@ async function run() { status: 200, statusText: 'OK', async text() { - return JSON.stringify({ txHash: relayedTxHash }); + return JSON.stringify({ + transactionID: relayerTransactionId, + hash: relayedTxHash, + state: 'STATE_PENDING', + }); }, }; } - if (asText.endsWith(`/relayer/transaction-status/${relayedTxHash}`)) { + if ( + asText.includes('/transaction?') && + (asText.includes(`id=${relayerTransactionId}`) || + asText.includes(`transactionID=${relayerTransactionId}`)) + ) { statusPollCount += 1; return { ok: true, @@ -79,13 +136,22 @@ async function run() { statusText: 'OK', async text() { if (statusPollCount === 1) { - return JSON.stringify({ status: 'PENDING', txHash: relayedTxHash }); + return JSON.stringify([ + { + transactionID: relayerTransactionId, + hash: relayedTxHash, + state: 'STATE_PENDING', + }, + ]); } - return JSON.stringify({ - status: 'MINED', - txHash: relayedTxHash, - transactionHash: onchainTxHash, - }); + return JSON.stringify([ + { + transactionID: relayerTransactionId, + hash: relayedTxHash, + state: 'STATE_CONFIRMED', + transactionHash: onchainTxHash, + }, + ]); }, }; } @@ -97,7 +163,7 @@ async function run() { const relayerWalletClient = { async signMessage({ message }) { relayerSignedMessage = message; - return `0x${'a'.repeat(130)}`; + return `0x${'a'.repeat(128)}1b`; }, }; @@ -135,14 +201,20 @@ async function run() { }); assert.equal(relayerDepositHash, onchainTxHash); + assert.equal(sawSubmitEndpoint, true); assert.equal(relayerSubmitBody.type, 'SAFE'); - assert.equal(relayerSubmitBody.from.toLowerCase(), relayerFromAddress.toLowerCase()); + assert.equal(relayerSubmitBody.from.toLowerCase(), account.address.toLowerCase()); + assert.equal(relayerSubmitBody.proxyWallet.toLowerCase(), relayerFromAddress.toLowerCase()); assert.equal(relayerSubmitBody.to.toLowerCase(), token.toLowerCase()); - assert.equal(relayerSubmitBody.value, '0'); - assert.equal(relayerSubmitBody.operation, 0); assert.equal(relayerSubmitBody.nonce, '12'); + assert.equal(typeof relayerSubmitBody.signatureParams, 'object'); + assert.equal(relayerSubmitBody.signatureParams.gasPrice, '0'); + assert.equal(relayerSubmitBody.signatureParams.safeTxnGas, '0'); + assert.equal(relayerSubmitBody.signatureParams.baseGas, '0'); + assert.equal(relayerSubmitBody.signatureParams.operation, '0'); assert.equal(typeof relayerSubmitBody.signature, 'string'); - assert.equal(relayerSubmitBody.metadata.tool, 'make_erc1155_deposit'); + assert.equal(typeof relayerSubmitBody.metadata, 'string'); + assert.equal(relayerSubmitBody.metadata.includes('make_erc1155_deposit'), true); assert.equal(relayerSubmitHeaders.POLY_BUILDER_API_KEY, 'builder-key'); assert.equal(relayerSubmitHeaders.POLY_BUILDER_PASSPHRASE, 'builder-passphrase'); assert.equal(typeof relayerSubmitHeaders.POLY_BUILDER_SIGNATURE, 'string'); diff --git a/agent/src/lib/polymarket-relayer.js b/agent/src/lib/polymarket-relayer.js index 92cc6663..bc01e845 100644 --- a/agent/src/lib/polymarket-relayer.js +++ b/agent/src/lib/polymarket-relayer.js @@ -1,12 +1,28 @@ import crypto from 'node:crypto'; -import { encodePacked, getAddress, hashTypedData, isHex, keccak256, parseAbi } from 'viem'; +import { + encodeAbiParameters, + encodePacked, + getAddress, + getCreate2Address, + hashTypedData, + isHex, + keccak256, + zeroAddress, +} from 'viem'; import { normalizeAddressOrNull, normalizeHashOrNull } from './utils.js'; const DEFAULT_RELAYER_HOST = 'https://relayer-v2.polymarket.com'; const DEFAULT_RELAYER_REQUEST_TIMEOUT_MS = 15_000; const DEFAULT_RELAYER_POLL_INTERVAL_MS = 2_000; const DEFAULT_RELAYER_POLL_TIMEOUT_MS = 120_000; -const SAFE_TX_NONCE_ABI = parseAbi(['function nonce() view returns (uint256)']); +const SAFE_FACTORY_ADDRESS = '0xaacfeea03eb1561c4e67d661e40682bd20e3541b'; +const PROXY_FACTORY_ADDRESS = SAFE_FACTORY_ADDRESS; +const SAFE_INIT_CODE_HASH = + '0xb61d27f6f0f1579b6af9d23fafd567586f35f7d2f43d6bd5f85c0b690952d469'; +const PROXY_INIT_CODE_HASH = + '0x72ea4f5319066fd7435f2f2e1e8f117d0848fa51987edc76b4e2207ee3f1fe6f'; +const CREATE_PROXY_DOMAIN_NAME = 'Polymarket Contract Proxy Factory'; + const SAFE_EIP712_TYPES = Object.freeze({ EIP712Domain: [ { name: 'chainId', type: 'uint256' }, @@ -17,15 +33,59 @@ const SAFE_EIP712_TYPES = Object.freeze({ { name: 'value', type: 'uint256' }, { name: 'data', type: 'bytes' }, { name: 'operation', type: 'uint8' }, + { name: 'safeTxGas', type: 'uint256' }, + { name: 'baseGas', type: 'uint256' }, + { name: 'gasPrice', type: 'uint256' }, + { name: 'gasToken', type: 'address' }, + { name: 'refundReceiver', type: 'address' }, { name: 'nonce', type: 'uint256' }, ], }); + +const CREATE_PROXY_EIP712_TYPES = Object.freeze({ + CreateProxy: [ + { name: 'paymentToken', type: 'address' }, + { name: 'payment', type: 'uint256' }, + { name: 'paymentReceiver', type: 'address' }, + ], +}); + const RELAYER_TX_TYPE = Object.freeze({ SAFE: 'SAFE', PROXY: 'PROXY', }); -const RELAYER_SUCCESS_STATUSES = new Set(['MINED', 'CONFIRMED']); -const RELAYER_FAILURE_STATUSES = new Set(['FAILED', 'REVERTED']); + +const RELAYER_ENDPOINTS = Object.freeze({ + ADDRESS: '/address', + NONCE: '/nonce', + RELAY_PAYLOAD: '/relay-payload', + SUBMIT: '/submit', + TRANSACTION: '/transaction', + DEPLOYED: '/deployed', +}); + +const LEGACY_ENDPOINTS = Object.freeze({ + PROXY_ADDRESS: (address) => `/relayer/proxy-address/${encodeURIComponent(address)}`, + PROXY_NONCE: (address) => `/relayer/proxy-nonce/${encodeURIComponent(address)}`, + CREATE_PROXY: '/relayer/create-proxy-wallet', + TRANSACTION: '/relayer/transaction', + TRANSACTION_STATUS: (hash) => `/relayer/transaction-status/${encodeURIComponent(hash)}`, +}); + +const RELAYER_SUCCESS_STATES = new Set([ + 'STATE_MINED', + 'STATE_CONFIRMED', + 'MINED', + 'CONFIRMED', +]); + +const RELAYER_FAILURE_STATES = new Set([ + 'STATE_FAILED', + 'STATE_INVALID', + 'FAILED', + 'REVERTED', + 'INVALID', +]); function normalizeNonNegativeInteger(value, fallback) { const parsed = Number(value); @@ -54,6 +114,20 @@ function normalizeHexData(data) { return data; } +function normalizeMetadata(metadata) { + if (typeof metadata === 'string') { + return metadata; + } + if (metadata === null || metadata === undefined) { + return ''; + } + try { + return JSON.stringify(metadata); + } catch (error) { + return String(metadata); + } +} + function getBuilderCredentials(config) { const apiKey = config?.polymarketBuilderApiKey ?? @@ -101,6 +175,38 @@ function buildRelayerAuthHeaders({ }; } +function buildPathWithQuery(basePath, params) { + const urlParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params ?? {})) { + if (value === undefined || value === null || value === '') continue; + urlParams.set(key, String(value)); + } + const query = urlParams.toString(); + return query.length > 0 ? `${basePath}?${query}` : basePath; +} + +function uniqueList(values) { + return [...new Set(values.filter(Boolean))]; +} + +function buildSignerQueryCandidates(basePath, signerAddress, txType) { + const address = getAddress(signerAddress); + const normalizedType = normalizeRelayerTxType(txType); + const typeCandidates = uniqueList([ + normalizedType, + normalizedType.toLowerCase(), + ]); + + const paths = []; + for (const signerType of typeCandidates) { + paths.push(buildPathWithQuery(basePath, { address, type: signerType })); + paths.push(buildPathWithQuery(basePath, { address, signerType })); + paths.push(buildPathWithQuery(basePath, { signerAddress: address, signerType })); + paths.push(buildPathWithQuery(basePath, { signerAddress: address, type: signerType })); + } + return uniqueList(paths); +} + async function sleep(ms) { if (ms <= 0) return; await new Promise((resolve) => setTimeout(resolve, ms)); @@ -143,14 +249,42 @@ async function relayerRequest({ } if (!response.ok) { - throw new Error( + const requestError = new Error( `Relayer request failed (${method} ${path}): ${response.status} ${response.statusText} ${text}` ); + requestError.statusCode = response.status; + requestError.responseBody = parsed; + throw requestError; } return parsed; } +async function relayerRequestFirst({ + config, + method, + candidatePaths, + body, +}) { + let lastError; + + for (const path of candidatePaths) { + try { + const payload = await relayerRequest({ + config, + method, + path, + body, + }); + return { payload, path }; + } catch (error) { + lastError = error; + } + } + + throw lastError ?? new Error(`Relayer request failed for ${method} ${candidatePaths.join(', ')}`); +} + function collectPayloadObjects(payload) { const out = []; const seen = new Set(); @@ -192,6 +326,26 @@ function extractStringField(payload, fieldNames) { return null; } +function extractBooleanField(payload, fieldNames) { + const candidates = collectPayloadObjects(payload); + for (const candidate of candidates) { + for (const fieldName of fieldNames) { + const value = candidate?.[fieldName]; + if (typeof value === 'boolean') return value; + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + if (normalized === 'true') return true; + if (normalized === 'false') return false; + } + if (typeof value === 'number') { + if (value === 1) return true; + if (value === 0) return false; + } + } + } + return null; +} + function extractRelayerTxHash(payload) { const hashCandidate = extractStringField(payload, [ 'txHash', @@ -199,8 +353,7 @@ function extractRelayerTxHash(payload) { 'relay_hash', 'hash', ]); - const normalized = normalizeHashOrNull(hashCandidate); - return normalized; + return normalizeHashOrNull(hashCandidate); } function extractTransactionHash(payload) { @@ -208,138 +361,336 @@ function extractTransactionHash(payload) { 'transactionHash', 'transaction_hash', 'chainTxHash', + 'minedTransactionHash', ]); return normalizeHashOrNull(hashCandidate); } +function extractTransactionId(payload) { + const idCandidate = extractStringField(payload, [ + 'transactionID', + 'transactionId', + 'id', + ]); + return idCandidate && idCandidate.length > 0 ? idCandidate : null; +} + +function normalizeRelayerState(value) { + if (typeof value !== 'string') return null; + const normalized = value.trim().toUpperCase(); + if (!normalized) return null; + if (!normalized.startsWith('STATE_')) { + if (normalized === 'PENDING') return 'STATE_PENDING'; + if (normalized === 'MINED') return 'STATE_MINED'; + if (normalized === 'CONFIRMED') return 'STATE_CONFIRMED'; + if (normalized === 'FAILED') return 'STATE_FAILED'; + if (normalized === 'INVALID') return 'STATE_INVALID'; + } + return normalized; +} + function extractRelayerStatus(payload) { - const statusCandidate = extractStringField(payload, ['status', 'txStatus', 'state']); + const statusCandidate = extractStringField(payload, ['state', 'status', 'txStatus']); if (!statusCandidate) return null; - return statusCandidate.toUpperCase(); + return normalizeRelayerState(statusCandidate); +} + +function extractTransactionRecord(payload) { + if (Array.isArray(payload)) { + return payload.length > 0 ? payload[0] : null; + } + if (payload && typeof payload === 'object') { + if (Array.isArray(payload.transactions) && payload.transactions.length > 0) { + return payload.transactions[0]; + } + if (payload.transaction && typeof payload.transaction === 'object') { + return payload.transaction; + } + } + return payload; +} + +function getSafeFactoryAddress(config) { + const configured = normalizeAddressOrNull(config?.polymarketRelayerSafeFactory); + return configured ? getAddress(configured) : SAFE_FACTORY_ADDRESS; +} + +function getProxyFactoryAddress(config) { + const configured = normalizeAddressOrNull(config?.polymarketRelayerProxyFactory); + return configured ? getAddress(configured) : PROXY_FACTORY_ADDRESS; +} + +function deriveSafeAddress({ signerAddress, safeFactory }) { + return getCreate2Address({ + from: getAddress(safeFactory), + salt: keccak256( + encodeAbiParameters( + [ + { + type: 'address', + }, + ], + [getAddress(signerAddress)] + ) + ), + bytecodeHash: SAFE_INIT_CODE_HASH, + }); +} + +function deriveProxyAddress({ signerAddress, proxyFactory }) { + return getCreate2Address({ + from: getAddress(proxyFactory), + salt: keccak256(encodePacked(['address'], [getAddress(signerAddress)])), + bytecodeHash: PROXY_INIT_CODE_HASH, + }); +} + +function splitAndPackSignature(signature) { + if (typeof signature !== 'string' || !/^0x[0-9a-fA-F]{130}$/.test(signature)) { + throw new Error('Invalid 65-byte signature.'); + } + + const body = signature.slice(2); + const r = body.slice(0, 64); + const s = body.slice(64, 128); + const vRaw = Number.parseInt(body.slice(128, 130), 16); + + let safeV = vRaw; + if (safeV === 0 || safeV === 1) { + safeV += 31; + } else if (safeV === 27 || safeV === 28) { + safeV += 4; + } + + if (safeV < 31 || safeV > 32) { + throw new Error(`Unexpected signature v=${vRaw} while packing SAFE signature.`); + } + + return `0x${r}${s}${safeV.toString(16).padStart(2, '0')}`; } function isPolymarketRelayerEnabled(config) { return Boolean(config?.polymarketRelayerEnabled); } +async function getRelayerPayload({ + config, + signerAddress, + txType, +}) { + const payloadCandidates = buildSignerQueryCandidates( + RELAYER_ENDPOINTS.RELAY_PAYLOAD, + signerAddress, + txType + ); + + try { + const { payload } = await relayerRequestFirst({ + config, + method: 'GET', + candidatePaths: payloadCandidates, + }); + return payload; + } catch (error) { + return null; + } +} + async function getRelayerProxyAddress({ config, signerAddress, + txType = RELAYER_TX_TYPE.SAFE, }) { - const response = await relayerRequest({ + const payload = await getRelayerPayload({ config, - method: 'GET', - path: `/relayer/proxy-address/${encodeURIComponent(getAddress(signerAddress))}`, + signerAddress, + txType, }); - const candidate = extractStringField(response, [ + + let candidate = extractStringField(payload, [ + 'address', 'proxyWallet', 'proxyAddress', 'walletAddress', - 'address', ]); + + if (!candidate) { + try { + const { payload: addressPayload } = await relayerRequestFirst({ + config, + method: 'GET', + candidatePaths: buildSignerQueryCandidates( + RELAYER_ENDPOINTS.ADDRESS, + signerAddress, + txType + ), + }); + candidate = extractStringField(addressPayload, [ + 'address', + 'proxyWallet', + 'proxyAddress', + 'walletAddress', + ]); + } catch (error) { + // Continue to legacy fallback. + } + } + + if (!candidate) { + try { + const { payload: legacyPayload } = await relayerRequestFirst({ + config, + method: 'GET', + candidatePaths: [LEGACY_ENDPOINTS.PROXY_ADDRESS(getAddress(signerAddress))], + }); + candidate = extractStringField(legacyPayload, [ + 'proxyWallet', + 'proxyAddress', + 'walletAddress', + 'address', + ]); + } catch (error) { + // No legacy proxy-address support. + } + } + const normalized = normalizeAddressOrNull(candidate); return normalized ? getAddress(normalized) : null; } -async function getSafeNonce({ - publicClient, - safeAddress, -}) { - return publicClient.readContract({ - address: getAddress(safeAddress), - abi: SAFE_TX_NONCE_ABI, - functionName: 'nonce', - }); -} - -async function getProxyNonce({ +async function getRelayerNonce({ config, + signerAddress, + txType, proxyAddress, }) { - const response = await relayerRequest({ + const payload = await getRelayerPayload({ config, - method: 'GET', - path: `/relayer/proxy-nonce/${encodeURIComponent(getAddress(proxyAddress))}`, + signerAddress, + txType, }); - const nonceCandidate = extractStringField(response, ['nonce']); - if (nonceCandidate === null) { - throw new Error('Relayer proxy nonce response did not include nonce.'); + + let nonceCandidate = extractStringField(payload, ['nonce']); + + if (!nonceCandidate) { + try { + const { payload: noncePayload } = await relayerRequestFirst({ + config, + method: 'GET', + candidatePaths: buildSignerQueryCandidates( + RELAYER_ENDPOINTS.NONCE, + signerAddress, + txType + ), + }); + nonceCandidate = extractStringField(noncePayload, ['nonce']); + } catch (error) { + // Continue to legacy fallback. + } + } + + if (!nonceCandidate && proxyAddress) { + try { + const { payload: legacyNoncePayload } = await relayerRequestFirst({ + config, + method: 'GET', + candidatePaths: [LEGACY_ENDPOINTS.PROXY_NONCE(getAddress(proxyAddress))], + }); + nonceCandidate = extractStringField(legacyNoncePayload, ['nonce']); + } catch (error) { + // Continue. + } + } + + if (!nonceCandidate) { + throw new Error('Relayer nonce response did not include nonce.'); } + return BigInt(nonceCandidate); } -async function createProxyWallet({ +async function getSafeDeployed({ config, - signerAddress, - chainId, - txType, + safeAddress, }) { - return relayerRequest({ - config, - method: 'POST', - path: '/relayer/create-proxy-wallet', - body: { - from: getAddress(signerAddress), - chainId: Number(chainId), - relayerTxType: txType, - }, - }); + try { + const { payload } = await relayerRequestFirst({ + config, + method: 'GET', + candidatePaths: uniqueList([ + buildPathWithQuery(RELAYER_ENDPOINTS.DEPLOYED, { + address: getAddress(safeAddress), + }), + buildPathWithQuery(RELAYER_ENDPOINTS.DEPLOYED, { + proxyWallet: getAddress(safeAddress), + }), + ]), + }); + return extractBooleanField(payload, ['deployed', 'isDeployed', 'safeDeployed']); + } catch (error) { + return null; + } } -async function waitForRelayerTransaction({ - config, - txHash, +async function buildSafeCreateRequest({ + walletClient, + account, + chainId, + signerAddress, + safeFactory, + safeAddress, + metadata, }) { - const normalizedTxHash = normalizeHashOrNull(txHash); - if (!normalizedTxHash) { - throw new Error(`Invalid relayer txHash: ${txHash}`); + if (!walletClient || typeof walletClient.signTypedData !== 'function') { + throw new Error( + 'Runtime signer does not support signTypedData; cannot create SAFE proxy wallet through relayer.' + ); } - const pollIntervalMs = normalizeNonNegativeInteger( - config?.polymarketRelayerPollIntervalMs, - DEFAULT_RELAYER_POLL_INTERVAL_MS - ); - const timeoutMs = normalizeNonNegativeInteger( - config?.polymarketRelayerPollTimeoutMs, - DEFAULT_RELAYER_POLL_TIMEOUT_MS - ); - const deadline = Date.now() + timeoutMs; - let lastPayload = null; - while (Date.now() <= deadline) { - lastPayload = await relayerRequest({ - config, - method: 'GET', - path: `/relayer/transaction-status/${encodeURIComponent(normalizedTxHash)}`, - }); - const status = extractRelayerStatus(lastPayload); - if (status && RELAYER_SUCCESS_STATUSES.has(status)) { - return lastPayload; - } - if (status && RELAYER_FAILURE_STATUSES.has(status)) { - throw new Error( - `Relayer transaction failed with status=${status} for txHash=${normalizedTxHash}.` - ); - } - await sleep(pollIntervalMs); - } + const signature = await walletClient.signTypedData({ + account, + domain: { + name: CREATE_PROXY_DOMAIN_NAME, + chainId: Number(chainId), + verifyingContract: getAddress(safeFactory), + }, + types: CREATE_PROXY_EIP712_TYPES, + primaryType: 'CreateProxy', + message: { + paymentToken: zeroAddress, + payment: 0n, + paymentReceiver: zeroAddress, + }, + }); - throw new Error( - `Timed out waiting for relayer transaction ${normalizedTxHash}. Last payload: ${JSON.stringify( - lastPayload - )}` - ); + return { + from: getAddress(signerAddress), + to: getAddress(safeFactory), + proxyWallet: getAddress(safeAddress), + data: '0x', + signature, + signatureParams: { + paymentToken: zeroAddress, + payment: '0', + paymentReceiver: zeroAddress, + }, + type: 'SAFE-CREATE', + metadata: normalizeMetadata(metadata), + }; } async function signSafeTransaction({ walletClient, account, chainId, - fromAddress, + signerAddress, + proxyWallet, toAddress, value, data, operation, nonce, + metadata, }) { if (!walletClient || typeof walletClient.signMessage !== 'function') { throw new Error( @@ -350,10 +701,10 @@ async function signSafeTransaction({ throw new Error('SAFE relayer transaction operation must be 0 or 1.'); } - const txHash = hashTypedData({ + const safeTxHash = hashTypedData({ domain: { chainId: Number(chainId), - verifyingContract: getAddress(fromAddress), + verifyingContract: getAddress(proxyWallet), }, primaryType: 'SafeTx', types: SAFE_EIP712_TYPES, @@ -362,24 +713,40 @@ async function signSafeTransaction({ value: BigInt(value), data: normalizeHexData(data), operation, + safeTxGas: 0n, + baseGas: 0n, + gasPrice: 0n, + gasToken: zeroAddress, + refundReceiver: zeroAddress, nonce: BigInt(nonce), }, }); + const signature = await walletClient.signMessage({ account, - message: { raw: txHash }, + message: { raw: safeTxHash }, }); return { - txHash, - signature, - signatureParams: { + request: { + from: getAddress(signerAddress), to: getAddress(toAddress), - value: BigInt(value).toString(), + proxyWallet: getAddress(proxyWallet), data: normalizeHexData(data), - operation, nonce: BigInt(nonce).toString(), + signature: splitAndPackSignature(signature), + signatureParams: { + gasPrice: '0', + operation: String(operation), + safeTxnGas: '0', + baseGas: '0', + gasToken: zeroAddress, + refundReceiver: zeroAddress, + }, + type: RELAYER_TX_TYPE.SAFE, + metadata: normalizeMetadata(metadata), }, + txHash: safeTxHash, }; } @@ -387,10 +754,12 @@ async function signProxyTransaction({ walletClient, account, chainId, - fromAddress, + signerAddress, + proxyWallet, toAddress, data, nonce, + metadata, }) { if (!walletClient || typeof walletClient.signMessage !== 'function') { throw new Error( @@ -402,40 +771,191 @@ async function signProxyTransaction({ ['uint256', 'address', 'address', 'bytes', 'uint256'], [ BigInt(chainId), - getAddress(fromAddress), + getAddress(proxyWallet), getAddress(toAddress), normalizeHexData(data), BigInt(nonce), ] ); - const txHash = keccak256(encoded); + + const proxyTxHash = keccak256(encoded); const signature = await walletClient.signMessage({ account, - message: { raw: txHash }, + message: { raw: proxyTxHash }, }); return { - txHash, - signature, - signatureParams: { - from: getAddress(fromAddress), + request: { + from: getAddress(signerAddress), to: getAddress(toAddress), + proxyWallet: getAddress(proxyWallet), data: normalizeHexData(data), nonce: BigInt(nonce).toString(), - chainId: Number(chainId), + signature, + signatureParams: { + chainId: String(chainId), + }, + type: RELAYER_TX_TYPE.PROXY, + metadata: normalizeMetadata(metadata), }, + txHash: proxyTxHash, + }; +} + +async function submitRelayerTransaction({ + config, + transactionRequest, +}) { + const { payload, path } = await relayerRequestFirst({ + config, + method: 'POST', + candidatePaths: [RELAYER_ENDPOINTS.SUBMIT, LEGACY_ENDPOINTS.TRANSACTION], + body: transactionRequest, + }); + + return { + payload, + path, + transactionId: extractTransactionId(payload), + relayTxHash: extractRelayerTxHash(payload), + transactionHash: extractTransactionHash(payload), + state: extractRelayerStatus(payload), + }; +} + +async function fetchTransactionStatus({ + config, + transactionId, + relayTxHash, +}) { + const candidatePaths = []; + + if (transactionId) { + candidatePaths.push( + buildPathWithQuery(RELAYER_ENDPOINTS.TRANSACTION, { id: transactionId }), + buildPathWithQuery(RELAYER_ENDPOINTS.TRANSACTION, { + transactionID: transactionId, + }), + buildPathWithQuery(RELAYER_ENDPOINTS.TRANSACTION, { + transactionId, + }) + ); + } + + if (relayTxHash) { + candidatePaths.push( + buildPathWithQuery(RELAYER_ENDPOINTS.TRANSACTION, { hash: relayTxHash }), + buildPathWithQuery(RELAYER_ENDPOINTS.TRANSACTION, { txHash: relayTxHash }), + LEGACY_ENDPOINTS.TRANSACTION_STATUS(relayTxHash) + ); + } + + const { payload, path } = await relayerRequestFirst({ + config, + method: 'GET', + candidatePaths: uniqueList(candidatePaths), + }); + + const record = extractTransactionRecord(payload); + const state = extractRelayerStatus(record ?? payload); + const nextTransactionId = extractTransactionId(record ?? payload) ?? transactionId; + const nextRelayTxHash = extractRelayerTxHash(record ?? payload) ?? relayTxHash; + const transactionHash = extractTransactionHash(record ?? payload); + + return { + payload, + path, + state, + transactionId: nextTransactionId, + relayTxHash: nextRelayTxHash, + transactionHash, }; } -async function resolveFromAddress({ +async function waitForRelayerTransaction({ + config, + transactionId, + relayTxHash, +}) { + if (!transactionId && !relayTxHash) { + throw new Error('waitForRelayerTransaction requires transactionId or relayTxHash.'); + } + + const pollIntervalMs = normalizeNonNegativeInteger( + config?.polymarketRelayerPollIntervalMs, + DEFAULT_RELAYER_POLL_INTERVAL_MS + ); + const timeoutMs = normalizeNonNegativeInteger( + config?.polymarketRelayerPollTimeoutMs, + DEFAULT_RELAYER_POLL_TIMEOUT_MS + ); + const deadline = Date.now() + timeoutMs; + let lastStatus; + let currentTransactionId = transactionId; + let currentRelayTxHash = relayTxHash; + + while (Date.now() <= deadline) { + const statusResult = await fetchTransactionStatus({ + config, + transactionId: currentTransactionId, + relayTxHash: currentRelayTxHash, + }); + + currentTransactionId = statusResult.transactionId ?? currentTransactionId; + currentRelayTxHash = statusResult.relayTxHash ?? currentRelayTxHash; + lastStatus = statusResult; + + if (statusResult.state && RELAYER_SUCCESS_STATES.has(statusResult.state)) { + return { + ...statusResult, + transactionId: currentTransactionId, + relayTxHash: currentRelayTxHash, + }; + } + + if (statusResult.state && RELAYER_FAILURE_STATES.has(statusResult.state)) { + throw new Error( + `Relayer transaction failed with state=${statusResult.state} for transactionId=${currentTransactionId ?? 'unknown'} relayTxHash=${currentRelayTxHash ?? 'unknown'}.` + ); + } + + await sleep(pollIntervalMs); + } + + throw new Error( + `Timed out waiting for relayer transaction. transactionId=${currentTransactionId ?? 'unknown'} relayTxHash=${currentRelayTxHash ?? 'unknown'} lastStatus=${JSON.stringify( + lastStatus?.payload ?? null + )}` + ); +} + +async function createProxyWallet({ config, - explicitFrom, - accountAddress, + signerAddress, chainId, txType, }) { - if (explicitFrom) { - return getAddress(explicitFrom); + return relayerRequest({ + config, + method: 'POST', + path: LEGACY_ENDPOINTS.CREATE_PROXY, + body: { + from: getAddress(signerAddress), + chainId: Number(chainId), + relayerTxType: txType, + }, + }); +} + +async function resolveProxyWalletAddress({ + config, + signerAddress, + chainId, + txType, + explicitProxyWallet, +}) { + if (explicitProxyWallet) { + return getAddress(explicitProxyWallet); } if (config?.polymarketRelayerFromAddress) { return getAddress(config.polymarketRelayerFromAddress); @@ -449,41 +969,90 @@ async function resolveFromAddress({ try { const existingProxy = await getRelayerProxyAddress({ config, - signerAddress: accountAddress, + signerAddress, + txType, }); if (existingProxy) { return existingProxy; } } catch (error) { - // Continue to optional deployment/fallback. + // Continue to deterministic derivation and optional deployment. } } - if (config?.polymarketRelayerAutoDeployProxy) { + const derivedAddress = + txType === RELAYER_TX_TYPE.SAFE + ? deriveSafeAddress({ + signerAddress, + safeFactory: getSafeFactoryAddress(config), + }) + : deriveProxyAddress({ + signerAddress, + proxyFactory: getProxyFactoryAddress(config), + }); + + if (txType === RELAYER_TX_TYPE.PROXY && config?.polymarketRelayerAutoDeployProxy) { const deployResponse = await createProxyWallet({ config, - signerAddress: accountAddress, + signerAddress, chainId, txType, }); + const deployTxId = extractTransactionId(deployResponse); const deployTxHash = extractRelayerTxHash(deployResponse); - if (!deployTxHash) { - throw new Error('Relayer proxy deployment did not return txHash.'); - } await waitForRelayerTransaction({ config, - txHash: deployTxHash, - }); - const deployedProxy = await getRelayerProxyAddress({ - config, - signerAddress: accountAddress, + transactionId: deployTxId, + relayTxHash: deployTxHash, }); - if (deployedProxy) { - return deployedProxy; - } } - return null; + return derivedAddress; +} + +async function ensureSafeDeployed({ + config, + walletClient, + account, + chainId, + signerAddress, + safeAddress, +}) { + const deployed = await getSafeDeployed({ + config, + safeAddress, + }); + + if (deployed === true || deployed === null) { + return; + } + + if (!config?.polymarketRelayerAutoDeployProxy) { + throw new Error( + `SAFE proxy wallet ${safeAddress} appears undeployed. Enable POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY=true to deploy automatically or deploy it out of band.` + ); + } + + const createRequest = await buildSafeCreateRequest({ + walletClient, + account, + chainId, + signerAddress, + safeFactory: getSafeFactoryAddress(config), + safeAddress, + metadata: 'Relayer SAFE deployment', + }); + + const createSubmission = await submitRelayerTransaction({ + config, + transactionRequest: createRequest, + }); + + await waitForRelayerTransaction({ + config, + transactionId: createSubmission.transactionId, + relayTxHash: createSubmission.relayTxHash, + }); } async function relayPolymarketTransaction({ @@ -491,7 +1060,7 @@ async function relayPolymarketTransaction({ walletClient, account, config, - from, + proxyWallet, to, data, value = 0n, @@ -508,13 +1077,15 @@ async function relayPolymarketTransaction({ if (!walletClient) { throw new Error('walletClient is required for relayer transaction submission.'); } - const runtimeAddress = getAddress(account?.address); + + const signerAddress = getAddress(account?.address); const chainId = Number( config?.polymarketRelayerChainId ?? (typeof publicClient.getChainId === 'function' ? await publicClient.getChainId() : undefined) ); + if (!Number.isInteger(chainId) || chainId <= 0) { throw new Error( 'Unable to resolve chainId for relayer transaction. Set POLYMARKET_RELAYER_CHAIN_ID.' @@ -522,46 +1093,54 @@ async function relayPolymarketTransaction({ } const txType = normalizeRelayerTxType(config?.polymarketRelayerTxType); - const fromAddress = await resolveFromAddress({ + const resolvedProxyWallet = await resolveProxyWalletAddress({ config, - explicitFrom: from, - accountAddress: runtimeAddress, + signerAddress, chainId, txType, + explicitProxyWallet: proxyWallet, }); - if (!fromAddress) { + + if (!resolvedProxyWallet) { throw new Error( - 'Unable to resolve relayer wallet address. Set POLYMARKET_RELAYER_FROM_ADDRESS or POLYMARKET_CLOB_ADDRESS, or enable POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY.' + 'Unable to resolve relayer proxy wallet address. Set POLYMARKET_RELAYER_FROM_ADDRESS or POLYMARKET_CLOB_ADDRESS, or enable POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY.' ); } + if (txType === RELAYER_TX_TYPE.SAFE) { + const expectedSafeAddress = deriveSafeAddress({ + signerAddress, + safeFactory: getSafeFactoryAddress(config), + }); + if (resolvedProxyWallet.toLowerCase() !== expectedSafeAddress.toLowerCase()) { + throw new Error( + `Configured SAFE proxy wallet ${resolvedProxyWallet} does not match expected relayer SAFE address ${expectedSafeAddress} for signer ${signerAddress}.` + ); + } + await ensureSafeDeployed({ + config, + walletClient, + account, + chainId, + signerAddress, + safeAddress: resolvedProxyWallet, + }); + } + const normalizedTo = getAddress(to); const normalizedData = normalizeHexData(data); const normalizedValue = BigInt(value ?? 0n); const normalizedOperation = Number(operation ?? 0); - let normalizedNonce; - if (nonce === undefined || nonce === null) { - if (txType === RELAYER_TX_TYPE.SAFE) { - try { - normalizedNonce = await getSafeNonce({ - publicClient, - safeAddress: fromAddress, - }); - } catch (error) { - const reason = error?.shortMessage ?? error?.message ?? String(error); - throw new Error( - `Failed to read SAFE nonce from ${fromAddress}. Ensure POLYMARKET_RELAYER_FROM_ADDRESS points to a deployed Safe proxy on chainId=${chainId}. ${reason}` - ); - } - } else { - normalizedNonce = await getProxyNonce({ + + const normalizedNonce = + nonce === undefined || nonce === null + ? await getRelayerNonce({ config, - proxyAddress: fromAddress, - }); - } - } else { - normalizedNonce = BigInt(nonce); - } + signerAddress, + txType, + proxyAddress: resolvedProxyWallet, + }) + : BigInt(nonce); const signed = txType === RELAYER_TX_TYPE.SAFE @@ -569,83 +1148,65 @@ async function relayPolymarketTransaction({ walletClient, account, chainId, - fromAddress, + signerAddress, + proxyWallet: resolvedProxyWallet, toAddress: normalizedTo, value: normalizedValue, data: normalizedData, operation: normalizedOperation, nonce: normalizedNonce, + metadata, }) : await signProxyTransaction({ walletClient, account, chainId, - fromAddress, + signerAddress, + proxyWallet: resolvedProxyWallet, toAddress: normalizedTo, data: normalizedData, nonce: normalizedNonce, + metadata, }); - const txRequest = { - type: txType, - from: fromAddress, - to: normalizedTo, - data: normalizedData, - value: normalizedValue.toString(), - nonce: normalizedNonce.toString(), - }; - if (txType === RELAYER_TX_TYPE.SAFE) { - txRequest.operation = normalizedOperation; - } - - const submitResponse = await relayerRequest({ + const submission = await submitRelayerTransaction({ config, - method: 'POST', - path: '/relayer/transaction', - body: { - ...txRequest, - txHash: signed.txHash, - signature: signed.signature, - signatureParams: signed.signatureParams, - metadata, - }, + transactionRequest: signed.request, }); - const relayTxHash = extractRelayerTxHash(submitResponse) ?? normalizeHashOrNull(signed.txHash); - if (!relayTxHash) { - throw new Error('Relayer submission did not return txHash.'); - } - - const statusResponse = await waitForRelayerTransaction({ + const waited = await waitForRelayerTransaction({ config, - txHash: relayTxHash, + transactionId: submission.transactionId, + relayTxHash: submission.relayTxHash ?? signed.txHash, }); - const status = extractRelayerStatus(statusResponse); - let transactionHash = extractTransactionHash(statusResponse); - if (!transactionHash) { + + let transactionHash = waited.transactionHash ?? submission.transactionHash; + if (!transactionHash && waited.relayTxHash) { try { - await publicClient.getTransactionReceipt({ hash: relayTxHash }); - transactionHash = relayTxHash; + await publicClient.getTransactionReceipt({ hash: waited.relayTxHash }); + transactionHash = waited.relayTxHash; } catch (error) { - // Relay tx hash is not necessarily the chain tx hash. + // Relay tx hash is not always the chain transaction hash. } } if (!transactionHash) { throw new Error( - `Relayer transaction ${relayTxHash} reached status=${status ?? 'unknown'} without transactionHash.` + `Relayer transaction reached state=${waited.state ?? 'unknown'} without transactionHash. transactionId=${waited.transactionId ?? 'unknown'} relayTxHash=${waited.relayTxHash ?? 'unknown'}` ); } return { - relayTxHash, transactionHash, - status, - from: fromAddress, + relayTxHash: waited.relayTxHash ?? submission.relayTxHash ?? null, + transactionId: waited.transactionId ?? submission.transactionId ?? null, + state: waited.state ?? submission.state ?? null, + from: signerAddress, + proxyWallet: resolvedProxyWallet, txType, nonce: normalizedNonce.toString(), - submitResponse, - statusResponse, + submitResponse: submission.payload, + statusResponse: waited.payload, }; } diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index 6b89defa..b124c524 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -694,7 +694,7 @@ async function makeErc1155Deposit({ walletClient, account, config, - from: relayerFromAddress, + proxyWallet: relayerFromAddress, to: normalizedToken, data: transferCallData, value: 0n, From 6b41d2df0b8bff40b1e611409796e2a3cfdddf62 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 14:59:15 -0800 Subject: [PATCH 162/174] fix more polymarket relayer issues Signed-off-by: John Shutt --- agent/.env.example | 2 + agent/README.md | 5 +- agent/scripts/test-erc1155-deposit.mjs | 360 +++++++++++++++++++++++++ agent/src/lib/config.js | 6 + agent/src/lib/polymarket-relayer.js | 100 +++++-- agent/src/lib/tx.js | 12 +- 6 files changed, 451 insertions(+), 34 deletions(-) diff --git a/agent/.env.example b/agent/.env.example index dcdff6f4..732983a5 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -51,6 +51,8 @@ WATCH_NATIVE_BALANCE=true # POLYMARKET_RELAYER_HOST=https://relayer-v2.polymarket.com # POLYMARKET_RELAYER_TX_TYPE=SAFE # POLYMARKET_RELAYER_FROM_ADDRESS= +# POLYMARKET_RELAYER_SAFE_FACTORY= +# POLYMARKET_RELAYER_PROXY_FACTORY= # POLYMARKET_RELAYER_RESOLVE_PROXY_ADDRESS=true # POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY=false # POLYMARKET_RELAYER_CHAIN_ID= diff --git a/agent/README.md b/agent/README.md index c5b7c895..a66cc02a 100644 --- a/agent/README.md +++ b/agent/README.md @@ -106,7 +106,8 @@ Set these when using Polymarket functionality: - `POLYMARKET_RELAYER_ENABLED`: Enable Polymarket relayer submission for ERC1155 deposits (`true`/`false`, default `false`). - `POLYMARKET_RELAYER_HOST`: Relayer API host (default `https://relayer-v2.polymarket.com`). - `POLYMARKET_RELAYER_TX_TYPE`: Relayer wallet type (`SAFE` default, or `PROXY`). -- `POLYMARKET_RELAYER_FROM_ADDRESS`: Optional explicit relayer wallet address to send from. +- `POLYMARKET_RELAYER_FROM_ADDRESS`: Optional explicit relayer proxy wallet address (if omitted, runtime auto-resolves from signer + relayer APIs / deterministic address). +- `POLYMARKET_RELAYER_SAFE_FACTORY`, `POLYMARKET_RELAYER_PROXY_FACTORY`: Optional factory overrides for deterministic SAFE/PROXY address derivation. - `POLYMARKET_RELAYER_RESOLVE_PROXY_ADDRESS`: Resolve proxy address via relayer API when from-address is not set (default `true`). - `POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY`: Optionally create proxy wallet when absent (default `false`). - `POLYMARKET_RELAYER_CHAIN_ID`, `POLYMARKET_RELAYER_REQUEST_TIMEOUT_MS`, `POLYMARKET_RELAYER_POLL_INTERVAL_MS`, `POLYMARKET_RELAYER_POLL_TIMEOUT_MS`: Optional relayer runtime tuning. @@ -165,7 +166,7 @@ Use `make_erc1155_deposit` after receiving YES/NO position tokens: } ``` -When `POLYMARKET_RELAYER_ENABLED=true`, this tool submits via Polymarket relayer (SAFE/PROXY) instead of direct onchain `writeContract`. For SAFE proxy-wallet mode, set `POLYMARKET_RELAYER_FROM_ADDRESS` (or `POLYMARKET_CLOB_ADDRESS`) to the proxy Safe that holds the YES/NO tokens. +When `POLYMARKET_RELAYER_ENABLED=true`, this tool submits via Polymarket relayer (SAFE/PROXY) instead of direct onchain `writeContract`. If `POLYMARKET_RELAYER_FROM_ADDRESS` is not set, the runtime resolves the proxy wallet from the signer and relayer metadata. For SAFE mode, any explicitly configured proxy wallet must match the relayer-derived SAFE address for that signer. #### CLOB Place/Cancel Tools diff --git a/agent/scripts/test-erc1155-deposit.mjs b/agent/scripts/test-erc1155-deposit.mjs index 2f965702..05f25bc5 100644 --- a/agent/scripts/test-erc1155-deposit.mjs +++ b/agent/scripts/test-erc1155-deposit.mjs @@ -237,6 +237,366 @@ async function run() { globalThis.fetch = oldFetch; } + const proxyWalletAddress = '0x5555555555555555555555555555555555555555'; + const proxyRelayTxHash = `0x${'3'.repeat(64)}`; + const proxyOnchainTxHash = `0x${'4'.repeat(64)}`; + const proxyTransactionId = 'relayer-proxy-tx-1'; + let proxySubmitBody; + const oldFetchProxy = globalThis.fetch; + try { + globalThis.fetch = async (url, options = {}) => { + const asText = String(url); + if (asText.includes('/relay-payload?')) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + address: proxyWalletAddress, + nonce: '3', + }); + }, + }; + } + + if (asText.endsWith('/submit')) { + proxySubmitBody = JSON.parse(options.body); + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + transactionID: proxyTransactionId, + hash: proxyRelayTxHash, + state: 'STATE_PENDING', + }); + }, + }; + } + + if (asText.includes('/transaction?') && asText.includes(proxyTransactionId)) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify([ + { + transactionID: proxyTransactionId, + hash: proxyRelayTxHash, + state: 'STATE_CONFIRMED', + transactionHash: proxyOnchainTxHash, + }, + ]); + }, + }; + } + + throw new Error(`Unexpected PROXY relayer fetch URL: ${asText}`); + }; + + let proxySignedMessage; + const proxyWalletClient = { + async signMessage({ message }) { + proxySignedMessage = message; + return `0x${'c'.repeat(128)}1b`; + }, + }; + const proxyPublicClient = { + async getChainId() { + return 137; + }, + }; + + const proxyConfig = { + commitmentSafe: config.commitmentSafe, + polymarketRelayerEnabled: true, + polymarketRelayerHost: 'https://relayer-v2.polymarket.com', + polymarketRelayerTxType: 'PROXY', + polymarketBuilderApiKey: 'builder-key', + polymarketBuilderSecret: Buffer.from('builder-secret').toString('base64'), + polymarketBuilderPassphrase: 'builder-passphrase', + polymarketRelayerPollIntervalMs: 0, + polymarketRelayerPollTimeoutMs: 1_000, + }; + + const proxyDepositHash = await makeErc1155Deposit({ + publicClient: proxyPublicClient, + walletClient: proxyWalletClient, + account, + config: proxyConfig, + token, + tokenId: '8', + amount: '4', + data: null, + }); + + assert.equal(proxyDepositHash, proxyOnchainTxHash); + assert.equal(proxySubmitBody.type, 'PROXY'); + assert.equal(proxySubmitBody.from.toLowerCase(), account.address.toLowerCase()); + assert.equal(proxySubmitBody.proxyWallet.toLowerCase(), proxyWalletAddress.toLowerCase()); + assert.equal(proxySubmitBody.signatureParams.chainId, '137'); + assert.equal(proxySubmitBody.nonce, '3'); + assert.ok(proxySignedMessage?.raw); + + const decodedProxy = decodeFunctionData({ + abi: parseAbi([ + 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)', + ]), + data: proxySubmitBody.data, + }); + assert.equal(decodedProxy.args[0].toLowerCase(), proxyWalletAddress.toLowerCase()); + assert.equal(decodedProxy.args[1].toLowerCase(), config.commitmentSafe.toLowerCase()); + assert.equal(decodedProxy.args[2], 8n); + assert.equal(decodedProxy.args[3], 4n); + } finally { + globalThis.fetch = oldFetchProxy; + } + + const safeCreateRelayHash = `0x${'5'.repeat(64)}`; + const safeCreateOnchainHash = `0x${'6'.repeat(64)}`; + const safeActionRelayHash = `0x${'7'.repeat(64)}`; + const safeActionOnchainHash = `0x${'8'.repeat(64)}`; + const safeCreateTransactionId = 'safe-create-1'; + const safeActionTransactionId = 'safe-action-1'; + let safeCreateSubmitBody; + let safeActionSubmitBody; + let submitCount = 0; + let signTypedDataCalled = false; + const oldFetchSafeCreate = globalThis.fetch; + try { + globalThis.fetch = async (url, options = {}) => { + const asText = String(url); + const asLower = asText.toLowerCase(); + + if (asText.includes('/relay-payload?')) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + address: relayerFromAddress, + nonce: '9', + }); + }, + }; + } + + if (asLower.includes('/deployed?') && asLower.includes(relayerFromAddress.toLowerCase())) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ deployed: false }); + }, + }; + } + + if (asText.endsWith('/submit')) { + submitCount += 1; + const body = JSON.parse(options.body); + if (submitCount === 1) { + safeCreateSubmitBody = body; + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + transactionID: safeCreateTransactionId, + hash: safeCreateRelayHash, + state: 'STATE_PENDING', + }); + }, + }; + } + safeActionSubmitBody = body; + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + transactionID: safeActionTransactionId, + hash: safeActionRelayHash, + state: 'STATE_PENDING', + }); + }, + }; + } + + if (asText.includes('/transaction?') && asText.includes(safeCreateTransactionId)) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify([ + { + transactionID: safeCreateTransactionId, + hash: safeCreateRelayHash, + state: 'STATE_CONFIRMED', + transactionHash: safeCreateOnchainHash, + }, + ]); + }, + }; + } + + if (asText.includes('/transaction?') && asText.includes(safeActionTransactionId)) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify([ + { + transactionID: safeActionTransactionId, + hash: safeActionRelayHash, + state: 'STATE_CONFIRMED', + transactionHash: safeActionOnchainHash, + }, + ]); + }, + }; + } + + throw new Error(`Unexpected SAFE-CREATE relayer fetch URL: ${asText}`); + }; + + const safeCreateWalletClient = { + async signTypedData(args) { + signTypedDataCalled = true; + assert.equal(args.primaryType, 'CreateProxy'); + return `0x${'d'.repeat(130)}`; + }, + async signMessage() { + return `0x${'e'.repeat(128)}1b`; + }, + }; + const safeCreatePublicClient = { + async getChainId() { + return 137; + }, + }; + + const safeCreateConfig = { + commitmentSafe: config.commitmentSafe, + polymarketRelayerEnabled: true, + polymarketRelayerHost: 'https://relayer-v2.polymarket.com', + polymarketRelayerTxType: 'SAFE', + polymarketRelayerAutoDeployProxy: true, + polymarketBuilderApiKey: 'builder-key', + polymarketBuilderSecret: Buffer.from('builder-secret').toString('base64'), + polymarketBuilderPassphrase: 'builder-passphrase', + polymarketRelayerPollIntervalMs: 0, + polymarketRelayerPollTimeoutMs: 1_000, + }; + + const safeCreateDepositHash = await makeErc1155Deposit({ + publicClient: safeCreatePublicClient, + walletClient: safeCreateWalletClient, + account, + config: safeCreateConfig, + token, + tokenId: '9', + amount: '5', + data: null, + }); + + assert.equal(safeCreateDepositHash, safeActionOnchainHash); + assert.equal(signTypedDataCalled, true); + assert.equal(safeCreateSubmitBody.type, 'SAFE-CREATE'); + assert.equal(safeCreateSubmitBody.from.toLowerCase(), account.address.toLowerCase()); + assert.equal(safeCreateSubmitBody.proxyWallet.toLowerCase(), relayerFromAddress.toLowerCase()); + assert.equal(safeActionSubmitBody.type, 'SAFE'); + assert.equal(safeActionSubmitBody.proxyWallet.toLowerCase(), relayerFromAddress.toLowerCase()); + } finally { + globalThis.fetch = oldFetchSafeCreate; + } + + const oldFetchMissingTracking = globalThis.fetch; + try { + globalThis.fetch = async (url) => { + const asText = String(url); + const asLower = asText.toLowerCase(); + if (asText.includes('/relay-payload?')) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + address: relayerFromAddress, + nonce: '12', + }); + }, + }; + } + if (asLower.includes('/deployed?') && asLower.includes(relayerFromAddress.toLowerCase())) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ deployed: true }); + }, + }; + } + if (asText.endsWith('/submit')) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({}); + }, + }; + } + throw new Error(`Unexpected missing-tracking fetch URL: ${asText}`); + }; + + await assert.rejects( + () => + makeErc1155Deposit({ + publicClient: { + async getChainId() { + return 137; + }, + }, + walletClient: { + async signMessage() { + return `0x${'f'.repeat(128)}1b`; + }, + }, + account, + config: { + commitmentSafe: config.commitmentSafe, + polymarketRelayerEnabled: true, + polymarketRelayerHost: 'https://relayer-v2.polymarket.com', + polymarketRelayerTxType: 'SAFE', + polymarketRelayerFromAddress: relayerFromAddress, + polymarketBuilderApiKey: 'builder-key', + polymarketBuilderSecret: Buffer.from('builder-secret').toString('base64'), + polymarketBuilderPassphrase: 'builder-passphrase', + polymarketRelayerPollIntervalMs: 0, + polymarketRelayerPollTimeoutMs: 1_000, + }, + token, + tokenId: '10', + amount: '1', + data: null, + }), + /did not return transactionID or txHash/ + ); + } finally { + globalThis.fetch = oldFetchMissingTracking; + } + console.log('[test] makeErc1155Deposit OK'); } diff --git a/agent/src/lib/config.js b/agent/src/lib/config.js index 3b97e9c3..f20187d9 100644 --- a/agent/src/lib/config.js +++ b/agent/src/lib/config.js @@ -90,6 +90,12 @@ function buildConfig() { polymarketRelayerFromAddress: process.env.POLYMARKET_RELAYER_FROM_ADDRESS ? getAddress(process.env.POLYMARKET_RELAYER_FROM_ADDRESS) : undefined, + polymarketRelayerSafeFactory: process.env.POLYMARKET_RELAYER_SAFE_FACTORY + ? getAddress(process.env.POLYMARKET_RELAYER_SAFE_FACTORY) + : undefined, + polymarketRelayerProxyFactory: process.env.POLYMARKET_RELAYER_PROXY_FACTORY + ? getAddress(process.env.POLYMARKET_RELAYER_PROXY_FACTORY) + : undefined, polymarketRelayerResolveProxyAddress: process.env.POLYMARKET_RELAYER_RESOLVE_PROXY_ADDRESS === undefined ? true diff --git a/agent/src/lib/polymarket-relayer.js b/agent/src/lib/polymarket-relayer.js index bc01e845..5c5bc248 100644 --- a/agent/src/lib/polymarket-relayer.js +++ b/agent/src/lib/polymarket-relayer.js @@ -1010,6 +1010,56 @@ async function resolveProxyWalletAddress({ return derivedAddress; } +async function resolveRelayerProxyWallet({ + publicClient, + account, + config, + proxyWallet, +}) { + if (!isPolymarketRelayerEnabled(config)) { + throw new Error('Polymarket relayer is disabled (POLYMARKET_RELAYER_ENABLED=false).'); + } + if (!publicClient) { + throw new Error('publicClient is required to resolve relayer proxy wallet.'); + } + + const signerAddress = getAddress(account?.address); + const chainId = Number( + config?.polymarketRelayerChainId ?? + (typeof publicClient.getChainId === 'function' + ? await publicClient.getChainId() + : undefined) + ); + + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error( + 'Unable to resolve chainId for relayer transaction. Set POLYMARKET_RELAYER_CHAIN_ID.' + ); + } + + const txType = normalizeRelayerTxType(config?.polymarketRelayerTxType); + const resolvedProxyWallet = await resolveProxyWalletAddress({ + config, + signerAddress, + chainId, + txType, + explicitProxyWallet: proxyWallet, + }); + + if (!resolvedProxyWallet) { + throw new Error( + 'Unable to resolve relayer proxy wallet address. Set POLYMARKET_RELAYER_FROM_ADDRESS or POLYMARKET_CLOB_ADDRESS, or enable POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY.' + ); + } + + return { + signerAddress, + chainId, + txType, + proxyWallet: resolvedProxyWallet, + }; +} + async function ensureSafeDeployed({ config, walletClient, @@ -1023,10 +1073,16 @@ async function ensureSafeDeployed({ safeAddress, }); - if (deployed === true || deployed === null) { + if (deployed === true) { return; } + if (deployed === null) { + throw new Error( + `Unable to verify whether SAFE proxy wallet ${safeAddress} is deployed via relayer /deployed endpoint.` + ); + } + if (!config?.polymarketRelayerAutoDeployProxy) { throw new Error( `SAFE proxy wallet ${safeAddress} appears undeployed. Enable POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY=true to deploy automatically or deploy it out of band.` @@ -1078,34 +1134,16 @@ async function relayPolymarketTransaction({ throw new Error('walletClient is required for relayer transaction submission.'); } - const signerAddress = getAddress(account?.address); - const chainId = Number( - config?.polymarketRelayerChainId ?? - (typeof publicClient.getChainId === 'function' - ? await publicClient.getChainId() - : undefined) - ); - - if (!Number.isInteger(chainId) || chainId <= 0) { - throw new Error( - 'Unable to resolve chainId for relayer transaction. Set POLYMARKET_RELAYER_CHAIN_ID.' - ); - } - - const txType = normalizeRelayerTxType(config?.polymarketRelayerTxType); - const resolvedProxyWallet = await resolveProxyWalletAddress({ + const resolved = await resolveRelayerProxyWallet({ + publicClient, + account, config, - signerAddress, - chainId, - txType, - explicitProxyWallet: proxyWallet, + proxyWallet, }); - - if (!resolvedProxyWallet) { - throw new Error( - 'Unable to resolve relayer proxy wallet address. Set POLYMARKET_RELAYER_FROM_ADDRESS or POLYMARKET_CLOB_ADDRESS, or enable POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY.' - ); - } + const signerAddress = resolved.signerAddress; + const chainId = resolved.chainId; + const txType = resolved.txType; + const resolvedProxyWallet = resolved.proxyWallet; if (txType === RELAYER_TX_TYPE.SAFE) { const expectedSafeAddress = deriveSafeAddress({ @@ -1173,11 +1211,16 @@ async function relayPolymarketTransaction({ config, transactionRequest: signed.request, }); + if (!submission.transactionId && !submission.relayTxHash) { + throw new Error( + 'Relayer submission did not return transactionID or txHash; cannot track transaction lifecycle.' + ); + } const waited = await waitForRelayerTransaction({ config, transactionId: submission.transactionId, - relayTxHash: submission.relayTxHash ?? signed.txHash, + relayTxHash: submission.relayTxHash, }); let transactionHash = waited.transactionHash ?? submission.transactionHash; @@ -1215,4 +1258,5 @@ export { getRelayerProxyAddress, isPolymarketRelayerEnabled, relayPolymarketTransaction, + resolveRelayerProxyWallet, }; diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index b124c524..2345c075 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -16,6 +16,7 @@ import { normalizeAssertion } from './og.js'; import { isPolymarketRelayerEnabled, relayPolymarketTransaction, + resolveRelayerProxyWallet, } from './polymarket-relayer.js'; import { normalizeHashOrNull, summarizeViemError } from './utils.js'; @@ -672,11 +673,14 @@ async function makeErc1155Deposit({ const transferData = data ?? '0x'; if (isPolymarketRelayerEnabled(config)) { - const relayerFromAddress = config?.polymarketRelayerFromAddress ?? config?.polymarketClobAddress; + let relayerFromAddress = config?.polymarketRelayerFromAddress ?? config?.polymarketClobAddress; if (!relayerFromAddress) { - throw new Error( - 'Polymarket relayer ERC1155 deposit requires POLYMARKET_RELAYER_FROM_ADDRESS or POLYMARKET_CLOB_ADDRESS.' - ); + const resolved = await resolveRelayerProxyWallet({ + publicClient, + account, + config, + }); + relayerFromAddress = resolved.proxyWallet; } const transferCallData = encodeFunctionData({ abi: erc1155TransferAbi, From 4a473340326d7c7f3ad9421e3f9ee9de7a81dbef Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 15:07:35 -0800 Subject: [PATCH 163/174] another fix Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 96 +++++++++++++--- .../copy-trading/test-copy-trading-agent.mjs | 107 ++++++++++++++++++ 2 files changed, 187 insertions(+), 16 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 3838b06c..9272606f 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -17,6 +17,7 @@ import { parseFiniteNumber, } from '../../../agent/src/lib/utils.js'; import { getAlwaysEmitBalanceSnapshotPollingOptions } from '../../../agent/src/lib/polling.js'; +import { resolveRelayerProxyWallet } from '../../../agent/src/lib/polymarket-relayer.js'; const COPY_BPS = 9900n; const FEE_BPS = 100n; @@ -98,6 +99,60 @@ function getClobAuthAddress({ config, accountAddress }) { ); } +async function resolveTokenHolderAddress({ + publicClient, + config, + account, +}) { + const fallbackAddress = + getClobAuthAddress({ + config, + accountAddress: account.address, + }) ?? normalizeAddress(account.address); + + if (!config?.polymarketRelayerEnabled) { + return { + tokenHolderAddress: fallbackAddress, + tokenHolderResolutionError: null, + }; + } + + const configuredRelayerAddress = + normalizeAddress(config?.polymarketRelayerFromAddress) ?? + normalizeAddress(config?.polymarketClobAddress); + if (configuredRelayerAddress) { + return { + tokenHolderAddress: configuredRelayerAddress, + tokenHolderResolutionError: null, + }; + } + + try { + const resolved = await resolveRelayerProxyWallet({ + publicClient, + account, + config, + }); + const resolvedProxyWallet = normalizeAddress(resolved?.proxyWallet); + if (!resolvedProxyWallet) { + return { + tokenHolderAddress: null, + tokenHolderResolutionError: + 'Relayer proxy wallet resolution returned an invalid address.', + }; + } + return { + tokenHolderAddress: resolvedProxyWallet, + tokenHolderResolutionError: null, + }; + } catch (error) { + return { + tokenHolderAddress: null, + tokenHolderResolutionError: error?.message ?? String(error), + }; + } +} + function extractOrderSummary(payload) { const order = payload?.order && typeof payload.order === 'object' @@ -520,31 +575,39 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe } catch (error) { tradeFetchError = error?.message ?? String(error); } - const tokenHolderAddress = - getClobAuthAddress({ - config, - accountAddress: account.address, - }) ?? normalizeAddress(account.address); + const { tokenHolderAddress, tokenHolderResolutionError } = await resolveTokenHolderAddress({ + publicClient, + config, + account, + }); - const [safeCollateralWei, yesBalance, noBalance] = await Promise.all([ - publicClient.readContract({ - address: policy.collateralToken, - abi: erc20Abi, - functionName: 'balanceOf', - args: [config.commitmentSafe], - }), - publicClient.readContract({ + const safeCollateralPromise = publicClient.readContract({ + address: policy.collateralToken, + abi: erc20Abi, + functionName: 'balanceOf', + args: [config.commitmentSafe], + }); + const yesBalancePromise = tokenHolderAddress + ? publicClient.readContract({ address: policy.ctfContract, abi: erc1155Abi, functionName: 'balanceOf', args: [tokenHolderAddress, BigInt(policy.yesTokenId)], - }), - publicClient.readContract({ + }) + : Promise.resolve(0n); + const noBalancePromise = tokenHolderAddress + ? publicClient.readContract({ address: policy.ctfContract, abi: erc1155Abi, functionName: 'balanceOf', args: [tokenHolderAddress, BigInt(policy.noTokenId)], - }), + }) + : Promise.resolve(0n); + + const [safeCollateralWei, yesBalance, noBalance] = await Promise.all([ + safeCollateralPromise, + yesBalancePromise, + noBalancePromise, ]); const amounts = calculateCopyAmounts(safeCollateralWei); @@ -699,6 +762,7 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe ), tradeFetchError, orderFillCheckError, + tokenHolderResolutionError, }); return outSignals; diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index 3c17e806..31f02ed4 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -17,6 +17,7 @@ const TEST_ACCOUNT = '0x1111111111111111111111111111111111111111'; const TEST_SAFE = '0x2222222222222222222222222222222222222222'; const TEST_SOURCE_USER = '0x3333333333333333333333333333333333333333'; const TEST_CLOB_PROXY = '0x4444444444444444444444444444444444444444'; +const TEST_RELAYER_PROXY = '0x5555555555555555555555555555555555555555'; const TEST_PROPOSAL_HASH = `0x${'a'.repeat(64)}`; const OTHER_PROPOSAL_HASH = `0x${'b'.repeat(64)}`; const TEST_TX_HASH = `0x${'c'.repeat(64)}`; @@ -1088,6 +1089,111 @@ async function runTokenBalancesUseClobAddressTest() { } } +async function runTokenBalancesUseResolvedRelayerProxyTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + const builderSecret = Buffer.from('test-builder-secret').toString('base64'); + globalThis.fetch = async (url) => { + const asText = String(url); + if (asText.includes('data-api.polymarket.com/activity')) { + return { + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + async text() { + return JSON.stringify([ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]); + }, + }; + } + if (asText.includes('/relay-payload?')) { + return { + ok: true, + async text() { + return JSON.stringify({ + address: TEST_RELAYER_PROXY, + }); + }, + }; + } + throw new Error(`Unexpected fetch URL in relayer test: ${asText}`); + }; + + const erc1155BalanceCallAddresses = []; + const outSignals = await enrichSignals([], { + publicClient: { + async getChainId() { + return 137; + }, + async readContract({ args }) { + if (args.length === 1) { + return 1_000_000n; + } + erc1155BalanceCallAddresses.push(String(args[0]).toLowerCase()); + return 1n; + }, + }, + config: { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + polymarketRelayerEnabled: true, + polymarketRelayerHost: 'https://relayer-v2.polymarket.com', + polymarketRelayerTxType: 'SAFE', + polymarketBuilderApiKey: 'builder-key', + polymarketBuilderSecret: builderSecret, + polymarketBuilderPassphrase: 'builder-passphrase', + }, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + assert.equal(erc1155BalanceCallAddresses.length, 2); + assert.equal(erc1155BalanceCallAddresses[0], TEST_RELAYER_PROXY.toLowerCase()); + assert.equal(erc1155BalanceCallAddresses[1], TEST_RELAYER_PROXY.toLowerCase()); + const copySignal = outSignals.find((signal) => signal.kind === 'copyTradingState'); + assert.equal(copySignal.balances.tokenHolderAddress, TEST_RELAYER_PROXY.toLowerCase()); + assert.equal(copySignal.tokenHolderResolutionError, null); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function run() { runPromptTest(); runMathTests(); @@ -1101,6 +1207,7 @@ async function run() { await runSubmissionWithoutHashesDoesNotWedgeTest(); await runFetchLatestBuyTradeTest(); await runTokenBalancesUseClobAddressTest(); + await runTokenBalancesUseResolvedRelayerProxyTest(); console.log('[test] copy-trading agent OK'); } From f9e33ca0fb3b5fb860883388542f4110f281910a Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 15:20:55 -0800 Subject: [PATCH 164/174] clob settlement wallet and relayer deposit wallet should be the same if using relayer Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 27 +++- .../copy-trading/test-copy-trading-agent.mjs | 123 ++++++++++++++++++ 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 9272606f..06ee52fa 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -575,11 +575,28 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe } catch (error) { tradeFetchError = error?.message ?? String(error); } + const clobAuthAddress = + getClobAuthAddress({ + config, + accountAddress: account.address, + }) ?? normalizeAddress(account.address); const { tokenHolderAddress, tokenHolderResolutionError } = await resolveTokenHolderAddress({ publicClient, config, account, }); + let walletAlignmentError = null; + if (config?.polymarketRelayerEnabled) { + if (!clobAuthAddress) { + walletAlignmentError = 'Unable to resolve CLOB auth address for relayer mode.'; + } else if (!tokenHolderAddress) { + walletAlignmentError = + tokenHolderResolutionError ?? 'Unable to resolve relayer token-holder address.'; + } else if (clobAuthAddress !== tokenHolderAddress) { + walletAlignmentError = + `POLYMARKET_CLOB_ADDRESS (${clobAuthAddress}) must match relayer proxy wallet (${tokenHolderAddress}) when POLYMARKET_RELAYER_ENABLED=true.`; + } + } const safeCollateralPromise = publicClient.readContract({ address: policy.collateralToken, @@ -616,6 +633,7 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe latestTrade.side === 'BUY' && latestTrade.id !== copyTradingState.seenSourceTradeId && !copyTradingState.activeSourceTradeId && + !walletAlignmentError && BigInt(amounts.copyAmountWei) > 0n ) { const targetTokenId = latestTrade.outcome === 'YES' ? policy.yesTokenId : policy.noTokenId; @@ -629,16 +647,13 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe let orderFillCheckError; if ( + !walletAlignmentError && copyTradingState.activeSourceTradeId && copyTradingState.orderSubmitted && !copyTradingState.tokenDeposited && !copyTradingState.copyOrderFilled && copyTradingState.copyOrderId ) { - const clobAuthAddress = getClobAuthAddress({ - config, - accountAddress: account.address, - }); if (hasClobCredentials(config) && clobAuthAddress) { try { const signingAddress = clobAuthAddress; @@ -763,6 +778,7 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe tradeFetchError, orderFillCheckError, tokenHolderResolutionError, + walletAlignmentError, }); return outSignals; @@ -789,6 +805,9 @@ async function validateToolCalls({ const state = copySignal.state ?? {}; const activeTokenBalance = BigInt(copySignal.balances?.activeTokenBalance ?? 0); const pendingProposal = Boolean(onchainPendingProposal || copySignal.pendingProposal); + if (copySignal.walletAlignmentError) { + throw new Error(copySignal.walletAlignmentError); + } for (const call of toolCalls) { if (call.name === 'dispute_assertion') { diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index 31f02ed4..f49995e7 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -1167,6 +1167,7 @@ async function runTokenBalancesUseResolvedRelayerProxyTest() { polymarketRelayerEnabled: true, polymarketRelayerHost: 'https://relayer-v2.polymarket.com', polymarketRelayerTxType: 'SAFE', + polymarketClobAddress: TEST_RELAYER_PROXY, polymarketBuilderApiKey: 'builder-key', polymarketBuilderSecret: builderSecret, polymarketBuilderPassphrase: 'builder-passphrase', @@ -1181,6 +1182,127 @@ async function runTokenBalancesUseResolvedRelayerProxyTest() { const copySignal = outSignals.find((signal) => signal.kind === 'copyTradingState'); assert.equal(copySignal.balances.tokenHolderAddress, TEST_RELAYER_PROXY.toLowerCase()); assert.equal(copySignal.tokenHolderResolutionError, null); + assert.equal(copySignal.walletAlignmentError, null); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + +async function runRelayerWalletMismatchIsBlockedTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + const builderSecret = Buffer.from('test-builder-secret').toString('base64'); + globalThis.fetch = async (url) => { + const asText = String(url); + if (asText.includes('data-api.polymarket.com/activity')) { + return { + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + async text() { + return JSON.stringify([ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]); + }, + }; + } + if (asText.includes('/relay-payload?')) { + return { + ok: true, + async text() { + return JSON.stringify({ + address: TEST_RELAYER_PROXY, + }); + }, + }; + } + throw new Error(`Unexpected fetch URL in relayer mismatch test: ${asText}`); + }; + + const outSignals = await enrichSignals([], { + publicClient: { + async getChainId() { + return 137; + }, + async readContract({ args }) { + if (args.length === 1) { + return 1_000_000n; + } + return 1n; + }, + }, + config: { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + polymarketRelayerEnabled: true, + polymarketRelayerHost: 'https://relayer-v2.polymarket.com', + polymarketRelayerTxType: 'SAFE', + polymarketRelayerFromAddress: TEST_RELAYER_PROXY, + polymarketClobAddress: TEST_CLOB_PROXY, + polymarketBuilderApiKey: 'builder-key', + polymarketBuilderSecret: builderSecret, + polymarketBuilderPassphrase: 'builder-passphrase', + }, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + const copySignal = outSignals.find((signal) => signal.kind === 'copyTradingState'); + assert.ok(copySignal.walletAlignmentError.includes('POLYMARKET_CLOB_ADDRESS')); + assert.equal(copySignal.state.activeSourceTradeId, null); + + await assert.rejects( + () => + validateToolCalls({ + toolCalls: [ + { + callId: 'order', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: {}, + }, + ], + signals: [copySignal], + config: {}, + agentAddress: TEST_ACCOUNT, + onchainPendingProposal: false, + }), + /must match relayer proxy wallet/ + ); } finally { for (const key of envKeys) { if (oldEnv[key] === undefined) { @@ -1208,6 +1330,7 @@ async function run() { await runFetchLatestBuyTradeTest(); await runTokenBalancesUseClobAddressTest(); await runTokenBalancesUseResolvedRelayerProxyTest(); + await runRelayerWalletMismatchIsBlockedTest(); console.log('[test] copy-trading agent OK'); } From 0cf6f52a67d98a81920c1fee8d1fc831e729dc0d Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 15:24:42 -0800 Subject: [PATCH 165/174] allow disputes even if clob and relayer wallets are not aligned Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 8 +++++--- .../copy-trading/test-copy-trading-agent.mjs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 06ee52fa..44a24935 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -805,9 +805,7 @@ async function validateToolCalls({ const state = copySignal.state ?? {}; const activeTokenBalance = BigInt(copySignal.balances?.activeTokenBalance ?? 0); const pendingProposal = Boolean(onchainPendingProposal || copySignal.pendingProposal); - if (copySignal.walletAlignmentError) { - throw new Error(copySignal.walletAlignmentError); - } + const walletAlignmentError = copySignal.walletAlignmentError; for (const call of toolCalls) { if (call.name === 'dispute_assertion') { @@ -815,6 +813,10 @@ async function validateToolCalls({ continue; } + if (walletAlignmentError) { + throw new Error(walletAlignmentError); + } + if (call.name === 'post_bond_and_propose') { continue; } diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index f49995e7..0e6a50a7 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -1303,6 +1303,22 @@ async function runRelayerWalletMismatchIsBlockedTest() { }), /must match relayer proxy wallet/ ); + + const disputeValidated = await validateToolCalls({ + toolCalls: [ + { + callId: 'dispute', + name: 'dispute_assertion', + arguments: {}, + }, + ], + signals: [copySignal], + config: {}, + agentAddress: TEST_ACCOUNT, + onchainPendingProposal: false, + }); + assert.equal(disputeValidated.length, 1); + assert.equal(disputeValidated[0].name, 'dispute_assertion'); } finally { for (const key of envKeys) { if (oldEnv[key] === undefined) { From f8c93a4193bdb99e55582cb20f7a9a5df824c01a Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 15:28:40 -0800 Subject: [PATCH 166/174] address possible error text issue Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 44a24935..60459c96 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -575,11 +575,12 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe } catch (error) { tradeFetchError = error?.message ?? String(error); } - const clobAuthAddress = - getClobAuthAddress({ - config, - accountAddress: account.address, - }) ?? normalizeAddress(account.address); + const configuredClobAddress = normalizeAddress(config?.polymarketClobAddress); + const runtimeSignerAddress = normalizeAddress(account.address); + const clobAuthAddress = configuredClobAddress ?? runtimeSignerAddress; + const clobAuthAddressLabel = configuredClobAddress + ? 'POLYMARKET_CLOB_ADDRESS' + : 'runtime signer address (fallback for POLYMARKET_CLOB_ADDRESS)'; const { tokenHolderAddress, tokenHolderResolutionError } = await resolveTokenHolderAddress({ publicClient, config, @@ -594,7 +595,7 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe tokenHolderResolutionError ?? 'Unable to resolve relayer token-holder address.'; } else if (clobAuthAddress !== tokenHolderAddress) { walletAlignmentError = - `POLYMARKET_CLOB_ADDRESS (${clobAuthAddress}) must match relayer proxy wallet (${tokenHolderAddress}) when POLYMARKET_RELAYER_ENABLED=true.`; + `${clobAuthAddressLabel} (${clobAuthAddress}) must match relayer proxy wallet (${tokenHolderAddress}) when POLYMARKET_RELAYER_ENABLED=true.`; } } From 8f2e913fa1c4c2c1d7d6ad514b24ccc4072bdb0b Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 15:30:40 -0800 Subject: [PATCH 167/174] avoid blocking disputes due to a wallet mismatch error Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 11 ++++++---- .../copy-trading/test-copy-trading-agent.mjs | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 60459c96..63273fbf 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -807,6 +807,13 @@ async function validateToolCalls({ const activeTokenBalance = BigInt(copySignal.balances?.activeTokenBalance ?? 0); const pendingProposal = Boolean(onchainPendingProposal || copySignal.pendingProposal); const walletAlignmentError = copySignal.walletAlignmentError; + if (walletAlignmentError) { + const disputeCalls = toolCalls.filter((call) => call?.name === 'dispute_assertion'); + if (disputeCalls.length > 0) { + return disputeCalls; + } + throw new Error(walletAlignmentError); + } for (const call of toolCalls) { if (call.name === 'dispute_assertion') { @@ -814,10 +821,6 @@ async function validateToolCalls({ continue; } - if (walletAlignmentError) { - throw new Error(walletAlignmentError); - } - if (call.name === 'post_bond_and_propose') { continue; } diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index 0e6a50a7..7dfd25d0 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -1319,6 +1319,27 @@ async function runRelayerWalletMismatchIsBlockedTest() { }); assert.equal(disputeValidated.length, 1); assert.equal(disputeValidated[0].name, 'dispute_assertion'); + + const mixedValidated = await validateToolCalls({ + toolCalls: [ + { + callId: 'order', + name: 'polymarket_clob_build_sign_and_place_order', + arguments: {}, + }, + { + callId: 'dispute', + name: 'dispute_assertion', + arguments: {}, + }, + ], + signals: [copySignal], + config: {}, + agentAddress: TEST_ACCOUNT, + onchainPendingProposal: false, + }); + assert.equal(mixedValidated.length, 1); + assert.equal(mixedValidated[0].name, 'dispute_assertion'); } finally { for (const key of envKeys) { if (oldEnv[key] === undefined) { From 009cc9495f66d4f245fa8727b795a48a957f0c88 Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 15:44:31 -0800 Subject: [PATCH 168/174] resolve potential issue of an invalid fallback to eoa when in safe relayer mode Signed-off-by: John Shutt --- agent-library/agents/copy-trading/agent.js | 3 +- agent/scripts/test-erc1155-deposit.mjs | 123 +++++++++++++++++++++ agent/src/lib/polymarket-relayer.js | 5 +- agent/src/lib/tx.js | 2 +- 4 files changed, 126 insertions(+), 7 deletions(-) diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 63273fbf..47857b49 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -118,8 +118,7 @@ async function resolveTokenHolderAddress({ } const configuredRelayerAddress = - normalizeAddress(config?.polymarketRelayerFromAddress) ?? - normalizeAddress(config?.polymarketClobAddress); + normalizeAddress(config?.polymarketRelayerFromAddress); if (configuredRelayerAddress) { return { tokenHolderAddress: configuredRelayerAddress, diff --git a/agent/scripts/test-erc1155-deposit.mjs b/agent/scripts/test-erc1155-deposit.mjs index 05f25bc5..1509adcb 100644 --- a/agent/scripts/test-erc1155-deposit.mjs +++ b/agent/scripts/test-erc1155-deposit.mjs @@ -237,6 +237,129 @@ async function run() { globalThis.fetch = oldFetch; } + const clobOnlyAddress = '0x3333333333333333333333333333333333333333'; + const clobIgnoredRelayTxHash = `0x${'9'.repeat(64)}`; + const clobIgnoredOnchainTxHash = `0x${'a'.repeat(64)}`; + const clobIgnoredTransactionId = 'relayer-safe-from-resolved-1'; + let clobIgnoredSubmitBody; + const oldFetchClobIgnored = globalThis.fetch; + try { + globalThis.fetch = async (url, options = {}) => { + const asText = String(url); + const asLower = asText.toLowerCase(); + if (asLower.includes('/deployed?') && asLower.includes(relayerFromAddress.toLowerCase())) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ deployed: true }); + }, + }; + } + + if (asText.includes('/relay-payload?')) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + address: relayerFromAddress, + nonce: '13', + }); + }, + }; + } + + if (asText.endsWith('/submit')) { + clobIgnoredSubmitBody = JSON.parse(options.body); + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + transactionID: clobIgnoredTransactionId, + hash: clobIgnoredRelayTxHash, + state: 'STATE_PENDING', + }); + }, + }; + } + + if ( + asText.includes('/transaction?') && + (asText.includes(`id=${clobIgnoredTransactionId}`) || + asText.includes(`transactionID=${clobIgnoredTransactionId}`)) + ) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify([ + { + transactionID: clobIgnoredTransactionId, + hash: clobIgnoredRelayTxHash, + state: 'STATE_CONFIRMED', + transactionHash: clobIgnoredOnchainTxHash, + }, + ]); + }, + }; + } + + throw new Error(`Unexpected relayer fetch URL (CLOB ignored test): ${asText}`); + }; + + const clobIgnoredDepositHash = await makeErc1155Deposit({ + publicClient: { + async getChainId() { + return 137; + }, + }, + walletClient: { + async signMessage() { + return `0x${'b'.repeat(128)}1b`; + }, + }, + account, + config: { + commitmentSafe: config.commitmentSafe, + polymarketRelayerEnabled: true, + polymarketRelayerHost: 'https://relayer-v2.polymarket.com', + polymarketRelayerTxType: 'SAFE', + polymarketClobAddress: clobOnlyAddress, + polymarketBuilderApiKey: 'builder-key', + polymarketBuilderSecret: Buffer.from('builder-secret').toString('base64'), + polymarketBuilderPassphrase: 'builder-passphrase', + polymarketRelayerPollIntervalMs: 0, + polymarketRelayerPollTimeoutMs: 1_000, + }, + token, + tokenId: '11', + amount: '2', + data: null, + }); + + assert.equal(clobIgnoredDepositHash, clobIgnoredOnchainTxHash); + assert.equal(clobIgnoredSubmitBody.proxyWallet.toLowerCase(), relayerFromAddress.toLowerCase()); + assert.notEqual(clobIgnoredSubmitBody.proxyWallet.toLowerCase(), clobOnlyAddress.toLowerCase()); + + const decodedClobIgnored = decodeFunctionData({ + abi: parseAbi([ + 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)', + ]), + data: clobIgnoredSubmitBody.data, + }); + assert.equal(decodedClobIgnored.functionName, 'safeTransferFrom'); + assert.equal(decodedClobIgnored.args[0].toLowerCase(), relayerFromAddress.toLowerCase()); + assert.notEqual(decodedClobIgnored.args[0].toLowerCase(), clobOnlyAddress.toLowerCase()); + } finally { + globalThis.fetch = oldFetchClobIgnored; + } + const proxyWalletAddress = '0x5555555555555555555555555555555555555555'; const proxyRelayTxHash = `0x${'3'.repeat(64)}`; const proxyOnchainTxHash = `0x${'4'.repeat(64)}`; diff --git a/agent/src/lib/polymarket-relayer.js b/agent/src/lib/polymarket-relayer.js index 5c5bc248..804e637c 100644 --- a/agent/src/lib/polymarket-relayer.js +++ b/agent/src/lib/polymarket-relayer.js @@ -960,9 +960,6 @@ async function resolveProxyWalletAddress({ if (config?.polymarketRelayerFromAddress) { return getAddress(config.polymarketRelayerFromAddress); } - if (config?.polymarketClobAddress) { - return getAddress(config.polymarketClobAddress); - } const resolveProxyAddress = config?.polymarketRelayerResolveProxyAddress !== false; if (resolveProxyAddress) { @@ -1048,7 +1045,7 @@ async function resolveRelayerProxyWallet({ if (!resolvedProxyWallet) { throw new Error( - 'Unable to resolve relayer proxy wallet address. Set POLYMARKET_RELAYER_FROM_ADDRESS or POLYMARKET_CLOB_ADDRESS, or enable POLYMARKET_RELAYER_AUTO_DEPLOY_PROXY.' + 'Unable to resolve relayer proxy wallet address. Set POLYMARKET_RELAYER_FROM_ADDRESS, or enable relayer proxy resolution/auto-deploy.' ); } diff --git a/agent/src/lib/tx.js b/agent/src/lib/tx.js index 2345c075..458c8d59 100644 --- a/agent/src/lib/tx.js +++ b/agent/src/lib/tx.js @@ -673,7 +673,7 @@ async function makeErc1155Deposit({ const transferData = data ?? '0x'; if (isPolymarketRelayerEnabled(config)) { - let relayerFromAddress = config?.polymarketRelayerFromAddress ?? config?.polymarketClobAddress; + let relayerFromAddress = config?.polymarketRelayerFromAddress; if (!relayerFromAddress) { const resolved = await resolveRelayerProxyWallet({ publicClient, From 8a50a0b87b4e3f1baed75cb1d13fc373ffba82af Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 15:59:01 -0800 Subject: [PATCH 169/174] more fixes for safe relayer implementation Signed-off-by: John Shutt --- agent-library/README.md | 2 +- agent-library/agents/copy-trading/agent.js | 38 ++++-- .../agents/copy-trading/commitment.txt | 4 +- .../copy-trading/test-copy-trading-agent.mjs | 7 ++ agent/scripts/test-erc1155-deposit.mjs | 117 ++++++++++++++++++ agent/src/lib/polymarket-relayer.js | 5 +- 6 files changed, 160 insertions(+), 13 deletions(-) diff --git a/agent-library/README.md b/agent-library/README.md index 96df8e32..08569ad0 100644 --- a/agent-library/README.md +++ b/agent-library/README.md @@ -14,4 +14,4 @@ To add a new agent: Example agents: - `agent-library/agents/default/`: generic agent using the commitment text. - `agent-library/agents/timelock-withdraw/`: timelock withdrawal agent that only withdraws to its own address after the timelock. -- `agent-library/agents/copy-trading/`: copy-trading agent for one configured source trader + market; it reacts to BUY trades only, submits a 99%-of-Safe collateral CLOB order from the configured trading wallet, waits for fill/token receipt, deposits YES/NO tokens to the Safe (direct onchain or via Polymarket relayer), then proposes reimbursement to the agent wallet for the full Safe collateral snapshot captured at trigger time (1% implied agent fee via reduced copy size). +- `agent-library/agents/copy-trading/`: copy-trading agent for one configured source trader + market; it reacts to BUY trades only, submits a 99%-of-Safe collateral CLOB order from the configured trading wallet, waits for fill/token receipt, deposits YES/NO tokens to the Safe (direct onchain or via Polymarket relayer), then proposes reimbursement to that same trading wallet for the full Safe collateral snapshot captured at trigger time (1% implied agent fee via reduced copy size). diff --git a/agent-library/agents/copy-trading/agent.js b/agent-library/agents/copy-trading/agent.js index 47857b49..44ab29b2 100644 --- a/agent-library/agents/copy-trading/agent.js +++ b/agent-library/agents/copy-trading/agent.js @@ -34,6 +34,7 @@ let copyTradingState = { activeTokenId: null, copyTradeAmountWei: null, reimbursementAmountWei: null, + reimbursementRecipientAddress: null, copyOrderId: null, copyOrderStatus: null, copyOrderFilled: false, @@ -272,13 +273,19 @@ async function fetchRelatedClobTrades({ function findMatchingReimbursementProposalHash({ signals, policy, - agentAddress, + proposerAddress, + recipientAddress, reimbursementAmountWei, }) { const normalizedCollateralToken = normalizeAddress(policy?.collateralToken); - const normalizedAgentAddress = normalizeAddress(agentAddress); + const normalizedProposerAddress = normalizeAddress(proposerAddress); + const normalizedRecipientAddress = normalizeAddress(recipientAddress); const normalizedAmount = BigInt(reimbursementAmountWei ?? 0); - if (!normalizedCollateralToken || !normalizedAgentAddress || normalizedAmount <= 0n) { + if ( + !normalizedCollateralToken || + !normalizedRecipientAddress || + normalizedAmount <= 0n + ) { return null; } @@ -288,7 +295,7 @@ function findMatchingReimbursementProposalHash({ if (!signalHash) continue; const proposer = normalizeAddress(signal.proposer); - if (proposer && proposer !== normalizedAgentAddress) continue; + if (normalizedProposerAddress && proposer && proposer !== normalizedProposerAddress) continue; const transactions = Array.isArray(signal.transactions) ? signal.transactions : []; for (const tx of transactions) { @@ -300,7 +307,7 @@ function findMatchingReimbursementProposalHash({ if (value !== 0n) continue; const decoded = decodeErc20TransferCallData(tx?.data); if (!decoded) continue; - if (decoded.to !== normalizedAgentAddress) continue; + if (decoded.to !== normalizedRecipientAddress) continue; if (decoded.amount !== normalizedAmount) continue; return signalHash; } @@ -478,6 +485,7 @@ function activateTradeCandidate({ tokenId, copyTradeAmountWei, reimbursementAmountWei, + reimbursementRecipientAddress, }) { copyTradingState.activeSourceTradeId = trade.id; copyTradingState.activeTradeSide = trade.side; @@ -486,6 +494,7 @@ function activateTradeCandidate({ copyTradingState.activeTokenId = tokenId; copyTradingState.copyTradeAmountWei = copyTradeAmountWei; copyTradingState.reimbursementAmountWei = reimbursementAmountWei; + copyTradingState.reimbursementRecipientAddress = reimbursementRecipientAddress; copyTradingState.copyOrderId = null; copyTradingState.copyOrderStatus = null; copyTradingState.copyOrderFilled = false; @@ -511,6 +520,7 @@ function clearActiveTrade({ markSeen = false } = {}) { copyTradingState.activeTokenId = null; copyTradingState.copyTradeAmountWei = null; copyTradingState.reimbursementAmountWei = null; + copyTradingState.reimbursementRecipientAddress = null; copyTradingState.copyOrderId = null; copyTradingState.copyOrderStatus = null; copyTradingState.copyOrderFilled = false; @@ -539,7 +549,7 @@ function getSystemPrompt({ proposeEnabled, disputeEnabled, commitmentText }) { 'You are a copy-trading commitment agent.', 'Copy only BUY trades from the configured source user and configured market.', 'Trade size must be exactly 99% of Safe collateral at detection time. Keep 1% in the Safe as fee.', - 'Flow must stay simple: place CLOB order from your configured trading wallet, wait for CLOB fill confirmation and YES/NO token receipt, deposit tokens to Safe, then propose reimbursement transfer to agentAddress.', + 'Flow must stay simple: place CLOB order from your configured trading wallet, wait for CLOB fill confirmation and YES/NO token receipt, deposit tokens to Safe, then propose reimbursement transfer to the same wallet that funded the copy trade.', 'Never trade more than 99% of Safe collateral. Reimburse exactly the stored reimbursement amount (full Safe collateral at detection).', 'Use polymarket_clob_build_sign_and_place_order for order placement, make_erc1155_deposit for YES/NO deposit, and build_og_transactions for reimbursement transfer.', 'If preconditions are not met, return ignore.', @@ -642,6 +652,7 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe tokenId: targetTokenId, copyTradeAmountWei: amounts.copyAmountWei, reimbursementAmountWei: amounts.safeBalanceWei, + reimbursementRecipientAddress: clobAuthAddress, }); } @@ -710,7 +721,11 @@ async function enrichSignals(signals, { publicClient, config, account, onchainPe const recoveredHash = findMatchingReimbursementProposalHash({ signals: outSignals, policy, - agentAddress: account.address, + proposerAddress: account.address, + recipientAddress: + normalizeAddress(copyTradingState.reimbursementRecipientAddress) ?? + clobAuthAddress ?? + normalizeAddress(account.address), reimbursementAmountWei: copyTradingState.reimbursementAmountWei, }); if (recoveredHash) { @@ -906,6 +921,12 @@ async function validateToolCalls({ if (reimbursementAmountWei <= 0n) { throw new Error('Reimbursement amount is zero; refusing proposal build.'); } + const reimbursementRecipientAddress = + normalizeAddress(state.reimbursementRecipientAddress) ?? + normalizeAddress(agentAddress); + if (!reimbursementRecipientAddress) { + throw new Error('Missing reimbursement recipient address for proposal build.'); + } validated.push({ ...call, @@ -914,7 +935,7 @@ async function validateToolCalls({ { kind: 'erc20_transfer', token: policy.collateralToken, - to: agentAddress, + to: reimbursementRecipientAddress, amountWei: reimbursementAmountWei.toString(), }, ], @@ -1033,6 +1054,7 @@ function resetCopyTradingState() { activeTokenId: null, copyTradeAmountWei: null, reimbursementAmountWei: null, + reimbursementRecipientAddress: null, copyOrderId: null, copyOrderStatus: null, copyOrderFilled: false, diff --git a/agent-library/agents/copy-trading/commitment.txt b/agent-library/agents/copy-trading/commitment.txt index aea5780b..22d78dd2 100644 --- a/agent-library/agents/copy-trading/commitment.txt +++ b/agent-library/agents/copy-trading/commitment.txt @@ -6,8 +6,8 @@ When the source trader executes a BUY trade in that market, the agent may copy t The agent must size the copied trade to exactly 99% of the Safe's collateral balance at detection time. The remaining 1% stays in the Safe as the agent fee. -The agent executes the copied trade from the agent wallet through the Polymarket CLOB API. +The agent executes the copied trade from the configured copy-trading funding wallet through the Polymarket CLOB API. After receiving YES or NO tokens, the agent deposits those ERC1155 tokens into this Safe. -After token deposit confirmation, the agent proposes one reimbursement transfer from this Safe to the agent wallet equal to the full Safe collateral balance captured when the copy trade was detected. +After token deposit confirmation, the agent proposes one reimbursement transfer from this Safe to that same copy-trading funding wallet equal to the full Safe collateral balance captured when the copy trade was detected. No other transfers are allowed. diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index 7dfd25d0..a2bf912e 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -203,6 +203,7 @@ async function runValidateToolCallTests() { activeTokenId: '123', copyTradeAmountWei: '990000', reimbursementAmountWei: '1000000', + reimbursementRecipientAddress: TEST_CLOB_PROXY, orderSubmitted: true, tokenDeposited: true, reimbursementProposed: false, @@ -220,6 +221,10 @@ async function runValidateToolCallTests() { assert.equal(reimbursementValidated.length, 1); assert.equal(reimbursementValidated[0].parsedArguments.actions.length, 1); assert.equal(reimbursementValidated[0].parsedArguments.actions[0].kind, 'erc20_transfer'); + assert.equal( + reimbursementValidated[0].parsedArguments.actions[0].to.toLowerCase(), + TEST_CLOB_PROXY.toLowerCase() + ); assert.equal(reimbursementValidated[0].parsedArguments.actions[0].amountWei, '1000000'); } @@ -277,6 +282,7 @@ async function runProposalHashGatingTest() { assert.equal(state.activeTradePrice, 0.5); assert.equal(state.copyTradeAmountWei, '990000'); assert.equal(state.reimbursementAmountWei, '1000000'); + assert.equal(state.reimbursementRecipientAddress, TEST_ACCOUNT.toLowerCase()); onToolOutput({ name: 'post_bond_and_propose', @@ -1183,6 +1189,7 @@ async function runTokenBalancesUseResolvedRelayerProxyTest() { assert.equal(copySignal.balances.tokenHolderAddress, TEST_RELAYER_PROXY.toLowerCase()); assert.equal(copySignal.tokenHolderResolutionError, null); assert.equal(copySignal.walletAlignmentError, null); + assert.equal(copySignal.state.reimbursementRecipientAddress, TEST_RELAYER_PROXY.toLowerCase()); } finally { for (const key of envKeys) { if (oldEnv[key] === undefined) { diff --git a/agent/scripts/test-erc1155-deposit.mjs b/agent/scripts/test-erc1155-deposit.mjs index 1509adcb..9a44b29d 100644 --- a/agent/scripts/test-erc1155-deposit.mjs +++ b/agent/scripts/test-erc1155-deposit.mjs @@ -237,6 +237,123 @@ async function run() { globalThis.fetch = oldFetch; } + const deployedUnavailableRelayTxHash = `0x${'d'.repeat(64)}`; + const deployedUnavailableOnchainTxHash = `0x${'e'.repeat(64)}`; + const deployedUnavailableTransactionId = 'relayer-safe-deployed-unavailable-1'; + let deployedUnavailableSubmitBody; + let sawUnavailableDeployedCheck = false; + const oldFetchDeployedUnavailable = globalThis.fetch; + try { + globalThis.fetch = async (url, options = {}) => { + const asText = String(url); + const asLower = asText.toLowerCase(); + if (asLower.includes('/deployed?') && asLower.includes(relayerFromAddress.toLowerCase())) { + sawUnavailableDeployedCheck = true; + return { + ok: false, + status: 503, + statusText: 'Service Unavailable', + async text() { + return JSON.stringify({ error: 'temporarily unavailable' }); + }, + }; + } + + if (asText.includes('/relay-payload?')) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + address: relayerFromAddress, + nonce: '14', + }); + }, + }; + } + + if (asText.endsWith('/submit')) { + deployedUnavailableSubmitBody = JSON.parse(options.body); + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify({ + transactionID: deployedUnavailableTransactionId, + hash: deployedUnavailableRelayTxHash, + state: 'STATE_PENDING', + }); + }, + }; + } + + if ( + asText.includes('/transaction?') && + (asText.includes(`id=${deployedUnavailableTransactionId}`) || + asText.includes(`transactionID=${deployedUnavailableTransactionId}`)) + ) { + return { + ok: true, + status: 200, + statusText: 'OK', + async text() { + return JSON.stringify([ + { + transactionID: deployedUnavailableTransactionId, + hash: deployedUnavailableRelayTxHash, + state: 'STATE_CONFIRMED', + transactionHash: deployedUnavailableOnchainTxHash, + }, + ]); + }, + }; + } + + throw new Error(`Unexpected relayer fetch URL (deployed unavailable test): ${asText}`); + }; + + const deployedUnavailableDepositHash = await makeErc1155Deposit({ + publicClient: { + async getChainId() { + return 137; + }, + }, + walletClient: { + async signMessage() { + return `0x${'f'.repeat(128)}1b`; + }, + }, + account, + config: { + commitmentSafe: config.commitmentSafe, + polymarketRelayerEnabled: true, + polymarketRelayerHost: 'https://relayer-v2.polymarket.com', + polymarketRelayerTxType: 'SAFE', + polymarketRelayerFromAddress: relayerFromAddress, + polymarketBuilderApiKey: 'builder-key', + polymarketBuilderSecret: Buffer.from('builder-secret').toString('base64'), + polymarketBuilderPassphrase: 'builder-passphrase', + polymarketRelayerPollIntervalMs: 0, + polymarketRelayerPollTimeoutMs: 1_000, + }, + token, + tokenId: '12', + amount: '1', + data: null, + }); + + assert.equal(sawUnavailableDeployedCheck, true); + assert.equal(deployedUnavailableDepositHash, deployedUnavailableOnchainTxHash); + assert.equal( + deployedUnavailableSubmitBody.proxyWallet.toLowerCase(), + relayerFromAddress.toLowerCase() + ); + } finally { + globalThis.fetch = oldFetchDeployedUnavailable; + } + const clobOnlyAddress = '0x3333333333333333333333333333333333333333'; const clobIgnoredRelayTxHash = `0x${'9'.repeat(64)}`; const clobIgnoredOnchainTxHash = `0x${'a'.repeat(64)}`; diff --git a/agent/src/lib/polymarket-relayer.js b/agent/src/lib/polymarket-relayer.js index 804e637c..06909fa4 100644 --- a/agent/src/lib/polymarket-relayer.js +++ b/agent/src/lib/polymarket-relayer.js @@ -1075,9 +1075,10 @@ async function ensureSafeDeployed({ } if (deployed === null) { - throw new Error( - `Unable to verify whether SAFE proxy wallet ${safeAddress} is deployed via relayer /deployed endpoint.` + console.warn( + `[agent] Unable to verify SAFE deployment for ${safeAddress} via relayer /deployed endpoint; proceeding without deployment check.` ); + return; } if (!config?.polymarketRelayerAutoDeployProxy) { From 569aeecfb818c182527a9a1e5088e96c8f31f30f Mon Sep 17 00:00:00 2001 From: John Shutt Date: Fri, 13 Feb 2026 16:02:30 -0800 Subject: [PATCH 170/174] resolve a test gap Signed-off-by: John Shutt --- .../copy-trading/test-copy-trading-agent.mjs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs index a2bf912e..7005337b 100644 --- a/agent-library/agents/copy-trading/test-copy-trading-agent.mjs +++ b/agent-library/agents/copy-trading/test-copy-trading-agent.mjs @@ -451,6 +451,143 @@ async function runProposalHashRecoveryFromSignalTest() { } } +async function runProposalHashRecoveryFromSignalUsesFundingWalletTest() { + resetCopyTradingState(); + const envKeys = [ + 'COPY_TRADING_SOURCE_USER', + 'COPY_TRADING_MARKET', + 'COPY_TRADING_YES_TOKEN_ID', + 'COPY_TRADING_NO_TOKEN_ID', + ]; + const oldEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); + const oldFetch = globalThis.fetch; + + process.env.COPY_TRADING_SOURCE_USER = TEST_SOURCE_USER; + process.env.COPY_TRADING_MARKET = 'test-market'; + process.env.COPY_TRADING_YES_TOKEN_ID = YES_TOKEN_ID; + process.env.COPY_TRADING_NO_TOKEN_ID = NO_TOKEN_ID; + + try { + globalThis.fetch = async () => ({ + ok: true, + async json() { + return [ + { + id: 'trade-1', + side: 'BUY', + outcome: 'YES', + price: 0.5, + }, + ]; + }, + }); + + const config = { + commitmentSafe: TEST_SAFE, + polymarketConditionalTokens: '0x4d97dcd97ec945f40cf65f87097ace5ea0476045', + polymarketClobAddress: TEST_CLOB_PROXY, + }; + const publicClient = { + async readContract({ args }) { + if (args.length === 1) return 1_000_000n; + return 0n; + }, + }; + + await enrichSignals([], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: false, + }); + + onToolOutput({ + name: 'post_bond_and_propose', + parsedOutput: { status: 'submitted', transactionHash: TEST_TX_HASH }, + }); + + let state = getCopyTradingState(); + assert.equal(state.reimbursementRecipientAddress, TEST_CLOB_PROXY.toLowerCase()); + assert.equal(state.reimbursementSubmissionPending, true); + + const reimbursementAmountWei = state.reimbursementAmountWei; + const wrongRecipientSignal = { + kind: 'proposal', + proposalHash: OTHER_PROPOSAL_HASH, + proposer: TEST_ACCOUNT, + transactions: [ + { + to: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + value: 0n, + operation: 0, + data: encodeErc20TransferData({ + to: TEST_ACCOUNT, + amount: reimbursementAmountWei, + }), + }, + ], + }; + + await enrichSignals([wrongRecipientSignal], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: true, + }); + + state = getCopyTradingState(); + assert.equal(state.reimbursementProposalHash, null); + assert.equal(state.reimbursementSubmissionPending, true); + + const correctRecipientSignal = { + kind: 'proposal', + proposalHash: TEST_PROPOSAL_HASH, + proposer: TEST_ACCOUNT, + transactions: [ + { + to: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + value: 0n, + operation: 0, + data: encodeErc20TransferData({ + to: TEST_CLOB_PROXY, + amount: reimbursementAmountWei, + }), + }, + ], + }; + + await enrichSignals([correctRecipientSignal], { + publicClient, + config, + account: { address: TEST_ACCOUNT }, + onchainPendingProposal: true, + }); + + state = getCopyTradingState(); + assert.equal(state.reimbursementProposed, true); + assert.equal(state.reimbursementProposalHash, TEST_PROPOSAL_HASH); + assert.equal(state.reimbursementSubmissionPending, false); + + onProposalEvents({ + executedProposals: [TEST_PROPOSAL_HASH], + executedProposalCount: 1, + }); + state = getCopyTradingState(); + assert.equal(state.activeSourceTradeId, null); + assert.equal(state.seenSourceTradeId, 'trade-1'); + } finally { + for (const key of envKeys) { + if (oldEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = oldEnv[key]; + } + } + globalThis.fetch = oldFetch; + resetCopyTradingState(); + } +} + async function runRevertedSubmissionClearsPendingTest() { resetCopyTradingState(); const envKeys = [ @@ -1367,6 +1504,7 @@ async function run() { await runValidateToolCallTests(); await runProposalHashGatingTest(); await runProposalHashRecoveryFromSignalTest(); + await runProposalHashRecoveryFromSignalUsesFundingWalletTest(); await runRevertedSubmissionClearsPendingTest(); await runOrderFillConfirmationGatesDepositTest(); await runMissingOrderIdDoesNotAdvanceOrderStateTest(); From b0a46ebaf33e800d0a9961ddbb57ba871aad5efa Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Mon, 16 Feb 2026 22:12:30 -0500 Subject: [PATCH 171/174] Validate limit price in validateToolCalls before approving swap proposals --- agent-library/agents/limit-order/agent.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/agent-library/agents/limit-order/agent.js b/agent-library/agents/limit-order/agent.js index 5e11b2b2..1d015c04 100644 --- a/agent-library/agents/limit-order/agent.js +++ b/agent-library/agents/limit-order/agent.js @@ -16,6 +16,7 @@ const QUOTER_CANDIDATES_BY_CHAIN = new Map([ [11155111, ['0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3', '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6']], ]); const SLIPPAGE_BPS = 50; +const LIMIT_PRICE_USD = 2000; const chainlinkAbi = parseAbi([ 'function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)', @@ -380,6 +381,19 @@ async function validateToolCalls({ action.amountOutMinWei = minAmountOut.toString(); args.actions[0] = action; + let ethPriceUSD; + try { + const feed = config?.chainlinkPriceFeed ?? CHAINLINK_ETH_USD_FEED_SEPOLIA; + ethPriceUSD = await getEthPriceUSD(publicClient, feed); + } catch { + ethPriceUSD = await getEthPriceUSDFallback(); + } + if (ethPriceUSD > LIMIT_PRICE_USD) { + throw new Error( + `Price condition not met: ethPriceUSD ${ethPriceUSD} exceeds limit ${LIMIT_PRICE_USD}` + ); + } + validated.push({ ...call, parsedArguments: args }); } From ccb19d6720e83f7d029ae933afc6d71136818a7e Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Mon, 16 Feb 2026 22:13:20 -0500 Subject: [PATCH 172/174] Validate limit price vs sma in validateToolCalls before approving swap proposals --- agent-library/agents/limit-order-sma/agent.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/agent-library/agents/limit-order-sma/agent.js b/agent-library/agents/limit-order-sma/agent.js index 558b525d..ec43476e 100644 --- a/agent-library/agents/limit-order-sma/agent.js +++ b/agent-library/agents/limit-order-sma/agent.js @@ -411,6 +411,13 @@ async function validateToolCalls({ action.amountOutMinWei = minAmountOut.toString(); args.actions[0] = action; + const { ethPriceUSD, smaEth200USD } = await fetchEthPriceDataFromCoinGecko(); + if (ethPriceUSD > smaEth200USD) { + throw new Error( + `SMA condition not met: ethPriceUSD ${ethPriceUSD} > smaEth200USD ${smaEth200USD}` + ); + } + validated.push({ ...call, parsedArguments: args }); } From 49700c574d3b98c29c05eb302c2af90ce003a9ab Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Mon, 16 Feb 2026 22:13:53 -0500 Subject: [PATCH 173/174] Defer hydration lock until ProposalExecuted log fetch succeeds in limit-order agents --- agent-library/agents/limit-order-sma/agent.js | 2 +- agent-library/agents/limit-order/agent.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agent-library/agents/limit-order-sma/agent.js b/agent-library/agents/limit-order-sma/agent.js index ec43476e..51c02636 100644 --- a/agent-library/agents/limit-order-sma/agent.js +++ b/agent-library/agents/limit-order-sma/agent.js @@ -461,7 +461,6 @@ function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 async function reconcileProposalSubmission({ publicClient, ogModule, startBlock }) { if (!hydratedFromChain && ogModule) { - hydratedFromChain = true; try { const toBlock = await publicClient.getBlockNumber(); const fromBlock = startBlock ?? 0n; @@ -478,6 +477,7 @@ async function reconcileProposalSubmission({ publicClient, ogModule, startBlock limitOrderState.proposalSubmitHash = null; limitOrderState.proposalSubmitMs = null; } + hydratedFromChain = true; } catch (err) { console.warn('[limit-order-sma] Failed to hydrate from chain:', err?.message ?? err); } diff --git a/agent-library/agents/limit-order/agent.js b/agent-library/agents/limit-order/agent.js index 1d015c04..0537ae7c 100644 --- a/agent-library/agents/limit-order/agent.js +++ b/agent-library/agents/limit-order/agent.js @@ -437,7 +437,6 @@ function onProposalEvents({ executedProposalCount = 0, deletedProposalCount = 0 async function reconcileProposalSubmission({ publicClient, ogModule, startBlock }) { if (!hydratedFromChain && ogModule) { - hydratedFromChain = true; try { const toBlock = await publicClient.getBlockNumber(); const fromBlock = startBlock ?? 0n; @@ -454,6 +453,7 @@ async function reconcileProposalSubmission({ publicClient, ogModule, startBlock limitOrderState.proposalSubmitHash = null; limitOrderState.proposalSubmitMs = null; } + hydratedFromChain = true; } catch (err) { console.warn('[limit-order] Failed to hydrate from chain:', err?.message ?? err); } From eb6bcfbea959c9307bf33cde783358cfda7c50bb Mon Sep 17 00:00:00 2001 From: Damian Kuthoore Date: Mon, 16 Feb 2026 22:19:37 -0500 Subject: [PATCH 174/174] Add SMA and price validity guard in limit-order-sma validateToolCalls --- agent-library/agents/limit-order-sma/agent.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/agent-library/agents/limit-order-sma/agent.js b/agent-library/agents/limit-order-sma/agent.js index 51c02636..88f814ef 100644 --- a/agent-library/agents/limit-order-sma/agent.js +++ b/agent-library/agents/limit-order-sma/agent.js @@ -412,6 +412,13 @@ async function validateToolCalls({ args.actions[0] = action; const { ethPriceUSD, smaEth200USD } = await fetchEthPriceDataFromCoinGecko(); + if ( + smaEth200USD == null || + !Number.isFinite(smaEth200USD) || + !Number.isFinite(ethPriceUSD) + ) { + throw new Error('SMA or price data unavailable; cannot validate condition.'); + } if (ethPriceUSD > smaEth200USD) { throw new Error( `SMA condition not met: ethPriceUSD ${ethPriceUSD} > smaEth200USD ${smaEth200USD}`