diff --git a/.docker/Dockerfile b/.docker/Dockerfile index cae842eb1..cb04ccfdb 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,15 +1,21 @@ -FROM golang:1.24-alpine AS builder +FROM golang:1.24.0-alpine AS builder -RUN apk update && apk add --no-cache make bash nodejs npm +run apk update && apk add --no-cache make bash nodejs npm + +ARG EXPLORER_BASE_PATH +ARG WALLET_BASE_PATH WORKDIR /go/src/github.com/canopy-network/canopy COPY . /go/src/github.com/canopy-network/canopy -RUN make build/wallet -RUN make build/explorer +ENV EXPLORER_BASE_PATH=${EXPLORER_BASE_PATH} +ENV WALLET_BASE_PATH=${WALLET_BASE_PATH} + +#RUN make build/wallet +#RUN make build/explorer RUN go build -a -o bin ./cmd/main/... -FROM alpine:3.19 -WORKDIR /app -COPY --from=builder /go/src/github.com/canopy-network/canopy/bin ./ -ENTRYPOINT ["/app/bin"] +from alpine:3.19 +workdir /app +copy --from=builder /go/src/github.com/canopy-network/canopy/bin ./ +entrypoint ["/app/bin"] diff --git a/.docker/Dockerfile.optimized b/.docker/Dockerfile.optimized new file mode 100644 index 000000000..ba8205d43 --- /dev/null +++ b/.docker/Dockerfile.optimized @@ -0,0 +1,78 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.24.0-alpine AS builder + +# Install build dependencies +RUN apk update && apk add --no-cache make bash nodejs npm git + +ARG EXPLORER_BASE_PATH +ARG WALLET_BASE_PATH + +WORKDIR /go/src/github.com/canopy-network/canopy + +# ============================================ +# OPTIMIZATION 1: Cache Go module dependencies +# Copy only go.mod and go.sum first to cache dependencies layer +# This layer only invalidates when dependencies change +# ============================================ +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download + +# ============================================ +# OPTIMIZATION 2: Cache NPM dependencies for wallet +# Copy only package.json files first to cache npm install layer +# ============================================ +COPY cmd/rpc/web/wallet/package*.json ./cmd/rpc/web/wallet/ +RUN --mount=type=cache,target=/root/.npm \ + npm install --prefix ./cmd/rpc/web/wallet + +# ============================================ +# OPTIMIZATION 3: Cache NPM dependencies for explorer +# ============================================ +COPY cmd/rpc/web/explorer/package*.json ./cmd/rpc/web/explorer/ +RUN --mount=type=cache,target=/root/.npm \ + npm install --prefix ./cmd/rpc/web/explorer + +# ============================================ +# OPTIMIZATION 4: Copy web source and build BEFORE copying all source +# This caches web builds - only invalidates when web files change +# ============================================ +COPY cmd/rpc/web/wallet/ ./cmd/rpc/web/wallet/ +COPY cmd/rpc/web/explorer/ ./cmd/rpc/web/explorer/ + +ENV EXPLORER_BASE_PATH=${EXPLORER_BASE_PATH} +ENV WALLET_BASE_PATH=${WALLET_BASE_PATH} + +# Build web assets (cached unless web source changes) +RUN --mount=type=cache,target=/root/.npm \ + npm run build --prefix ./cmd/rpc/web/wallet +RUN --mount=type=cache,target=/root/.npm \ + npm run build --prefix ./cmd/rpc/web/explorer + +# ============================================ +# OPTIMIZATION 5: Copy remaining source code +# Web builds are already cached above +# ============================================ +COPY . . + +# ============================================ +# OPTIMIZATION 6: Use build cache mounts for Go +# Preserves Go build cache and module cache between builds +# This can speed up rebuilds by 5-10x +# ============================================ +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + go build -a -o bin ./cmd/main/... + +# ============================================ +# Final stage - minimal runtime image +# ============================================ +FROM alpine:3.19 + +WORKDIR /app + +# Copy only the built binary (web assets are embedded) +COPY --from=builder /go/src/github.com/canopy-network/canopy/bin ./bin + +ENTRYPOINT ["/app/bin"] diff --git a/.docker/compose.yaml b/.docker/compose.yaml index cd6f02ef3..cf06dafaa 100644 --- a/.docker/compose.yaml +++ b/.docker/compose.yaml @@ -1,4 +1,32 @@ services: + anvil: + image: ghcr.io/foundry-rs/foundry:latest + container_name: anvil + ports: + - 8545:8545 + networks: + - canopy + entrypoint: ["anvil", "-vvv", "--host", "0.0.0.0", "--block-time", "2", "--port", "8545"] + volumes: + - ./anvil:/anvil + healthcheck: + test: ["CMD", "cast", "block-number", "--rpc-url", "http://localhost:8545"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + anvil-init: + image: ghcr.io/foundry-rs/foundry:latest + container_name: anvil-init + user: "0" + networks: + - canopy + depends_on: + - anvil + entrypoint: ["/anvil/anvil.sh"] + volumes: + - ./anvil:/anvil + working_dir: /anvil node-1: container_name: node-1 build: @@ -22,7 +50,6 @@ services: # limits: # memory: 2G # cpus: "1.0" - node-2: container_name: node-2 build: @@ -33,7 +60,7 @@ services: - 40001:40001 # Explorer - 40002:40002 # RPC - 40003:40003 # Admin RPC - - 9002:9002 # TCP P2P + - 9003:9003 # TCP P2P - 6061:6060 # Debug - 9091:9091 # Metrics networks: @@ -41,35 +68,36 @@ services: command: ["start"] volumes: - ./volumes/node_2:/root/.canopy -# deploy: -# resources: -# limits: -# memory: 2G -# cpus: "1.0" -# node-3: -# container_name: node-3 -# build: -# context: .. -# dockerfile: .docker/Dockerfile -# args: -# BUILD_PATH: cmd/cli -# ports: -# - 30000:30000 # Wallet -# - 30001:30001 # Explorer -# - 30002:30002 # RPC -# - 30003:30003 # Admin RPC -# - 9003:9003 # TCP P2P -# networks: -# - canopy -# command: ["start"] -# volumes: -# - ./volumes/node_3:/root/.canopy -# deploy: -# resources: -# limits: -# memory: 2G -# cpus: "1.0" + # deploy: + # resources: + # limits: + # memory: 1G + # cpus: "1.0" + # node-3: + # container_name: node-3 + # build: + # context: .. + # dockerfile: .docker/Dockerfile + # args: + # EXPLORER_BASE_PATH: "" + # WALLET_BASE_PATH: "" + # ports: + # - 30000:30000 # Wallet + # - 30001:30001 # Explorer + # - 30002:30002 # RPC + # - 30003:30003 # Admin RPC + # - 9003:9003 # TCP P2P + # networks: + # - canopy + # command: ["start"] + # volumes: + # - ./volumes/node_3:/root/.canopy + # deploy: + # resources: + # limits: + # memory: 2G + # cpus: "1.0" networks: canopy: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/.docker/oracle-compose.yaml b/.docker/oracle-compose.yaml new file mode 100644 index 000000000..6100410ba --- /dev/null +++ b/.docker/oracle-compose.yaml @@ -0,0 +1,71 @@ +services: + node-1: + container_name: node-1 + image: canopy-node:latest + ports: + - 50000:50000 # Wallet + - 50001:50001 # Explorer + - 50002:50002 # RPC + - 50003:50003 # Admin RPC + - 9001:9001 # TCP P2P + - 6060:6060 # Debug + - 9090:9090 # Metrics + networks: + - canopy + command: ["start"] + volumes: + - ./volumes/node_1:/root/.canopy +# deploy: +# resources: +# limits: +# memory: 2G +# cpus: "1.0" + node-2: + container_name: node-2 + image: canopy-node:latest + ports: + - 40000:40000 # Wallet + - 40001:40001 # Explorer + - 40002:40002 # RPC + - 40003:40003 # Admin RPC + - 9003:9003 # TCP P2P + - 6061:6060 # Debug + - 9091:9091 # Metrics + networks: + - canopy + command: ["start"] + volumes: + - ./volumes/node_2:/root/.canopy + # deploy: + # resources: + # limits: + # memory: 1G + # cpus: "1.0" + # node-3: + # container_name: node-3 + # build: + # context: .. + # dockerfile: .docker/Dockerfile + # args: + # EXPLORER_BASE_PATH: "" + # WALLET_BASE_PATH: "" + # ports: + # - 30000:30000 # Wallet + # - 30001:30001 # Explorer + # - 30002:30002 # RPC + # - 30003:30003 # Admin RPC + # - 9003:9003 # TCP P2P + # networks: + # - canopy + # command: ["start"] + # volumes: + # - ./volumes/node_3:/root/.canopy + # deploy: + # resources: + # limits: + # memory: 2G + # cpus: "1.0" + +networks: + canopy: + driver: bridge diff --git a/.docker/volumes/node_1/config.json b/.docker/volumes/node_1/config.json index b02e660ad..8e4d141bd 100644 --- a/.docker/volumes/node_1/config.json +++ b/.docker/volumes/node_1/config.json @@ -1,49 +1,61 @@ { - "logLevel": "debug", - "chainId": 1, - "sleepUntil": 0, - "rootChain": [ - { - "chainId": 1, - "url": "http://node-1:50002" - } - ], - "runVDF": false, - "headless": false, - "walletPort": "50000", - "explorerPort": "50001", - "rpcPort": "50002", "adminPort": "50003", - "rpcURL": "http://localhost:50002", - "adminRPCUrl": "http://localhost:50003", - "timeoutS": 3, + "adminRPCUrl": "http://node-1:50003", + "bannedIPs": null, + "bannedPeerIDs": null, + "chainId": 1, + "commitTimeoutMS": 2000, "dataDirPath": "/root/.canopy", "dbName": "canopy", + "dialPeers": [], + "dropPercentage": 35, + "electionTimeoutMS": 1500, + "electionVoteTimeoutMS": 1500, + "ethBlockProviderConfig": { + "ethChainId": 1, + "ethNodeUrl": "http://anvil:8545", + "ethNodeWsUrl": "ws://anvil:8545", + "retryDelay": 5, + "safeBlockConfirmations": 5 + }, + "explorerPort": "50001", + "externalAddress": "node-1", + "gossipThreshold": 0, + "headless": false, "inMemory": false, - "networkID": 1, + "individualMaxTxSize": 4000, "listenAddress": "0.0.0.0:9001", - "externalAddress": "node-1", + "logLevel": "debug", "maxInbound": 21, "maxOutbound": 7, - "trustedPeerIDs": null, - "dialPeers": [], - "bannedPeerIDs": null, - "bannedIPs": null, - "gossipThreshold": 0, + "maxTotalBytes": 1000000, + "maxTransactionCount": 5000, + "metricsEnabled": true, "minimumPeersToStart": 0, + "networkID": 1, "newHeightTimeoutMS": 4500, - "electionTimeoutMS": 1500, - "electionVoteTimeoutMS": 1500, - "proposeTimeoutMS": 2500, - "proposeVoteTimeoutMS": 4000, + "oracleConfig": { + "committee": 2, + "orderResubmitDelay": 1, + "stateSaveFile": "last_block_height.txt" + }, "precommitTimeoutMS": 2000, "precommitVoteTimeoutMS": 2000, - "commitTimeoutMS": 2000, + "prometheusAddress": "0.0.0.0:9090", + "proposeTimeoutMS": 2500, + "proposeVoteTimeoutMS": 4000, + "rootChain": [ + { + "chainId": 1, + "url": "http://node-1:50002" + } + ], "roundInterruptTimeoutMS": 2000, - "maxTotalBytes": 1000000, - "maxTransactionCount": 5000, - "individualMaxTxSize": 4000, - "dropPercentage": 35, - "metricsEnabled": true, - "prometheusAddress": "0.0.0.0:9090" + "rpcPort": "50002", + "rpcURL": "http://node-1:50002", + "runVDF": true, + "sleepUntil": 0, + "timeoutS": 3, + "trustedPeerIDs": null, + "walletPort": "50000" } diff --git a/.docker/volumes/node_1/genesis.json b/.docker/volumes/node_1/genesis.json old mode 100755 new mode 100644 index 607e38203..2eb6a7067 --- a/.docker/volumes/node_1/genesis.json +++ b/.docker/volumes/node_1/genesis.json @@ -1,84 +1,123 @@ { - "time": "2024-12-14 20:10:52", + "time": "2025-07-23 01:47:59", "accounts": [ { - "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "amount": 1000000 + "address": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "amount": 1000000000 }, { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "amount": 1000000 + "address": "45281f3e49287fb12a6721bffab01fb60ee02df9", + "amount": 1000000000 }, { - "address": "6f94783856d5ce46d24dd5946215086211d70776", - "amount": 1000000 + "address": "652d4ac1d5340ae9d55e464ae36f12824d6ec4fd", + "amount": 1000000000 + }, + { + "address": "2664360a95b274e37f3704a07d050df2c283de01", + "amount": 1000000000 + }, + { + "address": "4487360b0f75713180d3e7fc8a0abf7c49b4eb78", + "amount": 1000000000 + }, + { + "address": "ea80225e40d40bce7cc08953f2618615af8d998d", + "amount": 1000000000 + }, + { + "address": "ed02514375777fe172874a34890b9ef407e8ff63", + "amount": 1000000000 + }, + { + "address": "b5ca2d2b88cb91d21c235958d50d33d8fa176ad9", + "amount": 1000000000 + }, + { + "address": "f68e28dca6f7aa0a8fc2e2eb9e16a428e6e421c8", + "amount": 1000000000 + }, + { + "address": "2b5265ee258fb73ba8d186e9585190e1daa93c05", + "amount": 1000000000 + }, + { + "address": "5132ff6e85b30bd11dc80c9d74f4375c43440bed", + "amount": 1000000000 + }, + { + "address": "ef525b1faa2e9b670b38c1b6f2d011275f6be6a1", + "amount": 1000000000 } ], "nonSigners": null, "validators": [ { - "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", + "address": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "publicKey": "97a7432a31f9a2de939cd9ec1f252650ccaec3a46be90570483ac6ed9d9697c0e72fcb4adbd6065c5234c32c19c0addd", "committees": [ - 1 + 1, + 2 ], "netAddress": "tcp://node-1", "stakedAmount": 1000000000, - "output": "851e90eaef1fa27debaee2c2591503bdeec1d123" + "output": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "compound": true }, { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", + "address": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "publicKey": "97a7432a31f9a2de939cd9ec1f252650ccaec3a46be90570483ac6ed9d9697c0e72fcb4adbd6065c5234c32c19c0addd", "committees": [ - 1 + 2 ], "netAddress": "tcp://node-2", "stakedAmount": 1000000000, - "output": "02cd4e5eb53ea665702042a6ed6d31d616054dc5" + "output": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "compound": true } ], "params": { "consensus": { "blockSize": 1000000, "protocolVersion": "1/0", - "rootChainID": 1, - "retired": 0 - }, - "validator": { - "unstakingBlocks": 2, - "maxPauseBlocks": 4380, - "doubleSignSlashPercentage": 10, - "nonSignSlashPercentage": 1, - "maxNonSign": 4, - "nonSignWindow": 10, - "maxCommittees": 15, - "maxCommitteeSize": 100, - "earlyWithdrawalPenalty": 20, - "delegateUnstakingBlocks": 2, - "minimumOrderSize": 1000, - "stakePercentForSubsidizedCommittee": 33, - "maxSlashPerCommittee": 15, - "delegateRewardPercentage": 10, - "buyDeadlineBlocks": 15, - "lockOrderFeeMultiplier": 2 + "retired": 0, + "rootChainID": 1 }, "fee": { - "sendFee": 10000, - "stakeFee": 10000, - "editStakeFee": 10000, - "unstakeFee": 10000, - "pauseFee": 10000, - "unpauseFee": 10000, + "certificateResultsFee": 0, "changeParameterFee": 10000, - "daoTransferFee": 10000, - "subsidyFee": 10000, "createOrderFee": 10000, + "daoTransferFee": 10000, + "deleteOrderFee": 10000, "editOrderFee": 10000, - "deleteOrderFee": 10000 + "editStakeFee": 10000, + "pauseFee": 10000, + "sendFee": 10000, + "stakeFee": 10000, + "subsidyFee": 10000, + "unpauseFee": 10000, + "unstakeFee": 10000 }, "governance": { - "daoRewardPercentage": 10 + "daoRewardPercentage": 5 + }, + "validator": { + "buyDeadlineBlocks": 60, + "delegateRewardPercentage": 10, + "delegateUnstakingBlocks": 12960, + "doubleSignSlashPercentage": 10, + "earlyWithdrawalPenalty": 0, + "lockOrderFeeMultiplier": 2, + "maxCommitteeSize": 100, + "maxCommittees": 16, + "maxNonSign": 60, + "maxPauseBlocks": 4380, + "maxSlashPerCommittee": 15, + "minimumOrderSize": 1000, + "nonSignSlashPercentage": 1, + "nonSignWindow": 100, + "stakePercentForSubsidizedCommittee": 33, + "unstakingBlocks": 2 } - }, - "supply": null -} + } +} \ No newline at end of file diff --git a/.docker/volumes/node_1/keystore.json b/.docker/volumes/node_1/keystore.json old mode 100644 new mode 100755 index dece94a76..aa17f999f --- a/.docker/volumes/node_1/keystore.json +++ b/.docker/volumes/node_1/keystore.json @@ -1,30 +1,102 @@ { "addressMap": { - "02cd4e5eb53ea665702042a6ed6d31d616054dc5": { - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", - "salt": "74f0112bcffc91215b6f6266acec38ca", - "encrypted": "183444bb69d2693a892e90ef7ebca9167719113488e4e803f8e87603ea84ccb40c423bc72db7303e81d7d216368ed763", - "keyAddress": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "keyNickname": "node_2" - }, - "6f94783856d5ce46d24dd5946215086211d70776": { - "publicKey": "abda38eb50fbe53db9e9c3b141c6a1ec54ad40a4840e34784c975da4ee175eb4c5dd10b6d759ae8fdf8bc22511bbd97b", - "salt": "cfbafc41835a47660f822ee26112d2c6", - "encrypted": "f18135d9509b41b5edc42e74d22396cba3f11fd8a5acae008a49b6e8bd3540a48f74c0d9e65872b922091286a531eee7", - "keyAddress": "6f94783856d5ce46d24dd5946215086211d70776", - "keyNickname": "node_3" - }, - "851e90eaef1fa27debaee2c2591503bdeec1d123": { - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", - "salt": "3bff15134210c811e308eaa9b7b6024c", - "encrypted": "8b757090dfc98bfbff4f5972f0ae4bb0339a82a753f633cd37aa921955d76cda6a5f521120e7559eb57f497e88f7f555", - "keyAddress": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "keyNickname": "node_1" + "2664360a95b274e37f3704a07d050df2c283de01": { + "publicKey": "8e6b205bef5ee8a2688230d6793481132ee822d2fdaa9932bd35cd3136cea7963448469366e2d20806b638848c141a0e", + "salt": "6042dc131039adc5e7d230813ca697fd", + "encrypted": "7a0397d30b28fd68d7b2c508991f6cf3bd3431f0f685a914caa297b4393e35e3df3a93c3fcd8fba752b0648d20e4de22", + "keyAddress": "2664360a95b274e37f3704a07d050df2c283de01", + "keyNickname": "nick-3" + }, + "26ee9f7c00343389a61e3a7d7bb46be1dc025968": { + "publicKey": "97a7432a31f9a2de939cd9ec1f252650ccaec3a46be90570483ac6ed9d9697c0e72fcb4adbd6065c5234c32c19c0addd", + "salt": "591b2d06aaebd70841b184cec6d8e9fe", + "encrypted": "05a90e83152cc31a3462f501b51079ada9d928c2b8e2eb10069527a9d76d8e99034245ecff804eeac9554bd0346aecd5", + "keyAddress": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "keyNickname": "nick-0" + }, + "2b5265ee258fb73ba8d186e9585190e1daa93c05": { + "publicKey": "89e6db51fdef4e104b9766d978ff91009096002046c4e1eac5806383a98e2b29e59a4210b5bb6adce93d85aec74adb2f", + "salt": "19922c1cc895c3748128308dd7935615", + "encrypted": "7c58911ec6b6519979f96f13c2f3b6b2ff9710cfd6429e24e24804106105ee4bea8f185bb61123eb026e097799d1f5eb", + "keyAddress": "2b5265ee258fb73ba8d186e9585190e1daa93c05", + "keyNickname": "nick-9" + }, + "4487360b0f75713180d3e7fc8a0abf7c49b4eb78": { + "publicKey": "8df3bbad9275d36e045c517c5996739e41327aa5fd04db48a5f36a39bca8e3950881fbd2f2208089bcdcc17f71585c0e", + "salt": "e5a49ba65f93e7ce6ab7342b6fcc06ff", + "encrypted": "2a88a64d173bc522a7465f0d980a251fbe5fce6b55e1cf7343bc50eae7d41b14bcb1ac6f5b4a3c479eb3536427940e9b", + "keyAddress": "4487360b0f75713180d3e7fc8a0abf7c49b4eb78", + "keyNickname": "nick-4" + }, + "45281f3e49287fb12a6721bffab01fb60ee02df9": { + "publicKey": "96675abd358f4d2d5dc0881c7f374b09390c0370735d257e07bbd267ce70a37d68d8affba8cb542714a32c6e126114b1", + "salt": "b604c7badfa082612f4c5dab96e9db59", + "encrypted": "3d1347fff7f2077f936528fceb596e6aea1ce03209ed1b9b45f39bb9a3ab949117e943fe00ef5deaf36ea4bdecf520f8", + "keyAddress": "45281f3e49287fb12a6721bffab01fb60ee02df9", + "keyNickname": "nick-1" + }, + "5132ff6e85b30bd11dc80c9d74f4375c43440bed": { + "publicKey": "926bd30292ecc054f1dfe679dde43774be87a5429443b397e432bf74bf4d2e2684d3c611ce448583e34a63b230fb4f3b", + "salt": "c1ac6dcb1f10db541c19e012f4458d71", + "encrypted": "ce63b6827b350886507c94ff957a6fe4e8f72c03f30bd0f1a5608502fe0e07d4be44335e42fc4541b81d8870e7d3a593", + "keyAddress": "5132ff6e85b30bd11dc80c9d74f4375c43440bed", + "keyNickname": "nick-10" + }, + "652d4ac1d5340ae9d55e464ae36f12824d6ec4fd": { + "publicKey": "94968722385b483e96123a478b18372d3ab83bd51a7fe00214998476bb22a471fe9da41858f7a0cd9b21ad284df6c16c", + "salt": "725636d765cad987f979c56736662729", + "encrypted": "dd7d4ae3afcd95d2a506aa0e291efe405c11918b3b38147444b02e3fb619606fb4a1dc57a1d0b40bf64212274f2488c4", + "keyAddress": "652d4ac1d5340ae9d55e464ae36f12824d6ec4fd", + "keyNickname": "nick-2" + }, + "b5ca2d2b88cb91d21c235958d50d33d8fa176ad9": { + "publicKey": "89213e66bfd6f8c224144dd0b16222d6eb9600406621a6a7571a4c7a7c229aff93dd70bbaf80a7b4fda6516dbb8a9cd7", + "salt": "0530f5b1842eac11696fc13b1dcaad17", + "encrypted": "9446b72ade142470f9f3983b03592d8dd7547cb8311ca94ac06b71b79b0b8311663558d33288ae0935ee9b93ed7a91fb", + "keyAddress": "b5ca2d2b88cb91d21c235958d50d33d8fa176ad9", + "keyNickname": "nick-7" + }, + "ea80225e40d40bce7cc08953f2618615af8d998d": { + "publicKey": "a189e3c774bb35899c5e0121f27fabdbd8cfe8e5d1a4d832915836e5262dc4a48f3a0089155e7e4fa2660ebdec6fa87b", + "salt": "e18d097b211dcc9f845d33b3e07ec9c1", + "encrypted": "ba995551e9e5a5f2851f43c65f0fe951de18e993188953240783f16ee8682e89988efcef3f1998f05f6c60373f724a9b", + "keyAddress": "ea80225e40d40bce7cc08953f2618615af8d998d", + "keyNickname": "nick-5" + }, + "ed02514375777fe172874a34890b9ef407e8ff63": { + "publicKey": "b06844d5922330ccd6ff5630c41f69704cc6a67b39d2c57fdc11421b0d72b7a57895b81a9bd3c527d6a56c1c68958824", + "salt": "77ca480a32cc670602edc013af2ef33a", + "encrypted": "c5d85a44da6fdd926a3ae70701598cc72badeece9000bc4bc09d9545c8e3cd06bcb8785a62f94ae27e77c7c419b1ffb3", + "keyAddress": "ed02514375777fe172874a34890b9ef407e8ff63", + "keyNickname": "nick-6" + }, + "ef525b1faa2e9b670b38c1b6f2d011275f6be6a1": { + "publicKey": "98e38c1eab039ad82a07746ade27058a94817497788661d4ef31e333fa38eaac37e47f5fe0ec6afc897ab6f6ce313bfa", + "salt": "bde5ff46d0b5daa02e306cba0c48b107", + "encrypted": "21c5f64591be0ea2c855ccc9781b094ac834810f1b22ddc5ae2cb8c133e6207dff264d89591012b71a6b75b216725089", + "keyAddress": "ef525b1faa2e9b670b38c1b6f2d011275f6be6a1", + "keyNickname": "nick-11" + }, + "f68e28dca6f7aa0a8fc2e2eb9e16a428e6e421c8": { + "publicKey": "834b1eb8d6ef698f82e9fa1685a3dc9ff98efff8d02008e7f9279d517819e02e5a84a16cffa0f10c18f405301313cb0a", + "salt": "7a8d8594c9d22ba573b2289b6dd51f83", + "encrypted": "1d9be6f9fa938671a8edb4eb368feb7ff992f4958c86ad8e78e77152269b611b7e67c30ca242397ddc4a5e22a6083c90", + "keyAddress": "f68e28dca6f7aa0a8fc2e2eb9e16a428e6e421c8", + "keyNickname": "nick-8" } }, "nicknameMap": { - "node_1": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "node_2": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "node_3": "6f94783856d5ce46d24dd5946215086211d70776" + "nick-0": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "nick-1": "45281f3e49287fb12a6721bffab01fb60ee02df9", + "nick-10": "5132ff6e85b30bd11dc80c9d74f4375c43440bed", + "nick-11": "ef525b1faa2e9b670b38c1b6f2d011275f6be6a1", + "nick-2": "652d4ac1d5340ae9d55e464ae36f12824d6ec4fd", + "nick-3": "2664360a95b274e37f3704a07d050df2c283de01", + "nick-4": "4487360b0f75713180d3e7fc8a0abf7c49b4eb78", + "nick-5": "ea80225e40d40bce7cc08953f2618615af8d998d", + "nick-6": "ed02514375777fe172874a34890b9ef407e8ff63", + "nick-7": "b5ca2d2b88cb91d21c235958d50d33d8fa176ad9", + "nick-8": "f68e28dca6f7aa0a8fc2e2eb9e16a428e6e421c8", + "nick-9": "2b5265ee258fb73ba8d186e9585190e1daa93c05" } } \ No newline at end of file diff --git a/.docker/volumes/node_1/polls.json b/.docker/volumes/node_1/polls.json index 613902737..2297bc74e 100755 --- a/.docker/volumes/node_1/polls.json +++ b/.docker/volumes/node_1/polls.json @@ -1,7 +1,7 @@ { "activePolls": { "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c": { - "851e90eaef1fa27debaee2c2591503bdeec1d123": true + "902319fe35f86ae1eca54d714b3fdd8d79b3fb7a": true } }, "pollMeta": { diff --git a/.docker/volumes/node_1/proposals.json b/.docker/volumes/node_1/proposals.json index bd99a08be..26ef261db 100755 --- a/.docker/volumes/node_1/proposals.json +++ b/.docker/volumes/node_1/proposals.json @@ -1,5 +1,5 @@ { - "7edaf1a0cac74c54b9ec34023af7fe8977fe36b95e40f4566db12b6f69daf75c": { + "17c5674c8b7aef9d751e45a7597f8ab5fd6208a93180ea92c06b6c3a2dc362f4": { "proposal": { "type": "changeParameter", "msg": { @@ -8,14 +8,13 @@ "parameterValue": "example", "startHeight": 1, "endHeight": 1000, - "signer": "4646464646464646464646464646464646464646", - "proposalHash": "" + "signer": "4646464646464646464646464646464646464646" }, "signature": { - "publicKey": "95ecf39914b027b32132a8e24ad0e7d7a3599dcd06757b9eacf4cab391ea1fd7e3facde205d949a6e147402c81f90b6f", - "signature": "923396da659a35c81b4600853d1f151126f1b322d82fe36bea50c978abbb99b7a20867ec33cc97649f46d58572e87b8a0c3a108a146265163410d4ac03555515a15096446479d9983edb492d26e6ff3f2e464657179692c12498e5cc5426b7f3" + "publicKey": "8ce45569286ae34ff28c71e90a150a2a39650bea278f1bec521552414b1a109fc4f8cfb6f54ce5bbdeee471aea343413", + "signature": "aa5d31fab236cbd54002d1965329462caefc8c1fb1427e2aa0a18a58926ebd6fac287b4b3292627f69fe2c2efcdad26409ecfc640b6e378d43e1da95beefc4d41f360870670ab9760ebfbc336d4af0249bef54189ec0bb9352af49daabf99143" }, - "time": 1741350747491943, + "time": 1753234438293975, "createdHeight": 1, "fee": 10000, "memo": "example", diff --git a/.docker/volumes/node_1/validator_key.json b/.docker/volumes/node_1/validator_key.json old mode 100755 new mode 100644 index ada70389f..2462d07ce --- a/.docker/volumes/node_1/validator_key.json +++ b/.docker/volumes/node_1/validator_key.json @@ -1 +1 @@ -"6c275055a4f6ae6bccf1e6552e172c7b8cc538a7b8d2dd645125df9e25c9ed2d" \ No newline at end of file +"3aacb2e68b37647bc205bdd74db3fce36114f811de9ad1596ec3065ac8d5daa9" \ No newline at end of file diff --git a/.docker/volumes/node_2/config.json b/.docker/volumes/node_2/config.json index eaedd498c..67ce55fed 100644 --- a/.docker/volumes/node_2/config.json +++ b/.docker/volumes/node_2/config.json @@ -1,52 +1,60 @@ { - "logLevel": "debug", - "chainId": 1, - "sleepUntil": 0, - "rootChain": [ - { - "chainId": 1, - "url": "http://node-1:50002" - }, - { - "chainId": 2, - "url": "http://node-2:40002" - } - ], - "runVDF": false, - "headless": false, - "walletPort": "40000", - "explorerPort": "40001", - "rpcPort": "40002", "adminPort": "40003", - "rpcURL": "http://localhost:40002", - "adminRPCUrl": "http://localhost:40003", - "timeoutS": 3, + "adminRPCUrl": "http://node-2:40003", + "bannedIPs": null, + "bannedPeerIDs": null, + "chainId": 2, + "commitTimeoutMS": 2000, "dataDirPath": "/root/.canopy", "dbName": "canopy", - "inMemory": false, - "networkID": 1, - "listenAddress": "0.0.0.0:9001", + "dialPeers": [], + "dropPercentage": 35, + "electionTimeoutMS": 1500, + "electionVoteTimeoutMS": 1500, + "ethBlockProviderConfig": { + "ethChainId": 1, + "ethNodeUrl": "http://anvil:8545", + "ethNodeWsUrl": "ws://anvil:8545", + "retryDelay": 5, + "safeBlockConfirmations": 5 + }, + "explorerPort": "40001", "externalAddress": "node-2", + "headless": false, + "inMemory": false, + "individualMaxTxSize": 4000, + "listenAddress": "127.0.0.102:9002", + "logLevel": "debug", "maxInbound": 21, "maxOutbound": 7, - "trustedPeerIDs": null, - "dialPeers": [], - "bannedPeerIDs": null, - "bannedIPs": null, + "maxTotalBytes": 1000000, + "maxTransactionCount": 5000, + "metricsEnabled": true, "minimumPeersToStart": 0, + "networkID": 1, "newHeightTimeoutMS": 4500, - "electionTimeoutMS": 1500, - "electionVoteTimeoutMS": 1500, - "proposeTimeoutMS": 2500, - "proposeVoteTimeoutMS": 4000, + "oracleConfig": { + "committee": 2, + "orderResubmitDelay": 1, + "stateSaveFile": "last_block_height.txt" + }, "precommitTimeoutMS": 2000, "precommitVoteTimeoutMS": 2000, - "commitTimeoutMS": 2000, + "prometheusAddress": "0.0.0.0:9091", + "proposeTimeoutMS": 2500, + "proposeVoteTimeoutMS": 4000, + "rootChain": [ + { + "chainId": 1, + "url": "http://node-1:50002" + } + ], "roundInterruptTimeoutMS": 2000, - "maxTotalBytes": 1000000, - "maxTransactionCount": 5000, - "individualMaxTxSize": 4000, - "dropPercentage": 35, - "metricsEnabled": true, - "prometheusAddress": "0.0.0.0:9091" + "rpcPort": "40002", + "rpcURL": "http://node-2:40002", + "runVDF": true, + "sleepUntil": 0, + "timeoutS": 3, + "trustedPeerIDs": null, + "walletPort": "40000" } diff --git a/.docker/volumes/node_2/genesis.json b/.docker/volumes/node_2/genesis.json old mode 100755 new mode 100644 index 607e38203..2eb6a7067 --- a/.docker/volumes/node_2/genesis.json +++ b/.docker/volumes/node_2/genesis.json @@ -1,84 +1,123 @@ { - "time": "2024-12-14 20:10:52", + "time": "2025-07-23 01:47:59", "accounts": [ { - "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "amount": 1000000 + "address": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "amount": 1000000000 }, { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "amount": 1000000 + "address": "45281f3e49287fb12a6721bffab01fb60ee02df9", + "amount": 1000000000 }, { - "address": "6f94783856d5ce46d24dd5946215086211d70776", - "amount": 1000000 + "address": "652d4ac1d5340ae9d55e464ae36f12824d6ec4fd", + "amount": 1000000000 + }, + { + "address": "2664360a95b274e37f3704a07d050df2c283de01", + "amount": 1000000000 + }, + { + "address": "4487360b0f75713180d3e7fc8a0abf7c49b4eb78", + "amount": 1000000000 + }, + { + "address": "ea80225e40d40bce7cc08953f2618615af8d998d", + "amount": 1000000000 + }, + { + "address": "ed02514375777fe172874a34890b9ef407e8ff63", + "amount": 1000000000 + }, + { + "address": "b5ca2d2b88cb91d21c235958d50d33d8fa176ad9", + "amount": 1000000000 + }, + { + "address": "f68e28dca6f7aa0a8fc2e2eb9e16a428e6e421c8", + "amount": 1000000000 + }, + { + "address": "2b5265ee258fb73ba8d186e9585190e1daa93c05", + "amount": 1000000000 + }, + { + "address": "5132ff6e85b30bd11dc80c9d74f4375c43440bed", + "amount": 1000000000 + }, + { + "address": "ef525b1faa2e9b670b38c1b6f2d011275f6be6a1", + "amount": 1000000000 } ], "nonSigners": null, "validators": [ { - "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", + "address": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "publicKey": "97a7432a31f9a2de939cd9ec1f252650ccaec3a46be90570483ac6ed9d9697c0e72fcb4adbd6065c5234c32c19c0addd", "committees": [ - 1 + 1, + 2 ], "netAddress": "tcp://node-1", "stakedAmount": 1000000000, - "output": "851e90eaef1fa27debaee2c2591503bdeec1d123" + "output": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "compound": true }, { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", + "address": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "publicKey": "97a7432a31f9a2de939cd9ec1f252650ccaec3a46be90570483ac6ed9d9697c0e72fcb4adbd6065c5234c32c19c0addd", "committees": [ - 1 + 2 ], "netAddress": "tcp://node-2", "stakedAmount": 1000000000, - "output": "02cd4e5eb53ea665702042a6ed6d31d616054dc5" + "output": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "compound": true } ], "params": { "consensus": { "blockSize": 1000000, "protocolVersion": "1/0", - "rootChainID": 1, - "retired": 0 - }, - "validator": { - "unstakingBlocks": 2, - "maxPauseBlocks": 4380, - "doubleSignSlashPercentage": 10, - "nonSignSlashPercentage": 1, - "maxNonSign": 4, - "nonSignWindow": 10, - "maxCommittees": 15, - "maxCommitteeSize": 100, - "earlyWithdrawalPenalty": 20, - "delegateUnstakingBlocks": 2, - "minimumOrderSize": 1000, - "stakePercentForSubsidizedCommittee": 33, - "maxSlashPerCommittee": 15, - "delegateRewardPercentage": 10, - "buyDeadlineBlocks": 15, - "lockOrderFeeMultiplier": 2 + "retired": 0, + "rootChainID": 1 }, "fee": { - "sendFee": 10000, - "stakeFee": 10000, - "editStakeFee": 10000, - "unstakeFee": 10000, - "pauseFee": 10000, - "unpauseFee": 10000, + "certificateResultsFee": 0, "changeParameterFee": 10000, - "daoTransferFee": 10000, - "subsidyFee": 10000, "createOrderFee": 10000, + "daoTransferFee": 10000, + "deleteOrderFee": 10000, "editOrderFee": 10000, - "deleteOrderFee": 10000 + "editStakeFee": 10000, + "pauseFee": 10000, + "sendFee": 10000, + "stakeFee": 10000, + "subsidyFee": 10000, + "unpauseFee": 10000, + "unstakeFee": 10000 }, "governance": { - "daoRewardPercentage": 10 + "daoRewardPercentage": 5 + }, + "validator": { + "buyDeadlineBlocks": 60, + "delegateRewardPercentage": 10, + "delegateUnstakingBlocks": 12960, + "doubleSignSlashPercentage": 10, + "earlyWithdrawalPenalty": 0, + "lockOrderFeeMultiplier": 2, + "maxCommitteeSize": 100, + "maxCommittees": 16, + "maxNonSign": 60, + "maxPauseBlocks": 4380, + "maxSlashPerCommittee": 15, + "minimumOrderSize": 1000, + "nonSignSlashPercentage": 1, + "nonSignWindow": 100, + "stakePercentForSubsidizedCommittee": 33, + "unstakingBlocks": 2 } - }, - "supply": null -} + } +} \ No newline at end of file diff --git a/.docker/volumes/node_2/keystore.json b/.docker/volumes/node_2/keystore.json old mode 100644 new mode 100755 index dece94a76..aa17f999f --- a/.docker/volumes/node_2/keystore.json +++ b/.docker/volumes/node_2/keystore.json @@ -1,30 +1,102 @@ { "addressMap": { - "02cd4e5eb53ea665702042a6ed6d31d616054dc5": { - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", - "salt": "74f0112bcffc91215b6f6266acec38ca", - "encrypted": "183444bb69d2693a892e90ef7ebca9167719113488e4e803f8e87603ea84ccb40c423bc72db7303e81d7d216368ed763", - "keyAddress": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "keyNickname": "node_2" - }, - "6f94783856d5ce46d24dd5946215086211d70776": { - "publicKey": "abda38eb50fbe53db9e9c3b141c6a1ec54ad40a4840e34784c975da4ee175eb4c5dd10b6d759ae8fdf8bc22511bbd97b", - "salt": "cfbafc41835a47660f822ee26112d2c6", - "encrypted": "f18135d9509b41b5edc42e74d22396cba3f11fd8a5acae008a49b6e8bd3540a48f74c0d9e65872b922091286a531eee7", - "keyAddress": "6f94783856d5ce46d24dd5946215086211d70776", - "keyNickname": "node_3" - }, - "851e90eaef1fa27debaee2c2591503bdeec1d123": { - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", - "salt": "3bff15134210c811e308eaa9b7b6024c", - "encrypted": "8b757090dfc98bfbff4f5972f0ae4bb0339a82a753f633cd37aa921955d76cda6a5f521120e7559eb57f497e88f7f555", - "keyAddress": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "keyNickname": "node_1" + "2664360a95b274e37f3704a07d050df2c283de01": { + "publicKey": "8e6b205bef5ee8a2688230d6793481132ee822d2fdaa9932bd35cd3136cea7963448469366e2d20806b638848c141a0e", + "salt": "6042dc131039adc5e7d230813ca697fd", + "encrypted": "7a0397d30b28fd68d7b2c508991f6cf3bd3431f0f685a914caa297b4393e35e3df3a93c3fcd8fba752b0648d20e4de22", + "keyAddress": "2664360a95b274e37f3704a07d050df2c283de01", + "keyNickname": "nick-3" + }, + "26ee9f7c00343389a61e3a7d7bb46be1dc025968": { + "publicKey": "97a7432a31f9a2de939cd9ec1f252650ccaec3a46be90570483ac6ed9d9697c0e72fcb4adbd6065c5234c32c19c0addd", + "salt": "591b2d06aaebd70841b184cec6d8e9fe", + "encrypted": "05a90e83152cc31a3462f501b51079ada9d928c2b8e2eb10069527a9d76d8e99034245ecff804eeac9554bd0346aecd5", + "keyAddress": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "keyNickname": "nick-0" + }, + "2b5265ee258fb73ba8d186e9585190e1daa93c05": { + "publicKey": "89e6db51fdef4e104b9766d978ff91009096002046c4e1eac5806383a98e2b29e59a4210b5bb6adce93d85aec74adb2f", + "salt": "19922c1cc895c3748128308dd7935615", + "encrypted": "7c58911ec6b6519979f96f13c2f3b6b2ff9710cfd6429e24e24804106105ee4bea8f185bb61123eb026e097799d1f5eb", + "keyAddress": "2b5265ee258fb73ba8d186e9585190e1daa93c05", + "keyNickname": "nick-9" + }, + "4487360b0f75713180d3e7fc8a0abf7c49b4eb78": { + "publicKey": "8df3bbad9275d36e045c517c5996739e41327aa5fd04db48a5f36a39bca8e3950881fbd2f2208089bcdcc17f71585c0e", + "salt": "e5a49ba65f93e7ce6ab7342b6fcc06ff", + "encrypted": "2a88a64d173bc522a7465f0d980a251fbe5fce6b55e1cf7343bc50eae7d41b14bcb1ac6f5b4a3c479eb3536427940e9b", + "keyAddress": "4487360b0f75713180d3e7fc8a0abf7c49b4eb78", + "keyNickname": "nick-4" + }, + "45281f3e49287fb12a6721bffab01fb60ee02df9": { + "publicKey": "96675abd358f4d2d5dc0881c7f374b09390c0370735d257e07bbd267ce70a37d68d8affba8cb542714a32c6e126114b1", + "salt": "b604c7badfa082612f4c5dab96e9db59", + "encrypted": "3d1347fff7f2077f936528fceb596e6aea1ce03209ed1b9b45f39bb9a3ab949117e943fe00ef5deaf36ea4bdecf520f8", + "keyAddress": "45281f3e49287fb12a6721bffab01fb60ee02df9", + "keyNickname": "nick-1" + }, + "5132ff6e85b30bd11dc80c9d74f4375c43440bed": { + "publicKey": "926bd30292ecc054f1dfe679dde43774be87a5429443b397e432bf74bf4d2e2684d3c611ce448583e34a63b230fb4f3b", + "salt": "c1ac6dcb1f10db541c19e012f4458d71", + "encrypted": "ce63b6827b350886507c94ff957a6fe4e8f72c03f30bd0f1a5608502fe0e07d4be44335e42fc4541b81d8870e7d3a593", + "keyAddress": "5132ff6e85b30bd11dc80c9d74f4375c43440bed", + "keyNickname": "nick-10" + }, + "652d4ac1d5340ae9d55e464ae36f12824d6ec4fd": { + "publicKey": "94968722385b483e96123a478b18372d3ab83bd51a7fe00214998476bb22a471fe9da41858f7a0cd9b21ad284df6c16c", + "salt": "725636d765cad987f979c56736662729", + "encrypted": "dd7d4ae3afcd95d2a506aa0e291efe405c11918b3b38147444b02e3fb619606fb4a1dc57a1d0b40bf64212274f2488c4", + "keyAddress": "652d4ac1d5340ae9d55e464ae36f12824d6ec4fd", + "keyNickname": "nick-2" + }, + "b5ca2d2b88cb91d21c235958d50d33d8fa176ad9": { + "publicKey": "89213e66bfd6f8c224144dd0b16222d6eb9600406621a6a7571a4c7a7c229aff93dd70bbaf80a7b4fda6516dbb8a9cd7", + "salt": "0530f5b1842eac11696fc13b1dcaad17", + "encrypted": "9446b72ade142470f9f3983b03592d8dd7547cb8311ca94ac06b71b79b0b8311663558d33288ae0935ee9b93ed7a91fb", + "keyAddress": "b5ca2d2b88cb91d21c235958d50d33d8fa176ad9", + "keyNickname": "nick-7" + }, + "ea80225e40d40bce7cc08953f2618615af8d998d": { + "publicKey": "a189e3c774bb35899c5e0121f27fabdbd8cfe8e5d1a4d832915836e5262dc4a48f3a0089155e7e4fa2660ebdec6fa87b", + "salt": "e18d097b211dcc9f845d33b3e07ec9c1", + "encrypted": "ba995551e9e5a5f2851f43c65f0fe951de18e993188953240783f16ee8682e89988efcef3f1998f05f6c60373f724a9b", + "keyAddress": "ea80225e40d40bce7cc08953f2618615af8d998d", + "keyNickname": "nick-5" + }, + "ed02514375777fe172874a34890b9ef407e8ff63": { + "publicKey": "b06844d5922330ccd6ff5630c41f69704cc6a67b39d2c57fdc11421b0d72b7a57895b81a9bd3c527d6a56c1c68958824", + "salt": "77ca480a32cc670602edc013af2ef33a", + "encrypted": "c5d85a44da6fdd926a3ae70701598cc72badeece9000bc4bc09d9545c8e3cd06bcb8785a62f94ae27e77c7c419b1ffb3", + "keyAddress": "ed02514375777fe172874a34890b9ef407e8ff63", + "keyNickname": "nick-6" + }, + "ef525b1faa2e9b670b38c1b6f2d011275f6be6a1": { + "publicKey": "98e38c1eab039ad82a07746ade27058a94817497788661d4ef31e333fa38eaac37e47f5fe0ec6afc897ab6f6ce313bfa", + "salt": "bde5ff46d0b5daa02e306cba0c48b107", + "encrypted": "21c5f64591be0ea2c855ccc9781b094ac834810f1b22ddc5ae2cb8c133e6207dff264d89591012b71a6b75b216725089", + "keyAddress": "ef525b1faa2e9b670b38c1b6f2d011275f6be6a1", + "keyNickname": "nick-11" + }, + "f68e28dca6f7aa0a8fc2e2eb9e16a428e6e421c8": { + "publicKey": "834b1eb8d6ef698f82e9fa1685a3dc9ff98efff8d02008e7f9279d517819e02e5a84a16cffa0f10c18f405301313cb0a", + "salt": "7a8d8594c9d22ba573b2289b6dd51f83", + "encrypted": "1d9be6f9fa938671a8edb4eb368feb7ff992f4958c86ad8e78e77152269b611b7e67c30ca242397ddc4a5e22a6083c90", + "keyAddress": "f68e28dca6f7aa0a8fc2e2eb9e16a428e6e421c8", + "keyNickname": "nick-8" } }, "nicknameMap": { - "node_1": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "node_2": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "node_3": "6f94783856d5ce46d24dd5946215086211d70776" + "nick-0": "26ee9f7c00343389a61e3a7d7bb46be1dc025968", + "nick-1": "45281f3e49287fb12a6721bffab01fb60ee02df9", + "nick-10": "5132ff6e85b30bd11dc80c9d74f4375c43440bed", + "nick-11": "ef525b1faa2e9b670b38c1b6f2d011275f6be6a1", + "nick-2": "652d4ac1d5340ae9d55e464ae36f12824d6ec4fd", + "nick-3": "2664360a95b274e37f3704a07d050df2c283de01", + "nick-4": "4487360b0f75713180d3e7fc8a0abf7c49b4eb78", + "nick-5": "ea80225e40d40bce7cc08953f2618615af8d998d", + "nick-6": "ed02514375777fe172874a34890b9ef407e8ff63", + "nick-7": "b5ca2d2b88cb91d21c235958d50d33d8fa176ad9", + "nick-8": "f68e28dca6f7aa0a8fc2e2eb9e16a428e6e421c8", + "nick-9": "2b5265ee258fb73ba8d186e9585190e1daa93c05" } } \ No newline at end of file diff --git a/.docker/volumes/node_2/polls.json b/.docker/volumes/node_2/polls.json index eaa7a4d9e..2297bc74e 100755 --- a/.docker/volumes/node_2/polls.json +++ b/.docker/volumes/node_2/polls.json @@ -1,7 +1,7 @@ { "activePolls": { "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c": { - "02cd4e5eb53ea665702042a6ed6d31d616054dc5": true + "902319fe35f86ae1eca54d714b3fdd8d79b3fb7a": true } }, "pollMeta": { diff --git a/.docker/volumes/node_2/proposals.json b/.docker/volumes/node_2/proposals.json index 1cc2fe347..10ba08d4a 100755 --- a/.docker/volumes/node_2/proposals.json +++ b/.docker/volumes/node_2/proposals.json @@ -1,28 +1,5 @@ { - "4a98aaf954508812e1ff607b20c4d955152e065186c92b9e6433d2f9b690ee9a": { - "proposal": { - "type": "changeParameter", - "msg": { - "parameterSpace": "cons", - "parameterKey": "rootChainID", - "parameterValue": 2, - "startHeight": 1, - "endHeight": 100, - "signer": "851e90eaef1fa27debaee2c2591503bdeec1d123" - }, - "signature": { - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", - "signature": "b2b9da2b5e1d20229f8a319c1781db6d17471735ed36643c88690b2272b44813fe52346c16f6feeef516f9b1c52ca21218e29635be08ed093c0dcd652aec8333bfaa13d8c84cdfedfcfd8ca01d1f3bc41c290a6b1c837d3accb6068dcc4593b2" - }, - "time": 1752067787310553, - "createdHeight": 14, - "fee": 10000, - "networkID": 1, - "chainID": 2 - }, - "approve": true - }, - "7edaf1a0cac74c54b9ec34023af7fe8977fe36b95e40f4566db12b6f69daf75c": { + "ac4f9235e8b6bd7e4166fc0401435feaf6396790d54da37cc7f3a85286312148": { "proposal": { "type": "changeParameter", "msg": { @@ -31,14 +8,13 @@ "parameterValue": "example", "startHeight": 1, "endHeight": 1000, - "signer": "4646464646464646464646464646464646464646", - "proposalHash": "" + "signer": "4646464646464646464646464646464646464646" }, "signature": { - "publicKey": "95ecf39914b027b32132a8e24ad0e7d7a3599dcd06757b9eacf4cab391ea1fd7e3facde205d949a6e147402c81f90b6f", - "signature": "923396da659a35c81b4600853d1f151126f1b322d82fe36bea50c978abbb99b7a20867ec33cc97649f46d58572e87b8a0c3a108a146265163410d4ac03555515a15096446479d9983edb492d26e6ff3f2e464657179692c12498e5cc5426b7f3" + "publicKey": "8b5a8ff402522f46171fb9408c32568c8e007a31e2602ea09b69b0a3522b67e9e34e10084f66f85530f49c1ba981864c", + "signature": "8f240289940bbb1dd0da376e64ef56d4bf5723c4180470dc69e80acbe9f29e12a7a5050d061b408475f75262e467c35f0e4ced0cd55e19b443333fcf62fcf4a0e06f172fdca6f7f708821284322165d48ef7907f265adfa5fe2cd1b5cdfd1ec3" }, - "time": 1741350747491943, + "time": 1753234441772748, "createdHeight": 1, "fee": 10000, "memo": "example", diff --git a/.docker/volumes/node_2/validator_key.json b/.docker/volumes/node_2/validator_key.json old mode 100755 new mode 100644 index 9b2691182..2462d07ce --- a/.docker/volumes/node_2/validator_key.json +++ b/.docker/volumes/node_2/validator_key.json @@ -1 +1 @@ -"0add1e53f45d439b98182216a9215db238aaff12d1b057b3130b0c5e8e9b0b36" \ No newline at end of file +"3aacb2e68b37647bc205bdd74db3fce36114f811de9ad1596ec3065ac8d5daa9" \ No newline at end of file diff --git a/.docker/volumes/node_3/config.json b/.docker/volumes/node_3/config.json index 93663a2e9..9e4d04581 100755 --- a/.docker/volumes/node_3/config.json +++ b/.docker/volumes/node_3/config.json @@ -1,52 +1,61 @@ { - "logLevel": "debug", - "chainId": 2, - "sleepUntil": 0, - "rootChain": [ - { - "chainId": 1, - "url": "http://node-1:50002" - }, - { - "chainId": 2, - "url": "http://node-3:30002" - } - ], - "runVDF": false, - "headless": false, - "walletPort": "30000", - "explorerPort": "30001", - "rpcPort": "30002", - "adminPort": "30003", - "rpcURL": "http://localhost:30002", - "adminRPCUrl": "http://localhost:30003", - "timeoutS": 3, - "dataDirPath": "/root/.canopy", - "dbName": "canopy", - "inMemory": false, - "networkID": 1, - "listenAddress": "0.0.0.0:9002", - "externalAddress": "node-3", - "maxInbound": 21, - "maxOutbound": 7, - "trustedPeerIDs": null, - "dialPeers": ["b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2@tcp://node-2"], - "bannedPeerIDs": null, - "bannedIPs": null, - "minimumPeersToStart": 0, - "newHeightTimeoutMS": 4500, - "electionTimeoutMS": 1500, - "electionVoteTimeoutMS": 1500, - "proposeTimeoutMS": 2500, - "proposeVoteTimeoutMS": 4000, - "precommitTimeoutMS": 2000, - "precommitVoteTimeoutMS": 2000, - "commitTimeoutMS": 2000, - "roundInterruptTimeoutMS": 2000, + "logLevel": "debug", + "chainId": 2, + "sleepUntil": 0, + "rootChain": [ + { + "chainId": 1, + "url": "http://node-1:50002" + } + ], + "runVDF": false, + "headless": false, + "walletPort": "30000", + "explorerPort": "30001", + "rpcPort": "30002", + "adminPort": "30003", + "rpcURL": "http://localhost:30002", + "adminRPCUrl": "http://localhost:30003", + "timeoutS": 3, + "dataDirPath": "/root/.canopy", + "dbName": "canopy", + "inMemory": false, + "networkID": 1, + "listenAddress": "0.0.0.0:9002", + "externalAddress": "node-3", + "maxInbound": 21, + "maxOutbound": 7, + "trustedPeerIDs": null, + "dialPeers": [ + "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2@tcp://node-1" + ], + "bannedPeerIDs": null, + "bannedIPs": null, + "minimumPeersToStart": 0, + "newHeightTimeoutMS": 1000, + "electionTimeoutMS": 1000, + "electionVoteTimeoutMS": 1000, + "proposeTimeoutMS": 1000, + "proposeVoteTimeoutMS": 1000, + "precommitTimeoutMS": 1000, + "precommitVoteTimeoutMS": 1000, + "commitTimeoutMS": 1000, + "roundInterruptTimeoutMS": 0, "maxTotalBytes": 1000000, "maxTransactionCount": 5000, "individualMaxTxSize": 4000, "dropPercentage": 35, "metricsEnabled": true, - "prometheusAddress": "0.0.0.0:9090" + "prometheusAddress": "0.0.0.0:9090", + "ethBlockProviderConfig": { + "ethNodeUrl": "http://anvil:8545", + "ethNodeWsUrl": "ws://anvil:8545", + "ethChainId": 31337 + }, + "oracleConfig": { + "orderStorePath": "/root/.canopy/oracle/orders", + "stateSaveFile": "/root/.canopy/oracle/last_block_height.txt", + "logPath": "/root/.canopy/oracle/logs", + "orderResubmitDelay": 3 + } } diff --git a/.docker/volumes/node_3/genesis.json b/.docker/volumes/node_3/genesis.json index 3bade9eb2..57011e68c 100755 --- a/.docker/volumes/node_3/genesis.json +++ b/.docker/volumes/node_3/genesis.json @@ -17,14 +17,14 @@ "nonSigners": null, "validators": [ { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", - "committees": [ - 2 - ], - "netAddress": "tcp://node-2", - "stakedAmount": 1000000000, - "output": "02cd4e5eb53ea665702042a6ed6d31d616054dc5" + "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", + "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", + "committees": [ + 2 + ], + "netAddress": "tcp://node-3", + "stakedAmount": 1000000000, + "output": "851e90eaef1fa27debaee2c2591503bdeec1d123" } ], "params": { diff --git a/.docker/volumes/node_3/validator_key.json b/.docker/volumes/node_3/validator_key.json index 96c31b834..ada70389f 100755 --- a/.docker/volumes/node_3/validator_key.json +++ b/.docker/volumes/node_3/validator_key.json @@ -1 +1 @@ -"3e9eb7a46defde2fa42dc74d85d0ee8cebaf327efa24b972e97b54af38d6e602" \ No newline at end of file +"6c275055a4f6ae6bccf1e6552e172c7b8cc538a7b8d2dd645125df9e25c9ed2d" \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..2c5babf0b --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# API Keys (Required to enable respective provider) +ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-... +PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-... +OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI/OpenRouter models. Format: sk-proj-... +GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models. +MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models. +XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models. +AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json). +OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication. +GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_... \ No newline at end of file diff --git a/.gitignore b/.gitignore index ecdcdf6da..0d65af3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ vendor cmd/web/explorer/.idea **/.DS_Store /cmd/tps/data + +rag +node_modules \ No newline at end of file diff --git a/.projectile b/.projectile new file mode 100644 index 000000000..472153629 --- /dev/null +++ b/.projectile @@ -0,0 +1,3 @@ +-/node_modules +-/rag +-rag \ No newline at end of file diff --git a/cache/solidity-files-cache.json b/cache/solidity-files-cache.json new file mode 100644 index 000000000..cd0b68bf5 --- /dev/null +++ b/cache/solidity-files-cache.json @@ -0,0 +1 @@ +{"_format":"","paths":{"artifacts":"out","build_infos":"out/build-info","sources":"src","tests":"test","scripts":"script","libraries":["lib","node_modules"]},"files":{"cmd/rpc/oracle/testing/contracts/USDC.sol":{"lastModificationDate":1752574865334,"contentHash":"f3edf42ae79e8f47","interfaceReprHash":null,"sourceName":"cmd/rpc/oracle/testing/contracts/USDC.sol","imports":[],"versionRequirement":"^0.8.0","artifacts":{"USDC":{"0.8.20":{"default":{"path":"USDC.sol/USDC.json","build_id":"bea85887a954e3be"}}}},"seenByCompiler":true}},"builds":["bea85887a954e3be"],"profiles":{"default":{"solc":{"optimizer":{"enabled":false,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode.object","evm.bytecode.sourceMap","evm.bytecode.linkReferences","evm.deployedBytecode.object","evm.deployedBytecode.sourceMap","evm.deployedBytecode.linkReferences","evm.deployedBytecode.immutableReferences","evm.methodIdentifiers","metadata"]}},"evmVersion":"cancun","viaIR":false,"libraries":{}},"vyper":{"evmVersion":"cancun","outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode"]}}}}},"preprocessed":false,"mocks":[]} \ No newline at end of file diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 5511fd5b4..beb0aa2d1 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1,6 +1,7 @@ package cli import ( + "context" "encoding/json" "errors" "flag" @@ -15,6 +16,8 @@ import ( "time" "github.com/canopy-network/canopy/cmd/rpc" + "github.com/canopy-network/canopy/cmd/rpc/oracle" + "github.com/canopy-network/canopy/cmd/rpc/oracle/eth" "github.com/canopy-network/canopy/controller" "github.com/canopy-network/canopy/fsm" "github.com/canopy-network/canopy/lib" @@ -29,6 +32,9 @@ import ( var rootCmd = &cobra.Command{ Use: "canopy", Short: "the canopy blockchain software", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + initEnv() + }, } var versionCmd = &cobra.Command{ @@ -54,12 +60,23 @@ func init() { autoCompleteCmd.AddCommand(generateCompleteCmd) autoCompleteCmd.AddCommand(autoCompleteInstallCmd) rootCmd.PersistentFlags().StringVar(&DataDir, "data-dir", lib.DefaultDataDirPath(), "custom data directory location") - config, validatorKey = InitializeDataDirectory(DataDir, lib.NewDefaultLogger()) +} + +// initEnv initializes global components required for operation +func initEnv() { + data := os.Getenv("CANOPY_DATA_DIR") + if data != "" { + DataDir = data + } + // create logger used throughout the application l = lib.NewLogger(lib.LoggerConfig{ Level: config.GetLogLevel(), Structured: config.Structured, JSON: config.JSON, }) + // initialize data directory, creating required files if neccessary + config, validatorKey = InitializeDataDirectory(DataDir, lib.NewDefaultLogger()) + // create an rpc client for this node client = rpc.NewClient(config.RPCUrl, config.AdminRPCUrl) } @@ -84,6 +101,9 @@ func Start() { if err := proxy.Start(); err != nil { l.Fatal(err.Error()) } + // create a shared context for oracle and ethereum block provider + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // initialize and start the metrics server metrics := lib.NewMetricsServer(validatorKey.PublicKey().Address(), float64(config.ChainId), rpc.SoftwareVersion, config.MetricsConfig, l) // create a new database object from the config @@ -99,8 +119,39 @@ func Start() { if err != nil { l.Fatal(err.Error()) } + var o *oracle.Oracle + // only enable oracle if configuration is present + if config.OracleEnabled { + oracleRoot := filepath.Join(DataDir, "oracle") + l.Infof("Oracle enabled, see oracle log in %s for details", oracleRoot) + // create a seperate logger for the oracle and all oracle components + oracleLogger := lib.NewOracleLogger( + lib.LoggerConfig{Level: config.GetLogLevel()}, + oracleRoot, + ) + // create a new ethereum disk storage instance for the oracle order store + oracleStorage, e := oracle.NewOracleDiskStorage(filepath.Join(oracleRoot, "store"), oracleLogger) + if e != nil { + l.Fatal(e.Error()) + } + // create a new order validator + orderValidator := oracle.NewOrderValidator() + // create the ethereum block provider + ethBlockProvider := eth.NewEthBlockProvider(config.EthBlockProviderConfig, orderValidator, oracleLogger, metrics) + + // create an absolute path for the state save file + config.OracleConfig.StateFile = filepath.Join(oracleRoot, config.OracleConfig.StateFile) + // create a new oracle instance and pass the ethereum block provider with shared context + o, e = oracle.NewOracle(ctx, config.OracleConfig, ethBlockProvider, oracleStorage, oracleLogger, metrics) + if e != nil { + l.Fatal(e.Error()) + } + } else { + l.Infof("Oracle not enabled") + } + // create a new instance of the application - app, err := controller.New(sm, config, validatorKey, metrics, l) + app, err := controller.New(sm, o, config, validatorKey, metrics, l) if err != nil { l.Fatal(err.Error()) } @@ -115,6 +166,10 @@ func Start() { // block until a kill signal is received waitForKill() proxy.Stop() + // cancel the shared context to stop oracle components + cancel() + // gracefuly stop oracle + o.Stop() // gracefully stop the app app.Stop() // gracefully stop the metrics server diff --git a/cmd/cli/query.go b/cmd/cli/query.go index e69b4c9e6..9c29d982e 100644 --- a/cmd/cli/query.go +++ b/cmd/cli/query.go @@ -40,6 +40,7 @@ func init() { queryCmd.AddCommand(retiredCommitteeCmd) queryCmd.AddCommand(orderCmd) queryCmd.AddCommand(ordersCmd) + queryCmd.AddCommand(oracleOrdersCmd) queryCmd.AddCommand(nonSignersCmd) queryCmd.AddCommand(paramsCmd) queryCmd.AddCommand(supplyCmd) @@ -192,6 +193,15 @@ var ( }, } + oracleOrdersCmd = &cobra.Command{ + Use: "oracle-orders --height=1 --per-page=10 --page-number=1", + Short: "query oracle orders stored in the oracle order store", + Run: func(cmd *cobra.Command, args []string) { + h, p := getPaginatedArgs() + writeToConsole(client.OracleOrders(h, p)) + }, + } + nonSignersCmd = &cobra.Command{ Use: "non-signers --height=1", Short: "query all bft non signing validators and their non-sign counter", diff --git a/cmd/rpc/client.go b/cmd/rpc/client.go index 9abb925b1..72d6dafde 100644 --- a/cmd/rpc/client.go +++ b/cmd/rpc/client.go @@ -9,6 +9,7 @@ import ( "github.com/canopy-network/canopy/fsm" + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" "github.com/canopy-network/canopy/controller" "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" @@ -269,6 +270,11 @@ func (c *Client) NextDexBatch(height, chainId uint64, withPoints bool) (p *lib.D return } +func (c *Client) OracleOrders(height uint64, params lib.PageParams) (orders []*types.WitnessedOrder, err lib.ErrorI) { + err = c.paginatedHeightRequest(OracleOrdersRouteName, height, params, &orders) + return +} + func (c *Client) LastProposers(height uint64) (p *lib.Proposers, err lib.ErrorI) { p = new(lib.Proposers) err = c.heightRequest(LastProposersRouteName, height, p) diff --git a/cmd/rpc/oracle/README.md b/cmd/rpc/oracle/README.md new file mode 100644 index 000000000..8a66b22a3 --- /dev/null +++ b/cmd/rpc/oracle/README.md @@ -0,0 +1,249 @@ +# Oracle Package + +## Multi-Oracle Consensus with Validator Voting + +The Oracle package provides cross-chain transaction witnessing and validation capabilities for the Canopy blockchain. It implements a chain-agnostic oracle that coordinates between source blockchains (like Ethereum) and a Canopy nested chain (observer chain) running this software to facilitate cross-chain order execution and validation. + +The Canopy oracle nested chain employs a witness-based consensus mechanism that combines independent validator nodes with the NestBFT consensus algorithm to ensure reliable attestation of external blockchain transactions. + +Each validator node in the committee independently monitors external chains (such as Ethereum) through configurable block providers and witnesses lock/close order transactions. When a relevant transaction is detected, oracle nodes validate it against the current order book and stores any witnessed orders locally. + +They participate in the NestBFT consensus protocol where witnessed orders are proposed in blocks and validated against each nodes' witnessed orders. Thie ensures that the required +2/3 supermajority agreement among participating validators before any witnessed order is finalized on the observer chain and reported to the root chain. + +## Overview + +The Oracle package is designed to handle: +- Witnessing transactions on external blockchains containing Canopy lock & close orders +- Validating and storing witnessed orders in a local order store +- Participating in the BFT consensus process by providing witnessed orders for block proposals +- Synchronizing with the root chain order book to maintain consistency +- Managing persistent state for reliable order processing + +## Core Components + +### Oracle + +The core of the Canopy Oracle system. It manages the overall cross-chain witnessing process, including: +- Receiving blocks from block providers +- Validating witnessed orders against the root chain order book +- Persisting witnessed orders to local storage +- Coordinating with the BFT consensus mechanism +- Maintaining synchronization with root chain order book state + +# BlockProvider Integration + +The Oracle integrates with external block providers through the `BlockProvider` interface. It provides: +- Real-time block monitoring from external chains +- Transaction parsing and order extraction +- Integration with Oracle's state management for gap detection and reorg handling + +## Sequence Diagram + +The following sequence diagram illustrates the core interactions in the Oracle package: + +```mermaid +sequenceDiagram + participant SC as Source Chain + participant BP as BlockProvider + participant O as Oracle + participant BFT as BFT + participant RC as Root Chain + + %% Block retrieval and processing + Note over SC,O: Source Chain Block Processing + SC->>BP: New block header received + BP->>BP: Calculate safe block height +loop Fetch Safe Blocks + BP->>SC: Fetch block + SC->>BP: Return block data + BP->>O: Send block to Oracle +end + + %% Oracle block processing + O->>O: Validate & write to store + + Note over BFT,O: Consensus Participation + %% BFT consensus integration + BFT->>O: Request witnessed orders + O->>O: Check should submit logic + O->>BFT: Return witnessed orders + BFT->>BFT: Produce block with witnessed orders + + %% Block proposal validation + BFT->>O: Block proposal validation + O->>O: Compare proposed vs witnessed orders + O->>BFT: Return validation result + BFT->>BFT: Commit Certificate + + Note over O,RC: Root Chain Interaction + %% Root Chain interaction + BFT->>RC: Certificate Results + RC->>O: Synchronize order store to order book +``` + +## Technical Details + +### Cross-Chain Transaction Witnessing + +The Oracle system uses a block-based monitoring approach to witness transactions on external chains. This is achieved by: + +- **Block Provider Integration**: Connects to external blockchain nodes through configurable providers +- **Transaction Parsing**: Extracts Canopy-specific order data from external chain transactions +- **Order Validation**: Performs comprehensive validation against root chain order book data +- **State Persistence**: Maintains reliable state storage for witnessed orders and processing height + +The system works like a specialized blockchain monitor that specifically looks for transactions containing Canopy order data, validates them against known orders, and stores them for later use in the consensus process. + +State persistence ensures that the Oracle can recover from interruptions without losing witnessed orders or reprocessing previously seen blocks. + +# BFT Consensus Integration + +The Oracle hooks into the BFT process in two phases: + +1. **Proposal Phase**: When acting as a proposer, the Oracle queries its witnessed order store to find orders that should be included in the next block proposal +2. **Validation Phase**: When validating block proposals from other nodes, the Oracle verifies that all proposed orders exist in its local witnessed order store + +This ensures that only orders witnessed by a majority of validator nodes are included in the blockchain, providing strong guarantees about cross-chain transaction validity. + +## Component Interactions + +### 1. Block Processing: External Chain Monitoring + +When a new block arrives from an external blockchain, the Oracle performs the following: + +- **Block Reception**: Receives blocks through a channel-based interface from the configured BlockProvider +- **Height Persistence**: Saves the current block height to disk before processing to enable recovery +- **Transaction Analysis**: Examines each transaction in the block for Canopy-specific order data +- **Order Validation**: Validates witnessed orders against the current root chain order book +- **Storage Operations**: Persists valid orders to the local order store with appropriate metadata + +### 2. Consensus Participation: BFT Integration + +The Oracle participates in the BFT consensus process through two key interfaces: + +- **WitnessedOrders**: Called during block proposal to provide witnessed orders that should be included in the next block +- **ValidateProposedOrders**: Called during block validation to verify that proposed orders were witnessed by this node + +## Configuration + +The Oracle system utilizes two primary configuration structures defined in `lib/config.go` that control both Ethereum block monitoring and Oracle consensus behavior. + +### EthBlockProviderConfig + +Controls how the Oracle connects to and monitors Ethereum blockchain for order transactions (`lib/config.go:288-311`): + +- **`NodeUrl`** (string): Ethereum RPC node URL for fetching blocks and transaction receipts. Used by the RPC client for all read operations including block retrieval and transaction validation. +- **`NodeWSUrl`** (string): Ethereum WebSocket URL for real-time block header notifications. Enables efficient block monitoring by subscribing to new block events. +- **`EVMChainId`** (uint64): Ethereum chain ID for transaction signature validation. Ensures transaction sender addresses are correctly extracted using the appropriate chain-specific signer. +- **`RetryDelay`** (int): Connection retry delay in seconds for RPC/WebSocket failures. Prevents rapid reconnection attempts that could overwhelm nodes during network issues. +- **`SafeBlockConfirmations`** (int): Number of block confirmations required before processing. Provides protection against chain reorganizations by only processing blocks that are unlikely to be reverted. +- **`StartupBlockDepth`** (uint64): How far back to start processing when no previous height is available. Ensures the Oracle can catch recently witnessed orders after restarts. + +### OracleConfig + +Controls the Oracle's consensus participation and order submission behavior (`lib/config.go:313-332`): + +- **`StateFile`** (string): Filename for persisting Oracle processing state. Enables recovery of the last processed block height and hash for gap detection and chain reorganization handling. +- **`OrderResubmitDelay`** (uint64): Number of root chain blocks to wait before resubmitting an order. Prevents duplicate submissions. +- **`Committee`** (uint64): Committee identifier this Oracle witnesses orders for. Must match the target committee in the root chain order book for proper order validation. +- **`ProposeLeadTime`** (uint64): Number of source chain blocks to wait before including newly witnessed orders in proposals. This allows a small amount of time for other validators to receive eth blocks should they be behind. +- **`ErrorReprocessDepth`** (uint64): How far back to reprocess blocks when sequence errors are detected. Enables recovery from chain reorganizations and missed blocks. +- **`LockOrderHoldTime`** (uint64): Number of root blocks to prevent resubmission of lock orders with the same ID. Prevents duplicate lock order submissions and potential double-spending. + +### cmd/rpc/oracle/eth Package - Block and Transaction Processing + +#### Next Height and Safe Height Usage + +The Ethereum block provider implements a block processing system centered around height management: + +- **Next Height Tracking**: The `nextHeight` field in `EthBlockProvider` tracks the next block to be processed. + +- **Safe Height Calculation**: In `processBlocks()` safe height is calculated by subtracting `SafeBlockConfirmations` from the current block height received via new block header notifications. This ensures only confirmed blocks are processed, protecting against chain reorganizations. + +- **Block Processing Loop**: The system processes all blocks from `nextHeight` to `safeHeight` in sequential order, fetching each block via `fetchBlock()`, processing transactions, and sending complete blocks through the unbuffered channel to the Oracle. + +#### Transaction Processing for Order Data + +Transaction processing follows a multi-stage validation pipeline: + +- **ERC20 Detection**: `parseDataForOrders()` examines transaction data to detect ERC20 transfers using the method signature `a9059cbb` and validates data length (68 bytes minimum). + +- **Self-Sent Lock Orders**: For transactions where `From()` equals `To()`, the entire transaction data is validated as lock order JSON. For ERC20 transfers with amount 0 sent to self, the extra data beyond the transfer call is validated as lock order JSON. + +- **Close Order Processing**: For standard ERC20 transfers, the extra data beyond the transfer parameters is validated as close order JSON and associated with the token transfer information. + +- **Transaction Success Validation**: `transactionSuccess()` fetches transaction receipts to ensure only successful on-chain transactions are processed, preventing failed transaction exploitation. + +### cmd/rpc/oracle/oracle.go - Core Oracle Operations + +#### The run() Method Analysis + +The `run()` method implements the main Oracle processing loop with robust error handling: + +- **Order Book Dependency**: Waits for valid order book before processing any blocks. + +- **Height Recovery**: Uses `OracleState.GetLastHeight()` to determine starting height from persistent state, enabling crash recovery and gap detection. + +- **Block Validation**: Each received block undergoes sequence validation via `stateManager.ValidateSequence()` to detect gaps and chain reorganizations before processing. + +- **State Persistence**: After successful block processing, saves state atomically using `stateManager.SaveProcessedBlock()` for reliable crash recovery. + +#### Order Validation Methods + +**validateLockOrder() Method**: +- Verifies lock order ID matches sell order ID using byte-level comparison +- Ensures lock order chain ID matches sell order committee +- Placeholder for additional seller address validation + +**validateCloseOrder() Method**: +- Validates sell order data field matches transaction recipient address (Ethereum-specific) +- Ensures close order ID matches sell order ID via byte comparison +- Verifies close order chain ID matches sell order committee +- Validates ERC20 transfer amount matches sell order requested amount +- Comprehensive validation against malicious or erroneous off-chain data + +#### WitnessedOrders Method Analysis + +The `WitnessedOrders()` method implements the core consensus participation logic: + +- **Order Book Iteration**: Iterates through root chain order book to find corresponding witnessed orders in local store +- **Lock Order Processing**: For unlocked sell orders (BuyerReceiveAddress == nil), searches for witnessed lock orders and applies submission logic via `shouldSubmit()` +- **Close Order Processing**: For locked sell orders (BuyerReceiveAddress != nil), searches for witnessed close orders and applies submission logic +- **Submission History**: Updates `LastSubmitHeight` for each submitted order to enable resubmission delay tracking + +#### ValidateProposedOrders Method Analysis + +The `ValidateProposedOrders()` method ensures consensus integrity: + +- **Lock Order Validation**: For each proposed lock order, retrieves witnessed order from local store and performs exact equality comparison using `lock.Equals()` +- **Close Order Validation**: For proposed close orders, constructs comparison close order with committee ID and validates equality +- **Strict Validation**: Returns validation errors if any proposed order doesn't exactly match witnessed orders, preventing malicious or incorrect proposals from being accepted +- **Comprehensive Logging**: Provides detailed logging for each validation step to aid in debugging consensus issues + +### cmd/rpc/oracle/state.go - Submission Logic + +#### The shouldSubmit Method Analysis + +The `shouldSubmit()` method implements submission control logic with multiple validation layers: + +**CHECK 1 - Propose Lead Time**: +- Compares current source chain height with witnessed height plus `ProposeLeadTime` +- Ensures sufficient confirmations have passed since the order was witnessed +- Allows time for other validators to receive and process the same Ethereum blocks +- Prevents premature submission of recently witnessed orders + +**CHECK 2 - Resubmit Delay**: +- Compares current root height with last submission height plus `OrderResubmitDelay` +- Prevents rapid resubmission of the same order across consecutive root chain blocks +- Uses per-order tracking via `LastSubmitHeight` field in witnessed orders + +**CHECK 3 - Lock Order Specific Restrictions**: +- Maintains `lockOrderSubmissions` map tracking when each lock order ID was first submitted +- Enforces `LockOrderHoldTime` delay between submissions of lock orders with the same ID +- Prevents duplicate lock order submissions +- Records new submission heights for lock orders upon successful validation + +**CHECK 4 - General Submission History**: +- Maintains `submissionHistory` map preventing duplicate submissions within the same proposal round +- Tracks order submissions at specific root heights to prevent immediate resubmission +- Provides final approval for order submission after all other checks pass diff --git a/cmd/rpc/oracle/error.go b/cmd/rpc/oracle/error.go new file mode 100644 index 000000000..dfa18fafc --- /dev/null +++ b/cmd/rpc/oracle/error.go @@ -0,0 +1,134 @@ +package oracle + +import ( + "fmt" + "math" + + "github.com/canopy-network/canopy/lib" +) + +const ( + NoCode lib.ErrorCode = math.MaxUint32 + + // Oracle Module + OracleModule lib.ErrorModule = "oracle" + + // Oracle Module Error Codes + CodeReadStateFile lib.ErrorCode = 1 + CodeParseHeight lib.ErrorCode = 2 + CodeWriteStateFile lib.ErrorCode = 3 + CodeCreateDirectory lib.ErrorCode = 4 + CodeGetHomeDirectory lib.ErrorCode = 5 + CodeUnmarshalOrder lib.ErrorCode = 6 + CodeMarshalOrder lib.ErrorCode = 7 + CodeOrderNotFound lib.ErrorCode = 8 + CodeGetOrderBook lib.ErrorCode = 9 + CodeAmountMismatch lib.ErrorCode = 10 + CodeOrderNotVerified lib.ErrorCode = 11 + CodeOrderValidation lib.ErrorCode = 12 + CodeBlockSequence lib.ErrorCode = 13 + CodeChainReorg lib.ErrorCode = 14 + CodeNilOrderBook lib.ErrorCode = 15 + CodeContextCancelled lib.ErrorCode = 16 + CodeChannelClosed lib.ErrorCode = 17 + + // OrderStore Module + OrderStoreModule lib.ErrorModule = "order_store" + + // OracleStore Module Error Codes + CodeValidateOrder lib.ErrorCode = 1 + CodeVerifyOrder lib.ErrorCode = 2 + CodeReadOrder lib.ErrorCode = 3 + CodeRemoveOrder lib.ErrorCode = 4 + CodeWriteOrder lib.ErrorCode = 5 +) + +// Error functions for Order Store module +func ErrValidateOrder(err error) lib.ErrorI { + return lib.NewError(CodeValidateOrder, OrderStoreModule, "failed to validate order: "+err.Error()) +} + +func ErrVerifyOrder(err error) lib.ErrorI { + return lib.NewError(CodeVerifyOrder, OrderStoreModule, "failed to verify order: "+err.Error()) +} + +func ErrReadOrder(err error) lib.ErrorI { + return lib.NewError(CodeReadOrder, OrderStoreModule, "failed to read order: "+err.Error()) +} + +func ErrRemoveOrder(err error) lib.ErrorI { + return lib.NewError(CodeRemoveOrder, OrderStoreModule, "failed to remove order: "+err.Error()) +} + +func ErrWriteOrder(err error) lib.ErrorI { + return lib.NewError(CodeWriteOrder, OrderStoreModule, "failed to write order: "+err.Error()) +} + +// Error functions for Oracle module +func ErrReadStateFile(err error) lib.ErrorI { + return lib.NewError(CodeReadStateFile, OracleModule, "failed to read oracle state file: "+err.Error()) +} + +func ErrParseState(err error) lib.ErrorI { + return lib.NewError(CodeParseHeight, OracleModule, "failed to parse height: "+err.Error()) +} + +func ErrWriteStateFile(err error) lib.ErrorI { + return lib.NewError(CodeWriteStateFile, OracleModule, "failed to oracle state file: "+err.Error()) +} + +func ErrCreateDirectory(err error) lib.ErrorI { + return lib.NewError(CodeCreateDirectory, OracleModule, "failed to create directory: "+err.Error()) +} + +func ErrGetHomeDirectory(err error) lib.ErrorI { + return lib.NewError(CodeGetHomeDirectory, OracleModule, "failed to get home directory: "+err.Error()) +} + +func ErrUnmarshalOrder(err error) lib.ErrorI { + return lib.NewError(CodeUnmarshalOrder, OracleModule, "failed to unmarshal order: "+err.Error()) +} + +func ErrMarshalOrder(err error) lib.ErrorI { + return lib.NewError(CodeMarshalOrder, OracleModule, "failed to marshal order: "+err.Error()) +} + +func ErrOrderNotFoundInOrderBook(orderId string) lib.ErrorI { + return lib.NewError(CodeOrderNotFound, OracleModule, "order not found in order book: "+orderId) +} + +func ErrGetOrderBook(err error) lib.ErrorI { + return lib.NewError(CodeGetOrderBook, OracleModule, "failed to get order book: "+err.Error()) +} + +func ErrAmountMismatch(transferAmount, orderAmount uint64) lib.ErrorI { + return lib.NewError(CodeAmountMismatch, OracleModule, fmt.Sprintf("transfer amount %d does not match order amount %d", transferAmount, orderAmount)) +} + +func ErrOrderNotVerified(s string, err error) lib.ErrorI { + return lib.NewError(CodeOrderNotVerified, OracleModule, "order not verified: "+err.Error()) +} + +func ErrOrderValidation(s string) lib.ErrorI { + return lib.NewError(CodeOrderValidation, OracleModule, "order validation failure: "+s) +} + +func ErrBlockSequence(message string) lib.ErrorI { + return lib.NewError(CodeBlockSequence, OracleModule, "block sequence error: "+message) +} + +func ErrChainReorg(message string) lib.ErrorI { + return lib.NewError(CodeChainReorg, OracleModule, "chain reorganization detected: "+message) +} + +func ErrNilOrderBook() lib.ErrorI { + return lib.NewError(CodeNilOrderBook, OracleModule, "order book is nil") +} + +func ErrContextCancelled() lib.ErrorI { + return lib.NewError(CodeContextCancelled, OracleModule, "oracle context cancelled") +} + +func ErrChannelClosed() lib.ErrorI { + return lib.NewError(CodeChannelClosed, OracleModule, "channel closed") +} diff --git a/cmd/rpc/oracle/eth/ETH_FLOW.md b/cmd/rpc/oracle/eth/ETH_FLOW.md new file mode 100644 index 000000000..7a34e977e --- /dev/null +++ b/cmd/rpc/oracle/eth/ETH_FLOW.md @@ -0,0 +1,350 @@ +# Code Flow Analysis Report: Ethereum Block Processing Oracle + +## Executive Summary +- **Component Purpose**: Real-time Ethereum blockchain monitoring system that processes blocks, validates transactions, and extracts Canopy orders for the oracle system +- **Entry Points**: `EthBlockProvider.Start()` method in `block_provider.go:148` +- **Scope**: Complete flow from Ethereum block reception through WebSocket subscriptions to order extraction and channel delivery + +## Flow Diagram +```mermaid +graph TD + H[Receive header notification] + H --> I[processBlocks] + I --> J[Calculate safe height] + J --> K[fetchBlock for each height] + K --> L[processBlockTransactions] + L --> M[Parse transaction data] + M --> N{Order found?} + N -->|Yes| O[Validate transaction success] + N -->|No| P[Continue to next tx] + O --> Q{Transaction successful?} + Q -->|Yes| R[Extract ERC20 token info] + Q -->|No| S[Clear Order] + R --> T[Send block to Oracle] + T --> U[Increment next height] + U --> V{More blocks to process?} + V -->|Yes| K + V -->|No| H +``` + +## Detailed Flow Analysis + +### 1. Entry Points and Initialization +- **File**: `block_provider.go:148` +- **Function**: `Start(ctx context.Context)` +- **Purpose**: Initiates the Ethereum block monitoring system +- **Input Parameters**: Context for cancellation control +- **Initial Setup**: Launches `run()` goroutine for continuous operation + +### 2. Main Processing Pipeline + +**Step 1: Connection Establishment** - `block_provider.go:184` +- **Location**: `connect()` method +- **Purpose**: Establishes RPC and WebSocket connections to Ethereum node +- **Input**: RPC URL and WebSocket URL from configuration +- **Processing**: + - Creates RPC client via `ethclient.DialContext()` + - Creates WebSocket client for real-time subscriptions + - Closes existing connections before establishing new ones +- **Output**: Connected RPC and WebSocket clients +- **Safeguards**: Connection validation, automatic retry on failure with configurable delay + +**Step 2: Header Monitoring** - `block_provider.go:214` +- **Location**: `monitorHeaders()` method +- **Purpose**: Subscribes to new Ethereum block headers via WebSocket +- **Input**: WebSocket client and context +- **Processing**: + - Creates buffered channel for headers (size 10) + - Subscribes to `SubscribeNewHead()` + - Processes headers as they arrive +- **Output**: Stream of new block headers +- **Safeguards**: Nil header validation, subscription error handling, graceful unsubscribe + +**Step 3: Safe Block Processing** - `block_provider.go:255` +- **Location**: `processBlocks()` method +- **Purpose**: Calculates safe block height and processes all unprocessed blocks +- **Input**: Current block height from header +- **Processing**: + - Calculates safe height: `currentHeight - safeBlockConfirmations` + - Processes all blocks from `nextHeight` to safe height sequentially + - Uses mutex lock for thread-safe height management +- **Output**: Processed blocks sent through channel +- **Safeguards**: Negative height protection, mutex synchronization, sequential processing + +**Step 4: Block Fetching** - `block_provider.go:101` +- **Location**: `fetchBlock()` method +- **Purpose**: Retrieves block data from Ethereum RPC and wraps transactions +- **Input**: Block height/number +- **Processing**: + - Calls `rpcClient.BlockByNumber()` to fetch block + - Creates `Block` wrapper with metadata + - Wraps each transaction with `NewTransaction()` +- **Output**: `Block` instance with wrapped transactions +- **Safeguards**: Error handling for RPC failures, transaction wrapping validation + +**Step 5: Transaction Processing** - `block_provider.go:298` +- **Location**: `processBlockTransactions()` method +- **Purpose**: Analyzes transactions for Canopy orders and validates success +- **Input**: Block with transactions +- **Processing**: + - Iterates through all transactions in block + - Calls `parseDataForOrders()` to extract order data + - Validates transaction success via receipt check + - Fetches ERC20 token information if applicable +- **Output**: Transactions with validated orders and token metadata +- **Safeguards**: Transaction receipt validation, order validation, token info caching + +**Step 6: Order Data Parsing** - `transaction.go:72` +- **Location**: `parseDataForOrders()` method +- **Purpose**: Extracts Canopy lock/close orders from transaction data +- **Input**: Transaction data bytes and order validator +- **Processing**: + - Detects self-sent transactions (lock orders) + - Parses ERC20 transfer data for close orders + - Validates order JSON against schemas + - Creates `WitnessedOrder` instances +- **Output**: Populated order data in transaction +- **Safeguards**: JSON validation, order type checking, data length validation + +## Data Flow Mapping +```mermaid +flowchart TD + subgraph Input + A[Ethereum Node] --> B[WebSocket Headers] + A --> C[RPC Block Data] + end + subgraph Processing + B --> D[Header Processing] + D --> E[Safe Height Calc] + E --> F[Block Fetching] + C --> F + F --> G[Transaction Analysis] + G --> H[Order Extraction] + H --> I[Success Validation] + I --> J[Token Info Cache] + end + subgraph Output + J --> K[Block Channel] + K --> L[Oracle Consumer] + end +``` + +## Order Processing Flow +```mermaid +graph TD + A[Transaction Data] --> B{Self-sent tx?} + B -->|Yes| C[Parse as Lock Order] + B -->|No| D[Check ERC20 Transfer] + D --> E{Valid ERC20?} + E -->|No| F[Skip Transaction] + E -->|Yes| G[Extract Transfer Data] + G --> H{Self-sent + 0 amount?} + H -->|Yes| I[Parse as Lock Order] + H -->|No| J[Parse as Close Order] + C --> K[Validate Order JSON] + I --> K + J --> K + K --> L{Valid JSON?} + L -->|Yes| M[Create WitnessedOrder] + L -->|No| N[Clear Order Data] + M --> O[Check Tx Success] + O --> P{Receipt Status = 1?} + P -->|Yes| Q[Keep Order] + P -->|No| R[Clear Order] +``` + +## Security Architecture + +**Input Validation** +- **Location**: `transaction.go:47,205` +- **Mechanisms**: + - Nil transaction checks + - ERC20 data length validation (68 bytes minimum) + - Method ID verification (`a9059cbb` for transfers) + - Address format validation +- **Bypass Potential**: Method ID could be spoofed, but order validation provides secondary check + +**Access Controls** +- **Authentication**: No authentication - relies on Ethereum node security +- **Authorization**: No explicit authorization controls +- **Session Management**: WebSocket connection management with automatic reconnection + +**Data Protection** +- **Encryption**: Relies on TLS for RPC/WebSocket connections +- **Sensitive Data Handling**: Private keys not handled in this component +- **Audit Logging**: Extensive logging of block processing, errors, and order detection + +**Rate Limiting & Resource Controls** +- **Request Limits**: No explicit rate limiting implemented +- **Resource Quotas**: Buffered channels (size 10 for headers) +- **Timeout Protections**: Context-based cancellation throughout + +## Error Handling & Recovery + +**Error Scenarios** +```mermaid +graph TD + A[Operation] --> B{Success?} + B -->|Yes| C[Continue Processing] + B -->|No| D[Error Type?] + D -->|Connection| E[Retry with Delay] + D -->|Validation| F[Log Warning & Skip] + D -->|Transaction| G[Clear Order Data] + D -->|Context Cancel| H[Graceful Shutdown] + E --> I[Reconnect Loop] + F --> J[Continue Next Item] + G --> J + H --> K[Close Connections] +``` + +**Recovery Mechanisms** +- **Rollback Procedures**: None implemented - processes linearly +- **Retry Logic**: Connection failures retry with configurable delay +- **Circuit Breakers**: Context cancellation stops all operations +- **Graceful Degradation**: Failed transactions are skipped, processing continues + +## Connection Management Flow +```mermaid +sequenceDiagram + participant P as EthBlockProvider + participant R as RPC Client + participant W as WebSocket Client + participant E as Ethereum Node + + P->>P: Start() + P->>P: run() goroutine + loop Connection Loop + P->>P: connect() + P->>R: ethclient.DialContext(rpcUrl) + R->>E: Establish RPC connection + E-->>R: Connection established + R-->>P: RPC client ready + P->>W: ethclient.DialContext(wsUrl) + W->>E: Establish WebSocket connection + E-->>W: WebSocket connected + W-->>P: WS client ready + P->>P: monitorHeaders() + P->>W: SubscribeNewHead() + W->>E: Subscribe to new headers + loop Header Processing + E->>W: New header notification + W->>P: Header received + P->>P: processBlocks() + P->>R: BlockByNumber() for each safe block + R->>E: Fetch block data + E-->>R: Block data + R-->>P: Block received + P->>P: processBlockTransactions() + P->>R: TransactionReceipt() for orders + R->>E: Get receipt + E-->>R: Receipt data + R-->>P: Receipt received + P->>P: Send block to channel + end + Note over P,E: On error: close connections and retry + P->>R: Close() + P->>W: Close() + end +``` + +## Logic Analysis + +**State Management** +- **State Variables**: `nextHeight` (protected by mutex), connection clients, block channel +- **State Transitions**: Height increments sequentially, connections cycle through connect/disconnect +- **Consistency Guarantees**: Sequential block processing ensures ordering +- **Concurrency Handling**: Mutex protects height updates, goroutine-safe channel operations + +**Business Logic Validation** +- **Business Rules**: + - Only process blocks with sufficient confirmations + - Validate transaction success before processing orders + - Self-sent transactions indicate lock orders + - ERC20 transfers with data indicate close orders +- **Constraint Enforcement**: JSON schema validation, transaction receipt verification +- **Edge Case Handling**: Handles nil headers, negative heights, connection failures + +**Performance Considerations** +- **Bottlenecks**: + - Sequential block processing (no parallelization) + - Token info contract calls for each new token + - Transaction receipt fetching for each transaction with orders +- **Scalability**: Limited by Ethereum node RPC rate limits and single goroutine processing +- **Resource Usage**: Moderate memory usage, network I/O bound + +## Architecture Assessment + +**Design Patterns** +- **Patterns Used**: + - Observer pattern (WebSocket subscription monitoring) + - Interface segregation (EthereumRpcClient, EthereumWsClient, ContractCaller) + - Cache pattern (ERC20TokenCache) + - Producer-Consumer pattern (channel-based block delivery) +- **Pattern Appropriateness**: Well-suited for real-time blockchain monitoring +- **Pattern Implementation Quality**: Good interface abstractions, proper separation of concerns + +**Separation of Concerns** +- **Layer Boundaries**: + - Network layer (RPC/WebSocket clients) + - Data processing layer (block/transaction parsing) + - Business logic layer (order extraction and validation) + - Cache layer (token metadata) +- **Coupling Analysis**: Moderate coupling between components, interfaces help reduce dependencies +- **Cohesion Assessment**: High cohesion within each component, focused responsibilities + +**Extension Points** +- **Plugin Architecture**: Order validator is injected, allowing different validation strategies +- **Configuration Options**: Comprehensive configuration via `EthBlockProviderConfig` +- **API Stability**: Uses well-defined interfaces for external dependencies + +## Component Architecture +```mermaid +graph TB + subgraph "External Dependencies" + EN[Ethereum Node] + OV[Order Validator] + LOG[Logger] + end + + subgraph "EthBlockProvider" + direction TB + START[Start Method] --> RUN[Run Loop] + RUN --> CONN[Connection Manager] + CONN --> MON[Header Monitor] + MON --> PROC[Block Processor] + PROC --> FETCH[Block Fetcher] + FETCH --> TX_PROC[Transaction Processor] + end + + subgraph "Supporting Components" + CACHE[ERC20 Token Cache] + BLOCK[Block Wrapper] + TRANS[Transaction Wrapper] + end + + subgraph "Interfaces" + RPC_I[EthereumRpcClient] + WS_I[EthereumWsClient] + CC_I[ContractCaller] + end + + subgraph "Output" + CHAN[Block Channel] + CONSUMER[Oracle Consumer] + end + + EN --> RPC_I + EN --> WS_I + EN --> CC_I + RPC_I --> CONN + WS_I --> MON + CC_I --> CACHE + OV --> TX_PROC + LOG --> START + + FETCH --> BLOCK + TX_PROC --> TRANS + TX_PROC --> CACHE + PROC --> CHAN + CHAN --> CONSUMER +``` diff --git a/cmd/rpc/oracle/eth/ETH_FLOW_2.md b/cmd/rpc/oracle/eth/ETH_FLOW_2.md new file mode 100644 index 000000000..f9dd832a2 --- /dev/null +++ b/cmd/rpc/oracle/eth/ETH_FLOW_2.md @@ -0,0 +1,575 @@ +# Code Flow Analysis Report: Ethereum Block and Transaction Processing + +## Executive Summary +- **Component Purpose**: Real-time Ethereum blockchain monitoring system that processes blocks, validates transactions, and extracts Canopy orders for oracle operations +- **Entry Points**: `EthBlockProvider.Start()` method in `block_provider.go:150` +- **Scope**: Complete flow from Ethereum WebSocket block notifications through transaction parsing to order extraction and channel delivery +- **Risk Level**: High (critical security issues including debug statements exposing sensitive data) + +## Flow Diagram +```mermaid +graph TD + H --> I[Header received via WebSocket] + I --> J[processBlocks] + J --> K[Calculate safe height] + K --> L[For each safe block height] + L --> M[fetchBlock from RPC] + M --> N[processBlockTransactions] + N --> O[For each transaction] + O --> P[parseDataForOrders] + P --> Q{Order found?} + Q -->|Yes| R[transactionSuccess check] + Q -->|No| S[Continue to next tx] + R --> T{Transaction success?} + T -->|Yes| U[Fetch ERC20 token info] + T -->|No| V[Clear order data] + U --> W[Send block through channel] + W --> X[Increment block height] + X --> Y{More blocks to process?} + Y -->|Yes| L + Y -->|No| I +``` + +## Detailed Flow Analysis + +### 1. Entry Points and Initialization +- **File**: `block_provider.go:150` +- **Function**: `Start(ctx context.Context)` +- **Purpose**: Initiates the Ethereum block monitoring system +- **Input Parameters**: Context for cancellation and timeout control +- **Initial Setup**: Launches `run()` goroutine for continuous operation with connection retry logic + +### 2. Main Processing Pipeline + +**Step 1: Connection Management** - `block_provider.go:187` +- **Location**: `connect()` method +- **Purpose**: Establishes dual connections to Ethereum node (RPC + WebSocket) +- **Input**: RPC URL and WebSocket URL from configuration +- **Processing**: + - Creates RPC client via `ethclient.DialContext()` for block data fetching + - Creates WebSocket client for real-time header subscriptions + - Closes existing connections before establishing new ones +- **Output**: Active RPC and WebSocket clients +- **Safeguards**: Connection validation, graceful cleanup, automatic retry with configurable delay + +**Step 2: Header Monitoring** - `block_provider.go:216` +- **Location**: `monitorHeaders()` method +- **Purpose**: Subscribes to new Ethereum block headers via WebSocket for real-time notifications +- **Input**: WebSocket client and context +- **Processing**: + - Creates buffered channel for headers (size 10) + - Subscribes to `SubscribeNewHead()` for new block notifications + - Processes headers as they arrive from the network +- **Output**: Stream of new block headers triggering block processing +- **Safeguards**: Nil header validation, subscription error handling, graceful unsubscribe on context cancellation + +**Step 3: Safe Block Processing** - `block_provider.go:257` +- **Location**: `processBlocks()` method +- **Purpose**: Calculates safe block height based on confirmations and processes all pending blocks +- **Input**: Current block height from header notification +- **Processing**: + - Calculates safe height: `currentHeight - safeBlockConfirmations` + - Processes all blocks from `nextHeight` to safe height sequentially + - Uses mutex lock for thread-safe height management +- **Output**: Processed blocks sent through output channel +- **Safeguards**: Negative height protection, mutex synchronization, sequential processing to maintain order + +**Step 4: Block Data Retrieval** - `block_provider.go:103` +- **Location**: `fetchBlock()` method +- **Purpose**: Retrieves complete block data from Ethereum RPC and wraps transactions +- **Input**: Block height/number as `*big.Int` +- **Processing**: + - Calls `rpcClient.BlockByNumber()` to fetch full block data + - Creates `Block` wrapper with metadata (hash, parent hash, number) + - Wraps each Ethereum transaction with `NewTransaction()` including chain ID validation +- **Output**: `Block` instance with wrapped transactions ready for processing +- **Safeguards**: Error handling for RPC failures, transaction wrapping validation, nil block checks + +**Step 5: Transaction Analysis** - `block_provider.go:300` +- **Location**: `processBlockTransactions()` method +- **Purpose**: Analyzes all transactions in block for Canopy orders and validates transaction success +- **Input**: Block containing wrapped transactions +- **Processing**: + - Iterates through all transactions in the block + - Calls `parseDataForOrders()` to extract embedded order data + - Validates transaction success via receipt checking (status == 1) + - Fetches ERC20 token metadata for valid transfers +- **Output**: Transactions with validated orders, success status, and token metadata +- **Safeguards**: Transaction receipt validation with timeout, order validation, error handling with transaction clearing + +**Step 6: Order Data Extraction** - `transaction.go:72` +- **Location**: `parseDataForOrders()` method +- **Purpose**: Extracts Canopy lock/close orders from transaction input data +- **Input**: Transaction data bytes and order validator instance +- **Processing**: + - Detects self-sent transactions (lock orders in transaction data) + - Parses ERC20 transfer data to extract extra data (close orders) + - Validates order JSON against predefined schemas + - Creates `WitnessedOrder` instances with order ID and type-specific data +- **Output**: Populated order data in transaction or nil if no valid order found +- **Safeguards**: JSON validation, order type checking, data length validation, method ID verification + +## Transaction Order Processing Flow +```mermaid +graph TD + A[Transaction Data] --> B{Self-sent transaction?} + B -->|Yes| C[Parse entire data as Lock Order JSON] + B -->|No| D[Parse as ERC20 Transfer] + D --> E{Valid ERC20 transfer?} + E -->|No| F[Skip - No order processing] + E -->|Yes| G[Extract transfer data] + G --> H[Check for extra data beyond transfer] + H --> I{Has extra data?} + I -->|No| J[Skip - No order data] + I -->|Yes| K{Self-sent with 0 amount?} + K -->|Yes| L[Parse extra data as Lock Order] + K -->|No| M[Parse extra data as Close Order] + + C --> N[Validate Lock Order JSON] + L --> N + M --> O[Validate Close Order JSON] + + N --> P{Valid JSON Schema?} + O --> Q{Valid JSON Schema?} + + P -->|Yes| R[Create WitnessedOrder with LockOrder] + P -->|No| S[Clear order data] + Q -->|Yes| T[Create WitnessedOrder with CloseOrder] + Q -->|No| S + + R --> U[Check transaction success] + T --> U + U --> V{Receipt status == 1?} + V -->|Yes| W[Keep order data] + V -->|No| X[Clear order data] +``` + +## Data Flow Mapping +```mermaid +flowchart LR + subgraph External + A[Ethereum Node] --> B[WebSocket Headers] + A --> C[RPC Block Data] + A --> D[Transaction Receipts] + A --> E[ERC20 Contract Calls] + end + + subgraph Input_Processing + B --> F[Header Validation] + F --> G[Safe Height Calculation] + G --> H[Block Height Range] + end + + subgraph Block_Processing + H --> I[Block Fetch Loop] + C --> I + I --> J[Transaction Extraction] + J --> K[Order Data Parsing] + end + + subgraph Transaction_Validation + K --> L[Order JSON Validation] + L --> M[Transaction Success Check] + D --> M + M --> N[ERC20 Token Info Fetch] + E --> N + end + + subgraph Output + N --> O[Processed Block] + O --> P[Block Channel] + P --> Q[Oracle Consumer] + end +``` + +## Connection Management Flow +```mermaid +sequenceDiagram + participant P as EthBlockProvider + participant R as RPC Client + participant W as WebSocket Client + participant E as Ethereum Node + participant C as Block Channel Consumer + + P->>P: Start() - Launch run goroutine + + loop Connection Retry Loop + P->>P: connect() + P->>R: ethclient.DialContext(rpcUrl) + R->>E: Establish RPC connection + E-->>R: Connection established + R-->>P: RPC client ready + + P->>W: ethclient.DialContext(wsUrl) + W->>E: Establish WebSocket connection + E-->>W: WebSocket connected + W-->>P: WS client ready + + P->>P: monitorHeaders() + P->>W: SubscribeNewHead(headerCh) + W->>E: Subscribe to new block headers + + loop Header Processing Loop + E->>W: New block header notification + W->>P: Header received on channel + P->>P: processBlocks(currentHeight) + + loop Safe Block Processing + P->>R: BlockByNumber(height) + R->>E: Fetch block data + E-->>R: Block data with transactions + R-->>P: Complete block received + + P->>P: processBlockTransactions() + + loop Transaction Processing + P->>P: parseDataForOrders() + P->>R: TransactionReceipt(txHash) + R->>E: Get transaction receipt + E-->>R: Receipt with status + R-->>P: Transaction success status + + opt ERC20 Token Info + P->>R: CallContract(tokenAddress, method) + R->>E: Contract call for token metadata + E-->>R: Token name/symbol/decimals + R-->>P: Token information + end + end + + P->>C: Send processed block through channel + C-->>P: Block consumed + end + end + + Note over P,E: On error or context cancellation + P->>W: Close() + P->>R: Close() + end +``` + +## Security Architecture + +### Input Validation +- **Location**: `transaction.go:47,209` +- **Mechanisms**: + - Nil transaction validation in `NewTransaction()` + - ERC20 data length validation (minimum 68 bytes for valid transfer) + - Method ID verification (`a9059cbb` for ERC20 transfers) + - Address format validation using `common.IsHexAddress()` + - JSON schema validation for order data +- **Bypass Potential**: Method ID spoofing possible but mitigated by subsequent order validation + +### Access Controls +- **Authentication**: No explicit authentication - relies on Ethereum node security and TLS +- **Authorization**: No authorization controls - processes all valid Ethereum transactions +- **Session Management**: WebSocket connection management with automatic reconnection + +### Data Protection +- **Encryption**: Relies on TLS for RPC/WebSocket connections to Ethereum nodes +- **Sensitive Data Handling**: Private keys not handled in this component (read-only operations) +- **Audit Logging**: Comprehensive logging of block processing, errors, order detection, and transaction validation + +### Rate Limiting & Resource Controls +- **Request Limits**: No explicit rate limiting - bounded by Ethereum node capabilities +- **Resource Quotas**: + - Buffered channels (size 10 for headers) + - Sequential block processing prevents parallel resource consumption +- **Timeout Protections**: + - Transaction receipt calls: 5-second timeout (`transactionReceiptTimeoutS`) + - Contract calls: 5-second timeout (`callContractTimeoutS`) + - Context-based cancellation throughout + +## Error Handling & Recovery + +### Error Scenarios +```mermaid +graph TD + A[Operation Attempted] --> B{Success?} + B -->|Yes| C[Continue Processing] + B -->|No| D[Error Classification] + + D --> E{Connection Error?} + D --> F{Validation Error?} + D --> G{Transaction Error?} + D --> H{Context Cancelled?} + + E -->|Yes| I[Close Connections] + I --> J[Wait Retry Delay] + J --> K[Attempt Reconnection] + K --> L[Connection Loop] + + F -->|Yes| M[Log Warning] + M --> N[Skip Item & Continue] + + G -->|Yes| O[Clear Order Data] + O --> P[Log Error] + P --> N + + H -->|Yes| Q[Graceful Shutdown] + Q --> R[Close All Connections] + R --> S[Exit Goroutine] + + style E fill:#ffcdd2 + style F fill:#fff3e0 + style G fill:#fff3e0 + style H fill:#e8f5e8 +``` + +### Recovery Mechanisms +- **Rollback Procedures**: No rollback - processes blocks linearly and immutably +- **Retry Logic**: + - Connection failures trigger automatic retry with configurable delay + - Failed transactions are skipped, processing continues with next transaction +- **Circuit Breakers**: Context cancellation immediately stops all operations +- **Graceful Degradation**: Individual transaction failures don't stop block processing + +## Critical Findings + +### 🔴 High Risk Issues + +1. **Information Disclosure via Debug Statements** - `transaction.go:118-119,127` + - **Description**: `fmt.Println` statements output sensitive transaction data directly to stdout + - **Impact**: Exposes transaction addresses, amounts, and order data in production logs + - **Exploitation**: Attackers monitoring logs could extract trading patterns and sensitive information + - **Mitigation**: Remove debug statements or replace with proper logging levels + +2. **Race Condition in Height Initialization** - `block_provider.go:272` + - **Description**: `nextHeight` zero check and initialization lacks atomic operation + - **Impact**: Multiple goroutines could initialize height simultaneously causing data corruption + - **Exploitation**: Could lead to duplicate block processing or height skipping + - **Mitigation**: Use atomic operations or initialize height during construction + +3. **Unbounded Channel Blocking Risk** - `block_provider.go:74,291` + - **Description**: Block output channel is unbounded and could cause goroutine blocking + - **Impact**: System deadlock if consumer stops reading from channel + - **Exploitation**: DoS via memory exhaustion or complete system freeze + - **Mitigation**: Implement buffered channel with timeout or non-blocking sends + +### 🟡 Medium Risk Issues + +1. **ERC20 Token Cache Memory Leak** - `erc20_token_cache.go:43,85` + - **Description**: Token cache grows unbounded without eviction policy + - **Impact**: Memory exhaustion over time with many unique token contracts + - **Recommendation**: Implement LRU cache with configurable size limits + +2. **Insufficient Error Context** - Multiple locations + - **Description**: Many errors lack transaction hash, block number, or other identifying context + - **Impact**: Difficult debugging and troubleshooting in production environments + - **Recommendation**: Add structured logging with relevant context data + +3. **Missing Input Validation** - `block_provider.go:65` + - **Description**: Configuration parameters not validated during initialization + - **Impact**: Runtime failures with invalid URLs, timeouts, or other config values + - **Recommendation**: Add comprehensive configuration validation at startup + +### 🟢 Low Risk Issues + +1. **Magic Number Documentation** - `transaction.go:17-19` + - **Description**: Hard-coded constants lack detailed explanatory comments + - **Recommendation**: Add comprehensive comments explaining ERC20 standard values + +2. **Address Case Sensitivity** - `transaction.go:121` + - **Description**: Uses `strings.EqualFold` but address normalization should be verified + - **Recommendation**: Ensure consistent address formatting and comparison + +## Logic Analysis + +### State Management +- **State Variables**: + - `nextHeight` (*big.Int): Next block height to process (mutex protected) + - Connection clients: RPC and WebSocket clients with lifecycle management + - Block channel: Unbounded channel for block delivery + - Token cache: Map-based cache for ERC20 token metadata +- **State Transitions**: + - Height increments sequentially with each processed block + - Connections cycle through connect/disconnect based on errors + - Cache grows monotonically (no eviction policy) +- **Consistency Guarantees**: Sequential block processing ensures ordering, mutex protects height updates +- **Concurrency Handling**: Single goroutine for block processing, mutex for height synchronization + +### Business Logic Validation +- **Business Rules**: + - Process only blocks with sufficient confirmations (safety delay) + - Validate transaction success via receipt status before processing orders + - Self-sent transactions with data indicate lock orders + - ERC20 transfers with extra data indicate close orders + - Only process transactions with valid JSON schema orders +- **Constraint Enforcement**: JSON schema validation, transaction receipt verification, ERC20 method ID checking +- **Edge Case Handling**: + - Nil header validation + - Negative safe height protection + - Empty transaction data handling + - Invalid address format detection + +### Performance Considerations +- **Bottlenecks**: + - Sequential block processing (no parallelization) + - Individual RPC calls for each transaction receipt + - Token metadata contract calls for each new token + - Synchronous block channel sends (blocking potential) +- **Scalability**: Limited by Ethereum node RPC rate limits and single-threaded processing +- **Resource Usage**: + - Moderate memory usage with unbounded token cache + - Network I/O bound operations + - CPU usage primarily for JSON parsing and validation + +## Architecture Assessment + +### Design Patterns +- **Patterns Used**: + - Observer pattern: WebSocket subscription for block header monitoring + - Interface segregation: Separate interfaces for RPC, WebSocket, and contract operations + - Cache pattern: ERC20 token metadata caching for performance + - Producer-Consumer: Channel-based block delivery to consumers + - Template method: Consistent error handling and retry patterns +- **Pattern Appropriateness**: Well-suited for real-time blockchain monitoring with proper separation of concerns +- **Pattern Implementation Quality**: Good interface abstractions, though some coupling exists between components + +### Separation of Concerns +- **Layer Boundaries**: + - Network layer: RPC/WebSocket client management + - Data processing: Block/transaction parsing and wrapping + - Business logic: Order extraction and validation + - Caching layer: Token metadata management +- **Coupling Analysis**: Moderate coupling - components share common data structures but interfaces reduce dependency +- **Cohesion Assessment**: High cohesion within each component with focused, single-responsibility design + +### Extension Points +- **Plugin Architecture**: Order validator is injected dependency allowing different validation strategies +- **Configuration Options**: Comprehensive configuration via `EthBlockProviderConfig` struct +- **API Stability**: Well-defined interfaces for external dependencies enable testing and mocking + +## Component Interaction Architecture +```mermaid +graph TB + subgraph "External Systems" + ETH[Ethereum Node] + VALIDATOR[Order Validator] + LOGGER[Logger Interface] + end + + subgraph "EthBlockProvider Core" + START[Start Method] --> RUN[Run Loop] + RUN --> CONN[Connection Manager] + CONN --> MONITOR[Header Monitor] + MONITOR --> PROCESS[Block Processor] + PROCESS --> FETCH[Block Fetcher] + FETCH --> TX_PROC[Transaction Processor] + end + + subgraph "Supporting Components" + CACHE[ERC20 Token Cache] + BLOCK[Block Wrapper] + TX[Transaction Wrapper] + end + + subgraph "Interfaces" + RPC_IF[EthereumRpcClient] + WS_IF[EthereumWsClient] + CALL_IF[ContractCaller] + end + + subgraph "Output" + CHAN[Block Channel] + CONSUMER[Oracle Consumer] + end + + ETH --> RPC_IF + ETH --> WS_IF + ETH --> CALL_IF + + RPC_IF --> CONN + RPC_IF --> FETCH + RPC_IF --> TX_PROC + + WS_IF --> MONITOR + CALL_IF --> CACHE + + VALIDATOR --> TX_PROC + LOGGER --> START + + FETCH --> BLOCK + TX_PROC --> TX + TX_PROC --> CACHE + + PROCESS --> CHAN + CHAN --> CONSUMER + + style START fill:#e1f5fe + style CHAN fill:#c8e6c9 + style CACHE fill:#fff3e0 + style TX_PROC fill:#f3e5f5 +``` + +## Recommendations + +### Security Improvements +1. **Immediate: Remove Debug Statements** - Eliminate `fmt.Println` in `transaction.go:118-119,127` to prevent information disclosure +2. **High Priority: Add Channel Buffering** - Implement bounded channel with timeout to prevent blocking +3. **Fix Race Condition** - Use atomic operations for `nextHeight` initialization or initialize during construction +4. **Implement Request Rate Limiting** - Add configurable rate limiting for RPC calls to prevent node overload + +### Logic & Code Quality +1. **Atomic Height Management** - Replace mutex-based height checking with atomic operations +2. **Enhanced Error Context** - Add transaction hash, block number, and chain ID to all error messages +3. **Configuration Validation** - Validate URLs, timeouts, and numeric parameters during initialization +4. **Consistent Address Handling** - Ensure all address comparisons use normalized lowercase format + +### Performance Optimizations +1. **Token Cache Eviction Policy** - Implement LRU cache with configurable size and TTL limits +2. **Parallel Receipt Fetching** - Fetch transaction receipts concurrently using worker pool pattern +3. **Batch Token Metadata Calls** - Group multiple token info requests together when possible +4. **Connection Pooling** - Implement connection pooling for RPC calls to reduce overhead + +### Maintainability Enhancements +1. **Structured Logging** - Replace printf-style logging with structured logging including context fields +2. **Configuration Documentation** - Add comprehensive comments for all configuration parameters +3. **Interface Documentation** - Document expected behavior and error conditions for all interfaces +4. **Timeout Configuration** - Make all timeout values configurable rather than hard-coded constants + +## Testing Recommendations + +### Unit Tests Needed +- [ ] Test negative safe height calculation and edge cases around genesis block +- [ ] Test concurrent height initialization race conditions with multiple goroutines +- [ ] Test ERC20 data parsing with malformed input, truncated data, and invalid method IDs +- [ ] Test transaction receipt timeout scenarios and RPC failure handling +- [ ] Test WebSocket subscription error recovery and reconnection logic +- [ ] Test token cache behavior with contract call failures and invalid addresses + +### Integration Tests Needed +- [ ] Test complete flow from WebSocket header to block channel delivery +- [ ] Test blockchain reorganization scenarios and safe block confirmation handling +- [ ] Test order validation with various malformed JSON inputs and schema violations +- [ ] Test memory usage patterns under sustained load with many unique tokens +- [ ] Test graceful shutdown behavior with active connections and processing + +### Security Tests Needed +- [ ] Test with manipulated ERC20 method signatures and crafted transaction data +- [ ] Test resource exhaustion scenarios with rapid block notifications +- [ ] Test malicious JSON payloads in order data for injection vulnerabilities +- [ ] Test connection flooding and rapid connect/disconnect cycles +- [ ] Test integer overflow scenarios with extremely large block numbers + +## Conclusion + +**Overall Assessment**: The Ethereum block processing system demonstrates solid architectural principles with proper separation of concerns and robust error handling. However, critical security vulnerabilities require immediate attention, particularly the debug statements that leak sensitive transaction data and potential race conditions in height management. + +**Priority Actions**: +1. **CRITICAL**: Remove debug print statements exposing sensitive data (`transaction.go:118-119,127`) +2. **HIGH**: Fix race condition in height initialization using atomic operations +3. **HIGH**: Implement channel buffering with timeout to prevent deadlocks +4. **MEDIUM**: Add comprehensive configuration validation and structured logging + +**Long-term Improvements**: +- Implement comprehensive caching strategy with eviction policies for scalability +- Add performance monitoring and metrics collection for production observability +- Enhance test coverage for edge cases, failure scenarios, and security boundaries +- Consider architectural improvements for parallel processing and improved throughput + +The system shows good defensive programming practices with extensive validation and error handling, but requires immediate security fixes and performance optimizations before production deployment. The modular design with clear interfaces provides excellent foundation for future enhancements and testing. + +--- + +*Analysis completed - All critical flows, security measures, and potential vulnerabilities documented with actionable recommendations* diff --git a/cmd/rpc/oracle/eth/README.md b/cmd/rpc/oracle/eth/README.md new file mode 100644 index 000000000..ff6812dda --- /dev/null +++ b/cmd/rpc/oracle/eth/README.md @@ -0,0 +1,63 @@ +# Ethereum Block Provider Package + +The `eth` package provides real-time Ethereum blockchain monitoring and block processing capabilities for the Canopy Network oracle system. It implements a complete block provider that fetches, validates, and processes Ethereum blocks while maintaining safe block confirmations and ERC20 token transaction handling. + +## Overview + +The `eth` package is designed to handle: +- Real-time monitoring of Ethereum blockchain through WebSocket connections +- Safe block processing with configurable confirmation requirements +- ERC20 token transaction analysis and validation +- Transaction receipt verification to prevent processing of failed transactions +- Canopy order detection and processing within Ethereum transactions +- Robust connection management with automatic reconnection capabilities + +## Core Components + +### EthBlockProvider + +The main entry point for the Ethereum block monitoring system. It manages the overall blockchain monitoring process, including: +- Establishing and maintaining RPC and WebSocket connections to Ethereum nodes +- Subscribing to new block headers for real-time updates +- Processing safe blocks with required confirmations +- Managing block height tracking and sequential processing +- Coordinating transaction analysis and order extraction + +### ERC20TokenCache + +A caching layer for ERC20 token metadata retrieval. It manages: +- Token information caching (name, symbol, decimals) +- Smart contract interaction for token metadata +- Performance optimization through intelligent caching strategies + +## Technical Details + +### Safe Block Processing + +The Ethereum block provider system uses a confirmation-based approach to ensure block finality. This is achieved by: + +- **Confirmation Counting**: Only processing blocks that have received a configurable number of confirmations + +This approach is similar to how financial institutions handle transaction confirmations - they wait for multiple confirmations before considering a transaction final to prevent issues from blockchain reorganizations. + +The system ensures data integrity by avoiding processing blocks that might be reorganized out of the main chain. + +### Real-time Header Monitoring + +The Ethereum block provider system uses WebSocket subscriptions to achieve real-time blockchain monitoring: + +1. **Subscription Establishment**: Creates a WebSocket subscription to receive new block headers as they are produced +2. **Safe Block Calculation**: Calculates which blocks are now safe to process based on the new header height +3. **Batch Processing**: Processes all newly safe blocks in sequence to maintain ordering + +This provides near-real-time blockchain monitoring while maintaining safety through confirmation requirements. + +## Configuration + +The EthBlockProvider accepts the following configuration parameters: + +- **NodeUrl**: HTTP/HTTPS RPC endpoint for the Ethereum node +- **NodeWSUrl**: WebSocket endpoint for real-time subscriptions +- **ChainID**: Ethereum chain identifier (1 for mainnet, 5 for Goerli, etc.) +- **SafeBlockConfirmations**: Number of confirmations required before processing blocks +- **RetryDelay**: Delay in seconds between connection retry attempts diff --git a/cmd/rpc/oracle/eth/block.go b/cmd/rpc/oracle/eth/block.go new file mode 100644 index 000000000..010cbaa20 --- /dev/null +++ b/cmd/rpc/oracle/eth/block.go @@ -0,0 +1,59 @@ +package eth + +import ( + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +var _ types.BlockI = &Block{} // Ensures *Block implements BlockI + +// Block represents an ethereum block that implements BlockI interface +type Block struct { + hash string // block hash as hex string + parentHash string // parent hash + number uint64 // block number + transactions []*Transaction // array of transactions in this block +} + +// NewBlock creates a new Block from an ethereum block +func NewBlock(ethBlock *ethtypes.Block) (*Block, error) { + // validate input block is not nil + if ethBlock == nil { + return nil, lib.ErrNilBlock() + } + // create new block instance + block := &Block{ + hash: ethBlock.Hash().Hex(), // convert block hash to hex string + parentHash: ethBlock.ParentHash().Hex(), // convert parent block hash to hex string + number: ethBlock.NumberU64(), // get block number as uint64 + transactions: make([]*Transaction, 0), // initialize empty transaction slice + } + return block, nil // return successfully created block +} + +// Hash returns the block hash as a string +func (b *Block) Hash() string { + return b.hash // return the stored block hash +} + +// ParentHash returns the parent block hash as a string +func (b *Block) ParentHash() string { + return b.parentHash // return the parent hash +} + +// Number returns the block number +func (b *Block) Number() uint64 { + return b.number // return the stored block number +} + +// Transactions returns all transactions in this block +func (b *Block) Transactions() []types.TransactionI { + // create slice to hold transaction interfaces + txs := make([]types.TransactionI, len(b.transactions)) + // convert each transaction to interface type + for i, tx := range b.transactions { + txs[i] = tx // assign transaction to interface slice + } + return txs // return slice of transaction interfaces +} diff --git a/cmd/rpc/oracle/eth/block_provider.go b/cmd/rpc/oracle/eth/block_provider.go new file mode 100644 index 000000000..a7b8f8870 --- /dev/null +++ b/cmd/rpc/oracle/eth/block_provider.go @@ -0,0 +1,596 @@ +package eth + +import ( + "context" + "errors" + "fmt" + "math/big" + "sync" + "time" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" +) + +const ( + // header channel buffer size + headerChannelBufferSize = 1000 + // timeout for the transaction receipt call + transactionReceiptTimeoutS = 5 + // ethereum transaction receipt success status value + TransactionStatusSuccess = 1 + // how many times to try to process a transaction (erc20 token fetch + transaction receipt) + maxTransactionProcessAttempts = 3 + // how long to allow processBlock to run + processBlockTimeLimitS = 12 +) + +// Ensures *EthBlockProvider implements BlockProvider interface +var _ types.BlockProvider = &EthBlockProvider{} + +/* This file contains the high level functionality of the continued agreement on the blocks of the chain */ + +// EthereumRpcClient interface for ethereum rpc operations +type EthereumRpcClient interface { + BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) + TransactionReceipt(ctx context.Context, txHash common.Hash) (*ethtypes.Receipt, error) + Close() +} + +// EthereumWsClient interface for ethereum websocket operations +type EthereumWsClient interface { + SubscribeNewHead(context.Context, chan<- *ethtypes.Header) (ethereum.Subscription, error) + Close() +} + +type OrderValidator interface { + ValidateOrderJsonBytes(jsonBytes []byte, orderType types.OrderType) error +} + +// EthBlockProvider provides ethereum blocks through a channel +type EthBlockProvider struct { + config lib.EthBlockProviderConfig // provider configuration + blockChan chan types.BlockI // channel to send blocks + erc20TokenCache *ERC20TokenCache // erc20 token info cache + logger lib.LoggerI // logger for debug and error messages + rpcClient EthereumRpcClient // rpc client for fetching blocks + wsClient EthereumWsClient // websocket client for monitoring headers + orderValidator OrderValidator // order validator + nextHeight *big.Int // next block height to be sent through channel + chainId uint64 // ethereum chain id + synced bool // flag indicating synced to top + heightMu *sync.Mutex // mutex around next height + metrics *lib.Metrics // metrics for telemetry +} + +// NewEthBlockProvider creates a new EthBlockProvider instance +func NewEthBlockProvider(config lib.EthBlockProviderConfig, orderValidator OrderValidator, logger lib.LoggerI, metrics *lib.Metrics) *EthBlockProvider { + // create an ethereum client for the token cache + ethClient, ethErr := ethclient.Dial(config.NodeUrl) + if ethErr != nil { + logger.Fatal("[ETH-CONN] " + ethErr.Error()) + } + // create a new erc20 token cache + tokenCache := NewERC20TokenCache(ethClient, metrics) + // create the block output channel, this is unbuffered so the provider + // halts processing until the receiver is ready to process more blocks + ch := make(chan types.BlockI) + // create new provider instance + p := &EthBlockProvider{ + config: config, + blockChan: ch, + erc20TokenCache: tokenCache, + logger: logger, + orderValidator: orderValidator, + nextHeight: big.NewInt(0), + chainId: config.EVMChainId, + synced: false, + heightMu: &sync.Mutex{}, + metrics: metrics, + } + // log provider creation + p.logger.Infof("[ETH-CONN] created block provider with rpc: %s, ws: %s, chain id: %d", p.config.NodeUrl, p.config.NodeWSUrl, p.chainId) + return p +} + +// fetchBlock fetches the block at the specified height and wraps each transaction +func (p *EthBlockProvider) fetchBlock(ctx context.Context, height *big.Int) (*Block, error) { + // fetch block from ethereum client + ethBlock, err := p.rpcClient.BlockByNumber(ctx, height) + if err != nil { + // log error and return + p.logger.Errorf("[ETH-RPC] BlockByNumber failed for height %d: %v", height, err) + return nil, err + } + // create new block from ethereum block + block, err := NewBlock(ethBlock) + if err != nil { + // log error and return + p.logger.Errorf("[ETH-BLOCK] failed to wrap block at height %d: %v", height, err) + return nil, err + } + // iterate through ethereum transactions, creating a transaction wrappers + for _, ethTx := range ethBlock.Transactions() { + // create new Transaction from ethereum transaction + tx, err := NewTransaction(ethTx, p.chainId) + if err != nil { + p.logger.Errorf("[ETH-TX] failed to create transaction: %s", err) + continue + // return nil, err // return error if transaction creation fails + } + // append transaction to block's transaction list + block.transactions = append(block.transactions, tx) + } + // log successful block creation + // p.logger.Debugf("successfully created block at height: %d with %d transactions", height, len(block.transactions)) + return block, nil +} + +// BlockCh returns the channel through which new blocks will be sent +func (p *EthBlockProvider) BlockCh() chan types.BlockI { + // return the block channel + return p.blockChan +} + +// IsSynced returns whether the block provider has synced to the top of the chain +func (p *EthBlockProvider) IsSynced() bool { + // lock height mutex to safely read synced state + p.heightMu.Lock() + defer p.heightMu.Unlock() + // return current sync status + return p.synced +} + +func (p *EthBlockProvider) closeConnections() { + if p.rpcClient != nil { + p.logger.Debug("[ETH-CONN] closing RPC connection") + p.rpcClient.Close() + } + if p.wsClient != nil { + p.logger.Debug("[ETH-WS] closing WebSocket connection") + p.wsClient.Close() + } +} + +// Start begins the block provider operation +func (p *EthBlockProvider) Start(ctx context.Context, height uint64) { + p.nextHeight = new(big.Int).SetUint64(height) + p.logger.Info("[ETH-CONN] starting block provider") + go p.run(ctx) +} + +// run handles the main loop for block provider operations +func (p *EthBlockProvider) run(ctx context.Context) { + for { + select { + case <-ctx.Done(): + p.logger.Info("[ETH-CONN] shutting down block provider") + p.closeConnections() + return + default: + } + // try to connect to ethereum node + err := p.connect(ctx) + if err != nil { + p.logger.Errorf("[ETH-CONN] error connecting to node: %s", err.Error()) + p.logger.Infof("[ETH-CONN] retrying connection in %d seconds", p.config.RetryDelay) + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(p.config.RetryDelay) * time.Second): + continue + } + } + // fetch latest block + block, err := p.rpcClient.BlockByNumber(ctx, nil) + if err != nil { + p.logger.Errorf("[ETH-RPC] error fetching latest block: %s", err.Error()) + continue + } + // a next height of zero indicates no height was specified by the consumer + if lib.BigIntIsZero(p.nextHeight) { + // default to startup block depth + p.nextHeight = lib.BigIntSub(block.Number(), lib.BigInt(p.config.StartupBlockDepth)) + // ensure next height is not negative + if p.nextHeight.Sign() < 0 { + p.nextHeight.SetInt64(0) + } + p.logger.Warnf("[ETH-SYNC] next height was 0 - initialized to %s", p.nextHeight.String()) + } + // begin monitoring new block headers + err = p.monitorHeaders(ctx) + if err != nil { + p.logger.Errorf("[ETH-WS] subscription error: %v", err) + } + // close any remaining connections + p.closeConnections() + } +} + +// connect creates ethereum rpc and websocket connections +func (p *EthBlockProvider) connect(ctx context.Context) error { + // close any existing connections + p.closeConnections() + // set connection state to connecting + p.metrics.SetEthConnectionState(1) // connecting + // attempt to connect to rpc client + p.metrics.IncrementEthRPCConnectionAttempt() + rpcClient, err := ethclient.DialContext(ctx, p.config.NodeUrl) + if err != nil { + // log error and retry + p.logger.Errorf("[ETH-CONN] failed to connect to rpc client: %v, retrying in %v", err, time.Duration(p.config.RetryDelay)*time.Second) + p.metrics.IncrementEthRPCConnectionError("dial_error") + p.metrics.SetEthConnectionState(0) // disconnected + return err + } + // set rpc client + p.rpcClient = rpcClient + // log successful rpc connection + p.logger.Infof("[ETH-CONN] connected to RPC at %s", p.config.NodeUrl) + p.metrics.SetEthConnectionState(2) // rpc_connected + // attempt to connect to websocket client + p.metrics.IncrementEthWSConnectionAttempt() + wsClient, err := ethclient.DialContext(ctx, p.config.NodeWSUrl) + if err != nil { + p.rpcClient.Close() + // log error and retry + p.logger.Errorf("[ETH-WS] failed to connect: %v, retrying in %v", err, time.Duration(p.config.RetryDelay)*time.Second) + p.metrics.IncrementEthRPCConnectionError("ws_dial_error") + p.metrics.SetEthConnectionState(0) // disconnected + return err + } + // set websocket client + p.wsClient = wsClient + // log successful websocket connection + p.logger.Infof("[ETH-WS] connected at %s", p.config.NodeWSUrl) + p.metrics.SetEthConnectionState(3) // fully_connected + return nil +} + +// monitorHeaders establishes a websocket subscription to monitor new block headers, +// prcessing them as they arrive from the Ethereum network. +// a received header acts as a notification that a new block has been created on ethereum, +// and our ethereum block provider should execute a process loop +func (p *EthBlockProvider) monitorHeaders(ctx context.Context) error { + if p.wsClient == nil { + p.logger.Error("[ETH-WS] websocket client not initialized") + p.metrics.IncrementEthWSSubscriptionError() + return fmt.Errorf("websocket client not initialized") + } + // create header channel + headerCh := make(chan *ethtypes.Header, headerChannelBufferSize) + // subscribe to new headers + sub, err := p.wsClient.SubscribeNewHead(ctx, headerCh) + if err != nil { + // log error and return + p.logger.Errorf("[ETH-WS] failed to subscribe to headers: %v", err) + p.metrics.IncrementEthWSSubscriptionError() + return err + } + // log successful subscription + p.logger.Info("[ETH-WS] subscribed to new headers") + // set initial sync status + p.metrics.SetEthSyncStatus(1) // syncing + // create status ticker for periodic updates + statusTicker := time.NewTicker(30 * time.Second) + defer statusTicker.Stop() + // process headers in loop + for { + select { + case <-statusTicker.C: + // periodic status update + p.logger.Infof("[ETH-SYNC] status: nextHeight=%s, synced=%v", p.nextHeight.String(), p.synced) + case <-ctx.Done(): + p.logger.Info("[ETH-SYNC] context cancelled") + sub.Unsubscribe() + return ctx.Err() + case header := <-headerCh: + if header == nil || header.Number == nil { + p.logger.Warn("[ETH-BLOCK] received nil header, skipping") + continue + } + // update block height metrics + chainHead := header.Number.Uint64() + p.metrics.SetEthChainHeadHeight(chainHead) + if chainHead >= p.nextHeight.Uint64() { + p.metrics.SetEthBlockHeightLag(chainHead - p.nextHeight.Uint64()) + } + // ensure we haven't gotten ahead of the current chain height + if p.nextHeight.Cmp(header.Number) > 0 { + p.logger.Errorf("[ETH-SYNC] next height %d higher than current chain height %d", p.nextHeight, header.Number) + p.logger.Error("[ETH-SYNC] remove state file and restart node if expected") + // record reorg detection + p.metrics.IncrementEthReorgDetected() + // unsubscribe from new headers + sub.Unsubscribe() + // stop listening to new headers and return an error + return ErrSourceHeight + } + // not synced to top + if !p.synced { + // check for source chain sync + if p.nextHeight.Cmp(header.Number) == 0 { + // we've caught up to the latest block, mark as synced + p.synced = true + p.logger.Infof("[ETH-SYNC] synced at height %s", p.nextHeight.String()) + p.metrics.SetEthSyncStatus(2) // synced + } + } + // process all blocks up to current height + p.nextHeight = p.processBlocks(ctx, p.nextHeight, header.Number) + // update last processed height (nextHeight - 1 is the last block we sent) + if p.nextHeight.Uint64() > 0 { + lastProcessed := p.nextHeight.Uint64() - 1 + p.metrics.SetEthLastProcessedHeight(lastProcessed) + p.metrics.SetEthSafeHeight(lastProcessed) + } + case err := <-sub.Err(): + // log subscription error + p.logger.Errorf("[ETH-WS] subscription error received: %v", err) + p.metrics.IncrementEthWSSubscriptionError() + // unsubscribe from new headers + sub.Unsubscribe() + // return the error + return err + } + } +} + +// processBlocks fetches and processes ethereum blocks in the specified range +func (p *EthBlockProvider) processBlocks(ctx context.Context, start, end *big.Int) *big.Int { + // Create a context with ethereum block time timeout + // this is so this method does not block new eth neaders + timeoutCtx, cancel := context.WithTimeout(ctx, processBlockTimeLimitS*time.Second) + defer cancel() + // track next height to be processed + next := new(big.Int).Set(start) + p.logger.Debugf("[ETH-BLOCK] processing blocks from %d to %d", start, end) + // initialize metrics counters + var blocksProcessed, transactionsProcessed, retries int + // track batch size for metrics + batchStart := new(big.Int).Set(start) + // process blocks from next height to current height + for next.Cmp(end) <= 0 { + // Check if context has been cancelled or timed out + select { + case <-timeoutCtx.Done(): + p.logger.Errorf("[ETH-BLOCK] max run time hit, returning") + p.metrics.IncrementEthBlockProcessingTimeout() + // record batch size before returning + batchSize := int(next.Int64() - batchStart.Int64()) + if batchSize > 0 { + p.metrics.RecordEthProcessBlocksBatchSize(batchSize) + } + return next + default: + } + // get block from ethereum node and create our Block wrapper + fetchStart := time.Now() + block, err := p.fetchBlock(timeoutCtx, next) + if err != nil { + // log error and return without continuing + p.logger.Errorf("[ETH-BLOCK] failed to get block at height %d: %v", next, err) + p.metrics.IncrementEthBlockFetchError("fetch_error") + // update metrics before returning + p.metrics.UpdateEthBlockProviderMetrics(0, 0, 0, 0, 0, 1, blocksProcessed, transactionsProcessed, retries) + // record batch size before returning + batchSize := int(next.Int64() - batchStart.Int64()) + if batchSize > 0 { + p.metrics.RecordEthProcessBlocksBatchSize(batchSize) + } + // return same height so the provider tries this block again + return next + } + fetchTime := time.Since(fetchStart) + // track transactions encountered + p.metrics.IncrementEthTransactionsTotal(len(block.transactions)) + // process each transaction, populating orders and transfer data + txProcessStart := time.Now() + if err := p.processBlockTransactions(timeoutCtx, block); err != nil { + p.logger.Errorf("[ETH-TX] failed to process block transactions: %v", err) + // update metrics before returning + p.metrics.UpdateEthBlockProviderMetrics(fetchTime, 0, 0, 0, 0, 1, blocksProcessed, transactionsProcessed, retries) + // record batch size before returning + batchSize := int(next.Int64() - batchStart.Int64()) + if batchSize > 0 { + p.metrics.RecordEthProcessBlocksBatchSize(batchSize) + } + return next + } + txProcessTime := time.Since(txProcessStart) + // send block through channel + p.blockChan <- block + // log successful block processing + // p.logger.Infof("eth block provider sent safe block at height %d through channel", next) + // update counters + blocksProcessed++ + transactionsProcessed += len(block.transactions) + // update metrics with current block data + p.metrics.UpdateEthBlockProviderMetrics(fetchTime, txProcessTime, 0, 0, 0, 0, 1, len(block.transactions), 0) + // increment height for next iteration + next.Add(next, big.NewInt(1)) + } + // record batch size on successful completion + batchSize := int(next.Int64() - batchStart.Int64()) + if batchSize > 0 { + p.metrics.RecordEthProcessBlocksBatchSize(batchSize) + } + return next +} + +// processBlockTransactions validates and processes block transactions +func (p *EthBlockProvider) processBlockTransactions(ctx context.Context, block *Block) error { + // track retry count for metrics + var retryCount int + // perform validation on transactions that had canopy orders + for _, tx := range block.transactions { + var err error + // retry logic for processing transaction + for attempt := range maxTransactionProcessAttempts { + // process transaction - look for orders + err = p.processTransaction(ctx, block, tx) + // success indicates no order found, or order successfully found and validated + if err == nil { + break + } + // error condition - clear any order data that may have been set + tx.clearOrder() + // these errors can be temporary network errors, all others should not be retried + if !errors.Is(err, ErrTransactionReceipt) && !errors.Is(err, ErrTokenInfo) { + p.logger.Errorf("[ETH-TX] error processing tx %s with order in block %s: %v", tx.Hash(), block.Hash(), err) + // non-retryable error, break immediately + break + } + p.logger.Errorf("[ETH-TX] error processing tx %s in block %s: %v - attempt %d", tx.Hash(), block.Hash(), err, attempt+1) + // count retry for metrics and track by attempt number + if attempt > 0 { + retryCount++ + p.metrics.IncrementEthTransactionRetryByAttempt(attempt + 1) + } + // implement exponential backoff for failed attempts + if attempt < maxTransactionProcessAttempts-1 { + backoffDuration := time.Duration(1< 0 { + p.metrics.UpdateEthBlockProviderMetrics(0, 0, 0, 0, 0, 0, 0, 0, retryCount) + } + return nil +} + +// processTransaction processes a single transaction +// the return value of nil means there was no canopy order in this transaction and processing need not be retried +// a return of an error means there was a canopy order and what could be a temporary error. A retry should be attempted +func (p *EthBlockProvider) processTransaction(ctx context.Context, block *Block, tx *Transaction) error { + // examine transaction data for canopy orders + err := tx.parseDataForOrders(p.orderValidator) + // check for error + if err != nil { + // p.logger.Warnf("Error parsing data for orders: %s", err) + p.logAsciiBytes(tx.tx.Data()) + // a transaction having non-JSON data is an expected conditions + return nil + } + // check if parseDataForOrders found an order + if tx.order == nil { + // dev output + // no orders found, no processing required + return nil + } + // track order detection metrics + if tx.order.LockOrder != nil { + p.metrics.IncrementEthLockOrderDetected() + } + if tx.order.CloseOrder != nil { + p.metrics.IncrementEthCloseOrderDetected() + } + // set the ethereum height this order was witnessed + tx.order.WitnessedHeight = block.Number() + // a valid canopy order was found, check transaction success + success, err := p.transactionSuccess(ctx, tx) + // check for error + if err != nil { + p.logger.Errorf("[ETH-RPC] error fetching transaction receipt: %s", err.Error()) + // there was an error fetching the transaction receipt + return err + } + if !success { + // process next transaction + return nil + } + // test if this was an erc20 transfer + if !tx.isERC20 { + // no more processing required + return nil + } + // track ERC20 transfer detection + p.metrics.IncrementEthERC20TransferDetected() + // fetch erc20 token info (name, symbol, decimals) + tokenInfo, err := p.erc20TokenCache.TokenInfo(ctx, tx.To()) + if err != nil { + p.logger.Errorf("[ETH-ERC20] failed to get token info for %s: %v", tx.To(), err) + return err + } + p.logger.Infof("[ETH-ERC20] obtained token info for %s: %s", tx.To(), tokenInfo) + // store the erc20 token info + tx.tokenInfo = tokenInfo + return nil +} + +// transactionSuccess fetches the transaction receipt and determines transaction success +// This prevents scenarios where failed ERC20 transactions are processed as successful transfers +func (p *EthBlockProvider) transactionSuccess(ctx context.Context, tx *Transaction) (bool, error) { + txHash := tx.tx.Hash() + txHashStr := txHash.String() + + // create a fresh context with timeout for the RPC call + rpcCtx, cancel := context.WithTimeout(ctx, transactionReceiptTimeoutS*time.Second) + // get transaction receipt with timing + receiptStart := time.Now() + receipt, err := p.rpcClient.TransactionReceipt(rpcCtx, txHash) + cancel() + receiptTime := time.Since(receiptStart) + // check for error + if err != nil { + p.logger.Warnf("[ETH-RPC] failed to get receipt for tx %s: %v", txHashStr, err) + // update receipt fetch metrics on error + p.metrics.UpdateEthBlockProviderMetrics(0, 0, receiptTime, 0, 0, 1, 0, 0, 0) + p.metrics.IncrementEthReceiptFetchError() + p.metrics.IncrementEthTransactionSuccessStatus("unknown") + return false, ErrTransactionReceipt + } + // check for success using transaction receipt status + // This approach works for ALL ERC20 tokens including non-standard ones like USDT: + // - Receipt status reflects actual on-chain execution success/failure + // - Independent of function return values (USDT returns void, standard returns bool) + // - Also catches failures from paused contracts, blacklisted addresses, etc. + // By validating via receipt rather than parsing return values, we sidestep all + // token-specific quirks and get a universal success indicator. + if receipt.Status == TransactionStatusSuccess { + // update receipt fetch metrics on success + p.metrics.UpdateEthBlockProviderMetrics(0, 0, receiptTime, 0, 0, 0, 0, 0, 0) + p.metrics.IncrementEthTransactionSuccessStatus("success") + return true, nil + } + p.logger.Errorf("[ETH-TX] tx %s ERC20 transfer failed on-chain, ignoring", txHashStr) + p.metrics.IncrementEthTransactionSuccessStatus("failed") + // return unsuccessful transaction + return false, nil +} + +// logAsciiBytes logs the first 100 bytes of data only if all bytes are printable ASCII +func (p *EthBlockProvider) logAsciiBytes(data []byte) { + if len(data) == 0 { + return + } + // determine how many bytes to check (up to 100) + limit := len(data) + if limit > 100 { + limit = 100 + } + // check if ALL bytes are printable ASCII + for i := 0; i < limit; i++ { + if data[i] < 32 || data[i] > 126 { + // non-ASCII byte found, don't log + return + } + } + // all bytes are printable ASCII, log them + p.logger.Debugf("[ETH-TX] first 100 bytes ASCII: %s", string(data[:limit])) +} diff --git a/cmd/rpc/oracle/eth/block_provider_test.go b/cmd/rpc/oracle/eth/block_provider_test.go new file mode 100644 index 000000000..510c01c9c --- /dev/null +++ b/cmd/rpc/oracle/eth/block_provider_test.go @@ -0,0 +1,537 @@ +package eth + +import ( + "context" + "log" + "math/big" + "sync" + "testing" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + ethtrie "github.com/ethereum/go-ethereum/trie" + "github.com/google/go-cmp/cmp" +) + +type mockEthClient struct { + blocks map[uint64]*ethtypes.Block + receipts map[common.Hash]*ethtypes.Receipt + contractErr error +} + +func (m *mockEthClient) BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) { + height := number.Uint64() + if block, exists := m.blocks[height]; exists { + return block, nil + } + return nil, ethereum.NotFound +} + +func (m *mockEthClient) CallContract(ctx context.Context, msg ethereum.CallMsg, height *big.Int) ([]byte, error) { + return nil, m.contractErr +} + +func (m *mockEthClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*ethtypes.Receipt, error) { + if block, exists := m.receipts[txHash]; exists { + return block, nil + } + return nil, ethereum.NotFound +} + +func (m *mockEthClient) Close() {} + +// mockOrderValidator is a simple mock implementation of OrderValidator that always returns true +type mockOrderValidator struct{} + +// NewMockOrderValidator creates a new mock OrderValidator instance +func NewMockOrderValidator() *mockOrderValidator { + return &mockOrderValidator{} +} + +// ValidateOrderJsonBytes always returns nil (success) for any input +func (m *mockOrderValidator) ValidateOrderJsonBytes(jsonBytes []byte, orderType types.OrderType) error { + return nil +} + +func createTransaction(toAddress common.Address, data []byte) *ethtypes.Transaction { + tx := ethtypes.NewTransaction( + 0, + toAddress, + big.NewInt(0), + 21000, + big.NewInt(1000000000), + data, + ) + privateKey, err := crypto.GenerateKey() + if err != nil { + log.Fatal(err) + } + // Create signer for the specific chain + signer := ethtypes.NewEIP155Signer(big.NewInt(0)) + // Sign the transaction + signedTx, _ := ethtypes.SignTx(tx, signer, privateKey) + return signedTx +} + +func createEthereumBlock(height uint64, txs []*ethtypes.Transaction) *ethtypes.Block { + header := ðtypes.Header{ + Number: big.NewInt(int64(height)), + GasLimit: 8000000, + GasUsed: 21000, + Time: 1234567890, + Difficulty: big.NewInt(1000), + } + body := ðtypes.Body{ + Transactions: txs, + } + triedb := ethtrie.NewEmpty(nil) + return ethtypes.NewBlock(header, body, nil, triedb) +} + +func setupTokenCache(address common.Address, token types.TokenInfo) *ERC20TokenCache { + cache := NewERC20TokenCache(&mockEthClient{}, nil) + cache.cache.Put(address.Hex(), token) + return cache +} + +func TestEthBlockProvider_fetchBlock(t *testing.T) { + recipientAddress := "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" + + tests := []struct { + name string + height uint64 + setupBlocks func() map[uint64]*ethtypes.Block + expectedError bool + expectedTxs int + }{ + { + name: "block with no transactions", + height: 1, + setupBlocks: func() map[uint64]*ethtypes.Block { + blocks := make(map[uint64]*ethtypes.Block) + blocks[1] = createEthereumBlock(1, []*ethtypes.Transaction{}) + return blocks + }, + expectedError: false, + expectedTxs: 0, + }, + { + name: "block with regular transaction", + height: 2, + setupBlocks: func() map[uint64]*ethtypes.Block { + blocks := make(map[uint64]*ethtypes.Block) + tx := createTransaction(common.HexToAddress(recipientAddress), []byte("regular data")) + blocks[2] = createEthereumBlock(2, []*ethtypes.Transaction{tx}) + return blocks + }, + expectedError: false, + expectedTxs: 1, + }, + { + name: "block with multiple transactions", + height: 3, + setupBlocks: func() map[uint64]*ethtypes.Block { + blocks := make(map[uint64]*ethtypes.Block) + tx1 := createTransaction(common.HexToAddress(recipientAddress), []byte("data1")) + tx2 := createTransaction(common.HexToAddress(recipientAddress), []byte("data2")) + blocks[3] = createEthereumBlock(3, []*ethtypes.Transaction{tx1, tx2}) + return blocks + }, + expectedError: false, + expectedTxs: 2, + }, + { + name: "block not found", + height: 999, + setupBlocks: func() map[uint64]*ethtypes.Block { + return make(map[uint64]*ethtypes.Block) + }, + expectedError: true, + expectedTxs: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &mockEthClient{ + blocks: tt.setupBlocks(), + } + logger := lib.NewDefaultLogger() + + provider := &EthBlockProvider{ + rpcClient: mockClient, + logger: logger, + chainId: 1, + config: lib.EthBlockProviderConfig{}, + heightMu: &sync.Mutex{}, + } + + block, err := provider.fetchBlock(context.Background(), new(big.Int).SetUint64(tt.height)) + + if tt.expectedError { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if block == nil { + t.Errorf("expected block but got nil") + return + } + + if block.Number() != tt.height { + t.Errorf("expected block number %d, got %d", tt.height, block.Number()) + } + + transactions := block.Transactions() + if len(transactions) != tt.expectedTxs { + t.Errorf("expected %d transactions, got %d", tt.expectedTxs, len(transactions)) + } + }) + } +} + +func TestEthBlockProvider_processBlocks(t *testing.T) { + contractAddress := common.HexToAddress("0x1234567890123456789012345678901234567890") + testToken := types.TokenInfo{ + Name: "TestToken", + Symbol: "TEST", + Decimals: 18, + } + + tests := []struct { + name string + startHeight *big.Int + endHeight *big.Int + setupBlocks func() map[uint64]*ethtypes.Block + expectedBlocks []uint64 // expected block heights sent to channel + expectedNext *big.Int // expected return value from processBlocks + }{ + { + name: "no blocks to process - start height higher than end height", + startHeight: big.NewInt(96), + endHeight: big.NewInt(95), + setupBlocks: func() map[uint64]*ethtypes.Block { + return make(map[uint64]*ethtypes.Block) + }, + expectedNext: big.NewInt(96), + }, + { + name: "process single block", + startHeight: big.NewInt(94), + endHeight: big.NewInt(94), + setupBlocks: func() map[uint64]*ethtypes.Block { + blocks := make(map[uint64]*ethtypes.Block) + blocks[94] = createEthereumBlock(94, []*ethtypes.Transaction{}) + return blocks + }, + expectedBlocks: []uint64{94}, + expectedNext: big.NewInt(95), + }, + { + name: "process multiple blocks", + startHeight: big.NewInt(102), + endHeight: big.NewInt(105), + setupBlocks: func() map[uint64]*ethtypes.Block { + blocks := make(map[uint64]*ethtypes.Block) + for i := uint64(102); i <= 105; i++ { + blocks[i] = createEthereumBlock(i, []*ethtypes.Transaction{}) + } + return blocks + }, + expectedBlocks: []uint64{102, 103, 104, 105}, + expectedNext: big.NewInt(106), + }, + { + name: "start height higher than end height - no processing", + startHeight: big.NewInt(10), + endHeight: big.NewInt(8), + setupBlocks: func() map[uint64]*ethtypes.Block { + return make(map[uint64]*ethtypes.Block) + }, + expectedNext: big.NewInt(10), + }, + { + name: "exact range boundary", + startHeight: big.NewInt(5), + endHeight: big.NewInt(5), + setupBlocks: func() map[uint64]*ethtypes.Block { + blocks := make(map[uint64]*ethtypes.Block) + blocks[5] = createEthereumBlock(5, []*ethtypes.Transaction{}) + return blocks + }, + expectedBlocks: []uint64{5}, + expectedNext: big.NewInt(6), + }, + { + name: "process blocks with transactions", + startHeight: big.NewInt(18), + endHeight: big.NewInt(20), + setupBlocks: func() map[uint64]*ethtypes.Block { + blocks := make(map[uint64]*ethtypes.Block) + // Create blocks with some transactions + transferData := createERC20TransferData("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", big.NewInt(1000000000000000000), []byte{}) + tx := createTransaction(contractAddress, transferData) + blocks[18] = createEthereumBlock(18, []*ethtypes.Transaction{tx}) + blocks[19] = createEthereumBlock(19, []*ethtypes.Transaction{}) + blocks[20] = createEthereumBlock(20, []*ethtypes.Transaction{tx}) + return blocks + }, + expectedBlocks: []uint64{18, 19, 20}, + expectedNext: big.NewInt(21), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &mockEthClient{ + blocks: tt.setupBlocks(), + } + logger := lib.NewDefaultLogger() + tokenCache := setupTokenCache(contractAddress, testToken) + // Create a buffered channel to capture sent blocks + blockChan := make(chan types.BlockI, 10) + provider := &EthBlockProvider{ + rpcClient: mockClient, + erc20TokenCache: tokenCache, + orderValidator: &mockOrderValidator{}, + logger: logger, + blockChan: blockChan, + chainId: 1, + config: lib.EthBlockProviderConfig{}, + heightMu: &sync.Mutex{}, + } + // Call processBlocks with start and end parameters + resultNext := provider.processBlocks(context.Background(), tt.startHeight, tt.endHeight) + // Collect all blocks sent to channel + var receivedBlocks []types.BlockI + close(blockChan) // Close channel to stop range loop + for block := range blockChan { + receivedBlocks = append(receivedBlocks, block) + } + // Extract block numbers for comparison + var receivedBlockNumbers []uint64 + for _, block := range receivedBlocks { + receivedBlockNumbers = append(receivedBlockNumbers, block.Number()) + } + + // Verify expected blocks were sent + if diff := cmp.Diff(tt.expectedBlocks, receivedBlockNumbers); diff != "" { + t.Errorf("sent blocks mismatch (-want +got):\n%s", diff) + } + + // Verify return value is correct + if resultNext.Cmp(tt.expectedNext) != 0 { + t.Errorf("expected return value %s, got %s", tt.expectedNext.String(), resultNext.String()) + } + }) + } +} + +// func TestEthBlockProvider_checkTransfer(t *testing.T) { +// // Test constants +// contract1Address := common.HexToAddress("0x8884567890123456789012345678901234567888") +// contract2Address := common.HexToAddress("0x9994567890123456789012345678901234567999") +// unknownContract := common.HexToAddress("0x0006543210987654321098765432109876543000") +// recipient := "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" +// testToken := types.TokenInfo{ +// Name: "TestToken", +// Symbol: "TEST", +// Decimals: 18, +// } + +// // Create close order data +// orderId := "1010101010101010101010101010101010101010" +// orderIdBytes, _ := lib.StringToBytes(orderId) +// order := lib.CloseOrder{ +// OrderId: orderIdBytes, +// ChainId: 0, +// CloseOrder: true, +// } +// closeOrderBytes, _ := order.MarshalJSON() + +// // Set up token cache +// tokenCache := NewERC20TokenCache(&mockEthClient{contractErr: errors.New("error")}) +// tokenCache.cache.Put(contract1Address.Hex(), testToken) + +// tests := []struct { +// name string +// txConfig transactionConfig +// receiptStatus *uint64 // nil means no receipt +// expectedTokenTransfer types.TokenTransfer +// expectedExtraData []byte +// }{ +// { +// name: "valid erc20 transfer with no extra data, no token transfer", +// txConfig: transactionConfig{ +// contractAddress: contract1Address, +// isERC20: true, +// recipient: recipient, +// amount: big.NewInt(1000000000000000000), +// extraData: nil, +// }, +// receiptStatus: nil, +// expectedTokenTransfer: types.TokenTransfer{}, +// expectedExtraData: nil, +// }, +// { +// name: "valid erc20 transfer with close order and successful receipt", +// txConfig: transactionConfig{ +// contractAddress: contract1Address, +// isERC20: true, +// recipient: recipient, +// amount: big.NewInt(500000000000000000), +// extraData: closeOrderBytes, +// }, +// receiptStatus: uint64Ptr(1), +// expectedTokenTransfer: types.TokenTransfer{ +// Blockchain: "ethereum", +// TokenInfo: testToken, +// RecipientAddress: recipient, +// TokenBaseAmount: big.NewInt(500000000000000000), +// ContractAddress: contract1Address.Hex(), +// }, +// expectedExtraData: closeOrderBytes, +// }, +// { +// name: "erc20 transfer with close order but failed transaction receipt", +// txConfig: transactionConfig{ +// contractAddress: contract2Address, +// isERC20: true, +// recipient: recipient, +// amount: big.NewInt(500000000000000000), +// extraData: closeOrderBytes, +// }, +// receiptStatus: uint64Ptr(0), +// expectedTokenTransfer: types.TokenTransfer{}, +// expectedExtraData: nil, +// }, +// { +// name: "erc20 transfer with close order but receipt not found", +// txConfig: transactionConfig{ +// contractAddress: contract1Address, +// isERC20: true, +// recipient: recipient, +// amount: big.NewInt(500000000000000000), +// extraData: closeOrderBytes, +// }, +// receiptStatus: nil, +// expectedTokenTransfer: types.TokenTransfer{}, +// expectedExtraData: nil, +// }, +// { +// name: "non-erc20 transaction", +// txConfig: transactionConfig{ +// contractAddress: contract1Address, +// isERC20: false, +// recipient: "", +// amount: nil, +// extraData: nil, +// }, +// receiptStatus: nil, +// expectedTokenTransfer: types.TokenTransfer{}, +// expectedExtraData: nil, +// }, +// { +// name: "erc20 transfer but token info not found", +// txConfig: transactionConfig{ +// contractAddress: unknownContract, +// isERC20: true, +// recipient: recipient, +// amount: big.NewInt(1000000000000000000), +// extraData: nil, +// }, +// receiptStatus: nil, +// expectedTokenTransfer: types.TokenTransfer{}, +// expectedExtraData: nil, +// }, +// { +// name: "zero amount transfer. no extra data, no token transfer", +// txConfig: transactionConfig{ +// contractAddress: contract1Address, +// isERC20: true, +// recipient: recipient, +// amount: big.NewInt(0), +// extraData: nil, +// }, +// receiptStatus: nil, +// expectedTokenTransfer: types.TokenTransfer{}, +// expectedExtraData: nil, +// }, +// } + +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// // Create transaction based on test configuration +// tx := createTestTransaction(tt.txConfig) + +// // Set up receipts +// receipts := make(map[common.Hash]*ethtypes.Receipt) +// if tt.receiptStatus != nil { +// receipts[tx.tx.Hash()] = ðtypes.Receipt{Status: *tt.receiptStatus} +// } + +// // Set up provider +// logger := lib.NewDefaultLogger() +// mockClient := &mockEthClient{ +// receipts: receipts, +// } +// provider := &EthBlockProvider{ +// rpcClient: mockClient, +// erc20TokenCache: tokenCache, +// logger: logger, +// } + +// // Execute test +// provider.checkTransfer(tx) + +// // Verify results +// cmpopts := cmpopts.IgnoreFields(types.TokenTransfer{}, "TransactionID") +// if diff := cmp.Diff(tt.expectedTokenTransfer, tx.tokenTransfer, cmpopts); diff != "" { +// t.Errorf("token transfer mismatch (-want +got):\n%s", diff) +// } +// if diff := cmp.Diff(tt.expectedExtraData, tx.orderData); diff != "" { +// t.Errorf("extra data mismatch (-want +got):\n%s", diff) +// } +// }) +// } +// } + +// transactionConfig defines the configuration for creating test transactions +type transactionConfig struct { + contractAddress common.Address + isERC20 bool + recipient string + amount *big.Int + extraData []byte +} + +// createTestTransaction creates a transaction based on the provided configuration +func createTestTransaction(config transactionConfig) *Transaction { + var data []byte + + if config.isERC20 { + data = createERC20TransferData(config.recipient, config.amount, config.extraData) + } else { + data = []byte("not_erc20_data") + } + + ethTx := createTransaction(config.contractAddress, data) + tx, _ := NewTransaction(ethTx, 1) + return tx +} + +// uint64Ptr returns a pointer to a uint64 value +func uint64Ptr(v uint64) *uint64 { + return &v +} diff --git a/cmd/rpc/oracle/eth/erc20_token_cache.go b/cmd/rpc/oracle/eth/erc20_token_cache.go new file mode 100644 index 000000000..0661250db --- /dev/null +++ b/cmd/rpc/oracle/eth/erc20_token_cache.go @@ -0,0 +1,164 @@ +package eth + +import ( + "context" + "encoding/hex" + "math/big" + "strings" + "time" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" +) + +const ( + // erc20NameFunction is the function signature for name() + erc20NameFunction = "0x06fdde03" + // erc20SymbolFunction is the function signature for symbol() + erc20SymbolFunction = "0x95d89b41" + // erc20DecimalsFunction is the function signature for decimals() + erc20DecimalsFunction = "0x313ce567" + // context timeout for call contract calls + callContractTimeoutS = 5 + // default cache size for token information + defaultCacheSize = 1000 +) + +// ContractCaller interface defines the method needed to call ethereum contracts +type ContractCaller interface { + CallContract(ctx context.Context, msg ethereum.CallMsg, height *big.Int) ([]byte, error) +} + +// ERC20TokenCache caches token information for ERC20 contracts +type ERC20TokenCache struct { + // client is the ethereum client used to make contract calls + client ContractCaller + // cache stores token information by contract address using LRU eviction + cache *lib.LRUCache[types.TokenInfo] + // metrics for telemetry + metrics *lib.Metrics +} + +// NewERC20TokenCache creates a new ERC20TokenCache instance +func NewERC20TokenCache(client ContractCaller, metrics *lib.Metrics) *ERC20TokenCache { + return &ERC20TokenCache{ + client: client, + cache: lib.NewLRUCache[types.TokenInfo](defaultCacheSize), + metrics: metrics, + } +} + +// TokenInfo fetches an erc20's name, symbol and decimals from the contract +func (m *ERC20TokenCache) TokenInfo(ctx context.Context, contractAddress string) (types.TokenInfo, error) { + // check if token info is already cached + if info, exists := m.cache.Get(contractAddress); exists { + // record cache hit metrics + if m.metrics != nil { + m.metrics.UpdateEthBlockProviderMetrics(0, 0, 0, 1, 0, 0, 0, 0, 0) + } + return info, nil + } + // fetch name from contract + nameBytes, err := m.callContract(ctx, contractAddress, erc20NameFunction) + if err != nil { + m.metrics.IncrementEthTokenInfoFetchError("name") + return types.TokenInfo{}, ErrTokenInfo + } + // decode name from bytes + name := decodeString(nameBytes) + // fetch symbol from contract + symbolBytes, err := m.callContract(ctx, contractAddress, erc20SymbolFunction) + if err != nil { + m.metrics.IncrementEthTokenInfoFetchError("symbol") + return types.TokenInfo{}, ErrTokenInfo + } + // decode symbol from bytes + symbol := decodeString(symbolBytes) + // fetch decimals from contract + decimalsBytes, err := m.callContract(ctx, contractAddress, erc20DecimalsFunction) + if err != nil { + m.metrics.IncrementEthTokenInfoFetchError("decimals") + return types.TokenInfo{}, ErrTokenInfo + } + // decode decimals from bytes + decimals := decodeUint8(decimalsBytes) + // create token info struct + tokenInfo := types.TokenInfo{ + Name: name, + Symbol: symbol, + Decimals: decimals, + } + // cache the token info + m.cache.Put(contractAddress, tokenInfo) + // record cache miss metrics + if m.metrics != nil { + m.metrics.UpdateEthBlockProviderMetrics(0, 0, 0, 0, 1, 0, 0, 0, 0) + } + return tokenInfo, nil +} + +// callContract uses client to call the specified function at address +func (m *ERC20TokenCache) callContract(ctx context.Context, address, function string) ([]byte, error) { + // convert address string to common.Address + contractAddr := common.HexToAddress(address) + // decode function signature from hex + data, err := hex.DecodeString(strings.TrimPrefix(function, "0x")) + if err != nil { + return nil, ErrInvalidTransactionData + } + // create call message + msg := ethereum.CallMsg{ + To: &contractAddr, + Data: data, + } + // create fresh context with timeout that respects the parent context + callCtx, cancel := context.WithTimeout(ctx, callContractTimeoutS*time.Second) + defer cancel() + // make the contract call + result, err := m.client.CallContract(callCtx, msg, nil) + if err != nil { + // check if this was a timeout + if ctx.Err() != nil || callCtx.Err() != nil { + m.metrics.IncrementEthTokenContractCallTimeout() + } + return nil, ErrContractNotFound + } + return result, nil +} + +// decodeString decodes a string from ethereum contract call result +func decodeString(data []byte) string { + // check if data is long enough for offset and length + if len(data) < 64 { + return "" + } + // get offset (first 32 bytes) + offset := new(big.Int).SetBytes(data[0:32]).Uint64() + // check if offset is valid + if offset >= uint64(len(data)) { + return "" + } + // get length from offset position + if offset+32 > uint64(len(data)) { + return "" + } + length := new(big.Int).SetBytes(data[offset : offset+32]).Uint64() + // extract string data + if offset+32+length > uint64(len(data)) { + return "" + } + stringData := data[offset+32 : offset+32+length] + return string(stringData) +} + +// decodeUint8 decodes a uint8 from ethereum contract call result +func decodeUint8(data []byte) uint8 { + // check if data is long enough + if len(data) < 32 { + return 0 + } + // convert last byte to uint8 + return uint8(data[31]) +} diff --git a/cmd/rpc/oracle/eth/erc20_token_cache_test.go b/cmd/rpc/oracle/eth/erc20_token_cache_test.go new file mode 100644 index 000000000..d53614336 --- /dev/null +++ b/cmd/rpc/oracle/eth/erc20_token_cache_test.go @@ -0,0 +1,238 @@ +package eth + +import ( + "context" + "math/big" + "strings" + "testing" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/ethereum/go-ethereum" + "github.com/google/go-cmp/cmp" +) + +const ( + // nameFunction is the function signature for name() + nameFunction = "0x06fdde03" + // symbolFunction is the function signature for symbol() + symbolFunction = "0x95d89b41" + // decimalsFunction is the function signature for decimals() + decimalsFunction = "0x313ce567" +) + +// mockContractCaller implements ContractCaller for testing +type mockContractCaller struct { + // How many times CallContract was called + numCalls int + // map for name responses for each contract address + names map[string][]byte + // map for symbol responses for each contract address + symbols map[string][]byte + // map for decimal responses for each contract address + decimals map[string][]byte +} + +// CallContract implements ContractCaller interface +func (m *mockContractCaller) CallContract(ctx context.Context, msg ethereum.CallMsg, height *big.Int) ([]byte, error) { + // increment call counter + m.numCalls++ + // get contract address as string + address := strings.ToLower(msg.To.Hex()) + // decode function signature from call data + if len(msg.Data) < 4 { + return nil, ErrInvalidTransactionData + } + // get function signature + functionSig := "0x" + lib.BytesToString(msg.Data[:4]) + // return appropriate response based on function signature + switch functionSig { + case nameFunction: + if response, exists := m.names[address]; exists { + return response, nil + } + case symbolFunction: + if response, exists := m.symbols[address]; exists { + return response, nil + } + case decimalsFunction: + if response, exists := m.decimals[address]; exists { + return response, nil + } + } + // return error if no response found + return nil, ErrContractNotFound +} + +// buildContractResponse builds a map with contract address and response data +func buildContractResponse(contract, function, response string) map[string][]byte { + // create response map + responseMap := make(map[string][]byte) + // encode response based on function type + var encodedResponse []byte + if function == nameFunction || function == symbolFunction { + // encode string response + encodedResponse = encodeString(response) + } else if function == decimalsFunction { + // encode uint8 response + encodedResponse = encodeUint8(response) + } + // add response to map + responseMap[strings.ToLower(contract)] = encodedResponse + return responseMap +} + +// encodeString encodes a string for ethereum contract response +func encodeString(s string) []byte { + // create 64 byte buffer for offset and length + result := make([]byte, 64) + // set offset to 32 (0x20) + result[31] = 0x20 + // set length + length := len(s) + result[63] = byte(length) + // append string data + result = append(result, []byte(s)...) + // pad to 32 byte boundary + for len(result)%32 != 0 { + result = append(result, 0) + } + return result +} + +// encodeUint8 encodes a uint8 for ethereum contract response +func encodeUint8(s string) []byte { + // create 32 byte buffer + result := make([]byte, 32) + // parse string to uint8 + if s == "18" { + result[31] = 18 + } else if s == "6" { + result[31] = 6 + } + return result +} + +func TestERC20TokenCache_TokenInfo(t *testing.T) { + // define test contract addresses + usdcAddress := "0xa0b86a33e6441e6c7c5c8c8c8c8c8c8c8c8c8c8c" + invalidAddress := "invalid" + // define test cases + tests := []struct { + name string + contractAddress string + setupCache func(*ERC20TokenCache) + mockCaller *mockContractCaller + expectedResult types.TokenInfo + verifyCacheHit bool + expectedNumCalls int + expectError bool + }{ + { + name: "should call contract and return token info for USDC", + contractAddress: usdcAddress, + mockCaller: &mockContractCaller{ + names: buildContractResponse(usdcAddress, nameFunction, "USD Coin"), + symbols: buildContractResponse(usdcAddress, symbolFunction, "USDC"), + decimals: buildContractResponse(usdcAddress, decimalsFunction, "6"), + }, + expectedResult: types.TokenInfo{ + Name: "USD Coin", + Symbol: "USDC", + Decimals: 6, + }, + verifyCacheHit: true, + expectedNumCalls: 3, + expectError: false, + }, + { + name: "should return cached token info for USDC", + setupCache: func(cache *ERC20TokenCache) { + cache.cache.Put(usdcAddress, types.TokenInfo{ + Name: "USD Coin", + Symbol: "USDC", + Decimals: 6, + }) + }, + contractAddress: usdcAddress, + mockCaller: &mockContractCaller{ + names: buildContractResponse(usdcAddress, nameFunction, "USD Coin"), + symbols: buildContractResponse(usdcAddress, symbolFunction, "USDC"), + decimals: buildContractResponse(usdcAddress, decimalsFunction, "6"), + }, + expectedResult: types.TokenInfo{ + Name: "USD Coin", + Symbol: "USDC", + Decimals: 6, + }, + verifyCacheHit: true, + expectedNumCalls: 0, + expectError: false, + }, + { + name: "should return error for invalid address", + contractAddress: invalidAddress, + mockCaller: &mockContractCaller{ + names: make(map[string][]byte), + symbols: make(map[string][]byte), + decimals: make(map[string][]byte), + }, + expectedResult: types.TokenInfo{}, + expectedNumCalls: 1, + expectError: true, + }, + { + name: "should return error when contract not found", + contractAddress: "0xC2c86a33E6441E6C7C5c8c8c8c8c8c8c8c8c8c8c", + mockCaller: &mockContractCaller{ + names: make(map[string][]byte), + symbols: make(map[string][]byte), + decimals: make(map[string][]byte), + }, + expectedResult: types.TokenInfo{}, + expectedNumCalls: 1, + expectError: true, + }, + } + // run test cases + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create new token cache with mock caller + cache := NewERC20TokenCache(tt.mockCaller, nil) + // set up initial cache if provided + if tt.setupCache != nil { + tt.setupCache(cache) + } + // call TokenInfo method + result, err := cache.TokenInfo(context.Background(), tt.contractAddress) + // verify CallContract was called the expected number of times + if tt.mockCaller.numCalls != tt.expectedNumCalls { + t.Errorf("expected number of calls %d, got %d", tt.expectedNumCalls, tt.mockCaller.numCalls) + } + // check error expectation + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + // check for unexpected error + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + // Verify cache hit if expected + if tt.verifyCacheHit { + if cachedResult, exists := cache.cache.Get(tt.contractAddress); !exists { + t.Errorf("expected token info to be cached for %s", tt.contractAddress) + } else if diff := cmp.Diff(tt.expectedResult, cachedResult); diff != "" { + t.Errorf("cached result mismatch (-expected +actual):\n%s", diff) + } + } + // Compare the expected results + if diff := cmp.Diff(tt.expectedResult, result); diff != "" { + t.Errorf("result mismatch (-expected +actual):\n%s", diff) + } + }) + } +} diff --git a/cmd/rpc/oracle/eth/error.go b/cmd/rpc/oracle/eth/error.go new file mode 100644 index 000000000..113574561 --- /dev/null +++ b/cmd/rpc/oracle/eth/error.go @@ -0,0 +1,35 @@ +package eth + +import ( + "errors" + "fmt" +) + +var ( + ErrInvalidKey = errors.New("invalid private key") + ErrInvalidTransactionData = errors.New("invalid transaction data") + ErrNotERC20Transfer = errors.New("transaction is not an erc20 transfer") + ErrContractNotFound = errors.New("contract address not found") + ErrInvalidPrivateKey = errors.New("invalid private key") + ErrTransactionFailed = errors.New("transaction failed") + ErrGasPriceEstimation = errors.New("failed to estimate gas price") + ErrNonceRetrieval = errors.New("failed to retrieve nonce") + ErrGasEstimation = errors.New("failed to estimate gas") + ErrTransactionSigning = errors.New("failed to sign transaction") + ErrTransactionSending = errors.New("failed to send transaction") + ErrNilTransaction = errors.New("transaction is nil") + ErrMaxRetries = errors.New("maximum retries reached") + ErrTransactionReceipt = errors.New("failed to get transaction receipt") + ErrTokenInfo = errors.New("failed to get token info") + ErrSourceHeight = errors.New("ethereum block height lower than expected") +) + +// InvalidAddressError represents an error for an invalid ethereum address +type InvalidAddressError struct { + Address string +} + +// Error returns the error message including the invalid address +func (e *InvalidAddressError) Error() string { + return fmt.Sprintf("invalid address: %s", e.Address) +} diff --git a/cmd/rpc/oracle/eth/known_tokens.go b/cmd/rpc/oracle/eth/known_tokens.go new file mode 100644 index 000000000..61c64086a --- /dev/null +++ b/cmd/rpc/oracle/eth/known_tokens.go @@ -0,0 +1,50 @@ +package eth + +import "github.com/ethereum/go-ethereum/common" + +// Known ERC20 token contracts on Ethereum mainnet +// These are reference addresses for commonly supported tokens +// The oracle is token-agnostic and works with any ERC20 contract +var ( + // USDCMainnet is the USDC token contract on Ethereum mainnet + // Decimals: 6 + // Standard ERC20 implementation + USDCMainnet = common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") + + // USDTMainnet is the USDT token contract on Ethereum mainnet + // Decimals: 6 + // Non-standard ERC20 implementation: + // - transfer() and transferFrom() return void instead of bool + // - However, this does NOT affect the oracle since it observes transactions + // rather than calling these functions + // - The oracle validates transfers via transaction receipt status, + // which works identically for both standard and non-standard tokens + USDTMainnet = common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7") +) + +// TokenInfo provides metadata about known tokens +type TokenInfo struct { + Name string + Symbol string + Decimals uint8 + Address common.Address + Notes string +} + +// KnownTokens maps contract addresses to token information +var KnownTokens = map[common.Address]TokenInfo{ + USDCMainnet: { + Name: "USD Coin", + Symbol: "USDC", + Decimals: 6, + Address: USDCMainnet, + Notes: "Standard ERC20 implementation", + }, + USDTMainnet: { + Name: "Tether USD", + Symbol: "USDT", + Decimals: 6, + Address: USDTMainnet, + Notes: "Non-standard ERC20: returns void instead of bool. This doesn't affect the oracle's passive observation approach.", + }, +} diff --git a/cmd/rpc/oracle/eth/transaction.go b/cmd/rpc/oracle/eth/transaction.go new file mode 100644 index 000000000..4f902a6bd --- /dev/null +++ b/cmd/rpc/oracle/eth/transaction.go @@ -0,0 +1,263 @@ +package eth + +import ( + "fmt" + "math/big" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +const ( + // ethereumBlockchain represents the ethereum blockchain identifier + ethereumBlockchain = "ethereum" + // erc20TransferMethodID is the method signature for ERC20 transfer function + erc20TransferMethodID = "a9059cbb" + // erc20TransferDataLength is the expected length of ERC20 transfer data (4 bytes method + 32 bytes address + 32 bytes amount) + erc20TransferDataLength = 68 + // maxTransactionDataSize is the maximum allowed size for transaction data to prevent memory exhaustion + maxTransactionDataSize = 1024 +) + +var _ types.TransactionI = &Transaction{} // Ensures *Transaction implements TransactionI + +// Transaction represents an ethereum transaction that implements TransactionI +type Transaction struct { + // tx holds the underlying ethereum transaction + tx *ethtypes.Transaction + // to address + to string + // signer address (transaction from address) + from string + // tokenInfo holds erc20 token info + tokenInfo types.TokenInfo + // isERC20 stores whether a valid ERC20 transfer function id was detected, and transaction data is of sufficient length + isERC20 bool + // erc20Amount stores the amount of the erc20 token transferred + erc20Amount *big.Int + // erc20Recipient is the recipient of the erc20 transfer + erc20Recipient string + // order contains the witnessed order and height + order *types.WitnessedOrder + // orderData contains the validated bytes of a canopy lock or close order + orderData []byte +} + +// NewTransaction creates a new Transaction instance from an ethereum transaction +func NewTransaction(ethTx *ethtypes.Transaction, chainId uint64) (*Transaction, error) { + // check nil ethTx + if ethTx == nil { + return nil, ErrNilTransaction + } + // create new tx wrapper + tx := &Transaction{ + tx: ethTx, + } + // check if transaction has a recipient + if ethTx.To() != nil { + // set to address + tx.to = ethTx.To().Hex() + } + // validate address format + if !common.IsHexAddress(tx.to) { + return nil, &InvalidAddressError{Address: tx.to} + } + // extract sender address using latest signer + from, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(big.NewInt(int64(chainId))), ethTx) + if err != nil { + return nil, fmt.Errorf("failed to extract sender address: %w", err) + } + // set from address + tx.from = from.Hex() + // return transaction + return tx, nil +} + +// parseDataForOrders examines the transaction input data looking for canopy orders +func (t *Transaction) parseDataForOrders(orderValidator OrderValidator) error { + // get ethereum transaction data + txData := t.tx.Data() + // check for transaction data + if len(txData) == 0 { + // no transaction data to process + return nil + } + // test txData size + if len(txData) > maxTransactionDataSize { + // large transactions are not expected from Canopy swap clients + return nil + } + // support lock orders embedded in self-sent transactions + if t.To() == t.From() { + // canopy swap clients will place lock order json in transaction data + err := orderValidator.ValidateOrderJsonBytes(txData, types.LockOrderType) + if err != nil { + // self-sent transaction did not contain canopy lock order json - normal condition + return err + } + order := &lib.LockOrder{} + // unmarshal the validated json data + err = order.UnmarshalJSON(txData) + if err != nil { + return fmt.Errorf("failed to unmarshal lock order json: %w", err) + } + // create and store witnessed order + t.order = &types.WitnessedOrder{ + OrderId: order.OrderId, + LockOrder: order, + } + // lock order found - no error + return nil + } + // Canopy swap embeds lock and close orders in data trailing + // standard ERC20 ABI encoded function call data + recipient, amount, data, err := parseERC20Transfer(txData) + if err != nil { + // not an erc20 transfer - normal condition + return nil + } + // fmt.Println("checking", t.from, recipient, amount, string(data), err) + // fmt.Println("checking", t.from == recipient, amount.Cmp(big.NewInt(0))) + // all Canopy swap ERC20 transfers have aux data + if len(data) == 0 { + // no data to process - not a canopy swap ERC20 transfer + return nil + } + + switch amount.Cmp(big.NewInt(0)) { + case 0: // zero amount - potential lock order + // test for self-sent ERC20 transfers, lock orders must be self-sent + if t.from != recipient { + break + } + // attempt to validate a lock order + err = orderValidator.ValidateOrderJsonBytes(data, types.LockOrderType) + if err != nil { + // erc20 transaction did not contain canopy lock order json - normal condition + return err + } + fmt.Println("validated lock order", t.from, recipient, amount, string(data), err) + order := &lib.LockOrder{} + // unmarshal the validated json data + err = order.UnmarshalJSON(data) + if err != nil { + return fmt.Errorf("failed to unmarshal validated lock order json: %w", err) + } + // create witnessed order + t.order = &types.WitnessedOrder{ + OrderId: order.OrderId, + LockOrder: order, + } + case 1: // positive amount - potential close order + // attempt to validate a close order + err = orderValidator.ValidateOrderJsonBytes(data, types.CloseOrderType) + if err != nil { + // erc20 transaction did not contain canopy close order json - normal condition + return err + } + order := &lib.CloseOrder{} + // unmarshal the validated json data + err = order.UnmarshalJSON(data) + if err != nil { + return fmt.Errorf("failed to unmarshal validated close order json: %w", err) + } + // create witnessed order + t.order = &types.WitnessedOrder{ + OrderId: order.OrderId, + CloseOrder: order, + } + case -1: + // should not happen - input should be validated prior to this + return fmt.Errorf("transaction contained negative transfer amount") + } + // set erc20 flag + t.isERC20 = true + // store erc20 fields + t.erc20Recipient = recipient + t.erc20Amount = amount + return nil +} + +// Blockchain returns the blockchain identifier +func (t *Transaction) Blockchain() string { + // return ethereum blockchain identifier + return ethereumBlockchain +} + +// From returns the sender address of the transaction +func (t *Transaction) From() string { + return t.from +} + +// To returns the recipient address of the transaction +func (t *Transaction) To() string { + return t.to +} + +// Order returns the witnessed order +func (t *Transaction) Order() *types.WitnessedOrder { + return t.order +} + +// Hash returns the transaction hash +func (t *Transaction) Hash() string { + // return transaction hash as hex string + return t.tx.Hash().Hex() +} + +// clearOrder clears order and transfer data +func (t *Transaction) clearOrder() { + t.order = nil + t.isERC20 = false +} + +// TokenTransfer returns the token transfer information +func (t *Transaction) TokenTransfer() types.TokenTransfer { + return types.TokenTransfer{ + Blockchain: ethereumBlockchain, + TokenInfo: t.tokenInfo, + TransactionID: t.Hash(), + SenderAddress: t.From(), + RecipientAddress: t.erc20Recipient, + TokenBaseAmount: t.erc20Amount, + ContractAddress: t.To(), + } +} + +// parseERC20Transfer parses the transaction data looking for ERC20 transfers and any auxiliary data beyond the standard transfer call +// +// This function works with ANY ERC20 token, including non-standard implementations like USDT: +// - Standard ERC20: transfer() returns bool +// - USDT: transfer() returns void (no return value) +// +// This difference does NOT affect the oracle because: +// 1. The oracle observes transactions passively - it doesn't call transfer() +// 2. We parse the transaction INPUT data (the function call parameters), not return values +// 3. Transfer success is validated via transaction receipt status (see block_provider.go) +// +// This approach works identically for USDC, USDT, and all ERC20 variants. +func parseERC20Transfer(data []byte) (recipientAddress string, amount *big.Int, auxData []byte, err error) { + // check if data is long enough to contain a valid ERC20 transfer + if len(data) < erc20TransferDataLength { + return "", nil, nil, ErrNotERC20Transfer + } + // extract method signature from first 4 bytes + methodID := lib.BytesToString(data[:4]) + // verify this is an ERC20 transfer method call + if methodID != erc20TransferMethodID { + return "", nil, nil, ErrNotERC20Transfer + } + // extract recipient address from bytes 4-36 (32 bytes, but address is only last 20 bytes) + recipientBytes := data[16:36] + recipientAddress = common.BytesToAddress(recipientBytes).Hex() + // extract amount from bytes 36-68 (32 bytes) + amountBytes := data[36:68] + amount = new(big.Int).SetBytes(amountBytes) + // check if there is extra data beyond the standard transfer call + if len(data) > erc20TransferDataLength { + auxData = data[erc20TransferDataLength:] + } + return recipientAddress, amount, auxData, nil +} diff --git a/cmd/rpc/oracle/eth/transaction_test.go b/cmd/rpc/oracle/eth/transaction_test.go new file mode 100644 index 000000000..06c5a6d07 --- /dev/null +++ b/cmd/rpc/oracle/eth/transaction_test.go @@ -0,0 +1,232 @@ +package eth + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func createERC20TransferData(recipient string, amount *big.Int, extra []byte) []byte { + // Initialize empty byte slice to build the transaction data + data := make([]byte, 0) + // Decode the ERC20 transfer method ID from hex string and append to data + methodIDBytes, _ := hex.DecodeString(erc20TransferMethodID) + data = append(data, methodIDBytes...) + // Remove "0x" prefix from recipient address and decode from hex + recipientBytes, _ := hex.DecodeString(recipient[2:]) + // Create 32-byte padded recipient address (addresses are 20 bytes, padded with 12 zero bytes at start) + paddedRecipient := make([]byte, 32) + copy(paddedRecipient[12:], recipientBytes) + // Print recipient bytes for debugging (should be removed in production) + // Append padded recipient address to transaction data + data = append(data, paddedRecipient...) + // Convert big.Int amount to bytes + amountBytes := amount.Bytes() + // Create 32-byte padded amount (right-aligned, padded with zeros at start) + paddedAmount := make([]byte, 32) + copy(paddedAmount[32-len(amountBytes):], amountBytes) + // Append padded amount to transaction data + data = append(data, paddedAmount...) + // If extra data is provided, append it to the transaction data + if extra != nil { + data = append(data, extra...) + } + // Return the complete encoded transaction data + return data +} + +func TestParseERC20Transfer(t *testing.T) { + recipient1 := common.HexToAddress("0x742d35cc6634c0532925a3b8d0c9e3e0c8b0e8c2").Hex() + recipient2 := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678").Hex() + amount1 := big.NewInt(1000000000000000000) + amount2 := big.NewInt(500000000000000000) + extraData1 := []byte("extra") + extraData2 := []byte("more data") + + tests := []struct { + name string + data []byte + expectedRecipient string + expectedAmount *big.Int + expectedExtra []byte + expectedError error + }{ + { + name: "valid erc20 transfer with no extra data", + data: createERC20TransferData(recipient1, amount1, nil), + expectedRecipient: recipient1, + expectedAmount: amount1, + expectedExtra: nil, + expectedError: nil, + }, + { + name: "valid erc20 transfer with extra data", + data: createERC20TransferData(recipient2, amount2, extraData1), + expectedRecipient: recipient2, + expectedAmount: amount2, + expectedExtra: extraData1, + expectedError: nil, + }, + { + name: "valid erc20 transfer with longer extra data", + data: createERC20TransferData(recipient1, amount2, extraData2), + expectedRecipient: recipient1, + expectedAmount: amount2, + expectedExtra: extraData2, + expectedError: nil, + }, + { + name: "nil data", + data: nil, + expectedRecipient: "", + expectedAmount: nil, + expectedExtra: nil, + expectedError: ErrNotERC20Transfer, + }, + { + name: "empty data", + data: []byte{}, + expectedRecipient: "", + expectedAmount: nil, + expectedExtra: nil, + expectedError: ErrNotERC20Transfer, + }, + { + name: "data too short", + data: []byte{0xa9, 0x05, 0x9c, 0xbb, 0x00, 0x00}, + expectedRecipient: "", + expectedAmount: nil, + expectedExtra: nil, + expectedError: ErrNotERC20Transfer, + }, + { + name: "wrong method id", + data: append([]byte{0x12, 0x34, 0x56, 0x78}, make([]byte, 64)...), + expectedRecipient: "", + expectedAmount: nil, + expectedExtra: nil, + expectedError: ErrNotERC20Transfer, + }, + { + name: "exact minimum length", + data: createERC20TransferData(recipient1, big.NewInt(0), nil), + expectedRecipient: recipient1, + expectedAmount: big.NewInt(0), + expectedExtra: nil, + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + recipient, amount, extra, err := parseERC20Transfer(tt.data) + if err != tt.expectedError { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } + if recipient != tt.expectedRecipient { + t.Errorf("expected recipient %s, got %s", tt.expectedRecipient, recipient) + } + if tt.expectedAmount != nil && amount != nil { + if amount.Cmp(tt.expectedAmount) != 0 { + t.Errorf("expected amount %s, got %s", tt.expectedAmount.String(), amount.String()) + } + } else if tt.expectedAmount != amount { + t.Errorf("expected amount %v, got %v", tt.expectedAmount, amount) + } + if len(tt.expectedExtra) != len(extra) { + t.Errorf("expected extra data length %d, got %d", len(tt.expectedExtra), len(extra)) + } + for i, b := range tt.expectedExtra { + if i < len(extra) && extra[i] != b { + t.Errorf("expected extra data byte %d to be %x, got %x", i, b, extra[i]) + } + } + }) + } +} + +// TestParseERC20Transfer_USDT verifies that USDT transfers parse identically to standard ERC20 transfers +// USDT has a non-standard implementation (returns void instead of bool from transfer()), +// but this doesn't affect parsing since we parse the INPUT data, not return values +func TestParseERC20Transfer_USDT(t *testing.T) { + // Use USDT mainnet contract address + usdtContract := USDTMainnet.Hex() + // USDT uses 6 decimals, so 1 USDT = 1,000,000 base units + oneUSDT := big.NewInt(1000000) + recipient := common.HexToAddress("0x742d35cc6634c0532925a3b8d0c9e3e0c8b0e8c2").Hex() + orderJSON := []byte(`{"order_id":"abc123"}`) + + tests := []struct { + name string + amount *big.Int + extraData []byte + expectedRecipient string + expectedAmount *big.Int + expectedExtra []byte + }{ + { + name: "USDT transfer with order data", + amount: oneUSDT, + extraData: orderJSON, + expectedRecipient: recipient, + expectedAmount: oneUSDT, + expectedExtra: orderJSON, + }, + { + name: "USDT transfer without extra data", + amount: big.NewInt(5000000), // 5 USDT + extraData: nil, + expectedRecipient: recipient, + expectedAmount: big.NewInt(5000000), + expectedExtra: nil, + }, + { + name: "USDT zero amount transfer (lock order)", + amount: big.NewInt(0), + extraData: orderJSON, + expectedRecipient: recipient, + expectedAmount: big.NewInt(0), + expectedExtra: orderJSON, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create ERC20 transfer data (same format for USDT and standard ERC20) + data := createERC20TransferData(tt.expectedRecipient, tt.amount, tt.extraData) + + // Parse the transfer + parsedRecipient, parsedAmount, parsedExtra, err := parseERC20Transfer(data) + + // Verify parsing succeeds + if err != nil { + t.Errorf("USDT transfer parsing failed: %v", err) + } + + // Verify recipient matches + if parsedRecipient != tt.expectedRecipient { + t.Errorf("expected recipient %s, got %s", tt.expectedRecipient, parsedRecipient) + } + + // Verify amount matches + if parsedAmount.Cmp(tt.expectedAmount) != 0 { + t.Errorf("expected amount %s, got %s", tt.expectedAmount.String(), parsedAmount.String()) + } + + // Verify extra data matches + if len(parsedExtra) != len(tt.expectedExtra) { + t.Errorf("expected extra data length %d, got %d", len(tt.expectedExtra), len(parsedExtra)) + } + for i, b := range tt.expectedExtra { + if parsedExtra[i] != b { + t.Errorf("expected extra data byte %d to be %x, got %x", i, b, parsedExtra[i]) + } + } + + // Log success - USDT parsing works identically to standard ERC20 + t.Logf("✓ USDT transfer parsed successfully (contract: %s, amount: %s)", usdtContract, parsedAmount.String()) + }) + } +} diff --git a/cmd/rpc/oracle/oracle.go b/cmd/rpc/oracle/oracle.go new file mode 100644 index 000000000..757bd6965 --- /dev/null +++ b/cmd/rpc/oracle/oracle.go @@ -0,0 +1,856 @@ +package oracle + +import ( + "bytes" + "context" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/ethereum/go-ethereum/common" +) + +// Terminology +// 1. *Observer Chain* - The Canopy nested chain recording the witnessed transactions +// 2. *Source Chain* - The source chain, such as Ethereum, where this Oracle witnesses transactions +// 2. *Witness Node* - An individual validator monitoring source chain transactions, running in the observer chain +// 4. *Transaction Oracle* - The overall system connecting Ethereum to Canopy + +// Oracle is a chain-agnostic type implementing validation and storage logic for a cross-chain Oracle +// It coordinates between three components: +// - The source chain where transactions containing Canopy lock & close orders are witnessed +// - The witness nodes order store where Canopy lock & close orders are persisted +// - The witness nodes participation in the observer chain BFT process. +// - The root chain by submitting certificate results containing majority witnessed orders, and receiving order book updates which represent root chain order book activity + +// The oracle integrates with the BFT consensus process through two key methods: +// - WitnessedOrders +// - ValidateProposedOrders +// It also receives root chain updates through: +// - UpdateRootChainInfo +type Oracle struct { + // blockProvider is where the oracle will receive new blocks from + blockProvider types.BlockProvider + // the store with which the oracle can persist witnessed orders + orderStore types.OrderStore + // copy of the latest root chain order book + orderBook *lib.OrderBook + // mutex to protect order book + orderBookMu sync.RWMutex + // state handles block processing state, gap detection, and reorg detection + state *OracleState + // oracle configuration + config lib.OracleConfig + // committee to use when constructing close orders. this must match the order bookc committee + committee uint64 + // context to allow graceful shutdown + ctx context.Context + ctxCancel context.CancelFunc + // logger + log lib.LoggerI + // metrics for telemetry + metrics *lib.Metrics +} + +// NewOracle creates a new Oracle instance +func NewOracle(ctx context.Context, config lib.OracleConfig, blockProvider types.BlockProvider, transactionStore types.OrderStore, logger lib.LoggerI, metrics *lib.Metrics) (*Oracle, error) { + // create context cancel function for the passed context + ctx, cancel := context.WithCancel(ctx) + // create new oracle instance + o := &Oracle{ + blockProvider: blockProvider, + orderStore: transactionStore, + log: logger, + state: NewOracleState(config.StateFile, logger), + config: config, + committee: config.Committee, + ctx: ctx, + ctxCancel: cancel, + metrics: metrics, + } + // return new oracle instance + return o, nil +} + +// reorgRollback gets the last known good height and removes orders from the store until +// the reorgRollbackDelta +func (o *Oracle) reorgRollback() { + // get the last height processed by the oracle + height := o.state.GetLastHeight() + if height == 0 { + o.log.Warnf("[ORACLE-REORG] Reorg detected but no last known good height") + return + } + // calculate the rollback height - orders witnessed above this height will be removed + rollbackHeight := height - o.config.ReorgRollbackBlocks + o.log.Infof("[ORACLE-REORG] Rolling back orders witnessed above height %d (last height %d - delta %d)", rollbackHeight, height, o.config.ReorgRollbackBlocks) + // process lock orders first + o.rollbackOrderType(types.LockOrderType, rollbackHeight) + // process close orders second + o.rollbackOrderType(types.CloseOrderType, rollbackHeight) + // update reorg metrics + o.metrics.UpdateOracleErrorMetrics(1, 0, 0) + o.metrics.RecordOracleReorgDepth(o.config.ReorgRollbackBlocks) + o.log.Infof("[ORACLE-REORG] Reorg rollback completed") +} + +// rollbackOrderType removes orders of the specified type that were witnessed above the rollback height +func (o *Oracle) rollbackOrderType(orderType types.OrderType, rollbackHeight uint64) { + // get all order ids of the specified type from the order store + orderIds, err := o.orderStore.GetAllOrderIds(orderType) + if err != nil { + o.log.Errorf("[ORACLE-REORG] Error getting all %s order ids during rollback: %s", orderType, err.Error()) + return + } + // track rollback statistics + totalOrders := len(orderIds) + removedCount := 0 + // examine each stored order and remove if witnessed above rollback height + var storeReadErrors, storeRemoveErrors int + for _, orderId := range orderIds { + // read the witnessed order to check its witnessed height + witnessedOrder, err := o.orderStore.ReadOrder(orderId, orderType) + if err != nil { + o.log.Errorf("[ORACLE-REORG] Error reading %s order %x during rollback: %s", orderType, orderId, err.Error()) + storeReadErrors++ + continue + } + // check if this order was witnessed above the rollback height + if witnessedOrder.WitnessedHeight > rollbackHeight { + // remove the order from the store + err = o.orderStore.RemoveOrder(orderId, orderType) + if err != nil { + o.log.Errorf("[ORACLE-REORG] Error removing %s order %x during rollback: %s", orderType, orderId, err.Error()) + storeRemoveErrors++ + continue + } + removedCount++ + o.log.Debugf("[ORACLE-REORG] Removed %s order %x witnessed at height %d", orderType, orderId, witnessedOrder.WitnessedHeight) + } + } + o.log.Infof("[ORACLE-REORG] Rollback processed %d %s orders, removed %d orders witnessed above height %d", totalOrders, orderType, removedCount, rollbackHeight) + // update pruned orders metric + if removedCount > 0 { + o.metrics.UpdateOracleErrorMetrics(0, removedCount, 0) + } + // update store error metrics + if storeReadErrors > 0 || storeRemoveErrors > 0 { + o.metrics.UpdateOracleStoreErrorMetrics(0, storeReadErrors, storeRemoveErrors) + } +} + +// Start begins listening for blocks from the configured block provider +// syncCh: optional channel to notify when block provider syncs to top (can be nil) +func (o *Oracle) Start(ctx context.Context, syncCh chan<- bool) { + // log that we're starting the oracle + o.log.Info("[ORACLE-LIFECYCLE] Starting oracle") + go func() { + firstRun := true + for { + // an order book must be present to validate incoming orders + // wait for the controller to set it + for o.orderBook == nil { + o.log.Warnf("[ORACLE-LIFECYCLE] Oracle waiting for order book") + time.Sleep(1 * time.Second) + } + // listen for blocks + // only pass syncCh on the first run to avoid closing it multiple times + var ch chan<- bool + if firstRun { + ch = syncCh + firstRun = false + } + err := o.run(ctx, ch) + if err == nil { + // oracle context cancelled + return + } + o.log.Errorf("[ORACLE-LIFECYCLE] Oracle stopped running: %s", err.Error()) + // handle specific error types + switch err.Code() { + case CodeBlockSequence: + // remove current state + o.state.removeState() + o.log.Errorf("[ORACLE-LIFECYCLE] Block sequence gap detected, restarting block provider") + case CodeChainReorg: + // execute a rollback + o.reorgRollback() + // remove current state so oracle restarts from fresh + o.state.removeState() + o.log.Errorf("[ORACLE-LIFECYCLE] Chain reorganization detected - oracle rolled back and will reprocess from fresh state") + default: + o.log.Errorf("[ORACLE-LIFECYCLE] Oracle unexpected error: %v", err) + } + } + }() +} + +// run runs the main Oracle loop +// - get last height from state manager +// - start block provider +// syncCh: optional channel to notify when block provider syncs to top (can be nil) +func (o *Oracle) run(ctx context.Context, syncCh chan<- bool) lib.ErrorI { + // create a new context from the existing one + blockProviderCtx, cancelBlockProvider := context.WithCancel(ctx) + defer cancelBlockProvider() + // get the last height processed by the oracle + if height := o.state.GetLastHeight(); height == 0 { // no height found + // zero signals the block provider to determine its own starting height + o.blockProvider.Start(blockProviderCtx, height) + } else { // height found + // set the starting height for the block provider + o.blockProvider.Start(blockProviderCtx, height+1) + } + // get the block channel from provider + blockCh := o.blockProvider.BlockCh() + // track sync status to avoid duplicate notifications + lastSyncStatus := false + // start the main oracle loop + for { + select { + case block, ok := <-blockCh: + if !ok { + o.log.Warn("[ORACLE-LIFECYCLE] Block channel closed, stopping oracle") + return ErrChannelClosed() + } + if block == nil { + o.log.Warn("[ORACLE-BLOCK] received nil block, skipping") + continue + } + // check block for gaps and reorganizations + if err := o.state.ValidateSequence(block); err != nil { + o.log.Errorf("[ORACLE-BLOCK] block validation failed for height %d: %v", block.Number(), err) + return err + } + // process the received block + err := o.processBlock(block) + // check for processing error + if err != nil { + o.log.Errorf("[ORACLE-BLOCK] Failed to process block at height %d: %v", block.Number(), err) + o.metrics.UpdateOracleErrorMetrics(0, 0, 1) + continue + } + // update safe height after successful block processing + o.state.updateSafeHeight(block.Number(), o.config) + // save state after successful block processing + if err := o.state.saveState(block); err != nil { + o.log.Errorf("[ORACLE-BLOCK] Failed to save block state for height %d: %v", block.Number(), err) + o.metrics.UpdateOracleErrorMetrics(0, 0, 1) + return err + } + // close syncCh when provider is synced to top + if syncCh != nil && !lastSyncStatus { + if o.blockProvider.IsSynced() { + lastSyncStatus = true + close(syncCh) + } + } + case <-ctx.Done(): + // context cancelled, stop the goroutine + o.log.Info("[ORACLE-LIFECYCLE] Oracle context cancelled, stopping block processing") + // notify that oracle is no longer synced when shutting down + if syncCh != nil { + select { + case syncCh <- false: + o.log.Info("[ORACLE-LIFECYCLE] Oracle sync status set to false on shutdown") + default: + // channel full or closed, continue shutdown + } + } + return nil + } + } +} + +// Stop gracefully shuts down the oracle and all oracle components +func (o *Oracle) Stop() { + if o == nil { + return + } + o.log.Info("[ORACLE-LIFECYCLE] Stopping Oracle") + // Cancel the context, stopping oracle and oracle components + o.ctxCancel() +} + +// validateOrder ensures the witnessed order passes basic sanity checks, then validates any lock or close orders with more specific functions +func (o *Oracle) validateOrder(tx types.TransactionI, sellOrder *lib.SellOrder) lib.ErrorI { + // get witnessed order from transaction + order := tx.Order() + if order == nil { + o.metrics.IncrementValidationFailure("order_nil") + return ErrOrderValidation("witnessed order cannot be nil") + } + // convenience variables + hasLock := order.LockOrder != nil + hasClose := order.CloseOrder != nil + // witnessed order must contain either a lock or close order + if !hasLock && !hasClose { + o.metrics.IncrementValidationFailure("missing_order_type") + return ErrOrderValidation("witnessed order must contain either lock or close order") + } + // witnessed order cannot contain both a lock or close order + if hasLock && hasClose { + o.metrics.IncrementValidationFailure("both_order_types") + return ErrOrderValidation("witnessed order cannot contain both lock and close orders") + } + // validate the lock order + if hasLock { + return o.validateLockOrder(order.LockOrder, sellOrder) + } + // validate the close order + return o.validateCloseOrder(order.CloseOrder, sellOrder, tx) +} + +// validateLockOrder ensures a lock order matches a sell order +func (o *Oracle) validateLockOrder(lockOrder *lib.LockOrder, sellOrder *lib.SellOrder) lib.ErrorI { + if !bytes.Equal(lockOrder.OrderId, sellOrder.Id) { + o.metrics.IncrementValidationFailure("lock_id_mismatch") + return ErrOrderValidation("lock order ID does not match sell order ID") + } + if lockOrder.ChainId != sellOrder.Committee { + o.metrics.IncrementValidationFailure("lock_chain_mismatch") + return ErrOrderValidation("lock order chain ID does not match sell order committee") + } + return nil +} + +// validateCloseOrder ensures a close order matches a sell order +// as each field is user-supplied arbitrary data coming from off chain, strict validation +// is required to protect against costly erroneous behavior or malicious activity +func (o *Oracle) validateCloseOrder(closeOrder *lib.CloseOrder, sellOrder *lib.SellOrder, tx types.TransactionI) lib.ErrorI { + // Order data being equal to transaction To address is Ethereum-specific validation + // TODO move this logic into the block provider + + sellOrderDataHex := common.BytesToAddress(sellOrder.Data).String() + if sellOrderDataHex != tx.To() { + fmt.Println(sellOrderDataHex, tx.To()) + o.metrics.IncrementValidationFailure("close_data_mismatch") + return ErrOrderValidation("sell order data field does not match transaction recipient") + } + // ensure the order ids are a match + if !bytes.Equal(closeOrder.OrderId, sellOrder.Id) { + o.metrics.IncrementValidationFailure("close_id_mismatch") + return ErrOrderValidation("close order ID does not match sell order ID") + } + // ensure the chain and committee are a match + if closeOrder.ChainId != sellOrder.Committee { + o.metrics.IncrementValidationFailure("close_chain_mismatch") + return ErrOrderValidation("close order chain ID does not match sell order committee") + } + // convenience variable + tokenTransfer := tx.TokenTransfer() + recipient, err := lib.StringToBytes(strings.TrimPrefix(tokenTransfer.RecipientAddress, "0x")) + if err != nil { + o.metrics.IncrementValidationFailure("recipient_conversion_error") + return ErrOrderValidation("error converting recipient address to bytes") + } + // verify the recipient of the transfer was the seller receive address + if !bytes.Equal(sellOrder.SellerReceiveAddress, recipient) { + o.metrics.IncrementValidationFailure("recipient_mismatch") + return ErrOrderValidation("tokens not transferred to sell receive address") + } + // ensure transfer amount is not nil + // TODO validate further fields here? + if tokenTransfer.TokenBaseAmount == nil { + o.metrics.IncrementValidationFailure("amount_nil") + return ErrOrderValidation("token transfer amount cannot be nil") + } + // ensure the correct amount was transferred + if tokenTransfer.TokenBaseAmount.Uint64() != sellOrder.RequestedAmount { + o.metrics.IncrementValidationFailure("amount_mismatch") + return ErrOrderValidation(fmt.Sprintf("transfer amount %d does not match requested amount %d", + tokenTransfer.TokenBaseAmount.Uint64(), sellOrder.RequestedAmount)) + } + return nil +} + +// processBlock processes a block received from the source chain +// examines any witnessed orders in the block, validates them, and writes them to the order store +// any orders that are not present in the order book, or fail validation, are dropped and not saved to the order store +func (o *Oracle) processBlock(block types.BlockI) lib.ErrorI { + // track block processing start time for metrics + startTime := time.Now() + defer func() { + // update block processing metrics + o.metrics.UpdateOracleBlockMetrics(time.Since(startTime)) + }() + // lock order book for reading + o.orderBookMu.RLock() + defer o.orderBookMu.RUnlock() + // log that we received a new block + if len(block.Transactions()) > 0 { + o.log.Infof("[ORACLE-BLOCK] Received block %s at height %d (%d transactions)", block.Hash(), block.Number(), len(block.Transactions())) + } + // initialize metrics counters for this block + var witnessed, validated, rejected int + var notInOrderbook, duplicate, archived int + // iterate through each transaction + for _, tx := range block.Transactions() { + // get order in this transaction + order := tx.Order() + if order == nil { + // no order in this transaction + continue + } + // find the order in the order book + canopyOrder, orderErr := o.orderBook.GetOrder(order.OrderId) + // check for order error - only error possible is nil order book + if orderErr != nil { + o.log.Errorf("[ORACLE-ORDER] Error getting order from order book: %s", orderErr.Error()) + return orderErr + } + // the order book returns a nil order if no order was found + // this should not happen under normal circumstances but is not an error + if canopyOrder == nil { + // log a warning and continue processing transactions + o.log.Warnf("[ORACLE-ORDER] Order %s not found in order book", lib.BytesToString(order.OrderId)) + rejected++ + notInOrderbook++ + continue + } + // increment witnessed orders counter + witnessed++ + // validate the order that was witnessed against the order found in the order book + validationStart := time.Now() + if err := o.validateOrder(tx, canopyOrder); err != nil { + // log a warning and continue processing transactions + o.log.Warnf("[ORACLE-ORDER] %s", err.Error()) + rejected++ + continue + } + // order validation succeeded + validated++ + validationTime := time.Since(validationStart) + // determine order type + orderType := types.LockOrderType + if order.CloseOrder != nil { + orderType = types.CloseOrderType + } + // check if the witnessed order already exists in store + _, err := o.orderStore.ReadOrder(order.OrderId, orderType) + if err == nil { + o.log.Warnf("[ORACLE-ORDER] Order %s already exists in store, skipping new order", lib.BytesToString(order.OrderId)) + // order exists, skip writing + // this prevents newer orders from overwriting older orders + duplicate++ + continue + } + err = o.orderStore.WriteOrder(order, orderType) + if err != nil { + o.log.Errorf("[ORACLE-ORDER] Failed to write order %s: %v", lib.BytesToString(order.OrderId), err) + o.metrics.UpdateOracleStoreErrorMetrics(1, 0, 0) + return err + } + // write order to archive + err = o.orderStore.ArchiveOrder(order, orderType) + if err != nil { + o.log.Errorf("[ORACLE-ORDER] Failed to archive order %s: %v", lib.BytesToString(order.OrderId), err) + o.metrics.UpdateOracleStoreErrorMetrics(1, 0, 0) + return err + } + archived++ + // update order metrics for this successful write + o.metrics.UpdateOracleOrderMetrics(0, 0, 0, 0, validationTime) + o.log.Debugf("[ORACLE-ORDER] Wrote order %s %s to store", order, orderType) + } + // update order metrics for this block with counters + o.metrics.UpdateOracleOrderMetrics(witnessed, validated, 0, rejected, 0) + // update lifecycle metrics + o.metrics.UpdateOracleLifecycleMetrics(notInOrderbook, duplicate, archived, 0, 0) + // update state and store metrics + o.updateMetrics() + return nil +} + +// ValidateProposedOrders verifies that the passed orders are all present in the local order store. +// This is called when the BFT module validates a block proposal to ensure that each order +// in the proposed block is an exact match for an order in the witnessed order store. +func (o *Oracle) ValidateProposedOrders(orders *lib.Orders) lib.ErrorI { + // oracle is disabled + if o == nil { + return nil + } + // handle nil orders case + if orders == nil { + o.log.Error("[ORACLE-VALIDATE] Proposal orders == nil, unable to validate orders") + return nil + } + // skip validation and logging when no orders are present + if len(orders.LockOrders) == 0 && len(orders.CloseOrders) == 0 { + return nil + } + // get current safe height for validation + safeHeight := o.state.GetSafeHeight() + // entry log with context + o.log.Debugf("[ORACLE-VALIDATE] Validating proposal: %d lock orders, %d close orders, safeHeight=%d", + len(orders.LockOrders), len(orders.CloseOrders), safeHeight) + // validate each lock order against the witnessed order store + for _, lock := range orders.LockOrders { + orderId := lib.BytesToString(lock.OrderId) + // get order from order store + witnessedOrder, err := o.orderStore.ReadOrder(lock.OrderId, types.LockOrderType) + if err != nil { + o.log.Warnf("[ORACLE-VALIDATE] Lock order %s rejected: not found in store", orderId) + return ErrOrderNotVerified(orderId, err) + } + // check if the witnessed order is from a safe block (has sufficient confirmations) + if witnessedOrder.WitnessedHeight > safeHeight { + o.log.Warnf("[ORACLE-VALIDATE] Lock order %s rejected: not safe (witnessed=%d, safe=%d, need %d more blocks)", + orderId, witnessedOrder.WitnessedHeight, safeHeight, witnessedOrder.WitnessedHeight-safeHeight) + return ErrOrderNotVerified(orderId, errors.New("order witnessed above safe height")) + } + // compare orderbook order and witnessed order + if !lock.Equals(witnessedOrder.LockOrder) { + o.log.Warnf("[ORACLE-VALIDATE] Lock order %s rejected: order data mismatch", orderId) + return ErrOrderNotVerified(orderId, errors.New("lock order unequal")) + } + o.log.Infof("[ORACLE-VALIDATE] Lock order %s valid (witnessed=%d)", orderId, witnessedOrder.WitnessedHeight) + } + // validate each close order against the witnessed order store + for _, orderId := range orders.CloseOrders { + orderIdStr := lib.BytesToString(orderId) + // get the witnessed order + witnessedOrder, err := o.orderStore.ReadOrder(orderId, types.CloseOrderType) + if err != nil { + o.log.Warnf("[ORACLE-VALIDATE] Close order %s rejected: not found in store", orderIdStr) + return ErrOrderNotVerified(orderIdStr, err) + } + // check if the witnessed order is from a safe block (has sufficient confirmations) + if witnessedOrder.WitnessedHeight > safeHeight { + o.log.Warnf("[ORACLE-VALIDATE] Close order %s rejected: not safe (witnessed=%d, safe=%d, need %d more blocks)", + orderIdStr, witnessedOrder.WitnessedHeight, safeHeight, witnessedOrder.WitnessedHeight-safeHeight) + return ErrOrderNotVerified(orderIdStr, errors.New("order witnessed above safe height")) + } + // construct close order for comparison + order := lib.CloseOrder{ + OrderId: orderId, + ChainId: o.committee, + CloseOrder: true, + } + // compare orderbook order and witnessed order + if !order.Equals(witnessedOrder.CloseOrder) { + o.log.Warnf("[ORACLE-VALIDATE] Close order %s rejected: order data mismatch", orderIdStr) + return ErrOrderNotVerified(orderIdStr, errors.New("close order unequal")) + } + o.log.Infof("[ORACLE-VALIDATE] Close order %s valid (witnessed=%d)", orderIdStr, witnessedOrder.WitnessedHeight) + } + // summary log + o.log.Infof("[ORACLE-VALIDATE] Validated %d lock orders and %d close orders", len(orders.LockOrders), len(orders.CloseOrders)) + return nil +} + +// CommitCertificate is executed after the quorum agrees on a block +func (o *Oracle) CommitCertificate(qc *lib.QuorumCertificate, block *lib.Block, blockResult *lib.BlockResult, ts uint64) (err lib.ErrorI) { + // oracle is disabled + if o == nil { + return nil + } + // Update the last submit height for all lock orders in this certificate + for _, order := range qc.Results.Orders.LockOrders { + // get order from order store + wOrder, err := o.orderStore.ReadOrder(order.OrderId, types.LockOrderType) + if err != nil { + o.log.Warnf("[ORACLE-COMMIT] Unable to find order %s in order store", lib.BytesToString(order.OrderId)) + return ErrOrderNotVerified(lib.BytesToString(order.OrderId), err) + } + // update the last height this order was submitted + // TODO is this the proper way to get the root height? + wOrder.LastSubmitHeight = qc.Header.RootHeight + // save this update to disk + err = o.orderStore.WriteOrder(wOrder, types.LockOrderType) + if err != nil { + o.log.Errorf("[ORACLE-COMMIT] Failed to write order %s: %v", lib.BytesToString(order.OrderId), err) + o.metrics.UpdateOracleStoreErrorMetrics(1, 0, 0) + continue + } + o.log.Infof("[ORACLE-COMMIT] Updated last submit height for lock order %s: %d", lib.BytesToString(order.OrderId), qc.Header.RootHeight) + } + // Update the last submit height for all close orders in this certificate + for _, orderId := range qc.Results.Orders.CloseOrders { + // get order from order store + wOrder, err := o.orderStore.ReadOrder(orderId, types.CloseOrderType) + if err != nil { + o.log.Warnf("[ORACLE-COMMIT] Unable to find order %s in order store", lib.BytesToString(orderId)) + return ErrOrderNotVerified(lib.BytesToString(orderId), err) + } + // update the last height this order was submitted + // TODO is this the proper way to get the root height? + wOrder.LastSubmitHeight = qc.Header.RootHeight + // save this update to disk + err = o.orderStore.WriteOrder(wOrder, types.CloseOrderType) + if err != nil { + o.log.Errorf("[ORACLE-COMMIT] Failed to write close order %s: %v", lib.BytesToString(orderId), err) + o.metrics.UpdateOracleStoreErrorMetrics(1, 0, 0) + continue + } + o.log.Infof("[ORACLE-COMMIT] Updated last submit height for close order %s: %d", lib.BytesToString(orderId), qc.Header.RootHeight) + } + + // update lifecycle metrics for committed orders + o.metrics.UpdateOracleLifecycleMetrics(0, 0, 0, len(qc.Results.Orders.LockOrders), len(qc.Results.Orders.CloseOrders)) + return +} + +// UpdateRootChainInfo examines the new root chain order book and prunes the local order store. +// The method performs the following operations: +// - saves the order book for use in processBlocks +// - removes lock orders from the store when corresponding sell orders are locked on the root chain +// - removes lock/close orders when their corresponding sell orders are no longer present +func (o *Oracle) UpdateRootChainInfo(info *lib.RootChainInfo) { + // oracle is disabled + if o == nil { + return + } + // track timing for metrics + startTime := time.Now() + // track pruned orders and store errors for metrics + var ordersPruned, storeRemoveErrors int + defer func() { + if o.metrics == nil { + return + } + elapsed := time.Since(startTime) + o.metrics.OrderBookUpdateTime.Observe(elapsed.Seconds()) + o.metrics.RootChainSyncTime.Observe(elapsed.Seconds()) + if ordersPruned > 0 { + o.metrics.UpdateOracleErrorMetrics(0, ordersPruned, 0) + } + if storeRemoveErrors > 0 { + o.metrics.UpdateOracleStoreErrorMetrics(0, 0, storeRemoveErrors) + } + }() + // lock order book while updating it and updating order store + o.orderBookMu.Lock() + defer o.orderBookMu.Unlock() + // remove history for orders no longer in order book + o.state.PruneHistory(info.Orders) + // log a warning for a nil order book + if info.Orders == nil { + o.log.Warn("[ORACLE-ORDERBOOK] Order book from root chain was nil") + return + } + // copy and save order book + o.orderBook = info.Orders.Copy() + for _, order := range o.orderBook.Orders { + o.log.Infof("[ORACLE-ORDERBOOK] ORDER %s", formatSellOrder(order)) + } + // get all lock orders from the order store + storedOrders, err := o.orderStore.GetAllOrderIds(types.LockOrderType) + if err != nil { + o.log.Errorf("[ORACLE-ORDERBOOK] Error getting all order ids: %s", err.Error()) + return + } + // examine stored lock orders and remove any not present in the order book + for _, id := range storedOrders { + // o.log.Debugf("UpdateRootChainInfo checking stored lock order %x for removal", id) + // attempt to get stored lock order from order book + order, err := o.orderBook.GetOrder(id) + if err != nil { + o.log.Errorf("[ORACLE-ORDERBOOK] Error getting order from order book: %s", err.Error()) + continue + } + // remove lock order from store if one of the following conditions is met: + // - corresponding sell order was not found in the root chain order book + // - root chain sell order is locked + switch { + case order == nil: + o.log.Infof("[ORACLE-ORDERBOOK] Order %x no longer in order book, removing lock order from store", id) + case order.BuyerSendAddress != nil: + o.log.Infof("[ORACLE-ORDERBOOK] Order %x is locked in order book, removing lock order from store", order.Id) + default: + // neither condition was met, do not remove this order + // continue processing remaining stored orders + continue + } + // remove lock order from the store + err = o.orderStore.RemoveOrder(id, types.LockOrderType) + if err != nil { + o.log.Errorf("[ORACLE-ORDERBOOK] Error removing order from order store: %s", err.Error()) + storeRemoveErrors++ + } else { + ordersPruned++ + } + } + // get all close orders from the order store + storedOrders, err = o.orderStore.GetAllOrderIds(types.CloseOrderType) + if err != nil { + o.log.Errorf("[ORACLE-ORDERBOOK] Error getting all order ids: %s", err.Error()) + return + } + // examine every stored close order and remove it if is no long present in the order book + for _, id := range storedOrders { + // o.log.Debugf("UpdateRootChainInfo checking stored close order %x for removal", id) + // attempt to get stored close order from order book + order, err := o.orderBook.GetOrder(id) + if err != nil { + o.log.Errorf("[ORACLE-ORDERBOOK] Error getting order from order book: %s", err.Error()) + continue + } + // remove close order from store if it was not found in the order book + if order == nil { + o.log.Infof("[ORACLE-ORDERBOOK] Removing close order %x from store", id) + err := o.orderStore.RemoveOrder(id, types.CloseOrderType) + if err != nil { + o.log.Errorf("[ORACLE-ORDERBOOK] Error removing order from order store: %s", err.Error()) + storeRemoveErrors++ + } else { + ordersPruned++ + } + } + } +} + +// WitnessedOrders returns witnessed orders that match orders in the order book +// When the block proposer produces a block proposal it uses the orders returned here to build the proposed block +// TODO watch for conflicts while syncing ethereum block, prooducer might resubmit order +func (o *Oracle) WitnessedOrders(orderBook *lib.OrderBook, rootHeight uint64) ([]*lib.LockOrder, [][]byte) { + lockOrders := []*lib.LockOrder{} + closeOrders := [][]byte{} + // oracle is disabled + if o == nil { + return lockOrders, closeOrders + } + // get current heights for context + safeHeight := o.state.GetSafeHeight() + // track statistics for summary log + var stats struct { + lockChecked, lockSubmitting, lockHeldSafe, lockHeldDelay int + closeChecked, closeSubmitting, closeHeldSafe, closeHeldDelay int + } + // entry log with key context + // o.log.Debugf("[ORACLE-SUBMIT] Checking orders: orderBook=%d orders, rootHeight=%d, safeHeight=%d", + // len(orderBook.Orders), rootHeight, safeHeight) + // loop through the order book searching the order store for lock/close orders witnessed by this node + for _, order := range orderBook.Orders { + orderId := lib.BytesToString(order.Id) + // process unlocked sell order + if !order.IsLocked() { + stats.lockChecked++ + // try to find a lock order + wOrder, err := o.orderStore.ReadOrder(order.Id, types.LockOrderType) + if err != nil { + if err.Code() != CodeReadOrder { + o.log.Errorf("[ORACLE-SUBMIT] Failed to read lock order %s: %v", orderId, err) + } + continue + } + // check if the witnessed order is from a safe block (has sufficient confirmations) + if wOrder.WitnessedHeight > safeHeight { + blocksUntilSafe := wOrder.WitnessedHeight - safeHeight + o.log.Debugf("[ORACLE-SUBMIT] Lock order %s held: awaiting safe height (witnessed=%d, safe=%d, need %d more blocks)", + orderId, wOrder.WitnessedHeight, safeHeight, blocksUntilSafe) + stats.lockHeldSafe++ + continue + } + // check whether this witnessed lock order should be submitted in the next proposed block + if !o.state.shouldSubmit(wOrder, rootHeight, o.config) { + // shouldSubmit logs the specific reason internally + stats.lockHeldDelay++ + continue + } + o.log.Infof("[ORACLE-SUBMIT] Submitting lock order %s (witnessed=%d)", orderId, wOrder.WitnessedHeight) + stats.lockSubmitting++ + // submit this witnessed lock order by returning it in the lockOrders slice + lockOrders = append(lockOrders, wOrder.LockOrder) + } else { + stats.closeChecked++ + // process locked orders - look for witnessed close orders + wOrder, err := o.orderStore.ReadOrder(order.Id, types.CloseOrderType) + if err != nil { + if err.Code() != CodeReadOrder { + o.log.Errorf("[ORACLE-SUBMIT] Failed to read close order %s: %v", orderId, err) + } + // No witnessed order is a normal condition, do not log + continue + } + // check if the witnessed order is from a safe block (has sufficient confirmations) + if wOrder.WitnessedHeight > safeHeight { + blocksUntilSafe := wOrder.WitnessedHeight - safeHeight + o.log.Debugf("[ORACLE-SUBMIT] Close order %s held: awaiting safe height (witnessed=%d, safe=%d, need %d more blocks)", + orderId, wOrder.WitnessedHeight, safeHeight, blocksUntilSafe) + stats.closeHeldSafe++ + continue + } + // check whether this witnessed close order should be submitted in the next proposed block + if !o.state.shouldSubmit(wOrder, rootHeight, o.config) { + // shouldSubmit logs the specific reason internally + stats.closeHeldDelay++ + continue + } + // update the last height this order was submitted + wOrder.LastSubmitHeight = rootHeight + // update the witnessed order in the store + err = o.orderStore.WriteOrder(wOrder, types.CloseOrderType) + if err != nil { + o.log.Errorf("[ORACLE-SUBMIT] Failed to write close order %s: %v", orderId, err) + o.metrics.UpdateOracleStoreErrorMetrics(1, 0, 0) + continue + } + o.log.Infof("[ORACLE-SUBMIT] Submitting close order %s (witnessed=%d)", orderId, wOrder.WitnessedHeight) + stats.closeSubmitting++ + // submit this witnessed close order by returning it in the closeOrders slice + closeOrders = append(closeOrders, wOrder.OrderId) + } + } + // summary log with full picture + o.log.Infof("[ORACLE-SUBMIT] rootHeight=%d: lock[checked=%d submitting=%d heldSafe=%d heldDelay=%d] close[checked=%d submitting=%d heldSafe=%d heldDelay=%d]", + rootHeight, + stats.lockChecked, stats.lockSubmitting, stats.lockHeldSafe, stats.lockHeldDelay, + stats.closeChecked, stats.closeSubmitting, stats.closeHeldSafe, stats.closeHeldDelay) + // update submitted orders metrics + totalSubmitted := stats.lockSubmitting + stats.closeSubmitting + o.metrics.UpdateOracleOrderMetrics(0, 0, totalSubmitted, 0, 0) + // update submission tracking metrics + totalHeldSafe := stats.lockHeldSafe + stats.closeHeldSafe + o.metrics.UpdateOracleSubmissionMetrics(totalHeldSafe, 0, 0, 0, 0) + // update orders awaiting confirmation gauge + if o.metrics != nil && o.metrics.OrdersAwaitingConfirmation != nil { + o.metrics.OrdersAwaitingConfirmation.Set(float64(totalHeldSafe)) + } + return lockOrders, closeOrders +} + +// updateMetrics updates various oracle metrics with current state information +func (o *Oracle) updateMetrics() { + // exit if empty + if o.metrics == nil { + return + } + // get current state metrics + safeHeight := o.state.GetSafeHeight() + lastHeight := o.state.GetLastHeight() + sourceHeight := o.state.sourceChainHeight + // get submission history sizes + lockOrderSubmissionsSize := len(o.state.lockOrderSubmissions) + closeOrderSubmissionsSize := len(o.state.closeOrderSubmissions) + // get order store counts + lockOrderIds, err := o.orderStore.GetAllOrderIds(types.LockOrderType) + lockOrders := 0 + if err == nil { + lockOrders = len(lockOrderIds) + } + closeOrderIds, err := o.orderStore.GetAllOrderIds(types.CloseOrderType) + closeOrders := 0 + if err == nil { + closeOrders = len(closeOrderIds) + } + // update height metrics + o.metrics.UpdateOracleHeightMetrics(lastHeight, safeHeight, sourceHeight, 0) + // update state metrics + o.metrics.UpdateOracleStateMetrics(safeHeight, sourceHeight, lockOrderSubmissionsSize, closeOrderSubmissionsSize) + // update store metrics + o.metrics.UpdateOracleStoreMetrics(lockOrders, closeOrders) +} + +// formatSellOrder formats a SellOrder with hex-encoded byte fields for logging +func formatSellOrder(o *lib.SellOrder) string { + return fmt.Sprintf( + "Id:%x Committee:%d Data:%x AmountForSale:%d RequestedAmount:%d SellerReceive:%x SellerSend:%x", + o.Id, o.Committee, o.Data, o.AmountForSale, o.RequestedAmount, + o.SellerReceiveAddress, o.SellersSendAddress, + ) +} diff --git a/cmd/rpc/oracle/oracle_test.go b/cmd/rpc/oracle/oracle_test.go new file mode 100644 index 000000000..d6228b1f5 --- /dev/null +++ b/cmd/rpc/oracle/oracle_test.go @@ -0,0 +1,1508 @@ +package oracle + +import ( + "encoding/hex" + "fmt" + "math/big" + "os" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockOrderStore implements OrderStore interface for testing purposes +type mockOrderStore struct { + // lockOrders stores witnessed orders for lock order type + lockOrders map[string]*types.WitnessedOrder + // closeOrders stores witnessed orders for close order type + closeOrders map[string]*types.WitnessedOrder + // lockOrders stores witnessed orders for lock order type + archiveLockOrders map[string]*types.WitnessedOrder + // closeOrders stores witnessed orders for close order type + archivedCloseOrders map[string]*types.WitnessedOrder + // mutex to protect concurrent access + rwLock sync.RWMutex +} + +// NewMockOrderStore creates a new MockOrderStore instance +func NewMockOrderStore() *mockOrderStore { + return &mockOrderStore{ + lockOrders: make(map[string]*types.WitnessedOrder), + closeOrders: make(map[string]*types.WitnessedOrder), + archiveLockOrders: make(map[string]*types.WitnessedOrder), + archivedCloseOrders: make(map[string]*types.WitnessedOrder), + rwLock: sync.RWMutex{}, + } +} + +// VerifyOrder verifies the order with order id is present in the store +func (m *mockOrderStore) VerifyOrder(order *types.WitnessedOrder, orderType types.OrderType) lib.ErrorI { + m.rwLock.RLock() + defer m.rwLock.RUnlock() + + // validate parameters + if err := m.validateOrderParameters(order.OrderId, orderType); err != nil { + return ErrValidateOrder(err) + } + + // get the appropriate map based on order type + orderMap := m.getOrderMap(orderType) + key := hex.EncodeToString(order.OrderId) + + // check if order exists + storedOrder, exists := orderMap[key] + if !exists { + return ErrVerifyOrder(fmt.Errorf("order not found")) + } + + // compare lock order + if !order.LockOrder.Equals(storedOrder.LockOrder) { + return ErrVerifyOrder(fmt.Errorf("lock order not equal")) + } + // compare close order + if !order.CloseOrder.Equals(storedOrder.CloseOrder) { + return ErrVerifyOrder(fmt.Errorf("close order not equal")) + } + + return nil +} + +// WriteOrder writes an order to the appropriate map +func (m *mockOrderStore) WriteOrder(order *types.WitnessedOrder, orderType types.OrderType) lib.ErrorI { + m.rwLock.Lock() + defer m.rwLock.Unlock() + + // validate parameters + if err := m.validateOrderParameters(order.OrderId, orderType); err != nil { + return ErrValidateOrder(err) + } + + // get the appropriate map based on order type + orderMap := m.getOrderMap(orderType) + key := hex.EncodeToString(order.OrderId) + + // store the order + orderMap[key] = order + return nil +} + +// ReadOrder reads an order from the appropriate map +func (m *mockOrderStore) ReadOrder(orderId []byte, orderType types.OrderType) (*types.WitnessedOrder, lib.ErrorI) { + m.rwLock.RLock() + defer m.rwLock.RUnlock() + // validate parameters + if err := m.validateOrderParameters(orderId, orderType); err != nil { + return nil, ErrValidateOrder(err) + } + // get the appropriate map based on order type + orderMap := m.getOrderMap(orderType) + key := hex.EncodeToString(orderId) + // retrieve the order + storedOrder, exists := orderMap[key] + if !exists { + return nil, ErrReadOrder(fmt.Errorf("order not found")) + } + + return storedOrder, nil +} + +// RemoveOrder removes an order from the appropriate map +func (m *mockOrderStore) RemoveOrder(orderId []byte, orderType types.OrderType) lib.ErrorI { + m.rwLock.Lock() + defer m.rwLock.Unlock() + + // validate parameters + if err := m.validateOrderParameters(orderId, orderType); err != nil { + return ErrValidateOrder(err) + } + + // get the appropriate map based on order type + orderMap := m.getOrderMap(orderType) + key := hex.EncodeToString(orderId) + + // check if order exists + if _, exists := orderMap[key]; !exists { + return ErrRemoveOrder(fmt.Errorf("order not found")) + } + + // remove the order + delete(orderMap, key) + return nil +} + +// GetAllOrderIds gets all order ids present in the store for a specific order type +func (m *mockOrderStore) GetAllOrderIds(orderType types.OrderType) ([][]byte, lib.ErrorI) { + m.rwLock.RLock() + defer m.rwLock.RUnlock() + + // validate order type + if orderType != types.LockOrderType && orderType != types.CloseOrderType { + return nil, ErrVerifyOrder(fmt.Errorf("invalid order type: %s", orderType)) + } + + // get the appropriate map based on order type + orderMap := m.getOrderMap(orderType) + + // collect all order ids + var orderIds [][]byte + for key := range orderMap { + id, err := hex.DecodeString(key) + if err != nil { + continue // skip invalid keys + } + orderIds = append(orderIds, id) + } + + return orderIds, nil +} + +// ArchiveOrder archives a witnessed order (mock implementation - does nothing) +func (m *mockOrderStore) ArchiveOrder(order *types.WitnessedOrder, orderType types.OrderType) lib.ErrorI { + // Mock implementation - just return nil for now + return nil +} + +// getOrderMap returns the appropriate map based on order type +func (m *mockOrderStore) getOrderMap(orderType types.OrderType) map[string]*types.WitnessedOrder { + switch orderType { + case types.LockOrderType: + return m.lockOrders + case types.CloseOrderType: + return m.closeOrders + default: + return nil + } +} + +// validateOrderParameters validates order id and order type +func (m *mockOrderStore) validateOrderParameters(orderId []byte, orderType types.OrderType) error { + // orderId cannot be nil + if orderId == nil { + return fmt.Errorf("order id cannot be nil") + } + // verify order id length + // if len(orderId) != orderIdLenBytes { + // return fmt.Errorf("order id invalid length") + // } + // validate order type + if orderType != types.LockOrderType && orderType != types.CloseOrderType { + return fmt.Errorf("invalid order type: %s", orderType) + } + return nil +} + +type mockBlock struct { + number uint64 + hash string + parentHash string + transactions []types.TransactionI +} + +func (m *mockBlock) Number() uint64 { + return m.number +} + +func (m *mockBlock) Hash() string { + return m.hash +} + +func (m *mockBlock) ParentHash() string { + return m.parentHash +} + +func (m *mockBlock) Transactions() []types.TransactionI { + return m.transactions +} + +type mockTransaction struct { + blockchain string + from string + to string + data []byte + hash string + order *types.WitnessedOrder + tokenTransfer types.TokenTransfer +} + +func (m *mockTransaction) Blockchain() string { + return m.blockchain +} + +func (m *mockTransaction) From() string { + return m.from +} + +func (m *mockTransaction) To() string { + return m.to +} + +func (m *mockTransaction) Data() []byte { + return m.data +} + +func (m *mockTransaction) Hash() string { + return m.hash +} + +func (m *mockTransaction) Order() *types.WitnessedOrder { + return m.order +} + +func (m *mockTransaction) TokenTransfer() types.TokenTransfer { + return m.tokenTransfer +} + +func createMockBlockWithTransactions(blockNumber uint64, blockHash string, transactions []types.TransactionI) types.BlockI { + return &mockBlock{ + number: blockNumber, + hash: blockHash, + transactions: transactions, + } +} + +// createSellOrder creates a sell order with an optional buyer receive address and seller receive address +func createSellOrder(orderIdHex string, data string, buyerReceiveAddress ...string) *lib.SellOrder { + orderIdBytes := []byte(orderIdHex) + sellOrder := &lib.SellOrder{ + Id: orderIdBytes, + } + if data != "" { + b, err := lib.StringToBytes(strings.TrimPrefix(data, "0x")) + if err != nil { + panic(err) + } + sellOrder.Data = b + } + if len(buyerReceiveAddress) > 0 && len(buyerReceiveAddress[0]) > 0 { + sellOrder.BuyerReceiveAddress = []byte(buyerReceiveAddress[0]) + } + return sellOrder +} + +// createSellOrderWithSellerAddr creates a sell order with seller receive address +func createSellOrderWithSellerAddr(orderIdHex string, buyerReceiveAddress string, sellerReceiveAddress string) *lib.SellOrder { + orderIdBytes := []byte(orderIdHex) + sellOrder := &lib.SellOrder{ + Id: orderIdBytes, + BuyerReceiveAddress: []byte(buyerReceiveAddress), + SellerReceiveAddress: []byte(sellerReceiveAddress), + } + return sellOrder +} + +// createWitnessedLockOrder creates a witnessed lock order with an optional buyerAddress +func createWitnessedLockOrder(id string, buyerAddress ...string) *types.WitnessedOrder { + var addressValue string + if len(buyerAddress) > 0 { + addressValue = buyerAddress[0] + } + return &types.WitnessedOrder{ + OrderId: []byte(id), + LockOrder: &lib.LockOrder{ + OrderId: []byte(id), + BuyerReceiveAddress: []byte(addressValue), + }, + } +} + +// createWitnessedCloseOrder creates a witnessed close order with an optional chainId +func createWitnessedCloseOrder(id string, chainId ...uint64) *types.WitnessedOrder { + var chainIdValue uint64 + if len(chainId) > 0 { + chainIdValue = chainId[0] + } + + return &types.WitnessedOrder{ + OrderId: []byte(id), + CloseOrder: &lib.CloseOrder{ + OrderId: []byte(id), + ChainId: chainIdValue, + CloseOrder: true, + }, + } +} + +func createOrderStore(orders ...*types.WitnessedOrder) *mockOrderStore { + mockStore := NewMockOrderStore() + for _, order := range orders { + if order.LockOrder != nil { + err := mockStore.WriteOrder(order, types.LockOrderType) + if err != nil { + panic(fmt.Sprintf("failed to write lock order for test: %v", err)) + } + } + if order.CloseOrder != nil { + err := mockStore.WriteOrder(order, types.CloseOrderType) + if err != nil { + panic(fmt.Sprintf("failed to write close order for test: %v", err)) + } + } + } + return mockStore +} + +func withCommittee(order *lib.SellOrder, committee uint64) *lib.SellOrder { + order.Committee = committee + return order +} + +func withData(order *lib.SellOrder, dataHex string) *lib.SellOrder { + b, err := lib.StringToBytes(strings.TrimPrefix(dataHex, "0x")) + if err != nil { + panic(err) + } + order.Data = b + return order +} + +func createOrderBook(orders ...*lib.SellOrder) *lib.OrderBook { + orderBook := &lib.OrderBook{} + for _, order := range orders { + orderBook.Orders = append(orderBook.Orders, order) + } + return orderBook +} + +func TestOracle_ValidateProposedOrders(t *testing.T) { + orders := func(lockOrderIDs []string, closeOrderIDs []string) *lib.Orders { + lockOrders := make([]*lib.LockOrder, len(lockOrderIDs)) + for i, id := range lockOrderIDs { + lockOrders[i] = &lib.LockOrder{ + OrderId: []byte(id), + } + } + closeOrders := make([][]byte, len(closeOrderIDs)) + for i, id := range closeOrderIDs { + closeOrders[i] = []byte(id) + } + return &lib.Orders{ + LockOrders: lockOrders, + CloseOrders: closeOrders, + } + } + + tests := []struct { + name string + orders *lib.Orders + orderStore types.OrderStore + expectedError bool + errorContains string + }{ + { + name: "nil orders should return no error", + orders: nil, + expectedError: false, + orderStore: createOrderStore(), + }, + { + name: "no orders should return no error", + orders: orders(nil, nil), + orderStore: createOrderStore(createWitnessedLockOrder("lock1")), + expectedError: false, + }, + { + name: "valid lock orders should pass validation", + orders: orders([]string{"lock1", "lock2"}, nil), + orderStore: createOrderStore( + createWitnessedLockOrder("lock1"), + createWitnessedLockOrder("lock2"), + ), + expectedError: false, + }, + { + name: "valid close orders should pass validation", + orders: orders(nil, []string{"close1", "close2"}), + orderStore: createOrderStore( + createWitnessedCloseOrder("close1", 1), + createWitnessedCloseOrder("close2", 1), + ), + expectedError: false, + }, + { + name: "valid mixed orders should pass validation", + orders: orders([]string{"lock1"}, []string{"close1"}), + orderStore: createOrderStore( + createWitnessedLockOrder("lock1"), + createWitnessedCloseOrder("close1", 1), + ), + expectedError: false, + }, + { + name: "lock order verification failure should return error", + orders: orders([]string{"lock1"}, nil), + orderStore: createOrderStore( + createWitnessedLockOrder("lock1", "address"), + ), + expectedError: true, + errorContains: "order not verified", + }, + { + name: "close order verification failure should return error", + orders: orders(nil, []string{"close1"}), + orderStore: createOrderStore( + createWitnessedCloseOrder("close1"), // missing chain id + ), + expectedError: true, + errorContains: "order not verified", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create Oracle instance + tempDir, _ := os.MkdirTemp("", "oracle_test") + defer os.RemoveAll(tempDir) + oracle := &Oracle{ + orderStore: tt.orderStore, + state: NewOracleState(filepath.Join(tempDir, "test_state"), lib.NewDefaultLogger()), + committee: 1, + log: lib.NewDefaultLogger(), + } + + // Execute test + err := oracle.ValidateProposedOrders(tt.orders) + + // Verify results + if tt.expectedError { + require.Error(t, err, "expected error but got nil") + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + require.NoError(t, err) + } + }) + } +} + +func TestOracle_WitnessedOrders(t *testing.T) { + buyerReceiveAddress := "0034567890123456789012345678901234567800" + contractAddress := "0x1234567890123456789012345678901234567890" + + orderIdOne := "order1" + orderIdTwo := "order2" + + tests := []struct { + name string + orderStore types.OrderStore + orderBook *lib.OrderBook + orderBookOrders []*lib.SellOrder + storeLockOrders map[string]*lib.LockOrder + storeCloseOrders map[string]*lib.CloseOrder + expectedLockOrdersLen int + expectedCloseOrdersLen int + expectedLockOrderIds []string + expectedCloseOrderIds []string + }{ + { + name: "orders exist in store but not in order book. both lock and close orders should not be included in proposed block", + orderBook: &lib.OrderBook{}, + orderBookOrders: []*lib.SellOrder{}, + orderStore: createOrderStore( + createWitnessedLockOrder(orderIdOne), + createWitnessedCloseOrder(orderIdTwo), + ), + expectedLockOrdersLen: 0, + expectedCloseOrdersLen: 0, + expectedLockOrderIds: []string{}, + expectedCloseOrderIds: []string{}, + }, + { + name: "orders exist in order book but not in store.", + orderBook: createOrderBook( + createSellOrder(orderIdOne, "", ""), + createSellOrder(orderIdTwo, contractAddress, buyerReceiveAddress), + ), + orderStore: NewMockOrderStore(), + expectedLockOrdersLen: 0, + expectedCloseOrdersLen: 0, + expectedLockOrderIds: []string{}, + expectedCloseOrderIds: []string{}, + }, + { + name: "matching stored lock order, should be included", + orderBook: createOrderBook( + createSellOrder(orderIdOne, "", ""), + ), + orderStore: createOrderStore( + createWitnessedLockOrder(orderIdOne), + createWitnessedLockOrder(orderIdTwo), + ), + expectedLockOrdersLen: 1, + expectedCloseOrdersLen: 0, + expectedLockOrderIds: []string{orderIdOne}, + expectedCloseOrderIds: []string{}, + }, + { + name: "matching close order exists in store and order book. should be included", + orderBook: createOrderBook( + createSellOrder(orderIdOne, contractAddress, buyerReceiveAddress), + ), + orderStore: createOrderStore( + createWitnessedCloseOrder(orderIdOne), + createWitnessedCloseOrder(orderIdTwo), + ), + expectedLockOrdersLen: 0, + expectedCloseOrdersLen: 1, + expectedLockOrderIds: []string{}, + expectedCloseOrderIds: []string{orderIdOne}, + }, + { + name: "unlocked order exists in order book and matching lock order exists in store. should be included", + orderBook: createOrderBook( + createSellOrder(orderIdOne, "", ""), + ), + orderStore: createOrderStore( + createWitnessedLockOrder(orderIdOne), + ), + expectedLockOrdersLen: 1, + expectedCloseOrdersLen: 0, + expectedLockOrderIds: []string{orderIdOne}, + expectedCloseOrderIds: []string{}, + }, + { + name: "locked order exists in order book and matching close order exists in store. should be included", + orderBook: createOrderBook( + createSellOrder(orderIdOne, contractAddress, buyerReceiveAddress), + ), + orderStore: createOrderStore( + createWitnessedCloseOrder(orderIdOne), + ), + expectedLockOrdersLen: 0, + expectedCloseOrdersLen: 1, + expectedLockOrderIds: []string{}, + expectedCloseOrderIds: []string{orderIdOne}, + }, + { + name: "mixed scenario with multiple orders", + orderBook: createOrderBook( + createSellOrder(orderIdOne, "", ""), + createSellOrder(orderIdTwo, contractAddress, buyerReceiveAddress), + ), + orderStore: createOrderStore( + createWitnessedLockOrder(orderIdOne), + createWitnessedCloseOrder(orderIdTwo), + ), + expectedLockOrdersLen: 1, + expectedCloseOrdersLen: 1, + expectedLockOrderIds: []string{orderIdOne}, + expectedCloseOrderIds: []string{orderIdTwo}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := lib.NewDefaultLogger() + oracle := &Oracle{ + orderStore: tt.orderStore, + state: NewOracleState("file", log), + log: log, + } + // 100 to specify a high enough root height that shouldSubmit always passes + witnessedLockOrders, witnessedCloseOrders := oracle.WitnessedOrders(tt.orderBook, 100) + if len(witnessedLockOrders) != tt.expectedLockOrdersLen { + t.Errorf("expected %d lock orders, got %d", tt.expectedLockOrdersLen, len(witnessedLockOrders)) + } + if len(witnessedCloseOrders) != tt.expectedCloseOrdersLen { + t.Errorf("expected %d close orders, got %d", tt.expectedCloseOrdersLen, len(witnessedCloseOrders)) + } + for i, expectedId := range tt.expectedLockOrderIds { + if i < len(witnessedLockOrders) { + actualId := fmt.Sprintf("%x", witnessedLockOrders[i].OrderId) + expectedIdHex := fmt.Sprintf("%x", []byte(expectedId)) + if actualId != expectedIdHex { + t.Errorf("expected lock order id %s, got %s", expectedIdHex, actualId) + } + } + } + for i, expectedId := range tt.expectedCloseOrderIds { + if i < len(witnessedCloseOrders) { + actualId := fmt.Sprintf("%x", witnessedCloseOrders[i]) + expectedIdHex := fmt.Sprintf("%x", []byte(expectedId)) + if actualId != expectedIdHex { + t.Errorf("expected close order id %s, got %s", expectedIdHex, actualId) + } + } + } + }) + } +} + +func TestOracle_UpdateRootChainInfo(t *testing.T) { + // Test data builders + newSellOrder := func(id string) *lib.SellOrder { + return &lib.SellOrder{Id: []byte(id)} + } + + newOrderBook := func(orderIds ...string) *lib.OrderBook { + orders := make([]*lib.SellOrder, len(orderIds)) + for i, id := range orderIds { + orders[i] = newSellOrder(id) + } + return &lib.OrderBook{Orders: orders} + } + + tests := []struct { + name string + storedLock []string + storedClose []string + orderBookIds []string + expectedLock []string + expectedClose []string + }{ + { + name: "removes lock orders not in order book", + storedLock: []string{"lock1", "lock2"}, + storedClose: []string{}, + orderBookIds: []string{"lock1"}, + expectedLock: []string{"lock1"}, + expectedClose: []string{}, + }, + { + name: "removes close orders not in order book", + storedLock: []string{}, + storedClose: []string{"close1", "close2"}, + orderBookIds: []string{"close1"}, + expectedLock: []string{}, + expectedClose: []string{"close1"}, + }, + { + name: "removes all orders when order book is empty", + storedLock: []string{"lock1", "lock2"}, + storedClose: []string{"close1", "close2"}, + orderBookIds: []string{}, + expectedLock: []string{}, + expectedClose: []string{}, + }, + { + name: "keeps orders present in order book", + storedLock: []string{"lock1", "lock3"}, + storedClose: []string{"close1", "close3"}, + orderBookIds: []string{"lock1", "close1"}, + expectedLock: []string{"lock1"}, + expectedClose: []string{"close1"}, + }, + { + name: "handles empty stored orders", + storedLock: []string{}, + storedClose: []string{}, + orderBookIds: []string{"lock1", "close1"}, + expectedLock: []string{}, + expectedClose: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup + mockStore := &mockOrderStore{ + lockOrders: make(map[string]*types.WitnessedOrder), + closeOrders: make(map[string]*types.WitnessedOrder), + archiveLockOrders: make(map[string]*types.WitnessedOrder), + archivedCloseOrders: make(map[string]*types.WitnessedOrder), + } + + // Populate store with test data + for _, id := range tt.storedLock { + order := &types.WitnessedOrder{ + OrderId: []byte(id), + } + mockStore.WriteOrder(order, types.LockOrderType) + } + for _, id := range tt.storedClose { + order := &types.WitnessedOrder{ + OrderId: []byte(id), + } + mockStore.WriteOrder(order, types.CloseOrderType) + } + + orderBook := newOrderBook(tt.orderBookIds...) + oracle := &Oracle{ + orderStore: mockStore, + orderBook: orderBook, + state: NewOracleState("", lib.NewDefaultLogger()), + log: lib.NewDefaultLogger(), + } + + // Execute + info := &lib.RootChainInfo{ + Orders: orderBook, + } + oracle.UpdateRootChainInfo(info) + + // Verify + ids, _ := mockStore.GetAllOrderIds(types.LockOrderType) + assertOrderIds(t, "lock", ids, tt.expectedLock) + ids, _ = mockStore.GetAllOrderIds(types.CloseOrderType) + assertOrderIds(t, "close", ids, tt.expectedClose) + }) + } +} + +func assertOrderIds(t *testing.T, orderType string, actual [][]byte, expected []string) { + t.Helper() + + if len(actual) != len(expected) { + t.Errorf("expected %d %s orders, got %d", len(expected), orderType, len(actual)) + return + } + + expectedSet := make(map[string]bool) + for _, id := range expected { + expectedSet[id] = true + } + + for _, actualId := range actual { + if !expectedSet[string(actualId)] { + t.Errorf("unexpected %s order %s found in store", orderType, string(actualId)) + } + } +} + +func TestOracle_processBlock(t *testing.T) { + buyerReceiveAddress := "0034567890123456789012345678901234567800" + contractAddress := "0x1234567890123456789012345678901234567890" + + transactionWithOrder := func(order *types.WitnessedOrder, toAddress ...string) *mockTransaction { + t := &mockTransaction{ + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: new(big.Int), + }, + order: order, + } + if len(toAddress) > 0 { + t.to = toAddress[0] + } + return t + } + + tests := []struct { + name string + block types.BlockI + orderStore *mockOrderStore + orderBook *lib.OrderBook + expectedLockOrderIds [][]byte + expectedCloseOrderIds [][]byte + }{ + { + name: "should process block with no transactions", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{}, + ), + orderStore: createOrderStore(), + orderBook: createOrderBook(), + }, + { + name: "should skip transactions without orders", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{}, + ), + orderStore: createOrderStore(), + orderBook: createOrderBook(), + }, + { + name: "should process valid lock order transaction", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{ + transactionWithOrder(createWitnessedLockOrder("order1", buyerReceiveAddress)), + }, + ), + orderStore: createOrderStore(), + orderBook: createOrderBook(createSellOrder("order1", "", buyerReceiveAddress)), + expectedLockOrderIds: [][]byte{[]byte("order1")}, + }, + { + name: "should process valid close order transaction", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{ + // create transaction with To address as contract address + transactionWithOrder(createWitnessedCloseOrder("order1"), contractAddress), + }, + ), + orderStore: createOrderStore(), + // create sell order with contractAdcress as data + orderBook: createOrderBook(createSellOrder("order1", contractAddress, buyerReceiveAddress)), + expectedCloseOrderIds: [][]byte{[]byte("order1")}, + }, + { + name: "should process multiple valid transactions", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{ + transactionWithOrder(createWitnessedLockOrder("lock1", buyerReceiveAddress)), + transactionWithOrder(createWitnessedCloseOrder("close1"), contractAddress), + &mockTransaction{}, + }, + ), + orderStore: createOrderStore(), + orderBook: createOrderBook( + createSellOrder("lock1", contractAddress, buyerReceiveAddress), + createSellOrder("close1", contractAddress, buyerReceiveAddress), + ), + expectedLockOrderIds: [][]byte{[]byte("lock1")}, + expectedCloseOrderIds: [][]byte{[]byte("close1")}, + }, + { + name: "should skip transactions when order not found in order book", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{ + transactionWithOrder(createWitnessedLockOrder("lock1")), + }, + ), + orderStore: createOrderStore(), + orderBook: createOrderBook(), + }, + { + name: "should skip transactions when validation fails", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{ + transactionWithOrder(createWitnessedCloseOrder("close1")), + }, + ), + orderStore: createOrderStore(), + orderBook: createOrderBook( + createSellOrder("lock1", "", buyerReceiveAddress), + ), + }, + { + name: "should handle mixed successful and failed transactions", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{ + transactionWithOrder(createWitnessedLockOrder("lock1", buyerReceiveAddress)), + transactionWithOrder(createWitnessedLockOrder("nonexistent")), + transactionWithOrder(createWitnessedCloseOrder("close1"), contractAddress), + &mockTransaction{}, + }, + ), + orderStore: createOrderStore(), + orderBook: createOrderBook( + createSellOrder("lock1", contractAddress, buyerReceiveAddress), + createSellOrder("close1", contractAddress, buyerReceiveAddress), + ), + expectedLockOrderIds: [][]byte{[]byte("lock1")}, + expectedCloseOrderIds: [][]byte{[]byte("close1")}, + }, + { + name: "should not overwrite existing order in store", + block: createMockBlockWithTransactions( + 100, + "0xblockhash", + []types.TransactionI{ + transactionWithOrder(&types.WitnessedOrder{ + OrderId: []byte("order1"), + // these heights are different in the incoming transaction + WitnessedHeight: 200, + LastSubmitHeight: 150, + LockOrder: &lib.LockOrder{ + BuyerReceiveAddress: []byte(buyerReceiveAddress), + OrderId: []byte("order1"), + }, + }), + }, + ), + orderStore: createOrderStore(&types.WitnessedOrder{ + OrderId: []byte("order1"), + WitnessedHeight: 100, + LastSubmitHeight: 50, + LockOrder: &lib.LockOrder{ + BuyerReceiveAddress: []byte(buyerReceiveAddress), + OrderId: []byte("order1"), + }, + }), + orderBook: createOrderBook(createSellOrder("order1", "", buyerReceiveAddress)), + expectedLockOrderIds: [][]byte{[]byte("order1")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + oracle := &Oracle{ + orderStore: tt.orderStore, + orderBook: tt.orderBook, + log: lib.NewDefaultLogger(), + } + + // Execute the method under test + err := oracle.processBlock(tt.block) + if err != nil { + panic(err) + } + for _, expectedId := range tt.expectedLockOrderIds { + storedOrder, ok := tt.orderStore.lockOrders[hex.EncodeToString(expectedId)] + if !ok { + t.Errorf("expected lock order id %v not found", string(expectedId)) + } + // Special case: verify existing order wasn't overwritten + if tt.name == "should not overwrite existing order in store" && storedOrder != nil { + if storedOrder.WitnessedHeight != 100 || storedOrder.LastSubmitHeight != 50 { + t.Errorf("existing order was overwritten: expected WitnessedHeight=100, LastSubmitHeight=50, got WitnessedHeight=%d, LastSubmitHeight=%d", + storedOrder.WitnessedHeight, storedOrder.LastSubmitHeight) + } + } + } + for _, expectedId := range tt.expectedCloseOrderIds { + _, ok := tt.orderStore.closeOrders[hex.EncodeToString(expectedId)] + if !ok { + t.Errorf("expected close order id %v not found", string(expectedId)) + } + } + + // Check for unexpected lock orders when none were expected + if len(tt.expectedLockOrderIds) == 0 { + for orderId := range tt.orderStore.lockOrders { + t.Errorf("unexpected lock order id %v found when none were expected", orderId) + } + } + // Check for unexpected close orders when none were expected + if len(tt.expectedCloseOrderIds) == 0 { + for orderId := range tt.orderStore.closeOrders { + t.Errorf("unexpected close order id %v found when none were expected", orderId) + } + } + }) + } +} + +func TestOracle_validateLockOrder(t *testing.T) { + oracle := &Oracle{} + + // Base valid orders for comparison + baseOrderID := []byte("order123") + baseChainID := uint64(1) + baseBuyerReceiveAddr := []byte("receive_addr") + baseBuyerSendAddr := []byte("send_addr") + baseDeadline := uint64(1234567890) + + baseLockOrder := &lib.LockOrder{ + OrderId: baseOrderID, + ChainId: baseChainID, + BuyerReceiveAddress: baseBuyerReceiveAddr, + BuyerSendAddress: baseBuyerSendAddr, + BuyerChainDeadline: baseDeadline, + } + + baseSellOrder := &lib.SellOrder{ + Id: baseOrderID, + Committee: baseChainID, + BuyerReceiveAddress: baseBuyerReceiveAddr, + BuyerSendAddress: baseBuyerSendAddr, + BuyerChainDeadline: baseDeadline, + } + + tests := []struct { + name string + lockOrder *lib.LockOrder + sellOrder *lib.SellOrder + wantErr bool + errMsg string + }{ + { + name: "valid matching orders", + lockOrder: baseLockOrder, + sellOrder: baseSellOrder, + wantErr: false, + }, + { + name: "mismatched order IDs", + lockOrder: &lib.LockOrder{ + OrderId: []byte("different_id"), + ChainId: baseChainID, + BuyerReceiveAddress: baseBuyerReceiveAddr, + BuyerSendAddress: baseBuyerSendAddr, + BuyerChainDeadline: baseDeadline, + }, + sellOrder: baseSellOrder, + wantErr: true, + errMsg: "lock order ID does not match sell order ID", + }, + { + name: "mismatched chain ID and committee", + lockOrder: &lib.LockOrder{ + OrderId: baseOrderID, + ChainId: 999, + BuyerReceiveAddress: baseBuyerReceiveAddr, + BuyerSendAddress: baseBuyerSendAddr, + BuyerChainDeadline: baseDeadline, + }, + sellOrder: baseSellOrder, + wantErr: true, + errMsg: "lock order chain ID does not match sell order committee", + }, + { + name: "nil order ID in lock order", + lockOrder: &lib.LockOrder{ + OrderId: nil, + ChainId: baseChainID, + BuyerReceiveAddress: baseBuyerReceiveAddr, + BuyerSendAddress: baseBuyerSendAddr, + BuyerChainDeadline: baseDeadline, + }, + sellOrder: baseSellOrder, + wantErr: true, + errMsg: "lock order ID does not match sell order ID", + }, + { + name: "empty byte slices", + lockOrder: &lib.LockOrder{ + OrderId: []byte{}, + ChainId: baseChainID, + BuyerReceiveAddress: []byte{}, + BuyerSendAddress: []byte{}, + BuyerChainDeadline: baseDeadline, + }, + sellOrder: &lib.SellOrder{ + Id: []byte{}, + Committee: baseChainID, + BuyerReceiveAddress: []byte{}, + BuyerSendAddress: []byte{}, + BuyerChainDeadline: baseDeadline, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := oracle.validateLockOrder(tt.lockOrder, tt.sellOrder) + + if tt.wantErr { + if err == nil { + t.Errorf("validateLockOrder() expected error but got nil") + return + } + if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("validateLockOrder() error message = %v, want %v", err.Error(), tt.errMsg) + } + } else { + if err != nil { + t.Errorf("validateLockOrder() unexpected error = %v", err) + } + } + }) + } +} + +func TestOracle_validateCloseOrder(t *testing.T) { + oracle := &Oracle{} + + // Base test data + baseOrderId := []byte("order-123") + baseChainId := uint64(1) + baseRequestedAmount := uint64(1000) + baseTo := common.HexToAddress("1234567890abcdef") + + baseSellerReceiveAddress := []byte("seller_receive_addr") + baseSellerReceiveAddressHex := fmt.Sprintf("%x", baseSellerReceiveAddress) + baseSellOrder := &lib.SellOrder{ + Id: baseOrderId, + Committee: baseChainId, + RequestedAmount: baseRequestedAmount, + Data: baseTo.Bytes(), + SellerReceiveAddress: baseSellerReceiveAddress, + } + + baseCloseOrder := &lib.CloseOrder{ + OrderId: baseOrderId, + ChainId: baseChainId, + } + + baseTx := &mockTransaction{ + to: baseTo.String(), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(int64(baseRequestedAmount)), + RecipientAddress: baseSellerReceiveAddressHex, + }, + } + + tests := []struct { + name string + closeOrder *lib.CloseOrder + sellOrder *lib.SellOrder + tx types.TransactionI + expectError bool + errorMsg string + }{ + { + name: "valid close order", + closeOrder: baseCloseOrder, + sellOrder: baseSellOrder, + tx: baseTx, + expectError: false, + }, + { + name: "sell order data does not match transaction recipient", + closeOrder: baseCloseOrder, + sellOrder: &lib.SellOrder{ + Id: baseOrderId, + Committee: baseChainId, + RequestedAmount: baseRequestedAmount, + Data: []byte("0xdifferentaddress"), + SellerReceiveAddress: baseSellerReceiveAddress, + }, + tx: baseTx, + expectError: true, + errorMsg: "sell order data field does not match transaction recipient", + }, + { + name: "token transfer recipient does not match seller receive address", + closeOrder: baseCloseOrder, + sellOrder: baseSellOrder, + tx: &mockTransaction{ + to: baseTo.String(), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(int64(baseRequestedAmount)), + RecipientAddress: fmt.Sprintf("%x", []byte("different_recipient_address")), + }, + }, + expectError: true, + errorMsg: "tokens not transferred to sell receive address", + }, + { + name: "error converting recipient address to bytes", + closeOrder: baseCloseOrder, + sellOrder: baseSellOrder, + tx: &mockTransaction{ + to: baseTo.String(), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(int64(baseRequestedAmount)), + RecipientAddress: "invalid_hex_address_gg", + }, + }, + expectError: true, + errorMsg: "error converting recipient address to bytes", + }, + { + name: "close order ID does not match sell order ID", + closeOrder: &lib.CloseOrder{ + OrderId: []byte("different-order-id"), + ChainId: baseChainId, + }, + sellOrder: baseSellOrder, + tx: baseTx, + expectError: true, + errorMsg: "close order ID does not match sell order ID", + }, + { + name: "close order chain ID does not match sell order committee", + closeOrder: &lib.CloseOrder{ + OrderId: baseOrderId, + ChainId: uint64(999), + }, + sellOrder: baseSellOrder, + tx: baseTx, + expectError: true, + errorMsg: "close order chain ID does not match sell order committee", + }, + { + name: "token transfer amount is nil", + closeOrder: baseCloseOrder, + sellOrder: baseSellOrder, + tx: &mockTransaction{ + to: baseTo.String(), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: nil, + RecipientAddress: baseSellerReceiveAddressHex, + }, + }, + expectError: true, + errorMsg: "token transfer amount cannot be nil", + }, + { + name: "transfer amount does not match requested amount", + closeOrder: baseCloseOrder, + sellOrder: baseSellOrder, + tx: &mockTransaction{ + to: baseTo.String(), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(500), // Different from requested amount + RecipientAddress: baseSellerReceiveAddressHex, + }, + }, + expectError: true, + errorMsg: fmt.Sprintf("transfer amount %d does not match requested amount %d", 500, baseRequestedAmount), + }, + { + name: "zero amounts are valid", + closeOrder: baseCloseOrder, + sellOrder: &lib.SellOrder{ + Id: baseOrderId, + Committee: baseChainId, + RequestedAmount: 0, + Data: baseTo.Bytes(), + SellerReceiveAddress: baseSellerReceiveAddress, + }, + tx: &mockTransaction{ + to: baseTo.String(), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(0), + RecipientAddress: baseSellerReceiveAddressHex, + }, + }, + expectError: false, + }, + { + name: "large amounts are valid", + closeOrder: baseCloseOrder, + sellOrder: &lib.SellOrder{ + Id: baseOrderId, + Committee: baseChainId, + RequestedAmount: 18446744073709551615, // Max uint64 + Data: baseTo.Bytes(), + SellerReceiveAddress: baseSellerReceiveAddress, + }, + tx: &mockTransaction{ + to: baseTo.String(), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(0).SetUint64(18446744073709551615), + RecipientAddress: baseSellerReceiveAddressHex, + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := oracle.validateCloseOrder(tt.closeOrder, tt.sellOrder, tt.tx) + + if tt.expectError { + if err == nil { + t.Errorf("validateCloseOrder() expected error but got nil") + return + } + + // Check if the error message contains the expected text + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("validateCloseOrder() error = %v, expected to contain %v", err.Error(), tt.errorMsg) + } + } else { + if err != nil { + t.Errorf("validateCloseOrder() unexpected error = %v", err) + } + } + }) + } +} + +// TestOracle_MultiTokenSupport verifies that USDC and USDT orders can be processed on the same committee +// This test demonstrates that the oracle is token-agnostic and works with any ERC20 token +func TestOracle_MultiTokenSupport(t *testing.T) { + // Import known token addresses from eth package + usdcContract := "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + usdtContract := "0xdAC17F958D2ee523a2206206994597C13D831ec7" + + buyerReceiveAddress := "0034567890123456789012345678901234567800" + sellerReceiveAddress := "seller_receive_address" + + // Create order IDs for different tokens + usdcOrderId := "usdc_order_1" + usdtOrderId := "usdt_order_1" + + tests := []struct { + name string + orderBook *lib.OrderBook + witnessedOrders []*types.WitnessedOrder + transactions []types.TransactionI + expectedLockOrders int + expectedCloseOrders int + expectedStoreContains []string + }{ + { + name: "USDC and USDT lock orders on same committee", + orderBook: createOrderBook( + createSellOrder(usdcOrderId, usdcContract, ""), + createSellOrder(usdtOrderId, usdtContract, ""), + ), + witnessedOrders: []*types.WitnessedOrder{ + createWitnessedLockOrder(usdcOrderId, buyerReceiveAddress), + createWitnessedLockOrder(usdtOrderId, buyerReceiveAddress), + }, + transactions: []types.TransactionI{ + &mockTransaction{ + to: usdcContract, + order: createWitnessedLockOrder(usdcOrderId, buyerReceiveAddress), + }, + &mockTransaction{ + to: usdtContract, + order: createWitnessedLockOrder(usdtOrderId, buyerReceiveAddress), + }, + }, + expectedLockOrders: 2, + expectedCloseOrders: 0, + expectedStoreContains: []string{usdcOrderId, usdtOrderId}, + }, + { + name: "USDC and USDT close orders on same committee", + orderBook: createOrderBook( + withData(withCommittee(createSellOrderWithSellerAddr(usdcOrderId, buyerReceiveAddress, sellerReceiveAddress), 1), usdcContract), + withData(withCommittee(createSellOrderWithSellerAddr(usdtOrderId, buyerReceiveAddress, sellerReceiveAddress), 1), usdtContract), + ), + witnessedOrders: []*types.WitnessedOrder{ + createWitnessedCloseOrder(usdcOrderId, 1), + createWitnessedCloseOrder(usdtOrderId, 1), + }, + transactions: []types.TransactionI{ + &mockTransaction{ + to: usdcContract, + order: createWitnessedCloseOrder(usdcOrderId, 1), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(100000000), + RecipientAddress: fmt.Sprintf("%x", []byte(sellerReceiveAddress)), + }, + }, + &mockTransaction{ + to: usdtContract, + order: createWitnessedCloseOrder(usdtOrderId, 1), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(100000000), + RecipientAddress: fmt.Sprintf("%x", []byte(sellerReceiveAddress)), + }, + }, + }, + expectedLockOrders: 0, + expectedCloseOrders: 2, + expectedStoreContains: []string{usdcOrderId, usdtOrderId}, + }, + { + name: "mixed USDC lock and USDT close on same committee", + orderBook: createOrderBook( + createSellOrder(usdcOrderId, usdcContract, ""), + withData(withCommittee(createSellOrderWithSellerAddr(usdtOrderId, buyerReceiveAddress, sellerReceiveAddress), 1), usdtContract), + ), + witnessedOrders: []*types.WitnessedOrder{ + createWitnessedLockOrder(usdcOrderId, buyerReceiveAddress), + createWitnessedCloseOrder(usdtOrderId, 1), + }, + transactions: []types.TransactionI{ + &mockTransaction{ + to: usdcContract, + order: createWitnessedLockOrder(usdcOrderId, buyerReceiveAddress), + }, + &mockTransaction{ + to: usdtContract, + order: createWitnessedCloseOrder(usdtOrderId, 1), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(100000000), + RecipientAddress: fmt.Sprintf("%x", []byte(sellerReceiveAddress)), + }, + }, + }, + expectedLockOrders: 1, + expectedCloseOrders: 1, + expectedStoreContains: []string{usdcOrderId, usdtOrderId}, + }, + { + name: "USDT close order validation with exact amount match", + orderBook: createOrderBook( + // USDT with 6 decimals: 100 USDT = 100,000,000 base units + withData(withCommittee(createSellOrderWithSellerAddr(usdtOrderId, buyerReceiveAddress, sellerReceiveAddress), 1), usdtContract), + ), + witnessedOrders: []*types.WitnessedOrder{ + createWitnessedCloseOrder(usdtOrderId, 1), + }, + transactions: []types.TransactionI{ + &mockTransaction{ + to: usdtContract, + order: createWitnessedCloseOrder(usdtOrderId, 1), + tokenTransfer: types.TokenTransfer{ + TokenBaseAmount: big.NewInt(100000000), // 100 USDT + RecipientAddress: fmt.Sprintf("%x", []byte(sellerReceiveAddress)), + }, + }, + }, + expectedLockOrders: 0, + expectedCloseOrders: 1, + expectedStoreContains: []string{usdtOrderId}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create oracle with empty order store + mockStore := NewMockOrderStore() + tempDir, _ := os.MkdirTemp("", "oracle_multi_token_test") + defer os.RemoveAll(tempDir) + + oracle := &Oracle{ + orderStore: mockStore, + orderBook: tt.orderBook, + state: NewOracleState(filepath.Join(tempDir, "test_state"), lib.NewDefaultLogger()), + committee: 1, // Single committee handling both USDC and USDT + log: lib.NewDefaultLogger(), + metrics: nil, // Metrics methods are nil-safe + } + + // Process block with transactions + if len(tt.transactions) > 0 { + block := createMockBlockWithTransactions(100, "0xblockhash", tt.transactions) + // Update sell orders with requested amounts for close order validation + for _, order := range tt.orderBook.Orders { + order.RequestedAmount = 100000000 // 100 tokens with 6 decimals + } + err := oracle.processBlock(block) + if err != nil { + t.Fatalf("processBlock failed: %v", err) + } + } + + // Get witnessed orders from oracle + lockOrders, closeOrders := oracle.WitnessedOrders(tt.orderBook, 100) + + // Verify lock order count + if len(lockOrders) != tt.expectedLockOrders { + t.Errorf("expected %d lock orders, got %d", tt.expectedLockOrders, len(lockOrders)) + } + + // Verify close order count + if len(closeOrders) != tt.expectedCloseOrders { + t.Errorf("expected %d close orders, got %d", tt.expectedCloseOrders, len(closeOrders)) + } + + // Verify orders are in store + for _, expectedId := range tt.expectedStoreContains { + // Check lock orders + if tt.expectedLockOrders > 0 { + _, err := mockStore.ReadOrder([]byte(expectedId), types.LockOrderType) + if err != nil { + // Try close orders if not a lock order + if tt.expectedCloseOrders > 0 { + _, err = mockStore.ReadOrder([]byte(expectedId), types.CloseOrderType) + if err != nil { + t.Errorf("expected order %s not found in store", expectedId) + } + } + } + } else if tt.expectedCloseOrders > 0 { + _, err := mockStore.ReadOrder([]byte(expectedId), types.CloseOrderType) + if err != nil { + t.Errorf("expected close order %s not found in store", expectedId) + } + } + } + + t.Logf("✓ Successfully processed %d USDC/USDT orders on committee %d", + len(lockOrders)+len(closeOrders), oracle.committee) + }) + } +} diff --git a/cmd/rpc/oracle/order_store.go b/cmd/rpc/oracle/order_store.go new file mode 100644 index 000000000..3364b0d93 --- /dev/null +++ b/cmd/rpc/oracle/order_store.go @@ -0,0 +1,391 @@ +package oracle + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" +) + +const ( + // tempSuffix is the suffix used for temporary files during atomic writes + tempSuffix = ".tmp" + // jsonExtension is the file extension for JSON files + jsonExtension = ".json" +) + +// OracleDiskStorage implements OrderStore interface for Ethereum order storage +type OracleDiskStorage struct { + // storagePath is the directory path for order storage + storagePath string + // absStoragePath is the absolute path used for validation + absStoragePath string + // absArchivePath is the absolute archive path used for validation + absArchivePath string + // logger is used for logging operations + logger lib.LoggerI + // mutex to protect concurrent access + rwLock sync.RWMutex +} + +// NewOracleDiskStorage creates a new OracleDiskStorage instance +func NewOracleDiskStorage(storagePath string, logger lib.LoggerI) (*OracleDiskStorage, error) { + // validate storage path is not empty + if storagePath == "" { + return nil, fmt.Errorf("storage path cannot be empty") + } + // validate logger is not nil + if logger == nil { + return nil, fmt.Errorf("logger cannot be nil") + } + + if strings.HasPrefix(storagePath, "~/") { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + storagePath = filepath.Join(home, storagePath[2:]) + } + + // get absolute path for secure validation + absStoragePath, err := filepath.Abs(storagePath) + if err != nil { + return nil, fmt.Errorf("failed to get absolute storage path: %w", err) + } + // clean the path to resolve any .. or . elements + absStoragePath = filepath.Clean(absStoragePath) + + // create storage directory if it doesn't exist + if err := os.MkdirAll(absStoragePath, 0755); err != nil { + return nil, fmt.Errorf("failed to create storage directory: %w", err) + } + // create archive directory structure + archiveDir := filepath.Join(absStoragePath, "archive") + if err := os.MkdirAll(archiveDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create archive directory: %w", err) + } + // get absolute archive path for validation + absArchivePath, err := filepath.Abs(archiveDir) + if err != nil { + return nil, fmt.Errorf("failed to get absolute archive path: %w", err) + } + absArchivePath = filepath.Clean(absArchivePath) + // create lock and close subdirectories in archive + lockArchiveDir := filepath.Join(archiveDir, "lock") + if err := os.MkdirAll(lockArchiveDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create lock archive directory: %w", err) + } + closeArchiveDir := filepath.Join(archiveDir, "close") + if err := os.MkdirAll(closeArchiveDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create close archive directory: %w", err) + } + // return new instance + return &OracleDiskStorage{ + storagePath: storagePath, + absStoragePath: absStoragePath, + absArchivePath: absArchivePath, + logger: logger, + rwLock: sync.RWMutex{}, + }, nil +} + +// VerifyOrder verifies the order with order id is present in the store +// this verifies the lock order or close order fields of the witnessed order, ignoring the other fields +func (e *OracleDiskStorage) VerifyOrder(order *types.WitnessedOrder, orderType types.OrderType) lib.ErrorI { + // validate parameters + if err := e.validateOrderParameters(order.OrderId, orderType); err != nil { + return ErrValidateOrder(err) + } + // read the stored order + storedOrder, err := e.ReadOrder(order.OrderId, orderType) + if err != nil { + return ErrVerifyOrder(fmt.Errorf("failed to read stored order: %w", err)) + } + // compare lock order + if !order.LockOrder.Equals(storedOrder.LockOrder) { + return ErrVerifyOrder(fmt.Errorf("lock order not equal")) + } + // compare close order + if !order.CloseOrder.Equals(storedOrder.CloseOrder) { + return ErrVerifyOrder(fmt.Errorf("close order not equal")) + } + return nil +} + +// WriteOrder writes an order to disk with atomic write operation +func (e *OracleDiskStorage) WriteOrder(order *types.WitnessedOrder, orderType types.OrderType) lib.ErrorI { + e.rwLock.Lock() + defer e.rwLock.Unlock() + // validate parameters + if err := e.validateOrderParameters(order.OrderId, orderType); err != nil { + return ErrValidateOrder(err) + } + // build file path + bz, err := json.Marshal(order) + if err != nil { + return ErrMarshalOrder(err) + } + filePath, err := e.buildFilePath(order.OrderId, orderType) + if err != nil { + return ErrWriteOrder(err) + } + // create temporary file for atomic write + tempPath := filePath + tempSuffix + // write data to temporary file + if err := os.WriteFile(tempPath, bz, 0644); err != nil { + return ErrWriteOrder(fmt.Errorf("failed to write temporary file: %w", err)) + } + // atomically rename temporary file to final filename + if err := os.Rename(tempPath, filePath); err != nil { + // cleanup temporary file on failure + os.Remove(tempPath) + return ErrWriteOrder(fmt.Errorf("failed to rename temporary file: %w", err)) + } + // e.logger.Debugf("OrderStore: Wrote %d bytes to %s", len(bz), filePath) + return nil +} + +// ReadOrder reads an order from disk +func (e *OracleDiskStorage) ReadOrder(orderId []byte, orderType types.OrderType) (*types.WitnessedOrder, lib.ErrorI) { + e.rwLock.RLock() + defer e.rwLock.RUnlock() + // validate parameters + if err := e.validateOrderParameters(orderId, orderType); err != nil { + return nil, ErrValidateOrder(err) + } + // build file path + filePath, err := e.buildFilePath(orderId, orderType) + if err != nil { + return nil, ErrReadOrder(err) + } + // e.logger.Debugf("OrderStore: Attempting to read %s", filePath) + // read file contents + data, err := os.ReadFile(filePath) + if err != nil { + return nil, ErrReadOrder(err) + } + // e.logger.Debugf("OrderStore: Read %d bytes from %s", len(data), filePath) + // unmarshal the order + order := &types.WitnessedOrder{} + err = json.Unmarshal(data, order) + if err != nil { + return nil, ErrUnmarshalOrder(err) + } + return order, nil +} + +// RemoveOrder removes an order from disk +func (e *OracleDiskStorage) RemoveOrder(orderId []byte, orderType types.OrderType) lib.ErrorI { + e.rwLock.Lock() + defer e.rwLock.Unlock() + // validate parameters + if err := e.validateOrderParameters(orderId, orderType); err != nil { + return ErrValidateOrder(err) + } + // build file path + filePath, err := e.buildFilePath(orderId, orderType) + if err != nil { + return ErrRemoveOrder(err) + } + // remove the file + if err := os.Remove(filePath); err != nil { + return ErrRemoveOrder(err) + } + // e.logger.Debugf("OrderStore: Removed %s", filePath) + return nil +} + +// ArchiveOrder archives a witnessed order to the archive directory for historical retention +func (e *OracleDiskStorage) ArchiveOrder(order *types.WitnessedOrder, orderType types.OrderType) lib.ErrorI { + e.rwLock.Lock() + defer e.rwLock.Unlock() + // validate parameters + if err := e.validateOrderParameters(order.OrderId, orderType); err != nil { + return ErrValidateOrder(err) + } + // marshal order to JSON + bz, err := json.Marshal(order) + if err != nil { + return ErrMarshalOrder(err) + } + // build archive file path + archiveFilePath, err := e.buildArchiveFilePath(order.OrderId, orderType) + if err != nil { + return ErrWriteOrder(err) + } + // create temporary file for atomic write + tempPath := archiveFilePath + tempSuffix + // write data to temporary file + if err := os.WriteFile(tempPath, bz, 0644); err != nil { + return ErrWriteOrder(fmt.Errorf("failed to write archive temporary file: %w", err)) + } + // atomically rename temporary file to final filename + if err := os.Rename(tempPath, archiveFilePath); err != nil { + // cleanup temporary file on failure + os.Remove(tempPath) + return ErrWriteOrder(fmt.Errorf("failed to rename archive temporary file: %w", err)) + } + // e.logger.Debugf("OrderStore: Archived %d bytes to %s", len(bz), archiveFilePath) + return nil +} + +// GetAllOrderIds gets all order ids present in the store for a specific order type +func (e *OracleDiskStorage) GetAllOrderIds(orderType types.OrderType) ([][]byte, lib.ErrorI) { + e.rwLock.RLock() + defer e.rwLock.RUnlock() + // validate order type + if orderType != types.LockOrderType && orderType != types.CloseOrderType { + return nil, ErrVerifyOrder(fmt.Errorf("invalid order type: %s", orderType)) + } + // read directory contents + entries, err := os.ReadDir(e.storagePath) + if err != nil { + return nil, ErrReadOrder(err) + } + // collect order ids for the specified type + var orderIds [][]byte + orderTypeSuffix := fmt.Sprintf(".%s%s", string(orderType), jsonExtension) + // iterate through directory entries + for _, entry := range entries { + // skip directories + if entry.IsDir() { + continue + } + filename := entry.Name() + // check if filename matches the order type pattern + if strings.HasSuffix(filename, orderTypeSuffix) { + // extract order id from filename + orderId := strings.TrimSuffix(filename, orderTypeSuffix) + id, err := hex.DecodeString(orderId) + if err != nil { + e.logger.Errorf("[ORACLE-STORE] Failed to decode order id in filename: %s", err.Error()) + continue + } + orderIds = append(orderIds, id) + } + } + // e.logger.Debugf("OrderStore: All %s IDs %v", orderType, orderIds) + return orderIds, nil +} + +func (e *OracleDiskStorage) validateOrderParameters(orderId []byte, orderType types.OrderType) error { + // orderId cannot be nil + if orderId == nil { + return errors.New("order id cannot be nil") + } + if len(orderId) == 0 { + return errors.New("order id invalid length") + } + // validate order type + if orderType != types.LockOrderType && orderType != types.CloseOrderType { + return fmt.Errorf("invalid order type: %s", orderType) + } + return nil +} + +// buildFilePath builds a file path for an order JSON file +func (e *OracleDiskStorage) buildFilePath(orderId []byte, orderType types.OrderType) (string, error) { + // convert to hex string (orderId is already validated by caller) + orderIdHex := hex.EncodeToString(orderId) + // build filename with validated components + filename := fmt.Sprintf("%s.%s%s", orderIdHex, string(orderType), jsonExtension) + // use absolute path for security + filePath := filepath.Join(e.absStoragePath, filename) + // clean the final path to resolve any remaining path elements + filePath = filepath.Clean(filePath) + // ensure the resolved path is within the storage directory using absolute paths + if !e.isPathWithinDirectory(filePath, e.absStoragePath) { + return "", fmt.Errorf("path traversal attempt detected: resolved path outside storage directory") + } + return filePath, nil +} + +// buildArchiveFilePath builds a file path for an archived order JSON file +func (e *OracleDiskStorage) buildArchiveFilePath(orderId []byte, orderType types.OrderType) (string, error) { + // convert to hex string (orderId is already validated by caller) + orderIdHex := hex.EncodeToString(orderId) + // build filename with validated components + filename := fmt.Sprintf("%s.%s%s", orderIdHex, string(orderType), jsonExtension) + // determine archive subdirectory based on order type + var archiveSubDir string + switch orderType { + case types.LockOrderType: + archiveSubDir = "lock" + case types.CloseOrderType: + archiveSubDir = "close" + default: + return "", fmt.Errorf("invalid order type for archive: %s", orderType) + } + // validate subdirectory name + if err := e.validateFilename(archiveSubDir); err != nil { + return "", fmt.Errorf("invalid archive subdirectory: %w", err) + } + // build full archive path using absolute paths + archiveDir := filepath.Join(e.absArchivePath, archiveSubDir) + filePath := filepath.Join(archiveDir, filename) + // clean the final path to resolve any remaining path elements + filePath = filepath.Clean(filePath) + // ensure the resolved path is within the archive directory using absolute paths + if !e.isPathWithinDirectory(filePath, e.absArchivePath) { + return "", fmt.Errorf("path traversal attempt detected: resolved path outside archive directory") + } + return filePath, nil +} + +// validateFilename validates a filename component to prevent path traversal +func (e *OracleDiskStorage) validateFilename(filename string) error { + // check for empty filename + if filename == "" { + return errors.New("filename cannot be empty") + } + // check for path traversal sequences + if strings.Contains(filename, "..") { + return errors.New("filename cannot contain '..' sequences") + } + // check for path separators + if strings.ContainsAny(filename, "/\\") { + return errors.New("filename cannot contain path separators") + } + // check for null bytes + if strings.Contains(filename, "\x00") { + return errors.New("filename cannot contain null bytes") + } + // check for control characters + for _, r := range filename { + if r < 32 || r == 127 { + return errors.New("filename cannot contain control characters") + } + } + return nil +} + +// isPathWithinDirectory securely checks if a path is within a given directory +// This uses absolute paths and proper canonicalization to prevent bypass attempts +func (e *OracleDiskStorage) isPathWithinDirectory(targetPath, allowedDir string) bool { + // get absolute path of target + absTarget, err := filepath.Abs(targetPath) + if err != nil { + return false + } + // clean both paths to resolve symlinks and path elements + absTarget = filepath.Clean(absTarget) + allowedDir = filepath.Clean(allowedDir) + // ensure both paths end with separator for proper prefix checking + if !strings.HasSuffix(allowedDir, string(filepath.Separator)) { + allowedDir += string(filepath.Separator) + } + if !strings.HasSuffix(absTarget, string(filepath.Separator)) { + // for files, check if the directory part is within allowed directory + targetDir := filepath.Dir(absTarget) + string(filepath.Separator) + return strings.HasPrefix(targetDir, allowedDir) + } + // for directories, check direct prefix + return strings.HasPrefix(absTarget, allowedDir) +} diff --git a/cmd/rpc/oracle/order_store_test.go b/cmd/rpc/oracle/order_store_test.go new file mode 100644 index 000000000..e072b1482 --- /dev/null +++ b/cmd/rpc/oracle/order_store_test.go @@ -0,0 +1,826 @@ +package oracle + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testId = "53ecc91b68aba0e82ba09fbf205e4f81cc44b92b" + testId2 = "2222222222222222222222222222222222222222" + testId3 = "3333333333333333333333333333333333333333" + nonExistingId = "0000000000000000000000000000000000000000" +) + +// TestNewOracleDiskStorage tests the constructor for OracleDiskStorage +func TestNewOracleDiskStorage(t *testing.T) { + tests := []struct { + name string + storagePath string + wantErr bool + errMsg string + }{ + { + name: "valid parameters", + storagePath: "test_storage", + wantErr: false, + }, + { + name: "empty storage path", + storagePath: "", + wantErr: true, + errMsg: "storage path cannot be empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // cleanup test directory if it exists + if tt.storagePath != "" { + os.RemoveAll(tt.storagePath) + } + + // create new storage instance + storage, err := NewOracleDiskStorage(tt.storagePath, lib.NewDefaultLogger()) + + // check error expectation + if tt.wantErr { + if err == nil { + t.Errorf("NewOracleDiskStorage() expected error but got none") + } + if tt.errMsg != "" && err.Error() != tt.errMsg { + t.Errorf("NewOracleDiskStorage() error = %v, want %v", err.Error(), tt.errMsg) + } + return + } + + // check no error expected + if err != nil { + t.Errorf("NewOracleDiskStorage() unexpected error = %v", err) + return + } + + // verify storage instance is not nil + if storage == nil { + t.Errorf("NewOracleDiskStorage() returned nil storage") + } + + // verify storage path is set correctly + if storage.storagePath != tt.storagePath { + t.Errorf("NewOracleDiskStorage() storagePath = %v, want %v", storage.storagePath, tt.storagePath) + } + + // cleanup test directory + os.RemoveAll(tt.storagePath) + }) + } +} + +// TestOracleDiskStorage_VerifyOrder tests the VerifyOrder method +func TestOracleDiskStorage_VerifyOrder(t *testing.T) { + testId := "53ecc91b68aba0e82ba09fbf205e4f81cc44b92b" + testIdBytes, _ := hex.DecodeString(testId) + nonExistingId := "0000000000000000000000000000000000000000" + nonExistingIdBytes, _ := hex.DecodeString(nonExistingId) + + // create temporary directory for testing + tempDir, err := os.MkdirTemp("", "eth_storage_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // create storage instance + storage, err := NewOracleDiskStorage(tempDir, lib.NewDefaultLogger()) + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + + // create test witnessed order + testOrder := &types.WitnessedOrder{ + OrderId: testIdBytes, + WitnessedHeight: 100, + LastSubmitHeight: 50, + LockOrder: &lib.LockOrder{ /* test lock order fields */ }, + CloseOrder: &lib.CloseOrder{ /* test close order fields */ }, + } + + // create different order for mismatch test + differentOrder := &types.WitnessedOrder{ + OrderId: testIdBytes, + WitnessedHeight: 200, + LastSubmitHeight: 75, + LockOrder: &lib.LockOrder{ + ChainId: 1, + }, + CloseOrder: &lib.CloseOrder{ /* different close order fields */ }, + } + + // write test order first + err = storage.WriteOrder(testOrder, types.LockOrderType) + if err != nil { + t.Fatalf("failed to write test order: %v", err) + } + + tests := []struct { + name string + order *types.WitnessedOrder + orderType types.OrderType + wantErr bool + errContains string + }{ + { + name: "valid order with matching data", + order: testOrder, + orderType: types.LockOrderType, + wantErr: false, + }, + { + name: "valid order with mismatched data", + order: differentOrder, + orderType: types.LockOrderType, + wantErr: true, + errContains: "not equal", + }, + { + name: "non-existing order", + order: &types.WitnessedOrder{ + OrderId: nonExistingIdBytes, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + }, + orderType: types.LockOrderType, + wantErr: true, + errContains: "failed to read stored order", + }, + { + name: "nil order id", + order: &types.WitnessedOrder{ + OrderId: nil, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + }, + orderType: types.LockOrderType, + wantErr: true, + errContains: "cannot be nil", + }, + { + name: "invalid order type", + order: testOrder, + orderType: types.OrderType("invalid"), + wantErr: true, + errContains: "invalid order type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // call VerifyOrder method + err := storage.VerifyOrder(tt.order, tt.orderType) + + // check error expectation + if tt.wantErr { + if err == nil { + t.Errorf("VerifyOrder() expected error but got none") + return + } + if tt.errContains != "" { + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tt.errContains)) { + t.Errorf("VerifyOrder() error = %v, want error containing %v", err.Error(), tt.errContains) + } + } + return + } + + // check no error expected + if err != nil { + t.Errorf("VerifyOrder() unexpected error = %v", err) + return + } + }) + } +} + +// TestOracleDiskStorage_WriteOrder tests the WriteOrder method +func TestOracleDiskStorage_WriteOrder(t *testing.T) { + testId := "53ecc91b68aba0e82ba09fbf205e4f81cc44b92b" + testId2 := "2222222222222222222222222222222222222222" + testId3 := "3333333333333333333333333333333333333333" + testIdBytes, _ := hex.DecodeString(testId) + testId2Bytes, _ := hex.DecodeString(testId2) + testId3Bytes, _ := hex.DecodeString(testId3) + // create temporary directory for testing + tempDir, err := os.MkdirTemp("", "eth_storage_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // create storage instance + storage, err := NewOracleDiskStorage(tempDir, lib.NewDefaultLogger()) + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + + tests := []struct { + name string + order *types.WitnessedOrder + orderType types.OrderType + wantErr bool + errContains string + }{ + { + name: "valid lock order", + order: &types.WitnessedOrder{ + OrderId: testIdBytes, + WitnessedHeight: 100, + LastSubmitHeight: 50, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + }, + orderType: types.LockOrderType, + wantErr: false, + }, + { + name: "valid close order", + order: &types.WitnessedOrder{ + OrderId: testId2Bytes, + WitnessedHeight: 200, + LastSubmitHeight: 150, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + }, + orderType: types.CloseOrderType, + wantErr: false, + }, + { + name: "empty order id", + order: &types.WitnessedOrder{ + OrderId: []byte{}, + WitnessedHeight: 100, + LastSubmitHeight: 50, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + }, + orderType: types.LockOrderType, + wantErr: true, + errContains: "order id invalid length", + }, + { + name: "invalid order type", + order: &types.WitnessedOrder{ + OrderId: testId3Bytes, + WitnessedHeight: 100, + LastSubmitHeight: 50, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + }, + orderType: types.OrderType("invalid"), + wantErr: true, + errContains: "invalid order type: invalid", + }, + { + name: "duplicate order", + order: &types.WitnessedOrder{ + OrderId: testIdBytes, + WitnessedHeight: 300, + LastSubmitHeight: 250, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + }, + orderType: types.LockOrderType, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // call WriteOrder method + err := storage.WriteOrder(tt.order, tt.orderType) + + // check error expectation + if tt.wantErr { + if err == nil { + t.Errorf("WriteOrder() expected error but got none") + return + } + if tt.errContains != "" { + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tt.errContains)) { + t.Errorf("WriteOrder() error = %v, want error containing %v", err.Error(), tt.errContains) + } + } + return + } + + // check no error expected + if err != nil { + t.Errorf("WriteOrder() unexpected error = %v", err) + return + } + + // verify file was created + expectedFilename, _ := storage.buildFilePath(tt.order.OrderId, tt.orderType) + if _, err := os.Stat(expectedFilename); os.IsNotExist(err) { + t.Errorf("WriteOrder() file was not created: %v", expectedFilename) + } + o, err := storage.ReadOrder(tt.order.OrderId, tt.orderType) + if err != nil { + t.Errorf("ReadOrder() unexpected error = %v", err) + } + // Compare the retrieved order with the expected order + if diff := cmp.Diff(tt.order, o, + cmpopts.IgnoreUnexported(lib.LockOrder{}), + cmpopts.IgnoreUnexported(lib.CloseOrder{}), + ); diff != "" { + t.Errorf("ReadOrder() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestOracleDiskStorage_ReadOrder(t *testing.T) { + testId := "53ecc91b68aba0e82ba09fbf205e4f81cc44b92b" + testIdBytes, _ := hex.DecodeString(testId) + nonExistingId := "0000000000000000000000000000000000000000" + nonExistingIdBytes, _ := hex.DecodeString(nonExistingId) + + // create temporary directory for testing + tempDir, err := os.MkdirTemp("", "eth_storage_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // create storage instance + storage, err := NewOracleDiskStorage(tempDir, lib.NewDefaultLogger()) + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + + // create test witnessed order + testOrder := &types.WitnessedOrder{ + OrderId: testIdBytes, + WitnessedHeight: 12345, + LastSubmitHeight: 67890, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + } + + // write test order first + err = storage.WriteOrder(testOrder, types.LockOrderType) + if err != nil { + t.Fatalf("failed to write test order: %v", err) + } + + tests := []struct { + name string + orderId []byte + orderType types.OrderType + wantOrder *types.WitnessedOrder + wantErr bool + errContains string + }{ + { + name: "existing order", + orderId: testIdBytes, + orderType: types.LockOrderType, + wantOrder: testOrder, + wantErr: false, + }, + { + name: "non-existing order", + orderId: nonExistingIdBytes, + orderType: types.LockOrderType, + wantOrder: nil, + wantErr: true, + errContains: "no such file or directory", + }, + { + name: "empty order id", + orderId: []byte{}, + orderType: types.LockOrderType, + wantOrder: nil, + wantErr: true, + errContains: "order id invalid length", + }, + { + name: "invalid order type", + orderId: testIdBytes, + orderType: types.OrderType("invalid"), + wantOrder: nil, + wantErr: true, + errContains: "invalid order type: invalid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // call ReadOrder method + order, err := storage.ReadOrder(tt.orderId, tt.orderType) + + // check error expectation + if tt.wantErr { + if err == nil { + t.Errorf("ReadOrder() expected error but got none") + return + } + if tt.errContains != "" { + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tt.errContains)) { + t.Errorf("ReadOrder() error = %v, want error containing %v", err.Error(), tt.errContains) + } + } + return + } + + // check no error expected + if err != nil { + t.Errorf("ReadOrder() unexpected error = %v", err) + return + } + + // verify order matches expected + if order == nil { + t.Errorf("ReadOrder() returned nil order") + return + } + + if string(order.OrderId) != string(tt.wantOrder.OrderId) { + t.Errorf("ReadOrder() OrderId = %v, want %v", hex.EncodeToString(order.OrderId), hex.EncodeToString(tt.wantOrder.OrderId)) + } + + if order.WitnessedHeight != tt.wantOrder.WitnessedHeight { + t.Errorf("ReadOrder() WitnessedHeight = %v, want %v", order.WitnessedHeight, tt.wantOrder.WitnessedHeight) + } + + if order.LastSubmitHeight != tt.wantOrder.LastSubmitHeight { + t.Errorf("ReadOrder() LastSubmitHeight = %v, want %v", order.LastSubmitHeight, tt.wantOrder.LastSubmitHeight) + } + }) + } +} + +// TestOracleDiskStorage_RemoveOrder tests the RemoveOrder method +func TestOracleDiskStorage_RemoveOrder(t *testing.T) { + testId := "53ecc91b68aba0e82ba09fbf205e4f81cc44b92b" + testIdBytes, _ := hex.DecodeString(testId) + nonExistingId := "0000000000000000000000000000000000000000" + nonExistingIdBytes, _ := hex.DecodeString(nonExistingId) + // create temporary directory for testing + tempDir, err := os.MkdirTemp("", "eth_storage_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // create storage instance + storage, err := NewOracleDiskStorage(tempDir, lib.NewDefaultLogger()) + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + + // create test witnessed order + testOrder := &types.WitnessedOrder{ + OrderId: testIdBytes, + WitnessedHeight: 100, + LastSubmitHeight: 50, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + } + + tests := []struct { + name string + orderId []byte + orderType types.OrderType + setupOrder bool + wantErr bool + errContains string + }{ + { + name: "existing order", + orderId: testIdBytes, + orderType: types.OrderType(types.LockOrderType), + setupOrder: true, + wantErr: false, + }, + { + name: "non-existing order", + orderId: nonExistingIdBytes, + orderType: types.OrderType(types.LockOrderType), + setupOrder: false, + wantErr: true, + errContains: "no such file or directory", + }, + { + name: "empty order id", + orderId: []byte{}, + orderType: types.LockOrderType, + setupOrder: false, + wantErr: true, + errContains: "order id invalid length", + }, + { + name: "invalid order type", + orderId: testIdBytes, + orderType: types.OrderType("invalid"), + setupOrder: false, + wantErr: true, + errContains: "invalid order type: invalid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // setup order if needed + if tt.setupOrder { + err := storage.WriteOrder(testOrder, tt.orderType) + if err != nil { + t.Fatalf("failed to setup test order: %v", err) + } + } + + // call RemoveOrder method + err := storage.RemoveOrder(tt.orderId, tt.orderType) + + // check error expectation + if tt.wantErr { + if err == nil { + t.Errorf("RemoveOrder() expected error but got none") + return + } + if tt.errContains != "" { + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tt.errContains)) { + t.Errorf("RemoveOrder() error = %v, want error containing %v", err.Error(), tt.errContains) + } + } + return + } + + // check no error expected + if err != nil { + t.Errorf("RemoveOrder() unexpected error = %v", err) + return + } + + // verify file was removed + _, err = storage.ReadOrder(tt.orderId, tt.orderType) + if err == nil { + t.Errorf("RemoveOrder() file still exists after removal") + } + }) + } +} + +// TestOracleDiskStorage_GetAllOrderIds tests the GetAllOrderIds method +func TestOracleDiskStorage_GetAllOrderIds(t *testing.T) { + testId := "53ecc91b68aba0e82ba09fbf205e4f81cc44b92b" + testId2 := "2222222222222222222222222222222222222222" + testId3 := "3333333333333333333333333333333333333333" + testIdBytes, _ := hex.DecodeString(testId) + testId2Bytes, _ := hex.DecodeString(testId2) + testId3Bytes, _ := hex.DecodeString(testId3) + // create temporary directory for testing + tempDir, err := os.MkdirTemp("", "eth_storage_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // create storage instance + storage, err := NewOracleDiskStorage(tempDir, lib.NewDefaultLogger()) + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + + // create test witnessed orders + lockOrder1 := &types.WitnessedOrder{ + OrderId: testIdBytes, + WitnessedHeight: 100, + LastSubmitHeight: 50, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + } + lockOrder2 := &types.WitnessedOrder{ + OrderId: testId2Bytes, + WitnessedHeight: 200, + LastSubmitHeight: 150, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + } + lockOrder3 := &types.WitnessedOrder{ + OrderId: testId3Bytes, + WitnessedHeight: 300, + LastSubmitHeight: 250, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + } + closeOrder1 := &types.WitnessedOrder{ + OrderId: testIdBytes, + WitnessedHeight: 100, + LastSubmitHeight: 50, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + } + closeOrder2 := &types.WitnessedOrder{ + OrderId: testId2Bytes, + WitnessedHeight: 200, + LastSubmitHeight: 150, + LockOrder: &lib.LockOrder{}, + CloseOrder: &lib.CloseOrder{}, + } + + // setup test orders + lockOrders := []*types.WitnessedOrder{lockOrder1, lockOrder2, lockOrder3} + lockOrderIds := [][]byte{testIdBytes, testId2Bytes, testId3Bytes} + closeOrders := []*types.WitnessedOrder{closeOrder1, closeOrder2} + closeOrderIds := [][]byte{testIdBytes, testId2Bytes} + + // write lock orders + for _, order := range lockOrders { + err := storage.WriteOrder(order, types.LockOrderType) + if err != nil { + t.Fatalf("failed to write lock order: %v", err) + } + } + + // write close orders + for _, order := range closeOrders { + err := storage.WriteOrder(order, types.CloseOrderType) + if err != nil { + t.Fatalf("failed to write close order: %v", err) + } + } + + tests := []struct { + name string + orderType types.OrderType + expectedIds [][]byte + expectNil bool + }{ + { + name: "lock orders", + orderType: types.OrderType(types.LockOrderType), + expectedIds: lockOrderIds, + expectNil: false, + }, + { + name: "close orders", + orderType: types.OrderType(types.CloseOrderType), + expectedIds: closeOrderIds, + expectNil: false, + }, + { + name: "invalid order type", + orderType: types.OrderType("invalid"), + expectedIds: nil, + expectNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // call GetAllOrderIds method + orderIds, _ := storage.GetAllOrderIds(tt.orderType) + // check if nil expected + if tt.expectNil { + if orderIds != nil { + t.Errorf("GetAllOrderIds() expected nil but got %v", orderIds) + } + return + } + // verify count matches expected + if len(orderIds) != len(tt.expectedIds) { + t.Errorf("GetAllOrderIds() count = %v, want %v", len(orderIds), len(tt.expectedIds)) + return + } + // verify all expected ids are present + for _, expectedId := range tt.expectedIds { + found := false + for _, actualId := range orderIds { + if bytes.Equal(actualId, expectedId) { + found = true + break + } + } + if !found { + t.Errorf("GetAllOrderIds() missing expected id: %v", expectedId) + } + } + }) + } +} + +func TestOracleDiskStorage_ArchiveOrder(t *testing.T) { + tempDir := t.TempDir() + logger := lib.NewDefaultLogger() + storage, err := NewOracleDiskStorage(tempDir, logger) + require.NoError(t, err) + + orderIdHex := "53ecc91b68aba0e82ba09fbf205e4f81cc44b92b" // Use hex string + orderId, _ := hex.DecodeString(orderIdHex) + order := &types.WitnessedOrder{ + OrderId: orderId, + WitnessedHeight: 100, + LockOrder: &lib.LockOrder{ + OrderId: orderId, + BuyerReceiveAddress: []byte("test_buyer_address"), + }, + } + + tests := []struct { + name string + order *types.WitnessedOrder + orderType types.OrderType + wantErr bool + errMsg string + }{ + { + name: "valid lock order archive", + order: order, + orderType: types.LockOrderType, + wantErr: false, + }, + { + name: "valid close order archive", + order: &types.WitnessedOrder{ + OrderId: orderId, + WitnessedHeight: 200, + CloseOrder: &lib.CloseOrder{ + OrderId: orderId, + ChainId: 1, + CloseOrder: true, + }, + }, + orderType: types.CloseOrderType, + wantErr: false, + }, + { + name: "nil order id", + order: &types.WitnessedOrder{ + OrderId: nil, + }, + orderType: types.LockOrderType, + wantErr: true, + errMsg: "order id cannot be nil", + }, + { + name: "invalid order type", + order: order, + orderType: types.OrderType("invalid"), + wantErr: true, + errMsg: "invalid order type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := storage.ArchiveOrder(tt.order, tt.orderType) + + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + } else { + require.NoError(t, err) + + // Verify archive file exists in correct location + var expectedDir string + switch tt.orderType { + case types.LockOrderType: + expectedDir = "archive/lock" + case types.CloseOrderType: + expectedDir = "archive/close" + } + + filename := fmt.Sprintf("%s.%s.json", hex.EncodeToString(tt.order.OrderId), string(tt.orderType)) + archivePath := filepath.Join(tempDir, expectedDir, filename) + + // Check that archive file exists + _, err := os.Stat(archivePath) + assert.NoError(t, err, "archive file should exist") + + // Verify archive file contents + data, err := os.ReadFile(archivePath) + require.NoError(t, err) + + var archivedOrder types.WitnessedOrder + err = json.Unmarshal(data, &archivedOrder) + require.NoError(t, err) + + assert.Equal(t, tt.order.OrderId, archivedOrder.OrderId) + assert.Equal(t, tt.order.WitnessedHeight, archivedOrder.WitnessedHeight) + } + }) + } +} diff --git a/cmd/rpc/oracle/order_validator.go b/cmd/rpc/oracle/order_validator.go new file mode 100644 index 000000000..57a22326b --- /dev/null +++ b/cmd/rpc/oracle/order_validator.go @@ -0,0 +1,108 @@ +package oracle + +import ( + "fmt" + "strings" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/xeipuuv/gojsonschema" +) + +const ( + // length of valid order id strings + orderIdLen = 40 + // length of Canopy addresses in bytes + canopyAddressLenBytes = 20 + // JSON schema for lock order validation + lockOrderSchema = `{ + "type": "object", + "properties": { + "orderId": { + "type": "string", + "minLength": 40, + "maxLength": 40 + }, + "chain_id": { + "type": "integer" + }, + "buyerSendAddress": { + "type": "string" + }, + "buyerReceiveAddress": { + "type": "string" + }, + "buyerChainDeadline": { + "type": "integer", + "minimum": 1 + } + }, + "required": ["orderId", "chain_id", "buyerSendAddress", "buyerReceiveAddress", "buyerChainDeadline"], + "additionalProperties": false + }` + // JSON schema for close order validation + closeOrderSchema = `{ + "type": "object", + "properties": { + "orderId": { + "type": "string", + "minLength": 40, + "maxLength": 40 + }, + "chain_id": { + "type": "integer" + }, + "closeOrder": { + "type": "boolean", + "const": true + } + }, + "required": ["orderId", "chain_id", "closeOrder"], + "additionalProperties": false + }` +) + +// OrderValidator provides validation functionality for lock and close orders +type OrderValidator struct { + // JSON schema loaders for validation + lockOrderSchemaLoader gojsonschema.JSONLoader + closeOrderSchemaLoader gojsonschema.JSONLoader +} + +// NewOrderValidator creates a new OrderValidator instance with predefined schemas +func NewOrderValidator() *OrderValidator { + return &OrderValidator{ + lockOrderSchemaLoader: gojsonschema.NewStringLoader(lockOrderSchema), + closeOrderSchemaLoader: gojsonschema.NewStringLoader(closeOrderSchema), + } +} + +// validateOrderJsonSchema validates JSON using gojsonschema +func (v *OrderValidator) ValidateOrderJsonBytes(jsonBytes []byte, orderType types.OrderType) error { + var schemaLoader gojsonschema.JSONLoader + switch orderType { + case types.LockOrderType: + // use pre-created schema loader + schemaLoader = v.lockOrderSchemaLoader + case types.CloseOrderType: + // use pre-created schema loader + schemaLoader = v.closeOrderSchemaLoader + default: + return fmt.Errorf("error validating json bytes: Invalid order type") + } + // create document loader + documentLoader := gojsonschema.NewBytesLoader(jsonBytes) + // validate document against schema + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return fmt.Errorf("schema validation error: %w", err) + } + // check if validation passed + if !result.Valid() { + var errorMessages []string + for _, desc := range result.Errors() { + errorMessages = append(errorMessages, desc.String()) + } + return fmt.Errorf("schema validation failed: %s", strings.Join(errorMessages, "; ")) + } + return nil +} diff --git a/cmd/rpc/oracle/order_validator_test.go b/cmd/rpc/oracle/order_validator_test.go new file mode 100644 index 000000000..fe6563184 --- /dev/null +++ b/cmd/rpc/oracle/order_validator_test.go @@ -0,0 +1,249 @@ +package oracle + +import ( + "testing" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" +) + +func TestValidateCloseOrderJsonBytes(t *testing.T) { + tests := []struct { + name string + input []byte + requiredFields []string + expectError bool + }{ + { + name: "valid close order", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"closeOrder":true}`), + expectError: false, + }, + { + name: "valid close order but closeOrder false", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"closeOrder":false}`), + expectError: true, + }, + { + name: "valid close order with extra fields", + input: []byte(`{"orderId":"12345678901234567890","chain_id":1,"closeOrder":true,"status":"closed"}`), + expectError: true, + }, + { + name: "invalid JSON", + input: []byte(`{"orderId":"123456789012345678901234567890123456789"`), + expectError: true, + }, + { + name: "empty JSON", + input: []byte(`{}`), + expectError: true, + }, + { + name: "missing orderId", + input: []byte(`{"chain_id":1,"closeOrder":true}`), + expectError: true, + }, + { + name: "missing chain_id", + input: []byte(`{"orderId":"12345678901234567890","closeOrder":true}`), + expectError: true, + }, + { + name: "missing closeOrder", + input: []byte(`{"orderId":"12345678901234567890","chain_id":1}`), + expectError: true, + }, + { + name: "empty orderId", + input: []byte(`{"orderId":"","chain_id":1,"closeOrder":true}`), + expectError: true, + }, + { + name: "null orderId", + input: []byte(`{"orderId":null,"chain_id":1,"closeOrder":true}`), + expectError: true, + }, + { + name: "orderId as number", + input: []byte(`{"orderId":1234567890123456789012345678901234567890,"chain_id":1,"closeOrder":true}`), + expectError: true, + }, + { + name: "wrong length orderId", + input: []byte(`{"orderId":"123","chain_id":1,"closeOrder":true}`), + expectError: true, + }, + { + name: "just a string", + input: []byte(`not json`), + expectError: true, + }, + { + name: "empty byte array", + input: []byte(``), + expectError: true, + }, + { + name: "nil byte array", + input: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validator := NewOrderValidator() + var err error + err = validator.ValidateOrderJsonBytes(tt.input, types.CloseOrderType) + if (err != nil) != tt.expectError { + t.Errorf("ValidateOrderjsonBytes() error = %v, expectError %v", err, tt.expectError) + } + }) + } +} + +func TestValidateLockOrderJsonBytes(t *testing.T) { + tests := []struct { + name string + input []byte + requiredFields []string + expectError bool + }{ + { + name: "valid lock order", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: false, + }, + { + name: "valid lock order except object as a value", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":{},"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "invalid json - malformed", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890`), + expectError: true, + }, + { + name: "invalid json - missing closing brace", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1`), + expectError: true, + }, + { + name: "invalid json - missing quotes", + input: []byte(`{orderId:"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "invalid json - trailing comma", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890,}`), + expectError: true, + }, + { + name: "empty object", + input: []byte(`{}`), + expectError: true, + }, + { + name: "empty json", + input: []byte(``), + expectError: true, + }, + { + name: "null json", + input: []byte(`null`), + expectError: true, + }, + { + name: "array instead of object", + input: []byte(`["orderId","1234567890123456789012345678901234567890"]`), + expectError: true, + }, + { + name: "string instead of object", + input: []byte(`"not an object"`), + expectError: true, + }, + { + name: "number instead of object", + input: []byte(`123456`), + expectError: true, + }, + { + name: "boolean instead of object", + input: []byte(`true`), + expectError: true, + }, + { + name: "missing required field orderId", + input: []byte(`{"chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "missing required field chain_id", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "missing required field buyerSendAddress", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "missing required field buyerReceiveAddress", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "missing required field buyerChainDeadline", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890"}`), + expectError: true, + }, + { + name: "null values for required fields", + input: []byte(`{"orderId":null,"chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "empty string values", + input: []byte(`{"orderId":"","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "array as field value", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":["0x12345678901234567890"],"buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "nested object in field", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":{"address":"0x12345678901234567890"},"buyerChainDeadline":1234567890}`), + expectError: true, + }, + { + name: "extra fields with valid required fields", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":1234567890,"extraField":"value"}`), + expectError: true, + }, + { + name: "zero value for buyer chain deadline", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":0}`), + expectError: true, + }, + { + name: "negative value for buyer chain deadline", + input: []byte(`{"orderId":"1234567890123456789012345678901234567890","chain_id":1,"buyerSendAddress":"0x12345678901234567890","buyerReceiveAddress":"0x12345678901234567890","buyerChainDeadline":-1}`), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validator := NewOrderValidator() + var err error + err = validator.ValidateOrderJsonBytes(tt.input, types.LockOrderType) + if (err != nil) != tt.expectError { + t.Errorf("ValidateOrderjsonBytes() error = %v, expectError %v", err, tt.expectError) + } + }) + } +} diff --git a/cmd/rpc/oracle/state.go b/cmd/rpc/oracle/state.go new file mode 100644 index 000000000..1c443d834 --- /dev/null +++ b/cmd/rpc/oracle/state.go @@ -0,0 +1,301 @@ +package oracle + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" +) + +// OracleBlockState represents the last processed block +type OracleBlockState struct { + // Height is the last successfully processed block height + Height uint64 `json:"height"` + // Hash is the block hash for verification + Hash string `json:"hash"` + // ParentHash is the parent block hash for chain reorganization detection + ParentHash string `json:"parentHash"` + // Timestamp when the block was processed + Timestamp time.Time `json:"timestamp"` +} + +// OracleState manages block processing state, gap detection, and chain reorganization detection +type OracleState struct { + // sourceChainHeight is the last seen height for the source chain + sourceChainHeight uint64 + // stateSaveFile is the base path for state files + stateSaveFile string + // closeOrderSubmissions tracks the root height when each close order was submitted + closeOrderSubmissions map[string]uint64 + // lockOrderSubmissions tracks the root height when each lock order was submitted + lockOrderSubmissions map[string]uint64 + // safeHeight is the highest block height that has received sufficient confirmations to be considered safe + safeHeight uint64 + // rwLock protects all oracle state fields from concurrent access + rwLock sync.RWMutex + // logger for state management operations + log lib.LoggerI +} + +// NewOracleState creates a new OracleState instance +func NewOracleState(stateSaveFile string, logger lib.LoggerI) *OracleState { + // Ensure the state save file location exists + if err := os.MkdirAll(filepath.Dir(stateSaveFile), 0755); err != nil { + logger.Errorf("[ORACLE-STATE] failed to create directories for %s: %w", stateSaveFile, err) + } + return &OracleState{ + stateSaveFile: stateSaveFile, + lockOrderSubmissions: make(map[string]uint64), + closeOrderSubmissions: make(map[string]uint64), + rwLock: sync.RWMutex{}, + log: logger, + } +} + +// shouldSubmit determines if the current oracle state allows for submitting this order +// Performs all submission checks including lead time, resubmit delay, lock order restrictions, and history tracking +func (m *OracleState) shouldSubmit(order *types.WitnessedOrder, rootHeight uint64, config lib.OracleConfig) bool { + // protect all oracle state fields with write lock + m.rwLock.Lock() + defer m.rwLock.Unlock() + // convert order ID to string for use as map key + orderIdStr := lib.BytesToString(order.OrderId) + // propose lead time validation check + if m.sourceChainHeight < order.WitnessedHeight+config.ProposeDelayBlocks { + blocksNeeded := (order.WitnessedHeight + config.ProposeDelayBlocks) - m.sourceChainHeight + m.log.Infof("[ORACLE-STATE] Order %s held: propose delay (witnessed=%d, need %d more source chain blocks)", + orderIdStr, order.WitnessedHeight, blocksNeeded) + return false + } + // resubmit delay check + if rootHeight <= order.LastSubmitHeight+config.OrderResubmitDelayBlocks { + eligibleAt := order.LastSubmitHeight + config.OrderResubmitDelayBlocks + 1 + m.log.Infof("[ORACLE-STATE] Order %s held: resubmit delay (lastSubmit=%d, eligible at rootHeight=%d, current=%d)", + orderIdStr, order.LastSubmitHeight, eligibleAt, rootHeight) + return false + } + // lock order specific time restrictions + if order.LockOrder != nil { + // check if this lock order was previously submitted + if height, exists := m.lockOrderSubmissions[orderIdStr]; exists { + // test if already submitted at this root height + if height == rootHeight { + m.log.Infof("[ORACLE-STATE] Lock order %s held: already submitted at rootHeight=%d", orderIdStr, rootHeight) + return false + } + // calculate blocks since last submission + blocksSinceSubmission := rootHeight - height + // check if enough time has passed + if blocksSinceSubmission < config.LockOrderCooldownBlocks { + blocksNeeded := config.LockOrderCooldownBlocks - blocksSinceSubmission + m.log.Infof("[ORACLE-STATE] Lock order %s held: cooldown (lastSubmit=%d, need %d more blocks)", + orderIdStr, height, blocksNeeded) + return false + } + m.log.Debugf("[ORACLE-STATE] Lock order %s submitted at height %d, %d blocks ago, allowing resubmission", orderIdStr, height, blocksSinceSubmission) + } + // record the submission height for this lock order + m.lockOrderSubmissions[orderIdStr] = rootHeight + } else if order.CloseOrder != nil { + if height, exists := m.closeOrderSubmissions[orderIdStr]; exists { + // test if already submitted at this root height + if height == rootHeight { + m.log.Infof("[ORACLE-STATE] Close order %s held: already submitted at rootHeight=%d", orderIdStr, rootHeight) + return false + } + } + // record the submission height for this close order + m.closeOrderSubmissions[orderIdStr] = rootHeight + } + return true +} + +// ValidateSequence performs sequence validation and reorg detection +func (m *OracleState) ValidateSequence(block types.BlockI) lib.ErrorI { + // protect oracle state fields with write lock + m.rwLock.Lock() + defer m.rwLock.Unlock() + // verify sequential block processing to detect gaps and chain reorganizations + lastState, err := m.readBlockState() + if err != nil { + m.log.Debugf("[ORACLE-STATE] No previous state found, assuming first block") + // first block, no validation needed + return nil + } + // check for block sequence gaps + expectedHeight := lastState.Height + 1 + if block.Number() != expectedHeight { + errorMsg := fmt.Sprintf("expected height %d, got %d", expectedHeight, block.Number()) + m.log.Errorf("[ORACLE-STATE] Block gap detected: %s", errorMsg) + return ErrBlockSequence(errorMsg) + } + // check for chain reorganization by comparing parent hash with last processed block + if block.ParentHash() != lastState.Hash { + errorMsg := fmt.Sprintf("parent hash mismatch at height %d: expected %s, got %s", + block.Number(), lastState.Hash, block.ParentHash()) + m.log.Errorf("[ORACLE-STATE] Chain reorganization detected: %s", errorMsg) + return ErrChainReorg(errorMsg) + } + // save last seen source chain height + m.sourceChainHeight = block.Number() + return nil +} + +// saveState saves the state after a block has been successfully processed +func (m *OracleState) saveState(block types.BlockI) lib.ErrorI { + // create the simple block state + state := OracleBlockState{ + Height: block.Number(), + Hash: block.Hash(), + ParentHash: block.ParentHash(), + Timestamp: time.Now(), + } + // marshal state to JSON + stateBytes, err := json.Marshal(state) + if err != nil { + m.log.Errorf("[ORACLE-STATE] Failed to marshal block state: %v", err) + return ErrWriteStateFile(err) + } + // m.log.Debugf("Saved block state for height %d", state.Height) + // write state to file atomically + if err := lib.AtomicWriteFile(m.stateSaveFile, stateBytes); err != nil { + m.log.Errorf("[ORACLE-STATE] Failed to write state file: %v", err) + return ErrWriteStateFile(err) + } + return nil +} + +// removeState removes the state file from disk +func (m *OracleState) removeState() lib.ErrorI { + err := os.Remove(m.stateSaveFile) + if err != nil && !os.IsNotExist(err) { + return ErrWriteStateFile(err) + } + return nil +} + +// GetLastHeight returns the last processed source chain height +func (m *OracleState) GetLastHeight() uint64 { + // protect oracle state fields with read lock + m.rwLock.RLock() + defer m.rwLock.RUnlock() + // check for previous state from last run + if state, err := m.readBlockState(); err == nil { + m.log.Infof("[ORACLE-STATE] Found previous block state: height %d", state.Height) + // start from the next block after the last successfully processed one + return state.Height + } + m.log.Infof("[ORACLE-STATE] no previous state found, returning start height 0") + return 0 +} + +// readBlockState reads the oracle state from disk +func (m *OracleState) readBlockState() (*OracleBlockState, lib.ErrorI) { + // read file contents + data, err := os.ReadFile(m.stateSaveFile) + if err != nil { + m.log.Debugf("[ORACLE-STATE] block state file not found: %v", err) + return nil, ErrReadStateFile(err) + } + // unmarshal JSON data + var state OracleBlockState + err = json.Unmarshal(data, &state) + if err != nil { + m.log.Errorf("[ORACLE-STATE] failed to unmarshal block state: %v", err) + return nil, ErrParseState(err) + } + return &state, nil +} + +// updateSafeHeight calculates and updates the safe block height with monotonic guarantee +// The safe height can only increase, never decrease, providing stability during reorgs +func (m *OracleState) updateSafeHeight(currentBlockHeight uint64, config lib.OracleConfig) { + // calculate new safe height by subtracting required confirmations + var newSafeHeight uint64 + if currentBlockHeight > config.SafeBlockConfirmations { + newSafeHeight = currentBlockHeight - config.SafeBlockConfirmations + } else { + // handle startup case where current height is less than required confirmations + newSafeHeight = 0 + } + // protect oracle state fields with write lock + m.rwLock.Lock() + defer m.rwLock.Unlock() + // only update if the new safe height is higher (monotonic property) + if newSafeHeight > m.safeHeight { + m.log.Debugf("[ORACLE-STATE] Updating safe height from %d to %d (current height %d, confirmations %d)", + m.safeHeight, newSafeHeight, currentBlockHeight, config.SafeBlockConfirmations) + m.safeHeight = newSafeHeight + } +} + +// GetSafeHeight returns the current safe block height +func (m *OracleState) GetSafeHeight() uint64 { + // protect oracle state fields with read lock + m.rwLock.RLock() + defer m.rwLock.RUnlock() + return m.safeHeight +} + +// PruneHistory removes submission history for orders that are not present in the passed order book +func (m *OracleState) PruneHistory(orderBook *lib.OrderBook) { + // protect oracle state fields with write lock + m.rwLock.Lock() + defer m.rwLock.Unlock() + // handle nil order book case by clearing all history + if orderBook == nil { + m.lockOrderSubmissions = make(map[string]uint64) + m.closeOrderSubmissions = make(map[string]uint64) + m.log.Infof("[ORACLE-STATE] Order book is nil, cleared all submission history") + return + } + // prune lock order submissions for orders not in order book + for orderIdStr := range m.lockOrderSubmissions { + // convert string back to bytes for order book lookup + orderId, err := lib.StringToBytes(orderIdStr) + if err != nil { + m.log.Errorf("[ORACLE-STATE] Failed to convert lock order ID string %s to bytes: %v", orderIdStr, err) + continue + } + // check if order exists in order book + order, err := orderBook.GetOrder(orderId) + if err != nil { + m.log.Errorf("[ORACLE-STATE] Error checking lock order %s in order book: %v", orderIdStr, err) + continue + } + // remove lock order submission for orders not in order book + if order == nil { + delete(m.lockOrderSubmissions, orderIdStr) + m.log.Debugf("[ORACLE-STATE] Pruned lock order submission for order %s (not in order book)", orderIdStr) + } else { + m.log.Debugf("[ORACLE-STATE] Not pruning lock order submission for order %s (still in order book)", orderIdStr) + } + } + // prune close order submissions for orders not in order book + for orderIdStr := range m.closeOrderSubmissions { + // convert string back to bytes for order book lookup + orderId, err := lib.StringToBytes(orderIdStr) + if err != nil { + m.log.Errorf("[ORACLE-STATE] Failed to convert close order ID string %s to bytes: %v", orderIdStr, err) + continue + } + // check if order exists in order book + order, err := orderBook.GetOrder(orderId) + if err != nil { + m.log.Errorf("[ORACLE-STATE] Error checking close order %s in order book: %v", orderIdStr, err) + continue + } + // remove close order submission for orders not in order book + if order == nil { + delete(m.closeOrderSubmissions, orderIdStr) + m.log.Debugf("[ORACLE-STATE] Pruned close order submission for order %s (not in order book)", orderIdStr) + } else { + m.log.Debugf("[ORACLE-STATE] Not pruning close order submission for order %s (still in order book)", orderIdStr) + } + } +} diff --git a/cmd/rpc/oracle/state_test.go b/cmd/rpc/oracle/state_test.go new file mode 100644 index 000000000..d5d138773 --- /dev/null +++ b/cmd/rpc/oracle/state_test.go @@ -0,0 +1,391 @@ +package oracle + +import ( + "os" + "path/filepath" + "testing" + + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" + "github.com/canopy-network/canopy/lib" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// createTestBlock creates a test block with all required fields +func createTestBlock(number uint64, hash string, parentHash string) types.BlockI { + return &mockBlock{ + number: number, + hash: hash, + parentHash: parentHash, + transactions: []types.TransactionI{}, + } +} + +func TestOracleState_ValidateSequence(t *testing.T) { + // Helper function to create a temporary directory for test state files + createTempDir := func(t *testing.T) string { + dir, err := os.MkdirTemp("", "oracle_state_test_*") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(dir) }) + return dir + } + + // Helper function to create a state manager with test setup + createStateManager := func(t *testing.T, tempDir string) *OracleState { + stateFile := filepath.Join(tempDir, "test_state") + logger := lib.NewDefaultLogger() + return NewOracleState(stateFile, logger) + } + + // Helper function to setup a completed block state + setupCompletedBlock := func(t *testing.T, bsm *OracleState, height uint64, hash string, parentHash string) { + block := createTestBlock(height, hash, parentHash) + err := bsm.saveState(block) + require.NoError(t, err) + } + + tests := []struct { + name string + setupState func(t *testing.T, bsm *OracleState) + block types.BlockI + expectError bool + errorCode lib.ErrorCode + errorMessage string + }{ + { + name: "first block validation should pass", + setupState: func(t *testing.T, bsm *OracleState) { + // No setup needed - simulates first run + }, + block: createTestBlock(1, "0xblock1", "0xparent1"), + expectError: false, + }, + { + name: "sequential block validation should pass", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 1, "0xblock1", "0xparent1") + }, + block: createTestBlock(2, "0xblock2", "0xblock1"), + expectError: false, + }, + { + name: "block gap should be detected", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 1, "0xblock1", "0xparent1") + }, + block: createTestBlock(3, "0xblock3", "0xblock2"), // Skipping block 2 + expectError: true, + errorCode: CodeBlockSequence, + errorMessage: "expected height 2, got 3", + }, + { + name: "chain reorganization should be detected", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 1, "0xblock1", "0xparent1") + }, + block: createTestBlock(2, "0xblock2", "0xdifferentparent"), // Wrong parent hash + expectError: true, + errorCode: CodeChainReorg, + errorMessage: "parent hash mismatch at height 2: expected 0xblock1, got 0xdifferentparent", + }, + { + name: "valid chain continuation after multiple blocks", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 5, "0xblock5", "0xblock4") + }, + block: createTestBlock(6, "0xblock6", "0xblock5"), + expectError: false, + }, + { + name: "gap detection with large height difference", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 1, "0xblock1", "0xparent1") + }, + block: createTestBlock(100, "0xblock100", "0xblock99"), + expectError: true, + errorCode: CodeBlockSequence, + errorMessage: "expected height 2, got 100", + }, + { + name: "reorganization with correct height but wrong parent", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 10, "0xblock10", "0xblock9") + }, + block: createTestBlock(11, "0xblock11", "0xwrongparent"), + expectError: true, + errorCode: CodeChainReorg, + errorMessage: "parent hash mismatch at height 11: expected 0xblock10, got 0xwrongparent", + }, + { + name: "backward block should be detected as gap", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 5, "0xblock5", "0xblock4") + }, + block: createTestBlock(3, "0xblock3", "0xblock2"), // Going backwards + expectError: true, + errorCode: CodeBlockSequence, + errorMessage: "expected height 6, got 3", + }, + { + name: "same height block should be detected as gap", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 5, "0xblock5", "0xblock4") + }, + block: createTestBlock(5, "0xblock5_alt", "0xblock4"), // Same height, different block + expectError: true, + errorCode: CodeBlockSequence, + errorMessage: "expected height 6, got 5", + }, + { + name: "empty hash values should still work", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 1, "", "") + }, + block: createTestBlock(2, "", ""), + expectError: false, + }, + { + name: "reorg detection with empty parent hash", + setupState: func(t *testing.T, bsm *OracleState) { + setupCompletedBlock(t, bsm, 1, "0xblock1", "0xparent1") + }, + block: createTestBlock(2, "0xblock2", ""), // Empty parent hash + expectError: true, + errorCode: CodeChainReorg, + errorMessage: "parent hash mismatch at height 2: expected 0xblock1, got ", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory and state manager for this test + tempDir := createTempDir(t) + bsm := createStateManager(t, tempDir) + + // Setup initial state if needed + if tt.setupState != nil { + tt.setupState(t, bsm) + } + + // Execute the test + err := bsm.ValidateSequence(tt.block) + + // Verify results + if tt.expectError { + require.Error(t, err, "expected error but got nil") + + // Check error code if specified + if tt.errorCode != 0 { + assert.Equal(t, tt.errorCode, err.Code(), "unexpected error code") + } + + // Check error message if specified + if tt.errorMessage != "" { + assert.Contains(t, err.Error(), tt.errorMessage, "error message does not contain expected text") + } + } else { + require.NoError(t, err, "unexpected error: %v", err) + } + }) + } +} + +func TestOracleState_shouldSubmit(t *testing.T) { + tests := []struct { + name string + lastSubmitHeight uint64 + witnessedHeight uint64 + rootHeight uint64 + sourceChainHeight uint64 + orderResubmitDelay uint64 + proposeLeadTime uint64 + lockOrderHoldBlocks uint64 + orderType string // "lock" or "close" or "none" + setupPreviousSubmission bool // whether to simulate a previous lock order submission + previousSubmissionHeight uint64 + expected bool + }{ + { + name: "propose lead time not passed - should not submit", + lastSubmitHeight: 40, + witnessedHeight: 10, + rootHeight: 60, + sourceChainHeight: 14, // witnessedHeight(10) + proposeLeadTime(5) = 15, sourceChainHeight(14) < 15 + orderResubmitDelay: 20, + proposeLeadTime: 5, + lockOrderHoldBlocks: 10, + orderType: "close", + expected: false, + }, + { + name: "propose lead time exact boundary - should not submit", + lastSubmitHeight: 40, + witnessedHeight: 10, + rootHeight: 60, + sourceChainHeight: 15, // witnessedHeight(10) + proposeLeadTime(5) = 15, sourceChainHeight(15) >= 15 but still not passed + orderResubmitDelay: 20, + proposeLeadTime: 5, + lockOrderHoldBlocks: 10, + orderType: "close", + expected: false, + }, + { + name: "resubmit delay not reached - should not submit", + lastSubmitHeight: 40, + witnessedHeight: 10, + rootHeight: 55, // 40 + 20 = 60, so 55 <= 60 (delay not reached) + sourceChainHeight: 16, // witnessedHeight(10) + proposeLeadTime(5) = 15, sourceChainHeight(16) > 15 + orderResubmitDelay: 20, + proposeLeadTime: 5, + lockOrderHoldBlocks: 10, + orderType: "close", + expected: false, + }, + { + name: "resubmit delay exact boundary - should not submit", + lastSubmitHeight: 40, + witnessedHeight: 10, + rootHeight: 60, // 40 + 20 = 60, so 60 <= 60 (delay not reached) + sourceChainHeight: 16, + orderResubmitDelay: 20, + proposeLeadTime: 5, + lockOrderHoldBlocks: 10, + orderType: "close", + expected: false, + }, + { + name: "resubmit delay exceeded - should submit close order", + lastSubmitHeight: 30, + witnessedHeight: 10, + rootHeight: 100, // 30 + 20 = 50, so 100 > 50 (delay exceeded) + sourceChainHeight: 16, + orderResubmitDelay: 20, + proposeLeadTime: 5, + lockOrderHoldBlocks: 10, + orderType: "close", + expected: true, + }, + { + name: "first submission with all checks passed - should submit", + lastSubmitHeight: 0, + witnessedHeight: 10, + rootHeight: 100, + sourceChainHeight: 16, // witnessedHeight(10) + proposeLeadTime(5) = 15, sourceChainHeight(16) > 15 + orderResubmitDelay: 10, + proposeLeadTime: 5, + lockOrderHoldBlocks: 10, + orderType: "lock", + expected: true, + }, + { + name: "zero propose lead time with resubmit delay exceeded - should submit", + lastSubmitHeight: 40, + witnessedHeight: 10, + rootHeight: 80, // 40 + 20 = 60, so 80 > 60 (delay exceeded) + sourceChainHeight: 11, // witnessedHeight(10) + proposeLeadTime(0) = 10, sourceChainHeight(11) > 10 + orderResubmitDelay: 20, + proposeLeadTime: 0, + lockOrderHoldBlocks: 10, + orderType: "close", + expected: true, + }, + { + name: "zero delay with propose lead time passed - should submit", + lastSubmitHeight: 50, + witnessedHeight: 10, + rootHeight: 51, // 50 + 0 = 50, so 51 > 50 (delay exceeded) + sourceChainHeight: 16, + orderResubmitDelay: 0, + proposeLeadTime: 5, + lockOrderHoldBlocks: 10, + orderType: "close", + expected: true, + }, + { + name: "lock order first submission - should submit", + lastSubmitHeight: 0, + witnessedHeight: 10, + rootHeight: 100, + sourceChainHeight: 16, + orderResubmitDelay: 10, + proposeLeadTime: 5, + lockOrderHoldBlocks: 20, + orderType: "lock", + expected: true, + }, + { + name: "lock order resubmission too soon - should not submit", + lastSubmitHeight: 0, + witnessedHeight: 10, + rootHeight: 105, + sourceChainHeight: 16, + orderResubmitDelay: 10, + proposeLeadTime: 5, + lockOrderHoldBlocks: 20, + orderType: "lock", + setupPreviousSubmission: true, + previousSubmissionHeight: 100, // 105 - 100 = 5 blocks, need 20 + expected: false, + }, + { + name: "lock order resubmission after hold time - should submit", + lastSubmitHeight: 0, + witnessedHeight: 10, + rootHeight: 125, + sourceChainHeight: 16, + orderResubmitDelay: 10, + proposeLeadTime: 5, + lockOrderHoldBlocks: 20, + orderType: "lock", + setupPreviousSubmission: true, + previousSubmissionHeight: 100, // 125 - 100 = 25 blocks, need 20, so allowed + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create state manager with logger + logger := lib.NewDefaultLogger() + stateManager := NewOracleState("test_state", logger) + // Set external chain height for the test + stateManager.sourceChainHeight = tt.sourceChainHeight + // Setup previous submission if needed + if tt.setupPreviousSubmission { + stateManager.lockOrderSubmissions = make(map[string]uint64) + orderIdStr := lib.BytesToString([]byte("testorder")) + stateManager.lockOrderSubmissions[orderIdStr] = tt.previousSubmissionHeight + } + // Create config + config := lib.OracleConfig{ + OrderResubmitDelayBlocks: tt.orderResubmitDelay, + ProposeDelayBlocks: tt.proposeLeadTime, + LockOrderCooldownBlocks: tt.lockOrderHoldBlocks, + } + // Create witnessed order + order := &types.WitnessedOrder{ + OrderId: []byte("testorder"), + LastSubmitHeight: tt.lastSubmitHeight, + WitnessedHeight: tt.witnessedHeight, + } + // Set order type + switch tt.orderType { + case "lock": + order.LockOrder = &lib.LockOrder{ + OrderId: []byte("testorder"), + } + case "close": + order.CloseOrder = &lib.CloseOrder{ + OrderId: []byte("testorder"), + } + } + // Execute test + result := stateManager.shouldSubmit(order, tt.rootHeight, config) + + // Verify result + if result != tt.expected { + t.Errorf("shouldSubmit() = %v, expected %v", result, tt.expected) + } + }) + } +} diff --git a/cmd/rpc/oracle/types/types.go b/cmd/rpc/oracle/types/types.go new file mode 100644 index 000000000..2c9038267 --- /dev/null +++ b/cmd/rpc/oracle/types/types.go @@ -0,0 +1,217 @@ +package types + +import ( + "context" + "errors" + "fmt" + "math/big" + "time" + + "github.com/canopy-network/canopy/lib" +) + +type OrderType string + +const ( + // LockOrderType represents lock order transactions + LockOrderType OrderType = "lock" + // CloseOrderType represents close order transactions + CloseOrderType OrderType = "close" +) + +// ProcessingStatus represents the state of block processing +type ProcessingStatus string + +const ( + // ProcessingStatusPending indicates block is queued for processing + ProcessingStatusPending ProcessingStatus = "pending" + // ProcessingStatusProcessing indicates block is currently being processed + ProcessingStatusProcessing ProcessingStatus = "processing" + // ProcessingStatusCompleted indicates block processing completed successfully + ProcessingStatusCompleted ProcessingStatus = "completed" + // ProcessingStatusFailed indicates block processing failed + ProcessingStatusFailed ProcessingStatus = "failed" +) + +// BlockProcessingState tracks the processing state of a block +type BlockProcessingState struct { + // Height is the block height being processed + Height uint64 `json:"height"` + // Hash is the block hash for verification + Hash string `json:"hash"` + // ParentHash is the parent block hash for chain reorganization detection + ParentHash string `json:"parentHash"` + // Status indicates the current processing state + Status ProcessingStatus `json:"status"` + // Timestamp when the state was last updated + Timestamp time.Time `json:"timestamp"` + // RetryCount tracks how many times processing was attempted + RetryCount int `json:"retryCount"` +} + +type WitnessedOrder struct { + // OrderId for the enclosed lock or close order + OrderId lib.HexBytes `json:"orderId"` + // Witnessed height on the source block chain (ethereum, solana, etc) + WitnessedHeight uint64 `json:"witnessedHeight"` + // last canopy root chain height this order was submitted + LastSubmitHeight uint64 `json:"lastSubmightHeight"` + // Witnessed lock order + LockOrder *lib.LockOrder `json:"lockOrder,omitempty"` + // Witnessed close order + CloseOrder *lib.CloseOrder `json:"closeOrder,omitempty"` +} + +// String returns a formatted string representation of WitnessedOrder +func (w WitnessedOrder) String() string { + // determine which order type is present + var orderDetails string + if w.LockOrder != nil { + orderDetails = fmt.Sprintf("LockOrder{OrderId:%x ChainId:%d BuyerReceiveAddress:%x BuyerSendAddress:%x BuyerChainDeadline:%d}", + w.LockOrder.OrderId, w.LockOrder.ChainId, w.LockOrder.BuyerReceiveAddress, w.LockOrder.BuyerSendAddress, w.LockOrder.BuyerChainDeadline) + } else if w.CloseOrder != nil { + orderDetails = fmt.Sprintf("CloseOrder{OrderId:%x ChainId:%d CloseOrder:%t}", + w.CloseOrder.OrderId, w.CloseOrder.ChainId, w.CloseOrder.CloseOrder) + } else { + orderDetails = "No order data" + } + // return formatted string with order details + return fmt.Sprintf("Order{ID: %s, WitnessedHeight: %d, %s}", + w.OrderId, w.WitnessedHeight, orderDetails) +} + +// Format implements fmt.Formatter for custom formatting +func (w WitnessedOrder) Format(f fmt.State, verb rune) { + // handle different format verbs + switch verb { + case 'v': + if f.Flag('+') { + // detailed format with newlines and indentation + fmt.Fprintf(f, "WitnessedOrder{\n OrderId: %x\n WitnessedHeight: %d\n LastSubmitHeight: %d\n", + w.OrderId, w.WitnessedHeight, w.LastSubmitHeight) + if w.LockOrder != nil { + fmt.Fprintf(f, " LockOrder{OrderId:%x ChainId:%d BuyerReceiveAddress:%x BuyerSendAddress:%x BuyerChainDeadline:%d}\n", + w.LockOrder.OrderId, w.LockOrder.ChainId, w.LockOrder.BuyerReceiveAddress, w.LockOrder.BuyerSendAddress, w.LockOrder.BuyerChainDeadline) + } + if w.CloseOrder != nil { + fmt.Fprintf(f, " CloseOrder{OrderId:%x ChainId:%d CloseOrder:%t}\n", + w.CloseOrder.OrderId, w.CloseOrder.ChainId, w.CloseOrder.CloseOrder) + } + fmt.Fprint(f, "}") + } else { + // use default string representation + fmt.Fprint(f, w.String()) + } + case 's': + // string format uses String() method + fmt.Fprint(f, w.String()) + default: + // handle unsupported format verbs + fmt.Fprintf(f, "%%!%c(WitnessedOrder=%s)", verb, w.String()) + } +} + +// BlockI interface represents a blockchain block +type BlockI interface { + Hash() string + ParentHash() string + Number() uint64 + Transactions() []TransactionI +} + +// TransactionI interface represents a blockchain transaction +type TransactionI interface { + Blockchain() string + From() string + To() string + Hash() string + Order() *WitnessedOrder + TokenTransfer() TokenTransfer +} + +// OrderStore defines the methods that are required for order persistence. +type OrderStore interface { + // VerifyOrder verifies the byte data of a stored order + VerifyOrder(order *WitnessedOrder, orderType OrderType) lib.ErrorI + // WriteOrder writes an order + WriteOrder(order *WitnessedOrder, orderType OrderType) lib.ErrorI + // ReadOrder reads a witnessed order + ReadOrder(orderId []byte, orderType OrderType) (*WitnessedOrder, lib.ErrorI) + // RemoveOrder removes an order + RemoveOrder(order []byte, orderType OrderType) lib.ErrorI + // GetAllOrderIds gets all order ids present in the store + GetAllOrderIds(orderType OrderType) ([][]byte, lib.ErrorI) + // ArchiveOrder archives a witnessed order to the archive directory for historical retention + ArchiveOrder(order *WitnessedOrder, orderType OrderType) lib.ErrorI +} + +type BlockProvider interface { + // Start the block provider at height + Start(ctx context.Context, height uint64) + // Block returns the channel this provider will send new blocks through + BlockCh() chan BlockI + // IsSynced returns whether the provider has synced to the top of the chain + IsSynced() bool +} + +// TokenInfo holds the basic information about an ERC20 token +type TokenInfo struct { + Name string + Symbol string + Decimals uint8 +} + +// String returns a formatted string representation of TokenInfo +func (t TokenInfo) String() string { + // return formatted string with token information + return fmt.Sprintf("TokenInfo{Name: %s, Symbol: %s, Decimals: %d}", + t.Name, t.Symbol, t.Decimals) +} + +// TokenTransfer represents a generic token transfer across different blockchains. +type TokenTransfer struct { + Blockchain string // Name of the blockchain (e.g., Ethereum, Solana, Binance Smart Chain) + TokenInfo TokenInfo + TransactionID string // Unique identifier for the transaction + SenderAddress string // Address of the sender + RecipientAddress string // Address of the recipient + TokenBaseAmount *big.Int // Amount of tokens transferred represented in base units + ContractAddress string // Mint address or contract address of the token +} + +// Amount returns the decimal-adjusted token transfer amount +func (t TokenTransfer) DecimalAmount() (float64, error) { + // calculate decimal-adjusted amount + decimals := big.NewInt(int64(t.TokenInfo.Decimals)) + divisor := new(big.Int).Exp(big.NewInt(10), decimals, nil) + if divisor.Cmp(big.NewInt(0)) == 0 { + return 0, errors.New("divisor cannot be zero") + } + decimalAmount := new(big.Float).SetInt(t.TokenBaseAmount) + decimalAmount.Quo(decimalAmount, new(big.Float).SetInt(divisor)) + tokenAmount, accuracy := decimalAmount.Float64() + if accuracy != big.Exact && accuracy != big.Below && accuracy != big.Above { + return 0, errors.New("failed to convert decimal amount to float64") + } + return tokenAmount, nil +} + +// String returns a formatted string representation of TokenTransfer +func (t TokenTransfer) String() string { + // calculate decimal amount for display + decimalAmount, err := t.DecimalAmount() + var amountStr string + if err != nil { + // fallback to base amount if decimal conversion fails + amountStr = fmt.Sprintf("BaseAmount: %s (decimal conversion failed: %v)", + t.TokenBaseAmount.String(), err) + } else { + // show both decimal and base amounts + amountStr = fmt.Sprintf("Amount: %.6f %s (Base: %s)", + decimalAmount, t.TokenInfo.Symbol, t.TokenBaseAmount.String()) + } + // return formatted string with transfer details + return fmt.Sprintf("TokenTransfer{Blockchain: %s, %s, TxID: %s, From: %s, To: %s, Contract: %s}", + t.Blockchain, amountStr, t.TransactionID, t.SenderAddress, + t.RecipientAddress, t.ContractAddress) +} diff --git a/cmd/rpc/query.go b/cmd/rpc/query.go index f689f068a..f3e79b7f9 100644 --- a/cmd/rpc/query.go +++ b/cmd/rpc/query.go @@ -3,6 +3,7 @@ package rpc import ( "bytes" "encoding/json" + "fmt" "net/http" "net/http/pprof" "os" @@ -10,6 +11,8 @@ import ( "slices" "strconv" + "github.com/canopy-network/canopy/cmd/rpc/oracle" + "github.com/canopy-network/canopy/cmd/rpc/oracle/types" "github.com/canopy-network/canopy/fsm" "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" @@ -332,6 +335,86 @@ func (s *Server) NextDexBatch(w http.ResponseWriter, r *http.Request, _ httprout }) } +// OracleOrdersResponse holds categorized witnessed orders +type OracleOrdersResponse struct { + LockOrders []*types.WitnessedOrder `json:"lock_orders"` + CloseOrders []*types.WitnessedOrder `json:"close_orders"` +} + +// OracleOrders returns oracle orders stored in the order store +func (s *Server) OracleOrders(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + // use the standard heightPaginated helper for paginated height requests + s.heightPaginated(w, r, func(state *fsm.StateMachine, p *paginatedHeightRequest) (any, lib.ErrorI) { + // create order store instance using config data dir path + orderStorePath := filepath.Join(s.config.DataDirPath, "oracle/store") + fmt.Println(orderStorePath) + orderStore, err := oracle.NewOracleDiskStorage(orderStorePath, s.logger) + if err != nil { + return nil, lib.ErrNewStore(err) + } + // get all lock orders + lockOrderIds, libErr := orderStore.GetAllOrderIds(types.LockOrderType) + if libErr != nil { + return nil, libErr + } + // get all close orders + closeOrderIds, libErr := orderStore.GetAllOrderIds(types.CloseOrderType) + if libErr != nil { + return nil, libErr + } + fmt.Println("OracleOrders()", lockOrderIds, closeOrderIds) + // create response structure to hold categorized orders + response := &OracleOrdersResponse{ + LockOrders: make([]*types.WitnessedOrder, 0, len(lockOrderIds)), + CloseOrders: make([]*types.WitnessedOrder, 0, len(closeOrderIds)), + } + // read lock orders + for _, orderId := range lockOrderIds { + order, readErr := orderStore.ReadOrder(orderId, types.LockOrderType) + if readErr != nil { + s.logger.Errorf("Failed to read lock order %x: %v", orderId, readErr) + continue + } + response.LockOrders = append(response.LockOrders, order) + } + // read close orders + for _, orderId := range closeOrderIds { + order, readErr := orderStore.ReadOrder(orderId, types.CloseOrderType) + if readErr != nil { + s.logger.Errorf("Failed to read close order %x: %v", orderId, readErr) + continue + } + response.CloseOrders = append(response.CloseOrders, order) + } + // return categorized witnessed orders + return response, nil + }) +} + +// // orderMatchesAddress checks if a witnessed order is related to the given address +// func (s *Server) orderMatchesAddress(order *types.WitnessedOrder, address crypto.AddressI) bool { +// // check if address matches any address in lock order +// if order.LockOrder != nil { +// if order.LockOrder.BuyerReceiveAddress != nil { +// if bytes.Equal(order.LockOrder.BuyerReceiveAddress, address.Bytes()) { +// return true +// } +// } +// if order.LockOrder.BuyerSendAddress != nil { +// if bytes.Equal(order.LockOrder.BuyerSendAddress, address.Bytes()) { +// return true +// } +// } +// } +// // check if address matches any address in close order (close orders have limited address info) +// if order.CloseOrder != nil { +// // CloseOrder doesn't have specific address fields, so we'll rely on OrderId matching +// // which would be related to the original SellOrder +// return true +// } +// return false +// } + // LastProposers returns the last Proposer addresses func (s *Server) LastProposers(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { // Invoke helper with the HTTP request, response writer and an inline callback diff --git a/cmd/rpc/routes.go b/cmd/rpc/routes.go index dd09d2a07..48d8662a0 100644 --- a/cmd/rpc/routes.go +++ b/cmd/rpc/routes.go @@ -51,6 +51,7 @@ const ( DexPriceRoutePath = "/v1/query/dex-price" DexBatchRoutePath = "/v1/query/dex-batch" NextDexBatchRoutePath = "/v1/query/next-dex-batch" + OracleOrdersRoutePath = "/v1/query/oracle-orders" LastProposersRoutePath = "/v1/query/last-proposers" IsValidDoubleSignerRoutePath = "/v1/query/valid-double-signer" DoubleSignersRoutePath = "/v1/query/double-signers" @@ -156,6 +157,7 @@ const ( DexPriceRouteName = "dex-price" DexBatchRouteName = "dex-batch" NextDexBatchRouteName = "next-dex-batch" + OracleOrdersRouteName = "oracle-orders" LastProposersRouteName = "last-proposers" IsValidDoubleSignerRouteName = "valid-double-signer" DoubleSignersRouteName = "double-signers" @@ -258,6 +260,7 @@ var routePaths = routes{ DexPriceRouteName: {Method: http.MethodPost, Path: DexPriceRoutePath}, DexBatchRouteName: {Method: http.MethodPost, Path: DexBatchRoutePath}, NextDexBatchRouteName: {Method: http.MethodPost, Path: NextDexBatchRoutePath}, + OracleOrdersRouteName: {Method: http.MethodPost, Path: OracleOrdersRoutePath}, LastProposersRouteName: {Method: http.MethodPost, Path: LastProposersRoutePath}, IsValidDoubleSignerRouteName: {Method: http.MethodPost, Path: IsValidDoubleSignerRoutePath}, DoubleSignersRouteName: {Method: http.MethodPost, Path: DoubleSignersRoutePath}, @@ -364,6 +367,7 @@ func createRouter(s *Server) *httprouter.Router { DexPriceRouteName: s.DexPrice, DexBatchRouteName: s.DexBatch, NextDexBatchRouteName: s.NextDexBatch, + OracleOrdersRouteName: s.OracleOrders, LastProposersRouteName: s.LastProposers, IsValidDoubleSignerRouteName: s.IsValidDoubleSigner, DoubleSignersRouteName: s.DoubleSigners, diff --git a/cmd/rpc/server.go b/cmd/rpc/server.go index 7f47afd94..352cf8857 100644 --- a/cmd/rpc/server.go +++ b/cmd/rpc/server.go @@ -78,9 +78,10 @@ func NewServer(controller *controller.Controller, config lib.Config, logger lib. // Start initializes the Canopy RPC servers func (s *Server) Start() { + hostport := strings.Split(s.config.ListenAddress, ":") // Start the Query and Admin RPC servers concurrently - go s.startRPC(createRouter(s), s.config.RPCPort) - go s.startRPC(createAdminRouter(s), s.config.AdminPort) + go s.startRPC(createRouter(s), hostport[0], s.config.RPCPort) + go s.startRPC(createAdminRouter(s), hostport[0], s.config.AdminPort) // Start tasks to update poll results and poll root chain information go s.updatePollResults() @@ -101,7 +102,7 @@ func (s *Server) Start() { } // startRPC starts an RPC server with the provided router and port -func (s *Server) startRPC(router *httprouter.Router, port string) { +func (s *Server) startRPC(router *httprouter.Router, host, port string) { // Create CORS policy cor := cors.New(cors.Options{ @@ -113,9 +114,9 @@ func (s *Server) startRPC(router *httprouter.Router, port string) { timeout := time.Duration(s.config.TimeoutS) * time.Second // Start RPC server - s.logger.Infof("Starting RPC server at 0.0.0.0:%s", port) + s.logger.Infof("Starting RPC server at %s:%s", host, port) s.logger.Fatal((&http.Server{ - Addr: colon + port, + Addr: host + colon + port, ReadHeaderTimeout: timeout, ReadTimeout: timeout, WriteTimeout: timeout, @@ -154,7 +155,7 @@ func (s *Server) updatePollResults() { return nil }(); err != nil { - s.logger.Error(err.Error()) + // s.logger.Error(err.Error()) } time.Sleep(time.Second * 3) } @@ -162,9 +163,11 @@ func (s *Server) updatePollResults() { // startStaticFileServers starts a file server for the wallet and explorer func (s *Server) startStaticFileServers() { - s.logger.Infof("Starting Web Wallet 🔑 http://localhost:%s ⬅️", s.config.WalletPort) + hostport := strings.Split(s.config.ListenAddress, ":") + s.logger.Infof("Starting Web Wallet 🔑 http://%s:%s ⬅️", hostport[0], s.config.WalletPort) + s.runStaticFileServer(walletFS, walletStaticDir, s.config.WalletPort, s.config) - s.logger.Infof("Starting Block Explorer 🔍️ http://localhost:%s ⬅️", s.config.ExplorerPort) + s.logger.Infof("Starting Block Explorer 🔍️ http://%s:%s ⬅️", hostport[0], s.config.ExplorerPort) s.runStaticFileServer(explorerFS, explorerStaticDir, s.config.ExplorerPort, s.config) } diff --git a/cmd/rpc/sock.go b/cmd/rpc/sock.go index d4a58b58b..cdb294045 100644 --- a/cmd/rpc/sock.go +++ b/cmd/rpc/sock.go @@ -379,8 +379,13 @@ func (r *RCSubscription) dialWithBackoff(chainId uint64, config lib.RootChain) { // fallback if url didn't have a scheme and was treated as a path host = parsedUrl.Path } + // determine websocket scheme based on original URL scheme + wsScheme := "ws" + if parsedUrl.Scheme == "https" || parsedUrl.Scheme == "wss" { + wsScheme = "wss" + } // create a URL to connect to the root chain with - u := url.URL{Scheme: "ws", Host: host, Path: SubscribeRCInfoPath, RawQuery: fmt.Sprintf("%s=%d", chainIdParamName, chainId)} + u := url.URL{Scheme: wsScheme, Host: host, Path: SubscribeRCInfoPath, RawQuery: fmt.Sprintf("%s=%d", chainIdParamName, chainId)} // create a new retry for backoff retry := lib.NewRetry(uint64(time.Second.Milliseconds()), 25) // until backoff fails or connection succeeds diff --git a/cmd/rpc/web/wallet/components/api.js b/cmd/rpc/web/wallet/components/api.js index 47fa1d8c5..dcc6c4a29 100644 --- a/cmd/rpc/web/wallet/components/api.js +++ b/cmd/rpc/web/wallet/components/api.js @@ -8,8 +8,8 @@ if (typeof window !== "undefined") { adminRPCURL = window.__CONFIG__.adminRPCURL; chainId = Number(window.__CONFIG__.chainId); } - rpcURL = rpcURL.replace("localhost", window.location.hostname); - adminRPCURL = adminRPCURL.replace("localhost", window.location.hostname); + // rpcURL = rpcURL.replace("localhost", window.location.hostname); + // adminRPCURL = adminRPCURL.replace("localhost", window.location.hostname); } else { console.log("config undefined"); } diff --git a/controller/block.go b/controller/block.go index 5b49b2bd3..94cb79d3d 100644 --- a/controller/block.go +++ b/controller/block.go @@ -153,6 +153,11 @@ func (c *Controller) ProduceProposal(evidence *bft.ByzantineEvidence, vdf *crypt if err != nil { return } + // TODO change 1 to c.config.ChainId + orderBook, err := c.LoadRootChainOrderBook(1, rcBuildHeight) + if err != nil { + return + } // replace the VDF and last certificate in the header p.Block.BlockHeader.LastQuorumCertificate, p.Block.BlockHeader.Vdf = lastCertificate, vdf p.Block.BlockHeader.TotalVdfIterations = vdf.GetIterations() + lastBlock.BlockHeader.TotalVdfIterations @@ -163,6 +168,10 @@ func (c *Controller) ProduceProposal(evidence *bft.ByzantineEvidence, vdf *crypt // exit with error return } + // append any witnessed orders to the on chain orders + lockOrders, closeOrders := c.oracle.WitnessedOrders(orderBook, rcBuildHeight) + results.Orders.LockOrders = lockOrders + results.Orders.CloseOrders = closeOrders // convert the block reference to bytes blockBytes, err = lib.Marshal(p.Block) if err != nil { @@ -210,6 +219,18 @@ func (c *Controller) ValidateProposal(rcBuildHeight uint64, qc *lib.QuorumCertif } // create a comparable certificate results (includes reward recipients, slash recipients, swap commands, etc) compareResults := c.NewCertificateResults(c.FSM, block, blockResult, evidence, rcBuildHeight) + + // // get the root chain order book at latest height + // orderBook, err := c.GetOrderBook() + // if err != nil { + // c.log.Errorf("Error getting order book: %v", err) + // } + // validate the proposed orders were witnessed by the oracle + err = c.oracle.ValidateProposedOrders(qc.Results.Orders) + if err != nil { + return + } + compareResults.Orders = qc.Results.Orders // ensure generated the same results if !qc.Results.Equals(compareResults) { // exit with error @@ -227,6 +248,7 @@ func (c *Controller) ValidateProposal(rcBuildHeight uint64, qc *lib.QuorumCertif // - atomically writes all to the underlying db // - sets up the controller for the next height func (c *Controller) CommitCertificate(qc *lib.QuorumCertificate, block *lib.Block, blockResult *lib.BlockResult, ts uint64) (err lib.ErrorI) { + c.log.Warnf("CommitCertificate block: %d qc %d chain %d", block.BlockHeader.Height, qc.Header.Height, qc.Header.ChainId) start := time.Now() // cancel any running mempool check c.Mempool.stop() @@ -282,6 +304,12 @@ func (c *Controller) CommitCertificate(qc *lib.QuorumCertificate, block *lib.Blo } // log to signal finishing the commit c.log.Infof("Committed block %s at H:%d 🔒", lib.BytesToTruncatedString(qc.BlockHash), block.BlockHeader.Height) + // execute Oracle CommitCertificate + err = c.oracle.CommitCertificate(qc, block, blockResult, ts) + if err != nil { + // exit with error + return err + } // set up the finite state machine for the next height c.FSM, err = fsm.New(c.Config, storeI, c.Plugin, c.Metrics, c.log) if err != nil { diff --git a/controller/controller.go b/controller/controller.go index 2259d7b28..2cc0a712c 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -1,6 +1,7 @@ package controller import ( + "context" "encoding/json" "errors" "net" @@ -13,6 +14,7 @@ import ( "time" "github.com/canopy-network/canopy/bft" + "github.com/canopy-network/canopy/cmd/rpc/oracle" "github.com/canopy-network/canopy/fsm" "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" @@ -39,6 +41,7 @@ type Controller struct { RCManager lib.RCManagerI // the data manager for the 'root chain' Plugin *lib.Plugin // extensible plugin for FSM + oracle *oracle.Oracle // witness oracle checkpoints map[uint64]map[uint64]lib.HexBytes // cached checkpoints loaded from file isSyncing *atomic.Bool // is the chain currently being downloaded from peers log lib.LoggerI // object for logging @@ -46,7 +49,7 @@ type Controller struct { } // New() creates a new instance of a Controller, this is the entry point when initializing an instance of a Canopy application -func New(fsm *fsm.StateMachine, c lib.Config, valKey crypto.PrivateKeyI, metrics *lib.Metrics, l lib.LoggerI) (controller *Controller, err lib.ErrorI) { +func New(fsm *fsm.StateMachine, oracle *oracle.Oracle, c lib.Config, valKey crypto.PrivateKeyI, metrics *lib.Metrics, l lib.LoggerI) (controller *Controller, err lib.ErrorI) { address := valKey.PublicKey().Address() // load the maximum validators param to set limits on P2P maxMembersPerCommittee, err := fsm.GetMaxValidators() @@ -76,6 +79,8 @@ func New(fsm *fsm.StateMachine, c lib.Config, valKey crypto.PrivateKeyI, metrics isSyncing: &atomic.Bool{}, log: l, Mutex: &sync.Mutex{}, + + oracle: oracle, } // load checkpoints from file (if provided) controller.loadCheckpointsFile() @@ -125,6 +130,18 @@ func (c *Controller) Start() { c.Mempool.CheckMempool() // update the peer 'must connect' c.UpdateP2PMustConnect(rootChainInfo.ValidatorSet) + // oracle specific initialization + if c.Config.OracleEnabled { + // update oracle's order book so it can start processing blocks + c.oracle.UpdateRootChainInfo(rootChainInfo) + c.log.Info("Starting Oracle, waiting for source chain sync") + // channel to indicate source chain is synced + syncCh := make(chan bool) + // start the oracle with context and a channel to wait for source chain sync + c.oracle.Start(context.Background(), syncCh) + <-syncCh // wait for syncCh to be closed + c.log.Info("Oracle is synced to top, starting Canopy") + } // exit the loop break } @@ -190,6 +207,8 @@ func (c *Controller) UpdateRootChainInfo(info *lib.RootChainInfo) { c.log.Debugf("Detected inactive root-chain update at rootChainId=%d", info.RootChainId) return } + // sync the order store + c.oracle.UpdateRootChainInfo(info) // set timestamp if included var timestamp time.Time // if timestamp is not 0 @@ -218,6 +237,11 @@ func (c *Controller) LoadRootChainOrderBook(rootChainId, rootHeight uint64) (*li return c.RCManager.GetOrders(rootChainId, rootHeight, c.Config.ChainId) } +// GetOrderBook fetches the root chain order book at the latest height +func (c *Controller) GetOrderBook() (*lib.OrderBook, lib.ErrorI) { + return c.RCManager.GetOrders(c.LoadRootChainId(c.ChainHeight()), c.RootChainHeight(), c.Config.ChainId) +} + // GetRootChainLotteryWinner() gets the pseudorandomly selected delegate to reward and their cut func (c *Controller) GetRootChainLotteryWinner(fsm *fsm.StateMachine, rootHeight uint64) (winner *lib.LotteryWinner, err lib.ErrorI) { // get the root chain id from the state machine diff --git a/controller/result.go b/controller/result.go index 85d48ca24..898698969 100644 --- a/controller/result.go +++ b/controller/result.go @@ -2,11 +2,11 @@ package controller import ( "bytes" - "github.com/canopy-network/canopy/fsm" "slices" "time" "github.com/canopy-network/canopy/bft" + "github.com/canopy-network/canopy/fsm" "github.com/canopy-network/canopy/lib" ) diff --git a/docs/ORACLE_METRICS.md b/docs/ORACLE_METRICS.md new file mode 100644 index 000000000..f194116fe --- /dev/null +++ b/docs/ORACLE_METRICS.md @@ -0,0 +1,308 @@ +# Oracle Metrics Reference + +This document describes all Prometheus metrics exposed by the Canopy Oracle system for monitoring and observability. + +## Overview + +The oracle exposes metrics through a Prometheus-compatible endpoint. Metrics are organized into two main categories: + +1. **Oracle Metrics** (`canopy_oracle_*`) - High-level oracle operations, order lifecycle, and validation +2. **Eth Block Provider Metrics** (`canopy_eth_*`) - Ethereum connectivity, block processing, and transaction handling + +--- + +## Oracle Metrics + +These metrics track the oracle's core functionality including order processing, validation, and submission. + +### Block Height Metrics + +| Metric Name | Type | Description | +|-------------|------|-------------| +| `canopy_oracle_safe_height` | Gauge | Current safe block height in the oracle | +| `canopy_oracle_source_chain_height` | Gauge | Current source chain height | +| `canopy_oracle_last_processed_height` | Gauge | Last source chain block height processed | +| `canopy_oracle_confirmation_lag` | Gauge | Gap between source chain height and safe height (blocks awaiting confirmation) | +| `canopy_oracle_orders_awaiting_confirmation` | Gauge | Number of orders witnessed but not yet at safe height | + +### Order Lifecycle Metrics + +| Metric Name | Type | Labels | Description | +|-------------|------|--------|-------------| +| `canopy_oracle_orders_witnessed_total` | Counter | - | Total orders witnessed | +| `canopy_oracle_orders_validated_total` | Counter | - | Total orders validated successfully | +| `canopy_oracle_orders_submitted_total` | Counter | - | Total orders submitted for consensus | +| `canopy_oracle_orders_rejected_total` | Counter | - | Total orders rejected during validation | +| `canopy_oracle_orders_not_in_orderbook_total` | Counter | - | Orders witnessed but not found in order book | +| `canopy_oracle_orders_duplicate_total` | Counter | - | Duplicate orders (already in store) | +| `canopy_oracle_orders_archived_total` | Counter | - | Orders successfully archived | +| `canopy_oracle_lock_orders_committed_total` | Counter | - | Lock orders committed via certificate | +| `canopy_oracle_close_orders_committed_total` | Counter | - | Close orders committed via certificate | + +### Order Store Metrics + +| Metric Name | Type | Description | +|-------------|------|-------------| +| `canopy_oracle_total_orders_stored` | Gauge | Total orders currently stored in order store | +| `canopy_oracle_lock_orders_stored` | Gauge | Total lock orders currently stored | +| `canopy_oracle_close_orders_stored` | Gauge | Total close orders currently stored | + +### Validation Failure Metrics + +| Metric Name | Type | Labels | Description | +|-------------|------|--------|-------------| +| `canopy_oracle_validation_failures_total` | Counter | `reason` | Validation failures by reason | + +**Reason Labels:** +- `order_nil` - Order was nil +- `missing_order_type` - Order missing type specification +- `both_order_types` - Order has both lock and close types +- `lock_id_mismatch` - Lock order ID mismatch +- `lock_chain_mismatch` - Lock order chain ID mismatch +- `close_data_mismatch` - Close order data mismatch +- `close_id_mismatch` - Close order ID mismatch +- `close_chain_mismatch` - Close order chain ID mismatch +- `recipient_mismatch` - Recipient address mismatch +- `amount_nil` - Amount is nil +- `amount_mismatch` - Amount mismatch + +### Submission Tracking Metrics + +| Metric Name | Type | Description | +|-------------|------|-------------| +| `canopy_oracle_orders_held_awaiting_safe_total` | Counter | Orders not submitted due to safe height requirement | +| `canopy_oracle_orders_held_propose_delay_total` | Counter | Orders held by ProposeDelayBlocks | +| `canopy_oracle_orders_held_resubmit_delay_total` | Counter | Orders held by resubmit cooldown | +| `canopy_oracle_lock_order_resubmissions_total` | Counter | Lock orders resubmitted | +| `canopy_oracle_close_order_resubmissions_total` | Counter | Close orders resubmitted | + +### Error and Reorg Metrics + +| Metric Name | Type | Description | +|-------------|------|-------------| +| `canopy_oracle_chain_reorgs_total` | Counter | Total chain reorganizations detected | +| `canopy_oracle_orders_pruned_total` | Counter | Total orders pruned during cleanup | +| `canopy_oracle_block_processing_errors_total` | Counter | Total block processing errors | +| `canopy_oracle_reorg_rollback_depth` | Histogram | How many blocks reorgs roll back | + +### Store Operation Metrics + +| Metric Name | Type | Description | +|-------------|------|-------------| +| `canopy_oracle_store_write_errors_total` | Counter | Order store write failures | +| `canopy_oracle_store_read_errors_total` | Counter | Order store read failures | +| `canopy_oracle_store_remove_errors_total` | Counter | Order store remove failures | + +### Performance Metrics + +| Metric Name | Type | Description | +|-------------|------|-------------| +| `canopy_oracle_order_book_update_time` | Histogram | Time to update order book | +| `canopy_oracle_root_chain_sync_time` | Histogram | Time to sync with root chain | + +--- + +## Eth Block Provider Metrics + +These metrics track Ethereum connectivity, block fetching, and transaction processing. + +### Connection & Sync Status Metrics (High Priority) + +| Metric Name | Type | Labels | Description | +|-------------|------|--------|-------------| +| `canopy_eth_rpc_connection_attempts_total` | Counter | - | Total RPC connection attempts | +| `canopy_eth_rpc_connection_errors_total` | Counter | `error_type` | RPC connection errors by error type | +| `canopy_eth_ws_connection_attempts_total` | Counter | - | Total WebSocket connection attempts | +| `canopy_eth_ws_subscription_errors_total` | Counter | - | WebSocket subscription failures | +| `canopy_eth_connection_state` | Gauge | - | Current connection state | +| `canopy_eth_sync_status` | Gauge | - | Current sync status | + +**Connection State Values:** +- `0` = Disconnected +- `1` = Connecting +- `2` = RPC Connected +- `3` = Fully Connected (RPC + WebSocket) + +**Sync Status Values:** +- `0` = Unsynced +- `1` = Syncing +- `2` = Synced + +### Block Height Metrics + +| Metric Name | Type | Description | +|-------------|------|-------------| +| `canopy_eth_chain_head_height` | Gauge | Latest block height from chain head | +| `canopy_eth_last_processed_height` | Gauge | Last block height successfully processed | +| `canopy_eth_safe_height` | Gauge | Current safe (confirmed) block height | +| `canopy_eth_block_height_lag` | Gauge | Number of blocks behind chain head | + +### Block Processing Metrics (High Priority) + +| Metric Name | Type | Labels | Description | +|-------------|------|--------|-------------| +| `canopy_eth_block_fetch_errors_total` | Counter | `error_type` | Block fetch errors by error type | +| `canopy_eth_block_processing_timeouts_total` | Counter | - | Blocks that timed out during processing | +| `canopy_eth_process_blocks_batch_size` | Histogram | - | Number of blocks processed per batch | +| `canopy_eth_reorg_detected_total` | Counter | - | Chain reorganizations detected | +| `canopy_eth_blocks_processed_total` | Counter | - | Total Ethereum blocks processed | + +**Histogram Buckets for Batch Size:** 1, 5, 10, 25, 50, 100, 250, 500, 1000 + +### Transaction Processing Metrics (Medium Priority) + +| Metric Name | Type | Labels | Description | +|-------------|------|--------|-------------| +| `canopy_eth_transactions_total` | Counter | - | Total transactions encountered in blocks | +| `canopy_eth_transactions_processed_total` | Counter | - | Total Ethereum transactions processed | +| `canopy_eth_transaction_parse_errors_total` | Counter | `error_type` | Transaction parsing errors by error type | +| `canopy_eth_transaction_retry_by_attempt_total` | Counter | `attempt` | Transaction retry attempts by attempt number | +| `canopy_eth_transaction_exhausted_retries_total` | Counter | - | Transactions that exhausted all retry attempts | +| `canopy_eth_transaction_success_status_total` | Counter | `status` | Transaction success/failed/unknown breakdown | +| `canopy_eth_receipt_fetch_errors_total` | Counter | - | Receipt fetch failures | +| `canopy_eth_transaction_retries_total` | Counter | - | Total transaction processing retries | + +**Status Labels:** +- `success` - Transaction succeeded +- `failed` - Transaction failed +- `unknown` - Unable to determine status + +### Order Detection Metrics (Medium Priority) + +| Metric Name | Type | Labels | Description | +|-------------|------|--------|-------------| +| `canopy_eth_erc20_transfer_detected_total` | Counter | - | ERC20 transfers detected | +| `canopy_eth_lock_order_detected_total` | Counter | - | Lock orders successfully parsed | +| `canopy_eth_close_order_detected_total` | Counter | - | Close orders successfully parsed | +| `canopy_eth_order_validation_errors_total` | Counter | `order_type`, `error_type` | Order validation errors | + +**Order Type Labels:** +- `lock` - Lock order +- `close` - Close order + +### Token Cache Error Metrics (Medium Priority) + +| Metric Name | Type | Labels | Description | +|-------------|------|--------|-------------| +| `canopy_eth_token_info_fetch_errors_total` | Counter | `field` | Token info fetch errors by field | +| `canopy_eth_token_contract_call_timeouts_total` | Counter | - | Token contract call timeouts | +| `canopy_eth_token_cache_hits_total` | Counter | - | Token cache hits | +| `canopy_eth_token_cache_misses_total` | Counter | - | Token cache misses | + +**Field Labels:** +- `name` - Error fetching token name +- `symbol` - Error fetching token symbol +- `decimals` - Error fetching token decimals + +### Connection Error Metrics + +| Metric Name | Type | Description | +|-------------|------|-------------| +| `canopy_eth_connection_errors_total` | Counter | Total Ethereum connection errors | + +--- + +## Example Prometheus Queries + +### Oracle Health + +```promql +# Orders pending confirmation +canopy_oracle_orders_awaiting_confirmation + +# Validation failure rate by reason +rate(canopy_oracle_validation_failures_total[5m]) + +# Order processing success rate +rate(canopy_oracle_orders_validated_total[5m]) / rate(canopy_oracle_orders_witnessed_total[5m]) +``` + +### Ethereum Connectivity + +```promql +# Connection state (should be 3 for healthy) +canopy_eth_connection_state + +# Sync status (should be 2 for synced) +canopy_eth_sync_status + +# Block lag (should be low when synced) +canopy_eth_block_height_lag + +# RPC error rate +rate(canopy_eth_rpc_connection_errors_total[5m]) +``` + +### Block Processing + +```promql +# Blocks processed per second +rate(canopy_eth_blocks_processed_total[5m]) + +# Block fetch error rate +rate(canopy_eth_block_fetch_errors_total[5m]) + +# Average batch size +histogram_quantile(0.5, canopy_eth_process_blocks_batch_size) +``` + +### Transaction Processing + +```promql +# Transaction success rate +sum(rate(canopy_eth_transaction_success_status_total{status="success"}[5m])) / +sum(rate(canopy_eth_transaction_success_status_total[5m])) + +# Retry distribution +rate(canopy_eth_transaction_retry_by_attempt_total[5m]) + +# Exhausted retries (potential issues) +rate(canopy_eth_transaction_exhausted_retries_total[5m]) +``` + +### Order Detection + +```promql +# Lock orders detected per minute +rate(canopy_eth_lock_order_detected_total[1m]) * 60 + +# Close orders detected per minute +rate(canopy_eth_close_order_detected_total[1m]) * 60 + +# Order validation error rate +rate(canopy_eth_order_validation_errors_total[5m]) +``` + +--- + +## Alerting Recommendations + +### Critical Alerts + +| Condition | Threshold | Description | +|-----------|-----------|-------------| +| `canopy_eth_connection_state < 3` | > 5 min | Ethereum connection issues | +| `canopy_eth_sync_status < 2` | > 10 min | Oracle not synced | +| `canopy_eth_block_height_lag > 100` | > 5 min | Significant block processing lag | +| `rate(canopy_eth_rpc_connection_errors_total[5m]) > 0.1` | - | High RPC error rate | + +### Warning Alerts + +| Condition | Threshold | Description | +|-----------|-----------|-------------| +| `canopy_eth_block_height_lag > 10` | > 2 min | Block processing falling behind | +| `rate(canopy_oracle_validation_failures_total[5m]) > 0.01` | - | Validation failures occurring | +| `rate(canopy_eth_transaction_exhausted_retries_total[5m]) > 0` | - | Transactions failing after retries | +| `canopy_oracle_orders_awaiting_confirmation > 50` | - | Many orders pending confirmation | + +--- + +## Grafana Dashboard Tips + +1. **Connection Status Panel**: Use stat panel with value mappings for `canopy_eth_connection_state` and `canopy_eth_sync_status` + +2. **Block Heights Panel**: Graph showing `canopy_eth_chain_head_height`, `canopy_eth_last_processed_height`, and `canopy_eth_safe_height` together + +3. **Order Flow Panel**: Stacked graph of `canopy_eth_lock_order_detected_total` and `canopy_eth_close_order_detected_total` rates + +4. **Error Breakdown Panel**: Table showing validation failures grouped by reason label diff --git a/fsm/gov_params.go b/fsm/gov_params.go index 84aec8f68..fa2e46a58 100644 --- a/fsm/gov_params.go +++ b/fsm/gov_params.go @@ -56,7 +56,7 @@ func DefaultParams() *Params { MaxCommitteeSize: 100, EarlyWithdrawalPenalty: 20, DelegateUnstakingBlocks: 2, - MinimumOrderSize: 1000000000, + MinimumOrderSize: 10000, StakePercentForSubsidizedCommittee: 33, MaxSlashPerCommittee: 15, DelegateRewardPercentage: 10, diff --git a/fsm/swap.go b/fsm/swap.go index 2ed270203..739390411 100644 --- a/fsm/swap.go +++ b/fsm/swap.go @@ -15,12 +15,12 @@ import ( // - 'reset' is a 'claimed' order whose 'buyer' did not send the tokens to the seller before the deadline, thus the order is re-opened for sale // - 'close' is a 'claimed' order whose 'buyer' sent the tokens to the seller before the deadline, thus the order is 'closed' and the tokens are moved from escrow to the buyer func (s *StateMachine) HandleCommitteeSwaps(orders *lib.Orders, chainId uint64) { - if orders != nil { + if orders != nil && (s.Config.ChainId != 1 && s.Config.ChainId != chainId) { // allow root chain to 'swap to self' // lock orders are a result of the committee witnessing a 'reserve transaction' for the order on the 'buyer chain' // think of 'lock orders' like reserving the 'sell order' for _, lockOrder := range orders.LockOrders { if err := s.LockOrder(lockOrder, chainId); err != nil { - s.log.Warnf("LockOrder failed (can happen due to asynchronicity): %s", err.Error()) + s.log.Warnf("LockOrder %s failed (can happen due to asynchronicity): %s", lib.BytesToString(lockOrder.OrderId), err.Error()) } } // reset orders are a result of the committee witnessing 'no-action' from the buyer of the sell order aka NOT sending the @@ -35,7 +35,7 @@ func (s *StateMachine) HandleCommitteeSwaps(orders *lib.Orders, chainId uint64) // buy assets before the 'deadline height' of the 'buyer chain' for _, closeOrderId := range orders.CloseOrders { if err := s.CloseOrder(closeOrderId, chainId); err != nil { - s.log.Warnf("CloseOrder failed (can happen due to asynchronicity): %s", err.Error()) + s.log.Warnf("CloseOrder %s failed (can happen due to asynchronicity): %s", lib.BytesToString(closeOrderId), err.Error()) } } } diff --git a/go.mod b/go.mod index ffc56dfd2..666e2e90d 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.11.1 + github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/crypto v0.42.0 golang.org/x/mod v0.29.0 golang.org/x/net v0.44.0 @@ -40,11 +41,12 @@ require ( require ( github.com/DataDog/zstd v1.5.7 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RaduBerinde/axisds v0.0.0-20250419182453-5135a0650657 // indirect github.com/RaduBerinde/btreemap v0.0.0-20250419232817-bf0d809ae648 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/crlib v0.0.0-20251024155502-a2e0a212ef05 // indirect github.com/cockroachdb/errors v1.12.0 // indirect @@ -57,10 +59,12 @@ require ( github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/getsentry/sentry-go v0.36.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/holiman/uint256 v1.3.2 // indirect @@ -71,19 +75,24 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/minio/minlz v1.0.1 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.1 // indirect github.com/prometheus/procfs v0.19.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/supranational/blst v0.3.14 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/sys v0.37.0 // indirect diff --git a/go.sum b/go.sum index 29d265db5..e553f77b1 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/RaduBerinde/axisds v0.0.0-20250419182453-5135a0650657 h1:8XBWWQD+vFF+JqOsm16t0Kab1a7YWV8+GISVEP8AuZ8= github.com/RaduBerinde/axisds v0.0.0-20250419182453-5135a0650657/go.mod h1:UHGJonU9z4YYGKJxSaC6/TNcLOBptpmM5m2Cksbnw0Y= github.com/RaduBerinde/btreemap v0.0.0-20250419232817-bf0d809ae648 h1:0s1dtMVp3XcQ1tHazU9OCLCKoqj4TRD8GFU5SscItMM= @@ -18,8 +20,8 @@ github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3b github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= -github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -30,10 +32,14 @@ github.com/cockroachdb/datadriven v1.0.3-0.20250407164829-2945557346d5 h1:UycK/E github.com/cockroachdb/datadriven v1.0.3-0.20250407164829-2945557346d5/go.mod h1:jsaKMvD3RBCATk1/jbUZM8C9idWBJME9+VRZ5+Liq1g= github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo= github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895 h1:XANOgPYtvELQ/h4IrmPAohXqe2pWA8Bwhejr3VQoZsA= github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895/go.mod h1:aPd7gM9ov9M8v32Yy5NJrDyOcD8z642dqs+F0CeNXfA= +github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= +github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= github.com/cockroachdb/pebble/v2 v2.1.1 h1:sUpUJjorLDSL4zIRFqoduCBaf2LewaMUXOoOpK+MrXQ= github.com/cockroachdb/pebble/v2 v2.1.1/go.mod h1:Aza05DCCc05ghIJZkB4Q/axv/JK9wx5cFwWcnhG0eGw= github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= @@ -46,6 +52,7 @@ github.com/consensys/bavard v0.1.27 h1:j6hKUrGAy/H+gpNrpLU3I26n1yc+VMGmd6ID5+gAh github.com/consensys/bavard v0.1.27/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/gnark-crypto v0.16.0 h1:8Dl4eYmUWK9WmlP1Bj6je688gBRJCJbT8Mw4KoTAawo= github.com/consensys/gnark-crypto v0.16.0/go.mod h1:Ke3j06ndtPTVvo++PhGNgvm+lgpLvzbcE2MqljY7diU= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI= github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= @@ -57,6 +64,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= @@ -86,6 +95,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -97,12 +108,22 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= @@ -126,10 +147,15 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= @@ -147,6 +173,16 @@ github.com/phuslu/iploc v1.0.20240731 h1:4fLlMTe1bGrKfraWclGGC3WqndKzDv7xpKgVRvX github.com/phuslu/iploc v1.0.20240731/go.mod h1:VZqAWoi2A80YPvfk1AizLGHavNIG9nhBC8d87D/SeVs= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -167,6 +203,7 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= @@ -178,6 +215,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -186,10 +224,22 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= @@ -238,6 +288,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/lib/certificate.go b/lib/certificate.go index e955e3b3f..225fdafe0 100644 --- a/lib/certificate.go +++ b/lib/certificate.go @@ -817,6 +817,27 @@ func (x *LockOrder) UnmarshalJSON(jsonBytes []byte) (err error) { return } +// Equals() compares two LockOrders for equality +func (x *CloseOrder) Equals(y *CloseOrder) bool { + // if both the lock orders are empty + if x == nil && y == nil { + // exit with 'equal' + return true + } + // if either of the lock orders are empty + if x == nil || y == nil { + // exit with 'unequal' + return false + } + // if the chain ids aren't the same + if x.ChainId != y.ChainId { + // exit with 'unequal' + return false + } + // if the order ids are not the same + return bytes.Equal(x.OrderId, y.OrderId) +} + // closeOrderJSON implements the json.Marshaller & json.Unmarshaler interfaces for LockOrder type closeOrderJSON struct { // order_id: is the number id that is unique to this committee to identify the order diff --git a/lib/config.go b/lib/config.go index 1579f816b..88b43eae2 100644 --- a/lib/config.go +++ b/lib/config.go @@ -41,6 +41,9 @@ type Config struct { ConsensusConfig // bft options MempoolConfig // mempool options MetricsConfig // telemetry options + + EthBlockProviderConfig `json:"ethBlockProviderConfig"` // ethereum block provider configuration + OracleConfig `json:"oracleConfig"` // oracle configuration } // DefaultConfig() returns a Config with developer set options @@ -54,6 +57,9 @@ func DefaultConfig() Config { ConsensusConfig: DefaultConsensusConfig(), MempoolConfig: DefaultMempoolConfig(), MetricsConfig: DefaultMetricsConfig(), + + EthBlockProviderConfig: DefaultEthBlockProviderConfig(), + OracleConfig: DefaultOracleConfig(), } } @@ -332,6 +338,51 @@ func DefaultMetricsConfig() MetricsConfig { } } +type EthBlockProviderConfig struct { + NodeUrl string `json:"ethNodeUrl"` // ethereum rpc node url + NodeWSUrl string `json:"ethNodeWsUrl"` // ethereum node websocket url + EVMChainId uint64 `json:"evmChainId"` // ethereum chain id + RetryDelay int `json:"retryDelay"` // retry delay in seconds for connection failures + StartupBlockDepth uint64 `json:"startupBlockDepth"` // how far back to start processing blocks when no next height was provided +} + +// DefaultEthBlockProviderConfig() returns the default ethereum block provider configuration +func DefaultEthBlockProviderConfig() EthBlockProviderConfig { + return EthBlockProviderConfig{ + NodeUrl: "http://localhost:8545", + NodeWSUrl: "ws://localhost:8545", + EVMChainId: 1, + RetryDelay: 5, // default 5 seconds reconnect retry delay + StartupBlockDepth: 1000, + } +} + +// OracleConfig represents the configuration of the off-chain order witness oracle +type OracleConfig struct { + OracleEnabled bool `json:"oracleEnabled"` // enables or disables the oracle functionality + StateFile string `json:"stateSaveFile"` // file to save oracle state + OrderResubmitDelayBlocks uint64 `json:"orderResubmitDelayBlocks"` // how many root blocks to wait to resubmit order + Committee uint64 `json:"committee"` // committee this oracle will witnessed orders for + ProposeDelayBlocks uint64 `json:"proposeDelayBlocks"` // oracle will wait this number of source chain blocks before including a newly witnessed order in a proposed block + ReorgRollbackBlocks uint64 `json:"reorgRollbackBlocks"` // how far back to rollback the order store on reorgs + LockOrderCooldownBlocks uint64 `json:"lockOrderCooldownBlocks"` // how many root blocks to wait to prevent resubmission of lock orders with same ID + SafeBlockConfirmations uint64 `json:"safeBlockConfirmations"` // number of block confirmations required before considering a block safe +} + +// DefaultOracleConfig() returns the default ethereum block provider configuration +func DefaultOracleConfig() OracleConfig { + return OracleConfig{ + OracleEnabled: false, + StateFile: "oracle.state", + OrderResubmitDelayBlocks: 2, + Committee: 2, + ProposeDelayBlocks: 3, + ReorgRollbackBlocks: 60, + LockOrderCooldownBlocks: 2, + SafeBlockConfirmations: 5, + } +} + // WriteToFile() saves the Config object to a JSON file func (c Config) WriteToFile(filepath string) error { // convert the config to indented 'pretty' json bytes diff --git a/lib/config_test.go b/lib/config_test.go index 7687f2b83..e8210d62b 100644 --- a/lib/config_test.go +++ b/lib/config_test.go @@ -12,14 +12,16 @@ import ( func TestDefaultConfig(t *testing.T) { // calculate expected expected := Config{ - MainConfig: DefaultMainConfig(), - RPCConfig: DefaultRPCConfig(), - StateMachineConfig: DefaultStateMachineConfig(), - StoreConfig: DefaultStoreConfig(), - P2PConfig: DefaultP2PConfig(), - ConsensusConfig: DefaultConsensusConfig(), - MempoolConfig: DefaultMempoolConfig(), - MetricsConfig: DefaultMetricsConfig(), + MainConfig: DefaultMainConfig(), + RPCConfig: DefaultRPCConfig(), + StateMachineConfig: DefaultStateMachineConfig(), + StoreConfig: DefaultStoreConfig(), + P2PConfig: DefaultP2PConfig(), + ConsensusConfig: DefaultConsensusConfig(), + MempoolConfig: DefaultMempoolConfig(), + MetricsConfig: DefaultMetricsConfig(), + EthBlockProviderConfig: DefaultEthBlockProviderConfig(), + OracleConfig: DefaultOracleConfig(), } // execute the function call got := DefaultConfig() diff --git a/lib/log.go b/lib/log.go index 56542b2be..74c278aad 100644 --- a/lib/log.go +++ b/lib/log.go @@ -198,6 +198,34 @@ func (l *Logger) write(msg string) { } } +// NewLogger() creates a new Logger instance with the specified configuration and optional data directory path +func NewOracleLogger(config LoggerConfig, dataDirPath ...string) LoggerI { + if config.Out == nil { + if dataDirPath == nil || dataDirPath[0] == "" { + dataDirPath = make([]string, 1) + dataDirPath[0] = DefaultDataDirPath() + } + logPath := filepath.Join(dataDirPath[0], LogDirectory, LogFileName) + if _, err := os.Stat(logPath); errors.Is(err, os.ErrNotExist) { + if err = os.MkdirAll(filepath.Join(dataDirPath[0], LogDirectory), os.ModePerm); err != nil { + fmt.Println(err) + panic(err) + } + } + logFile := &lumberjack.Logger{ + Filename: logPath, + MaxSize: 1, // megabyte + MaxBackups: 1500, + MaxAge: 14, // days + Compress: true, + } + config.Out = logFile + } + return &Logger{ + config: config, + } +} + // NewLogger() creates a new Logger instance with the specified configuration and optional data directory path func NewLogger(config LoggerConfig, dataDirPath ...string) LoggerI { if config.Out == nil { diff --git a/lib/lru_cache.go b/lib/lru_cache.go new file mode 100644 index 000000000..1243c941f --- /dev/null +++ b/lib/lru_cache.go @@ -0,0 +1,118 @@ +package lib + +import ( + "sync" + "time" +) + +// LRUCacheEntry represents a single cache entry with value and access tracking +type LRUCacheEntry[V any] struct { + // value is the cached data + value V + // lastAccess tracks when this entry was last accessed for LRU eviction + lastAccess time.Time +} + +// LRUCache is a generic Least Recently Used cache with size limit +type LRUCache[V any] struct { + // mutex protects concurrent access to the cache + mutex sync.RWMutex + // maxSize is the maximum number of entries the cache can hold + maxSize int + // entries maps keys to cache entries for O(1) lookup + entries map[string]*LRUCacheEntry[V] +} + +// NewLRUCache creates a new LRU cache with the specified maximum size +func NewLRUCache[V any](maxSize int) *LRUCache[V] { + return &LRUCache[V]{ + maxSize: maxSize, + entries: make(map[string]*LRUCacheEntry[V]), + } +} + +// Get retrieves a value from the cache and marks it as recently used +func (c *LRUCache[V]) Get(key string) (V, bool) { + c.mutex.Lock() + defer c.mutex.Unlock() + // check if entry exists + entry, exists := c.entries[key] + if !exists { + var zero V + return zero, false + } + // update last access time + entry.lastAccess = time.Now() + return entry.value, true +} + +// Put adds or updates a value in the cache +func (c *LRUCache[V]) Put(key string, value V) { + c.mutex.Lock() + defer c.mutex.Unlock() + now := time.Now() + // check if entry already exists + if entry, exists := c.entries[key]; exists { + // update existing entry + entry.value = value + entry.lastAccess = now + return + } + // check if cache is at capacity + if len(c.entries) >= c.maxSize { + // find and remove least recently used entry + c.evictLRUUnsafe() + } + // create and add new entry + c.entries[key] = &LRUCacheEntry[V]{ + value: value, + lastAccess: now, + } +} + +// Remove deletes a specific key from the cache +func (c *LRUCache[V]) Remove(key string) bool { + c.mutex.Lock() + defer c.mutex.Unlock() + // check if entry exists + if _, exists := c.entries[key]; !exists { + return false + } + // remove entry from cache + delete(c.entries, key) + return true +} + +// Clear removes all entries from the cache +func (c *LRUCache[V]) Clear() { + c.mutex.Lock() + defer c.mutex.Unlock() + // clear all entries + c.entries = make(map[string]*LRUCacheEntry[V]) +} + +// Size returns the current number of entries in the cache +func (c *LRUCache[V]) Size() int { + c.mutex.RLock() + defer c.mutex.RUnlock() + return len(c.entries) +} + +// evictLRUUnsafe removes the least recently used entry (not thread-safe) +func (c *LRUCache[V]) evictLRUUnsafe() { + var oldestKey string + var oldestTime time.Time + first := true + // find the entry with the oldest last access time + for key, entry := range c.entries { + if first || entry.lastAccess.Before(oldestTime) { + oldestKey = key + oldestTime = entry.lastAccess + first = false + } + } + // remove the oldest entry + if oldestKey != "" { + delete(c.entries, oldestKey) + } +} diff --git a/lib/metrics.go b/lib/metrics.go index a107d1fe5..0bcc8be6b 100644 --- a/lib/metrics.go +++ b/lib/metrics.go @@ -3,6 +3,7 @@ package lib import ( "bytes" "context" + "fmt" "net/http" "time" @@ -72,6 +73,9 @@ type Metrics struct { FSMMetrics // fsm telemetry StoreMetrics // persistence telemetry MempoolMetrics // tx memory pool telemetry + + OracleMetrics // oracle telemetry + EthBlockProviderMetrics // ethereum block provider telemetry } // NodeMetrics represents general telemetry for the node's health @@ -166,6 +170,115 @@ type MempoolMetrics struct { MempoolTxCount prometheus.Gauge // how many transactions are in the mempool? } +// OracleMetrics represents the telemetry for the Oracle module +type OracleMetrics struct { + // Block processing metrics + OracleBlockProcessingTime prometheus.Histogram // how long does it take to process blocks? + OrderValidationTime prometheus.Histogram // how long does it take to validate orders? + // Order counting metrics + OrdersWitnessed prometheus.Counter // total orders witnessed + OrdersValidated prometheus.Counter // total orders validated successfully + OrdersSubmitted prometheus.Counter // total orders submitted for consensus + OrdersRejected prometheus.Counter // total orders rejected during validation + // Order store metrics + TotalOrdersStored prometheus.Gauge // total orders currently stored in order store + LockOrdersStored prometheus.Gauge // total lock orders currently stored + CloseOrdersStored prometheus.Gauge // total close orders currently stored + // State management metrics + SafeHeight prometheus.Gauge // current safe block height + SourceChainHeight prometheus.Gauge // current source chain height + LockOrderSubmissionsSize prometheus.Gauge // size of lock order submissions map + CloseOrderSubmissionsSize prometheus.Gauge // size of close order submissions map + // Error and reorg metrics + ChainReorgs prometheus.Counter // total chain reorganizations detected + OrdersPruned prometheus.Counter // total orders pruned during cleanup + BlockProcessingErrors prometheus.Counter // total block processing errors + // Performance metrics + OrderBookUpdateTime prometheus.Histogram // how long does it take to update order book? + RootChainSyncTime prometheus.Histogram // how long does it take to sync with root chain? + + // Block height metrics + LastProcessedHeight prometheus.Gauge // last source chain block height processed + ConfirmationLag prometheus.Gauge // gap between source chain height and safe height + OrdersAwaitingConfirmation prometheus.Gauge // orders witnessed but not yet at safe height + ReorgRollbackDepth prometheus.Histogram // how many blocks reorgs roll back + + // Order lifecycle metrics + OrdersNotInOrderbook prometheus.Counter // orders witnessed but not found in order book + OrdersDuplicate prometheus.Counter // duplicate orders (already in store) + OrdersArchived prometheus.Counter // orders successfully archived + LockOrdersCommitted prometheus.Counter // lock orders committed via certificate + CloseOrdersCommitted prometheus.Counter // close orders committed via certificate + + // Validation failure metrics + ValidationFailures *prometheus.CounterVec // validation failures by reason + + // Submission tracking metrics + OrdersHeldAwaitingSafe prometheus.Counter // orders not submitted due to safe height + OrdersHeldProposeDelay prometheus.Counter // orders held by ProposeDelayBlocks + OrdersHeldResubmitDelay prometheus.Counter // orders held by resubmit cooldown + LockOrderResubmissions prometheus.Counter // lock orders resubmitted + CloseOrderResubmissions prometheus.Counter // close orders resubmitted + + // Store operation metrics + StoreWriteErrors prometheus.Counter // order store write failures + StoreReadErrors prometheus.Counter // order store read failures + StoreRemoveErrors prometheus.Counter // order store remove failures +} + +// EthBlockProviderMetrics represents the telemetry for the Ethereum block provider +type EthBlockProviderMetrics struct { + // Block and transaction processing metrics + BlockFetchTime prometheus.Histogram // how long does it take to fetch Ethereum blocks? + TransactionProcessTime prometheus.Histogram // how long does it take to process Ethereum transactions? + ReceiptFetchTime prometheus.Histogram // how long does it take to fetch transaction receipts? + // Token cache metrics + TokenCacheHits prometheus.Counter // total ERC20 token cache hits + TokenCacheMisses prometheus.Counter // total ERC20 token cache misses + // Connection and error metrics + ConnectionErrors prometheus.Counter // total Ethereum connection errors + // Processing counters + BlocksProcessed prometheus.Counter // total Ethereum blocks processed + TransactionsProcessed prometheus.Counter // total Ethereum transactions processed + TransactionRetries prometheus.Counter // total Ethereum transaction processing retries + + // Connection & Sync Status Metrics (High Priority) + RPCConnectionAttempts prometheus.Counter // total RPC connection attempts + RPCConnectionErrors *prometheus.CounterVec // RPC connection errors by error type + WSConnectionAttempts prometheus.Counter // total WebSocket connection attempts + WSSubscriptionErrors prometheus.Counter // WebSocket subscription failures + ConnectionState prometheus.Gauge // current connection state (0=disconnected, 1=connecting, 2=rpc_connected, 3=fully_connected) + SyncStatus prometheus.Gauge // sync status (0=unsynced, 1=syncing, 2=synced) + BlockHeightLag prometheus.Gauge // blocks behind chain head + ChainHeadHeight prometheus.Gauge // latest block height from chain head + EthLastProcessedHeight prometheus.Gauge // last block height successfully processed + EthSafeHeight prometheus.Gauge // current safe (confirmed) block height + + // Block Processing Metrics (High Priority) + BlockFetchErrors *prometheus.CounterVec // block fetch errors by error type + BlockProcessingTimeouts prometheus.Counter // blocks that timed out during processing + ProcessBlocksBatchSize prometheus.Histogram // blocks processed per batch + ReorgDetected prometheus.Counter // chain reorganizations detected + + // Transaction Processing Metrics (Medium Priority) + TransactionsTotal prometheus.Counter // total transactions encountered + TransactionParseErrors *prometheus.CounterVec // TX parsing errors by error type + TransactionRetryByAttempt *prometheus.CounterVec // retry attempts by attempt number + TransactionExhaustedRetries prometheus.Counter // transactions that exhausted all retries + TransactionSuccessStatus *prometheus.CounterVec // TX success/failed/unknown breakdown + ReceiptFetchErrors prometheus.Counter // receipt fetch failures + + // Order Detection Metrics (Medium Priority) + ERC20TransferDetected prometheus.Counter // ERC20 transfers detected + LockOrderDetected prometheus.Counter // lock orders successfully parsed + CloseOrderDetected prometheus.Counter // close orders successfully parsed + OrderValidationErrors *prometheus.CounterVec // order validation errors by order_type and error_type + + // Token Cache Error Metrics (Medium Priority) + TokenInfoFetchErrors *prometheus.CounterVec // token info fetch errors by field + TokenContractCallTimeouts prometheus.Counter // token contract call timeouts +} + // NewMetricsServer() creates a new telemetry server func NewMetricsServer(nodeAddress crypto.AddressI, chainID float64, softwareVersion string, config MetricsConfig, logger LoggerI) *Metrics { mux := http.NewServeMux() @@ -448,6 +561,308 @@ func NewMetricsServer(nodeAddress crypto.AddressI, chainID float64, softwareVers Help: "Count of transactions in the transaction memory pool", }), }, + // ORACLE + OracleMetrics: OracleMetrics{ + OracleBlockProcessingTime: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_oracle_block_processing_time", + Help: "Time to process blocks in the oracle in seconds", + }), + OrderValidationTime: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_oracle_order_validation_time", + Help: "Time to validate orders in the oracle in seconds", + }), + OrdersWitnessed: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_witnessed_total", + Help: "Total number of orders witnessed from Ethereum", + }), + OrdersValidated: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_validated_total", + Help: "Total number of orders validated successfully", + }), + OrdersSubmitted: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_submitted_total", + Help: "Total number of orders submitted for consensus", + }), + OrdersRejected: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_rejected_total", + Help: "Total number of orders rejected during validation", + }), + TotalOrdersStored: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_total_orders_stored", + Help: "Total number of orders currently stored in order store", + }), + LockOrdersStored: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_lock_orders_stored", + Help: "Total number of lock orders currently stored", + }), + CloseOrdersStored: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_close_orders_stored", + Help: "Total number of close orders currently stored", + }), + SafeHeight: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_safe_height", + Help: "Current safe block height in the oracle", + }), + SourceChainHeight: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_source_chain_height", + Help: "Current source chain height in the oracle", + }), + LockOrderSubmissionsSize: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_lock_order_submissions_size", + Help: "Size of the lock order submissions map", + }), + CloseOrderSubmissionsSize: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_close_order_submissions_size", + Help: "Size of the close order submissions map", + }), + ChainReorgs: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_chain_reorgs_total", + Help: "Total number of chain reorganizations detected", + }), + OrdersPruned: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_pruned_total", + Help: "Total number of orders pruned during cleanup", + }), + BlockProcessingErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_block_processing_errors_total", + Help: "Total number of block processing errors", + }), + OrderBookUpdateTime: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_oracle_order_book_update_time", + Help: "Time to update order book in the oracle in seconds", + }), + RootChainSyncTime: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_oracle_root_chain_sync_time", + Help: "Time to sync with root chain in the oracle in seconds", + }), + // Block height metrics + LastProcessedHeight: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_last_processed_height", + Help: "Last source chain block height processed by the oracle", + }), + ConfirmationLag: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_confirmation_lag", + Help: "Gap between source chain height and safe height (blocks awaiting confirmation)", + }), + OrdersAwaitingConfirmation: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_oracle_orders_awaiting_confirmation", + Help: "Number of orders witnessed but not yet at safe height", + }), + ReorgRollbackDepth: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_oracle_reorg_rollback_depth", + Help: "Number of blocks rolled back during chain reorganizations", + Buckets: []float64{1, 2, 5, 10, 20, 50, 100}, + }), + // Order lifecycle metrics + OrdersNotInOrderbook: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_not_in_orderbook_total", + Help: "Total orders witnessed but not found in order book", + }), + OrdersDuplicate: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_duplicate_total", + Help: "Total duplicate orders encountered (already in store)", + }), + OrdersArchived: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_archived_total", + Help: "Total orders successfully archived", + }), + LockOrdersCommitted: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_lock_orders_committed_total", + Help: "Total lock orders committed via certificate", + }), + CloseOrdersCommitted: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_close_orders_committed_total", + Help: "Total close orders committed via certificate", + }), + // Validation failure metrics + ValidationFailures: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "canopy_oracle_validation_failures_total", + Help: "Total validation failures by reason", + }, []string{"reason"}), + // Submission tracking metrics + OrdersHeldAwaitingSafe: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_held_awaiting_safe_total", + Help: "Total orders not submitted due to safe height requirement", + }), + OrdersHeldProposeDelay: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_held_propose_delay_total", + Help: "Total orders held by ProposeDelayBlocks configuration", + }), + OrdersHeldResubmitDelay: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_orders_held_resubmit_delay_total", + Help: "Total orders held by resubmit cooldown", + }), + LockOrderResubmissions: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_lock_order_resubmissions_total", + Help: "Total lock orders resubmitted", + }), + CloseOrderResubmissions: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_close_order_resubmissions_total", + Help: "Total close orders resubmitted", + }), + // Store operation metrics + StoreWriteErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_store_write_errors_total", + Help: "Total order store write failures", + }), + StoreReadErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_store_read_errors_total", + Help: "Total order store read failures", + }), + StoreRemoveErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_oracle_store_remove_errors_total", + Help: "Total order store remove failures", + }), + }, + // ETH + EthBlockProviderMetrics: EthBlockProviderMetrics{ + BlockFetchTime: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_eth_block_fetch_time", + Help: "Time to fetch Ethereum blocks in seconds", + }), + TransactionProcessTime: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_eth_transaction_process_time", + Help: "Time to process Ethereum transactions in seconds", + }), + ReceiptFetchTime: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_eth_receipt_fetch_time", + Help: "Time to fetch Ethereum transaction receipts in seconds", + }), + TokenCacheHits: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_token_cache_hits_total", + Help: "Total number of ERC20 token cache hits", + }), + TokenCacheMisses: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_token_cache_misses_total", + Help: "Total number of ERC20 token cache misses", + }), + ConnectionErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_connection_errors_total", + Help: "Total number of Ethereum connection errors", + }), + BlocksProcessed: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_blocks_processed_total", + Help: "Total number of Ethereum blocks processed", + }), + TransactionsProcessed: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_transactions_processed_total", + Help: "Total number of Ethereum transactions processed", + }), + TransactionRetries: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_transaction_retries_total", + Help: "Total number of Ethereum transaction processing retries", + }), + // Connection & Sync Status Metrics + RPCConnectionAttempts: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_rpc_connection_attempts_total", + Help: "Total RPC connection attempts", + }), + RPCConnectionErrors: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "canopy_eth_rpc_connection_errors_total", + Help: "RPC connection errors by error type", + }, []string{"error_type"}), + WSConnectionAttempts: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_ws_connection_attempts_total", + Help: "Total WebSocket connection attempts", + }), + WSSubscriptionErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_ws_subscription_errors_total", + Help: "WebSocket subscription failures", + }), + ConnectionState: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_eth_connection_state", + Help: "Current connection state (0=disconnected, 1=connecting, 2=rpc_connected, 3=fully_connected)", + }), + SyncStatus: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_eth_sync_status", + Help: "Sync status (0=unsynced, 1=syncing, 2=synced)", + }), + BlockHeightLag: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_eth_block_height_lag", + Help: "Number of blocks behind chain head", + }), + ChainHeadHeight: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_eth_chain_head_height", + Help: "Latest block height from chain head", + }), + EthLastProcessedHeight: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_eth_last_processed_height", + Help: "Last block height successfully processed", + }), + EthSafeHeight: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_eth_safe_height", + Help: "Current safe (confirmed) block height", + }), + // Block Processing Metrics + BlockFetchErrors: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "canopy_eth_block_fetch_errors_total", + Help: "Block fetch errors by error type", + }, []string{"error_type"}), + BlockProcessingTimeouts: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_block_processing_timeouts_total", + Help: "Blocks that timed out during processing", + }), + ProcessBlocksBatchSize: promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "canopy_eth_process_blocks_batch_size", + Help: "Number of blocks processed per batch", + Buckets: []float64{1, 5, 10, 25, 50, 100, 250, 500, 1000}, + }), + ReorgDetected: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_reorg_detected_total", + Help: "Chain reorganizations detected", + }), + // Transaction Processing Metrics + TransactionsTotal: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_transactions_total", + Help: "Total transactions encountered in blocks", + }), + TransactionParseErrors: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "canopy_eth_transaction_parse_errors_total", + Help: "Transaction parsing errors by error type", + }, []string{"error_type"}), + TransactionRetryByAttempt: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "canopy_eth_transaction_retry_by_attempt_total", + Help: "Transaction retry attempts by attempt number", + }, []string{"attempt"}), + TransactionExhaustedRetries: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_transaction_exhausted_retries_total", + Help: "Transactions that exhausted all retry attempts", + }), + TransactionSuccessStatus: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "canopy_eth_transaction_success_status_total", + Help: "Transaction success/failed/unknown breakdown", + }, []string{"status"}), + ReceiptFetchErrors: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_receipt_fetch_errors_total", + Help: "Receipt fetch failures", + }), + // Order Detection Metrics + ERC20TransferDetected: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_erc20_transfer_detected_total", + Help: "ERC20 transfers detected", + }), + LockOrderDetected: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_lock_order_detected_total", + Help: "Lock orders successfully parsed", + }), + CloseOrderDetected: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_close_order_detected_total", + Help: "Close orders successfully parsed", + }), + OrderValidationErrors: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "canopy_eth_order_validation_errors_total", + Help: "Order validation errors by order type and error type", + }, []string{"order_type", "error_type"}), + // Token Cache Error Metrics + TokenInfoFetchErrors: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "canopy_eth_token_info_fetch_errors_total", + Help: "Token info fetch errors by field", + }, []string{"field"}), + TokenContractCallTimeouts: promauto.NewCounter(prometheus.CounterOpts{ + Name: "canopy_eth_token_contract_call_timeouts_total", + Help: "Token contract call timeouts", + }), + }, } } @@ -751,3 +1166,366 @@ func (m *Metrics) UpdateValidatorCount(count int) { // update the metric m.ValidatorCount.WithLabelValues("total").Set(float64(count)) } + +// UpdateOracleBlockMetrics() updates oracle block processing metrics +func (m *Metrics) UpdateOracleBlockMetrics(processingTime time.Duration) { + // exit if empty + if m == nil { + return + } + // update the block processing time + m.OracleBlockProcessingTime.Observe(processingTime.Seconds()) +} + +// UpdateOracleOrderMetrics() updates oracle order processing metrics +func (m *Metrics) UpdateOracleOrderMetrics(witnessed, validated, submitted, rejected int, validationTime time.Duration) { + // exit if empty + if m == nil { + return + } + // update counters + m.OrdersWitnessed.Add(float64(witnessed)) + m.OrdersValidated.Add(float64(validated)) + m.OrdersSubmitted.Add(float64(submitted)) + m.OrdersRejected.Add(float64(rejected)) + // update timing metrics + if validationTime > 0 { + m.OrderValidationTime.Observe(validationTime.Seconds()) + } +} + +// UpdateOracleStateMetrics() updates oracle state management metrics +func (m *Metrics) UpdateOracleStateMetrics(safeHeight, sourceHeight uint64, lockOrderSubmissionsSize, closeOrderSubmissionsSize int) { + // exit if empty + if m == nil { + return + } + // update state metrics + m.SafeHeight.Set(float64(safeHeight)) + m.SourceChainHeight.Set(float64(sourceHeight)) + m.LockOrderSubmissionsSize.Set(float64(lockOrderSubmissionsSize)) + m.CloseOrderSubmissionsSize.Set(float64(closeOrderSubmissionsSize)) +} + +// UpdateOracleStoreMetrics() updates oracle order store metrics +func (m *Metrics) UpdateOracleStoreMetrics(lockOrders, closeOrders int) { + // exit if empty + if m == nil { + return + } + // update store count metrics + m.TotalOrdersStored.Set(float64(lockOrders + closeOrders)) + m.LockOrdersStored.Set(float64(lockOrders)) + m.CloseOrdersStored.Set(float64(closeOrders)) +} + +// UpdateOracleErrorMetrics() updates oracle error and reorg metrics +func (m *Metrics) UpdateOracleErrorMetrics(reorgs, pruned, blockErrors int) { + // exit if empty + if m == nil { + return + } + // update error counters + m.ChainReorgs.Add(float64(reorgs)) + m.OrdersPruned.Add(float64(pruned)) + m.BlockProcessingErrors.Add(float64(blockErrors)) +} + +// UpdateEthBlockProviderMetrics() updates Ethereum block provider metrics +func (m *Metrics) UpdateEthBlockProviderMetrics(blockFetchTime, transactionProcessTime, receiptFetchTime time.Duration, + cacheHits, cacheMisses, connectionErrors, blocksProcessed, transactionsProcessed, retries int) { + // exit if empty + if m == nil { + return + } + // update timing metrics + if blockFetchTime > 0 { + m.BlockFetchTime.Observe(blockFetchTime.Seconds()) + } + if transactionProcessTime > 0 { + m.TransactionProcessTime.Observe(transactionProcessTime.Seconds()) + } + if receiptFetchTime > 0 { + m.ReceiptFetchTime.Observe(receiptFetchTime.Seconds()) + } + // update counters + m.TokenCacheHits.Add(float64(cacheHits)) + m.TokenCacheMisses.Add(float64(cacheMisses)) + m.ConnectionErrors.Add(float64(connectionErrors)) + m.BlocksProcessed.Add(float64(blocksProcessed)) + m.TransactionsProcessed.Add(float64(transactionsProcessed)) + m.TransactionRetries.Add(float64(retries)) +} + +// UpdateOracleHeightMetrics() updates oracle block height tracking metrics +func (m *Metrics) UpdateOracleHeightMetrics(lastHeight, safeHeight, sourceHeight uint64, awaitingConfirmation int) { + if m == nil { + return + } + m.LastProcessedHeight.Set(float64(lastHeight)) + m.ConfirmationLag.Set(float64(sourceHeight - safeHeight)) + m.OrdersAwaitingConfirmation.Set(float64(awaitingConfirmation)) +} + +// RecordOracleReorgDepth() records the depth of a chain reorganization rollback +func (m *Metrics) RecordOracleReorgDepth(depth uint64) { + if m == nil { + return + } + m.ReorgRollbackDepth.Observe(float64(depth)) +} + +// IncrementValidationFailure() increments the validation failure counter for a specific reason +func (m *Metrics) IncrementValidationFailure(reason string) { + if m == nil { + return + } + m.ValidationFailures.WithLabelValues(reason).Inc() +} + +// UpdateOracleLifecycleMetrics() updates order lifecycle metrics +func (m *Metrics) UpdateOracleLifecycleMetrics(notInOrderbook, duplicate, archived, lockCommitted, closeCommitted int) { + if m == nil { + return + } + m.OrdersNotInOrderbook.Add(float64(notInOrderbook)) + m.OrdersDuplicate.Add(float64(duplicate)) + m.OrdersArchived.Add(float64(archived)) + m.LockOrdersCommitted.Add(float64(lockCommitted)) + m.CloseOrdersCommitted.Add(float64(closeCommitted)) +} + +// UpdateOracleSubmissionMetrics() updates submission tracking metrics +func (m *Metrics) UpdateOracleSubmissionMetrics(heldSafe, heldPropose, heldResubmit, lockResub, closeResub int) { + if m == nil { + return + } + m.OrdersHeldAwaitingSafe.Add(float64(heldSafe)) + m.OrdersHeldProposeDelay.Add(float64(heldPropose)) + m.OrdersHeldResubmitDelay.Add(float64(heldResubmit)) + m.LockOrderResubmissions.Add(float64(lockResub)) + m.CloseOrderResubmissions.Add(float64(closeResub)) +} + +// UpdateOracleStoreErrorMetrics() updates store operation error metrics +func (m *Metrics) UpdateOracleStoreErrorMetrics(writeErrors, readErrors, removeErrors int) { + if m == nil { + return + } + m.StoreWriteErrors.Add(float64(writeErrors)) + m.StoreReadErrors.Add(float64(readErrors)) + m.StoreRemoveErrors.Add(float64(removeErrors)) +} + +// ========== Eth Block Provider Metrics Helper Functions ========== + +// SetEthConnectionState sets the current connection state +// States: 0=disconnected, 1=connecting, 2=rpc_connected, 3=fully_connected +func (m *Metrics) SetEthConnectionState(state int) { + if m == nil { + return + } + m.ConnectionState.Set(float64(state)) +} + +// SetEthSyncStatus sets the current sync status +// States: 0=unsynced, 1=syncing, 2=synced +func (m *Metrics) SetEthSyncStatus(status int) { + if m == nil { + return + } + m.SyncStatus.Set(float64(status)) +} + +// SetEthBlockHeightLag sets the number of blocks behind chain head +func (m *Metrics) SetEthBlockHeightLag(lag uint64) { + if m == nil { + return + } + m.BlockHeightLag.Set(float64(lag)) +} + +// SetEthChainHeadHeight sets the latest block height from chain head +func (m *Metrics) SetEthChainHeadHeight(height uint64) { + if m == nil { + return + } + m.ChainHeadHeight.Set(float64(height)) +} + +// SetEthLastProcessedHeight sets the last block height successfully processed +func (m *Metrics) SetEthLastProcessedHeight(height uint64) { + if m == nil { + return + } + m.EthLastProcessedHeight.Set(float64(height)) +} + +// SetEthSafeHeight sets the current safe (confirmed) block height +func (m *Metrics) SetEthSafeHeight(height uint64) { + if m == nil { + return + } + m.EthSafeHeight.Set(float64(height)) +} + +// IncrementEthRPCConnectionAttempt increments the RPC connection attempt counter +func (m *Metrics) IncrementEthRPCConnectionAttempt() { + if m == nil { + return + } + m.RPCConnectionAttempts.Inc() +} + +// IncrementEthRPCConnectionError increments the RPC connection error counter for a specific error type +func (m *Metrics) IncrementEthRPCConnectionError(errorType string) { + if m == nil { + return + } + m.RPCConnectionErrors.WithLabelValues(errorType).Inc() +} + +// IncrementEthWSConnectionAttempt increments the WebSocket connection attempt counter +func (m *Metrics) IncrementEthWSConnectionAttempt() { + if m == nil { + return + } + m.WSConnectionAttempts.Inc() +} + +// IncrementEthWSSubscriptionError increments the WebSocket subscription error counter +func (m *Metrics) IncrementEthWSSubscriptionError() { + if m == nil { + return + } + m.WSSubscriptionErrors.Inc() +} + +// IncrementEthBlockFetchError increments the block fetch error counter for a specific error type +func (m *Metrics) IncrementEthBlockFetchError(errorType string) { + if m == nil { + return + } + m.BlockFetchErrors.WithLabelValues(errorType).Inc() +} + +// IncrementEthBlockProcessingTimeout increments the block processing timeout counter +func (m *Metrics) IncrementEthBlockProcessingTimeout() { + if m == nil { + return + } + m.BlockProcessingTimeouts.Inc() +} + +// RecordEthProcessBlocksBatchSize records the number of blocks processed in a batch +func (m *Metrics) RecordEthProcessBlocksBatchSize(batchSize int) { + if m == nil { + return + } + m.ProcessBlocksBatchSize.Observe(float64(batchSize)) +} + +// IncrementEthReorgDetected increments the chain reorganization detected counter +func (m *Metrics) IncrementEthReorgDetected() { + if m == nil { + return + } + m.ReorgDetected.Inc() +} + +// IncrementEthTransactionsTotal increments the total transactions counter +func (m *Metrics) IncrementEthTransactionsTotal(count int) { + if m == nil { + return + } + m.TransactionsTotal.Add(float64(count)) +} + +// IncrementEthTransactionParseError increments the transaction parse error counter for a specific error type +func (m *Metrics) IncrementEthTransactionParseError(errorType string) { + if m == nil { + return + } + m.TransactionParseErrors.WithLabelValues(errorType).Inc() +} + +// IncrementEthTransactionRetryByAttempt increments the transaction retry counter for a specific attempt number +func (m *Metrics) IncrementEthTransactionRetryByAttempt(attempt int) { + if m == nil { + return + } + m.TransactionRetryByAttempt.WithLabelValues(fmt.Sprintf("%d", attempt)).Inc() +} + +// IncrementEthTransactionExhaustedRetries increments the exhausted retries counter +func (m *Metrics) IncrementEthTransactionExhaustedRetries() { + if m == nil { + return + } + m.TransactionExhaustedRetries.Inc() +} + +// IncrementEthTransactionSuccessStatus increments the transaction success status counter +// Status values: "success", "failed", "unknown" +func (m *Metrics) IncrementEthTransactionSuccessStatus(status string) { + if m == nil { + return + } + m.TransactionSuccessStatus.WithLabelValues(status).Inc() +} + +// IncrementEthReceiptFetchError increments the receipt fetch error counter +func (m *Metrics) IncrementEthReceiptFetchError() { + if m == nil { + return + } + m.ReceiptFetchErrors.Inc() +} + +// IncrementEthERC20TransferDetected increments the ERC20 transfer detected counter +func (m *Metrics) IncrementEthERC20TransferDetected() { + if m == nil { + return + } + m.ERC20TransferDetected.Inc() +} + +// IncrementEthLockOrderDetected increments the lock order detected counter +func (m *Metrics) IncrementEthLockOrderDetected() { + if m == nil { + return + } + m.LockOrderDetected.Inc() +} + +// IncrementEthCloseOrderDetected increments the close order detected counter +func (m *Metrics) IncrementEthCloseOrderDetected() { + if m == nil { + return + } + m.CloseOrderDetected.Inc() +} + +// IncrementEthOrderValidationError increments the order validation error counter +func (m *Metrics) IncrementEthOrderValidationError(orderType, errorType string) { + if m == nil { + return + } + m.OrderValidationErrors.WithLabelValues(orderType, errorType).Inc() +} + +// IncrementEthTokenInfoFetchError increments the token info fetch error counter for a specific field +func (m *Metrics) IncrementEthTokenInfoFetchError(field string) { + if m == nil { + return + } + m.TokenInfoFetchErrors.WithLabelValues(field).Inc() +} + +// IncrementEthTokenContractCallTimeout increments the token contract call timeout counter +func (m *Metrics) IncrementEthTokenContractCallTimeout() { + if m == nil { + return + } + m.TokenContractCallTimeouts.Inc() +} diff --git a/lib/swap.pb.go b/lib/swap.pb.go index a469a888f..ce8810627 100644 --- a/lib/swap.pb.go +++ b/lib/swap.pb.go @@ -7,11 +7,12 @@ package lib import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( @@ -184,6 +185,35 @@ func (x *SellOrder) GetSellersSendAddress() []byte { return nil } +func (x *SellOrder) IsLocked() bool { + if x == nil { + return false + } + if x.BuyerReceiveAddress == nil { + return false + } + return true +} + +// Copy returns a reference to a clone of the SellOrder +func (s *SellOrder) Copy() *SellOrder { + if s == nil { + return nil + } + return &SellOrder{ + Id: append([]byte(nil), s.Id...), + Committee: s.Committee, + Data: append([]byte(nil), s.Data...), + AmountForSale: s.AmountForSale, + RequestedAmount: s.RequestedAmount, + SellerReceiveAddress: append([]byte(nil), s.SellerReceiveAddress...), + BuyerSendAddress: append([]byte(nil), s.BuyerSendAddress...), + BuyerReceiveAddress: append([]byte(nil), s.BuyerReceiveAddress...), + BuyerChainDeadline: s.BuyerChainDeadline, + SellersSendAddress: append([]byte(nil), s.SellersSendAddress...), + } +} + // OrderBooks: is a list of order book objects held in the blockchain state type OrderBooks struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -285,6 +315,26 @@ func (x *OrderBook) GetOrders() []*SellOrder { return nil } +// Copy returns a reference to a clone of the OrderBook +func (x *OrderBook) Copy() *OrderBook { + if x == nil { + return nil + } + + copy := &OrderBook{ + ChainId: x.ChainId, + } + + if x.Orders != nil { + copy.Orders = make([]*SellOrder, len(x.Orders)) + for i, order := range x.Orders { + copy.Orders[i] = order.Copy() + } + } + + return copy +} + var File_swap_proto protoreflect.FileDescriptor const file_swap_proto_rawDesc = "" + diff --git a/lib/util.go b/lib/util.go index 409f893de..bdc733f37 100644 --- a/lib/util.go +++ b/lib/util.go @@ -1046,3 +1046,82 @@ func RandSlice(byteSize uint64) []byte { rand.Read(value) return value } + +// Integer defines a constraint for integer types including big.Int +type Integer interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 +} + +// BigInt converts integer t into a bigInt +func BigInt[T Integer](t T) *big.Int { + switch v := any(t).(type) { + case int: + return big.NewInt(int64(v)) + case int8: + return big.NewInt(int64(v)) + case int16: + return big.NewInt(int64(v)) + case int32: + return big.NewInt(int64(v)) + case int64: + return big.NewInt(v) + case uint: + return new(big.Int).SetUint64(uint64(v)) + case uint8: + return new(big.Int).SetUint64(uint64(v)) + case uint16: + return new(big.Int).SetUint64(uint64(v)) + case uint32: + return new(big.Int).SetUint64(uint64(v)) + case uint64: + return new(big.Int).SetUint64(v) + default: + return new(big.Int).SetUint64(0) + } +} + +func BigIntIsZero(i *big.Int) bool { + return i.Cmp(big.NewInt(0)) == 0 +} + +func BigIntSub(x, y *big.Int) *big.Int { + return new(big.Int).Sub(x, y) +} + +// AtomicWriteFile writes data to a file atomically using write-and-move pattern +func AtomicWriteFile(filePath string, data []byte) error { + // create temporary file in the same directory as the target file + dir := filepath.Dir(filePath) + tempFile, err := os.CreateTemp(dir, ".tmp_atomic_*") + if err != nil { + return fmt.Errorf("failed to create temporary file: %w", err) + } + tempFilePath := tempFile.Name() + // ensure temporary file is cleaned up if something goes wrong + defer func() { + tempFile.Close() + os.Remove(tempFilePath) + }() + // write data to temporary file + _, err = tempFile.Write(data) + if err != nil { + return fmt.Errorf("failed to write to temporary file: %w", err) + } + // sync to ensure data is written to disk + err = tempFile.Sync() + if err != nil { + return fmt.Errorf("failed to sync temporary file: %w", err) + } + // close temporary file before rename + err = tempFile.Close() + if err != nil { + return fmt.Errorf("failed to close temporary file: %w", err) + } + // atomically move temporary file to final destination + err = os.Rename(tempFilePath, filePath) + if err != nil { + return fmt.Errorf("failed to rename temporary file to final destination: %w", err) + } + return nil +} diff --git a/main b/main new file mode 100755 index 000000000..125bca552 Binary files /dev/null and b/main differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..3aae61647 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8329 @@ +{ + "name": "canopy", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "task-master-ai": "^0.19.0" + } + }, + "node_modules/@ai-sdk/amazon-bedrock": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-2.2.11.tgz", + "integrity": "sha512-tZmJbOhihNfkhDnL4sVyscYiMXadXOoZ8QCqU3NVi7kho6czbNal05QZA+EMv1QL87NJAd0FZwcDIy60tor4SQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "@smithy/eventstream-codec": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "aws4fetch": "^1.0.20" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/anthropic": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-1.2.12.tgz", + "integrity": "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/azure": { + "version": "1.3.24", + "resolved": "https://registry.npmjs.org/@ai-sdk/azure/-/azure-1.3.24.tgz", + "integrity": "sha512-6zOG8mwmd8esSL/L9oYFZSyZWORRTxuG6on9A3RdPe7MRJ607Q6BWsuvul79kecbLf5xQ4bfP7LzXaBizsd8OA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/openai": "1.3.23", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/google": { + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.22.tgz", + "integrity": "sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/google-vertex": { + "version": "2.2.27", + "resolved": "https://registry.npmjs.org/@ai-sdk/google-vertex/-/google-vertex-2.2.27.tgz", + "integrity": "sha512-iDGX/2yrU4OOL1p/ENpfl3MWxuqp9/bE22Z8Ip4DtLCUx6ismUNtrKO357igM1/3jrM6t9C6egCPniHqBsHOJA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/anthropic": "1.2.12", + "@ai-sdk/google": "1.2.22", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "google-auth-library": "^9.15.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/mistral": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/mistral/-/mistral-1.2.8.tgz", + "integrity": "sha512-lv857D9UJqCVxiq2Fcu7mSPTypEHBUqLl1K+lCaP6X/7QAkcaxI36QDONG+tOhGHJOXTsS114u8lrUTaEiGXbg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/openai": { + "version": "1.3.23", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.23.tgz", + "integrity": "sha512-86U7rFp8yacUAOE/Jz8WbGcwMCqWvjK33wk5DXkfnAOEn3mx2r7tNSJdjukQFZbAK97VMXGPPHxF+aEARDXRXQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/openai-compatible": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-0.2.15.tgz", + "integrity": "sha512-868uTi5/gx0OlK8x2OT6G/q/WKATVStM4XEXMMLOo9EQTaoNDtSndhLU+4N4kuxbMS7IFaVSJcMr7mKFwV5vvQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/perplexity": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@ai-sdk/perplexity/-/perplexity-1.1.9.tgz", + "integrity": "sha512-Ytolh/v2XupXbTvjE18EFBrHLoNMH0Ueji3lfSPhCoRUfkwrgZ2D9jlNxvCNCCRiGJG5kfinSHvzrH5vGDklYA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz", + "integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "nanoid": "^3.3.8", + "secure-json-parse": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.12.tgz", + "integrity": "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "2.2.8", + "@ai-sdk/ui-utils": "1.2.11", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/ui-utils": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz", + "integrity": "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@ai-sdk/xai": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@ai-sdk/xai/-/xai-1.2.17.tgz", + "integrity": "sha512-6r7/0t5prXaUC7A0G5rs6JdsRUhtYoK9tuwZ2gbc+oad4YE7rza199Il8/FaS9xAiGplC9SPB6dK1q59IjNkUg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/openai-compatible": "0.2.15", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@anthropic-ai/claude-code": { + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.48.tgz", + "integrity": "sha512-h63VBAZZ6Pl/DlYW2PjbfUeicZ4r9VSl8dymD3d+1lZEHwCPgfMpu3g+30+FDMs79Xqc7qSDm6CRnMApxhbjqw==", + "hasInstallScript": true, + "license": "SEE LICENSE IN README.md", + "optional": true, + "bin": { + "claude": "cli.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.33.5", + "@img/sharp-darwin-x64": "^0.33.5", + "@img/sharp-linux-arm": "^0.33.5", + "@img/sharp-linux-arm64": "^0.33.5", + "@img/sharp-linux-x64": "^0.33.5", + "@img/sharp-win32-x64": "^0.33.5" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.844.0.tgz", + "integrity": "sha512-LwuYN43+IWQ5hOSaaNx6VVrUbLZibaZ01pXNuwdbaJGZOKcCCnev5O7MY0Kud7xatJrf7B9l2GIZW7gmHFi+yQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.844.0", + "@aws-sdk/credential-provider-node": "3.844.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.844.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.844.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.844.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.14", + "@smithy/middleware-retry": "^4.1.15", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.6", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.22", + "@smithy/util-defaults-mode-node": "^4.0.22", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.844.0.tgz", + "integrity": "sha512-FktodSx+pfUfIqMjoNwZ6t1xqq/G3cfT7I4JJ0HKHoIIZdoCHQB52x0OzKDtHDJAnEQPInasdPS8PorZBZtHmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.844.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.844.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.844.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.844.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.14", + "@smithy/middleware-retry": "^4.1.15", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.6", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.22", + "@smithy/util-defaults-mode-node": "^4.0.22", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.844.0.tgz", + "integrity": "sha512-pfpI54bG5Xf2NkqrDBC2REStXlDXNCw/whORhkEs+Tp5exU872D5QKguzjPA6hH+8Pvbq1qgt5zXMbduISTHJw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.7.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.6", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.844.0.tgz", + "integrity": "sha512-LBigff8jHYZbQTRcybiqamZTQpRb63CBiCG9Ce0C1CzmZQ0WUZFmJA5ZbqwUK+BliOEdpl6kQFgsf6sz9ODbZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.844.0.tgz", + "integrity": "sha512-WB94Ox86MqcZ4CnRjKgopzaSuZH4hMP0GqdOxG4s1it1lRWOIPOHOC1dPiM0Zbj1uqITIhbXUQVXyP/uaJeNkw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.844.0.tgz", + "integrity": "sha512-e+efVqfkhpM8zxYeiLNgTUlX+tmtXzVm3bw1A02U9Z9cWBHyQNb8pi90M7QniLoqRURY1B0C2JqkOE61gd4KNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.6", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.844.0.tgz", + "integrity": "sha512-jc5ArGz2HfAx5QPXD+Ep36+QWyCKzl2TG6Vtl87/vljfLhVD0gEHv8fRsqWEp3Rc6hVfKnCjLW5ayR2HYcow9w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.844.0", + "@aws-sdk/credential-provider-env": "3.844.0", + "@aws-sdk/credential-provider-http": "3.844.0", + "@aws-sdk/credential-provider-process": "3.844.0", + "@aws-sdk/credential-provider-sso": "3.844.0", + "@aws-sdk/credential-provider-web-identity": "3.844.0", + "@aws-sdk/nested-clients": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.844.0.tgz", + "integrity": "sha512-pUqB0StTNyW0R03XjTA3wrQZcie/7FJKSXlYHue921ZXuhLOZpzyDkLNfdRsZTcEoYYWVPSmyS+Eu/g5yVsBNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.844.0", + "@aws-sdk/credential-provider-http": "3.844.0", + "@aws-sdk/credential-provider-ini": "3.844.0", + "@aws-sdk/credential-provider-process": "3.844.0", + "@aws-sdk/credential-provider-sso": "3.844.0", + "@aws-sdk/credential-provider-web-identity": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.844.0.tgz", + "integrity": "sha512-VCI8XvIDt2WBfk5Gi/wXKPcWTS3OkAbovB66oKcNQalllH8ESDg4SfLNhchdnN8A5sDGj6tIBJ19nk+dQ6GaqQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.844.0.tgz", + "integrity": "sha512-UNp/uWufGlb5nWa4dpc6uQnDOB/9ysJJFG95ACowNVL9XWfi1LJO7teKrqNkVhq0CzSJS1tCt3FvX4UfM+aN1g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.844.0", + "@aws-sdk/core": "3.844.0", + "@aws-sdk/token-providers": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.844.0.tgz", + "integrity": "sha512-iDmX4pPmatjttIScdspZRagaFnCjpHZIEEwTyKdXxUaU0iAOSXF8ecrCEvutETvImPOC86xdrq+MPacJOnMzUA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.844.0", + "@aws-sdk/nested-clients": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.844.0.tgz", + "integrity": "sha512-amTf3wxwTVNV5jBpN1dT77c5rlch3ooUhBxA+dAnlKLLbc0OlcUrF49Kh69PWBlACahcZDuBh/KPJm2wiIMyYQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.844.0", + "@aws-sdk/core": "3.844.0", + "@aws-sdk/credential-provider-cognito-identity": "3.844.0", + "@aws-sdk/credential-provider-env": "3.844.0", + "@aws-sdk/credential-provider-http": "3.844.0", + "@aws-sdk/credential-provider-ini": "3.844.0", + "@aws-sdk/credential-provider-node": "3.844.0", + "@aws-sdk/credential-provider-process": "3.844.0", + "@aws-sdk/credential-provider-sso": "3.844.0", + "@aws-sdk/credential-provider-web-identity": "3.844.0", + "@aws-sdk/nested-clients": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", + "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", + "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", + "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.844.0.tgz", + "integrity": "sha512-SIbDNUL6ZYXPj5Tk0qEz05sW9kNS1Gl3/wNWEmH+AuUACipkyIeKKWzD6z5433MllETh73vtka/JQF3g7AuZww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.844.0", + "@smithy/core": "^3.7.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.844.0.tgz", + "integrity": "sha512-p2XILWc7AcevUSpBg2VtQrk79eWQC4q2JsCSY7HxKpFLZB4mMOfmiTyYkR1gEA6AttK/wpCOtfz+hi1/+z2V1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.844.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.844.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.844.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.844.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.14", + "@smithy/middleware-retry": "^4.1.15", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.6", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.22", + "@smithy/util-defaults-mode-node": "^4.0.22", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", + "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.844.0.tgz", + "integrity": "sha512-Kh728FEny0fil+LeH8U1offPJCTd/EDh8liBAvLtViLHt2WoX2xC8rk98D38Q5p79aIUhHb3Pf4n9IZfTu/Kog==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.844.0", + "@aws-sdk/nested-clients": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.844.0.tgz", + "integrity": "sha512-1DHh0WTUmxlysz3EereHKtKoxVUG9UC5BsfAw6Bm4/6qDlJiqtY3oa2vebkYN23yltKdfsCK65cwnBRU59mWVg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", + "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.844.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.844.0.tgz", + "integrity": "sha512-0eTpURp9Gxbyyeqr78ogARZMSWS5KUMZuN+XMHxNpQLmn2S+J3g+MAyoklCcwhKXlbdQq2aMULEiy0mqIWytuw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.844.0", + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@google/gemini-cli-core": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@google/gemini-cli-core/-/gemini-cli-core-0.1.11.tgz", + "integrity": "sha512-Cdbstn6xiPDak09Oaoe46Om0Pv2dC1EkFYQwUlw/ooDeLIzeHAYCfHBuP/95fwahsdZhJcJ567yGqvc7IirlaQ==", + "optional": true, + "dependencies": { + "@google/genai": "1.8.0", + "@modelcontextprotocol/sdk": "^1.11.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-logs-otlp-grpc": "^0.52.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.52.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.52.0", + "@opentelemetry/instrumentation-http": "^0.52.0", + "@opentelemetry/sdk-node": "^0.52.0", + "@types/glob": "^8.1.0", + "@types/html-to-text": "^9.0.4", + "ajv": "^8.17.1", + "diff": "^7.0.0", + "dotenv": "^17.1.0", + "gaxios": "^7.1.1", + "glob": "^10.4.5", + "google-auth-library": "^9.11.0", + "html-to-text": "^9.0.5", + "ignore": "^7.0.0", + "micromatch": "^4.0.8", + "open": "^10.1.2", + "shell-quote": "^1.8.3", + "simple-git": "^3.28.0", + "strip-ansi": "^7.1.0", + "undici": "^7.10.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/@google/genai": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.8.0.tgz", + "integrity": "sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.11.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@google/gemini-cli-core/node_modules/dotenv": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@google/genai": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.9.0.tgz", + "integrity": "sha512-w9P93OXKPMs9H1mfAx9+p3zJqQGrWBGdvK/SVc7cLZEXNHr/3+vW2eif7ZShA6wU24rNLn9z9MK2vQFUvNRI2Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.11.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.9.tgz", + "integrity": "sha512-DBJBkzI5Wx4jFaYm221LHvAhpKYkhVS0k9plqHwaHhofGNxvYB7J3Bz8w+bFJ05zaMb0sZNHo4KdmENQFlNTuQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.13.tgz", + "integrity": "sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.14.tgz", + "integrity": "sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.14.tgz", + "integrity": "sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/type": "^3.0.7", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.16.tgz", + "integrity": "sha512-oiDqafWzMtofeJyyGkb1CTPaxUkjIcSxePHHQCfif8t3HV9pHcw1Kgdw3/uGpDvaFfeTluwQtWiqzPVjAqS3zA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.0.tgz", + "integrity": "sha512-opqpHPB1NjAmDISi3uvZOTrjEEU5CWVu/HBkDby8t93+6UxYX0Z7Ps0Ltjm5sZiEbWenjubwUkivAEYQmy9xHw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.16.tgz", + "integrity": "sha512-kMrXAaKGavBEoBYUCgualbwA9jWUx2TjMA46ek+pEKy38+LFpL9QHlTd8PO2kWPUgI/KB+qi02o4y2rwXbzr3Q==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.16.tgz", + "integrity": "sha512-g8BVNBj5Zeb5/Y3cSN+hDUL7CsIFDIuVxb9EPty3lkxBaYpjL5BNRKSYOF9yOLe+JOcKFd+TSVeADQ4iSY7rbg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.6.0.tgz", + "integrity": "sha512-jAhL7tyMxB3Gfwn4HIJ0yuJ5pvcB5maYUcouGcgd/ub79f9MqZ+aVnBtuFf+VC2GTkCBF+R+eo7Vi63w5VZlzw==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.9", + "@inquirer/confirm": "^5.1.13", + "@inquirer/editor": "^4.2.14", + "@inquirer/expand": "^4.0.16", + "@inquirer/input": "^4.2.0", + "@inquirer/number": "^3.0.16", + "@inquirer/password": "^4.0.16", + "@inquirer/rawlist": "^4.1.4", + "@inquirer/search": "^3.0.16", + "@inquirer/select": "^4.2.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.4.tgz", + "integrity": "sha512-5GGvxVpXXMmfZNtvWw4IsHpR7RzqAR624xtkPd1NxxlV5M+pShMqzL4oRddRkg8rVEOK9fKdJp1jjVML2Lr7TQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.16.tgz", + "integrity": "sha512-POCmXo+j97kTGU6aeRjsPyuCpQQfKcMXdeTMw708ZMtWrj5aykZvlUxH4Qgz3+Y1L/cAVZsSpA+UgZCu2GMOMg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.4.tgz", + "integrity": "sha512-unTppUcTjmnbl/q+h8XeQDhAqIOmwWYWNyiiP2e3orXrg6tOaa5DHXja9PChCSbChOsktyKgOieRZFnajzxoBg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT", + "optional": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT", + "optional": true + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", + "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@openrouter/ai-sdk-provider": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-0.4.6.tgz", + "integrity": "sha512-oUa8xtssyUhiKEU/aW662lsZ0HUvIUTRk8vVIF3Ha3KI/DnqX54zmVIuzYnaDpermqhy18CHqblAY4dDt1JW3g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.0.9", + "@ai-sdk/provider-utils": "2.1.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.0.9.tgz", + "integrity": "sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider-utils": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.1.10.tgz", + "integrity": "sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.0.9", + "eventsource-parser": "^3.0.0", + "nanoid": "^3.3.8", + "secure-json-parse": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", + "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz", + "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", + "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.52.1.tgz", + "integrity": "sha512-sXgcp4fsL3zCo96A0LmFIGYOj2LSEDI6wD7nBYRhuDDxeRsk18NQgqRVlCf4VIyTBZzGu1M7yOtdFukQPgII1A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/otlp-grpc-exporter-base": "0.52.1", + "@opentelemetry/otlp-transformer": "0.52.1", + "@opentelemetry/sdk-logs": "0.52.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.52.1.tgz", + "integrity": "sha512-CE0f1IEE1GQj8JWl/BxKvKwx9wBTLR09OpPQHaIs5LGBw3ODu8ek5kcbrHPNsFYh/pWh+pcjbZQoxq3CqvQVnA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.52.1", + "@opentelemetry/otlp-exporter-base": "0.52.1", + "@opentelemetry/otlp-grpc-exporter-base": "0.52.1", + "@opentelemetry/otlp-transformer": "0.52.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-metrics": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.52.1.tgz", + "integrity": "sha512-oAHPOy1sZi58bwqXaucd19F/v7+qE2EuVslQOEeLQT94CDuZJJ4tbWzx8DpYBTrOSzKqqrMtx9+PMxkrcbxOyQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/otlp-exporter-base": "0.52.1", + "@opentelemetry/otlp-transformer": "0.52.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-metrics": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz", + "integrity": "sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/otlp-grpc-exporter-base": "0.52.1", + "@opentelemetry/otlp-transformer": "0.52.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.52.1.tgz", + "integrity": "sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/otlp-exporter-base": "0.52.1", + "@opentelemetry/otlp-transformer": "0.52.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.52.1.tgz", + "integrity": "sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/otlp-exporter-base": "0.52.1", + "@opentelemetry/otlp-transformer": "0.52.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.1.tgz", + "integrity": "sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1", + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz", + "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api-logs": "0.52.1", + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.1.tgz", + "integrity": "sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/instrumentation": "0.52.1", + "@opentelemetry/semantic-conventions": "1.25.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.52.1.tgz", + "integrity": "sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/otlp-transformer": "0.52.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz", + "integrity": "sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/otlp-exporter-base": "0.52.1", + "@opentelemetry/otlp-transformer": "0.52.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.52.1.tgz", + "integrity": "sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api-logs": "0.52.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-logs": "0.52.1", + "@opentelemetry/sdk-metrics": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.25.1.tgz", + "integrity": "sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.25.1.tgz", + "integrity": "sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz", + "integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.52.1.tgz", + "integrity": "sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api-logs": "0.52.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/resources": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz", + "integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/resources": "1.25.1", + "lodash.merge": "^4.6.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.52.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz", + "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api-logs": "0.52.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1", + "@opentelemetry/exporter-trace-otlp-http": "0.52.1", + "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", + "@opentelemetry/exporter-zipkin": "1.25.1", + "@opentelemetry/instrumentation": "0.52.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/sdk-logs": "0.52.1", + "@opentelemetry/sdk-metrics": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1", + "@opentelemetry/sdk-trace-node": "1.25.1", + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz", + "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/core": "1.25.1", + "@opentelemetry/resources": "1.25.1", + "@opentelemetry/semantic-conventions": "1.25.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.25.1.tgz", + "integrity": "sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/context-async-hooks": "1.25.1", + "@opentelemetry/core": "1.25.1", + "@opentelemetry/propagator-b3": "1.25.1", + "@opentelemetry/propagator-jaeger": "1.25.1", + "@opentelemetry/sdk-trace-base": "1.25.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", + "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@oxlint/darwin-arm64": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.6.0.tgz", + "integrity": "sha512-m3wyqBh1TOHjpr/dXeIZY7OoX+MQazb+bMHQdDtwUvefrafUx+5YHRvulYh1sZSQ449nQ3nk3qj5qj535vZRjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint/darwin-x64": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.6.0.tgz", + "integrity": "sha512-75fJfF/9xNypr7cnOYoZBhfmG1yP7ex3pUOeYGakmtZRffO9z1i1quLYhjZsmaDXsAIZ3drMhenYHMmFKS3SRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint/linux-arm64-gnu": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.6.0.tgz", + "integrity": "sha512-YhXGf0FXa72bEt4F7eTVKx5X3zWpbAOPnaA/dZ6/g8tGhw1m9IFjrabVHFjzcx3dQny4MgA59EhyElkDvpUe8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-arm64-musl": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.6.0.tgz", + "integrity": "sha512-T3JDhx8mjGjvh5INsPZJrlKHmZsecgDYvtvussKRdkc1Nnn7WC+jH9sh5qlmYvwzvmetlPVNezAoNvmGO9vtMg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-x64-gnu": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.6.0.tgz", + "integrity": "sha512-Dx7ghtAl8aXBdqofJpi338At6lkeCtTfoinTYQXd9/TEJx+f+zCGNlQO6nJz3ydJBX48FDuOFKkNC+lUlWrd8w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/linux-x64-musl": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.6.0.tgz", + "integrity": "sha512-7KvMGdWmAZtAtg6IjoEJHKxTXdAcrHnUnqfgs0JpXst7trquV2mxBeRZusQXwxpu4HCSomKMvJfsp1qKaqSFDg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint/win32-arm64": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.6.0.tgz", + "integrity": "sha512-iSGC9RwX+dl7o5KFr5aH7Gq3nFbkq/3Gda6mxNPMvNkWrgXdIyiINxpyD8hJu566M+QSv1wEAu934BZotFDyoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/win32-x64": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.6.0.tgz", + "integrity": "sha512-jOj3L/gfLc0IwgOTkZMiZ5c673i/hbAmidlaylT0gE6H18hln9HxPgp5GCf4E4y6mwEJlW8QC5hQi221+9otdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.7.0.tgz", + "integrity": "sha512-7ov8hu/4j0uPZv8b27oeOFtIBtlFmM3ibrPv/Omx1uUdoXvcpJ00U+H/OWWC/keAguLlcqwtyL2/jTlSnApgNQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.4.tgz", + "integrity": "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.0.tgz", + "integrity": "sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.14.tgz", + "integrity": "sha512-+BGLpK5D93gCcSEceaaYhUD/+OCGXM1IDaq/jKUQ+ujB0PTWlWN85noodKw/IPFZhIKFCNEe19PGd/reUMeLSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.7.0", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.15.tgz", + "integrity": "sha512-iKYUJpiyTQ33U2KlOZeUb0GwtzWR3C0soYcKuCnTmJrvt6XwTPQZhMfsjJZNw7PpQ3TU4Ati1qLSrkSJxnnSMQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/smithy-client": "^4.4.6", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.0.tgz", + "integrity": "sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", + "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.6.tgz", + "integrity": "sha512-3wfhywdzB/CFszP6moa5L3lf5/zSfQoH0kvVSdkyK2az5qZet0sn2PAHjcTDiq296Y4RP5yxF7B6S6+3oeBUCQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.7.0", + "@smithy/middleware-endpoint": "^4.1.14", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.22.tgz", + "integrity": "sha512-hjElSW18Wq3fUAWVk6nbk7pGrV7ZT14DL1IUobmqhV3lxcsIenr5FUsDe2jlTVaS8OYBI3x+Og9URv5YcKb5QA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.6", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.22.tgz", + "integrity": "sha512-7B8mfQBtwwr2aNRRmU39k/bsRtv9B6/1mTMrGmmdJFKmLAH+KgIiOuhaqfKOBGh9sZ/VkZxbvm94rI4MMYpFjQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.6", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", + "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.3.tgz", + "integrity": "sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@tokenizer/inflate/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/html-to-text": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz", + "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/node": { + "version": "18.19.118", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.118.tgz", + "integrity": "sha512-hIPK0hSrrcaoAu/gJMzN3QClXE4QdCdFvaenJ0JsjIbExP1JFFVH+RHcBt25c9n8bx5dkIfqKE+uw6BmBns7ug==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "optional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ai": { + "version": "4.3.17", + "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.17.tgz", + "integrity": "sha512-uWqIQ94Nb1GTYtYElGHegJMOzv3r2mCKNFlKrqkft9xrfvIahTI5OdcnD5U9612RFGuUNGmSDTO1/YRNFXobaQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "@ai-sdk/react": "1.2.12", + "@ai-sdk/ui-utils": "1.2.11", + "@opentelemetry/api": "1.9.0", + "jsondiffpatch": "0.6.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/ai-sdk-provider-gemini-cli": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ai-sdk-provider-gemini-cli/-/ai-sdk-provider-gemini-cli-0.0.3.tgz", + "integrity": "sha512-gryNbArgNC2kqWlCsSlheZOCoowYlEfLWZZYac5kwDVG65P00hAaqiUNsBHLVPtDrqtE4rQZDj2zwhBuAwfwYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ai-sdk/provider": "^1.1.3", + "@ai-sdk/provider-utils": "^2.2.8", + "@google/gemini-cli-core": "^0.1.4", + "@google/genai": "^1.7.0", + "google-auth-library": "^9.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws4fetch": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/aws4fetch/-/aws4fetch-1.0.20.tgz", + "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT", + "optional": true + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-highlight/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "optional": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "optional": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT", + "optional": true + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/execa": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastmcp": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-2.2.4.tgz", + "integrity": "sha512-jDO0yZpZGdA809WGszsK2jmC68sklbSmXMpt7NedCb7MV2SCzmCCYnCR59DNtDwhSSZF2HIDKo6pLi3+2PwImg==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "@standard-schema/spec": "^1.0.0", + "execa": "^9.6.0", + "file-type": "^21.0.0", + "fuse.js": "^7.1.0", + "mcp-proxy": "^3.0.3", + "strict-event-emitter-types": "^2.0.0", + "undici": "^7.10.0", + "uri-templates": "^0.2.0", + "xsschema": "0.3.0-beta.3", + "yargs": "^18.0.0", + "zod": "^3.25.56", + "zod-to-json-schema": "^3.24.5" + }, + "bin": { + "fastmcp": "dist/bin/fastmcp.js" + } + }, + "node_modules/fastmcp/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fastmcp/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fastmcp/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/fastmcp/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/fastmcp/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figlet": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.1.tgz", + "integrity": "sha512-kEC3Sme+YvA8Hkibv0NR1oClGcWia0VB2fC1SlMy027cwe795Xx40Xiv/nw/iFAwQLupymWh+uhAAErn/7hwPg==", + "license": "MIT", + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-type": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "optional": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/gaxios": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz", + "integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gcp-metadata/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gpt-tokens": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/gpt-tokens/-/gpt-tokens-1.3.14.tgz", + "integrity": "sha512-cFNErQQYGWRwYmew0wVqhCBZxTvGNr96/9pMwNXqSNu9afxqB5PNHOKHlWtUC/P4UW6Ne2UQHHaO2PaWWLpqWQ==", + "license": "MIT", + "dependencies": { + "decimal.js": "^10.4.3", + "js-tiktoken": "^1.0.15", + "openai-chat-tokens": "^0.2.8" + } + }, + "node_modules/gradient-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-3.0.0.tgz", + "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "tinygradient": "^1.1.5" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gtoken/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gtoken/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz", + "integrity": "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.7.0.tgz", + "integrity": "sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/prompts": "^7.6.0", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "mute-stream": "^2.0.0", + "run-async": "^4.0.4", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "optional": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "optional": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tiktoken": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.20.tgz", + "integrity": "sha512-Xlaqhhs8VfCd6Sh7a1cFkZHQbYTLCwVJJWiHVxBYzLPxW0XsoxBy1hitmjkdIjD3Aon5BXLHFwU5O8WUx6HH+A==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "optional": true + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "license": "MIT", + "dependencies": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "bin": { + "jsondiffpatch": "bin/jsondiffpatch.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mcp-proxy": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-3.3.0.tgz", + "integrity": "sha512-xyFKQEZ64HC7lxScBHjb5fxiPoyJjjkPhwH5hWUT0oL/ttCpMGZDJrYZRGFKVJiLLkrZPAkHnMGkI+WMlyD/cg==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.11.4", + "eventsource": "^4.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "mcp-proxy": "dist/bin/mcp-proxy.js" + } + }, + "node_modules/mcp-proxy/node_modules/eventsource": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.0.0.tgz", + "integrity": "sha512-fvIkb9qZzdMxgZrEQDyll+9oJsyaVvY92I2Re+qK0qEJ+w5s0X3dtz+M0VAPOjP1gtU3iqWyjQ0G3nvd5CLZ2g==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ollama-ai-provider": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ollama-ai-provider/-/ollama-ai-provider-1.2.0.tgz", + "integrity": "sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "^1.0.0", + "@ai-sdk/provider-utils": "^2.0.0", + "partial-json": "0.1.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai-chat-tokens": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/openai-chat-tokens/-/openai-chat-tokens-0.2.8.tgz", + "integrity": "sha512-nW7QdFDIZlAYe6jsCT/VPJ/Lam3/w2DX9oxf/5wHpebBT49KI3TN43PPhYlq1klq2ajzXWKNOLY6U4FNZM7AoA==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.7" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oxlint": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.6.0.tgz", + "integrity": "sha512-jtaD65PqzIa1udvSxxscTKBxYKuZoFXyKGLiU1Qjo1ulq3uv/fQDtoV1yey1FrQZrQjACGPi1Widsy1TucC7Jg==", + "license": "MIT", + "bin": { + "oxc_language_server": "bin/oxc_language_server", + "oxlint": "bin/oxlint" + }, + "engines": { + "node": ">=8.*" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/darwin-arm64": "1.6.0", + "@oxlint/darwin-x64": "1.6.0", + "@oxlint/linux-arm64-gnu": "1.6.0", + "@oxlint/linux-arm64-musl": "1.6.0", + "@oxlint/linux-x64-gnu": "1.6.0", + "@oxlint/linux-x64-musl": "1.6.0", + "@oxlint/win32-arm64": "1.6.0", + "@oxlint/win32-x64": "1.6.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0", + "optional": true + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT", + "optional": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/protobufjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/require-in-the-middle/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/require-in-the-middle/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.4.tgz", + "integrity": "sha512-2cgeRHnV11lSXBEhq7sN7a5UVjTKm9JTb9x8ApIT//16D7QL96AgnNeWSGoB4gIHc0iYw/Ha0Z+waBaCYZVNhg==", + "license": "MIT", + "dependencies": { + "oxlint": "^1.2.0", + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "optional": true, + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", + "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/simple-git/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/simple-git/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "license": "ISC" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", + "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swr": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz", + "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/task-master-ai": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/task-master-ai/-/task-master-ai-0.19.0.tgz", + "integrity": "sha512-dYaXi4lGHLetjBEtZ0iTMmOyxU0fyqfPdojz28l+Zt4bElcB8ANaUCoJS/YVtBmSSRnvqz0QaO/wxN/2kOfkAA==", + "license": "MIT WITH Commons-Clause", + "dependencies": { + "@ai-sdk/amazon-bedrock": "^2.2.9", + "@ai-sdk/anthropic": "^1.2.10", + "@ai-sdk/azure": "^1.3.17", + "@ai-sdk/google": "^1.2.13", + "@ai-sdk/google-vertex": "^2.2.23", + "@ai-sdk/mistral": "^1.2.7", + "@ai-sdk/openai": "^1.3.20", + "@ai-sdk/perplexity": "^1.1.7", + "@ai-sdk/xai": "^1.2.15", + "@anthropic-ai/sdk": "^0.39.0", + "@aws-sdk/credential-providers": "^3.817.0", + "@inquirer/search": "^3.0.15", + "@openrouter/ai-sdk-provider": "^0.4.5", + "ai": "^4.3.10", + "boxen": "^8.0.1", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "commander": "^11.1.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.21.2", + "fastmcp": "^2.2.2", + "figlet": "^1.8.0", + "fuse.js": "^7.1.0", + "gpt-tokens": "^1.3.14", + "gradient-string": "^3.0.0", + "helmet": "^8.1.0", + "inquirer": "^12.5.0", + "jsonc-parser": "^3.3.1", + "jsonwebtoken": "^9.0.2", + "lru-cache": "^10.2.0", + "ollama-ai-provider": "^1.2.0", + "openai": "^4.89.0", + "ora": "^8.2.0", + "uuid": "^11.1.0", + "zod": "^3.23.8" + }, + "bin": { + "task-master": "bin/task-master.js", + "task-master-ai": "mcp-server/server.js", + "task-master-mcp": "mcp-server/server.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-code": "^1.0.25", + "ai-sdk-provider-gemini-cli": "^0.0.3" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinygradient": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", + "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", + "license": "MIT", + "dependencies": { + "@types/tinycolor2": "^1.4.0", + "tinycolor2": "^1.0.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.3.tgz", + "integrity": "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.11.0.tgz", + "integrity": "sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-templates": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uri-templates/-/uri-templates-0.2.0.tgz", + "integrity": "sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==", + "license": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xsschema": { + "version": "0.3.0-beta.3", + "resolved": "https://registry.npmjs.org/xsschema/-/xsschema-0.3.0-beta.3.tgz", + "integrity": "sha512-8fKI0Kqxs7npz3ElebNCeGdS0HDuS2qL3IqHK5O53yCdh419hcr3GQillwN39TNFasHjbMLQ+DjSwpY0NONdnQ==", + "license": "MIT", + "peerDependencies": { + "@valibot/to-json-schema": "^1.0.0", + "arktype": "^2.1.16", + "effect": "^3.14.5", + "sury": "^10.0.0-rc", + "zod": "^3.25.0", + "zod-to-json-schema": "^3.24.5" + }, + "peerDependenciesMeta": { + "@valibot/to-json-schema": { + "optional": true + }, + "arktype": { + "optional": true + }, + "effect": { + "optional": true + }, + "sury": { + "optional": true + }, + "zod": { + "optional": true + }, + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..510c6a136 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "task-master-ai": "^0.19.0" + } +} diff --git a/socket.sh b/socket.sh new file mode 100755 index 000000000..d7f478515 --- /dev/null +++ b/socket.sh @@ -0,0 +1,12 @@ +#!/bin/bash +sudo netstat -np | grep 50002 +sudo ss -K state TIME-WAIT src 127.0.0.1:50002 +sudo ss state time-wait sport = 50002 -K + +#netstat -np | grep 40002 +sudo ss -K state TIME-WAIT src 127.0.0.1:40002 +sudo ss state time-wait sport = 40002 -K + +#netstat -np | grep 60002 +sudo ss -K state TIME-WAIT src 127.0.0.1:60002 +sudo ss state time-wait sport = 60002 -K diff --git a/testdata/tx-lock-order.json b/testdata/tx-lock-order.json new file mode 100644 index 000000000..6aa9fc426 --- /dev/null +++ b/testdata/tx-lock-order.json @@ -0,0 +1,8 @@ +{ + "address": "e7279b6789b3438485145799d53a000f1c6905c2", + "receiveAddress": "e7279b6789b3438485145799d53a000f1c6905c2", + "orderId": 1, + "fee": 123123, + "submit": false, + "password": "123" +}