@waiaas/daemon 2.11.0-rc.8 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/api/middleware/address-validation.d.ts +6 -33
- package/dist/api/middleware/address-validation.d.ts.map +1 -1
- package/dist/api/middleware/address-validation.js +5 -129
- package/dist/api/middleware/address-validation.js.map +1 -1
- package/dist/api/middleware/host-guard.d.ts +1 -1
- package/dist/api/middleware/host-guard.js +2 -2
- package/dist/api/middleware/host-guard.js.map +1 -1
- package/dist/api/middleware/index.d.ts +1 -0
- package/dist/api/middleware/index.d.ts.map +1 -1
- package/dist/api/middleware/index.js +1 -0
- package/dist/api/middleware/index.js.map +1 -1
- package/dist/api/middleware/master-auth.d.ts +2 -5
- package/dist/api/middleware/master-auth.d.ts.map +1 -1
- package/dist/api/middleware/master-auth.js.map +1 -1
- package/dist/api/middleware/rate-limiter.d.ts +51 -0
- package/dist/api/middleware/rate-limiter.d.ts.map +1 -0
- package/dist/api/middleware/rate-limiter.js +146 -0
- package/dist/api/middleware/rate-limiter.js.map +1 -0
- package/dist/api/middleware/siwe-verify.d.ts +6 -26
- package/dist/api/middleware/siwe-verify.d.ts.map +1 -1
- package/dist/api/middleware/siwe-verify.js +5 -50
- package/dist/api/middleware/siwe-verify.js.map +1 -1
- package/dist/api/routes/actions.d.ts +1 -0
- package/dist/api/routes/actions.d.ts.map +1 -1
- package/dist/api/routes/actions.js +52 -4
- package/dist/api/routes/actions.js.map +1 -1
- package/dist/api/routes/admin-actions.d.ts +1 -0
- package/dist/api/routes/admin-actions.d.ts.map +1 -1
- package/dist/api/routes/admin-actions.js +3 -3
- package/dist/api/routes/admin-actions.js.map +1 -1
- package/dist/api/routes/admin-auth.d.ts.map +1 -1
- package/dist/api/routes/admin-auth.js +12 -7
- package/dist/api/routes/admin-auth.js.map +1 -1
- package/dist/api/routes/admin-credentials.js +2 -2
- package/dist/api/routes/admin-credentials.js.map +1 -1
- package/dist/api/routes/admin-monitoring.d.ts +10 -0
- package/dist/api/routes/admin-monitoring.d.ts.map +1 -1
- package/dist/api/routes/admin-monitoring.js +59 -14
- package/dist/api/routes/admin-monitoring.js.map +1 -1
- package/dist/api/routes/admin-notifications.d.ts.map +1 -1
- package/dist/api/routes/admin-notifications.js +2 -15
- package/dist/api/routes/admin-notifications.js.map +1 -1
- package/dist/api/routes/admin-settings.d.ts.map +1 -1
- package/dist/api/routes/admin-settings.js +90 -1
- package/dist/api/routes/admin-settings.js.map +1 -1
- package/dist/api/routes/admin-wallets.d.ts +16 -1
- package/dist/api/routes/admin-wallets.d.ts.map +1 -1
- package/dist/api/routes/admin-wallets.js +64 -75
- package/dist/api/routes/admin-wallets.js.map +1 -1
- package/dist/api/routes/admin.d.ts +1 -0
- package/dist/api/routes/admin.d.ts.map +1 -1
- package/dist/api/routes/admin.js.map +1 -1
- package/dist/api/routes/credentials.js +2 -2
- package/dist/api/routes/credentials.js.map +1 -1
- package/dist/api/routes/defi-positions.js.map +1 -1
- package/dist/api/routes/nfts.js.map +1 -1
- package/dist/api/routes/openapi-schemas.d.ts +412 -12
- package/dist/api/routes/openapi-schemas.d.ts.map +1 -1
- package/dist/api/routes/openapi-schemas.js +38 -5
- package/dist/api/routes/openapi-schemas.js.map +1 -1
- package/dist/api/routes/policies.d.ts +2 -0
- package/dist/api/routes/policies.d.ts.map +1 -1
- package/dist/api/routes/policies.js +55 -6
- package/dist/api/routes/policies.js.map +1 -1
- package/dist/api/routes/rpc-proxy.js.map +1 -1
- package/dist/api/routes/sessions.d.ts.map +1 -1
- package/dist/api/routes/sessions.js +47 -28
- package/dist/api/routes/sessions.js.map +1 -1
- package/dist/api/routes/staking.d.ts.map +1 -1
- package/dist/api/routes/staking.js +4 -76
- package/dist/api/routes/staking.js.map +1 -1
- package/dist/api/routes/tokens.d.ts.map +1 -1
- package/dist/api/routes/tokens.js.map +1 -1
- package/dist/api/routes/transactions.d.ts +1 -0
- package/dist/api/routes/transactions.d.ts.map +1 -1
- package/dist/api/routes/transactions.js +8 -2
- package/dist/api/routes/transactions.js.map +1 -1
- package/dist/api/routes/userop.d.ts.map +1 -1
- package/dist/api/routes/userop.js +0 -2
- package/dist/api/routes/userop.js.map +1 -1
- package/dist/api/routes/wallet-apps.d.ts.map +1 -1
- package/dist/api/routes/wallet-apps.js +20 -13
- package/dist/api/routes/wallet-apps.js.map +1 -1
- package/dist/api/routes/wallet.js.map +1 -1
- package/dist/api/routes/wallets.d.ts.map +1 -1
- package/dist/api/routes/wallets.js +3 -0
- package/dist/api/routes/wallets.js.map +1 -1
- package/dist/api/routes/wc.d.ts.map +1 -1
- package/dist/api/routes/wc.js +13 -8
- package/dist/api/routes/wc.js.map +1 -1
- package/dist/api/routes/x402.d.ts.map +1 -1
- package/dist/api/routes/x402.js +1 -2
- package/dist/api/routes/x402.js.map +1 -1
- package/dist/api/server.d.ts +8 -4
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +46 -5
- package/dist/api/server.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/infrastructure/action/action-provider-registry.d.ts.map +1 -1
- package/dist/infrastructure/action/action-provider-registry.js +2 -3
- package/dist/infrastructure/action/action-provider-registry.js.map +1 -1
- package/dist/infrastructure/action/builtin-metadata.d.ts +22 -0
- package/dist/infrastructure/action/builtin-metadata.d.ts.map +1 -0
- package/dist/infrastructure/action/builtin-metadata.js +29 -0
- package/dist/infrastructure/action/builtin-metadata.js.map +1 -0
- package/dist/infrastructure/adapter-pool.d.ts +2 -1
- package/dist/infrastructure/adapter-pool.d.ts.map +1 -1
- package/dist/infrastructure/adapter-pool.js.map +1 -1
- package/dist/infrastructure/auth/address-validation.d.ts +38 -0
- package/dist/infrastructure/auth/address-validation.d.ts.map +1 -0
- package/dist/infrastructure/auth/address-validation.js +134 -0
- package/dist/infrastructure/auth/address-validation.js.map +1 -0
- package/dist/infrastructure/auth/siwe-verify.d.ts +34 -0
- package/dist/infrastructure/auth/siwe-verify.d.ts.map +1 -0
- package/dist/infrastructure/auth/siwe-verify.js +58 -0
- package/dist/infrastructure/auth/siwe-verify.js.map +1 -0
- package/dist/infrastructure/auth/types.d.ts +12 -0
- package/dist/infrastructure/auth/types.d.ts.map +1 -0
- package/dist/infrastructure/auth/types.js +8 -0
- package/dist/infrastructure/auth/types.js.map +1 -0
- package/dist/infrastructure/config/loader.d.ts +1 -10
- package/dist/infrastructure/config/loader.d.ts.map +1 -1
- package/dist/infrastructure/config/loader.js +0 -2
- package/dist/infrastructure/config/loader.js.map +1 -1
- package/dist/infrastructure/database/migrate.d.ts +6 -18
- package/dist/infrastructure/database/migrate.d.ts.map +1 -1
- package/dist/infrastructure/database/migrate.js +25 -2856
- package/dist/infrastructure/database/migrate.js.map +1 -1
- package/dist/infrastructure/database/migrations/v11-v20.d.ts +17 -0
- package/dist/infrastructure/database/migrations/v11-v20.d.ts.map +1 -0
- package/dist/infrastructure/database/migrations/v11-v20.js +295 -0
- package/dist/infrastructure/database/migrations/v11-v20.js.map +1 -0
- package/dist/infrastructure/database/migrations/v2-v10.d.ts +16 -0
- package/dist/infrastructure/database/migrations/v2-v10.d.ts.map +1 -0
- package/dist/infrastructure/database/migrations/v2-v10.js +539 -0
- package/dist/infrastructure/database/migrations/v2-v10.js.map +1 -0
- package/dist/infrastructure/database/migrations/v21-v30.d.ts +17 -0
- package/dist/infrastructure/database/migrations/v21-v30.d.ts.map +1 -0
- package/dist/infrastructure/database/migrations/v21-v30.js +507 -0
- package/dist/infrastructure/database/migrations/v21-v30.js.map +1 -0
- package/dist/infrastructure/database/migrations/v31-v40.d.ts +17 -0
- package/dist/infrastructure/database/migrations/v31-v40.d.ts.map +1 -0
- package/dist/infrastructure/database/migrations/v31-v40.js +203 -0
- package/dist/infrastructure/database/migrations/v31-v40.js.map +1 -0
- package/dist/infrastructure/database/migrations/v41-v50.d.ts +17 -0
- package/dist/infrastructure/database/migrations/v41-v50.d.ts.map +1 -0
- package/dist/infrastructure/database/migrations/v41-v50.js +188 -0
- package/dist/infrastructure/database/migrations/v41-v50.js.map +1 -0
- package/dist/infrastructure/database/migrations/v51-v59.d.ts +17 -0
- package/dist/infrastructure/database/migrations/v51-v59.d.ts.map +1 -0
- package/dist/infrastructure/database/migrations/v51-v59.js +420 -0
- package/dist/infrastructure/database/migrations/v51-v59.js.map +1 -0
- package/dist/infrastructure/database/schema-ddl.d.ts +24 -0
- package/dist/infrastructure/database/schema-ddl.d.ts.map +1 -0
- package/dist/infrastructure/database/schema-ddl.js +596 -0
- package/dist/infrastructure/database/schema-ddl.js.map +1 -0
- package/dist/infrastructure/database/schema.d.ts +38 -0
- package/dist/infrastructure/database/schema.d.ts.map +1 -1
- package/dist/infrastructure/database/schema.js +2 -0
- package/dist/infrastructure/database/schema.js.map +1 -1
- package/dist/infrastructure/jwt/jwt-secret-manager.d.ts.map +1 -1
- package/dist/infrastructure/jwt/jwt-secret-manager.js +16 -3
- package/dist/infrastructure/jwt/jwt-secret-manager.js.map +1 -1
- package/dist/infrastructure/nft/alchemy-nft-indexer.d.ts.map +1 -1
- package/dist/infrastructure/nft/alchemy-nft-indexer.js +0 -1
- package/dist/infrastructure/nft/alchemy-nft-indexer.js.map +1 -1
- package/dist/infrastructure/nft/helius-nft-indexer.d.ts.map +1 -1
- package/dist/infrastructure/nft/helius-nft-indexer.js +1 -2
- package/dist/infrastructure/nft/helius-nft-indexer.js.map +1 -1
- package/dist/infrastructure/nft/nft-indexer-client.d.ts.map +1 -1
- package/dist/infrastructure/nft/nft-indexer-client.js +0 -2
- package/dist/infrastructure/nft/nft-indexer-client.js.map +1 -1
- package/dist/infrastructure/security/ssrf-guard.d.ts +33 -0
- package/dist/infrastructure/security/ssrf-guard.d.ts.map +1 -0
- package/dist/infrastructure/security/ssrf-guard.js +244 -0
- package/dist/infrastructure/security/ssrf-guard.js.map +1 -0
- package/dist/infrastructure/settings/hot-reload.d.ts +1 -1
- package/dist/infrastructure/settings/hot-reload.d.ts.map +1 -1
- package/dist/infrastructure/settings/hot-reload.js +0 -2
- package/dist/infrastructure/settings/hot-reload.js.map +1 -1
- package/dist/infrastructure/settings/index.d.ts +2 -2
- package/dist/infrastructure/settings/index.d.ts.map +1 -1
- package/dist/infrastructure/settings/index.js +1 -1
- package/dist/infrastructure/settings/index.js.map +1 -1
- package/dist/infrastructure/settings/setting-keys.d.ts +14 -0
- package/dist/infrastructure/settings/setting-keys.d.ts.map +1 -1
- package/dist/infrastructure/settings/setting-keys.js +296 -214
- package/dist/infrastructure/settings/setting-keys.js.map +1 -1
- package/dist/infrastructure/settings/settings-service.d.ts +6 -1
- package/dist/infrastructure/settings/settings-service.d.ts.map +1 -1
- package/dist/infrastructure/settings/settings-service.js +15 -5
- package/dist/infrastructure/settings/settings-service.js.map +1 -1
- package/dist/infrastructure/telegram/telegram-bot-service.d.ts.map +1 -1
- package/dist/infrastructure/telegram/telegram-bot-service.js +3 -2
- package/dist/infrastructure/telegram/telegram-bot-service.js.map +1 -1
- package/dist/infrastructure/token-registry/builtin-tokens.d.ts.map +1 -1
- package/dist/infrastructure/token-registry/builtin-tokens.js +4 -7
- package/dist/infrastructure/token-registry/builtin-tokens.js.map +1 -1
- package/dist/lifecycle/daemon-pipeline.d.ts +49 -0
- package/dist/lifecycle/daemon-pipeline.d.ts.map +1 -0
- package/dist/lifecycle/daemon-pipeline.js +281 -0
- package/dist/lifecycle/daemon-pipeline.js.map +1 -0
- package/dist/lifecycle/daemon-shutdown.d.ts +14 -0
- package/dist/lifecycle/daemon-shutdown.d.ts.map +1 -0
- package/dist/lifecycle/daemon-shutdown.js +176 -0
- package/dist/lifecycle/daemon-shutdown.js.map +1 -0
- package/dist/lifecycle/daemon-startup.d.ts +15 -0
- package/dist/lifecycle/daemon-startup.d.ts.map +1 -0
- package/dist/lifecycle/daemon-startup.js +1527 -0
- package/dist/lifecycle/daemon-startup.js.map +1 -0
- package/dist/lifecycle/daemon.d.ts +171 -114
- package/dist/lifecycle/daemon.d.ts.map +1 -1
- package/dist/lifecycle/daemon.js +22 -1904
- package/dist/lifecycle/daemon.js.map +1 -1
- package/dist/notifications/channels/discord.d.ts.map +1 -1
- package/dist/notifications/channels/discord.js +1 -0
- package/dist/notifications/channels/discord.js.map +1 -1
- package/dist/notifications/channels/slack.d.ts.map +1 -1
- package/dist/notifications/channels/slack.js +1 -0
- package/dist/notifications/channels/slack.js.map +1 -1
- package/dist/notifications/index.d.ts +0 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +0 -1
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/notification-service.d.ts.map +1 -1
- package/dist/notifications/notification-service.js +8 -6
- package/dist/notifications/notification-service.js.map +1 -1
- package/dist/pipeline/database-policy-engine.d.ts +18 -438
- package/dist/pipeline/database-policy-engine.d.ts.map +1 -1
- package/dist/pipeline/database-policy-engine.js +154 -1321
- package/dist/pipeline/database-policy-engine.js.map +1 -1
- package/dist/pipeline/dry-run.d.ts +5 -2
- package/dist/pipeline/dry-run.d.ts.map +1 -1
- package/dist/pipeline/dry-run.js +102 -8
- package/dist/pipeline/dry-run.js.map +1 -1
- package/dist/pipeline/evaluators/allowed-tokens.d.ts +28 -0
- package/dist/pipeline/evaluators/allowed-tokens.d.ts.map +1 -0
- package/dist/pipeline/evaluators/allowed-tokens.js +129 -0
- package/dist/pipeline/evaluators/allowed-tokens.js.map +1 -0
- package/dist/pipeline/evaluators/approved-spenders.d.ts +26 -0
- package/dist/pipeline/evaluators/approved-spenders.d.ts.map +1 -0
- package/dist/pipeline/evaluators/approved-spenders.js +115 -0
- package/dist/pipeline/evaluators/approved-spenders.js.map +1 -0
- package/dist/pipeline/evaluators/contract-whitelist.d.ts +28 -0
- package/dist/pipeline/evaluators/contract-whitelist.d.ts.map +1 -0
- package/dist/pipeline/evaluators/contract-whitelist.js +168 -0
- package/dist/pipeline/evaluators/contract-whitelist.js.map +1 -0
- package/dist/pipeline/evaluators/helpers.d.ts +9 -0
- package/dist/pipeline/evaluators/helpers.d.ts.map +1 -0
- package/dist/pipeline/evaluators/helpers.js +13 -0
- package/dist/pipeline/evaluators/helpers.js.map +1 -0
- package/dist/pipeline/evaluators/lending-asset-whitelist.d.ts +18 -0
- package/dist/pipeline/evaluators/lending-asset-whitelist.d.ts.map +1 -0
- package/dist/pipeline/evaluators/lending-asset-whitelist.js +44 -0
- package/dist/pipeline/evaluators/lending-asset-whitelist.js.map +1 -0
- package/dist/pipeline/evaluators/lending-ltv-limit.d.ts +24 -0
- package/dist/pipeline/evaluators/lending-ltv-limit.d.ts.map +1 -0
- package/dist/pipeline/evaluators/lending-ltv-limit.js +130 -0
- package/dist/pipeline/evaluators/lending-ltv-limit.js.map +1 -0
- package/dist/pipeline/evaluators/spending-limit.d.ts +46 -0
- package/dist/pipeline/evaluators/spending-limit.d.ts.map +1 -0
- package/dist/pipeline/evaluators/spending-limit.js +241 -0
- package/dist/pipeline/evaluators/spending-limit.js.map +1 -0
- package/dist/pipeline/evaluators/types.d.ts +71 -0
- package/dist/pipeline/evaluators/types.d.ts.map +1 -0
- package/dist/pipeline/evaluators/types.js +7 -0
- package/dist/pipeline/evaluators/types.js.map +1 -0
- package/dist/pipeline/external-action-pipeline.js.map +1 -1
- package/dist/pipeline/gas-condition-tracker.d.ts +1 -1
- package/dist/pipeline/gas-condition-tracker.js +1 -1
- package/dist/pipeline/pipeline-helpers.d.ts +146 -0
- package/dist/pipeline/pipeline-helpers.d.ts.map +1 -0
- package/dist/pipeline/pipeline-helpers.js +260 -0
- package/dist/pipeline/pipeline-helpers.js.map +1 -0
- package/dist/pipeline/pipeline.d.ts +1 -0
- package/dist/pipeline/pipeline.d.ts.map +1 -1
- package/dist/pipeline/pipeline.js +3 -2
- package/dist/pipeline/pipeline.js.map +1 -1
- package/dist/pipeline/resolve-effective-amount-usd.d.ts.map +1 -1
- package/dist/pipeline/resolve-effective-amount-usd.js +4 -10
- package/dist/pipeline/resolve-effective-amount-usd.js.map +1 -1
- package/dist/pipeline/sign-message.js +1 -1
- package/dist/pipeline/sign-message.js.map +1 -1
- package/dist/pipeline/sleep.d.ts +1 -5
- package/dist/pipeline/sleep.d.ts.map +1 -1
- package/dist/pipeline/sleep.js +2 -7
- package/dist/pipeline/sleep.js.map +1 -1
- package/dist/pipeline/stage1-validate.d.ts +8 -0
- package/dist/pipeline/stage1-validate.d.ts.map +1 -0
- package/dist/pipeline/stage1-validate.js +69 -0
- package/dist/pipeline/stage1-validate.js.map +1 -0
- package/dist/pipeline/stage2-auth.d.ts +12 -0
- package/dist/pipeline/stage2-auth.d.ts.map +1 -0
- package/dist/pipeline/stage2-auth.js +18 -0
- package/dist/pipeline/stage2-auth.js.map +1 -0
- package/dist/pipeline/stage3-policy.d.ts +26 -0
- package/dist/pipeline/stage3-policy.d.ts.map +1 -0
- package/dist/pipeline/stage3-policy.js +384 -0
- package/dist/pipeline/stage3-policy.js.map +1 -0
- package/dist/pipeline/stage4-wait.d.ts +8 -0
- package/dist/pipeline/stage4-wait.d.ts.map +1 -0
- package/dist/pipeline/stage4-wait.js +87 -0
- package/dist/pipeline/stage4-wait.js.map +1 -0
- package/dist/pipeline/stage5-execute.d.ts +120 -0
- package/dist/pipeline/stage5-execute.d.ts.map +1 -0
- package/dist/pipeline/stage5-execute.js +1070 -0
- package/dist/pipeline/stage5-execute.js.map +1 -0
- package/dist/pipeline/stage6-confirm.d.ts +11 -0
- package/dist/pipeline/stage6-confirm.d.ts.map +1 -0
- package/dist/pipeline/stage6-confirm.js +110 -0
- package/dist/pipeline/stage6-confirm.js.map +1 -0
- package/dist/pipeline/stages.d.ts +11 -245
- package/dist/pipeline/stages.d.ts.map +1 -1
- package/dist/pipeline/stages.js +11 -1896
- package/dist/pipeline/stages.js.map +1 -1
- package/dist/rpc-proxy/sync-pipeline.js +2 -2
- package/dist/rpc-proxy/sync-pipeline.js.map +1 -1
- package/dist/services/autostop/autostop-service.d.ts +4 -1
- package/dist/services/autostop/autostop-service.d.ts.map +1 -1
- package/dist/services/autostop/autostop-service.js +27 -7
- package/dist/services/autostop/autostop-service.js.map +1 -1
- package/dist/services/defi/position-tracker.d.ts +5 -0
- package/dist/services/defi/position-tracker.d.ts.map +1 -1
- package/dist/services/defi/position-tracker.js +41 -6
- package/dist/services/defi/position-tracker.js.map +1 -1
- package/dist/services/defi/position-write-queue.d.ts.map +1 -1
- package/dist/services/defi/position-write-queue.js +3 -2
- package/dist/services/defi/position-write-queue.js.map +1 -1
- package/dist/services/incoming/__tests__/integration-wiring.test.js +58 -0
- package/dist/services/incoming/__tests__/integration-wiring.test.js.map +1 -1
- package/dist/services/incoming/incoming-tx-monitor-service.d.ts.map +1 -1
- package/dist/services/incoming/incoming-tx-monitor-service.js +11 -14
- package/dist/services/incoming/incoming-tx-monitor-service.js.map +1 -1
- package/dist/services/incoming/incoming-tx-workers.d.ts +2 -2
- package/dist/services/incoming/incoming-tx-workers.d.ts.map +1 -1
- package/dist/services/incoming/incoming-tx-workers.js +1 -1
- package/dist/services/incoming/incoming-tx-workers.js.map +1 -1
- package/dist/services/incoming/safety-rules.d.ts.map +1 -1
- package/dist/services/incoming/safety-rules.js +3 -2
- package/dist/services/incoming/safety-rules.js.map +1 -1
- package/dist/services/incoming/subscription-multiplexer.d.ts +2 -6
- package/dist/services/incoming/subscription-multiplexer.d.ts.map +1 -1
- package/dist/services/incoming/subscription-multiplexer.js +1 -3
- package/dist/services/incoming/subscription-multiplexer.js.map +1 -1
- package/dist/services/monitoring/balance-monitor-service.d.ts.map +1 -1
- package/dist/services/monitoring/balance-monitor-service.js +2 -2
- package/dist/services/monitoring/balance-monitor-service.js.map +1 -1
- package/dist/services/signing-sdk/approval-channel-router.d.ts +7 -7
- package/dist/services/signing-sdk/approval-channel-router.d.ts.map +1 -1
- package/dist/services/signing-sdk/approval-channel-router.js +13 -13
- package/dist/services/signing-sdk/approval-channel-router.js.map +1 -1
- package/dist/services/signing-sdk/channels/index.d.ts +2 -2
- package/dist/services/signing-sdk/channels/index.d.ts.map +1 -1
- package/dist/services/signing-sdk/channels/index.js +1 -1
- package/dist/services/signing-sdk/channels/index.js.map +1 -1
- package/dist/services/signing-sdk/channels/push-relay-signing-channel.d.ts +59 -0
- package/dist/services/signing-sdk/channels/push-relay-signing-channel.d.ts.map +1 -0
- package/dist/services/signing-sdk/channels/push-relay-signing-channel.js +190 -0
- package/dist/services/signing-sdk/channels/push-relay-signing-channel.js.map +1 -0
- package/dist/services/signing-sdk/channels/telegram-signing-channel.d.ts +1 -1
- package/dist/services/signing-sdk/channels/telegram-signing-channel.js +1 -1
- package/dist/services/signing-sdk/channels/wallet-notification-channel.d.ts +6 -7
- package/dist/services/signing-sdk/channels/wallet-notification-channel.d.ts.map +1 -1
- package/dist/services/signing-sdk/channels/wallet-notification-channel.js +38 -55
- package/dist/services/signing-sdk/channels/wallet-notification-channel.js.map +1 -1
- package/dist/services/signing-sdk/index.d.ts +3 -3
- package/dist/services/signing-sdk/index.d.ts.map +1 -1
- package/dist/services/signing-sdk/index.js +2 -2
- package/dist/services/signing-sdk/index.js.map +1 -1
- package/dist/services/signing-sdk/preset-auto-setup.js +2 -2
- package/dist/services/signing-sdk/preset-auto-setup.js.map +1 -1
- package/dist/services/signing-sdk/sign-request-builder.d.ts +2 -2
- package/dist/services/signing-sdk/sign-request-builder.d.ts.map +1 -1
- package/dist/services/signing-sdk/sign-request-builder.js +17 -25
- package/dist/services/signing-sdk/sign-request-builder.js.map +1 -1
- package/dist/services/signing-sdk/wallet-app-service.d.ts +4 -0
- package/dist/services/signing-sdk/wallet-app-service.d.ts.map +1 -1
- package/dist/services/signing-sdk/wallet-app-service.js +12 -5
- package/dist/services/signing-sdk/wallet-app-service.js.map +1 -1
- package/dist/services/staking/aggregate-staking-balance.d.ts +24 -0
- package/dist/services/staking/aggregate-staking-balance.d.ts.map +1 -0
- package/dist/services/staking/aggregate-staking-balance.js +82 -0
- package/dist/services/staking/aggregate-staking-balance.js.map +1 -0
- package/dist/services/wc-session-service.d.ts.map +1 -1
- package/dist/services/wc-session-service.js +2 -1
- package/dist/services/wc-session-service.js.map +1 -1
- package/dist/services/wc-signing-bridge.js +2 -2
- package/dist/services/wc-signing-bridge.js.map +1 -1
- package/dist/services/x402/payment-signer.d.ts.map +1 -1
- package/dist/services/x402/payment-signer.js +2 -5
- package/dist/services/x402/payment-signer.js.map +1 -1
- package/dist/services/x402/ssrf-guard.d.ts +4 -23
- package/dist/services/x402/ssrf-guard.d.ts.map +1 -1
- package/dist/services/x402/ssrf-guard.js +3 -232
- package/dist/services/x402/ssrf-guard.js.map +1 -1
- package/dist/signing/capabilities/eip712-signer.d.ts.map +1 -1
- package/dist/signing/capabilities/eip712-signer.js +2 -0
- package/dist/signing/capabilities/eip712-signer.js.map +1 -1
- package/package.json +5 -5
- package/public/admin/assets/index-CpFF2lCo.js +3 -0
- package/public/admin/index.html +1 -1
- package/dist/notifications/channels/ntfy.d.ts +0 -13
- package/dist/notifications/channels/ntfy.d.ts.map +0 -1
- package/dist/notifications/channels/ntfy.js +0 -74
- package/dist/notifications/channels/ntfy.js.map +0 -1
- package/dist/services/signing-sdk/channels/ntfy-signing-channel.d.ts +0 -66
- package/dist/services/signing-sdk/channels/ntfy-signing-channel.d.ts.map +0 -1
- package/dist/services/signing-sdk/channels/ntfy-signing-channel.js +0 -270
- package/dist/services/signing-sdk/channels/ntfy-signing-channel.js.map +0 -1
- package/public/admin/assets/index-CQ3i4P2U.js +0 -3
|
@@ -1,84 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DatabasePolicyEngine - v1.2 DB-backed policy engine with network scoping.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* ALLOWED_NETWORKS (network whitelist, permissive default),
|
|
7
|
-
* ALLOWED_TOKENS (token transfer whitelist, default deny),
|
|
8
|
-
* CONTRACT_WHITELIST (contract call whitelist, default deny),
|
|
9
|
-
* METHOD_WHITELIST (optional method-level restriction for contract calls),
|
|
10
|
-
* APPROVED_SPENDERS (approve spender whitelist, default deny),
|
|
11
|
-
* APPROVE_AMOUNT_LIMIT (unlimited approve block + amount cap),
|
|
12
|
-
* and APPROVE_TIER_OVERRIDE (forced tier for APPROVE transactions).
|
|
13
|
-
*
|
|
14
|
-
* Algorithm:
|
|
15
|
-
* 1. Load enabled policies for wallet (wallet-specific + global), ORDER BY priority DESC
|
|
16
|
-
* 2. If no policies found, return INSTANT passthrough (Phase 7 compat)
|
|
17
|
-
* 3. Resolve overrides: 4-level priority (wallet+network > wallet+null > global+network > global+null)
|
|
18
|
-
* 4. Evaluate WHITELIST: deny if toAddress not in allowed_addresses
|
|
19
|
-
* 4a.5. Evaluate ALLOWED_NETWORKS: deny if network not in allowed list (permissive default)
|
|
20
|
-
* 4b. Evaluate ALLOWED_TOKENS: deny TOKEN_TRANSFER if no policy or token not whitelisted
|
|
21
|
-
* 4c. Evaluate CONTRACT_WHITELIST: deny CONTRACT_CALL if no policy or contract not whitelisted
|
|
22
|
-
* 4d. Evaluate METHOD_WHITELIST: deny CONTRACT_CALL if method selector not whitelisted (optional)
|
|
23
|
-
* 4e. Evaluate APPROVED_SPENDERS: deny APPROVE if no policy or spender not approved
|
|
24
|
-
* 4f. Evaluate APPROVE_AMOUNT_LIMIT: deny APPROVE if unlimited or exceeds max amount
|
|
25
|
-
* 4g. Evaluate APPROVE_TIER_OVERRIDE: force tier for APPROVE (defaults to APPROVAL, skips SPENDING_LIMIT)
|
|
26
|
-
* 4h. Evaluate LENDING_ASSET_WHITELIST: deny lending action if asset not whitelisted (default-deny)
|
|
27
|
-
* 4h-b. Evaluate LENDING_LTV_LIMIT: deny borrow if projected LTV exceeds maxLtv
|
|
28
|
-
* 4i. Evaluate PERP_ALLOWED_MARKETS: deny perp action if market not whitelisted (default-deny)
|
|
29
|
-
* 4i-b. Evaluate PERP_MAX_LEVERAGE: deny open/modify if leverage exceeds max
|
|
30
|
-
* 4i-c. Evaluate PERP_MAX_POSITION_USD: deny open/modify if position USD exceeds max
|
|
31
|
-
* 5. Evaluate SPENDING_LIMIT: classify amount into INSTANT/NOTIFY/DELAY/APPROVAL
|
|
32
|
-
* (skip for non-spending lending actions: supply/repay/withdraw)
|
|
4
|
+
* This file contains the orchestration class that dispatches to evaluator modules
|
|
5
|
+
* in the evaluators/ directory. Each policy type has its own evaluator file.
|
|
33
6
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
7
|
+
* Evaluates transactions against policies stored in the policies table.
|
|
8
|
+
* Supports SPENDING_LIMIT, WHITELIST, ALLOWED_NETWORKS, ALLOWED_TOKENS,
|
|
9
|
+
* CONTRACT_WHITELIST, METHOD_WHITELIST, APPROVED_SPENDERS, APPROVE_AMOUNT_LIMIT,
|
|
10
|
+
* APPROVE_TIER_OVERRIDE, LENDING_ASSET_WHITELIST, LENDING_LTV_LIMIT,
|
|
11
|
+
* PERP_ALLOWED_MARKETS, PERP_MAX_LEVERAGE, PERP_MAX_POSITION_USD,
|
|
12
|
+
* VENUE_WHITELIST, ACTION_CATEGORY_LIMIT, and REPUTATION_THRESHOLD.
|
|
38
13
|
*
|
|
39
14
|
* @see docs/33-time-lock-approval-mechanism.md
|
|
40
15
|
* @see docs/71-policy-engine-network-extension-design.md
|
|
41
16
|
*/
|
|
42
|
-
import {
|
|
17
|
+
import { safeJsonParse, WAIaaSError, SpendingLimitRulesSchema, ApproveTierOverrideRulesSchema, ReputationThresholdRulesSchema, } from '@waiaas/core';
|
|
18
|
+
import {} from 'zod';
|
|
43
19
|
import { eq, or, and, isNull, desc } from 'drizzle-orm';
|
|
44
20
|
import { policies, wallets, agentIdentities } from '../infrastructure/database/schema.js';
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
* Parse a human-readable decimal string (e.g. "1.5", "1000") to raw bigint units.
|
|
54
|
-
* Multiplies the value by 10^decimals for precise BigInt comparison.
|
|
55
|
-
*
|
|
56
|
-
* Examples:
|
|
57
|
-
* parseDecimalToBigInt("1.5", 9) -> 1500000000n (1.5 SOL in lamports)
|
|
58
|
-
* parseDecimalToBigInt("1000", 6) -> 1000000000n (1000 USDC in raw)
|
|
59
|
-
*/
|
|
60
|
-
function parseDecimalToBigInt(value, decimals) {
|
|
61
|
-
const parts = value.split('.');
|
|
62
|
-
const integerPart = parts[0] ?? '0';
|
|
63
|
-
let fractionalPart = parts[1] ?? '';
|
|
64
|
-
// Pad or truncate fractional part to exactly `decimals` digits
|
|
65
|
-
if (fractionalPart.length > decimals) {
|
|
66
|
-
fractionalPart = fractionalPart.slice(0, decimals);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
fractionalPart = fractionalPart.padEnd(decimals, '0');
|
|
70
|
-
}
|
|
71
|
-
return BigInt(integerPart + fractionalPart);
|
|
72
|
-
}
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Tier order + maxTier helper (Phase 127)
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
const TIER_ORDER = ['INSTANT', 'NOTIFY', 'DELAY', 'APPROVAL'];
|
|
77
|
-
function maxTier(a, b) {
|
|
78
|
-
const aIdx = TIER_ORDER.indexOf(a);
|
|
79
|
-
const bIdx = TIER_ORDER.indexOf(b);
|
|
80
|
-
return TIER_ORDER[Math.max(aIdx, bIdx)];
|
|
81
|
-
}
|
|
21
|
+
// Evaluator imports
|
|
22
|
+
import { evaluateWhitelist, evaluateAllowedNetworks, evaluateAllowedTokens } from './evaluators/allowed-tokens.js';
|
|
23
|
+
import { evaluateContractWhitelist, evaluateMethodWhitelist, evaluateVenueWhitelist, evaluatePerpAllowedMarkets } from './evaluators/contract-whitelist.js';
|
|
24
|
+
import { evaluateApprovedSpenders, evaluateApproveAmountLimit, evaluateApproveTierOverride } from './evaluators/approved-spenders.js';
|
|
25
|
+
import { evaluateSpendingLimit, evaluateActionCategoryLimit } from './evaluators/spending-limit.js';
|
|
26
|
+
import { evaluateLendingAssetWhitelist } from './evaluators/lending-asset-whitelist.js';
|
|
27
|
+
import { evaluateLendingLtvLimit, evaluatePerpMaxLeverage, evaluatePerpMaxPositionUsd } from './evaluators/lending-ltv-limit.js';
|
|
28
|
+
import { maxTier } from './evaluators/helpers.js';
|
|
82
29
|
// ---------------------------------------------------------------------------
|
|
83
30
|
// DatabasePolicyEngine
|
|
84
31
|
// ---------------------------------------------------------------------------
|
|
@@ -89,9 +36,6 @@ function maxTier(a, b) {
|
|
|
89
36
|
*
|
|
90
37
|
* Network scoping: policies can target specific networks via the `network` column.
|
|
91
38
|
* 4-level override priority: wallet+network > wallet+null > global+network > global+null.
|
|
92
|
-
*
|
|
93
|
-
* Constructor takes a Drizzle DB instance typed with the full schema,
|
|
94
|
-
* and optionally a raw better-sqlite3 Database instance for BEGIN IMMEDIATE transactions.
|
|
95
39
|
*/
|
|
96
40
|
export class DatabasePolicyEngine {
|
|
97
41
|
db;
|
|
@@ -104,6 +48,24 @@ export class DatabasePolicyEngine {
|
|
|
104
48
|
this.settingsService = settingsService ?? null;
|
|
105
49
|
this.reputationCacheService = reputationCacheService ?? null;
|
|
106
50
|
}
|
|
51
|
+
/** Evaluator context with parseRules + settingsService access. */
|
|
52
|
+
get ctx() {
|
|
53
|
+
return {
|
|
54
|
+
parseRules: this.parseRules.bind(this),
|
|
55
|
+
settingsService: this.settingsService,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parse policy rules JSON with Zod validation.
|
|
60
|
+
* Throws POLICY_RULES_CORRUPT on invalid JSON or schema mismatch.
|
|
61
|
+
*/
|
|
62
|
+
parseRules(rules, zodSchema, policyType) {
|
|
63
|
+
const result = safeJsonParse(rules, zodSchema);
|
|
64
|
+
if (!result.success) {
|
|
65
|
+
throw new WAIaaSError('POLICY_RULES_CORRUPT', { message: `${policyType} policy rules corrupt: ${result.error.message}` });
|
|
66
|
+
}
|
|
67
|
+
return result.data;
|
|
68
|
+
}
|
|
107
69
|
/**
|
|
108
70
|
* Evaluate a transaction against DB-stored policies.
|
|
109
71
|
*/
|
|
@@ -119,138 +81,117 @@ export class DatabasePolicyEngine {
|
|
|
119
81
|
if (rows.length === 0) {
|
|
120
82
|
return { allowed: true, tier: 'INSTANT' };
|
|
121
83
|
}
|
|
122
|
-
// Step 3: Resolve overrides
|
|
84
|
+
// Step 3: Resolve overrides
|
|
123
85
|
const resolved = this.resolveOverrides(rows, walletId, transaction.network);
|
|
86
|
+
const ctx = this.ctx;
|
|
124
87
|
// Step 4: Evaluate WHITELIST (deny-first)
|
|
125
|
-
const whitelistResult =
|
|
126
|
-
if (whitelistResult !== null)
|
|
88
|
+
const whitelistResult = evaluateWhitelist(ctx, resolved, transaction.toAddress);
|
|
89
|
+
if (whitelistResult !== null)
|
|
127
90
|
return whitelistResult;
|
|
128
|
-
|
|
129
|
-
// Step 4a.5: Evaluate ALLOWED_NETWORKS (network whitelist, permissive default)
|
|
91
|
+
// Step 4a.5: Evaluate ALLOWED_NETWORKS
|
|
130
92
|
if (transaction.network) {
|
|
131
|
-
const allowedNetworksResult =
|
|
132
|
-
if (allowedNetworksResult !== null)
|
|
93
|
+
const allowedNetworksResult = evaluateAllowedNetworks(ctx, resolved, transaction.network);
|
|
94
|
+
if (allowedNetworksResult !== null)
|
|
133
95
|
return allowedNetworksResult;
|
|
134
|
-
}
|
|
135
96
|
}
|
|
136
|
-
// Step 4b: Evaluate ALLOWED_TOKENS
|
|
137
|
-
const allowedTokensResult =
|
|
138
|
-
if (allowedTokensResult !== null)
|
|
97
|
+
// Step 4b: Evaluate ALLOWED_TOKENS
|
|
98
|
+
const allowedTokensResult = evaluateAllowedTokens(ctx, resolved, transaction);
|
|
99
|
+
if (allowedTokensResult !== null)
|
|
139
100
|
return allowedTokensResult;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (contractWhitelistResult !== null) {
|
|
101
|
+
// Step 4c: Evaluate CONTRACT_WHITELIST
|
|
102
|
+
const contractWhitelistResult = evaluateContractWhitelist(ctx, resolved, transaction);
|
|
103
|
+
if (contractWhitelistResult !== null)
|
|
144
104
|
return contractWhitelistResult;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (methodWhitelistResult !== null) {
|
|
105
|
+
// Step 4d: Evaluate METHOD_WHITELIST
|
|
106
|
+
const methodWhitelistResult = evaluateMethodWhitelist(ctx, resolved, transaction);
|
|
107
|
+
if (methodWhitelistResult !== null)
|
|
149
108
|
return methodWhitelistResult;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (approvedSpendersResult !== null) {
|
|
109
|
+
// Step 4e: Evaluate APPROVED_SPENDERS
|
|
110
|
+
const approvedSpendersResult = evaluateApprovedSpenders(ctx, resolved, transaction);
|
|
111
|
+
if (approvedSpendersResult !== null)
|
|
154
112
|
return approvedSpendersResult;
|
|
155
|
-
|
|
156
|
-
// Step 4e.5: Evaluate REPUTATION_THRESHOLD (counterparty reputation check, Phase 320)
|
|
113
|
+
// Step 4e.5: Evaluate REPUTATION_THRESHOLD (async, kept in this class)
|
|
157
114
|
const reputationFloorTier = await this.evaluateReputationThreshold(resolved, transaction);
|
|
158
|
-
// Step 4f: Evaluate APPROVE_AMOUNT_LIMIT
|
|
159
|
-
const approveAmountResult =
|
|
160
|
-
if (approveAmountResult !== null)
|
|
115
|
+
// Step 4f: Evaluate APPROVE_AMOUNT_LIMIT
|
|
116
|
+
const approveAmountResult = evaluateApproveAmountLimit(ctx, resolved, transaction);
|
|
117
|
+
if (approveAmountResult !== null)
|
|
161
118
|
return approveAmountResult;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return approveTierResult; // FINAL result, skips SPENDING_LIMIT (including token_limits)
|
|
167
|
-
}
|
|
119
|
+
// Step 4g: Evaluate APPROVE_TIER_OVERRIDE
|
|
120
|
+
const approveTierResult = evaluateApproveTierOverride(ctx, resolved, transaction);
|
|
121
|
+
if (approveTierResult !== null)
|
|
122
|
+
return approveTierResult;
|
|
168
123
|
// Step 4g.5: NFT_TRANSFER default tier APPROVAL (PLCY-03, v31.0)
|
|
169
|
-
// NFT transfers default to APPROVAL tier (owner approval required) unless overridden.
|
|
170
124
|
if (transaction.type === 'NFT_TRANSFER') {
|
|
171
125
|
let nftTierOverride = null;
|
|
172
126
|
try {
|
|
173
127
|
nftTierOverride = this.settingsService?.get('policy.nft_transfer_default_tier') ?? null;
|
|
174
128
|
}
|
|
175
|
-
catch {
|
|
176
|
-
// Setting key not registered yet -- use default
|
|
177
|
-
}
|
|
129
|
+
catch { /* Setting key not registered yet */ }
|
|
178
130
|
const nftTier = (nftTierOverride ?? 'APPROVAL');
|
|
179
131
|
const finalTier = reputationFloorTier ? maxTier(nftTier, reputationFloorTier) : nftTier;
|
|
180
132
|
return { allowed: true, tier: finalTier };
|
|
181
133
|
}
|
|
182
134
|
// v31.14: CONTRACT_DEPLOY default tier APPROVAL (DEPL-04)
|
|
183
|
-
// Contract deployments default to APPROVAL tier (owner approval required) unless overridden.
|
|
184
135
|
if (transaction.type === 'CONTRACT_DEPLOY') {
|
|
185
136
|
let deployTierOverride = null;
|
|
186
137
|
try {
|
|
187
138
|
deployTierOverride = this.settingsService?.get('rpc_proxy.deploy_default_tier') ?? null;
|
|
188
139
|
}
|
|
189
|
-
catch {
|
|
190
|
-
// Setting key not registered yet -- use default
|
|
191
|
-
}
|
|
140
|
+
catch { /* Setting key not registered yet */ }
|
|
192
141
|
const deployTier = (deployTierOverride ?? 'APPROVAL');
|
|
193
142
|
const finalTier = reputationFloorTier ? maxTier(deployTier, reputationFloorTier) : deployTier;
|
|
194
143
|
return { allowed: true, tier: finalTier };
|
|
195
144
|
}
|
|
196
|
-
// Step 4h: Evaluate LENDING_ASSET_WHITELIST
|
|
197
|
-
const lendingAssetResult =
|
|
198
|
-
if (lendingAssetResult !== null)
|
|
145
|
+
// Step 4h: Evaluate LENDING_ASSET_WHITELIST
|
|
146
|
+
const lendingAssetResult = evaluateLendingAssetWhitelist(ctx, resolved, transaction);
|
|
147
|
+
if (lendingAssetResult !== null)
|
|
199
148
|
return lendingAssetResult;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (ltvResult !== null) {
|
|
149
|
+
// Step 4h-b: Evaluate LENDING_LTV_LIMIT
|
|
150
|
+
const ltvResult = evaluateLendingLtvLimit(ctx, resolved, transaction, walletId, this.sqlite);
|
|
151
|
+
if (ltvResult !== null)
|
|
204
152
|
return ltvResult;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const perpMarketResult = this.evaluatePerpAllowedMarkets(resolved, transaction);
|
|
153
|
+
// Step 4i: Evaluate PERP_ALLOWED_MARKETS
|
|
154
|
+
const perpMarketResult = evaluatePerpAllowedMarkets(ctx, resolved, transaction);
|
|
208
155
|
if (perpMarketResult !== null)
|
|
209
156
|
return perpMarketResult;
|
|
210
|
-
// Step 4i-b: Evaluate PERP_MAX_LEVERAGE
|
|
211
|
-
const leverageResult =
|
|
157
|
+
// Step 4i-b: Evaluate PERP_MAX_LEVERAGE
|
|
158
|
+
const leverageResult = evaluatePerpMaxLeverage(ctx, resolved, transaction);
|
|
212
159
|
if (leverageResult !== null)
|
|
213
160
|
return leverageResult;
|
|
214
|
-
// Step 4i-c: Evaluate PERP_MAX_POSITION_USD
|
|
215
|
-
const positionUsdResult =
|
|
161
|
+
// Step 4i-c: Evaluate PERP_MAX_POSITION_USD
|
|
162
|
+
const positionUsdResult = evaluatePerpMaxPositionUsd(ctx, resolved, transaction);
|
|
216
163
|
if (positionUsdResult !== null)
|
|
217
164
|
return positionUsdResult;
|
|
218
|
-
// Step 4j: Evaluate VENUE_WHITELIST
|
|
219
|
-
const venueResult =
|
|
165
|
+
// Step 4j: Evaluate VENUE_WHITELIST
|
|
166
|
+
const venueResult = evaluateVenueWhitelist(ctx, resolved, transaction);
|
|
220
167
|
if (venueResult !== null)
|
|
221
168
|
return venueResult;
|
|
222
|
-
// Step 4k: Evaluate ACTION_CATEGORY_LIMIT
|
|
223
|
-
const categoryLimitResult =
|
|
169
|
+
// Step 4k: Evaluate ACTION_CATEGORY_LIMIT
|
|
170
|
+
const categoryLimitResult = evaluateActionCategoryLimit(ctx, resolved, transaction, walletId, this.sqlite);
|
|
224
171
|
if (categoryLimitResult !== null) {
|
|
225
|
-
// ACTION_CATEGORY_LIMIT returns allowed:true with escalated tier
|
|
226
|
-
// Combine with reputation floor tier and return
|
|
227
172
|
if (reputationFloorTier) {
|
|
228
173
|
categoryLimitResult.tier = maxTier(categoryLimitResult.tier, reputationFloorTier);
|
|
229
174
|
}
|
|
230
175
|
return categoryLimitResult;
|
|
231
176
|
}
|
|
232
|
-
// Step 5: Non-spending classification for lending actions
|
|
233
|
-
// supply/repay/withdraw are non-spending — skip SPENDING_LIMIT entirely
|
|
177
|
+
// Step 5: Non-spending classification for lending actions
|
|
234
178
|
const NON_SPENDING_ACTIONS = new Set(['supply', 'repay', 'withdraw', 'close_position', 'add_margin']);
|
|
235
179
|
if (transaction.actionName && NON_SPENDING_ACTIONS.has(transaction.actionName)) {
|
|
236
180
|
const baseTier = 'INSTANT';
|
|
237
181
|
const finalTier = reputationFloorTier ? maxTier(baseTier, reputationFloorTier) : baseTier;
|
|
238
182
|
return { allowed: true, tier: finalTier };
|
|
239
183
|
}
|
|
240
|
-
// Step 5 (continued): Evaluate SPENDING_LIMIT
|
|
241
|
-
// Phase 236: Build tokenContext from transaction for token_limits evaluation
|
|
184
|
+
// Step 5 (continued): Evaluate SPENDING_LIMIT
|
|
242
185
|
const spendingPolicy = resolved.find((p) => p.type === 'SPENDING_LIMIT');
|
|
243
186
|
const tokenContext = this.buildTokenContext(transaction, spendingPolicy);
|
|
244
|
-
const spendingResult =
|
|
187
|
+
const spendingResult = evaluateSpendingLimit(ctx, resolved, transaction.amount, undefined, tokenContext);
|
|
245
188
|
if (spendingResult !== null) {
|
|
246
|
-
// [Phase 320] Apply reputation floor tier via maxTier (escalation only)
|
|
247
189
|
if (reputationFloorTier) {
|
|
248
190
|
spendingResult.tier = maxTier(spendingResult.tier, reputationFloorTier);
|
|
249
191
|
}
|
|
250
192
|
return spendingResult;
|
|
251
193
|
}
|
|
252
|
-
// Default: INSTANT passthrough
|
|
253
|
-
// [Phase 320] Apply reputation floor tier if set
|
|
194
|
+
// Default: INSTANT passthrough
|
|
254
195
|
const defaultTier = 'INSTANT';
|
|
255
196
|
const finalDefaultTier = reputationFloorTier ? maxTier(defaultTier, reputationFloorTier) : defaultTier;
|
|
256
197
|
return { allowed: true, tier: finalDefaultTier };
|
|
@@ -258,23 +199,7 @@ export class DatabasePolicyEngine {
|
|
|
258
199
|
// -------------------------------------------------------------------------
|
|
259
200
|
// Batch evaluation: evaluateBatch
|
|
260
201
|
// -------------------------------------------------------------------------
|
|
261
|
-
/**
|
|
262
|
-
* Evaluate a batch of instructions using 2-stage policy evaluation.
|
|
263
|
-
*
|
|
264
|
-
* Phase A: Evaluate each instruction individually against its applicable policies.
|
|
265
|
-
* All-or-Nothing: if any instruction is denied, entire batch is denied.
|
|
266
|
-
*
|
|
267
|
-
* Phase B: Sum native amounts (TRANSFER.amount) and evaluate
|
|
268
|
-
* aggregate against SPENDING_LIMIT. If batch contains APPROVE, apply
|
|
269
|
-
* APPROVE_TIER_OVERRIDE and take max(amount tier, approve tier).
|
|
270
|
-
*
|
|
271
|
-
* @param walletId - Wallet whose policies to evaluate
|
|
272
|
-
* @param instructions - Array of instruction parameters (same shape as TransactionParam)
|
|
273
|
-
* @returns PolicyEvaluation with final tier or denial with violation details
|
|
274
|
-
*/
|
|
275
202
|
async evaluateBatch(walletId, instructions, batchUsdAmount) {
|
|
276
|
-
// Step 1: Load policies with network filter
|
|
277
|
-
// All instructions in a BATCH share the same network
|
|
278
203
|
const resolvedNetwork = instructions[0]?.network;
|
|
279
204
|
const rows = await this.db
|
|
280
205
|
.select()
|
|
@@ -282,16 +207,15 @@ export class DatabasePolicyEngine {
|
|
|
282
207
|
.where(and(or(eq(policies.walletId, walletId), isNull(policies.walletId)), or(resolvedNetwork ? eq(policies.network, resolvedNetwork) : isNull(policies.network), isNull(policies.network)), eq(policies.enabled, true)))
|
|
283
208
|
.orderBy(desc(policies.priority))
|
|
284
209
|
.all();
|
|
285
|
-
if (rows.length === 0)
|
|
210
|
+
if (rows.length === 0)
|
|
286
211
|
return { allowed: true, tier: 'INSTANT' };
|
|
287
|
-
}
|
|
288
212
|
const resolved = this.resolveOverrides(rows, walletId, resolvedNetwork);
|
|
213
|
+
const ctx = this.ctx;
|
|
289
214
|
// ALLOWED_NETWORKS evaluation before Phase A
|
|
290
215
|
if (resolvedNetwork) {
|
|
291
|
-
const allowedNetworksResult =
|
|
292
|
-
if (allowedNetworksResult !== null)
|
|
216
|
+
const allowedNetworksResult = evaluateAllowedNetworks(ctx, resolved, resolvedNetwork);
|
|
217
|
+
if (allowedNetworksResult !== null)
|
|
293
218
|
return allowedNetworksResult;
|
|
294
|
-
}
|
|
295
219
|
}
|
|
296
220
|
// Phase A: Evaluate each instruction individually
|
|
297
221
|
const violations = [];
|
|
@@ -321,94 +245,72 @@ export class DatabasePolicyEngine {
|
|
|
321
245
|
if (instr.type === 'TRANSFER') {
|
|
322
246
|
totalNativeAmount += BigInt(instr.amount);
|
|
323
247
|
}
|
|
324
|
-
// TOKEN_TRANSFER and APPROVE: 0 (no native amount)
|
|
325
|
-
// CONTRACT_CALL: Solana has no native value attachment in CPI, so 0
|
|
326
248
|
}
|
|
327
|
-
|
|
328
|
-
// BATCH: token_limits not applicable -- aggregate native amount evaluated via raw/USD only (tokenContext intentionally omitted)
|
|
329
|
-
const amountTier = this.evaluateSpendingLimit(resolved, totalNativeAmount.toString(), batchUsdAmount);
|
|
249
|
+
const amountTier = evaluateSpendingLimit(ctx, resolved, totalNativeAmount.toString(), batchUsdAmount);
|
|
330
250
|
let finalTier = amountTier ? amountTier.tier : 'INSTANT';
|
|
331
251
|
// If batch contains APPROVE, apply APPROVE_TIER_OVERRIDE
|
|
332
252
|
const hasApprove = instructions.some((i) => i.type === 'APPROVE');
|
|
333
253
|
if (hasApprove) {
|
|
334
|
-
// Get approve tier from APPROVE_TIER_OVERRIDE policy (or default APPROVAL)
|
|
335
254
|
const approveTierPolicy = resolved.find((p) => p.type === 'APPROVE_TIER_OVERRIDE');
|
|
336
255
|
let approveTier;
|
|
337
256
|
if (approveTierPolicy) {
|
|
338
|
-
const rules =
|
|
257
|
+
const rules = this.parseRules(approveTierPolicy.rules, ApproveTierOverrideRulesSchema, 'APPROVE_TIER_OVERRIDE');
|
|
339
258
|
approveTier = rules.tier;
|
|
340
259
|
}
|
|
341
260
|
else {
|
|
342
261
|
approveTier = 'APPROVAL';
|
|
343
262
|
}
|
|
344
|
-
// Final tier = max(amount tier, approve tier)
|
|
345
263
|
const tierOrder = ['INSTANT', 'NOTIFY', 'DELAY', 'APPROVAL'];
|
|
346
264
|
const amountIdx = tierOrder.indexOf(finalTier);
|
|
347
265
|
const approveIdx = tierOrder.indexOf(approveTier);
|
|
348
266
|
finalTier = tierOrder[Math.max(amountIdx, approveIdx)];
|
|
349
267
|
}
|
|
350
|
-
return {
|
|
351
|
-
allowed: true,
|
|
352
|
-
tier: finalTier,
|
|
353
|
-
};
|
|
268
|
+
return { allowed: true, tier: finalTier };
|
|
354
269
|
}
|
|
355
270
|
// -------------------------------------------------------------------------
|
|
356
271
|
// Private: Per-instruction policy evaluation (Phase A helper)
|
|
357
272
|
// -------------------------------------------------------------------------
|
|
358
|
-
/**
|
|
359
|
-
* Evaluate applicable policies for a single instruction in a batch.
|
|
360
|
-
*
|
|
361
|
-
* Only evaluates type-specific policies:
|
|
362
|
-
* - TRANSFER: WHITELIST
|
|
363
|
-
* - TOKEN_TRANSFER: WHITELIST + ALLOWED_TOKENS
|
|
364
|
-
* - CONTRACT_CALL: CONTRACT_WHITELIST + METHOD_WHITELIST
|
|
365
|
-
* - APPROVE: APPROVED_SPENDERS + APPROVE_AMOUNT_LIMIT
|
|
366
|
-
*
|
|
367
|
-
* Does NOT evaluate SPENDING_LIMIT (that's Phase B aggregate) or
|
|
368
|
-
* APPROVE_TIER_OVERRIDE (that's Phase B).
|
|
369
|
-
*
|
|
370
|
-
* Returns null if all policies pass, PolicyEvaluation with allowed=false if denied.
|
|
371
|
-
*/
|
|
372
273
|
evaluateInstructionPolicies(resolved, instr) {
|
|
274
|
+
const ctx = this.ctx;
|
|
373
275
|
// WHITELIST applies to TRANSFER and TOKEN_TRANSFER
|
|
374
276
|
if (instr.type === 'TRANSFER' || instr.type === 'TOKEN_TRANSFER') {
|
|
375
|
-
const whitelistResult =
|
|
277
|
+
const whitelistResult = evaluateWhitelist(ctx, resolved, instr.toAddress);
|
|
376
278
|
if (whitelistResult !== null)
|
|
377
279
|
return whitelistResult;
|
|
378
280
|
}
|
|
379
281
|
// ALLOWED_TOKENS applies to TOKEN_TRANSFER
|
|
380
282
|
if (instr.type === 'TOKEN_TRANSFER') {
|
|
381
|
-
const allowedTokensResult =
|
|
283
|
+
const allowedTokensResult = evaluateAllowedTokens(ctx, resolved, instr);
|
|
382
284
|
if (allowedTokensResult !== null)
|
|
383
285
|
return allowedTokensResult;
|
|
384
286
|
}
|
|
385
287
|
// CONTRACT_WHITELIST applies to CONTRACT_CALL
|
|
386
288
|
if (instr.type === 'CONTRACT_CALL') {
|
|
387
|
-
const contractResult =
|
|
289
|
+
const contractResult = evaluateContractWhitelist(ctx, resolved, instr);
|
|
388
290
|
if (contractResult !== null)
|
|
389
291
|
return contractResult;
|
|
390
|
-
const methodResult =
|
|
292
|
+
const methodResult = evaluateMethodWhitelist(ctx, resolved, instr);
|
|
391
293
|
if (methodResult !== null)
|
|
392
294
|
return methodResult;
|
|
393
295
|
}
|
|
394
296
|
// APPROVED_SPENDERS + APPROVE_AMOUNT_LIMIT apply to APPROVE
|
|
395
297
|
if (instr.type === 'APPROVE') {
|
|
396
|
-
const spendersResult =
|
|
298
|
+
const spendersResult = evaluateApprovedSpenders(ctx, resolved, instr);
|
|
397
299
|
if (spendersResult !== null)
|
|
398
300
|
return spendersResult;
|
|
399
|
-
const amountResult =
|
|
301
|
+
const amountResult = evaluateApproveAmountLimit(ctx, resolved, instr);
|
|
400
302
|
if (amountResult !== null)
|
|
401
303
|
return amountResult;
|
|
402
304
|
}
|
|
403
|
-
// LENDING_ASSET_WHITELIST applies to lending actions
|
|
305
|
+
// LENDING_ASSET_WHITELIST applies to lending actions
|
|
404
306
|
if (instr.type === 'CONTRACT_CALL') {
|
|
405
|
-
const lendingAssetResult =
|
|
307
|
+
const lendingAssetResult = evaluateLendingAssetWhitelist(ctx, resolved, instr);
|
|
406
308
|
if (lendingAssetResult !== null)
|
|
407
309
|
return lendingAssetResult;
|
|
408
310
|
}
|
|
409
|
-
// PERP_ALLOWED_MARKETS applies to perp actions
|
|
311
|
+
// PERP_ALLOWED_MARKETS applies to perp actions
|
|
410
312
|
if (instr.type === 'CONTRACT_CALL') {
|
|
411
|
-
const perpMarketResult =
|
|
313
|
+
const perpMarketResult = evaluatePerpAllowedMarkets(ctx, resolved, instr);
|
|
412
314
|
if (perpMarketResult !== null)
|
|
413
315
|
return perpMarketResult;
|
|
414
316
|
}
|
|
@@ -417,32 +319,13 @@ export class DatabasePolicyEngine {
|
|
|
417
319
|
// -------------------------------------------------------------------------
|
|
418
320
|
// TOCTOU Prevention: evaluateAndReserve
|
|
419
321
|
// -------------------------------------------------------------------------
|
|
420
|
-
/**
|
|
421
|
-
* Evaluate transaction and reserve amount atomically using BEGIN IMMEDIATE.
|
|
422
|
-
*
|
|
423
|
-
* This method:
|
|
424
|
-
* 1. Begins an IMMEDIATE transaction (exclusive write lock)
|
|
425
|
-
* 2. Loads policies (same as evaluate)
|
|
426
|
-
* 3. For SPENDING_LIMIT: computes current reserved total from PENDING/QUEUED txs
|
|
427
|
-
* 4. Adds current request amount to reserved total
|
|
428
|
-
* 5. Evaluates against limits with reserved total considered
|
|
429
|
-
* 6. If allowed: sets reserved_amount on the transaction row
|
|
430
|
-
* 7. Commits
|
|
431
|
-
*
|
|
432
|
-
* @param walletId - The wallet whose policies to evaluate
|
|
433
|
-
* @param transaction - Transaction details for evaluation
|
|
434
|
-
* @param txId - The transaction ID to update with reserved_amount
|
|
435
|
-
* @returns PolicyEvaluation result
|
|
436
|
-
* @throws Error if sqlite instance not provided in constructor
|
|
437
|
-
*/
|
|
438
322
|
evaluateAndReserve(walletId, transaction, txId, usdAmount, reputationFloorTier) {
|
|
439
323
|
if (!this.sqlite) {
|
|
440
324
|
throw new Error('evaluateAndReserve requires raw sqlite instance in constructor');
|
|
441
325
|
}
|
|
442
326
|
const sqlite = this.sqlite;
|
|
443
|
-
|
|
327
|
+
const ctx = this.ctx;
|
|
444
328
|
const txn = sqlite.transaction(() => {
|
|
445
|
-
// Step 1: Load enabled policies via raw SQL with network filter (inside IMMEDIATE txn)
|
|
446
329
|
const policyRows = sqlite
|
|
447
330
|
.prepare(`SELECT id, wallet_id AS walletId, type, rules, priority, enabled, network
|
|
448
331
|
FROM policies
|
|
@@ -451,103 +334,73 @@ export class DatabasePolicyEngine {
|
|
|
451
334
|
AND enabled = 1
|
|
452
335
|
ORDER BY priority DESC`)
|
|
453
336
|
.all(walletId, transaction.network ?? null);
|
|
454
|
-
// Step 2: No policies -> INSTANT passthrough (apply reputation floor if set)
|
|
455
337
|
if (policyRows.length === 0) {
|
|
456
338
|
const baseTier = 'INSTANT';
|
|
457
339
|
const effectiveTier = reputationFloorTier ? maxTier(baseTier, reputationFloorTier) : baseTier;
|
|
458
340
|
return { allowed: true, tier: effectiveTier };
|
|
459
341
|
}
|
|
460
|
-
// Step 3: Resolve overrides (4-level with network)
|
|
461
342
|
const resolved = this.resolveOverrides(policyRows, walletId, transaction.network);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (whitelistResult !== null) {
|
|
343
|
+
const whitelistResult = evaluateWhitelist(ctx, resolved, transaction.toAddress);
|
|
344
|
+
if (whitelistResult !== null)
|
|
465
345
|
return whitelistResult;
|
|
466
|
-
}
|
|
467
|
-
// Step 4a.5: Evaluate ALLOWED_NETWORKS (network whitelist, permissive default)
|
|
468
346
|
if (transaction.network) {
|
|
469
|
-
const allowedNetworksResult =
|
|
470
|
-
if (allowedNetworksResult !== null)
|
|
347
|
+
const allowedNetworksResult = evaluateAllowedNetworks(ctx, resolved, transaction.network);
|
|
348
|
+
if (allowedNetworksResult !== null)
|
|
471
349
|
return allowedNetworksResult;
|
|
472
|
-
}
|
|
473
350
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (allowedTokensResult !== null) {
|
|
351
|
+
const allowedTokensResult = evaluateAllowedTokens(ctx, resolved, transaction);
|
|
352
|
+
if (allowedTokensResult !== null)
|
|
477
353
|
return allowedTokensResult;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const contractWhitelistResult = this.evaluateContractWhitelist(resolved, transaction);
|
|
481
|
-
if (contractWhitelistResult !== null) {
|
|
354
|
+
const contractWhitelistResult = evaluateContractWhitelist(ctx, resolved, transaction);
|
|
355
|
+
if (contractWhitelistResult !== null)
|
|
482
356
|
return contractWhitelistResult;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const methodWhitelistResult = this.evaluateMethodWhitelist(resolved, transaction);
|
|
486
|
-
if (methodWhitelistResult !== null) {
|
|
357
|
+
const methodWhitelistResult = evaluateMethodWhitelist(ctx, resolved, transaction);
|
|
358
|
+
if (methodWhitelistResult !== null)
|
|
487
359
|
return methodWhitelistResult;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const approvedSpendersResult = this.evaluateApprovedSpenders(resolved, transaction);
|
|
491
|
-
if (approvedSpendersResult !== null) {
|
|
360
|
+
const approvedSpendersResult = evaluateApprovedSpenders(ctx, resolved, transaction);
|
|
361
|
+
if (approvedSpendersResult !== null)
|
|
492
362
|
return approvedSpendersResult;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const approveAmountResult = this.evaluateApproveAmountLimit(resolved, transaction);
|
|
496
|
-
if (approveAmountResult !== null) {
|
|
363
|
+
const approveAmountResult = evaluateApproveAmountLimit(ctx, resolved, transaction);
|
|
364
|
+
if (approveAmountResult !== null)
|
|
497
365
|
return approveAmountResult;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
// Step 4h: Evaluate LENDING_ASSET_WHITELIST (default-deny for lending assets)
|
|
505
|
-
const lendingAssetResult = this.evaluateLendingAssetWhitelist(resolved, transaction);
|
|
506
|
-
if (lendingAssetResult !== null) {
|
|
366
|
+
const approveTierResult = evaluateApproveTierOverride(ctx, resolved, transaction);
|
|
367
|
+
if (approveTierResult !== null)
|
|
368
|
+
return approveTierResult;
|
|
369
|
+
const lendingAssetResult = evaluateLendingAssetWhitelist(ctx, resolved, transaction);
|
|
370
|
+
if (lendingAssetResult !== null)
|
|
507
371
|
return lendingAssetResult;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const ltvResult = this.evaluateLendingLtvLimit(resolved, transaction, walletId, usdAmount);
|
|
511
|
-
if (ltvResult !== null) {
|
|
372
|
+
const ltvResult = evaluateLendingLtvLimit(ctx, resolved, transaction, walletId, sqlite, usdAmount);
|
|
373
|
+
if (ltvResult !== null)
|
|
512
374
|
return ltvResult;
|
|
513
|
-
|
|
514
|
-
// Step 4i: Evaluate PERP_ALLOWED_MARKETS (default-deny for perp markets)
|
|
515
|
-
const perpMarketResult = this.evaluatePerpAllowedMarkets(resolved, transaction);
|
|
375
|
+
const perpMarketResult = evaluatePerpAllowedMarkets(ctx, resolved, transaction);
|
|
516
376
|
if (perpMarketResult !== null)
|
|
517
377
|
return perpMarketResult;
|
|
518
|
-
|
|
519
|
-
const leverageResult = this.evaluatePerpMaxLeverage(resolved, transaction);
|
|
378
|
+
const leverageResult = evaluatePerpMaxLeverage(ctx, resolved, transaction);
|
|
520
379
|
if (leverageResult !== null)
|
|
521
380
|
return leverageResult;
|
|
522
|
-
|
|
523
|
-
const positionUsdResult = this.evaluatePerpMaxPositionUsd(resolved, transaction);
|
|
381
|
+
const positionUsdResult = evaluatePerpMaxPositionUsd(ctx, resolved, transaction);
|
|
524
382
|
if (positionUsdResult !== null)
|
|
525
383
|
return positionUsdResult;
|
|
526
|
-
|
|
527
|
-
const venueResult = this.evaluateVenueWhitelist(resolved, transaction);
|
|
384
|
+
const venueResult = evaluateVenueWhitelist(ctx, resolved, transaction);
|
|
528
385
|
if (venueResult !== null)
|
|
529
386
|
return venueResult;
|
|
530
|
-
|
|
531
|
-
const categoryLimitResult = this.evaluateActionCategoryLimit(resolved, transaction, walletId);
|
|
387
|
+
const categoryLimitResult = evaluateActionCategoryLimit(ctx, resolved, transaction, walletId, sqlite);
|
|
532
388
|
if (categoryLimitResult !== null) {
|
|
533
389
|
if (reputationFloorTier) {
|
|
534
390
|
categoryLimitResult.tier = maxTier(categoryLimitResult.tier, reputationFloorTier);
|
|
535
391
|
}
|
|
536
392
|
return categoryLimitResult;
|
|
537
393
|
}
|
|
538
|
-
//
|
|
539
|
-
// supply/repay/withdraw are non-spending — skip SPENDING_LIMIT entirely
|
|
394
|
+
// Non-spending actions
|
|
540
395
|
const NON_SPENDING_ACTIONS_R = new Set(['supply', 'repay', 'withdraw', 'close_position', 'add_margin']);
|
|
541
396
|
if (transaction.actionName && NON_SPENDING_ACTIONS_R.has(transaction.actionName)) {
|
|
542
397
|
const baseTier = 'INSTANT';
|
|
543
398
|
const finalTier = reputationFloorTier ? maxTier(baseTier, reputationFloorTier) : baseTier;
|
|
544
399
|
return { allowed: true, tier: finalTier };
|
|
545
400
|
}
|
|
546
|
-
//
|
|
401
|
+
// SPENDING_LIMIT with reservation
|
|
547
402
|
const spendingPolicy = resolved.find((p) => p.type === 'SPENDING_LIMIT');
|
|
548
403
|
if (spendingPolicy) {
|
|
549
|
-
// Sum of reserved_amount for wallet's PENDING/QUEUED/SIGNED transactions
|
|
550
|
-
// SIGNED included for sign-only pipeline reservation (TOCTOU prevention)
|
|
551
404
|
const reservedRow = sqlite
|
|
552
405
|
.prepare(`SELECT COALESCE(SUM(CAST(reserved_amount AS INTEGER)), 0) AS total
|
|
553
406
|
FROM transactions
|
|
@@ -558,22 +411,19 @@ export class DatabasePolicyEngine {
|
|
|
558
411
|
const reservedTotal = BigInt(reservedRow.total);
|
|
559
412
|
const requestAmount = BigInt(transaction.amount);
|
|
560
413
|
const effectiveAmount = reservedTotal + requestAmount;
|
|
561
|
-
// Evaluate with effective amount (reserved + current) via unified evaluateSpendingLimit
|
|
562
|
-
// Phase 236: Build tokenContext for token_limits evaluation
|
|
563
414
|
const tokenContext = this.buildTokenContext(transaction, spendingPolicy);
|
|
564
|
-
const spendingResult =
|
|
565
|
-
//
|
|
415
|
+
const spendingResult = evaluateSpendingLimit(ctx, resolved, effectiveAmount.toString(), usdAmount, tokenContext);
|
|
416
|
+
// Cumulative USD limit evaluation
|
|
566
417
|
if (usdAmount !== undefined && usdAmount > 0) {
|
|
567
|
-
const rules =
|
|
418
|
+
const rules = this.parseRules(spendingPolicy.rules, SpendingLimitRulesSchema, 'SPENDING_LIMIT');
|
|
568
419
|
const hasCumulativeLimits = rules.daily_limit_usd !== undefined || rules.monthly_limit_usd !== undefined;
|
|
569
420
|
if (hasCumulativeLimits) {
|
|
570
421
|
const now = Math.floor(Date.now() / 1000);
|
|
571
422
|
let cumulativeTier = 'INSTANT';
|
|
572
423
|
let cumulativeReason;
|
|
573
424
|
let cumulativeWarning;
|
|
574
|
-
// 6a: Daily (24h rolling window)
|
|
575
425
|
if (rules.daily_limit_usd !== undefined) {
|
|
576
|
-
const windowStart = now - 86400;
|
|
426
|
+
const windowStart = now - 86400;
|
|
577
427
|
const spent = this.getCumulativeUsdSpent(sqlite, walletId, windowStart);
|
|
578
428
|
const totalWithCurrent = spent + usdAmount;
|
|
579
429
|
if (totalWithCurrent > rules.daily_limit_usd) {
|
|
@@ -581,16 +431,14 @@ export class DatabasePolicyEngine {
|
|
|
581
431
|
cumulativeReason = 'cumulative_daily';
|
|
582
432
|
}
|
|
583
433
|
else {
|
|
584
|
-
// 80% warning check
|
|
585
434
|
const ratio = totalWithCurrent / rules.daily_limit_usd;
|
|
586
435
|
if (ratio >= 0.8) {
|
|
587
436
|
cumulativeWarning = { type: 'daily', ratio, spent: totalWithCurrent, limit: rules.daily_limit_usd };
|
|
588
437
|
}
|
|
589
438
|
}
|
|
590
439
|
}
|
|
591
|
-
// 6b: Monthly (30-day rolling window) -- only if daily didn't already escalate
|
|
592
440
|
if (rules.monthly_limit_usd !== undefined && cumulativeReason === undefined) {
|
|
593
|
-
const windowStart = now - 2592000;
|
|
441
|
+
const windowStart = now - 2592000;
|
|
594
442
|
const spent = this.getCumulativeUsdSpent(sqlite, walletId, windowStart);
|
|
595
443
|
const totalWithCurrent = spent + usdAmount;
|
|
596
444
|
if (totalWithCurrent > rules.monthly_limit_usd) {
|
|
@@ -598,26 +446,21 @@ export class DatabasePolicyEngine {
|
|
|
598
446
|
cumulativeReason = 'cumulative_monthly';
|
|
599
447
|
}
|
|
600
448
|
else if (!cumulativeWarning) {
|
|
601
|
-
// 80% warning check (only if daily warning not already set)
|
|
602
449
|
const ratio = totalWithCurrent / rules.monthly_limit_usd;
|
|
603
450
|
if (ratio >= 0.8) {
|
|
604
451
|
cumulativeWarning = { type: 'monthly', ratio, spent: totalWithCurrent, limit: rules.monthly_limit_usd };
|
|
605
452
|
}
|
|
606
453
|
}
|
|
607
454
|
}
|
|
608
|
-
// Step 7: Final tier = max(per-tx tier, cumulative tier)
|
|
609
455
|
const perTxTier = spendingResult?.tier ?? 'INSTANT';
|
|
610
456
|
const finalTier = maxTier(perTxTier, cumulativeTier);
|
|
611
|
-
// Determine approvalReason
|
|
612
457
|
let approvalReason;
|
|
613
458
|
if (finalTier === 'APPROVAL') {
|
|
614
459
|
approvalReason = cumulativeReason ?? 'per_tx';
|
|
615
460
|
}
|
|
616
|
-
// Record USD amounts + reserved_amount
|
|
617
461
|
sqlite
|
|
618
462
|
.prepare(`UPDATE transactions SET reserved_amount = ?, amount_usd = ?, reserved_amount_usd = ? WHERE id = ?`)
|
|
619
463
|
.run(transaction.amount, usdAmount, usdAmount, txId);
|
|
620
|
-
// [Phase 320] Apply reputation floor tier via maxTier (escalation only)
|
|
621
464
|
const repFinalTier = reputationFloorTier ? maxTier(finalTier, reputationFloorTier) : finalTier;
|
|
622
465
|
return {
|
|
623
466
|
allowed: true,
|
|
@@ -628,8 +471,7 @@ export class DatabasePolicyEngine {
|
|
|
628
471
|
};
|
|
629
472
|
}
|
|
630
473
|
}
|
|
631
|
-
// No cumulative limits
|
|
632
|
-
// Set reserved_amount on the transaction row + USD amounts if available
|
|
474
|
+
// No cumulative limits
|
|
633
475
|
if (usdAmount !== undefined) {
|
|
634
476
|
sqlite
|
|
635
477
|
.prepare(`UPDATE transactions SET reserved_amount = ?, amount_usd = ?, reserved_amount_usd = ? WHERE id = ?`)
|
|
@@ -640,30 +482,21 @@ export class DatabasePolicyEngine {
|
|
|
640
482
|
.prepare(`UPDATE transactions SET reserved_amount = ? WHERE id = ?`)
|
|
641
483
|
.run(transaction.amount, txId);
|
|
642
484
|
}
|
|
643
|
-
// [Phase 320] Apply reputation floor tier via maxTier (escalation only)
|
|
644
485
|
const baseResult = spendingResult ?? { allowed: true, tier: 'INSTANT' };
|
|
645
486
|
if (reputationFloorTier && baseResult.allowed) {
|
|
646
487
|
baseResult.tier = maxTier(baseResult.tier, reputationFloorTier);
|
|
647
488
|
}
|
|
648
489
|
return baseResult;
|
|
649
490
|
}
|
|
650
|
-
// No SPENDING_LIMIT -> INSTANT passthrough
|
|
651
|
-
// [Phase 320] Apply reputation floor tier if set
|
|
491
|
+
// No SPENDING_LIMIT -> INSTANT passthrough
|
|
652
492
|
const noSpendingTier = reputationFloorTier ?? 'INSTANT';
|
|
653
493
|
return { allowed: true, tier: noSpendingTier };
|
|
654
494
|
});
|
|
655
|
-
// Execute with IMMEDIATE isolation
|
|
656
495
|
return txn.immediate();
|
|
657
496
|
}
|
|
658
497
|
// -------------------------------------------------------------------------
|
|
659
498
|
// releaseReservation
|
|
660
499
|
// -------------------------------------------------------------------------
|
|
661
|
-
/**
|
|
662
|
-
* Release a reserved amount on a transaction.
|
|
663
|
-
* Called when transaction reaches FAILED/CANCELLED/EXPIRED state.
|
|
664
|
-
*
|
|
665
|
-
* @param txId - The transaction ID to clear reservation for
|
|
666
|
-
*/
|
|
667
500
|
releaseReservation(txId) {
|
|
668
501
|
if (!this.sqlite) {
|
|
669
502
|
throw new Error('releaseReservation requires raw sqlite instance in constructor');
|
|
@@ -675,23 +508,13 @@ export class DatabasePolicyEngine {
|
|
|
675
508
|
// -------------------------------------------------------------------------
|
|
676
509
|
// Private: Cumulative USD spending aggregation
|
|
677
510
|
// -------------------------------------------------------------------------
|
|
678
|
-
/**
|
|
679
|
-
* Get cumulative USD spent by wallet within a time window.
|
|
680
|
-
* Includes both confirmed amounts (amount_usd) and pending reservations (reserved_amount_usd).
|
|
681
|
-
*
|
|
682
|
-
* CONFIRMED/SIGNED: counted via amount_usd (confirmed or about to be broadcasted).
|
|
683
|
-
* PENDING/QUEUED: counted via reserved_amount_usd (awaiting processing, not yet confirmed).
|
|
684
|
-
* Deduplication: SIGNED is in the first query only (amount_usd). PENDING/QUEUED in second only.
|
|
685
|
-
*/
|
|
686
511
|
getCumulativeUsdSpent(sqlite, walletId, windowStart) {
|
|
687
|
-
// 1. Confirmed transactions (CONFIRMED/SIGNED) amount_usd within window
|
|
688
512
|
const confirmedRow = sqlite
|
|
689
513
|
.prepare(`SELECT COALESCE(SUM(amount_usd), 0) AS total
|
|
690
514
|
FROM transactions
|
|
691
515
|
WHERE wallet_id = ? AND status IN ('CONFIRMED', 'SIGNED')
|
|
692
516
|
AND created_at >= ? AND amount_usd IS NOT NULL`)
|
|
693
517
|
.get(walletId, windowStart);
|
|
694
|
-
// 2. Pending transactions (PENDING/QUEUED) reserved_amount_usd (no window filter -- all pending count)
|
|
695
518
|
const pendingRow = sqlite
|
|
696
519
|
.prepare(`SELECT COALESCE(SUM(reserved_amount_usd), 0) AS total
|
|
697
520
|
FROM transactions
|
|
@@ -703,20 +526,6 @@ export class DatabasePolicyEngine {
|
|
|
703
526
|
// -------------------------------------------------------------------------
|
|
704
527
|
// Private: Override resolution
|
|
705
528
|
// -------------------------------------------------------------------------
|
|
706
|
-
/**
|
|
707
|
-
* Resolve policy overrides with 4-level priority:
|
|
708
|
-
* 1. wallet-specific + network-specific (highest)
|
|
709
|
-
* 2. wallet-specific + all-networks
|
|
710
|
-
* 3. global + network-specific
|
|
711
|
-
* 4. global + all-networks (lowest)
|
|
712
|
-
*
|
|
713
|
-
* For each policy type, one policy is selected.
|
|
714
|
-
* Lower priority entries are inserted first, higher priority entries overwrite.
|
|
715
|
-
* Key: typeMap[row.type] (same as current -- no composite key needed, PLCY-D03).
|
|
716
|
-
*
|
|
717
|
-
* Backward compat: when all policies have network=NULL,
|
|
718
|
-
* phases 2+4 collapse into current 2-level (wallet > global) behavior.
|
|
719
|
-
*/
|
|
720
529
|
resolveOverrides(rows, walletId, resolvedNetwork) {
|
|
721
530
|
const typeMap = new Map();
|
|
722
531
|
// Phase 1: global + all-networks (4th priority, lowest)
|
|
@@ -750,868 +559,35 @@ export class DatabasePolicyEngine {
|
|
|
750
559
|
return Array.from(typeMap.values());
|
|
751
560
|
}
|
|
752
561
|
// -------------------------------------------------------------------------
|
|
753
|
-
// Private: ALLOWED_NETWORKS evaluation
|
|
754
|
-
// -------------------------------------------------------------------------
|
|
755
|
-
/**
|
|
756
|
-
* Evaluate ALLOWED_NETWORKS policy.
|
|
757
|
-
*
|
|
758
|
-
* Logic:
|
|
759
|
-
* - Applies to ALL 5 transaction types (TRANSFER, TOKEN_TRANSFER, CONTRACT_CALL, APPROVE, BATCH)
|
|
760
|
-
* - If no ALLOWED_NETWORKS policy exists: return null (permissive default -- all networks allowed)
|
|
761
|
-
* - If policy exists: check if resolvedNetwork is in rules.networks[].network
|
|
762
|
-
* -> If found: return null (continue to next evaluation)
|
|
763
|
-
* -> If not found: deny with reason 'Network not in allowed list'
|
|
764
|
-
* - Comparison: case-insensitive (toLowerCase)
|
|
765
|
-
* - Tier: INSTANT (immediate denial)
|
|
766
|
-
*
|
|
767
|
-
* Returns PolicyEvaluation if denied, null if allowed (or no policy).
|
|
768
|
-
*/
|
|
769
|
-
evaluateAllowedNetworks(resolved, resolvedNetwork) {
|
|
770
|
-
const policy = resolved.find((p) => p.type === 'ALLOWED_NETWORKS');
|
|
771
|
-
// No ALLOWED_NETWORKS policy -> permissive default (all networks allowed)
|
|
772
|
-
if (!policy)
|
|
773
|
-
return null;
|
|
774
|
-
const rules = JSON.parse(policy.rules);
|
|
775
|
-
// Case-insensitive comparison
|
|
776
|
-
const isAllowed = rules.networks.some((n) => n.network.toLowerCase() === resolvedNetwork.toLowerCase());
|
|
777
|
-
if (!isAllowed) {
|
|
778
|
-
const allowedList = rules.networks.map((n) => n.network).join(', ');
|
|
779
|
-
return {
|
|
780
|
-
allowed: false,
|
|
781
|
-
tier: 'INSTANT',
|
|
782
|
-
reason: `Network '${resolvedNetwork}' not in allowed networks list. Allowed: ${allowedList}`,
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
return null; // Network allowed, continue evaluation
|
|
786
|
-
}
|
|
787
|
-
// -------------------------------------------------------------------------
|
|
788
|
-
// Private: WHITELIST evaluation
|
|
789
|
-
// -------------------------------------------------------------------------
|
|
790
|
-
/**
|
|
791
|
-
* Evaluate WHITELIST policy.
|
|
792
|
-
* Returns PolicyEvaluation if denied, null if allowed (or no whitelist).
|
|
793
|
-
*/
|
|
794
|
-
evaluateWhitelist(resolved, toAddress) {
|
|
795
|
-
const whitelist = resolved.find((p) => p.type === 'WHITELIST');
|
|
796
|
-
if (!whitelist)
|
|
797
|
-
return null;
|
|
798
|
-
const rules = JSON.parse(whitelist.rules);
|
|
799
|
-
// Empty allowed_addresses = whitelist inactive
|
|
800
|
-
if (!rules.allowed_addresses || rules.allowed_addresses.length === 0) {
|
|
801
|
-
return null;
|
|
802
|
-
}
|
|
803
|
-
// Case-insensitive comparison (EVM compat)
|
|
804
|
-
const normalizedTo = toAddress.toLowerCase();
|
|
805
|
-
const isWhitelisted = rules.allowed_addresses.some((addr) => addr.toLowerCase() === normalizedTo);
|
|
806
|
-
if (!isWhitelisted) {
|
|
807
|
-
return {
|
|
808
|
-
allowed: false,
|
|
809
|
-
tier: 'INSTANT',
|
|
810
|
-
reason: `Address ${toAddress} not in whitelist`,
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
// -------------------------------------------------------------------------
|
|
816
|
-
// Private: ALLOWED_TOKENS evaluation
|
|
817
|
-
// -------------------------------------------------------------------------
|
|
818
|
-
/**
|
|
819
|
-
* Evaluate ALLOWED_TOKENS policy with 4-scenario matching matrix (PLCY-03).
|
|
820
|
-
*
|
|
821
|
-
* Logic:
|
|
822
|
-
* - Only applies to TOKEN_TRANSFER transaction type
|
|
823
|
-
* - If transaction type is TOKEN_TRANSFER and no ALLOWED_TOKENS policy exists:
|
|
824
|
-
* -> deny with reason 'Token transfer not allowed: no ALLOWED_TOKENS policy configured'
|
|
825
|
-
* - If ALLOWED_TOKENS policy exists, match using 4-scenario matrix:
|
|
826
|
-
* Scenario 1: Policy assetId + TX assetId -> exact CAIP-19 string match
|
|
827
|
-
* Scenario 2: Policy assetId + TX address only -> extract address from policy assetId, compare lowercase
|
|
828
|
-
* Scenario 3: Policy address only + TX assetId -> extract address from TX assetId, compare lowercase
|
|
829
|
-
* Scenario 4: Policy address only + TX address only -> current behavior (case-insensitive)
|
|
830
|
-
* - EVM addresses normalized to lowercase for comparison (PLCY-04)
|
|
831
|
-
*
|
|
832
|
-
* Returns PolicyEvaluation if denied, null if allowed (or not applicable).
|
|
833
|
-
*/
|
|
834
|
-
evaluateAllowedTokens(resolved, transaction) {
|
|
835
|
-
// Only evaluate for TOKEN_TRANSFER transactions
|
|
836
|
-
if (transaction.type !== 'TOKEN_TRANSFER')
|
|
837
|
-
return null;
|
|
838
|
-
const allowedTokensPolicy = resolved.find((p) => p.type === 'ALLOWED_TOKENS');
|
|
839
|
-
// No ALLOWED_TOKENS policy -> check toggle, then deny (default deny)
|
|
840
|
-
if (!allowedTokensPolicy) {
|
|
841
|
-
if (this.settingsService?.get('policy.default_deny_tokens') === 'false') {
|
|
842
|
-
return null; // default-allow mode: skip token whitelist check
|
|
843
|
-
}
|
|
844
|
-
return {
|
|
845
|
-
allowed: false,
|
|
846
|
-
tier: 'INSTANT',
|
|
847
|
-
reason: 'Token transfer not allowed: no ALLOWED_TOKENS policy configured',
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
// Parse rules.tokens array
|
|
851
|
-
const rules = JSON.parse(allowedTokensPolicy.rules);
|
|
852
|
-
const txTokenAddress = transaction.tokenAddress;
|
|
853
|
-
const txAssetId = transaction.assetId;
|
|
854
|
-
if (!txTokenAddress && !txAssetId) {
|
|
855
|
-
return {
|
|
856
|
-
allowed: false,
|
|
857
|
-
tier: 'INSTANT',
|
|
858
|
-
reason: 'Token transfer missing token address',
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
// 4-scenario matching matrix (PLCY-03)
|
|
862
|
-
const isAllowed = rules.tokens.some((policyToken) => {
|
|
863
|
-
// Scenario 1: Both have assetId -> exact CAIP-19 string match
|
|
864
|
-
// CAIP-19 strings are already normalized (EVM lowercase by tokenAssetId, Solana preserved)
|
|
865
|
-
if (policyToken.assetId && txAssetId) {
|
|
866
|
-
return policyToken.assetId === txAssetId;
|
|
867
|
-
}
|
|
868
|
-
// Scenario 2: Policy has assetId, TX has address only
|
|
869
|
-
if (policyToken.assetId && txTokenAddress) {
|
|
870
|
-
try {
|
|
871
|
-
const policyAddr = parseCaip19(policyToken.assetId).assetReference;
|
|
872
|
-
return policyAddr.toLowerCase() === txTokenAddress.toLowerCase();
|
|
873
|
-
}
|
|
874
|
-
catch {
|
|
875
|
-
return false; // Invalid policy assetId -> no match
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
// Scenario 3: Policy has address only, TX has assetId
|
|
879
|
-
if (!policyToken.assetId && txAssetId) {
|
|
880
|
-
try {
|
|
881
|
-
const txAddr = parseCaip19(txAssetId).assetReference;
|
|
882
|
-
return policyToken.address.toLowerCase() === txAddr.toLowerCase();
|
|
883
|
-
}
|
|
884
|
-
catch {
|
|
885
|
-
return false; // Invalid TX assetId -> no match
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
// Scenario 4: Both address only -> current behavior (case-insensitive)
|
|
889
|
-
return policyToken.address.toLowerCase() === (txTokenAddress ?? '').toLowerCase();
|
|
890
|
-
});
|
|
891
|
-
if (!isAllowed) {
|
|
892
|
-
return {
|
|
893
|
-
allowed: false,
|
|
894
|
-
tier: 'INSTANT',
|
|
895
|
-
reason: `Token not in allowed list: ${txAssetId ?? txTokenAddress}`,
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
return null; // Token is allowed, continue evaluation
|
|
899
|
-
}
|
|
900
|
-
// -------------------------------------------------------------------------
|
|
901
|
-
// Private: CONTRACT_WHITELIST evaluation
|
|
902
|
-
// -------------------------------------------------------------------------
|
|
903
|
-
/**
|
|
904
|
-
* Evaluate CONTRACT_WHITELIST policy.
|
|
905
|
-
*
|
|
906
|
-
* Logic:
|
|
907
|
-
* - Only applies to CONTRACT_CALL transaction type
|
|
908
|
-
* - Provider-trust: if transaction has actionProvider and the provider is enabled
|
|
909
|
-
* in SettingsService, skip CONTRACT_WHITELIST entirely (trusted provider bypass)
|
|
910
|
-
* - If transaction type is CONTRACT_CALL and no CONTRACT_WHITELIST policy exists:
|
|
911
|
-
* -> deny with reason 'Contract calls disabled: no CONTRACT_WHITELIST policy configured'
|
|
912
|
-
* - If CONTRACT_WHITELIST policy exists, check if contract address is in rules.contracts[].address:
|
|
913
|
-
* -> If found: return null (continue to next evaluation)
|
|
914
|
-
* -> If not found: deny with reason 'Contract not whitelisted: {address}'
|
|
915
|
-
* - For non-CONTRACT_CALL types: return null (not applicable)
|
|
916
|
-
*
|
|
917
|
-
* Returns PolicyEvaluation if denied, null if allowed (or not applicable).
|
|
918
|
-
*/
|
|
919
|
-
evaluateContractWhitelist(resolved, transaction) {
|
|
920
|
-
// Evaluate for CONTRACT_CALL and NFT_TRANSFER transactions (v31.0)
|
|
921
|
-
if (transaction.type !== 'CONTRACT_CALL' && transaction.type !== 'NFT_TRANSFER')
|
|
922
|
-
return null;
|
|
923
|
-
// Provider-trust: skip CONTRACT_WHITELIST for trusted action providers
|
|
924
|
-
if (transaction.actionProvider && this.settingsService) {
|
|
925
|
-
const enabledKey = `actions.${transaction.actionProvider}_enabled`;
|
|
926
|
-
try {
|
|
927
|
-
const enabled = this.settingsService.get(enabledKey);
|
|
928
|
-
if (enabled === 'true') {
|
|
929
|
-
return null; // Skip CONTRACT_WHITELIST -- provider is trusted
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
catch {
|
|
933
|
-
// Unknown setting key means provider is not registered -- fall through to normal check
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
const contractWhitelistPolicy = resolved.find((p) => p.type === 'CONTRACT_WHITELIST');
|
|
937
|
-
// No CONTRACT_WHITELIST policy -> check toggle, then deny (default deny)
|
|
938
|
-
if (!contractWhitelistPolicy) {
|
|
939
|
-
if (this.settingsService?.get('policy.default_deny_contracts') === 'false') {
|
|
940
|
-
return null; // default-allow mode: skip contract whitelist check
|
|
941
|
-
}
|
|
942
|
-
return {
|
|
943
|
-
allowed: false,
|
|
944
|
-
tier: 'INSTANT',
|
|
945
|
-
reason: 'Contract calls disabled: no CONTRACT_WHITELIST policy configured',
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
// Parse rules.contracts array
|
|
949
|
-
const rules = JSON.parse(contractWhitelistPolicy.rules);
|
|
950
|
-
const contractAddress = transaction.contractAddress ?? transaction.toAddress;
|
|
951
|
-
// Check if contract is in whitelist (case-insensitive comparison for EVM addresses)
|
|
952
|
-
const isWhitelisted = rules.contracts.some((c) => c.address.toLowerCase() === contractAddress.toLowerCase());
|
|
953
|
-
if (!isWhitelisted) {
|
|
954
|
-
return {
|
|
955
|
-
allowed: false,
|
|
956
|
-
tier: 'INSTANT',
|
|
957
|
-
reason: `Contract not whitelisted: ${contractAddress}`,
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
return null; // Contract is whitelisted, continue evaluation
|
|
961
|
-
}
|
|
962
|
-
// -------------------------------------------------------------------------
|
|
963
|
-
// Private: METHOD_WHITELIST evaluation
|
|
964
|
-
// -------------------------------------------------------------------------
|
|
965
|
-
/**
|
|
966
|
-
* Evaluate METHOD_WHITELIST policy.
|
|
967
|
-
*
|
|
968
|
-
* Logic:
|
|
969
|
-
* - Only applies to CONTRACT_CALL transaction type
|
|
970
|
-
* - If no METHOD_WHITELIST policy exists: return null (method restriction is optional)
|
|
971
|
-
* - If METHOD_WHITELIST policy exists, find matching entry for transaction's contract address:
|
|
972
|
-
* -> If no entry for this contract: return null (no method restriction for this contract)
|
|
973
|
-
* -> If entry found, check if transaction's selector is in entry.selectors:
|
|
974
|
-
* -> If found: return null (method allowed)
|
|
975
|
-
* -> If not found: deny with reason 'Method not whitelisted: {selector} on contract {address}'
|
|
976
|
-
*
|
|
977
|
-
* Returns PolicyEvaluation if denied, null if allowed (or not applicable).
|
|
978
|
-
*/
|
|
979
|
-
evaluateMethodWhitelist(resolved, transaction) {
|
|
980
|
-
// Only evaluate for CONTRACT_CALL transactions
|
|
981
|
-
if (transaction.type !== 'CONTRACT_CALL')
|
|
982
|
-
return null;
|
|
983
|
-
const methodWhitelistPolicy = resolved.find((p) => p.type === 'METHOD_WHITELIST');
|
|
984
|
-
// No METHOD_WHITELIST policy -> no method restriction (optional policy)
|
|
985
|
-
if (!methodWhitelistPolicy)
|
|
986
|
-
return null;
|
|
987
|
-
// Parse rules.methods array
|
|
988
|
-
const rules = JSON.parse(methodWhitelistPolicy.rules);
|
|
989
|
-
const contractAddress = transaction.contractAddress ?? transaction.toAddress;
|
|
990
|
-
const selector = transaction.selector;
|
|
991
|
-
// Find matching entry for this contract (case-insensitive)
|
|
992
|
-
const entry = rules.methods.find((m) => m.contractAddress.toLowerCase() === contractAddress.toLowerCase());
|
|
993
|
-
// No entry for this contract -> no method restriction for this specific contract
|
|
994
|
-
if (!entry)
|
|
995
|
-
return null;
|
|
996
|
-
// Check if selector is in the allowed list (case-insensitive)
|
|
997
|
-
if (!selector) {
|
|
998
|
-
return {
|
|
999
|
-
allowed: false,
|
|
1000
|
-
tier: 'INSTANT',
|
|
1001
|
-
reason: `Method not whitelisted: missing selector on contract ${contractAddress}`,
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
const isAllowed = entry.selectors.some((s) => s.toLowerCase() === selector.toLowerCase());
|
|
1005
|
-
if (!isAllowed) {
|
|
1006
|
-
return {
|
|
1007
|
-
allowed: false,
|
|
1008
|
-
tier: 'INSTANT',
|
|
1009
|
-
reason: `Method not whitelisted: ${selector} on contract ${contractAddress}`,
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
return null; // Method is whitelisted, continue evaluation
|
|
1013
|
-
}
|
|
1014
|
-
// -------------------------------------------------------------------------
|
|
1015
|
-
// Private: APPROVED_SPENDERS evaluation
|
|
1016
|
-
// -------------------------------------------------------------------------
|
|
1017
|
-
/**
|
|
1018
|
-
* Evaluate APPROVED_SPENDERS policy.
|
|
1019
|
-
*
|
|
1020
|
-
* Logic:
|
|
1021
|
-
* - Only applies to APPROVE transaction type
|
|
1022
|
-
* - If transaction type is APPROVE and no APPROVED_SPENDERS policy exists:
|
|
1023
|
-
* -> deny with reason 'Token approvals disabled: no APPROVED_SPENDERS policy configured'
|
|
1024
|
-
* - If APPROVED_SPENDERS policy exists, check if transaction's spenderAddress is in rules.spenders[]:
|
|
1025
|
-
* -> If found: return null (continue evaluation)
|
|
1026
|
-
* -> If not found: deny with reason 'Spender not in approved list: {address}'
|
|
1027
|
-
* - Case-insensitive comparison (EVM addresses)
|
|
1028
|
-
*
|
|
1029
|
-
* Returns PolicyEvaluation if denied, null if allowed (or not applicable).
|
|
1030
|
-
*/
|
|
1031
|
-
evaluateApprovedSpenders(resolved, transaction) {
|
|
1032
|
-
// Only evaluate for APPROVE transactions
|
|
1033
|
-
if (transaction.type !== 'APPROVE')
|
|
1034
|
-
return null;
|
|
1035
|
-
const approvedSpendersPolicy = resolved.find((p) => p.type === 'APPROVED_SPENDERS');
|
|
1036
|
-
// No APPROVED_SPENDERS policy -> check toggle, then deny (default deny)
|
|
1037
|
-
if (!approvedSpendersPolicy) {
|
|
1038
|
-
if (this.settingsService?.get('policy.default_deny_spenders') === 'false') {
|
|
1039
|
-
return null; // default-allow mode: skip approved spenders check
|
|
1040
|
-
}
|
|
1041
|
-
return {
|
|
1042
|
-
allowed: false,
|
|
1043
|
-
tier: 'INSTANT',
|
|
1044
|
-
reason: 'Token approvals disabled: no APPROVED_SPENDERS policy configured',
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
// Parse rules.spenders array
|
|
1048
|
-
const rules = JSON.parse(approvedSpendersPolicy.rules);
|
|
1049
|
-
const spenderAddress = transaction.spenderAddress;
|
|
1050
|
-
if (!spenderAddress) {
|
|
1051
|
-
return {
|
|
1052
|
-
allowed: false,
|
|
1053
|
-
tier: 'INSTANT',
|
|
1054
|
-
reason: 'Approve missing spender address',
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
// Check if spender is in approved list (case-insensitive for EVM addresses)
|
|
1058
|
-
const isApproved = rules.spenders.some((s) => s.address.toLowerCase() === spenderAddress.toLowerCase());
|
|
1059
|
-
if (!isApproved) {
|
|
1060
|
-
return {
|
|
1061
|
-
allowed: false,
|
|
1062
|
-
tier: 'INSTANT',
|
|
1063
|
-
reason: `Spender not in approved list: ${spenderAddress}`,
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
return null; // Spender is approved, continue evaluation
|
|
1067
|
-
}
|
|
1068
|
-
// -------------------------------------------------------------------------
|
|
1069
|
-
// Private: APPROVE_AMOUNT_LIMIT evaluation
|
|
1070
|
-
// -------------------------------------------------------------------------
|
|
1071
|
-
/**
|
|
1072
|
-
* Evaluate APPROVE_AMOUNT_LIMIT policy.
|
|
1073
|
-
*
|
|
1074
|
-
* Logic:
|
|
1075
|
-
* - Only applies to APPROVE transaction type
|
|
1076
|
-
* - Checks for unlimited approve amounts (>= UNLIMITED_THRESHOLD)
|
|
1077
|
-
* - Checks for amount cap (maxAmount)
|
|
1078
|
-
* - If no policy exists: default block_unlimited=true (block unlimited approvals)
|
|
1079
|
-
*
|
|
1080
|
-
* Returns PolicyEvaluation if denied, null if allowed (or not applicable).
|
|
1081
|
-
*/
|
|
1082
|
-
evaluateApproveAmountLimit(resolved, transaction) {
|
|
1083
|
-
// Only evaluate for APPROVE transactions
|
|
1084
|
-
if (transaction.type !== 'APPROVE')
|
|
1085
|
-
return null;
|
|
1086
|
-
const approveAmount = transaction.approveAmount;
|
|
1087
|
-
if (!approveAmount)
|
|
1088
|
-
return null; // No amount to check
|
|
1089
|
-
const amount = BigInt(approveAmount);
|
|
1090
|
-
const approveAmountPolicy = resolved.find((p) => p.type === 'APPROVE_AMOUNT_LIMIT');
|
|
1091
|
-
if (!approveAmountPolicy) {
|
|
1092
|
-
// No policy: default block unlimited
|
|
1093
|
-
if (amount >= UNLIMITED_THRESHOLD) {
|
|
1094
|
-
return {
|
|
1095
|
-
allowed: false,
|
|
1096
|
-
tier: 'INSTANT',
|
|
1097
|
-
reason: 'Unlimited token approval is blocked',
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
return null;
|
|
1101
|
-
}
|
|
1102
|
-
// Parse rules
|
|
1103
|
-
const rules = JSON.parse(approveAmountPolicy.rules);
|
|
1104
|
-
// Check unlimited block
|
|
1105
|
-
if (rules.blockUnlimited && amount >= UNLIMITED_THRESHOLD) {
|
|
1106
|
-
return {
|
|
1107
|
-
allowed: false,
|
|
1108
|
-
tier: 'INSTANT',
|
|
1109
|
-
reason: 'Unlimited token approval is blocked',
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
// Check maxAmount cap
|
|
1113
|
-
if (rules.maxAmount && amount > BigInt(rules.maxAmount)) {
|
|
1114
|
-
return {
|
|
1115
|
-
allowed: false,
|
|
1116
|
-
tier: 'INSTANT',
|
|
1117
|
-
reason: 'Approve amount exceeds limit',
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
return null; // Amount within limits, continue evaluation
|
|
1121
|
-
}
|
|
1122
|
-
// -------------------------------------------------------------------------
|
|
1123
|
-
// Private: APPROVE_TIER_OVERRIDE evaluation
|
|
1124
|
-
// -------------------------------------------------------------------------
|
|
1125
|
-
/**
|
|
1126
|
-
* Evaluate APPROVE_TIER_OVERRIDE policy.
|
|
1127
|
-
*
|
|
1128
|
-
* Logic:
|
|
1129
|
-
* - Only applies to APPROVE transaction type
|
|
1130
|
-
* - If APPROVE_TIER_OVERRIDE policy exists: return configured tier (FINAL, skips SPENDING_LIMIT)
|
|
1131
|
-
* - If no APPROVE_TIER_OVERRIDE policy exists: return null (Phase 236: fall through to SPENDING_LIMIT
|
|
1132
|
-
* for token_limits evaluation; if no SPENDING_LIMIT either, INSTANT passthrough)
|
|
1133
|
-
*
|
|
1134
|
-
* Phase 236 change: Previously defaulted to APPROVAL when no override policy existed.
|
|
1135
|
-
* Now falls through to SPENDING_LIMIT to allow token_limits evaluation for APPROVE transactions.
|
|
1136
|
-
*
|
|
1137
|
-
* Returns PolicyEvaluation if override policy exists, null otherwise.
|
|
1138
|
-
*/
|
|
1139
|
-
evaluateApproveTierOverride(resolved, transaction) {
|
|
1140
|
-
// Only evaluate for APPROVE transactions
|
|
1141
|
-
if (transaction.type !== 'APPROVE')
|
|
1142
|
-
return null;
|
|
1143
|
-
const approveTierPolicy = resolved.find((p) => p.type === 'APPROVE_TIER_OVERRIDE');
|
|
1144
|
-
if (!approveTierPolicy) {
|
|
1145
|
-
// Phase 236: No override -> fall through to SPENDING_LIMIT for token_limits evaluation
|
|
1146
|
-
return null;
|
|
1147
|
-
}
|
|
1148
|
-
// Parse rules
|
|
1149
|
-
const rules = JSON.parse(approveTierPolicy.rules);
|
|
1150
|
-
return { allowed: true, tier: rules.tier };
|
|
1151
|
-
}
|
|
1152
|
-
// -------------------------------------------------------------------------
|
|
1153
|
-
// Private: SPENDING_LIMIT evaluation
|
|
1154
|
-
// -------------------------------------------------------------------------
|
|
1155
|
-
/**
|
|
1156
|
-
* Evaluate SPENDING_LIMIT policy.
|
|
1157
|
-
* Returns PolicyEvaluation with tier classification, or null if no spending limit.
|
|
1158
|
-
*
|
|
1159
|
-
* Phase 127: usdAmount가 전달되고 rules에 USD 임계값이 설정되어 있으면,
|
|
1160
|
-
* 네이티브 티어와 USD 티어 중 더 보수적인(높은) 티어를 채택한다.
|
|
1161
|
-
*
|
|
1162
|
-
* Phase 236: tokenContext가 전달되고 rules에 token_limits가 설정되어 있으면,
|
|
1163
|
-
* evaluateTokenTier()를 사용하여 토큰별 human-readable 한도를 평가한다.
|
|
1164
|
-
*/
|
|
1165
|
-
evaluateSpendingLimit(resolved, amount, usdAmount, tokenContext) {
|
|
1166
|
-
const spending = resolved.find((p) => p.type === 'SPENDING_LIMIT');
|
|
1167
|
-
if (!spending)
|
|
1168
|
-
return null;
|
|
1169
|
-
const rules = JSON.parse(spending.rules);
|
|
1170
|
-
// 1. Token-specific tier (Phase 236)
|
|
1171
|
-
let tokenTier = 'INSTANT';
|
|
1172
|
-
if (tokenContext && rules.token_limits) {
|
|
1173
|
-
const tokenResult = this.evaluateTokenTier(BigInt(amount), rules, tokenContext);
|
|
1174
|
-
if (tokenResult !== null) {
|
|
1175
|
-
tokenTier = tokenResult;
|
|
1176
|
-
}
|
|
1177
|
-
else {
|
|
1178
|
-
// No token_limits match -> fall back to raw fields
|
|
1179
|
-
tokenTier = this.evaluateNativeTier(BigInt(amount), rules);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
else {
|
|
1183
|
-
// No tokenContext or no token_limits -> use raw fields (existing behavior)
|
|
1184
|
-
tokenTier = this.evaluateNativeTier(BigInt(amount), rules);
|
|
1185
|
-
}
|
|
1186
|
-
// 2. USD 기준 티어 (Phase 127)
|
|
1187
|
-
let finalTier = tokenTier;
|
|
1188
|
-
if (usdAmount !== undefined && usdAmount > 0 && this.hasUsdThresholds(rules)) {
|
|
1189
|
-
const usdTier = this.evaluateUsdTier(usdAmount, rules);
|
|
1190
|
-
finalTier = maxTier(tokenTier, usdTier);
|
|
1191
|
-
}
|
|
1192
|
-
// delaySeconds는 최종 tier가 DELAY일 때만 포함
|
|
1193
|
-
const delaySeconds = finalTier === 'DELAY' ? rules.delay_seconds : undefined;
|
|
1194
|
-
return {
|
|
1195
|
-
allowed: true,
|
|
1196
|
-
tier: finalTier,
|
|
1197
|
-
...(delaySeconds !== undefined ? { delaySeconds } : {}),
|
|
1198
|
-
...(finalTier === 'APPROVAL' ? { approvalReason: 'per_tx' } : {}),
|
|
1199
|
-
};
|
|
1200
|
-
}
|
|
1201
|
-
/**
|
|
1202
|
-
* Evaluate token-specific tier using token_limits with CAIP-19 key matching.
|
|
1203
|
-
* Returns PolicyTier if a matching token limit is found, null otherwise (-> raw fallback).
|
|
1204
|
-
*
|
|
1205
|
-
* Matching priority:
|
|
1206
|
-
* 1. Exact CAIP-19 asset ID match (TOKEN_TRANSFER, APPROVE)
|
|
1207
|
-
* 2. "native:{chain}" match (TRANSFER)
|
|
1208
|
-
* 3. "native" shorthand match (TRANSFER, only when policy has network set)
|
|
1209
|
-
* 4. No match -> return null (caller falls back to raw fields)
|
|
1210
|
-
*/
|
|
1211
|
-
evaluateTokenTier(amountBig, rules, tokenContext) {
|
|
1212
|
-
if (!rules.token_limits)
|
|
1213
|
-
return null;
|
|
1214
|
-
// Skip for CONTRACT_CALL and BATCH (they don't use token_limits)
|
|
1215
|
-
if (tokenContext.type === 'CONTRACT_CALL' || tokenContext.type === 'BATCH') {
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
let matchedLimit;
|
|
1219
|
-
let decimals;
|
|
1220
|
-
if (tokenContext.type === 'TOKEN_TRANSFER' || tokenContext.type === 'APPROVE') {
|
|
1221
|
-
// Try exact CAIP-19 asset ID match
|
|
1222
|
-
if (tokenContext.assetId && rules.token_limits[tokenContext.assetId]) {
|
|
1223
|
-
matchedLimit = rules.token_limits[tokenContext.assetId];
|
|
1224
|
-
decimals = tokenContext.tokenDecimals;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
else if (tokenContext.type === 'TRANSFER') {
|
|
1228
|
-
// Try "native:{chain}" match
|
|
1229
|
-
const nativeChainKey = tokenContext.chain ? `native:${tokenContext.chain}` : undefined;
|
|
1230
|
-
if (nativeChainKey && rules.token_limits[nativeChainKey]) {
|
|
1231
|
-
matchedLimit = rules.token_limits[nativeChainKey];
|
|
1232
|
-
decimals = tokenContext.chain ? NATIVE_DECIMALS[tokenContext.chain] : undefined;
|
|
1233
|
-
}
|
|
1234
|
-
// Fallback: try "native" shorthand (only when policy has a network set)
|
|
1235
|
-
if (!matchedLimit && tokenContext.policyNetwork && rules.token_limits['native']) {
|
|
1236
|
-
matchedLimit = rules.token_limits['native'];
|
|
1237
|
-
decimals = tokenContext.chain ? NATIVE_DECIMALS[tokenContext.chain] : undefined;
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
if (!matchedLimit || decimals === undefined) {
|
|
1241
|
-
return null; // No match -> caller falls back to raw fields
|
|
1242
|
-
}
|
|
1243
|
-
// Use fixed-point comparison: multiply limit by divisor (avoids precision loss)
|
|
1244
|
-
const instantMaxRaw = parseDecimalToBigInt(matchedLimit.instant_max, decimals);
|
|
1245
|
-
const notifyMaxRaw = parseDecimalToBigInt(matchedLimit.notify_max, decimals);
|
|
1246
|
-
const delayMaxRaw = parseDecimalToBigInt(matchedLimit.delay_max, decimals);
|
|
1247
|
-
if (amountBig <= instantMaxRaw)
|
|
1248
|
-
return 'INSTANT';
|
|
1249
|
-
if (amountBig <= notifyMaxRaw)
|
|
1250
|
-
return 'NOTIFY';
|
|
1251
|
-
if (amountBig <= delayMaxRaw)
|
|
1252
|
-
return 'DELAY';
|
|
1253
|
-
return 'APPROVAL';
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Evaluate native amount tier (extracted from evaluateSpendingLimit).
|
|
1257
|
-
* Phase 236: proper undefined guards for optional raw fields.
|
|
1258
|
-
*/
|
|
1259
|
-
evaluateNativeTier(amountBig, rules) {
|
|
1260
|
-
// Phase 236: raw fields are now optional -- skip native tier if all undefined
|
|
1261
|
-
if (rules.instant_max === undefined && rules.notify_max === undefined && rules.delay_max === undefined) {
|
|
1262
|
-
return 'INSTANT'; // No raw thresholds -> permissive (USD/token_limits will handle)
|
|
1263
|
-
}
|
|
1264
|
-
const instantMax = rules.instant_max !== undefined ? BigInt(rules.instant_max) : 0n;
|
|
1265
|
-
const notifyMax = rules.notify_max !== undefined ? BigInt(rules.notify_max) : 0n;
|
|
1266
|
-
const delayMax = rules.delay_max !== undefined ? BigInt(rules.delay_max) : 0n;
|
|
1267
|
-
if (amountBig <= instantMax)
|
|
1268
|
-
return 'INSTANT';
|
|
1269
|
-
if (amountBig <= notifyMax)
|
|
1270
|
-
return 'NOTIFY';
|
|
1271
|
-
if (amountBig <= delayMax)
|
|
1272
|
-
return 'DELAY';
|
|
1273
|
-
return 'APPROVAL';
|
|
1274
|
-
}
|
|
1275
|
-
/**
|
|
1276
|
-
* Check if rules have any USD thresholds configured.
|
|
1277
|
-
*/
|
|
1278
|
-
hasUsdThresholds(rules) {
|
|
1279
|
-
return rules.instant_max_usd !== undefined
|
|
1280
|
-
|| rules.notify_max_usd !== undefined
|
|
1281
|
-
|| rules.delay_max_usd !== undefined;
|
|
1282
|
-
}
|
|
1283
|
-
/**
|
|
1284
|
-
* Evaluate USD amount tier.
|
|
1285
|
-
*/
|
|
1286
|
-
evaluateUsdTier(usdAmount, rules) {
|
|
1287
|
-
if (rules.instant_max_usd !== undefined && usdAmount <= rules.instant_max_usd) {
|
|
1288
|
-
return 'INSTANT';
|
|
1289
|
-
}
|
|
1290
|
-
if (rules.notify_max_usd !== undefined && usdAmount <= rules.notify_max_usd) {
|
|
1291
|
-
return 'NOTIFY';
|
|
1292
|
-
}
|
|
1293
|
-
if (rules.delay_max_usd !== undefined && usdAmount <= rules.delay_max_usd) {
|
|
1294
|
-
return 'DELAY';
|
|
1295
|
-
}
|
|
1296
|
-
return 'APPROVAL';
|
|
1297
|
-
}
|
|
1298
|
-
/**
|
|
1299
|
-
* Build tokenContext from TransactionParam for evaluateTokenTier().
|
|
1300
|
-
* Phase 236: Extracts relevant fields and attaches the policy's network.
|
|
1301
|
-
*/
|
|
1302
|
-
buildTokenContext(transaction, spendingPolicy) {
|
|
1303
|
-
return {
|
|
1304
|
-
type: transaction.type,
|
|
1305
|
-
tokenAddress: transaction.tokenAddress,
|
|
1306
|
-
tokenDecimals: transaction.tokenDecimals,
|
|
1307
|
-
chain: transaction.chain,
|
|
1308
|
-
assetId: transaction.assetId,
|
|
1309
|
-
policyNetwork: spendingPolicy?.network ?? undefined,
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
// -------------------------------------------------------------------------
|
|
1313
|
-
// Private: LENDING_ASSET_WHITELIST evaluation (Step 4h)
|
|
1314
|
-
// -------------------------------------------------------------------------
|
|
1315
|
-
/**
|
|
1316
|
-
* Evaluate LENDING_ASSET_WHITELIST policy.
|
|
1317
|
-
*
|
|
1318
|
-
* Logic:
|
|
1319
|
-
* - Only applies to lending actions (supply/borrow/repay/withdraw)
|
|
1320
|
-
* - If no LENDING_ASSET_WHITELIST policy exists: deny (default-deny per CLAUDE.md)
|
|
1321
|
-
* - If policy exists: check if target contract address is in rules.assets[].address
|
|
1322
|
-
*
|
|
1323
|
-
* Returns PolicyEvaluation if denied, null if allowed (or not applicable).
|
|
1324
|
-
*/
|
|
1325
|
-
evaluateLendingAssetWhitelist(resolved, transaction) {
|
|
1326
|
-
// Only applies to lending actions
|
|
1327
|
-
const LENDING_ACTIONS = new Set(['supply', 'borrow', 'repay', 'withdraw']);
|
|
1328
|
-
if (!transaction.actionName || !LENDING_ACTIONS.has(transaction.actionName)) {
|
|
1329
|
-
return null;
|
|
1330
|
-
}
|
|
1331
|
-
const assetPolicy = resolved.find((p) => p.type === 'LENDING_ASSET_WHITELIST');
|
|
1332
|
-
if (!assetPolicy) {
|
|
1333
|
-
// Default-deny (CLAUDE.md compliance): no whitelist -> deny lending
|
|
1334
|
-
return {
|
|
1335
|
-
allowed: false,
|
|
1336
|
-
tier: 'INSTANT',
|
|
1337
|
-
reason: 'No LENDING_ASSET_WHITELIST policy configured. Lending assets require explicit whitelist.',
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
const rules = JSON.parse(assetPolicy.rules);
|
|
1341
|
-
const targetAddress = transaction.contractAddress ?? transaction.toAddress;
|
|
1342
|
-
const isWhitelisted = rules.assets.some((a) => a.address.toLowerCase() === targetAddress.toLowerCase());
|
|
1343
|
-
if (!isWhitelisted) {
|
|
1344
|
-
return {
|
|
1345
|
-
allowed: false,
|
|
1346
|
-
tier: 'INSTANT',
|
|
1347
|
-
reason: `Asset ${targetAddress} not in lending asset whitelist`,
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
return null; // pass through
|
|
1351
|
-
}
|
|
1352
|
-
// -------------------------------------------------------------------------
|
|
1353
|
-
// Private: LENDING_LTV_LIMIT evaluation (Step 4h-b)
|
|
1354
|
-
// -------------------------------------------------------------------------
|
|
1355
|
-
/**
|
|
1356
|
-
* Evaluate LENDING_LTV_LIMIT policy for borrow actions.
|
|
1357
|
-
*
|
|
1358
|
-
* Logic:
|
|
1359
|
-
* - Only applies to borrow actions
|
|
1360
|
-
* - Reads cached LENDING positions from defi_positions table
|
|
1361
|
-
* - Calculates projected LTV = (currentDebtUsd + newBorrowUsd) / totalCollateralUsd
|
|
1362
|
-
* - Denies if projected LTV > maxLtv
|
|
1363
|
-
* - Returns DELAY tier if projected LTV > warningLtv
|
|
1364
|
-
*
|
|
1365
|
-
* @param usdAmount - USD value of the new borrow (from pipeline IPriceOracle, LEND-09)
|
|
1366
|
-
* Returns PolicyEvaluation if denied/escalated, null if allowed (or not applicable).
|
|
1367
|
-
*/
|
|
1368
|
-
evaluateLendingLtvLimit(resolved, transaction, walletId, usdAmount) {
|
|
1369
|
-
// Only applies to borrow actions (matches 'borrow', 'aave_borrow', 'kamino_borrow', etc.)
|
|
1370
|
-
if (!transaction.actionName?.endsWith('borrow'))
|
|
1371
|
-
return null;
|
|
1372
|
-
const ltvPolicy = resolved.find((p) => p.type === 'LENDING_LTV_LIMIT');
|
|
1373
|
-
if (!ltvPolicy)
|
|
1374
|
-
return null; // No LTV policy -> pass through
|
|
1375
|
-
const rules = JSON.parse(ltvPolicy.rules);
|
|
1376
|
-
// Read cached position data from defi_positions
|
|
1377
|
-
if (!this.sqlite)
|
|
1378
|
-
return null;
|
|
1379
|
-
const positions = this.sqlite.prepare("SELECT amount_usd, metadata, status FROM defi_positions WHERE wallet_id = ? AND category = 'LENDING' AND status = 'ACTIVE'").all(walletId);
|
|
1380
|
-
// Aggregate collateral and debt from positions
|
|
1381
|
-
let totalCollateralUsd = 0;
|
|
1382
|
-
let totalDebtUsd = 0;
|
|
1383
|
-
for (const pos of positions) {
|
|
1384
|
-
if (!pos.metadata)
|
|
1385
|
-
continue;
|
|
1386
|
-
try {
|
|
1387
|
-
const meta = JSON.parse(pos.metadata);
|
|
1388
|
-
const posType = meta.positionType;
|
|
1389
|
-
const usd = pos.amount_usd ?? 0;
|
|
1390
|
-
if (posType === 'SUPPLY')
|
|
1391
|
-
totalCollateralUsd += usd;
|
|
1392
|
-
else if (posType === 'BORROW')
|
|
1393
|
-
totalDebtUsd += usd;
|
|
1394
|
-
}
|
|
1395
|
-
catch { /* ignore parse errors */ }
|
|
1396
|
-
}
|
|
1397
|
-
// Calculate projected LTV including new borrow amount (LEND-09)
|
|
1398
|
-
const newBorrowUsd = usdAmount ?? 0;
|
|
1399
|
-
const projectedLtv = totalCollateralUsd > 0
|
|
1400
|
-
? (totalDebtUsd + newBorrowUsd) / totalCollateralUsd
|
|
1401
|
-
: (totalDebtUsd > 0 || newBorrowUsd > 0 ? Infinity : 0);
|
|
1402
|
-
if (projectedLtv > rules.maxLtv) {
|
|
1403
|
-
return {
|
|
1404
|
-
allowed: false,
|
|
1405
|
-
tier: 'INSTANT',
|
|
1406
|
-
reason: `Borrow would exceed max LTV (projected: ${(projectedLtv * 100).toFixed(1)}%, limit: ${(rules.maxLtv * 100).toFixed(1)}%)`,
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
if (projectedLtv > rules.warningLtv) {
|
|
1410
|
-
return {
|
|
1411
|
-
allowed: true,
|
|
1412
|
-
tier: 'DELAY',
|
|
1413
|
-
reason: `LTV approaching limit (projected: ${(projectedLtv * 100).toFixed(1)}%)`,
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
return null; // pass through
|
|
1417
|
-
}
|
|
1418
|
-
// -------------------------------------------------------------------------
|
|
1419
|
-
// Private: PERP_ALLOWED_MARKETS evaluation (Step 4i)
|
|
1420
|
-
// -------------------------------------------------------------------------
|
|
1421
|
-
/**
|
|
1422
|
-
* Evaluate PERP_ALLOWED_MARKETS policy.
|
|
1423
|
-
*
|
|
1424
|
-
* Logic:
|
|
1425
|
-
* - Only applies to perp actions (suffix matching: open_position, close_position,
|
|
1426
|
-
* modify_position, add_margin, withdraw_margin)
|
|
1427
|
-
* - If no PERP_ALLOWED_MARKETS policy exists: deny (default-deny per CLAUDE.md)
|
|
1428
|
-
* - If policy exists: check if transaction's market (from actionName prefix or params)
|
|
1429
|
-
* is in rules.markets[].market (case-insensitive)
|
|
1430
|
-
*
|
|
1431
|
-
* Market identification: TransactionParam.contractAddress is used as the market
|
|
1432
|
-
* identifier for perp actions (the protocol program/contract address).
|
|
1433
|
-
*/
|
|
1434
|
-
evaluatePerpAllowedMarkets(resolved, transaction) {
|
|
1435
|
-
const PERP_ACTIONS = new Set([
|
|
1436
|
-
'open_position', 'close_position', 'modify_position',
|
|
1437
|
-
'add_margin', 'withdraw_margin',
|
|
1438
|
-
]);
|
|
1439
|
-
// Suffix matching for prefixed actions (drift_open_position -> open_position)
|
|
1440
|
-
const actionSuffix = transaction.actionName
|
|
1441
|
-
? [...PERP_ACTIONS].find((a) => transaction.actionName.endsWith(a))
|
|
1442
|
-
: undefined;
|
|
1443
|
-
if (!actionSuffix)
|
|
1444
|
-
return null; // Not a perp action
|
|
1445
|
-
const marketPolicy = resolved.find((p) => p.type === 'PERP_ALLOWED_MARKETS');
|
|
1446
|
-
if (!marketPolicy) {
|
|
1447
|
-
return {
|
|
1448
|
-
allowed: false,
|
|
1449
|
-
tier: 'INSTANT',
|
|
1450
|
-
reason: 'No PERP_ALLOWED_MARKETS policy configured. Perp markets require explicit whitelist.',
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
const rules = JSON.parse(marketPolicy.rules);
|
|
1454
|
-
const targetMarket = transaction.contractAddress ?? transaction.toAddress;
|
|
1455
|
-
const isAllowed = rules.markets.some((m) => m.market.toLowerCase() === targetMarket.toLowerCase());
|
|
1456
|
-
if (!isAllowed) {
|
|
1457
|
-
return {
|
|
1458
|
-
allowed: false,
|
|
1459
|
-
tier: 'INSTANT',
|
|
1460
|
-
reason: `Market ${targetMarket} not in perp allowed markets whitelist`,
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
return null;
|
|
1464
|
-
}
|
|
1465
|
-
// -------------------------------------------------------------------------
|
|
1466
|
-
// Private: PERP_MAX_LEVERAGE evaluation (Step 4i-b)
|
|
1467
|
-
// -------------------------------------------------------------------------
|
|
1468
|
-
/**
|
|
1469
|
-
* Evaluate PERP_MAX_LEVERAGE policy.
|
|
1470
|
-
*
|
|
1471
|
-
* Logic:
|
|
1472
|
-
* - Only applies to open_position and modify_position (suffix matching)
|
|
1473
|
-
* - Reads perpLeverage from TransactionParam
|
|
1474
|
-
* - Denies if perpLeverage > rules.maxLeverage
|
|
1475
|
-
* - Returns DELAY tier if perpLeverage > rules.warningLeverage (optional)
|
|
1476
|
-
*/
|
|
1477
|
-
evaluatePerpMaxLeverage(resolved, transaction) {
|
|
1478
|
-
if (!transaction.actionName)
|
|
1479
|
-
return null;
|
|
1480
|
-
const isLeverageAction = transaction.actionName.endsWith('open_position') ||
|
|
1481
|
-
transaction.actionName.endsWith('modify_position');
|
|
1482
|
-
if (!isLeverageAction)
|
|
1483
|
-
return null;
|
|
1484
|
-
const leveragePolicy = resolved.find((p) => p.type === 'PERP_MAX_LEVERAGE');
|
|
1485
|
-
if (!leveragePolicy)
|
|
1486
|
-
return null; // No leverage policy -> pass through
|
|
1487
|
-
const rules = JSON.parse(leveragePolicy.rules);
|
|
1488
|
-
const leverage = transaction.perpLeverage;
|
|
1489
|
-
if (typeof leverage !== 'number')
|
|
1490
|
-
return null; // No leverage info -> pass through
|
|
1491
|
-
if (leverage > rules.maxLeverage) {
|
|
1492
|
-
return {
|
|
1493
|
-
allowed: false,
|
|
1494
|
-
tier: 'INSTANT',
|
|
1495
|
-
reason: `Leverage ${leverage}x exceeds max allowed (${rules.maxLeverage}x)`,
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
if (rules.warningLeverage && leverage > rules.warningLeverage) {
|
|
1499
|
-
return {
|
|
1500
|
-
allowed: true,
|
|
1501
|
-
tier: 'DELAY',
|
|
1502
|
-
reason: `Leverage ${leverage}x approaching limit (warning: ${rules.warningLeverage}x, max: ${rules.maxLeverage}x)`,
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
return null;
|
|
1506
|
-
}
|
|
1507
|
-
// -------------------------------------------------------------------------
|
|
1508
|
-
// Private: PERP_MAX_POSITION_USD evaluation (Step 4i-c)
|
|
1509
|
-
// -------------------------------------------------------------------------
|
|
1510
|
-
/**
|
|
1511
|
-
* Evaluate PERP_MAX_POSITION_USD policy.
|
|
1512
|
-
*
|
|
1513
|
-
* Logic:
|
|
1514
|
-
* - Only applies to open_position and modify_position (suffix matching)
|
|
1515
|
-
* - Reads perpSizeUsd from TransactionParam
|
|
1516
|
-
* - Denies if perpSizeUsd > rules.maxPositionUsd
|
|
1517
|
-
* - Returns DELAY tier if perpSizeUsd > rules.warningPositionUsd (optional)
|
|
1518
|
-
*/
|
|
1519
|
-
evaluatePerpMaxPositionUsd(resolved, transaction) {
|
|
1520
|
-
if (!transaction.actionName)
|
|
1521
|
-
return null;
|
|
1522
|
-
const isSizeAction = transaction.actionName.endsWith('open_position') ||
|
|
1523
|
-
transaction.actionName.endsWith('modify_position');
|
|
1524
|
-
if (!isSizeAction)
|
|
1525
|
-
return null;
|
|
1526
|
-
const sizePolicy = resolved.find((p) => p.type === 'PERP_MAX_POSITION_USD');
|
|
1527
|
-
if (!sizePolicy)
|
|
1528
|
-
return null; // No size policy -> pass through
|
|
1529
|
-
const rules = JSON.parse(sizePolicy.rules);
|
|
1530
|
-
const sizeUsd = transaction.perpSizeUsd;
|
|
1531
|
-
if (typeof sizeUsd !== 'number')
|
|
1532
|
-
return null; // No size info -> pass through
|
|
1533
|
-
if (sizeUsd > rules.maxPositionUsd) {
|
|
1534
|
-
return {
|
|
1535
|
-
allowed: false,
|
|
1536
|
-
tier: 'INSTANT',
|
|
1537
|
-
reason: `Position size $${sizeUsd.toLocaleString()} exceeds max allowed ($${rules.maxPositionUsd.toLocaleString()})`,
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
if (rules.warningPositionUsd && sizeUsd > rules.warningPositionUsd) {
|
|
1541
|
-
return {
|
|
1542
|
-
allowed: true,
|
|
1543
|
-
tier: 'DELAY',
|
|
1544
|
-
reason: `Position size $${sizeUsd.toLocaleString()} approaching limit (warning: $${rules.warningPositionUsd.toLocaleString()}, max: $${rules.maxPositionUsd.toLocaleString()})`,
|
|
1545
|
-
};
|
|
1546
|
-
}
|
|
1547
|
-
return null;
|
|
1548
|
-
}
|
|
1549
|
-
// -------------------------------------------------------------------------
|
|
1550
562
|
// Private: REPUTATION_THRESHOLD evaluation (Phase 320)
|
|
1551
563
|
// -------------------------------------------------------------------------
|
|
1552
|
-
/**
|
|
1553
|
-
* Evaluate REPUTATION_THRESHOLD policy.
|
|
1554
|
-
*
|
|
1555
|
-
* Logic:
|
|
1556
|
-
* - Find REPUTATION_THRESHOLD policy in resolved list
|
|
1557
|
-
* - If not found or check_counterparty=false, return null (skip)
|
|
1558
|
-
* - Resolve counterparty agentId from toAddress via agent_identities table
|
|
1559
|
-
* - If no agentId found, treat as unrated
|
|
1560
|
-
* - If no reputationCacheService, treat as unrated
|
|
1561
|
-
* - Lookup reputation score via cache
|
|
1562
|
-
* - If null (unrated/RPC failure), return unrated_tier
|
|
1563
|
-
* - If score < min_score, return below_threshold_tier
|
|
1564
|
-
* - If score >= min_score, return null (pass, continue evaluation)
|
|
1565
|
-
*
|
|
1566
|
-
* Returns the reputation floor tier (PolicyTier) or null if no escalation needed.
|
|
1567
|
-
* The caller applies maxTier to the final result.
|
|
1568
|
-
*/
|
|
1569
564
|
async evaluateReputationThreshold(resolved, transaction) {
|
|
1570
565
|
const policy = resolved.find((p) => p.type === 'REPUTATION_THRESHOLD');
|
|
1571
566
|
if (!policy)
|
|
1572
567
|
return null;
|
|
1573
|
-
const rules =
|
|
1574
|
-
// If check_counterparty is disabled, skip entirely
|
|
568
|
+
const rules = this.parseRules(policy.rules, ReputationThresholdRulesSchema, 'REPUTATION_THRESHOLD');
|
|
1575
569
|
if (!rules.check_counterparty)
|
|
1576
570
|
return null;
|
|
1577
|
-
// Resolve counterparty agentId from toAddress
|
|
1578
571
|
const agentId = this.resolveAgentIdFromAddress(transaction.toAddress);
|
|
1579
|
-
// No agent identity found -> treat as unrated
|
|
1580
572
|
if (!agentId) {
|
|
1581
573
|
return (rules.unrated_tier ?? 'APPROVAL');
|
|
1582
574
|
}
|
|
1583
|
-
// No reputation cache service -> treat as unrated
|
|
1584
575
|
if (!this.reputationCacheService) {
|
|
1585
576
|
return (rules.unrated_tier ?? 'APPROVAL');
|
|
1586
577
|
}
|
|
1587
|
-
// Lookup reputation
|
|
1588
578
|
const reputation = await this.reputationCacheService.getReputation(agentId, rules.tag1 ?? '', rules.tag2 ?? '');
|
|
1589
|
-
// Unrated (no data or RPC failure)
|
|
1590
579
|
if (!reputation) {
|
|
1591
580
|
return (rules.unrated_tier ?? 'APPROVAL');
|
|
1592
581
|
}
|
|
1593
|
-
// Score below threshold -> escalate
|
|
1594
582
|
if (reputation.score < rules.min_score) {
|
|
1595
583
|
return (rules.below_threshold_tier ?? 'APPROVAL');
|
|
1596
584
|
}
|
|
1597
|
-
// Score meets threshold -> no escalation
|
|
1598
585
|
return null;
|
|
1599
586
|
}
|
|
1600
|
-
/**
|
|
1601
|
-
* Resolve ERC-8004 agentId from a counterparty address.
|
|
1602
|
-
*
|
|
1603
|
-
* Looks up agent_identities table via wallet publicKey join to find
|
|
1604
|
-
* the chain_agent_id for the counterparty. Case-insensitive for EVM addresses.
|
|
1605
|
-
*
|
|
1606
|
-
* @returns chain_agent_id string if found, null otherwise
|
|
1607
|
-
*/
|
|
1608
587
|
resolveAgentIdFromAddress(toAddress) {
|
|
1609
588
|
if (!toAddress)
|
|
1610
589
|
return null;
|
|
1611
590
|
const normalizedAddress = toAddress.toLowerCase();
|
|
1612
|
-
// Join agent_identities + wallets to find chain_agent_id for the address.
|
|
1613
|
-
// Case-insensitive comparison (EVM address case varies).
|
|
1614
|
-
// Only consider REGISTERED or WALLET_LINKED identities.
|
|
1615
591
|
const rows = this.db
|
|
1616
592
|
.select({
|
|
1617
593
|
chainAgentId: agentIdentities.chainAgentId,
|
|
@@ -1625,16 +601,7 @@ export class DatabasePolicyEngine {
|
|
|
1625
601
|
(r.status === 'REGISTERED' || r.status === 'WALLET_LINKED'));
|
|
1626
602
|
return match?.chainAgentId ?? null;
|
|
1627
603
|
}
|
|
1628
|
-
/**
|
|
1629
|
-
* Pre-fetch reputation floor tier for use in evaluateAndReserve (synchronous context).
|
|
1630
|
-
*
|
|
1631
|
-
* Called from stage3Policy before entering the IMMEDIATE transaction, since
|
|
1632
|
-
* evaluateReputationThreshold is async (RPC call) but evaluateAndReserve is sync.
|
|
1633
|
-
*
|
|
1634
|
-
* @returns Object with tier and notification context if escalation needed, undefined otherwise
|
|
1635
|
-
*/
|
|
1636
604
|
async prefetchReputationTier(walletId, transaction, reputationCache) {
|
|
1637
|
-
// Load policies to check for REPUTATION_THRESHOLD
|
|
1638
605
|
const rows = await this.db
|
|
1639
606
|
.select()
|
|
1640
607
|
.from(policies)
|
|
@@ -1647,15 +614,13 @@ export class DatabasePolicyEngine {
|
|
|
1647
614
|
const policy = resolved.find((p) => p.type === 'REPUTATION_THRESHOLD');
|
|
1648
615
|
if (!policy)
|
|
1649
616
|
return undefined;
|
|
1650
|
-
const rules =
|
|
617
|
+
const rules = this.parseRules(policy.rules, ReputationThresholdRulesSchema, 'REPUTATION_THRESHOLD');
|
|
1651
618
|
if (!rules.check_counterparty)
|
|
1652
619
|
return undefined;
|
|
1653
|
-
// Resolve counterparty agentId
|
|
1654
620
|
const agentId = this.resolveAgentIdFromAddress(transaction.toAddress);
|
|
1655
621
|
if (!agentId) {
|
|
1656
622
|
return { tier: (rules.unrated_tier ?? 'APPROVAL') };
|
|
1657
623
|
}
|
|
1658
|
-
// Lookup reputation via the provided cache
|
|
1659
624
|
const reputation = await reputationCache.getReputation(agentId, rules.tag1 ?? '', rules.tag2 ?? '');
|
|
1660
625
|
if (!reputation) {
|
|
1661
626
|
return { tier: (rules.unrated_tier ?? 'APPROVAL') };
|
|
@@ -1667,152 +632,20 @@ export class DatabasePolicyEngine {
|
|
|
1667
632
|
threshold: String(rules.min_score),
|
|
1668
633
|
};
|
|
1669
634
|
}
|
|
1670
|
-
return undefined;
|
|
1671
|
-
}
|
|
1672
|
-
// ---------------------------------------------------------------------------
|
|
1673
|
-
// Phase 389: VENUE_WHITELIST evaluation
|
|
1674
|
-
// ---------------------------------------------------------------------------
|
|
1675
|
-
/**
|
|
1676
|
-
* Evaluate VENUE_WHITELIST policy (default-deny when enabled).
|
|
1677
|
-
*
|
|
1678
|
-
* Logic:
|
|
1679
|
-
* - If transaction has no venue (contractCall) -> return null (skip)
|
|
1680
|
-
* - If venue_whitelist_enabled setting is not 'true' -> return null (disabled)
|
|
1681
|
-
* - Find VENUE_WHITELIST policy in resolved list
|
|
1682
|
-
* - If no policy found + venue present -> DENY (default-deny)
|
|
1683
|
-
* - If policy found + venue in whitelist -> return null (allowed)
|
|
1684
|
-
* - If policy found + venue not in whitelist -> DENY
|
|
1685
|
-
*/
|
|
1686
|
-
evaluateVenueWhitelist(resolved, transaction) {
|
|
1687
|
-
// No venue (contractCall) -> skip
|
|
1688
|
-
if (!transaction.venue)
|
|
1689
|
-
return null;
|
|
1690
|
-
// Check if venue whitelist is enabled via Admin Settings
|
|
1691
|
-
let enabled = false;
|
|
1692
|
-
try {
|
|
1693
|
-
enabled = this.settingsService?.get('venue_whitelist_enabled') === 'true';
|
|
1694
|
-
}
|
|
1695
|
-
catch {
|
|
1696
|
-
// Setting key not registered -- disabled by default
|
|
1697
|
-
}
|
|
1698
|
-
if (!enabled)
|
|
1699
|
-
return null;
|
|
1700
|
-
const policy = resolved.find((p) => p.type === 'VENUE_WHITELIST');
|
|
1701
|
-
const venueNorm = transaction.venue.toLowerCase();
|
|
1702
|
-
if (!policy) {
|
|
1703
|
-
// Default-deny: venue present but no whitelist policy
|
|
1704
|
-
return {
|
|
1705
|
-
allowed: false,
|
|
1706
|
-
tier: 'INSTANT',
|
|
1707
|
-
reason: `VENUE_NOT_ALLOWED: venue '${transaction.venue}' is not whitelisted (no VENUE_WHITELIST policy)`,
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
|
-
const rules = JSON.parse(policy.rules);
|
|
1711
|
-
const isAllowed = rules.venues.some((v) => v.id.toLowerCase() === venueNorm);
|
|
1712
|
-
if (!isAllowed) {
|
|
1713
|
-
return {
|
|
1714
|
-
allowed: false,
|
|
1715
|
-
tier: 'INSTANT',
|
|
1716
|
-
reason: `VENUE_NOT_ALLOWED: venue '${transaction.venue}' is not in the whitelist`,
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
return null; // Venue allowed
|
|
635
|
+
return undefined;
|
|
1720
636
|
}
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
* - On exceed: return tier_on_exceed (default 'DELAY')
|
|
1734
|
-
*/
|
|
1735
|
-
evaluateActionCategoryLimit(resolved, transaction, walletId) {
|
|
1736
|
-
if (!transaction.actionCategory || transaction.notionalUsd === undefined)
|
|
1737
|
-
return null;
|
|
1738
|
-
// Find matching ACTION_CATEGORY_LIMIT policies
|
|
1739
|
-
const categoryPolicies = resolved.filter((p) => p.type === 'ACTION_CATEGORY_LIMIT');
|
|
1740
|
-
if (categoryPolicies.length === 0)
|
|
1741
|
-
return null;
|
|
1742
|
-
for (const policy of categoryPolicies) {
|
|
1743
|
-
const rules = JSON.parse(policy.rules);
|
|
1744
|
-
if (rules.category !== transaction.actionCategory)
|
|
1745
|
-
continue;
|
|
1746
|
-
const tierOnExceed = (rules.tier_on_exceed ?? 'DELAY');
|
|
1747
|
-
// Per-action limit
|
|
1748
|
-
if (rules.per_action_limit_usd !== undefined && transaction.notionalUsd > rules.per_action_limit_usd) {
|
|
1749
|
-
return {
|
|
1750
|
-
allowed: true,
|
|
1751
|
-
tier: tierOnExceed,
|
|
1752
|
-
reason: `ACTION_CATEGORY_LIMIT: per-action $${transaction.notionalUsd} exceeds ${rules.category} limit $${rules.per_action_limit_usd}`,
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
// Daily limit (cumulative query)
|
|
1756
|
-
if (rules.daily_limit_usd !== undefined && this.sqlite) {
|
|
1757
|
-
const todayStart = new Date();
|
|
1758
|
-
todayStart.setUTCHours(0, 0, 0, 0);
|
|
1759
|
-
const todayStartSec = Math.floor(todayStart.getTime() / 1000);
|
|
1760
|
-
const row = this.sqlite.prepare(`
|
|
1761
|
-
SELECT COALESCE(SUM(
|
|
1762
|
-
CASE
|
|
1763
|
-
WHEN json_extract(metadata, '$.notionalUsd') IS NOT NULL
|
|
1764
|
-
THEN CAST(json_extract(metadata, '$.notionalUsd') AS REAL)
|
|
1765
|
-
ELSE 0
|
|
1766
|
-
END
|
|
1767
|
-
), 0) AS total
|
|
1768
|
-
FROM transactions
|
|
1769
|
-
WHERE wallet_id = ?
|
|
1770
|
-
AND action_kind IN ('signedData', 'signedHttp')
|
|
1771
|
-
AND json_extract(metadata, '$.actionCategory') = ?
|
|
1772
|
-
AND created_at >= ?
|
|
1773
|
-
AND status != 'FAILED'
|
|
1774
|
-
`).get(walletId ?? resolved[0]?.walletId ?? null, rules.category, todayStartSec);
|
|
1775
|
-
const cumulative = (row?.total ?? 0) + transaction.notionalUsd;
|
|
1776
|
-
if (cumulative > rules.daily_limit_usd) {
|
|
1777
|
-
return {
|
|
1778
|
-
allowed: true,
|
|
1779
|
-
tier: tierOnExceed,
|
|
1780
|
-
reason: `ACTION_CATEGORY_LIMIT: daily cumulative $${cumulative.toFixed(2)} exceeds ${rules.category} limit $${rules.daily_limit_usd}`,
|
|
1781
|
-
};
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
// Monthly limit (cumulative query)
|
|
1785
|
-
if (rules.monthly_limit_usd !== undefined && this.sqlite) {
|
|
1786
|
-
const monthStart = new Date();
|
|
1787
|
-
monthStart.setUTCDate(1);
|
|
1788
|
-
monthStart.setUTCHours(0, 0, 0, 0);
|
|
1789
|
-
const monthStartSec = Math.floor(monthStart.getTime() / 1000);
|
|
1790
|
-
const row = this.sqlite.prepare(`
|
|
1791
|
-
SELECT COALESCE(SUM(
|
|
1792
|
-
CASE
|
|
1793
|
-
WHEN json_extract(metadata, '$.notionalUsd') IS NOT NULL
|
|
1794
|
-
THEN CAST(json_extract(metadata, '$.notionalUsd') AS REAL)
|
|
1795
|
-
ELSE 0
|
|
1796
|
-
END
|
|
1797
|
-
), 0) AS total
|
|
1798
|
-
FROM transactions
|
|
1799
|
-
WHERE wallet_id = ?
|
|
1800
|
-
AND action_kind IN ('signedData', 'signedHttp')
|
|
1801
|
-
AND json_extract(metadata, '$.actionCategory') = ?
|
|
1802
|
-
AND created_at >= ?
|
|
1803
|
-
AND status != 'FAILED'
|
|
1804
|
-
`).get(walletId ?? resolved[0]?.walletId ?? null, rules.category, monthStartSec);
|
|
1805
|
-
const cumulative = (row?.total ?? 0) + transaction.notionalUsd;
|
|
1806
|
-
if (cumulative > rules.monthly_limit_usd) {
|
|
1807
|
-
return {
|
|
1808
|
-
allowed: true,
|
|
1809
|
-
tier: tierOnExceed,
|
|
1810
|
-
reason: `ACTION_CATEGORY_LIMIT: monthly cumulative $${cumulative.toFixed(2)} exceeds ${rules.category} limit $${rules.monthly_limit_usd}`,
|
|
1811
|
-
};
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
return null; // Within limits
|
|
637
|
+
// -------------------------------------------------------------------------
|
|
638
|
+
// Private: buildTokenContext helper
|
|
639
|
+
// -------------------------------------------------------------------------
|
|
640
|
+
buildTokenContext(transaction, spendingPolicy) {
|
|
641
|
+
return {
|
|
642
|
+
type: transaction.type,
|
|
643
|
+
tokenAddress: transaction.tokenAddress,
|
|
644
|
+
tokenDecimals: transaction.tokenDecimals,
|
|
645
|
+
chain: transaction.chain,
|
|
646
|
+
assetId: transaction.assetId,
|
|
647
|
+
policyNetwork: spendingPolicy?.network ?? undefined,
|
|
648
|
+
};
|
|
1816
649
|
}
|
|
1817
650
|
}
|
|
1818
651
|
//# sourceMappingURL=database-policy-engine.js.map
|