diff --git a/.gitignore b/.gitignore index 92eb06c..2cb995e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ dist/ .env.local .env.*.local +sentinel.config.yaml + # IDE files .vscode/ .idea/ diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..72b394f --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,30 @@ +# Gitleaks custom configuration for blockchain projects +title = "Gitleaks config for eco-routes-cli" + +[extend] +# Extend the default gitleaks config +useDefault = true + +[[rules]] +id = "ethereum-private-key" +description = "Ethereum/EVM Private Key (64 hex chars with 0x prefix)" +regex = '''0x[a-fA-F0-9]{64}''' +keywords = ["0x"] + +[[rules]] +id = "ethereum-private-key-no-prefix" +description = "Ethereum/EVM Private Key (64 hex chars in quotes)" +regex = '''["'][a-fA-F0-9]{64}["']''' + +[[rules]] +id = "solana-private-key-array" +description = "Solana Private Key (byte array)" +regex = '''\[\s*\d{1,3}(?:\s*,\s*\d{1,3}){31,63}\s*\]''' + +[extend.allowlist] +# Allow .env.example files +paths = [ + '''\.env\.example$''', + '''\.example$''', + '''\.sample$''', +] diff --git a/.husky/pre-commit b/.husky/pre-commit index 2312dc5..4948b6e 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,4 @@ +# Run pre-commit hooks (detect private keys, etc.) +pre-commit run + npx lint-staged diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f14dca3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.21.2 + hooks: + - id: gitleaks + args: ['--config', '.gitleaks.toml'] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: detect-private-key + - id: detect-aws-credentials + args: ['--allow-missing-credentials'] + - id: check-added-large-files + - id: check-merge-conflict diff --git a/package.json b/package.json index ab76153..ac828ca 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "author": "Eco Protocol", "license": "MIT", "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.946.0", "@coral-xyz/anchor": "^0.31.1", "@solana/spl-token": "^0.4.14", "@solana/web3.js": "^1.91.8", @@ -41,14 +42,17 @@ "commander": "^12.1.0", "dotenv": "^16.4.5", "inquirer": "^9.3.7", + "js-yaml": "^4.1.1", "ora": "^8.2.0", "tronweb": "^6.0.0", - "viem": "^2.21.58" + "viem": "^2.21.58", + "zod": "^4.1.13" }, "devDependencies": { "@types/bn.js": "^5.2.0", "@types/inquirer": "^9.0.7", "@types/jest": "^30.0.0", + "@types/js-yaml": "^4.0.9", "@types/node": "^20.16.16", "@typescript-eslint/eslint-plugin": "^8.43.0", "@typescript-eslint/parser": "^8.43.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d829899..d60a7c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@aws-sdk/client-secrets-manager': + specifier: ^3.946.0 + version: 3.946.0 '@coral-xyz/anchor': specifier: ^0.31.1 version: 0.31.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) @@ -32,6 +35,9 @@ importers: inquirer: specifier: ^9.3.7 version: 9.3.7 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 ora: specifier: ^8.2.0 version: 8.2.0 @@ -40,7 +46,10 @@ importers: version: 6.0.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) viem: specifier: ^2.21.58 - version: 2.37.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + version: 2.37.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + zod: + specifier: ^4.1.13 + version: 4.1.13 devDependencies: '@types/bn.js': specifier: ^5.2.0 @@ -51,6 +60,9 @@ importers: '@types/jest': specifier: ^30.0.0 version: 30.0.0 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/node': specifier: ^20.16.16 version: 20.19.11 @@ -108,6 +120,123 @@ packages: '@adraffy/ens-normalize@1.11.0': resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-secrets-manager@3.946.0': + resolution: {integrity: sha512-z9shW7duU48T1mn4XJiC0uc0UYJ9J5RjJv+AX63dEVKmzq+LS5z8vEaG9BjXFLEBqe+YhTc7gvgslD/aymgTDw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.946.0': + resolution: {integrity: sha512-kGAs5iIVyUz4p6TX3pzG5q3cNxXnVpC4pwRC6DCSaSv9ozyPjc2d74FsK4fZ+J+ejtvCdJk72uiuQtWJc86Wuw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.946.0': + resolution: {integrity: sha512-u2BkbLLVbMFrEiXrko2+S6ih5sUZPlbVyRPtXOqMHlCyzr70sE8kIiD6ba223rQeIFPcYfW/wHc6k4ihW2xxVg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.946.0': + resolution: {integrity: sha512-P4l+K6wX1tf8LmWUvZofdQ+BgCNyk6Tb9u1H10npvqpuCD+dCM4pXIBq3PQcv/juUBOvLGGREo+Govuh3lfD0Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.946.0': + resolution: {integrity: sha512-/zeOJ6E7dGZQ/l2k7KytEoPJX0APIhwt0A79hPf/bUpMF4dDs2P6JmchDrotk0a0Y/MIdNF8sBQ/MEOPnBiYoQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.946.0': + resolution: {integrity: sha512-Pdgcra3RivWj/TuZmfFaHbqsvvgnSKO0CxlRUMMr0PgBiCnUhyl+zBktdNOeGsOPH2fUzQpYhcUjYUgVSdcSDQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-login@3.946.0': + resolution: {integrity: sha512-5iqLNc15u2Zx+7jOdQkIbP62N7n2031tw5hkmIG0DLnozhnk64osOh2CliiOE9x3c4P9Pf4frAwgyy9GzNTk2g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.946.0': + resolution: {integrity: sha512-I7URUqnBPng1a5y81OImxrwERysZqMBREG6svhhGeZgxmqcpAZ8z5ywILeQXdEOCuuES8phUp/ojzxFjPXp/eA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.946.0': + resolution: {integrity: sha512-GtGHX7OGqIeVQ3DlVm5RRF43Qmf3S1+PLJv9svrdvAhAdy2bUb044FdXXqrtSsIfpzTKlHgQUiRo5MWLd35Ntw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.946.0': + resolution: {integrity: sha512-LeGSSt2V5iwYey1ENGY75RmoDP3bA2iE/py8QBKW8EDA8hn74XBLkprhrK5iccOvU3UGWY8WrEKFAFGNjJOL9g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.946.0': + resolution: {integrity: sha512-ocBCvjWfkbjxElBI1QUxOnHldsNhoU0uOICFvuRDAZAoxvypJHN3m5BJkqb7gqorBbcv3LRgmBdEnWXOAvq+7Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.936.0': + resolution: {integrity: sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.936.0': + resolution: {integrity: sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.936.0': + resolution: {integrity: sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.946.0': + resolution: {integrity: sha512-7QcljCraeaWQNuqmOoAyZs8KpZcuhPiqdeeKoRd397jVGNRehLFsZbIMOvwaluUDFY11oMyXOkQEERe1Zo2fCw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.946.0': + resolution: {integrity: sha512-rjAtEguukeW8mlyEQMQI56vxFoyWlaNwowmz1p1rav948SUjtrzjHAp4TOQWhibb7AR7BUTHBCgIcyCRjBEf4g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.936.0': + resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.946.0': + resolution: {integrity: sha512-a5c+rM6CUPX2ExmUZ3DlbLlS5rQr4tbdoGcgBsjnAHiYx8MuMNAI+8M7wfjF13i2yvUQj5WEIddvLpayfEZj9g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.936.0': + resolution: {integrity: sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.936.0': + resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.893.0': + resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-user-agent-browser@3.936.0': + resolution: {integrity: sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==} + + '@aws-sdk/util-user-agent-node@3.946.0': + resolution: {integrity: sha512-a2UwwvzbK5AxHKUBupfg4s7VnkqRAHjYsuezHnKCniczmT4HZfP1NnfwwvLKEH8qaTrwenxjKSfq4UWmWkvG+Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.930.0': + resolution: {integrity: sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.2.2': + resolution: {integrity: sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -719,6 +848,178 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@smithy/abort-controller@4.2.5': + resolution: {integrity: sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.3': + resolution: {integrity: sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.18.7': + resolution: {integrity: sha512-axG9MvKhMWOhFbvf5y2DuyTxQueO0dkedY9QC3mAfndLosRI/9LJv8WaL0mw7ubNhsO4IuXX9/9dYGPFvHrqlw==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.5': + resolution: {integrity: sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.6': + resolution: {integrity: sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.5': + resolution: {integrity: sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.5': + resolution: {integrity: sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.5': + resolution: {integrity: sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.3.14': + resolution: {integrity: sha512-v0q4uTKgBM8dsqGjqsabZQyH85nFaTnFcgpWU1uydKFsdyyMzfvOkNum9G7VK+dOP01vUnoZxIeRiJ6uD0kjIg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.14': + resolution: {integrity: sha512-Z2DG8Ej7FyWG1UA+7HceINtSLzswUgs2np3sZX0YBBxCt+CXG4QUxv88ZDS3+2/1ldW7LqtSY1UO/6VQ1pND8Q==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.6': + resolution: {integrity: sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.5': + resolution: {integrity: sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.5': + resolution: {integrity: sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.5': + resolution: {integrity: sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.5': + resolution: {integrity: sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.5': + resolution: {integrity: sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.5': + resolution: {integrity: sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.5': + resolution: {integrity: sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.5': + resolution: {integrity: sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.0': + resolution: {integrity: sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.5': + resolution: {integrity: sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.9.10': + resolution: {integrity: sha512-Jaoz4Jw1QYHc1EFww/E6gVtNjhoDU+gwRKqXP6C3LKYqqH2UQhP8tMP3+t/ePrhaze7fhLE8vS2q6vVxBANFTQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.9.0': + resolution: {integrity: sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.5': + resolution: {integrity: sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.13': + resolution: {integrity: sha512-hlVLdAGrVfyNei+pKIgqDTxfu/ZI2NSyqj4IDxKd5bIsIqwR/dSlkxlPaYxFiIaDVrBy0he8orsFy+Cz119XvA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.16': + resolution: {integrity: sha512-F1t22IUiJLHrxW9W1CQ6B9PN+skZ9cqSuzB18Eh06HrJPbjsyZ7ZHecAKw80DQtyGTRcVfeukKaCRYebFwclbg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.5': + resolution: {integrity: sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.5': + resolution: {integrity: sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.5': + resolution: {integrity: sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.6': + resolution: {integrity: sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + '@solana/buffer-layout-utils@0.2.0': resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} engines: {node: '>= 10'} @@ -858,6 +1159,9 @@ packages: '@types/jest@30.0.0': resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1183,6 +1487,9 @@ packages: borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} + bowser@2.13.1: + resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1586,6 +1893,10 @@ packages: fast-stable-stringify@1.0.0: resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} @@ -2019,8 +2330,8 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsesc@3.1.0: @@ -2525,6 +2836,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@2.1.1: + resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + superstruct@0.15.5: resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==} @@ -2822,12 +3136,382 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + snapshots: '@adraffy/ens-normalize@1.10.1': {} '@adraffy/ens-normalize@1.11.0': {} + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-locate-window': 3.893.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.936.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-secrets-manager@3.946.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.946.0 + '@aws-sdk/credential-provider-node': 3.946.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.946.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.946.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.7 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.14 + '@smithy/middleware-retry': 4.4.14 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.10 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.13 + '@smithy/util-defaults-mode-node': 4.2.16 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.946.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.946.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.946.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.946.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.7 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.14 + '@smithy/middleware-retry': 4.4.14 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.10 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.13 + '@smithy/util-defaults-mode-node': 4.2.16 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.946.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@aws-sdk/xml-builder': 3.930.0 + '@smithy/core': 3.18.7 + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/signature-v4': 5.3.5 + '@smithy/smithy-client': 4.9.10 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.946.0': + dependencies: + '@aws-sdk/core': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.946.0': + dependencies: + '@aws-sdk/core': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/node-http-handler': 4.4.5 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.10 + '@smithy/types': 4.9.0 + '@smithy/util-stream': 4.5.6 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.946.0': + dependencies: + '@aws-sdk/core': 3.946.0 + '@aws-sdk/credential-provider-env': 3.946.0 + '@aws-sdk/credential-provider-http': 3.946.0 + '@aws-sdk/credential-provider-login': 3.946.0 + '@aws-sdk/credential-provider-process': 3.946.0 + '@aws-sdk/credential-provider-sso': 3.946.0 + '@aws-sdk/credential-provider-web-identity': 3.946.0 + '@aws-sdk/nested-clients': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.946.0': + dependencies: + '@aws-sdk/core': 3.946.0 + '@aws-sdk/nested-clients': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.946.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.946.0 + '@aws-sdk/credential-provider-http': 3.946.0 + '@aws-sdk/credential-provider-ini': 3.946.0 + '@aws-sdk/credential-provider-process': 3.946.0 + '@aws-sdk/credential-provider-sso': 3.946.0 + '@aws-sdk/credential-provider-web-identity': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.946.0': + dependencies: + '@aws-sdk/core': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.946.0': + dependencies: + '@aws-sdk/client-sso': 3.946.0 + '@aws-sdk/core': 3.946.0 + '@aws-sdk/token-providers': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.946.0': + dependencies: + '@aws-sdk/core': 3.946.0 + '@aws-sdk/nested-clients': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-host-header@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@aws/lambda-invoke-store': 0.2.2 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.946.0': + dependencies: + '@aws-sdk/core': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@smithy/core': 3.18.7 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.946.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.946.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.946.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.946.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.7 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.14 + '@smithy/middleware-retry': 4.4.14 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.10 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.13 + '@smithy/util-defaults-mode-node': 4.2.16 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.946.0': + dependencies: + '@aws-sdk/core': 3.946.0 + '@aws-sdk/nested-clients': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.936.0': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-endpoints': 3.2.5 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.893.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + bowser: 2.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.946.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.946.0 + '@aws-sdk/types': 3.936.0 + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.930.0': + dependencies: + '@smithy/types': 4.9.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.2': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -3182,7 +3866,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -3516,6 +4200,280 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/abort-controller@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.3': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + tslib: 2.8.1 + + '@smithy/core@3.18.7': + dependencies: + '@smithy/middleware-serde': 4.2.6 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-stream': 4.5.6 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.5': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.6': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/querystring-builder': 4.2.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.5': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.3.14': + dependencies: + '@smithy/core': 3.18.7 + '@smithy/middleware-serde': 4.2.6 + '@smithy/node-config-provider': 4.3.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-middleware': 4.2.5 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.14': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/service-error-classification': 4.2.5 + '@smithy/smithy-client': 4.9.10 + '@smithy/types': 4.9.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.6': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.5': + dependencies: + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.4.5': + dependencies: + '@smithy/abort-controller': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/querystring-builder': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + + '@smithy/shared-ini-file-loader@4.4.0': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.5': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.9.10': + dependencies: + '@smithy/core': 3.18.7 + '@smithy/middleware-endpoint': 4.3.14 + '@smithy/middleware-stack': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-stream': 4.5.6 + tslib: 2.8.1 + + '@smithy/types@4.9.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.5': + dependencies: + '@smithy/querystring-parser': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.13': + dependencies: + '@smithy/property-provider': 4.2.5 + '@smithy/smithy-client': 4.9.10 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.16': + dependencies: + '@smithy/config-resolver': 4.4.3 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/smithy-client': 4.9.10 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.5': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.5': + dependencies: + '@smithy/service-error-classification': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.6': + dependencies: + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/node-http-handler': 4.4.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + dependencies: + tslib: 2.8.1 + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -3725,6 +4683,8 @@ snapshots: expect: 30.1.2 pretty-format: 30.0.5 + '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} '@types/node@12.20.55': {} @@ -3913,9 +4873,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - abitype@1.1.0(typescript@5.9.2): + abitype@1.1.0(typescript@5.9.2)(zod@4.1.13): optionalDependencies: typescript: 5.9.2 + zod: 4.1.13 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: @@ -4069,6 +5030,8 @@ snapshots: bs58: 4.0.1 text-encoding-utf-8: 1.0.2 + bowser@2.13.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -4482,6 +5445,10 @@ snapshots: fast-stable-stringify@1.0.0: {} + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.1 + fastestsmallesttextencoderdecoder@1.0.22: {} fastq@1.19.1: @@ -5100,7 +6067,7 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -5312,7 +6279,7 @@ snapshots: os-tmpdir@1.0.2: {} - ox@0.9.3(typescript@5.9.2): + ox@0.9.3(typescript@5.9.2)(zod@4.1.13): dependencies: '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 @@ -5320,7 +6287,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.2) + abitype: 1.1.0(typescript@5.9.2)(zod@4.1.13) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -5564,6 +6531,8 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.1.1: {} + superstruct@0.15.5: {} superstruct@2.0.2: {} @@ -5749,15 +6718,15 @@ snapshots: validator@13.12.0: {} - viem@2.37.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10): + viem@2.37.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.2) + abitype: 1.1.0(typescript@5.9.2)(zod@4.1.13) isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.9.3(typescript@5.9.2) + ox: 0.9.3(typescript@5.9.2)(zod@4.1.13) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.2 @@ -5858,3 +6827,5 @@ snapshots: yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.3: {} + + zod@4.1.13: {} diff --git a/sentinel.config.example.yaml b/sentinel.config.example.yaml new file mode 100644 index 0000000..03b63a5 --- /dev/null +++ b/sentinel.config.example.yaml @@ -0,0 +1,54 @@ +service: + name: "sentinel" + environment: "development" + logLevel: "info" + +chains: + - base + - solana + +tokens: + - symbol: USDC + amount: "0.05" + +routes: + mode: "all" + exclude: + - source: solana + destination: solana + +scheduler: + strategy: "periodic" + intervalMs: 60000 + +execution: + parallelism: 1 + timeoutMs: 300000 + dryRun: false + retries: + maxAttempts: 2 + backoffMs: 5000 + +health: + degradedAfterFailures: 2 + failedAfterFailures: 5 + healthyAfterSuccesses: 3 + maxFulfillmentTimeMs: 180000 + +reporting: + console: + enabled: true + verbose: true + summaryIntervalMs: 300000 + +evm: + wallets: + basic: + type: "basic" + privateKey: "0x..." + +svm: + wallets: + basic: + type: "basic" + secretKey: "..." diff --git a/src/commands/sentinel.ts b/src/commands/sentinel.ts new file mode 100644 index 0000000..183263c --- /dev/null +++ b/src/commands/sentinel.ts @@ -0,0 +1,22 @@ +/** + * Sentinel Command + * + * CLI command for running Sentinel locally + */ + +import { Command } from 'commander'; + +import { startSentinel } from '@/sentinel'; + +export function createSentinelCommand(): Command { + const command = new Command('sentinel'); + + command + .description('Start Sentinel route health monitor') + .option('-c, --config ', 'Path to config file') + .action(async options => { + await startSentinel(options.config); + }); + + return command; +} diff --git a/src/index.ts b/src/index.ts index d0c88af..efaeac2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { Command } from 'commander'; import { createConfigCommand } from '@/commands/config'; import { createPublishCommand } from '@/commands/publish'; +import { createSentinelCommand } from '@/commands/sentinel'; import { createStatusCommand } from '@/commands/status'; import { type ChainConfig, updatePortalAddresses } from '@/config/chains'; import { type TokenConfig } from '@/config/tokens'; @@ -38,6 +39,7 @@ program program.addCommand(createPublishCommand()); program.addCommand(createStatusCommand()); program.addCommand(createConfigCommand()); +program.addCommand(createSentinelCommand()); // List chains command program diff --git a/src/sentinel/config.ts b/src/sentinel/config.ts new file mode 100644 index 0000000..5770282 --- /dev/null +++ b/src/sentinel/config.ts @@ -0,0 +1,119 @@ +/** + * Sentinel Config Loader + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +import * as yaml from 'js-yaml'; +import { z } from 'zod'; + +import { SentinelConfig } from './types'; + +// Wallet schemas +const evmWalletsSchema = z.object({ + basic: z.object({ + type: z.literal('basic'), + privateKey: z.string(), + }), +}); + +const svmWalletsSchema = z.object({ + basic: z.object({ + type: z.literal('basic'), + secretKey: z.string(), + }), +}); + +const configSchema = z.object({ + service: z.object({ + name: z.string().default('sentinel'), + environment: z.enum(['development', 'staging', 'production']).default('development'), + logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'), + }), + chains: z.array(z.string()).min(1), + tokens: z + .array( + z.object({ + symbol: z.string(), + amount: z.string(), + }) + ) + .min(1), + routes: z.object({ + mode: z.enum(['all', 'explicit']).default('all'), + exclude: z + .array( + z.object({ + source: z.string(), + destination: z.string(), + }) + ) + .optional(), + }), + scheduler: z.object({ + strategy: z.literal('periodic'), + intervalMs: z.number().positive().default(60000), + }), + execution: z.object({ + parallelism: z.number().positive().default(1), + timeoutMs: z.number().positive().default(300000), + dryRun: z.boolean().default(false), + retries: z.object({ + maxAttempts: z.number().nonnegative().default(2), + backoffMs: z.number().nonnegative().default(5000), + }), + }), + health: z.object({ + degradedAfterFailures: z.number().positive().default(2), + failedAfterFailures: z.number().positive().default(5), + healthyAfterSuccesses: z.number().positive().default(3), + maxFulfillmentTimeMs: z.number().positive().default(180000), + }), + reporting: z.object({ + console: z.object({ + enabled: z.boolean().default(true), + verbose: z.boolean().default(false), + summaryIntervalMs: z.number().positive().default(300000), + }), + }), + evm: z + .object({ + wallets: evmWalletsSchema, + }) + .optional(), + svm: z + .object({ + wallets: svmWalletsSchema, + }) + .optional(), +}); + +export function loadConfig(configPath?: string): SentinelConfig { + // Default config paths to try + const paths = configPath + ? [configPath] + : [ + './sentinel.config.yaml', + './sentinel.config.yml', + './config/sentinel.config.yaml', + path.join(__dirname, '../../sentinel.config.yaml'), + ]; + + let configFile: string | undefined; + for (const p of paths) { + if (fs.existsSync(p)) { + configFile = p; + break; + } + } + + if (!configFile) { + throw new Error(`Config file not found. Tried: ${paths.join(', ')}`); + } + + const rawConfig = yaml.load(fs.readFileSync(configFile, 'utf8')); + const parsed = configSchema.parse(rawConfig); + + return parsed as SentinelConfig; +} diff --git a/src/sentinel/engine.ts b/src/sentinel/engine.ts new file mode 100644 index 0000000..2ec92ed --- /dev/null +++ b/src/sentinel/engine.ts @@ -0,0 +1,140 @@ +/** + * Sentinel Engine + * + * Main orchestrator that runs the periodic route testing + */ + +import { RouteMatrix } from './matrix'; +import { Reporter } from './reporter'; +import { RouteTester } from './tester'; +import { RouteHealth, SentinelConfig } from './types'; +import { WalletManager } from './wallet'; + +export class SentinelEngine { + private config: SentinelConfig; + private matrix: RouteMatrix; + private tester: RouteTester; + private reporter: Reporter; + private running = false; + private currentTimeout?: NodeJS.Timeout; + + constructor(config: SentinelConfig) { + this.config = config; + this.matrix = new RouteMatrix(config); + const walletManager = new WalletManager(config); + this.tester = new RouteTester(config, walletManager); + this.reporter = new Reporter(config, this.matrix); + } + + async start(): Promise { + if (this.running) return; + + this.running = true; + + // Initialize route matrix + const tokens = this.config.tokens.map(t => t.symbol); + this.matrix.initialize(this.config.chains, tokens); + + // Start reporter + this.reporter.start(); + + // Run first cycle immediately + await this.runCycle(); + + // Schedule subsequent cycles + this.scheduleNext(); + } + + async stop(): Promise { + this.running = false; + + if (this.currentTimeout) { + clearTimeout(this.currentTimeout); + } + + this.reporter.stop(); + this.reporter.printFinalSummary(); + } + + private scheduleNext(): void { + if (!this.running) return; + + this.currentTimeout = setTimeout(async () => { + await this.runCycle(); + this.scheduleNext(); + }, this.config.scheduler.intervalMs); + } + + private async runCycle(): Promise { + const routes = this.matrix.getRoutes(); + + // Test routes based on parallelism + if (this.config.execution.parallelism === 1) { + // Sequential + for (const route of routes) { + if (!this.running) break; + await this.testRoute(route); + } + } else { + // Parallel with concurrency limit + const chunks = this.chunk(routes, this.config.execution.parallelism); + for (const chunk of chunks) { + if (!this.running) break; + await Promise.all(chunk.map(route => this.testRoute(route))); + } + } + } + + private async testRoute(route: RouteHealth): Promise { + const tokenConfig = this.config.tokens.find(t => t.symbol === route.token); + const amount = tokenConfig?.amount ?? '0.05'; + + let lastError: string | undefined; + + // Retry logic + for (let attempt = 0; attempt <= this.config.execution.retries.maxAttempts; attempt++) { + if (!this.running) return; + + if (attempt > 0) { + // Wait before retry + await this.sleep(this.config.execution.retries.backoffMs * attempt); + } + + const result = await this.tester.test(route.source, route.destination, route.token, amount); + + if (result.success) { + const updatedRoute = this.matrix.updateFromResult(result); + this.reporter.reportResult(result, updatedRoute); + return; + } + + lastError = result.error; + } + + // All retries failed + const failedResult = { + source: route.source, + destination: route.destination, + token: route.token, + success: false, + error: lastError ?? 'Unknown error', + publishTimeMs: 0, + timestamp: new Date(), + }; + + const updatedRoute = this.matrix.updateFromResult(failedResult); + this.reporter.reportResult(failedResult, updatedRoute); + } + + private chunk(arr: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + chunks.push(arr.slice(i, i + size)); + } + return chunks; + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} diff --git a/src/sentinel/index.ts b/src/sentinel/index.ts new file mode 100644 index 0000000..6ea22ee --- /dev/null +++ b/src/sentinel/index.ts @@ -0,0 +1,48 @@ +/** + * Sentinel - Route Health Monitor + * + * Entry point for standalone service execution + */ + +import { loadConfig } from './config'; +import { SentinelEngine } from './engine'; + +export { loadConfig } from './config'; +export { SentinelEngine } from './engine'; +export { RouteMatrix } from './matrix'; +export { Reporter } from './reporter'; +export { RouteTester } from './tester'; +export * from './types'; +export { WalletManager } from './wallet'; + +/** + * Start Sentinel as a standalone service + */ +export async function startSentinel(configPath?: string): Promise { + // Load config + const config = loadConfig(configPath); + + // Create and start engine + const engine = new SentinelEngine(config); + + // Handle graceful shutdown + const shutdown = async () => { + console.log('\nShutting down Sentinel...'); + await engine.stop(); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + + // Start + await engine.start(); +} + +// If running directly (not imported) +if (require.main === module) { + startSentinel().catch(err => { + console.error('Fatal error:', err); + process.exit(1); + }); +} diff --git a/src/sentinel/matrix.ts b/src/sentinel/matrix.ts new file mode 100644 index 0000000..b0863ff --- /dev/null +++ b/src/sentinel/matrix.ts @@ -0,0 +1,133 @@ +/** + * Sentinel Route Matrix + * + * In-memory tracking of route health status + */ + +import { RouteHealth, RouteStatus, SentinelConfig, TestResult } from './types'; + +export class RouteMatrix { + private routes: Map = new Map(); + private config: SentinelConfig; + + constructor(config: SentinelConfig) { + this.config = config; + } + + private key(source: string, destination: string, token: string): string { + return `${source}->${destination}:${token}`; + } + + initialize(chains: string[], tokens: string[]): void { + const excludeSet = new Set( + (this.config.routes.exclude ?? []).map(e => `${e.source}->${e.destination}`) + ); + + for (const source of chains) { + for (const destination of chains) { + if (source === destination) continue; + if (excludeSet.has(`${source}->${destination}`)) continue; + + for (const token of tokens) { + const k = this.key(source, destination, token); + this.routes.set(k, { + source, + destination, + token, + status: 'unknown', + consecutiveFailures: 0, + consecutiveSuccesses: 0, + totalTests: 0, + totalSuccesses: 0, + totalFailures: 0, + }); + } + } + } + } + + getRoutes(): RouteHealth[] { + return Array.from(this.routes.values()); + } + + getRoute(source: string, destination: string, token: string): RouteHealth | undefined { + return this.routes.get(this.key(source, destination, token)); + } + + updateFromResult(result: TestResult): RouteHealth { + const k = this.key(result.source, result.destination, result.token); + const route = this.routes.get(k); + + if (!route) { + throw new Error(`Unknown route: ${k}`); + } + + route.lastCheck = result.timestamp; + route.totalTests++; + + if (result.success) { + route.consecutiveSuccesses++; + route.consecutiveFailures = 0; + route.totalSuccesses++; + route.lastSuccess = result.timestamp; + + // Update avg fulfillment time + if (result.fulfillTimeMs) { + route.avgFulfillTimeMs = route.avgFulfillTimeMs + ? (route.avgFulfillTimeMs + result.fulfillTimeMs) / 2 + : result.fulfillTimeMs; + } + } else { + route.consecutiveFailures++; + route.consecutiveSuccesses = 0; + route.totalFailures++; + route.lastFailure = result.timestamp; + route.lastError = result.error; + } + + // Update status based on thresholds + route.status = this.computeStatus(route); + + return route; + } + + private computeStatus(route: RouteHealth): RouteStatus { + const { health } = this.config; + + if (route.consecutiveFailures >= health.failedAfterFailures) { + return 'failed'; + } + + if (route.consecutiveFailures >= health.degradedAfterFailures) { + return 'degraded'; + } + + if (route.consecutiveSuccesses >= health.healthyAfterSuccesses) { + return 'healthy'; + } + + // Keep current status if thresholds not met + if (route.status === 'unknown' && route.totalTests > 0) { + return route.consecutiveFailures > 0 ? 'degraded' : 'healthy'; + } + + return route.status; + } + + getSummary(): { + total: number; + healthy: number; + degraded: number; + failed: number; + unknown: number; + } { + const routes = this.getRoutes(); + return { + total: routes.length, + healthy: routes.filter(r => r.status === 'healthy').length, + degraded: routes.filter(r => r.status === 'degraded').length, + failed: routes.filter(r => r.status === 'failed').length, + unknown: routes.filter(r => r.status === 'unknown').length, + }; + } +} diff --git a/src/sentinel/reporter.ts b/src/sentinel/reporter.ts new file mode 100644 index 0000000..a1d2e5c --- /dev/null +++ b/src/sentinel/reporter.ts @@ -0,0 +1,158 @@ +/** + * Sentinel Console Reporter + */ + +import chalk from 'chalk'; + +import { logger } from '@/utils/logger'; + +import { RouteMatrix } from './matrix'; +import { RouteHealth, SentinelConfig, TestResult } from './types'; + +export class Reporter { + private config: SentinelConfig; + private matrix: RouteMatrix; + private summaryInterval?: NodeJS.Timeout; + + constructor(config: SentinelConfig, matrix: RouteMatrix) { + this.config = config; + this.matrix = matrix; + } + + start(): void { + if (!this.config.reporting.console.enabled) return; + + // Print initial status + this.printHeader(); + + // Schedule periodic summaries + if (this.config.reporting.console.summaryIntervalMs > 0) { + this.summaryInterval = setInterval(() => { + this.printSummary(); + }, this.config.reporting.console.summaryIntervalMs); + } + } + + stop(): void { + if (this.summaryInterval) { + clearInterval(this.summaryInterval); + } + } + + private printHeader(): void { + console.log(''); + console.log(chalk.bold.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(chalk.bold.cyan(' SENTINEL - Route Health Monitor')); + console.log(chalk.bold.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(''); + console.log(chalk.dim(` Environment: ${this.config.service.environment}`)); + console.log(chalk.dim(` Chains: ${this.config.chains.join(', ')}`)); + console.log(chalk.dim(` Tokens: ${this.config.tokens.map(t => t.symbol).join(', ')}`)); + console.log(chalk.dim(` Interval: ${this.config.scheduler.intervalMs / 1000}s`)); + console.log(''); + + const summary = this.matrix.getSummary(); + console.log(chalk.dim(` Monitoring ${summary.total} routes`)); + console.log(''); + } + + reportResult(result: TestResult, route: RouteHealth): void { + if (!this.config.reporting.console.enabled) return; + if (!this.config.reporting.console.verbose) return; + + const statusIcon = this.getStatusIcon(route.status); + const routeStr = `${result.source} → ${result.destination} (${result.token})`; + + if (result.success) { + const timeStr = result.fulfillTimeMs + ? `${(result.fulfillTimeMs / 1000).toFixed(1)}s` + : `${(result.publishTimeMs / 1000).toFixed(1)}s (publish)`; + console.log(` ${statusIcon} ${chalk.green('✓')} ${routeStr} - ${timeStr}`); + } else { + const errorStr = result.error ? `: ${result.error}` : ''; + console.log(` ${statusIcon} ${chalk.red('✗')} ${routeStr}${errorStr}`); + } + } + + printSummary(): void { + if (!this.config.reporting.console.enabled) return; + + const summary = this.matrix.getSummary(); + const routes = this.matrix.getRoutes(); + + console.log(''); + console.log(chalk.bold('─── Summary ───────────────────────────────────────────')); + console.log(` ${chalk.green('●')} Healthy: ${summary.healthy}`); + console.log(` ${chalk.yellow('●')} Degraded: ${summary.degraded}`); + console.log(` ${chalk.red('●')} Failed: ${summary.failed}`); + console.log(` ${chalk.gray('●')} Unknown: ${summary.unknown}`); + console.log(''); + + // Show failed/degraded routes + const problemRoutes = routes.filter(r => r.status === 'failed' || r.status === 'degraded'); + if (problemRoutes.length > 0) { + console.log(chalk.bold(' Problem Routes:')); + for (const route of problemRoutes) { + const icon = this.getStatusIcon(route.status); + const routeStr = `${route.source} → ${route.destination} (${route.token})`; + const errorStr = route.lastError ? ` - ${route.lastError}` : ''; + console.log(` ${icon} ${routeStr}${errorStr}`); + } + console.log(''); + } + + console.log(chalk.dim(` Last updated: ${new Date().toLocaleTimeString()}`)); + console.log(''); + } + + printFinalSummary(): void { + if (!this.config.reporting.console.enabled) return; + + const routes = this.matrix.getRoutes(); + + console.log(''); + console.log(chalk.bold.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(chalk.bold.cyan(' SENTINEL - Final Report')); + console.log(chalk.bold.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(''); + + // Overall stats + const totalTests = routes.reduce((acc, r) => acc + r.totalTests, 0); + const totalSuccesses = routes.reduce((acc, r) => acc + r.totalSuccesses, 0); + const totalFailures = routes.reduce((acc, r) => acc + r.totalFailures, 0); + const successRate = totalTests > 0 ? ((totalSuccesses / totalTests) * 100).toFixed(1) : '0'; + + console.log(` Total Tests: ${totalTests}`); + console.log(` Successes: ${chalk.green(totalSuccesses)}`); + console.log(` Failures: ${chalk.red(totalFailures)}`); + console.log(` Success Rate: ${successRate}%`); + console.log(''); + + // Route breakdown + const headers = ['Route', 'Status', 'Tests', 'Pass', 'Fail', 'Avg Time']; + const rows = routes.map(r => [ + `${r.source} → ${r.destination}`, + this.getStatusIcon(r.status) + ' ' + r.status, + r.totalTests.toString(), + r.totalSuccesses.toString(), + r.totalFailures.toString(), + r.avgFulfillTimeMs ? `${(r.avgFulfillTimeMs / 1000).toFixed(1)}s` : '-', + ]); + + logger.displayTable(headers, rows); + console.log(''); + } + + private getStatusIcon(status: string): string { + switch (status) { + case 'healthy': + return chalk.green('●'); + case 'degraded': + return chalk.yellow('●'); + case 'failed': + return chalk.red('●'); + default: + return chalk.gray('●'); + } + } +} diff --git a/src/sentinel/tester.ts b/src/sentinel/tester.ts new file mode 100644 index 0000000..5d1ca36 --- /dev/null +++ b/src/sentinel/tester.ts @@ -0,0 +1,204 @@ +/** + * Sentinel Route Tester + * + * Executes a single route test (publish intent, watch for fulfillment) + */ + +import { parseUnits } from 'viem'; + +import { BasePublisher } from '@/blockchain/base-publisher'; +import { EvmPublisher } from '@/blockchain/evm-publisher'; +import { createScanner, isScanningSupported, ScanEventType } from '@/blockchain/scanner'; +import { SvmPublisher } from '@/blockchain/svm-publisher'; +import { getChainByName } from '@/config/chains'; +import { getTokenAddress, getTokenBySymbol } from '@/config/tokens'; +import { ChainType } from '@/core/interfaces/intent'; +import { AddressNormalizer } from '@/core/utils/address-normalizer'; +import { getQuote } from '@/core/utils/quote'; + +import { SentinelConfig, TestResult } from './types'; +import { WalletManager } from './wallet'; + +export class RouteTester { + private config: SentinelConfig; + private walletManager: WalletManager; + + constructor(config: SentinelConfig, walletManager: WalletManager) { + this.config = config; + this.walletManager = walletManager; + } + + async test( + source: string, + destination: string, + token: string, + amount: string + ): Promise { + const startTime = Date.now(); + const timestamp = new Date(); + + try { + const sourceChain = getChainByName(source); + const destChain = getChainByName(destination); + + if (!sourceChain) throw new Error(`Unknown source chain: ${source}`); + if (!destChain) throw new Error(`Unknown destination chain: ${destination}`); + + const tokenConfig = getTokenBySymbol(token); + if (!tokenConfig) throw new Error(`Unknown token: ${token}`); + + const rewardTokenAddr = getTokenAddress(token, sourceChain.id); + const routeTokenAddr = getTokenAddress(token, destChain.id); + + if (!rewardTokenAddr) throw new Error(`Token ${token} not available on ${source}`); + if (!routeTokenAddr) throw new Error(`Token ${token} not available on ${destination}`); + + // Get wallet address for source chain + const walletAddr = this.walletManager.getAddress(sourceChain.type); + const normalizedWallet = AddressNormalizer.normalize(walletAddr, sourceChain.type); + const denormalizedWallet = AddressNormalizer.denormalize(normalizedWallet, sourceChain.type); + + // Parse amount + const amountUnits = parseUnits(amount, tokenConfig.decimals); + + // Get quote + const quote = await getQuote({ + source: sourceChain.id, + destination: destChain.id, + funder: denormalizedWallet, + recipient: AddressNormalizer.denormalize(normalizedWallet, destChain.type), + amount: amountUnits, + routeToken: AddressNormalizer.denormalize(routeTokenAddr, destChain.type), + rewardToken: AddressNormalizer.denormalize(rewardTokenAddr, sourceChain.type), + }); + + if (!quote?.quoteResponse?.encodedRoute || !quote?.contracts?.sourcePortal) { + throw new Error('Invalid quote response'); + } + + // Build reward + const reward = { + deadline: BigInt(quote.quoteResponse.deadline), + prover: AddressNormalizer.normalize(quote.contracts.prover, sourceChain.type), + creator: normalizedWallet, + nativeAmount: 0n, + tokens: [ + { + token: rewardTokenAddr, + amount: amountUnits, + }, + ], + }; + + // Create publisher + let publisher: BasePublisher; + switch (sourceChain.type) { + case ChainType.EVM: + publisher = new EvmPublisher(sourceChain.rpcUrl); + break; + case ChainType.SVM: + publisher = new SvmPublisher(sourceChain.rpcUrl); + break; + default: + throw new Error(`Unsupported chain type: ${sourceChain.type}`); + } + + // Dry run check + if (this.config.execution.dryRun) { + return { + source, + destination, + token, + success: true, + publishTimeMs: Date.now() - startTime, + timestamp, + }; + } + + // Publish + const sourcePortal = AddressNormalizer.normalize( + quote.contracts.sourcePortal, + sourceChain.type + ); + const publishResult = await publisher.publish( + sourceChain.id, + destChain.id, + reward, + quote.quoteResponse.encodedRoute as `0x${string}`, + this.walletManager.getPrivateKey(sourceChain.type), + sourcePortal + ); + + const publishTimeMs = Date.now() - startTime; + + if (!publishResult.success) { + return { + source, + destination, + token, + success: false, + error: publishResult.error ?? 'Publish failed', + publishTimeMs, + timestamp, + }; + } + + // Watch for fulfillment + let fulfillTimeMs: number | undefined; + + if (publishResult.intentHash && quote.contracts.destinationPortal) { + if (isScanningSupported(destChain.type)) { + const scanner = createScanner({ + intentHash: publishResult.intentHash, + portalAddress: quote.contracts.destinationPortal, + rpcUrl: destChain.rpcUrl, + chainId: destChain.id, + chainType: destChain.type, + chainName: destChain.name, + timeoutMs: this.config.execution.timeoutMs, + }); + + const scanResult = await scanner.scan(ScanEventType.FULFILLMENT); + + if (scanResult.found) { + fulfillTimeMs = scanResult.elapsedMs; + } else if (scanResult.timedOut) { + return { + source, + destination, + token, + success: false, + intentHash: publishResult.intentHash, + txHash: publishResult.transactionHash, + error: 'Fulfillment timeout', + publishTimeMs, + timestamp, + }; + } + } + } + + return { + source, + destination, + token, + success: true, + intentHash: publishResult.intentHash, + txHash: publishResult.transactionHash, + publishTimeMs, + fulfillTimeMs, + timestamp, + }; + } catch (error) { + return { + source, + destination, + token, + success: false, + error: error instanceof Error ? error.message : String(error), + publishTimeMs: Date.now() - startTime, + timestamp, + }; + } + } +} diff --git a/src/sentinel/types.ts b/src/sentinel/types.ts new file mode 100644 index 0000000..a89ef92 --- /dev/null +++ b/src/sentinel/types.ts @@ -0,0 +1,104 @@ +/** + * Sentinel Types + */ + +// Wallet types - basic only for now +export interface BasicWallet { + type: 'basic'; + privateKey: string; +} + +export interface EvmWallets { + basic: BasicWallet; +} + +export interface SvmWallets { + basic: { + type: 'basic'; + secretKey: string; + }; +} + +export interface SentinelConfig { + service: { + name: string; + environment: 'development' | 'staging' | 'production'; + logLevel: 'debug' | 'info' | 'warn' | 'error'; + }; + chains: string[]; + tokens: Array<{ + symbol: string; + amount: string; + }>; + routes: { + mode: 'all' | 'explicit'; + exclude?: Array<{ + source: string; + destination: string; + }>; + }; + scheduler: { + strategy: 'periodic'; + intervalMs: number; + }; + execution: { + parallelism: number; + timeoutMs: number; + dryRun: boolean; + retries: { + maxAttempts: number; + backoffMs: number; + }; + }; + health: { + degradedAfterFailures: number; + failedAfterFailures: number; + healthyAfterSuccesses: number; + maxFulfillmentTimeMs: number; + }; + reporting: { + console: { + enabled: boolean; + verbose: boolean; + summaryIntervalMs: number; + }; + }; + evm?: { + wallets: EvmWallets; + }; + svm?: { + wallets: SvmWallets; + }; +} + +export type RouteStatus = 'healthy' | 'degraded' | 'failed' | 'unknown'; + +export interface RouteHealth { + source: string; + destination: string; + token: string; + status: RouteStatus; + consecutiveFailures: number; + consecutiveSuccesses: number; + lastCheck?: Date; + lastSuccess?: Date; + lastFailure?: Date; + lastError?: string; + avgFulfillTimeMs?: number; + totalTests: number; + totalSuccesses: number; + totalFailures: number; +} + +export interface TestResult { + source: string; + destination: string; + token: string; + success: boolean; + intentHash?: string; + txHash?: string; + publishTimeMs: number; + fulfillTimeMs?: number; + error?: string; + timestamp: Date; +} diff --git a/src/sentinel/wallet.ts b/src/sentinel/wallet.ts new file mode 100644 index 0000000..76e1187 --- /dev/null +++ b/src/sentinel/wallet.ts @@ -0,0 +1,75 @@ +/** + * Sentinel Wallet Manager + */ + +import { Keypair } from '@solana/web3.js'; +import { Hex, PrivateKeyAccount } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +import { ChainType } from '@/core/interfaces/intent'; +import { BlockchainAddress, SvmAddress } from '@/core/types/blockchain-addresses'; + +import { SentinelConfig } from './types'; + +export class WalletManager { + private evmAccount?: PrivateKeyAccount; + private evmPrivateKey?: string; + private svmKeypair?: Keypair; + private svmSecretKey?: string; + + constructor(config: SentinelConfig) { + if (config.evm?.wallets.basic) { + this.evmPrivateKey = config.evm.wallets.basic.privateKey; + this.evmAccount = privateKeyToAccount(this.evmPrivateKey as Hex); + } + + if (config.svm?.wallets.basic) { + this.svmSecretKey = config.svm.wallets.basic.secretKey; + if (this.svmSecretKey.startsWith('[')) { + const bytes = JSON.parse(this.svmSecretKey); + this.svmKeypair = Keypair.fromSecretKey(new Uint8Array(bytes)); + } else { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const bs58 = require('bs58'); + this.svmKeypair = Keypair.fromSecretKey(bs58.decode(this.svmSecretKey)); + } + } + } + + getAddress(chainType: ChainType): BlockchainAddress { + switch (chainType) { + case ChainType.EVM: + if (!this.evmAccount) throw new Error('No EVM wallet configured'); + return this.evmAccount.address; + case ChainType.SVM: + if (!this.svmKeypair) throw new Error('No SVM wallet configured'); + return this.svmKeypair.publicKey.toBase58() as SvmAddress; + default: + throw new Error(`Unsupported chain type: ${chainType}`); + } + } + + getPrivateKey(chainType: ChainType): string { + switch (chainType) { + case ChainType.EVM: + if (!this.evmPrivateKey) throw new Error('No EVM wallet configured'); + return this.evmPrivateKey; + case ChainType.SVM: + if (!this.svmSecretKey) throw new Error('No SVM wallet configured'); + return this.svmSecretKey; + default: + throw new Error(`Unsupported chain type: ${chainType}`); + } + } + + hasWallet(chainType: ChainType): boolean { + switch (chainType) { + case ChainType.EVM: + return !!this.evmAccount; + case ChainType.SVM: + return !!this.svmKeypair; + default: + return false; + } + } +}